From df4c0a9956c2853e18c8be14de359ff44ff95cb3 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Wed, 12 Nov 2025 22:35:08 +0100 Subject: [PATCH 1/3] HAL: add initial SecureChip HAL --- src/rust/bitbox02-rust/src/hal.rs | 59 +++++++++++++++++++ src/rust/bitbox02-rust/src/hww/api/restore.rs | 4 +- .../bitbox02-rust/src/hww/api/set_password.rs | 4 +- src/rust/bitbox02-rust/src/keystore.rs | 24 ++++---- 4 files changed, 76 insertions(+), 15 deletions(-) diff --git a/src/rust/bitbox02-rust/src/hal.rs b/src/rust/bitbox02-rust/src/hal.rs index 06f1ece584..886d635755 100644 --- a/src/rust/bitbox02-rust/src/hal.rs +++ b/src/rust/bitbox02-rust/src/hal.rs @@ -38,11 +38,16 @@ pub trait Random { fn random_32_bytes(&mut self) -> Box>; } +pub trait SecureChip { + fn init_new_password(&mut self, password: &str) -> Result<(), bitbox02::securechip::Error>; +} + /// Hardware abstraction layer for BitBox devices. pub trait Hal { fn ui(&mut self) -> &mut impl Ui; fn sd(&mut self) -> &mut impl Sd; fn random(&mut self) -> &mut impl Random; + fn securechip(&mut self) -> &mut impl SecureChip; } pub struct BitBox02Sd; @@ -97,10 +102,19 @@ impl Random for BitBox02Random { } } +pub struct BitBox02SecureChip; + +impl SecureChip for BitBox02SecureChip { + fn init_new_password(&mut self, password: &str) -> Result<(), bitbox02::securechip::Error> { + bitbox02::securechip::init_new_password(password) + } +} + pub struct BitBox02Hal { ui: RealWorkflows, sd: BitBox02Sd, random: BitBox02Random, + securechip: BitBox02SecureChip, } impl BitBox02Hal { @@ -109,6 +123,7 @@ impl BitBox02Hal { ui: crate::workflow::RealWorkflows, sd: BitBox02Sd, random: BitBox02Random, + securechip: BitBox02SecureChip, } } } @@ -123,6 +138,9 @@ impl Hal for BitBox02Hal { fn random(&mut self) -> &mut impl Random { &mut self.random } + fn securechip(&mut self) -> &mut impl SecureChip { + &mut self.securechip + } } #[cfg(feature = "testing")] @@ -223,10 +241,47 @@ pub mod testing { } } + pub struct TestingSecureChip { + // Count how man seceurity events happen. The numbers were obtained by reading the security + // event counter slot (0xE0C5) on a real device. We can use this to assert how many events + // were used in unit tests. The number is relevant due to Optiga's throttling mechanism. + event_counter: u32, + } + + impl TestingSecureChip { + pub fn new() -> Self { + TestingSecureChip { event_counter: 0 } + } + + /// Resets the event counter. + pub fn event_counter_reset(&mut self) { + self.event_counter = 0; + // TODO: remove once all unit tests use the SecureChip HAL. + bitbox02::securechip::fake_event_counter_reset() + } + + /// Retrieves the event counter. + pub fn get_event_counter(&self) -> u32 { + // TODO: remove fake_event_counter() once all unit tests use the SecureChip HAL. + bitbox02::securechip::fake_event_counter() + self.event_counter + } + } + + impl super::SecureChip for TestingSecureChip { + fn init_new_password( + &mut self, + _password: &str, + ) -> Result<(), bitbox02::securechip::Error> { + self.event_counter += 1; + Ok(()) + } + } + pub struct TestingHal<'a> { pub ui: crate::workflow::testing::TestingWorkflows<'a>, pub sd: TestingSd, pub random: TestingRandom, + pub securechip: TestingSecureChip, } impl TestingHal<'_> { @@ -235,6 +290,7 @@ pub mod testing { ui: crate::workflow::testing::TestingWorkflows::new(), sd: TestingSd::new(), random: TestingRandom::new(), + securechip: TestingSecureChip::new(), } } } @@ -249,6 +305,9 @@ pub mod testing { fn random(&mut self) -> &mut impl super::Random { &mut self.random } + fn securechip(&mut self) -> &mut impl super::SecureChip { + &mut self.securechip + } } #[cfg(test)] diff --git a/src/rust/bitbox02-rust/src/hww/api/restore.rs b/src/rust/bitbox02-rust/src/hww/api/restore.rs index ff6199156d..68945fdfa6 100644 --- a/src/rust/bitbox02-rust/src/hww/api/restore.rs +++ b/src/rust/bitbox02-rust/src/hww/api/restore.rs @@ -189,7 +189,7 @@ mod tests { Ok("password".into()) })); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!( block_on(from_mnemonic( &mut mock_hal, @@ -200,7 +200,7 @@ mod tests { )), Ok(Response::Success(pb::Success {})) ); - assert_eq!(bitbox02::securechip::fake_event_counter(), 8); + assert_eq!(mock_hal.securechip.get_event_counter(), 8); drop(mock_hal); // to remove mutable borrow of counter assert_eq!(counter, 2); assert!(!crate::keystore::is_locked()); diff --git a/src/rust/bitbox02-rust/src/hww/api/set_password.rs b/src/rust/bitbox02-rust/src/hww/api/set_password.rs index 939829196a..332c0845a8 100644 --- a/src/rust/bitbox02-rust/src/hww/api/set_password.rs +++ b/src/rust/bitbox02-rust/src/hww/api/set_password.rs @@ -70,7 +70,7 @@ mod tests { Ok("password".into()) })); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!( block_on(process( &mut mock_hal, @@ -80,7 +80,7 @@ mod tests { )), Ok(Response::Success(pb::Success {})) ); - assert_eq!(bitbox02::securechip::fake_event_counter(), 9); + assert_eq!(mock_hal.securechip.get_event_counter(), 9); drop(mock_hal); // to remove mutable borrow of counter assert_eq!(counter, 2); assert!(!keystore::is_locked()); diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs index 4665ae720d..3402d8b6a4 100644 --- a/src/rust/bitbox02-rust/src/keystore.rs +++ b/src/rust/bitbox02-rust/src/keystore.rs @@ -19,7 +19,7 @@ use alloc::string::String; use alloc::vec::Vec; use crate::bip32; -use crate::hal::Random; +use crate::hal::{Random, SecureChip}; pub use bitbox02::keystore::SignResult; use bitbox02::{keystore, securechip}; @@ -211,7 +211,7 @@ pub fn encrypt_and_store_seed( bitbox02::usb_processing::timeout_reset(LONG_TIMEOUT); - securechip::init_new_password(password)?; + hal.securechip().init_new_password(password)?; let secret = securechip::stretch_password(password)?; let iv_rand = hal.random().random_32_bytes(); @@ -1510,9 +1510,11 @@ mod tests { lock(); let seed = &seed[..test.seed_len]; + let mut mock_hal = crate::hal::testing::TestingHal::new(); + assert!( block_on(unlock_bip39( - &mut crate::hal::testing::TestingRandom::new(), + &mut mock_hal.random, seed, test.mnemonic_passphrase, async || {} @@ -1520,23 +1522,23 @@ mod tests { .is_err() ); - bitbox02::securechip::fake_event_counter_reset(); - assert!(encrypt_and_store_seed(&mut TestingHal::new(), seed, "foo").is_ok()); - assert_eq!(bitbox02::securechip::fake_event_counter(), 7); + mock_hal.securechip.event_counter_reset(); + assert!(encrypt_and_store_seed(&mut mock_hal, seed, "foo").is_ok()); + assert_eq!(mock_hal.securechip.get_event_counter(), 7); assert!(is_locked()); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert!( block_on(unlock_bip39( - &mut crate::hal::testing::TestingRandom::new(), + &mut mock_hal.random, seed, test.mnemonic_passphrase, async || {} )) .is_ok() ); - assert_eq!(bitbox02::securechip::fake_event_counter(), 1); + assert_eq!(mock_hal.securechip.get_event_counter(), 1); assert!(!is_locked()); assert_eq!( @@ -1545,9 +1547,9 @@ mod tests { ); let keypath = &[44 + HARDENED, 0 + HARDENED, 0 + HARDENED]; - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); let xpub = get_xpub_once(keypath).unwrap(); - assert_eq!(bitbox02::securechip::fake_event_counter(), 1); + assert_eq!(mock_hal.securechip.get_event_counter(), 1); assert_eq!( xpub.serialize_str(crate::bip32::XPubType::Xpub).unwrap(), From 3ba824810e29416889f3f488a4a0081b3d6b1ecd Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Wed, 12 Nov 2025 22:42:08 +0100 Subject: [PATCH 2/3] HAL: use SecureChip event counter Proactivly replace calls wherever we have a HAL instance, so these tests will not fail later when the code starts using the SecureChip HAL. --- src/rust/bitbox02-rust/src/hww/api/backup.rs | 8 ++++---- src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs | 12 ++++++------ src/rust/bitbox02-rust/src/keystore.rs | 8 ++++---- src/rust/bitbox02-rust/src/workflow/unlock.rs | 8 ++++---- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/rust/bitbox02-rust/src/hww/api/backup.rs b/src/rust/bitbox02-rust/src/hww/api/backup.rs index 1e84b06dcd..a115154b2c 100644 --- a/src/rust/bitbox02-rust/src/hww/api/backup.rs +++ b/src/rust/bitbox02-rust/src/hww/api/backup.rs @@ -187,7 +187,7 @@ mod tests { let mut mock_hal = TestingHal::new(); mock_hal.sd.inserted = Some(true); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!( block_on(create( &mut mock_hal, @@ -198,7 +198,7 @@ mod tests { )), Ok(Response::Success(pb::Success {})) ); - assert_eq!(bitbox02::securechip::fake_event_counter(), 1); + assert_eq!(mock_hal.securechip.get_event_counter(), 1); assert_eq!(EXPECTED_TIMESTMAP, bitbox02::memory::get_seed_birthdate()); assert_eq!( mock_hal.ui.screens, @@ -246,7 +246,7 @@ mod tests { password_entered = true; Ok("password".into()) })); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!( block_on(create( &mut mock_hal, @@ -257,7 +257,7 @@ mod tests { )), Ok(Response::Success(pb::Success {})) ); - assert_eq!(bitbox02::securechip::fake_event_counter(), 5); + assert_eq!(mock_hal.securechip.get_event_counter(), 5); assert_eq!( mock_hal.ui.screens, vec![ diff --git a/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs b/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs index 5648fb1c57..2fe7b34be3 100644 --- a/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs +++ b/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs @@ -97,13 +97,13 @@ mod tests { panic!("unexpected call to enter password") })); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!( block_on(process(&mut mock_hal)), Ok(Response::Success(pb::Success {})) ); // 1 operation for one copy_seed() to get the seed to display it. - assert_eq!(bitbox02::securechip::fake_event_counter(), 1); + assert_eq!(mock_hal.securechip.get_event_counter(), 1); assert_eq!( mock_hal.ui.screens, @@ -151,12 +151,12 @@ mod tests { Ok("password".into()) })); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!( block_on(process(&mut mock_hal)), Ok(Response::Success(pb::Success {})) ); - assert_eq!(bitbox02::securechip::fake_event_counter(), 5); + assert_eq!(mock_hal.securechip.get_event_counter(), 5); assert_eq!( mock_hal.ui.screens, @@ -206,9 +206,9 @@ mod tests { .ui .set_enter_string(Box::new(|_params| Ok("wrong password".into()))); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!(block_on(process(&mut mock_hal)), Err(Error::Generic)); - assert_eq!(bitbox02::securechip::fake_event_counter(), 5); + assert_eq!(mock_hal.securechip.get_event_counter(), 5); assert_eq!( mock_hal.ui.screens, diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs index 3402d8b6a4..113f1e105e 100644 --- a/src/rust/bitbox02-rust/src/keystore.rs +++ b/src/rust/bitbox02-rust/src/keystore.rs @@ -892,17 +892,17 @@ mod tests { )); // First call: unlock. The first one does a seed rentention (1 securechip event). - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!(unlock(&mut mock_hal, "password").unwrap().as_slice(), seed); - assert_eq!(bitbox02::securechip::fake_event_counter(), 6); + assert_eq!(mock_hal.securechip.get_event_counter(), 6); // Loop to check that unlocking works while unlocked. for _ in 0..2 { // Further calls perform a password check.The password check does not do the retention // so it ends up needing one secure chip operation less. - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!(unlock(&mut mock_hal, "password").unwrap().as_slice(), seed); - assert_eq!(bitbox02::securechip::fake_event_counter(), 5); + assert_eq!(mock_hal.securechip.get_event_counter(), 5); } // Also check that the retained seed was encrypted with the expected encryption key. diff --git a/src/rust/bitbox02-rust/src/workflow/unlock.rs b/src/rust/bitbox02-rust/src/workflow/unlock.rs index 021b0f7720..934799a96a 100644 --- a/src/rust/bitbox02-rust/src/workflow/unlock.rs +++ b/src/rust/bitbox02-rust/src/workflow/unlock.rs @@ -218,10 +218,10 @@ mod tests { password_entered = true; Ok("password".into()) })); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!(block_on(unlock(&mut mock_hal)), Ok(())); // 6 for keystore unlock, 1 for keystore bip39 unlock. - assert_eq!(bitbox02::securechip::fake_event_counter(), 7); + assert_eq!(mock_hal.securechip.get_event_counter(), 7); assert!(!crate::keystore::is_locked()); @@ -261,7 +261,7 @@ mod tests { Ok("wrong password".into()) })); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert!(matches!( block_on(unlock_keystore( &mut mock_hal, @@ -270,7 +270,7 @@ mod tests { )), Err(UnlockError::IncorrectPassword), )); - assert_eq!(bitbox02::securechip::fake_event_counter(), 5); + assert_eq!(mock_hal.securechip.get_event_counter(), 5); // Checks that the device is locked. assert!(crate::keystore::copy_seed().is_err()); From f5389883c4c52a278df4361c9aeb4711ec388527 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Wed, 12 Nov 2025 22:53:29 +0100 Subject: [PATCH 3/3] HAL: add stretch_password to SecureChip HAL --- src/rust/bitbox02-rust/src/hal.rs | 26 ++++++++++++++++++++++++++ src/rust/bitbox02-rust/src/keystore.rs | 12 ++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/rust/bitbox02-rust/src/hal.rs b/src/rust/bitbox02-rust/src/hal.rs index 886d635755..92245281d1 100644 --- a/src/rust/bitbox02-rust/src/hal.rs +++ b/src/rust/bitbox02-rust/src/hal.rs @@ -40,6 +40,10 @@ pub trait Random { pub trait SecureChip { fn init_new_password(&mut self, password: &str) -> Result<(), bitbox02::securechip::Error>; + fn stretch_password( + &mut self, + password: &str, + ) -> Result>, bitbox02::securechip::Error>; } /// Hardware abstraction layer for BitBox devices. @@ -108,6 +112,13 @@ impl SecureChip for BitBox02SecureChip { fn init_new_password(&mut self, password: &str) -> Result<(), bitbox02::securechip::Error> { bitbox02::securechip::init_new_password(password) } + + fn stretch_password( + &mut self, + password: &str, + ) -> Result>, bitbox02::securechip::Error> { + bitbox02::securechip::stretch_password(password) + } } pub struct BitBox02Hal { @@ -275,6 +286,21 @@ pub mod testing { self.event_counter += 1; Ok(()) } + + fn stretch_password( + &mut self, + password: &str, + ) -> Result>, bitbox02::securechip::Error> { + self.event_counter += 5; + + use bitcoin::hashes::{HashEngine, Hmac, HmacEngine, sha256}; + let mut engine = HmacEngine::::new(b"unit-test"); + engine.input(password.as_bytes()); + let hmac_result: Hmac = Hmac::from_engine(engine); + Ok(zeroize::Zeroizing::new( + hmac_result.to_byte_array().to_vec(), + )) + } } pub struct TestingHal<'a> { diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs index 113f1e105e..c1a651c646 100644 --- a/src/rust/bitbox02-rust/src/keystore.rs +++ b/src/rust/bitbox02-rust/src/keystore.rs @@ -213,7 +213,8 @@ pub fn encrypt_and_store_seed( hal.securechip().init_new_password(password)?; - let secret = securechip::stretch_password(password)?; + let secret = hal.securechip().stretch_password(password)?; + let iv_rand = hal.random().random_32_bytes(); let iv: &[u8; 16] = iv_rand.first_chunk::<16>().unwrap(); let encrypted = bitbox_aes::encrypt_with_hmac(iv, &secret, seed); @@ -243,13 +244,16 @@ fn check_retained_seed(seed: &[u8]) -> Result<(), ()> { Ok(()) } -fn get_and_decrypt_seed(password: &str) -> Result>, Error> { +fn get_and_decrypt_seed( + hal: &mut impl crate::hal::Hal, + password: &str, +) -> Result>, Error> { let encrypted = bitbox02::memory::get_encrypted_seed_and_hmac().map_err(|_| Error::Memory)?; // Our Optiga securechip implementation fails password stretching if the password is // wrong, so it already returns an error here. The ATECC stretches the password without checking // if the password is correct, and we determine if it is correct in the seed decryption // step below. - let secret = securechip::stretch_password(password)?; + let secret = hal.securechip().stretch_password(password)?; let seed = match bitbox_aes::decrypt_with_hmac(&secret, &encrypted) { Ok(seed) => seed, Err(()) => return Err(Error::IncorrectPassword), @@ -279,7 +283,7 @@ pub fn unlock( } bitbox02::usb_processing::timeout_reset(LONG_TIMEOUT); bitbox02::memory::smarteeprom_increment_unlock_attempts(); - let seed = match get_and_decrypt_seed(password) { + let seed = match get_and_decrypt_seed(hal, password) { Ok(seed) => seed, err @ Err(_) => { if get_remaining_unlock_attempts() == 0 {