payment_request: derive validation addresses once
What changed, and why it matters
This commit is a small optimization in the BitBox02 hardware wallet firmware. It changes how many times the device derives a cryptocurrency address when validating a payment request. Previously, addresses were derived twice (a safety check called 'compute twice'); now they are derived once during payment-request validation, while other operations still derive them twice. This is a performance/counter fix, not a security vulnerability fix. There is no evidence in the commit that it addresses an exploitable bug.
No security action required. Treat as a normal code-quality/performance change. If reviewing for a security release, verify separately that reducing derivation to Compute::Once in payment-request validation does not weaken any anti-fault-injection guarantees the vendor intends to maintain.
Security signals we found
Change reduces secure-chip event counter usage from Twice to Once in payment-request validation
No validation, keypath, or authorization logic is modified
No mention of vulnerability, CVE, bug bounty, or security advisory in commit message or diff
Refactor only affects internal compute parameter plumbing
Evidence from the diff
The patch refactors derive_address_simple() and derive_address() to accept a compute: crate::keystore::Compute parameter instead of hardcoding Compute::Twice. Callers in bitcoin.rs, signmsg.rs, and pubrequest.rs continue passing Compute::Twice. Payment-request validation in payment_request.rs now passes Compute::Once for both Bitcoin and Ethereum address derivations. Unit-test expectations for the secure-chip event counter are reduced from 2 to 1 accordingly. The change reduces redundant derivation work during payment-request validation but does not alter validation logic or keypath checks.
Changed components
src/rust/bitbox02-rust/src/hww/api/bitcoin.rssrc/rust/bitbox02-rust/src/hww/api/bitcoin/signmsg.rssrc/rust/bitbox02-rust/src/hww/api/ethereum.rssrc/rust/bitbox02-rust/src/hww/api/ethereum/pubrequest.rssrc/rust/bitbox02-rust/src/hww/api/payment_request.rsInspect captured patch +57 / −21
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs
index 1a4eb3e..2d2aaa3 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs
@@ -141,6 +141,7 @@ pub async fn derive_address_simple(
coin: BtcCoin,
simple_type: SimpleType,
keypath: &[u32],
+ compute: crate::keystore::Compute,
) -> Result<String, Error> {
let coin_params = params::get(coin);
keypath::validate_address_simple(
@@ -153,7 +154,7 @@ pub async fn derive_address_simple(
.or(Err(Error::InvalidInput))?;
Ok(common::Payload::from_simple(
hal,
- &mut crate::xpubcache::XpubCache::new(crate::keystore::Compute::Twice),
+ &mut crate::xpubcache::XpubCache::new(compute),
coin_params,
simple_type,
keypath,
@@ -170,7 +171,14 @@ async fn address_simple(
keypath: &[u32],
display: bool,
) -> Result<Response, Error> {
- let address = derive_address_simple(hal, coin, simple_type, keypath).await?;
+ let address = derive_address_simple(
+ hal,
+ coin,
+ simple_type,
+ keypath,
+ crate::keystore::Compute::Twice,
+ )
+ .await?;
if display {
let address_formatted = util::strings::format_address(&address);
let confirm_params = ConfirmParams {
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signmsg.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signmsg.rs
index 3d78db8..3245b47 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signmsg.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signmsg.rs
@@ -51,7 +51,14 @@ pub async fn process(
}
// Keypath and script_config are validated in address_simple().
- let address = super::derive_address_simple(hal, coin, simple_type, keypath).await?;
+ let address = super::derive_address_simple(
+ hal,
+ coin,
+ simple_type,
+ keypath,
+ crate::keystore::Compute::Twice,
+ )
+ .await?;
let address_formatted = util::strings::format_address(&address);
let basic_info = format!("Coin: {}", super::params::get(coin).name);
diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum.rs b/src/rust/bitbox02-rust/src/hww/api/ethereum.rs
index 2df9169..98dfdbf 100644
--- a/src/rust/bitbox02-rust/src/hww/api/ethereum.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum.rs
@@ -44,14 +44,15 @@ fn truncating_hex_preview_byte_cap(prefix_len: usize, data_length: usize) -> usi
pub(crate) async fn derive_address(
hal: &mut impl crate::hal::Hal,
keypath: &[u32],
+ compute: crate::keystore::Compute,
) -> Result<alloc::string::String, Error> {
if !keypath::is_valid_keypath_address(keypath) {
return Err(Error::InvalidInput);
}
- let pubkey = crate::keystore::get_xpub(hal, keypath, crate::keystore::Compute::Twice)
+ let xpub = crate::keystore::get_xpub(hal, keypath, compute)
.await
- .or(Err(Error::InvalidInput))?
- .pubkey_uncompressed()?;
+ .or(Err(Error::InvalidInput))?;
+ let pubkey = xpub.pubkey_uncompressed().or(Err(Error::InvalidInput))?;
Ok(address::from_pubkey(&pubkey))
}
@@ -126,7 +127,9 @@ mod tests {
// Standard Ethereum keypath
let keypath = vec![44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0];
- let address = derive_address(&mut hal, &keypath).await.unwrap();
+ let address = derive_address(&mut hal, &keypath, crate::keystore::Compute::Twice)
+ .await
+ .unwrap();
// This is the expected address for the mock keystore seed with this keypath
assert_eq!(address, "0x773A77b9D32589be03f9132AF759e294f7851be9");
@@ -138,7 +141,7 @@ mod tests {
// Invalid keypath (too short)
let keypath = vec![44 + HARDENED, 60 + HARDENED];
- let result = derive_address(&mut hal, &keypath).await;
+ let result = derive_address(&mut hal, &keypath, crate::keystore::Compute::Twice).await;
assert!(matches!(result, Err(Error::InvalidInput)));
}
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 2a28cbd..8338e9d 100644
--- a/src/rust/bitbox02-rust/src/hww/api/ethereum/pubrequest.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum/pubrequest.rs
@@ -30,7 +30,8 @@ async fn process_address(
Some(erc20_params::get(params.chain_id, address).ok_or(Error::InvalidInput)?)
};
- let address = super::derive_address(hal, &request.keypath).await?;
+ let address =
+ super::derive_address(hal, &request.keypath, crate::keystore::Compute::Twice).await?;
if request.display {
let address_display = super::address::format_display_address(&address);
diff --git a/src/rust/bitbox02-rust/src/hww/api/payment_request.rs b/src/rust/bitbox02-rust/src/hww/api/payment_request.rs
index 109b43b..9470e84 100644
--- a/src/rust/bitbox02-rust/src/hww/api/payment_request.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/payment_request.rs
@@ -431,9 +431,13 @@ async fn validate_common(
if _eth.keypath.get(1) != Some(&expected_bip44) {
return Err(ValidationError::Other);
}
- let derived_address = super::ethereum::derive_address(hal, &_eth.keypath)
- .await
- .map_err(|_| ValidationError::Other)?;
+ let derived_address = super::ethereum::derive_address(
+ hal,
+ &_eth.keypath,
+ crate::keystore::Compute::Once,
+ )
+ .await
+ .map_err(|_| ValidationError::Other)?;
if derived_address != coin_purchase_memo.address {
return Err(ValidationError::AddressMismatch);
}
@@ -471,6 +475,7 @@ async fn validate_common(
destination_coin,
simple_type,
&script_config.keypath,
+ crate::keystore::Compute::Once,
)
.await
.map_err(|_| ValidationError::Other)?;
@@ -813,7 +818,7 @@ mod tests {
.await
.is_ok()
);
- assert_eq!(mock_hal.securechip.get_event_counter(), 2);
+ assert_eq!(mock_hal.securechip.get_event_counter(), 1);
}
#[cfg(feature = "app-litecoin")]
@@ -831,6 +836,7 @@ mod tests {
pb::BtcCoin::Btc,
pb::btc_script_config::SimpleType::P2wpkh,
&source_keypath,
+ crate::keystore::Compute::Once,
)
.await
.unwrap();
@@ -847,6 +853,7 @@ mod tests {
pb::BtcCoin::Ltc,
pb::btc_script_config::SimpleType::P2wpkh,
&destination_keypath,
+ crate::keystore::Compute::Once,
)
.await
.unwrap();
@@ -888,7 +895,7 @@ mod tests {
.await
.is_ok()
);
- assert_eq!(mock_hal.securechip.get_event_counter(), 2);
+ assert_eq!(mock_hal.securechip.get_event_counter(), 1);
}
#[cfg(feature = "app-litecoin")]
@@ -906,6 +913,7 @@ mod tests {
pb::BtcCoin::Ltc,
pb::btc_script_config::SimpleType::P2wpkh,
&source_keypath,
+ crate::keystore::Compute::Once,
)
.await
.unwrap();
@@ -922,6 +930,7 @@ mod tests {
pb::BtcCoin::Btc,
pb::btc_script_config::SimpleType::P2wpkh,
&destination_keypath,
+ crate::keystore::Compute::Once,
)
.await
.unwrap();
@@ -963,7 +972,7 @@ mod tests {
.await
.is_ok()
);
- assert_eq!(mock_hal.securechip.get_event_counter(), 2);
+ assert_eq!(mock_hal.securechip.get_event_counter(), 1);
}
// Unhappy cases:
@@ -978,9 +987,13 @@ mod tests {
0,
0,
];
- let destination_address = ethereum::derive_address(&mut mock_hal, destination_keypath)
- .await
- .unwrap();
+ let destination_address = ethereum::derive_address(
+ &mut mock_hal,
+ destination_keypath,
+ crate::keystore::Compute::Once,
+ )
+ .await
+ .unwrap();
let mut payment_request = pb::BtcPaymentRequestRequest {
recipient_name: "Test Merchant".into(),
memos: vec![make_coin_purchase_memo(
@@ -1026,9 +1039,13 @@ mod tests {
0,
0,
];
- let destination_address = ethereum::derive_address(&mut mock_hal, destination_keypath)
- .await
- .unwrap();
+ let destination_address = ethereum::derive_address(
+ &mut mock_hal,
+ destination_keypath,
+ crate::keystore::Compute::Once,
+ )
+ .await
+ .unwrap();
let mut payment_request = pb::BtcPaymentRequestRequest {
recipient_name: "Test Merchant".into(),
memos: vec![make_coin_purchase_memo(
Why this scored 18/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.