hal: add random, factory_randomness, drop random_32_bytes
What changed, and why it matters
This commit changes how the BitBox02 hardware wallet generates random numbers used to create secret keys and encrypt stored data. Previously, the firmware used a single 32-byte random value from the microcontroller. After this change, it mixes three sources: the microcontroller's random generator, the secure chip's random generator, and a fixed 32-byte 'factory randomness' value stored in a special flash memory location. The commit also adds a factory script to write that fixed value into devices during production. The change is a defensive hardening measure, not a fix for an active bug, but it introduces a new dependency: if the factory randomness is not actually random or is reused across devices, it could weaken security instead of strengthening it.
Verify that the factory randomness value is generated with a cryptographically secure random source per device and never logged, committed, or reused. Audit the J-Link production workflow to ensure the 32-byte value is written before firmware runs and that the flash region is read-only or protected after programming. Complete the BitBox03 `todo!()` implementations before release. Review whether the fixed flash address collides with any existing bootloader or firmware layout.
Security signals we found
New factory randomness storage at fixed flash address (0xdfe0) programmed via J-Link script
Random generation now mixes three entropy sources (MCU, secure chip, factory) and hashes them
Secure chip random function exposed through Rust HAL with error propagation
Removal of single-source `random_32_bytes()` API
Test vectors updated to reflect new deterministic output of mixed randomness
BitBox03 implementation left as `todo!()` (incomplete)
Evidence from the diff
The patch refactors the randomness API: Random::random_32_bytes() is removed and replaced by Random::factory_randomness() (returning a static 32-byte slice from flash) plus SecureChip::random() (returning secure-chip TRNG output). A new random_32_bytes() helper in bitbox02-rust/src/random.rs combines MCU entropy (mcu_32_bytes), secure-chip entropy, and factory randomness via XOR, then hashes the result with SHA-256. Call sites in keystore.rs (seed creation, retained-seed encryption, IV generation, Schnorr aux-rand) are updated to use the new helper. C wrappers atecc_random and optiga_random now return error codes instead of booleans. A J-Link script and binary payload are added to program 32 bytes of factory randomness at FLASH_BOOT_START + FLASH_BOOT_LEN - 32 (0xdfe0). BitBox03 code is updated with todo!() placeholders, indicating the refactor is incomplete for that platform.
Changed components
src/rust/bitbox02-rust/src/random.rssrc/rust/bitbox02-rust/src/keystore.rssrc/rust/bitbox02/src/hal/random.rssrc/rust/bitbox02/src/hal/securechip.rssrc/rust/bitbox02/src/securechip/imp.rssrc/atecc/atecc.csrc/optiga/optiga.cscripts/bb02-set-factory-randomness.jlinkscripts/bb02-set-factory-randomness.binInspect captured patch +255 / −74
diff --git a/Makefile b/Makefile
index 32356a2..9dfd75c 100644
--- a/Makefile
+++ b/Makefile
@@ -152,6 +152,8 @@ jlink-flash-set-securechip-optiga:
JLinkExe -NoGui 1 -if SWD -device ATSAMD51J20 -speed 4000 -autoconnect 1 -CommanderScript ./scripts/set-securechip-optiga.jlink
jlink-flash-set-bb02plus:
JLinkExe -NoGui 1 -if SWD -device ATSAMD51J20 -speed 4000 -autoconnect 1 -CommanderScript ./scripts/set-bb02plus.jlink
+jlink-flash-bb02-set-factory-randomness:
+ JLinkExe -NoGui 1 -if SWD -device ATSAMD51J20 -speed 4000 -autoconnect 1 -CommanderScript ./scripts/bb02-set-factory-randomness.jlink
jlink-erase-firmware-quick:
JLinkExe -NoGui 1 -if SWD -device ATSAMD51J20 -speed 4000 -autoconnect 1 -CommanderScript ./scripts/erase-firmware-quick.jlink
jlink-gdb-server:
diff --git a/scripts/bb02-set-factory-randomness.bin b/scripts/bb02-set-factory-randomness.bin
new file mode 100644
index 0000000..2a0944f
--- /dev/null
+++ b/scripts/bb02-set-factory-randomness.bin
@@ -0,0 +1 @@
+�{B�g����EZʫ�����=�[���1�K�M
\ No newline at end of file
diff --git a/scripts/bb02-set-factory-randomness.jlink b/scripts/bb02-set-factory-randomness.jlink
new file mode 100644
index 0000000..acfa8c4
--- /dev/null
+++ b/scripts/bb02-set-factory-randomness.jlink
@@ -0,0 +1,9 @@
+// Factory randomness at FLASH_BOOT_START + FLASH_BOOT_LEN - 32 = 0xdfe0 (57312).
+// Writes the following 32-byte value in a single flash download:
+// df7b42ab6789d2f2ea16455acaab81e0d7eed33d8e195b9589fa3107814b944d
+mem 0xdfe0 0x20
+// Always use read-modify-write so only this small range is updated.
+exec SetFlashDLNoRMWThreshold = 0xFFFFFFFF
+loadfile scripts/bb02-set-factory-randomness.bin 0xdfe0 noreset
+mem 0xdfe0 0x20
+q
diff --git a/src/atecc/atecc.c b/src/atecc/atecc.c
index ff668c5..13bfe22 100644
--- a/src/atecc/atecc.c
+++ b/src/atecc/atecc.c
@@ -672,14 +672,16 @@ bool atecc_monotonic_increments_remaining(uint32_t* remaining_out)
return true;
}
-bool atecc_random(uint8_t* rand_out)
+int atecc_random(uint8_t* rand_out)
{
+ ATCA_STATUS result = ATCA_GEN_FAIL;
for (int retries = 0; retries < 5; retries++) {
- if (atcab_random(rand_out) == ATCA_SUCCESS) {
- return true;
+ result = atcab_random(rand_out);
+ if (result == ATCA_SUCCESS) {
+ return 0;
}
}
- return false;
+ return result;
}
#if APP_U2F == 1 || FACTORYSETUP == 1
diff --git a/src/atecc/atecc.h b/src/atecc/atecc.h
index dbdf4ab..a3a5ec8 100644
--- a/src/atecc/atecc.h
+++ b/src/atecc/atecc.h
@@ -26,7 +26,7 @@ USE_RESULT bool atecc_reset_keys(void);
USE_RESULT bool atecc_gen_attestation_key(uint8_t* pubkey_out);
USE_RESULT bool atecc_attestation_sign(const uint8_t* challenge, uint8_t* signature_out);
USE_RESULT bool atecc_monotonic_increments_remaining(uint32_t* remaining_out);
-USE_RESULT bool atecc_random(uint8_t* rand_out);
+USE_RESULT int atecc_random(uint8_t* rand_out);
#if APP_U2F == 1 || FACTORYSETUP == 1
USE_RESULT bool atecc_u2f_counter_set(uint32_t counter);
#endif
diff --git a/src/optiga/optiga.c b/src/optiga/optiga.c
index 8f7f3a4..f72ebc8 100644
--- a/src/optiga/optiga.c
+++ b/src/optiga/optiga.c
@@ -1871,15 +1871,15 @@ bool optiga_monotonic_increments_remaining(uint32_t* remaining_out)
}
// rand_out must be 32 bytes
-bool optiga_random(uint8_t* rand_out)
+int optiga_random(uint8_t* rand_out)
{
optiga_lib_status_t res =
optiga_ops_crypt_random_sync(_crypt, OPTIGA_RNG_TYPE_TRNG, rand_out, 32);
if (res != OPTIGA_CRYPT_SUCCESS) {
util_log("optiga_random failed: %x", res);
- return false;
+ return res;
}
- return true;
+ return 0;
}
#if APP_U2F == 1 || FACTORYSETUP == 1
diff --git a/src/optiga/optiga.h b/src/optiga/optiga.h
index 939535c..8cbb3e5 100644
--- a/src/optiga/optiga.h
+++ b/src/optiga/optiga.h
@@ -95,7 +95,7 @@ USE_RESULT bool optiga_reset_keys(void);
USE_RESULT bool optiga_gen_attestation_key(uint8_t* pubkey_out);
USE_RESULT bool optiga_attestation_sign(const uint8_t* challenge, uint8_t* signature_out);
USE_RESULT bool optiga_monotonic_increments_remaining(uint32_t* remaining_out);
-USE_RESULT bool optiga_random(uint8_t* rand_out);
+USE_RESULT int optiga_random(uint8_t* rand_out);
#if APP_U2F == 1 || FACTORYSETUP == 1
USE_RESULT bool optiga_u2f_counter_set(uint32_t counter);
#endif
diff --git a/src/rust/bitbox-hal/src/random.rs b/src/rust/bitbox-hal/src/random.rs
index 1536f76..0dc441a 100644
--- a/src/rust/bitbox-hal/src/random.rs
+++ b/src/rust/bitbox-hal/src/random.rs
@@ -1,8 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
-use alloc::boxed::Box;
-
pub trait Random {
- fn random_32_bytes(&mut self) -> Box<zeroize::Zeroizing<[u8; 32]>>;
+ fn factory_randomness(&mut self) -> &'static [u8; 32];
fn mcu_32_bytes(&mut self, out: &mut [u8; 32]);
}
diff --git a/src/rust/bitbox-hal/src/securechip.rs b/src/rust/bitbox-hal/src/securechip.rs
index a44fab7..82f1414 100644
--- a/src/rust/bitbox-hal/src/securechip.rs
+++ b/src/rust/bitbox-hal/src/securechip.rs
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
-use alloc::vec::Vec;
+use alloc::{boxed::Box, vec::Vec};
use super::memory::PasswordStretchAlgo;
@@ -48,6 +48,9 @@ pub enum SecureChipError {
}
pub trait SecureChip {
+ /// Returns 32 bytes of randomness generated by the secure chip.
+ fn random(&mut self) -> Result<Box<zeroize::Zeroizing<[u8; 32]>>, Error>;
+
/// Prepares the secure chip for a new password and returns the stretched password.
///
/// This reinitializes the secure-chip state used for password derivation and returns the same
diff --git a/src/rust/bitbox-platform-host/src/securechip.rs b/src/rust/bitbox-platform-host/src/securechip.rs
index a2e3beb..0cd53d4 100644
--- a/src/rust/bitbox-platform-host/src/securechip.rs
+++ b/src/rust/bitbox-platform-host/src/securechip.rs
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
-use alloc::vec::Vec;
+use alloc::collections::VecDeque;
+use alloc::{boxed::Box, vec::Vec};
use bitcoin::hashes::Hash;
use hex_lit::hex;
@@ -17,6 +18,7 @@ pub struct FakeSecureChip {
#[cfg(feature = "app-u2f")]
u2f_counter: u32,
mock_attestation_signature: [u8; 64],
+ mock_random_values: VecDeque<[u8; 32]>,
last_attestation_challenge: Option<[u8; 32]>,
}
@@ -28,6 +30,7 @@ impl FakeSecureChip {
#[cfg(feature = "app-u2f")]
u2f_counter: 0,
mock_attestation_signature: [0u8; 64],
+ mock_random_values: VecDeque::new(),
last_attestation_challenge: None,
}
}
@@ -56,12 +59,22 @@ impl FakeSecureChip {
self.mock_attestation_signature = *sig;
}
+ pub fn mock_random(&mut self, random: [u8; 32]) {
+ self.mock_random_values.push_back(random);
+ }
+
pub fn last_attestation_challenge(&self) -> Option<[u8; 32]> {
self.last_attestation_challenge
}
}
impl bitbox_hal::SecureChip for FakeSecureChip {
+ fn random(&mut self) -> Result<Box<zeroize::Zeroizing<[u8; 32]>>, Error> {
+ Ok(Box::new(zeroize::Zeroizing::new(
+ self.mock_random_values.pop_front().unwrap_or([0u8; 32]),
+ )))
+ }
+
fn init_new_password(
&mut self,
password: &str,
@@ -155,3 +168,21 @@ impl bitbox_hal::SecureChip for FakeSecureChip {
Ok(())
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use bitbox_hal::SecureChip;
+ use hex_lit::hex;
+
+ #[test]
+ fn test_mock_random() {
+ let mut securechip = FakeSecureChip::new();
+ let expected = hex!("00112233445566778899aabbccddeefffedcba98765432100123456789abcdef");
+ securechip.mock_random(expected);
+ let first = securechip.random().unwrap();
+ let second = securechip.random().unwrap();
+ assert_eq!(first.as_slice(), &expected);
+ assert_eq!(second.as_slice(), &[0u8; 32]);
+ }
+}
diff --git a/src/rust/bitbox-securechip/src/atecc.rs b/src/rust/bitbox-securechip/src/atecc.rs
index 5283267..e0f1f89 100644
--- a/src/rust/bitbox-securechip/src/atecc.rs
+++ b/src/rust/bitbox-securechip/src/atecc.rs
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::{Error, Model, PasswordStretchAlgo, SecureChipError};
-use alloc::{vec, vec::Vec};
+use alloc::{boxed::Box, vec, vec::Vec};
use zeroize::Zeroizing;
pub fn attestation_sign(challenge: &[u8; 32], signature: &mut [u8; 64]) -> Result<(), ()> {
@@ -13,6 +13,15 @@ pub fn attestation_sign(challenge: &[u8; 32], signature: &mut [u8; 64]) -> Resul
}
}
+pub fn random() -> Result<Box<Zeroizing<[u8; 32]>>, Error> {
+ let mut result = Box::new(Zeroizing::new([0u8; 32]));
+ let status = unsafe { bitbox_securechip_sys::atecc_random(result.as_mut_ptr()) };
+ if status == 0 {
+ Ok(result)
+ } else {
+ Err(Error::from_status(status))
+ }
+}
pub fn monotonic_increments_remaining() -> Result<u32, ()> {
let mut result = 0u32;
match unsafe { bitbox_securechip_sys::atecc_monotonic_increments_remaining(&mut result) } {
diff --git a/src/rust/bitbox-securechip/src/optiga.rs b/src/rust/bitbox-securechip/src/optiga.rs
index be64e2e..a96c541 100644
--- a/src/rust/bitbox-securechip/src/optiga.rs
+++ b/src/rust/bitbox-securechip/src/optiga.rs
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::{Error, Model, PasswordStretchAlgo, SecureChipError};
-use alloc::{vec, vec::Vec};
+use alloc::{boxed::Box, vec, vec::Vec};
use zeroize::Zeroizing;
pub fn attestation_sign(challenge: &[u8; 32], signature: &mut [u8; 64]) -> Result<(), ()> {
@@ -13,6 +13,15 @@ pub fn attestation_sign(challenge: &[u8; 32], signature: &mut [u8; 64]) -> Resul
}
}
+pub fn random() -> Result<Box<Zeroizing<[u8; 32]>>, Error> {
+ let mut result = Box::new(Zeroizing::new([0u8; 32]));
+ let status = unsafe { bitbox_securechip_sys::optiga_random(result.as_mut_ptr()) };
+ if status == 0 {
+ Ok(result)
+ } else {
+ Err(Error::from_status(status))
+ }
+}
pub fn monotonic_increments_remaining() -> Result<u32, ()> {
let mut result = 0u32;
match unsafe { bitbox_securechip_sys::optiga_monotonic_increments_remaining(&mut result) } {
diff --git a/src/rust/bitbox02-rust/src/hal/testing/random.rs b/src/rust/bitbox02-rust/src/hal/testing/random.rs
index d871412..1304d3e 100644
--- a/src/rust/bitbox02-rust/src/hal/testing/random.rs
+++ b/src/rust/bitbox02-rust/src/hal/testing/random.rs
@@ -1,9 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
-
-use alloc::boxed::Box;
use alloc::collections::VecDeque;
use bitcoin::hashes::{Hash, sha256};
+use hex_lit::hex;
pub struct TestingRandom {
mock_next_values: VecDeque<[u8; 32]>,
@@ -11,6 +10,9 @@ pub struct TestingRandom {
}
impl TestingRandom {
+ pub const FACTORY_RANDOMNESS: [u8; 32] =
+ hex!("f71df5932e61dbaab9b9eca90e59c4b45ec91fadf803db15578c260c608eb46b");
+
pub fn new() -> Self {
Self {
mock_next_values: VecDeque::new(),
@@ -34,8 +36,8 @@ impl TestingRandom {
}
impl crate::hal::Random for TestingRandom {
- fn random_32_bytes(&mut self) -> Box<zeroize::Zeroizing<[u8; 32]>> {
- Box::new(zeroize::Zeroizing::new(self.next_value()))
+ fn factory_randomness(&mut self) -> &'static [u8; 32] {
+ &Self::FACTORY_RANDOMNESS
}
fn mcu_32_bytes(&mut self, out: &mut [u8; 32]) {
@@ -49,21 +51,6 @@ mod tests {
use crate::hal::Random;
use hex_lit::hex;
- #[test]
- fn test_random() {
- let mut random = TestingRandom::new();
- let first = random.random_32_bytes();
- let second = random.random_32_bytes();
- assert_eq!(
- first.as_slice(),
- &hex!("b40711a88c7039756fb8a73827eabe2c0fe5a0346ca7e0a104adc0fc764f528d"),
- );
- assert_eq!(
- second.as_slice(),
- &hex!("433ebf5bc03dffa38536673207a21281612cef5faa9bc7a4d5b9be2fdb12cf1a"),
- );
- }
-
#[test]
fn test_mcu_32_bytes() {
let mut random = TestingRandom::new();
@@ -80,4 +67,13 @@ mod tests {
hex!("433ebf5bc03dffa38536673207a21281612cef5faa9bc7a4d5b9be2fdb12cf1a"),
);
}
+
+ #[test]
+ fn test_factory_randomness() {
+ let mut random = TestingRandom::new();
+ let first = random.factory_randomness();
+ let second = random.factory_randomness();
+ assert_eq!(first, &TestingRandom::FACTORY_RANDOMNESS);
+ assert_eq!(second, &TestingRandom::FACTORY_RANDOMNESS);
+ }
}
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 eb534ca..6c596b7 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
@@ -2211,7 +2211,7 @@ mod tests {
assert_eq!(
next.signature,
hex!(
- "74fa05435a838a76ab34105f783d8d69136977b85df4644dec6afc85bba669ddb7c127d7a5a6d3cb406843b6e4366a276872228bb9efa4e3c22cfd07be3198b5"
+ "87f00346f53b11e03175eaf5254e3aec432bfd9585465c83fb483e8ddaf0893b0460d764711b4adf5865419d06116abc174b1578372e11fcd41cd2db18c306a7"
)
);
}
@@ -3415,7 +3415,7 @@ mod tests {
assert_eq!(
next.signature,
hex!(
- "63bb140c52b30f8625219dac0951cad4a6c1c2c5c6a014be40fd46a80ab77207780626f7d568e885f26484bbc3624714a26234a0da5236775cbfae5ed7a6ad8d"
+ "d2816fd56495bac5815283381775e0a0dec837a5d646a0e5602c2451ed173ffae16d10bfe391458aa9499dd908fe6ab669ef571f8a78e98ea8de27d82013a76d"
)
);
}
diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs
index a1cafeb..046c6f6 100644
--- a/src/rust/bitbox02-rust/src/keystore.rs
+++ b/src/rust/bitbox02-rust/src/keystore.rs
@@ -3,6 +3,7 @@
#[cfg(feature = "ed25519")]
pub mod ed25519;
+use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
@@ -36,6 +37,7 @@ pub trait KeystoreHal {
fn memory(&mut self) -> &mut Self::Memory;
fn random(&mut self) -> &mut Self::Random;
fn securechip(&mut self) -> &mut Self::SecureChip;
+ fn random_and_securechip(&mut self) -> (&mut Self::Random, &mut Self::SecureChip);
}
pub struct KeystoreHalImpl<'a, E: Eeprom, M: Memory, R: Random, S: SecureChip> {
@@ -98,6 +100,10 @@ impl<E: Eeprom, M: Memory, R: Random, S: SecureChip> KeystoreHal
fn securechip(&mut self) -> &mut Self::SecureChip {
self.securechip
}
+
+ fn random_and_securechip(&mut self) -> (&mut Self::Random, &mut Self::SecureChip) {
+ (self.random, self.securechip)
+ }
}
#[derive(Debug)]
@@ -127,6 +133,11 @@ impl core::convert::From<securechip::Error> for Error {
}
}
+fn random_32_bytes(hal: &mut impl KeystoreHal) -> Result<Box<zeroize::Zeroizing<[u8; 32]>>, Error> {
+ let (random, securechip) = hal.random_and_securechip();
+ crate::random::random_32_bytes(random, securechip).map_err(Into::into)
+}
+
#[derive(Copy, Clone)]
struct ReadOnlyBuffer {
// 64 is the biggest retained buffer (bip39 seed) we will store, and 64 is added for the
@@ -166,19 +177,14 @@ impl RetainedEncryptedBuffer {
data: &[u8],
purpose: &'static str,
) -> Result<Self, Error> {
- let rand: [u8; 32] = hal
- .random()
- .random_32_bytes()
- .as_slice()
- .try_into()
- .unwrap();
+ let rand: [u8; 32] = random_32_bytes(hal)?.as_slice().try_into().unwrap();
let encryption_key = stretch_retained_seed_encryption_key(
hal,
&rand,
&format!("{}_in", purpose),
&format!("{}_out", purpose),
)?;
- let iv_rand = hal.random().random_32_bytes();
+ let iv_rand = random_32_bytes(hal)?;
let iv: &[u8; 16] = iv_rand.first_chunk::<16>().unwrap();
let encrypted = bitbox_aes::encrypt_with_hmac(iv, &encryption_key, data);
Ok(RetainedEncryptedBuffer {
@@ -313,7 +319,7 @@ fn encrypt_and_store_seed_internal(
.securechip()
.init_new_password(password, password_stretch_algo)?;
- let iv_rand = hal.random().random_32_bytes();
+ let iv_rand = crate::random::random_32_bytes_from_hal(hal)?;
let iv: &[u8; 16] = iv_rand.first_chunk::<16>().unwrap();
let encrypted = bitbox_aes::encrypt_with_hmac(iv, &secret, seed);
@@ -538,7 +544,7 @@ pub fn create_and_store_seed(
return Err(Error::SeedSize);
}
- let mut seed_vec = hal.random().random_32_bytes();
+ let mut seed_vec = crate::random::random_32_bytes_from_hal(hal)?;
let seed = &mut seed_vec[..seed_len];
// Mix in host entropy.
@@ -805,7 +811,7 @@ pub fn secp256k1_schnorr_sign(
.map_err(|_| ())?;
}
- let aux_rand = hal.random().random_32_bytes();
+ let aux_rand = crate::random::random_32_bytes_from_hal(hal).map_err(|_| ())?;
let sig = SECP256K1.sign_schnorr_with_aux_rand(
&bitcoin::secp256k1::Message::from_digest(*msg),
&keypair,
@@ -856,8 +862,9 @@ pub mod testing {
mod tests {
use super::*;
- use crate::hal::testing::TestingHal;
+ use crate::hal::testing::{TestingHal, TestingRandom};
use hex_lit::hex;
+ use sha2::Digest;
use bitbox02::testing::mock_memory;
use testing::{TEST_MNEMONIC, mock_unlocked, mock_unlocked_using_mnemonic};
@@ -926,15 +933,23 @@ mod tests {
));
}
- // Hack to get the random bytes that will be used.
+ // Mock the randomness that will be used.
let seed_random = [0x34; 32];
+ let securechip_random =
+ hex!("1111111111111111222222222222222233333333333333334444444444444444");
+ let factory_randomness = TestingRandom::FACTORY_RANDOMNESS;
// Derived from mock_salt_root and "password".
let password_salted_hashed =
hex!("e8c70a20d9108fbb9454b1b8e2d7373e78cbaf9de025ab2d4f4d3c7a6711694c");
- // expected_seed = seed_random ^ host_entropy ^ password_salted_hashed
- let expected_seed: Vec<u8> = seed_random
+ // expected_seed =
+ // sha256(seed_random ^ securechip_random ^ factory_randomness) ^ host_entropy ^
+ // password_salted_hashed
+ let mixed_random: [u8; 32] =
+ core::array::from_fn(|i| seed_random[i] ^ securechip_random[i] ^ factory_randomness[i]);
+ let expected_random: [u8; 32] = sha2::Sha256::digest(mixed_random).into();
+ let expected_seed: Vec<u8> = expected_random
.into_iter()
.zip(host_entropy.iter())
.zip(password_salted_hashed)
@@ -946,6 +961,7 @@ mod tests {
hal.memory.set_salt_root(&mock_salt_root);
hal.random.mock_next(seed_random);
+ hal.securechip.mock_random(securechip_random);
assert!(create_and_store_seed(&mut hal, "password", &host_entropy[..size]).is_ok());
assert_eq!(
copy_seed(&mut hal).unwrap().as_slice(),
@@ -1243,7 +1259,7 @@ mod tests {
// Also check that the retained seed was encrypted with the expected encryption key.
let decrypted = {
let expected_retained_seed_secret =
- hex!("b156be416530c6fc00018844161774a3546a53ac6dd4a0462608838e216008f7");
+ hex!("15964d70dc2075025c46db12843b4c5a68f54c4809b4e2e078e88b9a4a3106ff");
bitbox_aes::decrypt_with_hmac(
&expected_retained_seed_secret,
RETAINED_SEED.read().unwrap().encrypted_seed.as_slice(),
@@ -1572,7 +1588,7 @@ mod tests {
// Check that the retained bip39 seed was encrypted with the expected encryption key.
let decrypted = {
let expected_retained_bip39_seed_secret =
- hex!("856d9a8c1ea42a69ae76324244ace674397ff1360a4ba4c85ffbd42cee8a7f29");
+ hex!("e0985ca64ab7b7a70d9c890d87aed1c1dc35ad2bd7f88785b37696610fea8ead");
bitbox_aes::decrypt_with_hmac(
&expected_retained_bip39_seed_secret,
RETAINED_BIP39_SEED
diff --git a/src/rust/bitbox02-rust/src/lib.rs b/src/rust/bitbox02-rust/src/lib.rs
index 7301252..15b99e9 100644
--- a/src/rust/bitbox02-rust/src/lib.rs
+++ b/src/rust/bitbox02-rust/src/lib.rs
@@ -28,6 +28,7 @@ pub mod keystore;
not(any(feature = "c-unit-testing", feature = "simulator-graphical"))
))]
pub mod main_loop;
+pub mod random;
pub mod reset;
pub mod salt;
pub mod secp256k1;
diff --git a/src/rust/bitbox02-rust/src/random.rs b/src/rust/bitbox02-rust/src/random.rs
new file mode 100644
index 0000000..75b8eb6
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/random.rs
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use alloc::boxed::Box;
+
+use crate::hal::{Hal, Random, SecureChip, securechip};
+use digest::FixedOutput;
+use sha2::Digest;
+
+pub fn random_32_bytes(
+ hal_random: &mut impl Random,
+ hal_securechip: &mut impl SecureChip,
+) -> Result<Box<zeroize::Zeroizing<[u8; 32]>>, securechip::Error> {
+ let mut mixed = zeroize::Zeroizing::new([0u8; 32]);
+ hal_random.mcu_32_bytes(&mut mixed);
+
+ let securechip_random = hal_securechip.random()?;
+ for (byte, securechip_byte) in mixed.iter_mut().zip(securechip_random.iter()) {
+ *byte ^= *securechip_byte;
+ }
+
+ let factory_randomness = hal_random.factory_randomness();
+ for (byte, factory_randomness_byte) in mixed.iter_mut().zip(factory_randomness.iter()) {
+ *byte ^= *factory_randomness_byte;
+ }
+
+ let mut result = Box::new(zeroize::Zeroizing::new([0u8; 32]));
+ let mut hasher = sha2::Sha256::new();
+ hasher.update(mixed.as_slice());
+ FixedOutput::finalize_into(hasher, result.as_mut_slice().into());
+ Ok(result)
+}
+
+pub fn random_32_bytes_from_hal(
+ hal: &mut impl Hal,
+) -> Result<Box<zeroize::Zeroizing<[u8; 32]>>, securechip::Error> {
+ let crate::hal::HalSubsystems {
+ random, securechip, ..
+ } = hal.as_mut();
+ random_32_bytes(random, securechip)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::hal::testing::{TestingHal, TestingRandom};
+ use hex_lit::hex;
+
+ #[test]
+ fn test_random_32_bytes() {
+ let mut mock_hal = TestingHal::new();
+ let mcu_random = hex!("00112233445566778899aabbccddeefffedcba98765432100123456789abcdef");
+ let securechip_random =
+ hex!("102030405060708090a0b0c0d0e0f0000f1e2d3c4b5a69788796a5b4c3d2e1f0");
+ let factory_randomness = TestingRandom::FACTORY_RANDOMNESS;
+
+ mock_hal.random.mock_next(mcu_random);
+ mock_hal.securechip.mock_random(securechip_random);
+ assert_eq!(mock_hal.random.factory_randomness(), &factory_randomness);
+
+ let (hal_random, hal_securechip) = (&mut mock_hal.random, &mut mock_hal.securechip);
+ let result = random_32_bytes(hal_random, hal_securechip).unwrap();
+
+ /* Reproduce expected with Python:
+ import hashlib
+ mcu_random = bytes.fromhex(
+ "00112233445566778899aabbccddeefffedcba98765432100123456789abcdef"
+ )
+ securechip_random = bytes.fromhex(
+ "102030405060708090a0b0c0d0e0f0000f1e2d3c4b5a69788796a5b4c3d2e1f0"
+ )
+ factory_randomness = bytes.fromhex(
+ "f71df5932e61dbaab9b9eca90e59c4b45ec91fadf803db15578c260c608eb46b"
+ )
+ mixed = bytes(
+ m ^ s ^ f
+ for m, s, f in zip(mcu_random, securechip_random, factory_randomness)
+ )
+ print(hashlib.sha256(mixed).hexdigest())
+ */
+ let expected = hex!("843595519af3ac2a92cbe2be42a77d5297f64a1c98c1edbc27e1fc661f1d4ac8");
+ assert_eq!(result.as_slice(), &expected);
+ }
+}
diff --git a/src/rust/bitbox02-sys/build.rs b/src/rust/bitbox02-sys/build.rs
index c70143c..3c01945 100644
--- a/src/rust/bitbox02-sys/build.rs
+++ b/src/rust/bitbox02-sys/build.rs
@@ -11,6 +11,8 @@ const ALLOWLIST_VARS: &[&str] = &[
"BIP39_WORDLIST_LEN",
"da14531_handler_current_product",
"da14531_handler_current_product_len",
+ "BITBOX02_FLASH_BOOT_LEN",
+ "BITBOX02_FLASH_BOOT_START",
"font_font_a_11X10",
"font_font_a_9X9",
"font_monogram_5X9",
diff --git a/src/rust/bitbox02-sys/wrapper.h b/src/rust/bitbox02-sys/wrapper.h
index 455ba87..dfbac05 100644
--- a/src/rust/bitbox02-sys/wrapper.h
+++ b/src/rust/bitbox02-sys/wrapper.h
@@ -1,9 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
+#include <stdint.h>
+
#include <da14531/da14531.h>
#include <da14531/da14531_handler.h>
#include <da14531/da14531_protocol.h>
#include <delay.h>
+#include <flags.h>
#include <hww.h>
#include <memory/bitbox02_smarteeprom.h>
#include <memory/memory.h>
@@ -51,6 +54,9 @@
#include <util.h>
#include <utils_ringbuffer.h>
+static const uintptr_t BITBOX02_FLASH_BOOT_START = FLASH_BOOT_START;
+static const uintptr_t BITBOX02_FLASH_BOOT_LEN = FLASH_BOOT_LEN;
+
#if defined(TESTING)
#include <fake_memory.h>
#include <touch/gestures.h>
diff --git a/src/rust/bitbox02/src/hal/random.rs b/src/rust/bitbox02/src/hal/random.rs
index 0b40e14..9921543 100644
--- a/src/rust/bitbox02/src/hal/random.rs
+++ b/src/rust/bitbox02/src/hal/random.rs
@@ -1,15 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
-use alloc::boxed::Box;
-
use bitbox_hal::Random;
pub struct BitBox02Random;
impl Random for BitBox02Random {
#[inline(always)]
- fn random_32_bytes(&mut self) -> Box<zeroize::Zeroizing<[u8; 32]>> {
- crate::random::random_32_bytes()
+ fn factory_randomness(&mut self) -> &'static [u8; 32] {
+ let addr =
+ bitbox02_sys::BITBOX02_FLASH_BOOT_START + bitbox02_sys::BITBOX02_FLASH_BOOT_LEN - 32;
+ unsafe { &*(addr as *const [u8; 32]) }
}
#[inline(always)]
diff --git a/src/rust/bitbox02/src/hal/securechip.rs b/src/rust/bitbox02/src/hal/securechip.rs
index d1360fc..267d653 100644
--- a/src/rust/bitbox02/src/hal/securechip.rs
+++ b/src/rust/bitbox02/src/hal/securechip.rs
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
-use alloc::vec::Vec;
+use alloc::{boxed::Box, vec::Vec};
use bitbox_hal::SecureChip;
use bitbox_hal::memory::PasswordStretchAlgo;
@@ -77,6 +77,10 @@ fn to_c_password_stretch_algo(algo: PasswordStretchAlgo) -> bitbox_securechip::P
}
impl SecureChip for BitBox02SecureChip {
+ fn random(&mut self) -> Result<Box<zeroize::Zeroizing<[u8; 32]>>, Error> {
+ crate::securechip::random().map_err(to_hal_error)
+ }
+
fn init_new_password(
&mut self,
password: &str,
diff --git a/src/rust/bitbox02/src/random.rs b/src/rust/bitbox02/src/random.rs
index 5b6c63e..76f74ff 100644
--- a/src/rust/bitbox02/src/random.rs
+++ b/src/rust/bitbox02/src/random.rs
@@ -26,12 +26,6 @@ pub fn mcu_32_bytes(out: &mut [u8; 32]) {
}
}
-pub fn random_32_bytes() -> alloc::boxed::Box<zeroize::Zeroizing<[u8; 32]>> {
- let mut out = alloc::boxed::Box::new(zeroize::Zeroizing::new([0u8; 32]));
- unsafe { bitbox02_sys::random_32_bytes(out.as_mut_ptr()) }
- out
-}
-
/// `private_key_out` must be 32 bytes.
#[unsafe(no_mangle)]
pub extern "C" fn rust_noise_generate_static_private_key(
diff --git a/src/rust/bitbox02/src/securechip/imp.rs b/src/rust/bitbox02/src/securechip/imp.rs
index 0260088..f04c9da 100644
--- a/src/rust/bitbox02/src/securechip/imp.rs
+++ b/src/rust/bitbox02/src/securechip/imp.rs
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
-use alloc::vec::Vec;
+use alloc::{boxed::Box, vec::Vec};
use bitbox_securechip::{Error, Model, PasswordStretchAlgo, atecc, optiga};
use core::ffi::c_int;
use util::cell::SyncCell;
@@ -25,6 +25,13 @@ pub fn attestation_sign(challenge: &[u8; 32], signature: &mut [u8; 64]) -> Resul
}
}
+pub fn random() -> Result<Box<Zeroizing<[u8; 32]>>, Error> {
+ match backend() {
+ Backend::Atecc => atecc::random(),
+ Backend::Optiga => optiga::random(),
+ }
+}
+
pub fn monotonic_increments_remaining() -> Result<u32, ()> {
match backend() {
Backend::Atecc => atecc::monotonic_increments_remaining(),
@@ -136,8 +143,8 @@ pub unsafe extern "C" fn rust_securechip_gen_attestation_key(pubkey_out: *mut u8
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_securechip_random(rand_out: *mut u8) -> bool {
match backend() {
- Backend::Atecc => unsafe { bitbox_securechip_sys::atecc_random(rand_out) },
- Backend::Optiga => unsafe { bitbox_securechip_sys::optiga_random(rand_out) },
+ Backend::Atecc => unsafe { bitbox_securechip_sys::atecc_random(rand_out) == 0 },
+ Backend::Optiga => unsafe { bitbox_securechip_sys::optiga_random(rand_out) == 0 },
}
}
diff --git a/src/rust/bitbox02/src/securechip/imp_fake.rs b/src/rust/bitbox02/src/securechip/imp_fake.rs
index 73a14e7..3aa5408 100644
--- a/src/rust/bitbox02/src/securechip/imp_fake.rs
+++ b/src/rust/bitbox02/src/securechip/imp_fake.rs
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
-use alloc::vec::Vec;
+use alloc::{boxed::Box, vec::Vec};
use bitbox_securechip::{Error, Model, PasswordStretchAlgo, SecureChipError};
use hex_lit::hex;
use hmac::{Hmac, Mac};
@@ -28,6 +28,10 @@ pub fn attestation_sign(_challenge: &[u8; 32], _signature: &mut [u8; 64]) -> Res
Err(())
}
+pub fn random() -> Result<Box<Zeroizing<[u8; 32]>>, Error> {
+ Ok(Box::new(Zeroizing::new([0u8; 32])))
+}
+
pub fn monotonic_increments_remaining() -> Result<u32, ()> {
Ok(1)
}
diff --git a/src/rust/bitbox03/src/random.rs b/src/rust/bitbox03/src/random.rs
index a76fa97..63029a3 100644
--- a/src/rust/bitbox03/src/random.rs
+++ b/src/rust/bitbox03/src/random.rs
@@ -3,7 +3,7 @@ use bitbox_hal as hal;
pub struct BitBox03Random;
impl hal::random::Random for BitBox03Random {
- fn random_32_bytes(&mut self) -> alloc::boxed::Box<zeroize::Zeroizing<[u8; 32]>> {
+ fn factory_randomness(&mut self) -> &'static [u8; 32] {
todo!()
}
diff --git a/src/rust/bitbox03/src/securechip.rs b/src/rust/bitbox03/src/securechip.rs
index 2ccd172..c7bbca4 100644
--- a/src/rust/bitbox03/src/securechip.rs
+++ b/src/rust/bitbox03/src/securechip.rs
@@ -3,6 +3,13 @@ use bitbox_hal as hal;
pub struct BitBox03SecureChip;
impl hal::securechip::SecureChip for BitBox03SecureChip {
+ fn random(
+ &mut self,
+ ) -> Result<alloc::boxed::Box<zeroize::Zeroizing<[u8; 32]>>, bitbox_hal::securechip::Error>
+ {
+ todo!()
+ }
+
fn init_new_password(
&mut self,
_password: &str,
diff --git a/test/simulator-graphical-bb03/src/hal/random.rs b/test/simulator-graphical-bb03/src/hal/random.rs
index d7ab636..a578b97 100644
--- a/test/simulator-graphical-bb03/src/hal/random.rs
+++ b/test/simulator-graphical-bb03/src/hal/random.rs
@@ -1,16 +1,13 @@
// SPDX-License-Identifier: Apache-2.0
-use alloc::boxed::Box;
use bitbox_hal as hal;
use rand::Rng;
pub struct BitBox03Random;
impl hal::random::Random for BitBox03Random {
- fn random_32_bytes(&mut self) -> Box<zeroize::Zeroizing<[u8; 32]>> {
- let mut out = Box::new(zeroize::Zeroizing::new([0u8; 32]));
- self.mcu_32_bytes(out.as_mut());
- out
+ fn factory_randomness(&mut self) -> &'static [u8; 32] {
+ &[0u8; 32]
}
fn mcu_32_bytes(&mut self, out: &mut [u8; 32]) {
Why this scored 45/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.