What changed, and why it matters
This commit adds a new first-stage bootloader component called bitbox03-boot1 for the BitBox03 hardware wallet. It is purely additive: it wires a new Rust binary into the build system, adds a flashing script, and provides a small program that validates a firmware image header and then jumps to the main firmware. Nothing in the commit changes existing security behavior or fixes a known vulnerability.
No security action required. Treat as normal feature/enablement commit. If reviewing for secure boot design, verify that vector_table_from_image_header and bootload enforce the expected cryptographic signature and downgrade-protection checks elsewhere, since this commit only invokes them.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces src/rust/bins/bitbox03-boot1, a new no_std Rust bootloader binary for the STM32U5A9J-DK board. It adds Cargo workspace membership, cargo aliases, Makefile targets, a CI build matrix entry, an OpenOCD flashing script, a linker script placing a 1024-byte image header before the vector table, a build.rs that renders the header from image_header.json, and a main.rs that calls vector_table_from_image_header() on the fixed firmware slot and then bootload(). The magic string in image_header.json is ‘BBB1’. The code relies on existing bitbox-boot-utils validation routines and does not alter them.
Changed components
BitBox03 bootloader (new bitbox03-boot1 component)CI workflow ci-common.ymlMakefile build targetssrc/rust workspaceInspect captured patch +224 / −0
### .github/workflows/ci-common.yml
@@ -211,6 +211,7 @@ jobs:
- factory-setup
- firmware-debug
- bitbox03-boot0
+ - bitbox03-boot1
- simulator
- simulator-graphical
- simulator-graphical-bb03
### Makefile
@@ -269,6 +269,18 @@ bitbox03-boot0-release:
(cd src/rust; cargo bitbox03-boot0-stm32u5a9j-dk-release)
arm-none-eabi-size src/rust/target/thumbv8m.main-none-eabihf/release/bitbox03-boot0
arm-none-eabi-size -Ax src/rust/target/thumbv8m.main-none-eabihf/release/bitbox03-boot0
+bitbox03-boot1:
+ (cd src/rust; cargo bitbox03-boot1-stm32u5a9j-dk)
+ python3 scripts/bitbox03_image_header.py finalize-elf src/rust/target/thumbv8m.main-none-eabihf/debug/bitbox03-boot1
+ arm-none-eabi-size src/rust/target/thumbv8m.main-none-eabihf/debug/bitbox03-boot1
+ arm-none-eabi-size -Ax src/rust/target/thumbv8m.main-none-eabihf/debug/bitbox03-boot1
+bitbox03-boot1-release:
+ (cd src/rust; cargo bitbox03-boot1-stm32u5a9j-dk-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
flash-bitbox03-boot0-openocd:
./scripts/flash-bitbox03-boot0-openocd.sh
+flash-bitbox03-boot1-openocd:
+ ./scripts/flash-bitbox03-boot1-openocd.sh
### scripts/flash-bitbox03-boot1-openocd.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+image="${1:-$repo_root/src/rust/target/thumbv8m.main-none-eabihf/debug/bitbox03-boot1}"
+config="$repo_root/scripts/stm32u5a9j-dk.cfg"
+
+if [[ ! -f "$image" ]]; then
+ echo "image not found: $image" >&2
+ exit 1
+fi
+
+exec openocd \
+ -f "$config" \
+ -c "program \"$image\" verify reset exit"
### src/rust/.cargo/config.toml
@@ -24,3 +24,5 @@ directory = "../../external/vendor"
[alias]
bitbox03-boot0-stm32u5a9j-dk = "build -p bitbox03-boot0 --target=thumbv8m.main-none-eabihf --features board-stm32u5a9j-dk,rtt"
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"
### src/rust/Cargo.lock
@@ -472,6 +472,18 @@ dependencies = [
"log",
]
+[[package]]
+name = "bitbox03-boot1"
+version = "0.1.0"
+dependencies = [
+ "bitbox-board-stm32u5a9j-dk",
+ "bitbox-boot-utils",
+ "bitbox-debug",
+ "bitbox-platform-stm32u5",
+ "cortex-m-rt",
+ "log",
+]
+
[[package]]
name = "bitcoin"
version = "0.32.7"
### src/rust/Cargo.toml
@@ -5,6 +5,7 @@
members = [
"async_test",
"bins/bitbox03-boot0",
+ "bins/bitbox03-boot1",
"bitbox-aes",
"bitbox-board-stm32u5a9j-dk",
"bitbox-board-stm32u5a9j-dk-build",
### src/rust/bins/bitbox03-boot1/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "bitbox03-boot1"
+version = "0.1.0"
+edition = "2024"
+
+[[bin]]
+name = "bitbox03-boot1"
+path = "src/main.rs"
+test = false
+bench = false
+
+[features]
+board-stm32u5a9j-dk = ["dep:bitbox-board-stm32u5a9j-dk"]
+rtt = ["bitbox-debug/rtt"]
+
+[dependencies]
+bitbox-boot-utils = { path = "../../bitbox-boot-utils" }
+bitbox-board-stm32u5a9j-dk = { path = "../../bitbox-board-stm32u5a9j-dk", optional = true }
+bitbox-debug = { path = "../../bitbox-debug" }
+bitbox-platform-stm32u5 = { path = "../../bitbox-platform-stm32u5" }
+cortex-m-rt = "0.7"
+log = { version = "0.4.22", default-features = false }
### src/rust/bins/bitbox03-boot1/bitbox03-boot1.ld
@@ -0,0 +1,27 @@
+INCLUDE memory.x
+REGION_ALIAS("FLASH", BOOT1_FLASH);
+
+IMAGE_HEADER_LEN = 1024;
+
+SECTIONS
+{
+ .image_header ORIGIN(FLASH) :
+ {
+ . = ALIGN(4);
+ KEEP(*(.image_header .image_header.*));
+ . = ALIGN(4);
+ } > FLASH :flash
+} INSERT BEFORE .vector_table;
+
+PROVIDE(_image_payload_start = ORIGIN(FLASH) + IMAGE_HEADER_LEN);
+
+INCLUDE bitbox03-common.ld
+
+ASSERT(ADDR(.image_header) == ORIGIN(FLASH), "
+ERROR(cortex-m-rt): The image header must start at the beginning of the FLASH memory.");
+
+ASSERT(SIZEOF(.image_header) == IMAGE_HEADER_LEN, "
+ERROR(cortex-m-rt): The image header must exactly fill the reserved header space.");
+
+ASSERT(ADDR(.vector_table) == ORIGIN(FLASH) + IMAGE_HEADER_LEN, "
+ERROR(cortex-m-rt): The vector table must be placed immediately after the image header.");
### src/rust/bins/bitbox03-boot1/build.rs
@@ -0,0 +1,78 @@
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
+fn run_command(command: &mut Command, description: &str) {
+ let status = command.status().unwrap_or_else(|err| {
+ panic!("failed to execute {description}: {err}");
+ });
+ assert!(
+ status.success(),
+ "{description} failed with status {status}"
+ );
+}
+
+fn generate_header_object(manifest_dir: &Path, out_dir: &Path) {
+ let repo_root = manifest_dir.join("../../../..");
+ let script = repo_root.join("scripts/bitbox03_image_header.py");
+ let header_manifest = manifest_dir.join("image_header.json");
+ let header_bin = out_dir.join("bitbox03-boot1-header.bin");
+ let header_object = out_dir.join("bitbox03-boot1-header.o");
+
+ println!("cargo::rerun-if-changed={}", script.display());
+ println!("cargo::rerun-if-changed={}", header_manifest.display());
+
+ run_command(
+ Command::new("python3")
+ .arg(&script)
+ .arg("render-header")
+ .arg("--manifest")
+ .arg(&header_manifest)
+ .arg("--output")
+ .arg(&header_bin),
+ "render boot1 image header",
+ );
+
+ run_command(
+ Command::new("arm-none-eabi-objcopy")
+ .arg("-I")
+ .arg("binary")
+ .arg("-O")
+ .arg("elf32-littlearm")
+ .arg("-B")
+ .arg("arm")
+ .arg("--rename-section")
+ .arg(".data=.image_header,alloc,load,readonly,data,contents")
+ .arg(&header_bin)
+ .arg(&header_object),
+ "convert boot1 image header to object",
+ );
+
+ println!("cargo::rustc-link-arg={}", header_object.display());
+}
+
+fn main() {
+ let target = std::env::var("TARGET").expect("TARGET not set");
+
+ if target.starts_with("thumb") {
+ if !cfg!(feature = "board-stm32u5a9j-dk") {
+ panic!("select a BitBox03 board feature, e.g. `board-stm32u5a9j-dk`")
+ }
+
+ 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 lds_from = manifest_dir.join("bitbox03-boot1.ld");
+ let lds_to = out_dir.join("bitbox03-boot1.ld");
+ println!("cargo::rerun-if-changed={}", lds_from.display());
+ std::fs::copy(lds_from, &lds_to).expect("copy linker script");
+
+ generate_header_object(&manifest_dir, &out_dir);
+
+ println!("cargo::rustc-link-search={}", out_dir.display());
+ println!("cargo::rustc-link-arg=-Tbitbox03-boot1.ld");
+ println!(
+ "cargo::rustc-link-arg=-Map={}",
+ out_dir.join("bitbox03-boot1.map").display()
+ );
+ }
+}
### src/rust/bins/bitbox03-boot1/image_header.json
@@ -0,0 +1,3 @@
+{
+ "magic": "BBB1"
+}
### src/rust/bins/bitbox03-boot1/src/main.rs
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#![no_std]
+#![no_main]
+
+#[cfg(feature = "board-stm32u5a9j-dk")]
+use bitbox_board_stm32u5a9j_dk as board;
+use bitbox_boot_utils::{
+ IMAGE_HEADER_MAGIC_FIRMWARE, bootload, halt, vector_table_from_image_header,
+};
+use core::panic::PanicInfo;
+use cortex_m_rt::entry;
+
+#[panic_handler]
+fn panic(info: &PanicInfo) -> ! {
+ log::error!("{info}");
+ halt();
+}
+
+fn firmware_vector_table() -> Result<*const u32, ()> {
+ // SAFETY: The board memory layout defines FIRMWARE_ADDR as an aligned,
+ // readable flash slot of FIRMWARE_MAX_LEN bytes. Boot1 does not mutate it
+ // while validating the image.
+ unsafe {
+ vector_table_from_image_header(
+ board::memory::FIRMWARE_ADDR,
+ board::memory::FIRMWARE_MAX_LEN,
+ board::memory::RAM_ADDR,
+ board::memory::RAM_LEN,
+ IMAGE_HEADER_MAGIC_FIRMWARE,
+ )
+ }
+}
+
+#[entry]
+fn main() -> ! {
+ bitbox_debug::rtt_logger_init!();
+ log::debug!("init");
+
+ let vector_table = match firmware_vector_table() {
+ Ok(vector_table) => vector_table,
+ Err(()) => {
+ log::error!("halt: firmware image is invalid");
+ halt();
+ }
+ };
+ // SAFETY: Boot1 deliberately trusts the firmware at the fixed slot after
+ // checking its image header and vector table, including the stack pointer and
+ // reset vector ranges.
+ unsafe { bootload(vector_table) }
+}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.