keystore: consolidate xpub compute mode
What changed, and why it matters
This commit is a straightforward internal code cleanup. It merges two similar functions for computing extended public keys (xpubs) into one function that accepts a mode flag, and moves the mode definition to a more central location. There is no change to security behavior: operations that previously computed xpubs twice still do so, and operations that computed them once still do so. It is a refactoring, not a security fix.
No security action required. Review as normal refactoring if desired.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors keystore xpub derivation. It replaces get_xpub_once() and get_xpub_twice() with a single get_xpub(hal, keypath, Compute) where Compute is an enum with Once and Twice variants. The enum is moved from xpubcache.rs to keystore.rs. All call sites are updated to pass the equivalent mode. The underlying derivation logic and the double-compute integrity check for critical operations are preserved unchanged.
Changed components
src/rust/bitbox02-rust/src/keystore.rssrc/rust/bitbox02-rust/src/xpubcache.rsBitcoin API modulesEthereum API modulesElectrum API moduleInspect captured patch +142 / −93
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs
index 1376498..1a4eb3e 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs
@@ -112,7 +112,7 @@ async fn xpub(
})
.await?
}
- let xpub = keystore::get_xpub_twice(hal, keypath)
+ let xpub = keystore::get_xpub(hal, keypath, keystore::Compute::Twice)
.await
.or(Err(Error::InvalidInput))?
.serialize_str(xpub_type)?;
@@ -153,7 +153,7 @@ pub async fn derive_address_simple(
.or(Err(Error::InvalidInput))?;
Ok(common::Payload::from_simple(
hal,
- &mut crate::xpubcache::XpubCache::new(crate::xpubcache::Compute::Twice),
+ &mut crate::xpubcache::XpubCache::new(crate::keystore::Compute::Twice),
coin_params,
simple_type,
keypath,
@@ -1109,20 +1109,28 @@ mod tests {
root_fingerprint: keystore::root_fingerprint().unwrap(),
keypath: KEYPATH_ACCOUNT_TESTNET.to_vec(),
xpub: Some(
- crate::keystore::get_xpub_once(&mut TestingHal::new(), KEYPATH_ACCOUNT_TESTNET)
- .await
- .unwrap()
- .into(),
+ crate::keystore::get_xpub(
+ &mut TestingHal::new(),
+ KEYPATH_ACCOUNT_TESTNET,
+ crate::keystore::Compute::Once,
+ )
+ .await
+ .unwrap()
+ .into(),
),
};
let our_key_mainnet = pb::KeyOriginInfo {
root_fingerprint: keystore::root_fingerprint().unwrap(),
keypath: KEYPATH_ACCOUNT_MAINNET.to_vec(),
xpub: Some(
- crate::keystore::get_xpub_once(&mut TestingHal::new(), KEYPATH_ACCOUNT_MAINNET)
- .await
- .unwrap()
- .into(),
+ crate::keystore::get_xpub(
+ &mut TestingHal::new(),
+ KEYPATH_ACCOUNT_MAINNET,
+ crate::keystore::Compute::Once,
+ )
+ .await
+ .unwrap()
+ .into(),
),
};
let some_key = pb::KeyOriginInfo {
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/common.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/common.rs
index 92d629a..355d41a 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/common.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/common.rs
@@ -576,7 +576,7 @@ mod tests {
"sudden tenant fault inject concert weather maid people chunk youth stumble grit",
"",
);
- let mut xpub_cache = Bip32XpubCache::new(crate::xpubcache::Compute::Once);
+ let mut xpub_cache = Bip32XpubCache::new(crate::keystore::Compute::Once);
let coin_params = super::super::params::get(pb::BtcCoin::Btc);
// p2wpkh
assert_eq!(
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/multisig.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/multisig.rs
index 089215b..80ae4f7 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/multisig.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/multisig.rs
@@ -262,7 +262,7 @@ pub async fn validate(
return Err(Error::InvalidInput);
}
- let our_xpub = crate::keystore::get_xpub_once(hal, keypath)
+ let our_xpub = crate::keystore::get_xpub(hal, keypath, crate::keystore::Compute::Once)
.await?
.serialize(None)?;
let maybe_our_xpub =
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/policies.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/policies.rs
index 934a3bb..18574a3 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/policies.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/policies.rs
@@ -48,7 +48,7 @@ async fn is_our_key(
xpub: Some(xpub),
..
} if root_fingerprint.as_slice() == our_root_fingerprint => {
- let our_xpub = crate::keystore::get_xpub_once(hal, keypath)
+ let our_xpub = crate::keystore::get_xpub(hal, keypath, crate::keystore::Compute::Once)
.await?
.serialize(None)?;
let maybe_our_xpub = bip32::Xpub::from(xpub).serialize(None)?;
@@ -810,10 +810,13 @@ mod tests {
// Creates a policy for one of our own keys at keypath.
async fn make_our_key(keypath: &[u32]) -> pb::KeyOriginInfo {
- let our_xpub =
- crate::keystore::get_xpub_once(&mut crate::hal::testing::TestingHal::new(), keypath)
- .await
- .unwrap();
+ let our_xpub = crate::keystore::get_xpub(
+ &mut crate::hal::testing::TestingHal::new(),
+ keypath,
+ crate::keystore::Compute::Once,
+ )
+ .await
+ .unwrap();
pb::KeyOriginInfo {
root_fingerprint: crate::keystore::root_fingerprint().unwrap(),
keypath: keypath.to_vec(),
@@ -1858,7 +1861,7 @@ mod tests {
let parsed_policy = parse(&mut hal, &policy, coin).await.unwrap();
const ADDRESS_INDEX: u32 = 5;
- let mut xpub_cache = Bip32XpubCache::new(crate::xpubcache::Compute::Once);
+ let mut xpub_cache = Bip32XpubCache::new(crate::keystore::Compute::Once);
// Internal key results in a key path spend. The internal key is ` @0/<0;1>/*`, so `/0/5`
// selects that one as `0` matches the first multipath index of that key.
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/script_configs.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/script_configs.rs
index a4356fe..8f4e958 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/script_configs.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/script_configs.rs
@@ -116,10 +116,14 @@ mod tests {
root_fingerprint: crate::keystore::root_fingerprint().unwrap(),
keypath: keypath.to_vec(),
xpub: Some(
- crate::keystore::get_xpub_once(&mut mock_hal, keypath)
- .await
- .unwrap()
- .into(),
+ crate::keystore::get_xpub(
+ &mut mock_hal,
+ keypath,
+ crate::keystore::Compute::Once,
+ )
+ .await
+ .unwrap()
+ .into(),
),
},
pb::KeyOriginInfo {
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
index 3f3b3a6..7d71735 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
@@ -11,9 +11,10 @@ use super::script_configs::{ValidatedScriptConfig, ValidatedScriptConfigWithKeyp
use super::{bip143, bip341, common, keypath};
use crate::hal::Ui;
+use crate::keystore::Compute;
use crate::secp256k1::SECP256K1;
use crate::workflow::transaction;
-use crate::xpubcache::{Bip32XpubCache, Compute};
+use crate::xpubcache::Bip32XpubCache;
use alloc::string::String;
use alloc::vec::Vec;
@@ -1875,10 +1876,14 @@ mod tests {
let multisig = pb::btc_script_config::Multisig {
threshold: 1,
xpubs: vec![
- crate::keystore::get_xpub_once(&mut TestingHal::new(), keypath)
- .await
- .unwrap()
- .into(),
+ crate::keystore::get_xpub(
+ &mut TestingHal::new(),
+ keypath,
+ crate::keystore::Compute::Once,
+ )
+ .await
+ .unwrap()
+ .into(),
parse_xpub("xpub6ERxBysTYfQyY4USv6c6J1HNVv9hpZFN9LHVPu47Ac4rK8fLy6NnAeeAHyEsMvG4G66ay5aFZii2VM7wT3KxLKX8Q8keZPd67kRGmrD1WJj").unwrap(),
],
our_xpub_index: 0,
@@ -3283,10 +3288,14 @@ mod tests {
root_fingerprint: crate::keystore::root_fingerprint().unwrap(),
keypath: keypath_account.to_vec(),
xpub: Some(
- crate::keystore::get_xpub_once(&mut mock_hal, keypath_account)
- .await
- .unwrap()
- .into(),
+ crate::keystore::get_xpub(
+ &mut mock_hal,
+ keypath_account,
+ crate::keystore::Compute::Once,
+ )
+ .await
+ .unwrap()
+ .into(),
),
},
pb::KeyOriginInfo {
@@ -3408,10 +3417,14 @@ mod tests {
root_fingerprint: crate::keystore::root_fingerprint().unwrap(),
keypath: keypath_account.to_vec(),
xpub: Some(
- crate::keystore::get_xpub_once(&mut TestingHal::new(), keypath_account)
- .await
- .unwrap()
- .into(),
+ crate::keystore::get_xpub(
+ &mut TestingHal::new(),
+ keypath_account,
+ crate::keystore::Compute::Once,
+ )
+ .await
+ .unwrap()
+ .into(),
),
},
pb::KeyOriginInfo {
@@ -3483,10 +3496,14 @@ mod tests {
root_fingerprint: crate::keystore::root_fingerprint().unwrap(),
keypath: keypath_account.to_vec(),
xpub: Some(
- crate::keystore::get_xpub_once(&mut TestingHal::new(), keypath_account)
- .await
- .unwrap()
- .into(),
+ crate::keystore::get_xpub(
+ &mut TestingHal::new(),
+ keypath_account,
+ crate::keystore::Compute::Once,
+ )
+ .await
+ .unwrap()
+ .into(),
),
},
],
@@ -3593,10 +3610,14 @@ mod tests {
root_fingerprint: crate::keystore::root_fingerprint().unwrap(),
keypath: keypath_account.to_vec(),
xpub: Some(
- crate::keystore::get_xpub_once(&mut TestingHal::new(), keypath_account)
- .await
- .unwrap()
- .into(),
+ crate::keystore::get_xpub(
+ &mut TestingHal::new(),
+ keypath_account,
+ crate::keystore::Compute::Once,
+ )
+ .await
+ .unwrap()
+ .into(),
),
},
pb::KeyOriginInfo {
@@ -3656,10 +3677,14 @@ mod tests {
root_fingerprint: crate::keystore::root_fingerprint().unwrap(),
keypath: keypath_account.to_vec(),
xpub: Some(
- crate::keystore::get_xpub_once(&mut TestingHal::new(), keypath_account)
- .await
- .unwrap()
- .into(),
+ crate::keystore::get_xpub(
+ &mut TestingHal::new(),
+ keypath_account,
+ crate::keystore::Compute::Once,
+ )
+ .await
+ .unwrap()
+ .into(),
),
},
pb::KeyOriginInfo {
@@ -3711,10 +3736,14 @@ mod tests {
root_fingerprint: crate::keystore::root_fingerprint().unwrap(),
keypath: keypath_account.to_vec(),
xpub: Some(
- crate::keystore::get_xpub_once(&mut TestingHal::new(), keypath_account)
- .await
- .unwrap()
- .into(),
+ crate::keystore::get_xpub(
+ &mut TestingHal::new(),
+ keypath_account,
+ crate::keystore::Compute::Once,
+ )
+ .await
+ .unwrap()
+ .into(),
),
},
pb::KeyOriginInfo {
diff --git a/src/rust/bitbox02-rust/src/hww/api/electrum.rs b/src/rust/bitbox02-rust/src/hww/api/electrum.rs
index 536ea58..d78fcbd 100644
--- a/src/rust/bitbox02-rust/src/hww/api/electrum.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/electrum.rs
@@ -28,7 +28,7 @@ pub async fn process(
{
return Err(Error::InvalidInput);
}
- let xpub = keystore::get_xpub_twice(hal, keypath)
+ let xpub = keystore::get_xpub(hal, keypath, keystore::Compute::Twice)
.await
.or(Err(Error::InvalidInput))?
.serialize_str(bip32::XPubType::Xpub)?;
diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum.rs b/src/rust/bitbox02-rust/src/hww/api/ethereum.rs
index 85b6d3a..2df9169 100644
--- a/src/rust/bitbox02-rust/src/hww/api/ethereum.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum.rs
@@ -48,7 +48,7 @@ pub(crate) async fn derive_address(
if !keypath::is_valid_keypath_address(keypath) {
return Err(Error::InvalidInput);
}
- let pubkey = crate::keystore::get_xpub_twice(hal, keypath)
+ let pubkey = crate::keystore::get_xpub(hal, keypath, crate::keystore::Compute::Twice)
.await
.or(Err(Error::InvalidInput))?
.pubkey_uncompressed()?;
diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum/pubrequest.rs b/src/rust/bitbox02-rust/src/hww/api/ethereum/pubrequest.rs
index 1b5e1ef..2a28cbd 100644
--- a/src/rust/bitbox02-rust/src/hww/api/ethereum/pubrequest.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum/pubrequest.rs
@@ -65,7 +65,7 @@ async fn process_xpub(
if !super::keypath::is_valid_keypath_xpub(&request.keypath) {
return Err(Error::InvalidInput);
}
- let xpub = keystore::get_xpub_twice(hal, &request.keypath)
+ let xpub = keystore::get_xpub(hal, &request.keypath, keystore::Compute::Twice)
.await
.or(Err(Error::InvalidInput))?
.serialize_str(bip32::XPubType::Xpub)?;
diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs
index 1da9c65..15f7551 100644
--- a/src/rust/bitbox02-rust/src/keystore.rs
+++ b/src/rust/bitbox02-rust/src/keystore.rs
@@ -652,13 +652,17 @@ pub async fn secp256k1_get_private_key_twice(
}
}
-/// Can be used only if the keystore is unlocked. Returns the derived xpub,
-/// using bip32 derivation. Derivation is done from the xprv master, so hardened
-/// derivation is allowed.
-pub async fn get_xpub_once(
- hal: &mut impl crate::hal::Hal,
- keypath: &[u32],
-) -> Result<bip32::Xpub, ()> {
+#[derive(Copy, Clone)]
+pub enum Compute {
+ /// The xpub is derived once. Use for non-critical operations like signing a transaction,
+ /// where a compute error will lead to an invalid signature only.
+ Once,
+ /// The xpub is derived twice, to mitigate the risk of bitflips or similar compute corruption.
+ /// Used for critical operations, like delivering xpubs to the host.
+ Twice,
+}
+
+async fn derive_xpub(hal: &mut impl crate::hal::Hal, keypath: &[u32]) -> Result<bip32::Xpub, ()> {
let xpriv = get_xprv(hal, keypath).await?;
let xpub = bitcoin::bip32::Xpub::from_priv(SECP256K1, &xpriv.xprv);
Ok(bip32::Xpub::from(xpub))
@@ -667,20 +671,27 @@ pub async fn get_xpub_once(
/// Can be used only if the keystore is unlocked. Returns the derived xpub,
/// using bip32 derivation. Derivation is done from the xprv master, so hardened
/// derivation is allowed.
-pub async fn get_xpub_twice(
+pub async fn get_xpub(
hal: &mut impl crate::hal::Hal,
keypath: &[u32],
+ compute: Compute,
) -> Result<bip32::Xpub, ()> {
- let res1 = get_xpub_once(hal, keypath).await?;
- let res2 = get_xpub_once(hal, keypath).await?;
- if res1 != res2 {
- return Err(());
+ match compute {
+ Compute::Once => derive_xpub(hal, keypath).await,
+ Compute::Twice => {
+ let res1 = derive_xpub(hal, keypath).await?;
+ let res2 = derive_xpub(hal, keypath).await?;
+ if res1 != res2 {
+ return Err(());
+ }
+ Ok(res1)
+ }
}
- Ok(res1)
}
-/// Gets multiple xpubs at once. This is better than multiple calls to `get_xpub_twice()` as it only
-/// uses two secure chip operations in total, instead of two per xpub.
+/// Gets multiple xpubs at once. This is better than multiple calls to
+/// `get_xpub(Compute::Twice)` as it only uses two secure chip operations in total, instead of two
+/// per xpub.
pub async fn get_xpubs_twice(
hal: &mut impl crate::hal::Hal,
keypaths: &[&[u32]],
@@ -1788,7 +1799,7 @@ mod tests {
}
#[async_test::test]
- async fn test_get_xpub_twice() {
+ async fn test_get_xpub() {
let keypath = &[44 + HARDENED, 0 + HARDENED, 0 + HARDENED];
// Also test with unhardened and non-zero elements.
let keypath_5 = &[44 + HARDENED, 1 + HARDENED, 10 + HARDENED, 1, 100];
@@ -1796,7 +1807,11 @@ mod tests {
let mut mock_hal = TestingHal::new();
lock();
- assert!(get_xpub_twice(&mut mock_hal, keypath).await.is_err());
+ assert!(
+ get_xpub(&mut mock_hal, keypath, Compute::Twice)
+ .await
+ .is_err()
+ );
// 24 words
mock_unlocked_using_mnemonic(
@@ -1807,7 +1822,7 @@ mod tests {
mock_hal.securechip.event_counter_reset();
assert_eq!(
- get_xpub_twice(&mut mock_hal, &[])
+ get_xpub(&mut mock_hal, &[], Compute::Twice)
.await
.unwrap()
.serialize_str(bip32::XPubType::Xpub)
@@ -1818,7 +1833,7 @@ mod tests {
assert_eq!(mock_hal.securechip.get_event_counter(), 2);
assert_eq!(
- get_xpub_twice(&mut mock_hal, keypath)
+ get_xpub(&mut mock_hal, keypath, Compute::Twice)
.await
.unwrap()
.serialize_str(bip32::XPubType::Xpub)
@@ -1826,7 +1841,7 @@ mod tests {
"xpub6Cj6NNCGj2CRPHvkuEG1rbW3nrNCAnLjaoTg1P67FCGoahSsbg9WQ7YaMEEP83QDxt2kZ3hTPAPpGdyEZcfAC1C75HfR66UbjpAb39f4PnG",
);
assert_eq!(
- get_xpub_twice(&mut mock_hal, keypath_5)
+ get_xpub(&mut mock_hal, keypath_5, Compute::Twice)
.await
.unwrap()
.serialize_str(bip32::XPubType::Xpub)
@@ -1840,7 +1855,7 @@ mod tests {
"",
);
assert_eq!(
- get_xpub_twice(&mut mock_hal, keypath)
+ get_xpub(&mut mock_hal, keypath, Compute::Twice)
.await
.unwrap()
.serialize_str(bip32::XPubType::Xpub)
@@ -1854,7 +1869,7 @@ mod tests {
"",
);
assert_eq!(
- get_xpub_twice(&mut mock_hal, keypath)
+ get_xpub(&mut mock_hal, keypath, Compute::Twice)
.await
.unwrap()
.serialize_str(bip32::XPubType::Xpub)
@@ -2172,7 +2187,9 @@ mod tests {
let keypath = &[44 + HARDENED, 0 + HARDENED, 0 + HARDENED];
mock_hal.securechip.event_counter_reset();
- let xpub = get_xpub_once(&mut mock_hal, keypath).await.unwrap();
+ let xpub = get_xpub(&mut mock_hal, keypath, Compute::Once)
+ .await
+ .unwrap();
assert_eq!(mock_hal.securechip.get_event_counter(), 1);
assert_eq!(
diff --git a/src/rust/bitbox02-rust/src/xpubcache.rs b/src/rust/bitbox02-rust/src/xpubcache.rs
index d297d84..646b0be 100644
--- a/src/rust/bitbox02-rust/src/xpubcache.rs
+++ b/src/rust/bitbox02-rust/src/xpubcache.rs
@@ -4,16 +4,7 @@ use super::keystore;
use crate::bip32;
use alloc::{boxed::Box, vec::Vec};
-
-#[derive(Copy, Clone)]
-pub enum Compute {
- /// The xpubs are derived once. Use for non-critical operations like signing a transaction,
- /// where a compute error will lead to an invalid signature only.
- Once,
- /// The xpubs are derive twice, to mitigate the risk of bitflips or similar compute corruption.
- /// Used for critical operations, like delivering xpubs to the host.
- Twice,
-}
+use keystore::Compute;
#[allow(async_fn_in_trait)]
pub trait Xpub: Sized {
@@ -139,10 +130,7 @@ impl Xpub for bip32::Xpub {
keypath: &[u32],
compute: Compute,
) -> Result<Self, ()> {
- match compute {
- Compute::Once => keystore::get_xpub_once(hal, keypath).await,
- Compute::Twice => keystore::get_xpub_twice(hal, keypath).await,
- }
+ keystore::get_xpub(hal, keypath, compute).await
}
}
@@ -193,7 +181,7 @@ mod tests {
type MockCache = XpubCache<MockXpub>;
- let mut cache = MockCache::new(crate::xpubcache::Compute::Once);
+ let mut cache = MockCache::new(crate::keystore::Compute::Once);
assert_eq!(
cache
@@ -282,7 +270,7 @@ mod tests {
#[async_test::test]
async fn test_bip32_xpub_cache() {
- let mut cache = Bip32XpubCache::new(crate::xpubcache::Compute::Twice);
+ let mut cache = Bip32XpubCache::new(crate::keystore::Compute::Twice);
cache.add_keypath(&[84 + HARDENED, 0 + HARDENED, 0 + HARDENED, 1]);
cache.add_keypath(&[84 + HARDENED, 0 + HARDENED, 0 + HARDENED]);
Why this scored 15/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.