What changed, and why it matters
This commit is a straightforward code reorganization: it moves the U2F C API module from one Rust crate (bitbox02-rust) to another (bitbox02-rust-c). The actual logic of the U2F workflow functions is copied almost unchanged, with only import paths adjusted. There is no indication in the commit message or diff that this fixes a security bug, changes behavior, or addresses a reported vulnerability. It appears to be a routine refactoring to improve code structure.
No security action required. Treat as normal refactoring. If reviewing for release, verify that the moved module still compiles under all feature combinations (app-u2f, c-unit-testing, simulator-graphical, firmware) and that no callers depend on the old module path.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch relocates src/rust/bitbox02-rust/src/workflow/u2f_c_api.rs to src/rust/bitbox02-rust-c/src/u2f_c_api.rs and moves the corresponding stub module. Cargo.toml dependencies (grounded, bitbox-hal) are shifted accordingly. The C API functions (rust_workflow_spawn_unlock, rust_workflow_spawn_confirm, rust_workflow_*_poll, rust_workflow_abort_current) and their internal state management remain functionally identical. Safety documentation is preserved. No logic changes or bug fixes are visible.
Changed components
src/rust/bitbox02-rust-c/src/u2f_c_api.rssrc/rust/bitbox02-rust-c/src/u2f_c_api_stubs.rssrc/rust/bitbox02-rust/src/workflow.rssrc/rust/bitbox02-rust/src/workflow/u2f_c_api.rssrc/rust/bitbox02-rust-c/Cargo.tomlsrc/rust/bitbox02-rust/Cargo.tomlInspect captured patch +226 / −221
diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock
index 5633c2b..d96d381 100644
--- a/src/rust/Cargo.lock
+++ b/src/rust/Cargo.lock
@@ -184,7 +184,6 @@ dependencies = [
"erc20_params",
"fatfs-sys",
"futures-lite",
- "grounded",
"hex",
"hex_lit",
"hmac",
@@ -210,6 +209,7 @@ dependencies = [
"bip39",
"bitbox-aes",
"bitbox-framed-serial-link",
+ "bitbox-hal",
"bitbox02",
"bitbox02-noise",
"bitbox02-rust",
@@ -217,6 +217,7 @@ dependencies = [
"cortex-m",
"der",
"digest",
+ "grounded",
"hex",
"sha2",
"util",
diff --git a/src/rust/bitbox02-rust-c/Cargo.toml b/src/rust/bitbox02-rust-c/Cargo.toml
index 2f8f6f6..eb19eb1 100644
--- a/src/rust/bitbox02-rust-c/Cargo.toml
+++ b/src/rust/bitbox02-rust-c/Cargo.toml
@@ -11,12 +11,14 @@ license = "Apache-2.0"
bitbox02-rust = { path = "../bitbox02-rust", optional = true }
bitbox-aes = { path = "../bitbox-aes", optional = true }
bitbox02 = { path = "../bitbox02", optional = true }
+bitbox-hal = { path = "../bitbox-hal" }
bitbox02-noise = { path = "../bitbox02-noise", optional = true }
cortex-m = { workspace = true }
util = { path = "../util" }
bitbox-framed-serial-link = { path = "../bitbox-framed-serial-link" }
der = { version = "0.7.9", default-features = false, optional = true }
hex = { workspace = true }
+grounded = { workspace = true }
sha2 = { workspace = true, optional = true }
digest = { workspace = true, optional = true }
bitcoin = { workspace = true, optional = true }
diff --git a/src/rust/bitbox02-rust-c/src/lib.rs b/src/rust/bitbox02-rust-c/src/lib.rs
index 3e57597..57d267d 100644
--- a/src/rust/bitbox02-rust-c/src/lib.rs
+++ b/src/rust/bitbox02-rust-c/src/lib.rs
@@ -18,6 +18,16 @@ mod firmware_c_api;
#[cfg(feature = "factory-setup")]
mod secp256k1;
+#[cfg(feature = "app-u2f")]
+// Stubs for C unit tests and C simulator - these are currently compiled and linked but they don't
+// actually have to spawn/poll futures. The C simulator does not contain U2F, and the unit tests
+// don't contain an executor.
+#[cfg_attr(
+ any(feature = "c-unit-testing", feature = "simulator-graphical"),
+ path = "u2f_c_api_stubs.rs"
+)]
+mod u2f_c_api;
+
// Expose C interface defined in bitbox_aes
#[cfg(feature = "firmware")]
extern crate bitbox_aes;
@@ -36,6 +46,10 @@ extern crate bitbox_framed_serial_link;
// Expose C interface defined in util
extern crate util;
+#[allow(unused)]
+#[cfg(feature = "firmware")]
+type HalImpl = bitbox02::hal::BitBox02Hal;
+
// Whenever execution reaches somewhere it isn't supposed to rust code will "panic". Our panic
// handler will print the available information on the screen and over RTT. If we compile with
// `panic=abort` this code will never get executed.
diff --git a/src/rust/bitbox02-rust-c/src/u2f_c_api.rs b/src/rust/bitbox02-rust-c/src/u2f_c_api.rs
new file mode 100644
index 0000000..465a51f
--- /dev/null
+++ b/src/rust/bitbox02-rust-c/src/u2f_c_api.rs
@@ -0,0 +1,177 @@
+// SPDX-License-Identifier: Apache-2.0
+
+//! This module is a quick workaround to use async workflows from U2F/FIDO2, where the root of the
+//! usb message proessing is not ported to Rust. If that happens, the `async_usb` module can be
+//! used and this can be deleted.
+
+extern crate alloc;
+
+use alloc::boxed::Box;
+use alloc::string::String;
+use bitbox_hal::ui::{ConfirmParams, UserAbort};
+use bitbox_hal::{Hal, Ui};
+use core::ffi::CStr;
+use core::sync::atomic::{AtomicU32, Ordering};
+use grounded::const_init::ConstInit;
+use grounded::uninit::GroundedCell;
+
+enum TaskState<O> {
+ Nothing,
+ Running(u32),
+ ResultAvailable(O),
+}
+
+impl<O> ConstInit for TaskState<O> {
+ const VAL: Self = Self::Nothing;
+}
+
+static NEXT_TASK_TOKEN: AtomicU32 = AtomicU32::new(0);
+static UNLOCK_STATE: GroundedCell<TaskState<Result<(), ()>>> = GroundedCell::const_init();
+static CONFIRM_STATE: GroundedCell<TaskState<Result<(), UserAbort>>> = GroundedCell::const_init();
+static BITBOX02_HAL: GroundedCell<crate::HalImpl> = GroundedCell::const_init();
+
+fn next_task_token() -> u32 {
+ NEXT_TASK_TOKEN.fetch_add(1, Ordering::Relaxed)
+}
+
+/// # Safety
+/// Must not be called concurrently or reentrantly with other operations that mutate unlock
+/// workflow state in this module.
+/// Callers must guarantee single-threaded access to this workflow.
+unsafe fn complete_unlock(token: u32, result: Result<(), ()>) {
+ unsafe {
+ if let TaskState::Running(current_token) = UNLOCK_STATE.get().as_ref().unwrap()
+ && *current_token == token
+ {
+ UNLOCK_STATE.get().write(TaskState::ResultAvailable(result));
+ }
+ }
+}
+
+/// # Safety
+/// Must not be called concurrently or reentrantly with other operations that mutate confirm
+/// workflow state in this module.
+/// Callers must guarantee single-threaded access to this workflow.
+unsafe fn complete_confirm(token: u32, result: Result<(), UserAbort>) {
+ unsafe {
+ if let TaskState::Running(current_token) = CONFIRM_STATE.get().as_ref().unwrap()
+ && *current_token == token
+ {
+ CONFIRM_STATE
+ .get()
+ .write(TaskState::ResultAvailable(result));
+ }
+ }
+}
+
+/// # Safety
+/// Must be called from the same single-threaded, non-reentrant execution context as all other
+/// U2F workflow C API calls. In particular, do not call this from interrupts or from multiple
+/// threads.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_workflow_spawn_unlock() {
+ let token = next_task_token();
+ unsafe {
+ UNLOCK_STATE.get().write(TaskState::Running(token));
+ }
+ bitbox02_rust::main_loop::spawn(Box::pin(async move {
+ let result = unsafe {
+ bitbox02_rust::workflow::unlock::unlock(BITBOX02_HAL.get().as_mut().unwrap()).await
+ };
+ unsafe { complete_unlock(token, result) };
+ }));
+}
+
+/// # Safety
+/// `title` and `body` must be valid non-null pointers to NUL-terminated UTF-8 strings, readable
+/// for the duration of this call.
+///
+/// This must be called from the same single-threaded, non-reentrant execution context as all
+/// other U2F workflow C API calls (no interrupts/multi-threaded callers).
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_workflow_spawn_confirm(
+ title: *const core::ffi::c_char,
+ body: *const core::ffi::c_char,
+) {
+ let title: String = unsafe { CStr::from_ptr(title).to_str().unwrap().into() };
+ let body: String = unsafe { CStr::from_ptr(body).to_str().unwrap().into() };
+ let token = next_task_token();
+ unsafe {
+ CONFIRM_STATE.get().write(TaskState::Running(token));
+ }
+ bitbox02_rust::main_loop::spawn(Box::pin(async move {
+ let params = ConfirmParams {
+ title: &title,
+ body: &body,
+ accept_only: true,
+ ..Default::default()
+ };
+ let result = unsafe {
+ BITBOX02_HAL
+ .get()
+ .as_mut()
+ .unwrap()
+ .ui()
+ .confirm(¶ms)
+ .await
+ };
+ unsafe { complete_confirm(token, result) };
+ }));
+}
+
+/// Returns true if there was a result.
+///
+/// # Safety
+/// `result_out` must be a valid, non-null writable pointer to a `bool` for the duration of this
+/// call.
+///
+/// This must be called from the same single-threaded, non-reentrant execution context as all
+/// other U2F workflow C API calls (no interrupts/multi-threaded callers).
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_workflow_unlock_poll(result_out: &mut bool) -> bool {
+ unsafe {
+ match UNLOCK_STATE.get().as_ref().unwrap() {
+ TaskState::ResultAvailable(result) => {
+ *result_out = result.is_ok();
+ UNLOCK_STATE.get().write(TaskState::Nothing);
+ true
+ }
+ TaskState::Running(_) => false,
+ TaskState::Nothing => panic!("polled non-existing future"),
+ }
+ }
+}
+
+/// Returns true if there was a result.
+///
+/// # Safety
+/// `result_out` must be a valid, non-null writable pointer to a `bool` for the duration of this
+/// call.
+///
+/// This must be called from the same single-threaded, non-reentrant execution context as all
+/// other U2F workflow C API calls (no interrupts/multi-threaded callers).
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_workflow_confirm_poll(result_out: &mut bool) -> bool {
+ unsafe {
+ match CONFIRM_STATE.get().as_ref().unwrap() {
+ TaskState::ResultAvailable(result) => {
+ CONFIRM_STATE.get().write(TaskState::Nothing);
+ *result_out = result.is_ok();
+ true
+ }
+ TaskState::Running(_) => false,
+ TaskState::Nothing => false,
+ }
+ }
+}
+
+/// # Safety
+/// Must be called from the same single-threaded, non-reentrant execution context as all other
+/// U2F workflow C API calls (no interrupts/multi-threaded callers).
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_workflow_abort_current() {
+ unsafe {
+ UNLOCK_STATE.get().write(TaskState::Nothing);
+ CONFIRM_STATE.get().write(TaskState::Nothing);
+ }
+}
diff --git a/src/rust/bitbox02-rust-c/src/u2f_c_api_stubs.rs b/src/rust/bitbox02-rust-c/src/u2f_c_api_stubs.rs
new file mode 100644
index 0000000..aacd011
--- /dev/null
+++ b/src/rust/bitbox02-rust-c/src/u2f_c_api_stubs.rs
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_workflow_spawn_unlock() {}
+
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_workflow_spawn_confirm(
+ _title: *const core::ffi::c_char,
+ _body: *const core::ffi::c_char,
+) {
+ panic!("unused");
+}
+
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_workflow_unlock_poll(_result_out: &mut bool) -> bool {
+ panic!("unused");
+}
+
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_workflow_confirm_poll(_result_out: &mut bool) -> bool {
+ panic!("unused");
+}
+
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_workflow_abort_current() {
+ panic!("unused");
+}
diff --git a/src/rust/bitbox02-rust/Cargo.toml b/src/rust/bitbox02-rust/Cargo.toml
index 27354cc..aa40fc7 100644
--- a/src/rust/bitbox02-rust/Cargo.toml
+++ b/src/rust/bitbox02-rust/Cargo.toml
@@ -50,7 +50,6 @@ bip39 = { workspace = true }
bitcoin_hashes = { version = "0.14.0", default-features = false, features = ["small-hash"] }
futures-lite = { workspace = true }
hex_lit = { workspace = true, features = ["rust_v_1_46"] }
-grounded = { workspace = true }
[dependencies.prost]
# keep version in sync with tools/prost-build/Cargo.toml.
diff --git a/src/rust/bitbox02-rust/src/workflow.rs b/src/rust/bitbox02-rust/src/workflow.rs
index 43b1524..5fd920f 100644
--- a/src/rust/bitbox02-rust/src/workflow.rs
+++ b/src/rust/bitbox02-rust/src/workflow.rs
@@ -12,43 +12,3 @@ pub mod transaction;
pub mod unlock;
pub mod unlock_animation;
pub mod verify_message;
-
-// Active in production firmware.
-#[cfg(all(
- feature = "app-u2f",
- not(any(feature = "c-unit-testing", feature = "simulator-graphical"))
-))]
-pub mod u2f_c_api;
-
-// Stubs for C unit tests and C simulator - these are currently compiled and linked but they don't
-// actually have to spawn/poll futures. The C simulator does not contain U2F, and the unit tests
-// don't contain an executor.
-#[cfg(all(
- feature = "app-u2f",
- any(feature = "c-unit-testing", feature = "simulator-graphical")
-))]
-pub mod u2f_c_api {
- #![allow(clippy::missing_safety_doc)]
-
- #[unsafe(no_mangle)]
- pub unsafe extern "C" fn rust_workflow_spawn_unlock() {}
- #[unsafe(no_mangle)]
- pub unsafe extern "C" fn rust_workflow_spawn_confirm(
- _title: *const core::ffi::c_char,
- _body: *const core::ffi::c_char,
- ) {
- panic!("unused");
- }
- #[unsafe(no_mangle)]
- pub unsafe extern "C" fn rust_workflow_unlock_poll(_result_out: &mut bool) -> bool {
- panic!("unused");
- }
- #[unsafe(no_mangle)]
- pub unsafe extern "C" fn rust_workflow_confirm_poll(_result_out: &mut bool) -> bool {
- panic!("unused");
- }
- #[unsafe(no_mangle)]
- pub unsafe extern "C" fn rust_workflow_abort_current() {
- panic!("unused");
- }
-}
diff --git a/src/rust/bitbox02-rust/src/workflow/u2f_c_api.rs b/src/rust/bitbox02-rust/src/workflow/u2f_c_api.rs
deleted file mode 100644
index d03b8ce..0000000
--- a/src/rust/bitbox02-rust/src/workflow/u2f_c_api.rs
+++ /dev/null
@@ -1,177 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-
-//! This module is a quick workaround to use async workflows from U2F/FIDO2, where the root of the
-//! usb message proessing is not ported to Rust. If that happens, the `async_usb` module can be
-//! used and this can be deleted.
-
-extern crate alloc;
-
-use crate::hal::ui::ConfirmParams;
-use crate::hal::{Hal, Ui};
-use alloc::boxed::Box;
-use alloc::string::String;
-use core::ffi::CStr;
-use core::sync::atomic::{AtomicU32, Ordering};
-use grounded::const_init::ConstInit;
-use grounded::uninit::GroundedCell;
-
-enum TaskState<O> {
- Nothing,
- Running(u32),
- ResultAvailable(O),
-}
-
-impl<O> ConstInit for TaskState<O> {
- const VAL: Self = Self::Nothing;
-}
-
-static NEXT_TASK_TOKEN: AtomicU32 = AtomicU32::new(0);
-static UNLOCK_STATE: GroundedCell<TaskState<Result<(), ()>>> = GroundedCell::const_init();
-static CONFIRM_STATE: GroundedCell<TaskState<Result<(), crate::hal::ui::UserAbort>>> =
- GroundedCell::const_init();
-static BITBOX02_HAL: GroundedCell<crate::hal::BitBox02Hal> = GroundedCell::const_init();
-
-fn next_task_token() -> u32 {
- NEXT_TASK_TOKEN.fetch_add(1, Ordering::Relaxed)
-}
-
-/// # Safety
-/// Must not be called concurrently or reentrantly with other operations that mutate unlock
-/// workflow state in this module.
-/// Callers must guarantee single-threaded access to this workflow.
-unsafe fn complete_unlock(token: u32, result: Result<(), ()>) {
- unsafe {
- if let TaskState::Running(current_token) = UNLOCK_STATE.get().as_ref().unwrap()
- && *current_token == token
- {
- UNLOCK_STATE.get().write(TaskState::ResultAvailable(result));
- }
- }
-}
-
-/// # Safety
-/// Must not be called concurrently or reentrantly with other operations that mutate confirm
-/// workflow state in this module.
-/// Callers must guarantee single-threaded access to this workflow.
-unsafe fn complete_confirm(token: u32, result: Result<(), crate::hal::ui::UserAbort>) {
- unsafe {
- if let TaskState::Running(current_token) = CONFIRM_STATE.get().as_ref().unwrap()
- && *current_token == token
- {
- CONFIRM_STATE
- .get()
- .write(TaskState::ResultAvailable(result));
- }
- }
-}
-
-/// # Safety
-/// Must be called from the same single-threaded, non-reentrant execution context as all other
-/// U2F workflow C API calls. In particular, do not call this from interrupts or from multiple
-/// threads.
-#[unsafe(no_mangle)]
-pub unsafe extern "C" fn rust_workflow_spawn_unlock() {
- let token = next_task_token();
- unsafe {
- UNLOCK_STATE.get().write(TaskState::Running(token));
- }
- crate::main_loop::spawn(Box::pin(async move {
- let result =
- unsafe { crate::workflow::unlock::unlock(BITBOX02_HAL.get().as_mut().unwrap()).await };
- unsafe { complete_unlock(token, result) };
- }));
-}
-
-/// # Safety
-/// `title` and `body` must be valid non-null pointers to NUL-terminated UTF-8 strings, readable
-/// for the duration of this call.
-///
-/// This must be called from the same single-threaded, non-reentrant execution context as all
-/// other U2F workflow C API calls (no interrupts/multi-threaded callers).
-#[unsafe(no_mangle)]
-pub unsafe extern "C" fn rust_workflow_spawn_confirm(
- title: *const core::ffi::c_char,
- body: *const core::ffi::c_char,
-) {
- let title: String = unsafe { CStr::from_ptr(title).to_str().unwrap().into() };
- let body: String = unsafe { CStr::from_ptr(body).to_str().unwrap().into() };
- let token = next_task_token();
- unsafe {
- CONFIRM_STATE.get().write(TaskState::Running(token));
- }
- crate::main_loop::spawn(Box::pin(async move {
- let params = ConfirmParams {
- title: &title,
- body: &body,
- accept_only: true,
- ..Default::default()
- };
- let result = unsafe {
- BITBOX02_HAL
- .get()
- .as_mut()
- .unwrap()
- .ui()
- .confirm(¶ms)
- .await
- };
- unsafe { complete_confirm(token, result) };
- }));
-}
-
-/// Returns true if there was a result.
-///
-/// # Safety
-/// `result_out` must be a valid, non-null writable pointer to a `bool` for the duration of this
-/// call.
-///
-/// This must be called from the same single-threaded, non-reentrant execution context as all
-/// other U2F workflow C API calls (no interrupts/multi-threaded callers).
-#[unsafe(no_mangle)]
-pub unsafe extern "C" fn rust_workflow_unlock_poll(result_out: &mut bool) -> bool {
- unsafe {
- match UNLOCK_STATE.get().as_ref().unwrap() {
- TaskState::ResultAvailable(result) => {
- *result_out = result.is_ok();
- UNLOCK_STATE.get().write(TaskState::Nothing);
- true
- }
- TaskState::Running(_) => false,
- TaskState::Nothing => panic!("polled non-existing future"),
- }
- }
-}
-
-/// Returns true if there was a result.
-///
-/// # Safety
-/// `result_out` must be a valid, non-null writable pointer to a `bool` for the duration of this
-/// call.
-///
-/// This must be called from the same single-threaded, non-reentrant execution context as all
-/// other U2F workflow C API calls (no interrupts/multi-threaded callers).
-#[unsafe(no_mangle)]
-pub unsafe extern "C" fn rust_workflow_confirm_poll(result_out: &mut bool) -> bool {
- unsafe {
- match CONFIRM_STATE.get().as_ref().unwrap() {
- TaskState::ResultAvailable(result) => {
- CONFIRM_STATE.get().write(TaskState::Nothing);
- *result_out = result.is_ok();
- true
- }
- TaskState::Running(_) => false,
- TaskState::Nothing => false,
- }
- }
-}
-
-/// # Safety
-/// Must be called from the same single-threaded, non-reentrant execution context as all other
-/// U2F workflow C API calls (no interrupts/multi-threaded callers).
-#[unsafe(no_mangle)]
-pub unsafe extern "C" fn rust_workflow_abort_current() {
- unsafe {
- UNLOCK_STATE.get().write(TaskState::Nothing);
- CONFIRM_STATE.get().write(TaskState::Nothing);
- }
-}
diff --git a/test/simulator-graphical-bb03/Cargo.lock b/test/simulator-graphical-bb03/Cargo.lock
index 15e8d9f..ff25580 100644
--- a/test/simulator-graphical-bb03/Cargo.lock
+++ b/test/simulator-graphical-bb03/Cargo.lock
@@ -404,7 +404,6 @@ dependencies = [
"erc20_params",
"fatfs-sys",
"futures-lite",
- "grounded",
"hex",
"hex_lit",
"hmac",
@@ -428,11 +427,13 @@ dependencies = [
"bip39",
"bitbox-aes",
"bitbox-framed-serial-link",
+ "bitbox-hal",
"bitbox02",
"bitbox02-noise",
"bitbox02-rust",
"cortex-m",
"der",
+ "grounded",
"hex",
"util",
"zeroize",
diff --git a/test/simulator-graphical/Cargo.lock b/test/simulator-graphical/Cargo.lock
index 54902ae..91084ca 100644
--- a/test/simulator-graphical/Cargo.lock
+++ b/test/simulator-graphical/Cargo.lock
@@ -366,7 +366,6 @@ dependencies = [
"erc20_params",
"fatfs-sys",
"futures-lite",
- "grounded",
"hex",
"hex_lit",
"hmac",
@@ -390,11 +389,13 @@ dependencies = [
"bip39",
"bitbox-aes",
"bitbox-framed-serial-link",
+ "bitbox-hal",
"bitbox02",
"bitbox02-noise",
"bitbox02-rust",
"cortex-m",
"der",
+ "grounded",
"hex",
"util",
"zeroize",
Why this scored 12/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.