Skip to content

Commit e3b21df

Browse files
committed
keystore: reduce secure chip operations by precomputing fingerprint
The root fingerprint API call, calling `bitbox02_rust::keystore::root_fingerprint()`, used two securechip operations. Using too many operations too quickly in Optiga leads to throttling, and the BitBoxApp fetches the root fingerprint every time the BitBox is unlocked. We can get away with not using hte securechip at all to get the root fingerprint, by computing and storing it during unlock. The global static mut could have lived in keystore.c with the other static muts there, but adding more C code and Rust wrappers seemed wrong. For now it lives in bitbox02::keystore, and would move over to bitbox02_rust::keystore when the unlocking functions are migrated to Rust.
1 parent 99d8eb3 commit e3b21df

File tree

8 files changed

+130
-15
lines changed

8 files changed

+130
-15
lines changed

src/keystore.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ keystore_error_t keystore_unlock(
448448
return result;
449449
}
450450

451-
bool keystore_unlock_bip39(const char* mnemonic_passphrase)
451+
bool keystore_unlock_bip39(const char* mnemonic_passphrase, uint8_t* root_fingerprint_out)
452452
{
453453
if (!_is_unlocked_device) {
454454
return false;
@@ -467,7 +467,8 @@ bool keystore_unlock_bip39(const char* mnemonic_passphrase)
467467
rust_derive_bip39_seed(
468468
rust_util_bytes(seed, seed_length),
469469
mnemonic_passphrase,
470-
rust_util_bytes_mut(bip39_seed, sizeof(bip39_seed)));
470+
rust_util_bytes_mut(bip39_seed, sizeof(bip39_seed)),
471+
rust_util_bytes_mut(root_fingerprint_out, 4));
471472

472473
if (!_retain_bip39_seed(bip39_seed)) {
473474
return false;

src/keystore.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,13 @@ keystore_unlock(const char* password, uint8_t* remaining_attempts_out, int* secu
106106
/** Unlocks the bip39 seed.
107107
* @param[in] mnemonic_passphrase bip39 passphrase used in the derivation. Use the
108108
* empty string if no passphrase is needed or provided.
109+
* @param[out] root_fingerprint_out must be 4 bytes long and will contain the root fingerprint of
110+
* the wallet.
109111
* @return returns false if there was a critital memory error, otherwise true.
110112
*/
111-
USE_RESULT bool keystore_unlock_bip39(const char* mnemonic_passphrase);
113+
USE_RESULT bool keystore_unlock_bip39(
114+
const char* mnemonic_passphrase,
115+
uint8_t* root_fingerprint_out);
112116

113117
/**
114118
* Locks the keystore (resets to state before `keystore_unlock()`).

src/rust/bitbox02-rust/src/bip39.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,30 @@ pub fn get_word(idx: u16) -> Result<zeroize::Zeroizing<String>, ()> {
3232
/// The passphrase must be not NULL and null-terminated.
3333
///
3434
/// `seed` must be 16, 24 or 32 bytes long.
35-
/// `out` must be exactly 64 bytes long.
35+
/// `bip39_seed_out` must be exactly 64 bytes long.
36+
/// `root_fingerprint_out` must be exactly 4 bytes long.
3637
#[unsafe(no_mangle)]
3738
pub unsafe extern "C" fn rust_derive_bip39_seed(
3839
seed: util::bytes::Bytes,
3940
passphrase: *const core::ffi::c_char,
40-
mut out: util::bytes::BytesMut,
41+
mut bip39_seed_out: util::bytes::BytesMut,
42+
mut root_fingerprint_out: util::bytes::BytesMut,
4143
) {
4244
let mnemonic =
4345
bip39::Mnemonic::from_entropy_in(bip39::Language::English, seed.as_ref()).unwrap();
4446
let passphrase = unsafe { core::ffi::CStr::from_ptr(passphrase) };
45-
let bip39_seed =
47+
let bip39_seed: zeroize::Zeroizing<[u8; 64]> =
4648
zeroize::Zeroizing::new(mnemonic.to_seed_normalized(passphrase.to_str().unwrap()));
47-
out.as_mut().clone_from_slice(&bip39_seed[..]);
49+
bip39_seed_out.as_mut().clone_from_slice(&bip39_seed[..]);
50+
51+
let root_fingerprint: [u8; 4] =
52+
bitcoin::bip32::Xpriv::new_master(bitcoin::NetworkKind::Main, bip39_seed.as_ref())
53+
.unwrap()
54+
.fingerprint(crate::secp256k1::SECP256K1)
55+
.to_bytes();
56+
root_fingerprint_out
57+
.as_mut()
58+
.clone_from_slice(&root_fingerprint);
4859
}
4960

5061
#[unsafe(no_mangle)]
@@ -73,6 +84,7 @@ mod tests {
7384
seed: &'static str,
7485
passphrase: &'static core::ffi::CStr,
7586
expected_bip39_seed: &'static str,
87+
expected_root_fingerprint: &'static str,
7688
}
7789

7890
let tests = &[
@@ -81,47 +93,62 @@ mod tests {
8193
seed: "fb5cf00d5ea61059fa066e25a6be9544",
8294
passphrase: c"",
8395
expected_bip39_seed: "f4577e463be595868060e5a763328153155b4167cd284998c8c6096d044742372020f5b052d0c41c1c5e6a6a7da2cb8a367aaaa074fab7773e8d5b2f684257ed",
96+
expected_root_fingerprint: "0b2fa4e5",
8497
},
8598
Test {
8699
seed: "fb5cf00d5ea61059fa066e25a6be9544",
87100
passphrase: c"password",
88101
expected_bip39_seed: "5922fb7630bc7cb871af102f733b6bdb8f05945147cd4646a89056fde0bdad5c3a4ff5be3f9e7af535f570e7053b5b22472555b331bc89cb797c306f7eb6a5a1",
102+
expected_root_fingerprint: "c4062d44",
89103
},
90104
// 24 byte seed
91105
Test {
92106
seed: "23705a91b177b49822f28b3f1a60072d113fcaff4f250191",
93107
passphrase: c"",
94108
expected_bip39_seed: "4a2a016a6d90eb3a79b7931ca0a172df5c5bfee3e5b47f0fd84bc0791ea3bbc9476c3d5de71cdb12c37e93c2aa3d5c303257f1992aed400fc5bbfc7da787bfa7",
109+
expected_root_fingerprint: "62fd19e0",
95110
},
96111
Test {
97112
seed: "23705a91b177b49822f28b3f1a60072d113fcaff4f250191",
98113
passphrase: c"password",
99114
expected_bip39_seed: "bc317ee0f88870254be32274d63ec2b0e962bf09f3ca04287912bfc843f2fab7c556f8657cadc924f99a217b0daa91898303a8414102031a125c50023e45a80b",
115+
expected_root_fingerprint: "c745266d",
100116
},
101117
// 32 byte seed
102118
Test {
103119
seed: "bd83a008b3b78c8cc56c678d1b7bfc651cc5be8242f44b5c0db96a34ee297833",
104120
passphrase: c"",
105121
expected_bip39_seed: "63f844e2c61ecfb20f9100de381a7a9ec875b085f5ac7735a2ba4d615a0f4147b87be402f65651969130683deeef752760c09e291604fe4b89d61ffee2630be8",
122+
expected_root_fingerprint: "93ba3a7b",
106123
},
107124
Test {
108125
seed: "bd83a008b3b78c8cc56c678d1b7bfc651cc5be8242f44b5c0db96a34ee297833",
109126
passphrase: c"password",
110127
expected_bip39_seed: "42e90dacd61f3373542d212f0fb9c291dcea84a6d85034272372dde7188638a98527280d65e41599f30d3434d8ee3d4747dbb84801ff1a851d2306c7d1648374",
128+
expected_root_fingerprint: "b95c9318",
111129
},
112130
];
113131

114132
for test in tests {
115133
let seed = hex::decode(test.seed).unwrap();
116134
let mut bip39_seed = [0u8; 64];
135+
let mut root_fingerprint = [0u8; 4];
117136
unsafe {
118137
rust_derive_bip39_seed(
119138
util::bytes::rust_util_bytes(seed.as_ptr(), seed.len()),
120139
test.passphrase.as_ptr(),
121140
util::bytes::rust_util_bytes_mut(bip39_seed.as_mut_ptr(), bip39_seed.len()),
141+
util::bytes::rust_util_bytes_mut(
142+
root_fingerprint.as_mut_ptr(),
143+
root_fingerprint.len(),
144+
),
122145
);
123146
}
124147
assert_eq!(hex::encode(bip39_seed).as_str(), test.expected_bip39_seed);
148+
assert_eq!(
149+
hex::encode(root_fingerprint).as_str(),
150+
test.expected_root_fingerprint
151+
);
125152
}
126153
}
127154

src/rust/bitbox02-rust/src/keystore.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,7 @@ pub fn get_xpub_twice(keypath: &[u32]) -> Result<bip32::Xpub, ()> {
8989
/// according to:
9090
/// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#serialization-format
9191
pub fn root_fingerprint() -> Result<Vec<u8>, ()> {
92-
Ok(get_xpub_twice(&[])?
93-
.pubkey_hash160()
94-
.get(..4)
95-
.ok_or(())?
96-
.to_vec())
92+
keystore::root_fingerprint()
9793
}
9894

9995
fn bip85_entropy(keypath: &[u32]) -> Result<zeroize::Zeroizing<Vec<u8>>, ()> {
@@ -342,13 +338,17 @@ mod tests {
342338

343339
bitbox02::securechip::fake_event_counter_reset();
344340
assert_eq!(root_fingerprint(), Ok(vec![0x02, 0x40, 0xe9, 0x2a]));
345-
assert_eq!(bitbox02::securechip::fake_event_counter(), 2);
341+
// fingerprint is precomputed during bip39 unlock, so takes no securechip events.
342+
assert_eq!(bitbox02::securechip::fake_event_counter(), 0);
346343

347344
mock_unlocked_using_mnemonic(
348345
"small agent wife animal marine cloth exit thank stool idea steel frame",
349346
"",
350347
);
351348
assert_eq!(root_fingerprint(), Ok(vec![0xf4, 0x0b, 0x46, 0x9a]));
349+
350+
keystore::lock();
351+
assert_eq!(root_fingerprint(), Err(()));
352352
}
353353

354354
#[test]

src/rust/bitbox02-rust/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub mod hal;
3636
pub mod hash;
3737
pub mod hww;
3838
pub mod keystore;
39-
mod secp256k1;
39+
pub mod secp256k1;
4040
#[cfg(feature = "app-u2f")]
4141
mod u2f;
4242
mod version;

src/rust/bitbox02/src/keystore.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
// limitations under the License.
1414

1515
extern crate alloc;
16-
use alloc::string::{String, ToString};
1716

17+
use alloc::string::{String, ToString};
1818
use alloc::vec;
1919
use alloc::vec::Vec;
2020

21+
use util::cell::SyncUnsafeCell;
22+
2123
use bitcoin::secp256k1::{All, Secp256k1};
2224

2325
use core::convert::TryInto;
@@ -28,6 +30,8 @@ use bitbox02_sys::keystore_error_t;
2830
const EC_PUBLIC_KEY_LEN: usize = 33;
2931
pub const MAX_SEED_LENGTH: usize = bitbox02_sys::KEYSTORE_MAX_SEED_LENGTH as usize;
3032

33+
static ROOT_FINGERPRINT: SyncUnsafeCell<Option<[u8; 4]>> = SyncUnsafeCell::new(None);
34+
3135
pub fn is_locked() -> bool {
3236
unsafe { bitbox02_sys::keystore_is_locked() }
3337
}
@@ -87,23 +91,39 @@ pub fn unlock(password: &str) -> Result<(), Error> {
8791

8892
pub fn lock() {
8993
unsafe { bitbox02_sys::keystore_lock() }
94+
95+
unsafe { ROOT_FINGERPRINT.write(None) }
9096
}
9197

9298
pub fn unlock_bip39(mnemonic_passphrase: &str) -> Result<(), Error> {
99+
let mut root_fingerprint = [0u8; 4];
93100
if unsafe {
94101
bitbox02_sys::keystore_unlock_bip39(
95102
crate::util::str_to_cstr_vec(mnemonic_passphrase)
96103
.unwrap()
97104
.as_ptr()
98105
.cast(),
106+
root_fingerprint.as_mut_ptr(),
99107
)
100108
} {
109+
// Store root fingerprint.
110+
unsafe {
111+
ROOT_FINGERPRINT.write(Some(root_fingerprint));
112+
}
113+
101114
Ok(())
102115
} else {
103116
Err(Error::CannotUnlockBIP39)
104117
}
105118
}
106119

120+
pub fn root_fingerprint() -> Result<Vec<u8>, ()> {
121+
if is_locked() {
122+
return Err(());
123+
}
124+
unsafe { ROOT_FINGERPRINT.read().ok_or(()).map(|fp| fp.to_vec()) }
125+
}
126+
107127
pub fn create_and_store_seed(password: &str, host_entropy: &[u8]) -> Result<(), Error> {
108128
match unsafe {
109129
bitbox02_sys::keystore_create_and_store_seed(
@@ -485,9 +505,12 @@ mod tests {
485505
.unwrap();
486506
crate::memory::set_salt_root(mock_salt_root.as_slice().try_into().unwrap()).unwrap();
487507

508+
assert!(root_fingerprint().is_err());
488509
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
489510
assert!(unlock("password").is_ok());
511+
assert!(root_fingerprint().is_err());
490512
assert!(unlock_bip39("foo").is_ok());
513+
assert_eq!(root_fingerprint(), Ok(vec![0xf1, 0xbc, 0x3c, 0x46]),);
491514

492515
let expected_bip39_seed = hex::decode("2b3c63de86f0f2b13cc6a36c1ba2314fbc1b40c77ab9cb64e96ba4d5c62fc204748ca6626a9f035e7d431bce8c9210ec0bdffc2e7db873dee56c8ac2153eee9a").unwrap();
493516

src/rust/util/src/cell.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2025 Shift Crypto AG
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Same as `core::sync::SyncUnsafeCell`, which is still in nightly. Can remove once it is stable.
16+
pub struct SyncUnsafeCell<T: ?Sized> {
17+
value: core::cell::UnsafeCell<T>,
18+
}
19+
20+
// Implement Sync if the wrapped type is Sync.
21+
unsafe impl<T: ?Sized + Sync> Sync for SyncUnsafeCell<T> {}
22+
23+
impl<T> SyncUnsafeCell<T> {
24+
pub const fn new(val: T) -> Self {
25+
SyncUnsafeCell {
26+
value: core::cell::UnsafeCell::new(val),
27+
}
28+
}
29+
30+
/// Reads the value from `self` without moving it. This leaves the
31+
/// memory in `self` unchanged.
32+
///
33+
/// # Safety
34+
///
35+
/// This is unsafe because it allows accessing the interior value without
36+
/// synchronization. The caller must ensure no other code is currently
37+
/// writing to this cell during the read operation.
38+
pub unsafe fn read(&self) -> T
39+
where
40+
T: Sized,
41+
{
42+
unsafe { self.value.get().read() }
43+
}
44+
45+
/// Overwrites a memory location with the given value without reading or
46+
/// dropping the old value.
47+
///
48+
/// # Safety
49+
///
50+
/// This is unsafe because it allows accessing the interior value without
51+
/// synchronization. The caller must ensure no other code is currently
52+
/// accessing this cell (either reading or writing) during the write operation.
53+
pub unsafe fn write(&self, val: T)
54+
where
55+
T: Sized,
56+
{
57+
unsafe { self.value.get().write(val) }
58+
}
59+
}

src/rust/util/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
pub mod ascii;
1717
pub mod bip32;
1818
pub mod bytes;
19+
pub mod cell;
1920
pub mod decimal;
2021
pub mod log;
2122
pub mod name;

0 commit comments

Comments
 (0)