What changed, and why it matters
This commit adds a new factory-setup helper program for the BitBox03 hardware wallet and moves some debug logging code around. It does not change the main wallet firmware's security behavior. There is no indication this fixes or introduces a security vulnerability.
No security action required. Treat as normal feature/refactoring commit. If reviewing the factory-setup tool, ensure it is only used in a controlled manufacturing environment and cannot be triggered or left resident on end-user devices.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces a new Rust binary target, bitbox03-factorysetup, intended to run during device manufacturing to configure the STM32U5 non-secure boot address option bytes and provide a simple RTT echo channel. It also refactors bitbox-debug so that rtt-target is no longer a direct dependency of bitbox03-firmware and the shared RTT helper only initializes a logging channel. The factory-setup binary runs from RAM, uses unlocked flash/option-byte routines to write NSBOOTADD0, and then resets the device. No runtime safety checks, cryptographic operations, or user-facing firmware logic are modified.
Changed components
bitbox03-factorysetup (new manufacturing tool)bitbox-debug RTT initialization helperbitbox03-firmware Cargo.toml (dependency cleanup only)Inspect captured patch +243 / −37
### .github/workflows/ci-common.yml
@@ -212,6 +212,7 @@ jobs:
- firmware-debug
- bitbox03-boot0
- bitbox03-boot1
+ - bitbox03-factorysetup
- bitbox03-firmware
- simulator
- simulator-graphical
### Makefile
@@ -279,6 +279,14 @@ bitbox03-boot1-release:
python3 scripts/bitbox03_image_header.py finalize-elf src/rust/target/thumbv8m.main-none-eabihf/release/bitbox03-boot1
arm-none-eabi-size src/rust/target/thumbv8m.main-none-eabihf/release/bitbox03-boot1
arm-none-eabi-size -Ax src/rust/target/thumbv8m.main-none-eabihf/release/bitbox03-boot1
+bitbox03-factorysetup:
+ (cd src/rust; cargo bitbox03-factorysetup-stm32u5a9j-dk)
+ arm-none-eabi-size src/rust/target/thumbv8m.main-none-eabihf/debug/bitbox03-factorysetup
+ arm-none-eabi-size -Ax src/rust/target/thumbv8m.main-none-eabihf/debug/bitbox03-factorysetup
+bitbox03-factorysetup-release:
+ (cd src/rust; cargo bitbox03-factorysetup-stm32u5a9j-dk-release)
+ arm-none-eabi-size src/rust/target/thumbv8m.main-none-eabihf/release/bitbox03-factorysetup
+ arm-none-eabi-size -Ax src/rust/target/thumbv8m.main-none-eabihf/release/bitbox03-factorysetup
bitbox03-firmware:
(cd src/rust; cargo bitbox03-firmware-stm32u5a9j-dk)
python3 scripts/bitbox03_image_header.py finalize-elf src/rust/target/thumbv8m.main-none-eabihf/debug/bitbox03-firmware
### src/rust/.cargo/config.toml
@@ -26,5 +26,7 @@ bitbox03-boot0-stm32u5a9j-dk = "build -p bitbox03-boot0 --target=thumbv8m.main-n
bitbox03-boot0-stm32u5a9j-dk-release = "build -p bitbox03-boot0 --target=thumbv8m.main-none-eabihf --release --features board-stm32u5a9j-dk"
bitbox03-boot1-stm32u5a9j-dk = "build -p bitbox03-boot1 --target=thumbv8m.main-none-eabihf --features board-stm32u5a9j-dk,rtt"
bitbox03-boot1-stm32u5a9j-dk-release = "build -p bitbox03-boot1 --target=thumbv8m.main-none-eabihf --release --features board-stm32u5a9j-dk"
+bitbox03-factorysetup-stm32u5a9j-dk = "build -p bitbox03-factorysetup --target=thumbv8m.main-none-eabihf --features board-stm32u5a9j-dk"
+bitbox03-factorysetup-stm32u5a9j-dk-release = "build -p bitbox03-factorysetup --target=thumbv8m.main-none-eabihf --release --features board-stm32u5a9j-dk"
bitbox03-firmware-stm32u5a9j-dk = "build -p bitbox03-firmware --target=thumbv8m.main-none-eabihf --features board-stm32u5a9j-dk,rtt"
bitbox03-firmware-stm32u5a9j-dk-release = "build -p bitbox03-firmware --target=thumbv8m.main-none-eabihf --release --features board-stm32u5a9j-dk"
### src/rust/Cargo.lock
@@ -484,6 +484,20 @@ dependencies = [
"log",
]
+[[package]]
+name = "bitbox03-factorysetup"
+version = "0.1.0"
+dependencies = [
+ "bitbox-board-stm32u5a9j-dk",
+ "bitbox-board-stm32u5a9j-dk-build",
+ "bitbox-mcu-stm32u5",
+ "bitbox-platform-stm32u5",
+ "cortex-m",
+ "cortex-m-rt",
+ "log",
+ "rtt-target",
+]
+
[[package]]
name = "bitbox03-firmware"
version = "0.1.0"
@@ -496,7 +510,6 @@ dependencies = [
"cortex-m",
"cortex-m-rt",
"log",
- "rtt-target",
]
[[package]]
### src/rust/Cargo.toml
@@ -6,6 +6,7 @@ members = [
"async_test",
"bins/bitbox03-boot0",
"bins/bitbox03-boot1",
+ "bins/bitbox03-factorysetup",
"bins/bitbox03-firmware",
"bitbox-aes",
"bitbox-board-stm32u5a9j-dk",
### src/rust/bins/bitbox03-factorysetup/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "bitbox03-factorysetup"
+version = "0.1.0"
+edition = "2024"
+
+[[bin]]
+name = "bitbox03-factorysetup"
+path = "src/main.rs"
+test = false
+bench = false
+
+[features]
+board-stm32u5a9j-dk = [
+ "dep:bitbox-board-stm32u5a9j-dk",
+ "dep:bitbox-board-stm32u5a9j-dk-build",
+]
+
+[dependencies]
+bitbox-board-stm32u5a9j-dk = { path = "../../bitbox-board-stm32u5a9j-dk", optional = true }
+bitbox-mcu-stm32u5 = { path = "../../bitbox-mcu-stm32u5" }
+bitbox-platform-stm32u5 = { path = "../../bitbox-platform-stm32u5" }
+cortex-m = { workspace = true }
+cortex-m-rt = "0.7"
+log = { version = "0.4.22", default-features = false }
+rtt-target = { version = "0.6.2", features = ["log"] }
+
+[build-dependencies]
+bitbox-board-stm32u5a9j-dk-build = { path = "../../bitbox-board-stm32u5a9j-dk-build", optional = true }
+bitbox-board-stm32u5a9j-dk = { path = "../../bitbox-board-stm32u5a9j-dk", optional = true }
### src/rust/bins/bitbox03-factorysetup/bitbox03-factorysetup.ld
@@ -0,0 +1,5 @@
+INCLUDE memory.x
+REGION_ALIAS("FLASH", RAM_CODE);
+PROVIDE(_image_payload_start = ORIGIN(FLASH));
+
+INCLUDE bitbox03-common.ld
### src/rust/bins/bitbox03-factorysetup/build.rs
@@ -0,0 +1,62 @@
+use std::path::{Path, PathBuf};
+
+#[cfg(feature = "board-stm32u5a9j-dk")]
+fn generate_memory_x(out_dir: &Path) {
+ use bitbox_board_stm32u5a9j_dk::memory;
+
+ let contents = format!(
+ concat!(
+ "MEMORY\n",
+ "{{\n",
+ " BOOT_ARGS (xrw) : ORIGIN = 0x{:08X}, LENGTH = {}\n",
+ " RAM (xrw) : ORIGIN = 0x{:08X}, LENGTH = {}\n",
+ " RAM_CODE (xrw) : ORIGIN = 0x{:08X}, LENGTH = {}\n",
+ "}}\n",
+ ),
+ memory::BOOT_ARGS_ADDR,
+ memory::BOOT_ARGS_LEN,
+ memory::FACTORYSETUP_RAM_ADDR,
+ memory::FACTORYSETUP_RAM_LEN,
+ memory::FACTORYSETUP_RAM_CODE_ADDR,
+ memory::FACTORYSETUP_RAM_CODE_LEN,
+ );
+ std::fs::write(out_dir.join("memory.x"), contents).expect("write memory layout script");
+}
+
+fn build_board_hal_overrides_object(repo_root: &Path, out_dir: &Path) {
+ #[cfg(feature = "board-stm32u5a9j-dk")]
+ bitbox_board_stm32u5a9j_dk_build::build_hal_overrides_object(repo_root, out_dir);
+ #[cfg(not(feature = "board-stm32u5a9j-dk"))]
+ let _ = (repo_root, out_dir);
+}
+
+fn main() {
+ if !cfg!(feature = "board-stm32u5a9j-dk") {
+ panic!("select a BitBox03 board feature, e.g. `board-stm32u5a9j-dk`")
+ }
+
+ let target = std::env::var("TARGET").expect("TARGET not set");
+ if target.starts_with("thumb") {
+ let manifest_dir =
+ PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
+ let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR not set"));
+ let repo_root = manifest_dir.join("../../../..");
+
+ let lds_from = manifest_dir.join("bitbox03-factorysetup.ld");
+ let lds_to = out_dir.join("bitbox03-factorysetup.ld");
+ println!("cargo::rerun-if-changed={}", lds_from.display());
+ std::fs::copy(lds_from, &lds_to).expect("copy linker script");
+
+ generate_memory_x(&out_dir);
+
+ println!("cargo::rustc-link-search={}", out_dir.display());
+ println!(
+ "cargo::rustc-link-arg=-Map={}",
+ out_dir.join("bitbox03-factorysetup.map").display()
+ );
+
+ println!("cargo::rustc-link-arg=-Tbitbox03-factorysetup.ld");
+
+ build_board_hal_overrides_object(&repo_root, &out_dir);
+ }
+}
### src/rust/bins/bitbox03-factorysetup/src/main.rs
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#![no_std]
+#![no_main]
+
+#[cfg(feature = "board-stm32u5a9j-dk")]
+use bitbox_board_stm32u5a9j_dk as board;
+use bitbox_mcu_stm32u5 as _;
+use bitbox_platform_stm32u5 as _;
+use bitbox_platform_stm32u5::flash::{self, BootAddressConfig};
+#[cfg(feature = "board-stm32u5a9j-dk")]
+use board::ffi;
+use core::panic::PanicInfo;
+use cortex_m_rt::entry;
+
+const BOOT0_ADDR: u32 = board::memory::BOOT0_ADDR as u32;
+const API_REQUEST_LEN: usize = 512;
+const ECHO_PREFIX: &[u8] = b"echo: ";
+
+#[panic_handler]
+fn panic(_info: &PanicInfo) -> ! {
+ halt()
+}
+
+fn halt() -> ! {
+ cortex_m::asm::bkpt();
+ loop {
+ cortex_m::asm::wfe();
+ }
+}
+
+fn current_nsbootadd0() -> u32 {
+ flash::boot_address(BootAddressConfig::NonSecure0)
+}
+
+fn program_boot0_nsbootadd0(addr: u32) -> flash::Result<()> {
+ let mut unlocked_flash = flash::UnlockedFlash::unlock()?;
+ let mut option_bytes = unlocked_flash.unlock_option_bytes()?;
+ option_bytes.program_boot_address(BootAddressConfig::NonSecure0, addr)?;
+
+ // Launch will issue a system reset on success. This needs to be the last thing the factory
+ // setup does. OB_Launch does not work if flash or option bytes are locked.
+ log::info!("launching option bytes; device will reset");
+ option_bytes.launch()
+}
+
+#[entry]
+fn main() -> ! {
+ board::init();
+ let mut channels = rtt_target::rtt_init! {
+ up: {
+ 0: {
+ size: 1024,
+ mode: rtt_target::ChannelMode::NoBlockSkip,
+ name: "Terminal",
+ section: ".segger_rtt_buf",
+ }
+ 1: {
+ size: 1024,
+ mode: rtt_target::ChannelMode::NoBlockSkip,
+ name: "API Response",
+ section: ".segger_rtt_buf",
+ }
+ }
+ down: {
+ // OpenOCD maps channel ids to TCP ports, so keep channel 0 defined even though the
+ // API uses channel 1.
+ 0: {
+ size: 16,
+ mode: rtt_target::ChannelMode::NoBlockSkip,
+ name: "Terminal",
+ section: ".segger_rtt_buf",
+ }
+ 1: {
+ size: 1024,
+ mode: rtt_target::ChannelMode::NoBlockSkip,
+ name: "API Request",
+ section: ".segger_rtt_buf",
+ }
+ }
+ section_cb: ".segger_rtt"
+ };
+ rtt_target::set_print_channel(channels.up.0);
+ rtt_target::init_logger_with_level(log::LevelFilter::Trace);
+ let nsbootadd0 = current_nsbootadd0();
+ if nsbootadd0 != BOOT0_ADDR {
+ log::info!(
+ "writing nsbootadd0 from {:#010x} to {:#010x}",
+ nsbootadd0,
+ BOOT0_ADDR
+ );
+ if program_boot0_nsbootadd0(BOOT0_ADDR).is_err() {
+ halt();
+ }
+ }
+
+ log::info!("nsbootadd0 OK, {:#010x}", nsbootadd0);
+ let mut read_buf = [0u8; API_REQUEST_LEN];
+ let mut response_buf = [0u8; ECHO_PREFIX.len() + API_REQUEST_LEN];
+ loop {
+ let read = channels.down.1.read(&mut read_buf);
+ if read > 0 {
+ response_buf[..ECHO_PREFIX.len()].copy_from_slice(ECHO_PREFIX);
+ response_buf[ECHO_PREFIX.len()..ECHO_PREFIX.len() + read]
+ .copy_from_slice(&read_buf[..read]);
+
+ let response_len = ECHO_PREFIX.len() + read;
+ let written = channels.up.1.write(&response_buf[..response_len]);
+ if written != response_len {
+ log::warn!("short API response write: {written}");
+ }
+ }
+ unsafe {
+ ffi::HAL_Delay(100);
+ }
+ }
+}
### src/rust/bins/bitbox03-firmware/Cargo.toml
@@ -14,7 +14,7 @@ board-stm32u5a9j-dk = [
"dep:bitbox-board-stm32u5a9j-dk",
"dep:bitbox-board-stm32u5a9j-dk-build",
]
-rtt = ["bitbox-debug/rtt", "dep:rtt-target"]
+rtt = ["bitbox-debug/rtt"]
[dependencies]
bitbox-board-stm32u5a9j-dk = { path = "../../bitbox-board-stm32u5a9j-dk", optional = true }
@@ -24,7 +24,6 @@ bitbox-debug = { path = "../../bitbox-debug" }
cortex-m-rt = "0.7"
cortex-m = { workspace = true }
log = { version = "0.4.22", default-features = false }
-rtt-target = { version = "0.6.2", features = ["log"], optional = true }
[build-dependencies]
bitbox-board-stm32u5a9j-dk-build = { path = "../../bitbox-board-stm32u5a9j-dk-build", optional = true }
### src/rust/bitbox-debug/src/lib.rs
@@ -9,14 +9,9 @@ pub use log;
#[cfg(all(feature = "rtt", target_os = "none"))]
pub use rtt_target;
+#[doc(hidden)]
#[cfg(all(feature = "rtt", target_os = "none"))]
-pub struct RttChannels {
- pub api_response: rtt_target::UpChannel,
- pub api_request: rtt_target::DownChannel,
-}
-
-#[cfg(all(feature = "rtt", target_os = "none"))]
-pub fn rtt_channels_init() -> RttChannels {
+pub fn rtt_logger_init() {
let channels = rtt_target::rtt_init! {
up: {
0: {
@@ -25,46 +20,20 @@ pub fn rtt_channels_init() -> RttChannels {
name: "Terminal",
section: ".segger_rtt_buf",
}
- 1: {
- size: 1024,
- mode: rtt_target::ChannelMode::NoBlockSkip,
- name: "API Response",
- section: ".segger_rtt_buf",
- }
- }
- down: {
- // OpenOCD maps channel ids to TCP ports, so keep channel 0 defined even though the
- // API uses channel 1.
- 0: {
- size: 16,
- mode: rtt_target::ChannelMode::NoBlockSkip,
- name: "Terminal",
- section: ".segger_rtt_buf",
- }
- 1: {
- size: 1024,
- mode: rtt_target::ChannelMode::NoBlockSkip,
- name: "API Request",
- section: ".segger_rtt_buf",
- }
}
section_cb: ".segger_rtt"
reuse_if_initialized: true
};
rtt_target::set_print_channel(channels.up.0);
rtt_target::init_logger_with_level(log::LevelFilter::Trace);
- RttChannels {
- api_response: channels.up.1,
- api_request: channels.down.1,
- }
}
#[macro_export]
macro_rules! rtt_logger_init {
() => {{
#[cfg(all(feature = "rtt", target_os = "none"))]
{
- let _ = $crate::rtt_channels_init();
+ $crate::rtt_logger_init();
}
}};
}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.