What changed, and why it matters
This commit fixes a bug in the BitBox02 hardware wallet where unplugging the USB cable at the wrong moment could leave a half-finished operation running. If the device stayed powered and a new host reconnected, the new host's first message could be misinterpreted as a continuation of the old operation, producing a confusing encrypted error instead of a proper response. The fix adds a new 'reset session' command that the companion app sends at the start of every connection on firmware 9.28.0 and newer, which cancels any leftover task, clears queued responses, resets the encrypted Noise channel, and restores the user interface. The change also lets the host ask for the firmware version without extending an abandoned operation's timeout, so it can safely decide whether to reset.
Treat this as a security-hardening fix with a clear bug-class remediation. Users should upgrade to firmware v9.28.0 and update the Python client so that the new REQ_RESET handshake is used. Developers should verify the powered-device reconnect flow on real hardware as noted in the commit message, and review that no other async workflows can leak state across host sessions.
Security signals we found
Fixes cross-session state confusion on USB reconnect
Adds explicit session reset command to cancel stale async workflows
Resets Noise cryptographic session to prevent old-key encrypted responses
Clears queued USB responses that could leak to a new host
Guards reset against U2F UI ownership to avoid interrupting security operations
Allows version discovery without refreshing abandoned operation timeout
Includes regression tests for abandoned continuations, pending responses, U2F arbitration, INFO timeout behavior, and client compatibility
Evidence from the diff
The root cause is a race between USB disconnect/reconnect and the async HWW workflow. When a workflow calls next_request().await after releasing the HWW lock, a host disconnect does not cancel it because the USB timeout only governs the locked phase. On reconnect, the Python client’s attestation request is consumed by the stale workflow as its continuation, and the response is encrypted with the old Noise session. The patch introduces HWW_REQ_RESET (0x03) at the top-level framing layer. On the firmware side, _reset_session() checks that no U2F workflow owns the shared UI, then calls rust_hww_reset_session(), which cancels the async USB task, resets the Noise state, and resets the UI. It also unlocks HWW if locked and clears the HWW output queue. REQ_INFO is allowed through the busy gate and no longer refreshes the operation timeout, so the host can query the version before choosing to reset. The Python client now issues REQ_RESET before attestation, unlock, and Noise setup only for v9.28.0+.
Changed components
src/hww.csrc/hww.hsrc/rust/bitbox02-rust/src/hww.rssrc/rust/bitbox02-rust/src/hww/noise.rssrc/rust/bitbox02-rust/src/hww/transport.rssrc/rust/bitbox02-rust-c/src/firmware_c_api.rssrc/usb/usb_processing.cpy/bitbox02/bitbox02/communication/bitbox_api_protocol.pypy/bitbox02/tests/test_session.pytest/unit-test/test_hww.cInspect captured patch +473 / −17
### py/bitbox02/bitbox02/communication/bitbox_api_protocol.py
@@ -38,6 +38,8 @@ class HwwRequestCode:
REQ_RETRY = b"\x01"
# Cancel any outstanding request.
REQ_CANCEL = b"\x02"
+ # Reset the host session, discarding any outstanding operation (since firmware v9.28.0).
+ REQ_RESET = b"\x03"
# INFO api call (used to be OP_INFO api call), graduated to the toplevel framing so it works
# the same way for all firmware versions.
REQ_INFO = b"i"
@@ -300,6 +302,18 @@ def __init__(self, transport: TransportLayer):
def close(self) -> None:
self._transport.close()
+ def reset_session(self) -> None:
+ """Reset the previous session before attestation, unlock and the Noise handshake."""
+ cid = self._transport.generate_cid()
+ while True:
+ response = self._transport.query(HwwRequestCode.REQ_RESET, HWW_CMD, cid)
+ if response == HwwResponseCode.RSP_BUSY:
+ time.sleep(1)
+ continue
+ if response != HwwResponseCode.RSP_ACK:
+ raise Exception("Unexpected response to RESET.")
+ return
+
def _raw_query(self, msg: bytes) -> bytes:
cid = self._transport.generate_cid()
return self._transport.query(msg, HWW_CMD, cid)
@@ -598,6 +612,9 @@ def __init__(
else:
self._bitbox_protocol = BitBoxProtocolV1(transport)
+ if self.version >= semver.VersionInfo(9, 28, 0):
+ self._bitbox_protocol.reset_session()
+
if self.version >= semver.VersionInfo(2, 0, 0):
noise_config.attestation_check(self._perform_attestation())
self._bitbox_protocol.unlock_query()
### py/bitbox02/tests/test_session.py
@@ -0,0 +1,95 @@
+# SPDX-License-Identifier: Apache-2.0
+
+"""Tests for starting a fresh host session before regular connection setup."""
+
+import unittest
+from unittest import mock
+
+from bitbox02.communication import bitbox_api_protocol as protocol
+from bitbox02.communication.communication import TransportLayer
+from bitbox02.communication.devices import BITBOX02MULTI
+
+
+class TestSession(unittest.TestCase):
+ """Session startup and compatibility with older firmware."""
+
+ def test_reset_session_version_gate_and_order(self) -> None:
+ """Discover the version if needed, then reset supported devices before attestation."""
+ for version in ("v9.27.2", "v9.28.0", "v9.28.0-dev", "v9.29.0"):
+ for discover_version in (False, True):
+ with self.subTest(version=version, discover_version=discover_version):
+ self._check_session_setup(version, discover_version)
+
+ def _check_session_setup(self, version: str, discover_version: bool) -> None:
+ events = []
+ transport = mock.Mock(spec=TransportLayer)
+ transport.generate_cid.return_value = 123
+
+ def query(data: bytes, endpoint: int, cid: int) -> bytes:
+ self.assertEqual(endpoint, protocol.HWW_CMD)
+ self.assertEqual(cid, 123)
+ events.append(data)
+ if data == protocol.HwwRequestCode.REQ_INFO:
+ encoded = version.encode("ascii")
+ return bytes([len(encoded)]) + encoded + bytes(4)
+ self.assertEqual(data, protocol.HwwRequestCode.REQ_RESET)
+ return protocol.HwwResponseCode.RSP_ACK
+
+ transport.query.side_effect = query
+ device_info = (
+ None
+ if discover_version
+ else {
+ "serial_number": version,
+ "product_string": BITBOX02MULTI,
+ "path": b"device",
+ }
+ )
+ with mock.patch.object(
+ protocol.BitBoxCommonAPI,
+ "_perform_attestation",
+ side_effect=lambda: events.append("attestation") or True,
+ ), mock.patch.object(
+ protocol.BitBoxProtocolV7,
+ "unlock_query",
+ side_effect=lambda: events.append("unlock"),
+ ), mock.patch.object(
+ protocol.BitBoxProtocolV7,
+ "noise_connect",
+ side_effect=lambda _config: events.append("noise"),
+ ):
+ protocol.BitBoxCommonAPI(transport, device_info, protocol.BitBoxNoiseConfig())
+ expected = [protocol.HwwRequestCode.REQ_INFO] if discover_version else []
+ if version != "v9.27.2":
+ expected.append(protocol.HwwRequestCode.REQ_RESET)
+ expected.extend(["attestation", "unlock", "noise"])
+ self.assertEqual(events, expected)
+
+ def test_reset_session_busy(self) -> None:
+ """Wait for another owner of the UI before continuing startup."""
+ transport = mock.Mock(spec=TransportLayer)
+ transport.generate_cid.return_value = 123
+ transport.query.side_effect = [
+ protocol.HwwResponseCode.RSP_BUSY,
+ protocol.HwwResponseCode.RSP_ACK,
+ ]
+ with mock.patch.object(protocol.time, "sleep") as sleep:
+ protocol.BitBoxProtocolV7(transport).reset_session()
+ sleep.assert_called_once_with(1)
+ self.assertEqual(
+ transport.query.call_args_list,
+ [mock.call(protocol.HwwRequestCode.REQ_RESET, protocol.HWW_CMD, 123)] * 2,
+ )
+
+ def test_reset_session_requires_ack(self) -> None:
+ """Reject malformed replies and firmware that does not acknowledge cleanup."""
+ for response in (b"", protocol.HwwResponseCode.RSP_NACK, b"\x00payload"):
+ with self.subTest(response=response):
+ transport = mock.Mock(spec=TransportLayer)
+ transport.query.return_value = response
+ with self.assertRaisesRegex(Exception, "Unexpected response to RESET"):
+ protocol.BitBoxProtocolV7(transport).reset_session()
+
+
+if __name__ == "__main__":
+ unittest.main()
### src/hww.c
@@ -21,6 +21,7 @@ typedef enum {
HWW_REQ_NEW = 0,
HWW_REQ_RETRY = 1,
HWW_REQ_CANCEL = 2,
+ HWW_REQ_RESET = 3,
HWW_REQ_INFO = ((uint8_t)'i'),
} hww_req_t;
@@ -166,6 +167,23 @@ static void _cancel_packet(hww_packet_rsp_t* response)
// TODO: cancel async usb task.
}
+static void _reset_session(hww_packet_rsp_t* response)
+{
+#if APP_U2F == 1
+ // U2F workflows can own the shared UI even after releasing the USB processing lock.
+ if (rust_workflow_u2f_is_active()) {
+ response->status = HWW_RSP_BUSY;
+ return;
+ }
+#endif
+ rust_hww_reset_session();
+ if (usb_processing_locked(usb_processing_hww())) {
+ usb_processing_unlock();
+ }
+ rust_usb_report_queue_clear(usb_processing_out_queue(usb_processing_hww()));
+ response->status = HWW_RSP_ACK;
+}
+
static void _msg(const Packet* in_packet, Packet* out_packet, const size_t max_out_len)
{
if (in_packet->len == 0) {
@@ -175,6 +193,11 @@ static void _msg(const Packet* in_packet, Packet* out_packet, const size_t max_o
}
hww_req_t cmd = in_packet->data_addr[0];
+ if (cmd == HWW_REQ_RESET && in_packet->len != 1) {
+ out_packet->data_addr[0] = HWW_RSP_NACK;
+ out_packet->len = 1;
+ return;
+ }
if (cmd == HWW_REQ_INFO) {
// HWW_REQ_INFO is treated as a special case: it has a direct response without a status
// code, so it can be called independently of the firmware version and framing protocol.
@@ -192,6 +215,9 @@ static void _msg(const Packet* in_packet, Packet* out_packet, const size_t max_o
.status = HWW_RSP_NACK,
.buffer = {.data = out_packet->data_addr + 1, .len = 0, .max_len = max_out_len - 1}};
switch (cmd) {
+ case HWW_REQ_RESET:
+ _reset_session(&response);
+ break;
case HWW_REQ_NEW:
_process_packet(&decoded_buffer, &response);
break;
@@ -219,7 +245,14 @@ bool hww_blocking_request_can_go_through(const Packet* in_packet)
return false;
}
uint8_t cmd = in_packet->data_addr[0];
- return cmd == HWW_REQ_CANCEL || cmd == HWW_REQ_RETRY;
+ return cmd == HWW_REQ_CANCEL || cmd == HWW_REQ_RETRY || cmd == HWW_REQ_INFO ||
+ cmd == HWW_REQ_RESET;
+}
+
+bool hww_request_is_info(const Packet* in_packet)
+{
+ return in_packet->cmd == HWW_MSG && in_packet->len == 1 &&
+ in_packet->data_addr[0] == HWW_REQ_INFO;
}
void hww_blocked_req_error(Packet* out_packet, const Packet* in_packet)
### src/hww.h
@@ -17,10 +17,13 @@ void hww_setup(void);
/**
* When the HWW stack is blocking the device, checks if
* a HWW request is allowed to be processed.
- * HWW requests that are allowed are OP_CANCEL and OP_RETRY.
+ * Allows CANCEL, RETRY, INFO and RESET framing requests.
*/
bool hww_blocking_request_can_go_through(const Packet* in_packet);
+/** Whether this is an INFO request, which must not change an operation's timeout. */
+bool hww_request_is_info(const Packet* in_packet);
+
/**
* Create an output packet used to signal to the client
* that a valid HWW request has been received, but it can't be processed
### src/rust/bitbox02-rust-c/src/firmware_c_api.rs
@@ -22,6 +22,11 @@ pub extern "C" fn rust_main_loop() -> ! {
bitbox02_rust::main_loop::main_loop(&mut crate::HalImpl::new())
}
+#[unsafe(no_mangle)]
+pub extern "C" fn rust_hww_reset_session() {
+ bitbox02_rust::hww::reset_session(&mut crate::HalImpl::new());
+}
+
/// # Safety
///
/// `purpose` must be a valid, null-terminated UTF-8 string pointer.
### src/rust/bitbox02-rust/src/hww.rs
@@ -15,6 +15,15 @@ const OP_STATUS_SUCCESS: u8 = 0;
const OP_STATUS_FAILURE: u8 = 1;
const OP_STATUS_FAILURE_UNINITIALIZED: u8 = 2;
+/// Reset the host session. Call between task polls, after transport arbitration has ensured
+/// that no U2F workflow owns the shared UI.
+pub fn reset_session(hal: &mut impl crate::hal::Hal) {
+ // Drop the task first: its UI components must be dropped before resetting the screen stack.
+ crate::async_usb::cancel();
+ noise::reset();
+ hal.ui().reset();
+}
+
/// Must be called during the execution of a usb task. This sends out the response to the host and
/// awaits the next request. If the request is not a valid noise encrypted protofbuf api request
/// message, `Err(Error::InvalidInput)` is returned.
@@ -214,6 +223,31 @@ mod tests {
})
}
+ #[async_test::test]
+ async fn test_reset_session_resets_noise() {
+ let mut old_query = init_noise();
+ let mut hal = TestingHal::new();
+ reset_session(&mut hal);
+ assert!(noise::encrypt(b"response", &mut Vec::new()).is_err());
+ assert!(old_query(&mut hal, b"request").is_err());
+
+ // Starting a session repeatedly is safe, and a new handshake works normally.
+ reset_session(&mut hal);
+ let mut new_query = init_noise();
+ let request = crate::pb::Request {
+ request: Some(crate::pb::request::Request::ListBackups(
+ crate::pb::ListBackupsRequest {},
+ )),
+ };
+ let response = new_query(&mut hal, &request.encode_to_vec()).unwrap();
+ assert!(matches!(
+ crate::pb::Response::decode(response.as_slice())
+ .unwrap()
+ .response,
+ Some(crate::pb::response::Response::ListBackups(_))
+ ));
+ }
+
/// Can't unlock when the device is not initialized yet (not seeded).
#[async_test::test]
async fn test_cant_unlock() {
### src/rust/bitbox02-rust/src/hww/noise.rs
@@ -42,6 +42,10 @@ pub fn decrypt(msg: &[u8]) -> Result<Vec<u8>, Error> {
NOISE_STATE.0.borrow_mut().decrypt(msg).or(Err(Error))
}
+pub(super) fn reset() {
+ NOISE_STATE.0.borrow_mut().reset();
+}
+
/// Process noise-encrypted messages:
/// - Enforce handshake
/// - Handle pairing verification
### src/rust/bitbox02-rust/src/hww/transport.rs
@@ -10,6 +10,7 @@ const HWW_CMD: u8 = COMMAND_VENDOR_FIRST + 1;
const HWW_REQ_NEW: u8 = 0;
const HWW_REQ_RETRY: u8 = 1;
const HWW_REQ_CANCEL: u8 = 2;
+const HWW_REQ_RESET: u8 = 3;
const HWW_REQ_INFO: u8 = b'i';
const HWW_RSP_ACK: u8 = 0;
@@ -110,7 +111,15 @@ where
let request = payload[0];
let body = &payload[1..];
+ if request == HWW_REQ_RESET && !body.is_empty() {
+ return Ok(vec![HWW_RSP_NACK]);
+ }
match request {
+ HWW_REQ_RESET => {
+ crate::hww::reset_session(&mut self.hal);
+ self.deadline_ms = None;
+ Ok(vec![HWW_RSP_ACK])
+ }
HWW_REQ_INFO => {
// HWW_REQ_INFO is treated as a special case: it has a direct response without a
// status code, so it can be called independently of the firmware version and
@@ -171,6 +180,7 @@ mod tests {
use super::*;
use crate::hal::testing::TestingHal;
use crate::hal::{Memory, System, memory::Platform};
+ use hex_lit::hex;
use std::sync::{Mutex, MutexGuard};
static TEST_LOCK: Mutex<()> = Mutex::new(());
@@ -259,6 +269,118 @@ mod tests {
crate::async_usb::cancel();
}
+ #[test]
+ fn test_req_reset_session_cancels_task() {
+ let _guard = test_guard();
+ crate::async_usb::spawn(pending_task, &[]);
+ crate::async_usb::spin();
+ let mut handler = handler();
+ handler.refresh_timeout(0);
+
+ for _ in 0..2 {
+ assert_eq!(
+ handler
+ .handle_vendor_command(1, HWW_CMD, &[HWW_REQ_RESET], 1)
+ .unwrap(),
+ vec![HWW_RSP_ACK],
+ );
+ assert!(crate::async_usb::is_idle());
+ assert_eq!(handler.deadline_ms, None);
+ }
+ assert_eq!(
+ handler
+ .handle_vendor_command(1, HWW_CMD, &[HWW_REQ_RETRY], 2)
+ .unwrap(),
+ vec![HWW_RSP_NACK],
+ );
+ }
+
+ #[test]
+ fn test_req_reset_session_cancels_next_request() {
+ let _guard = test_guard();
+ for (collect_response, supply_request) in [(false, false), (true, false), (true, true)] {
+ crate::async_usb::spawn(next_request_task, &hex!("aa"));
+ crate::async_usb::spin();
+ if collect_response {
+ assert_eq!(crate::async_usb::take_response().unwrap(), hex!("bb"));
+ }
+ if supply_request {
+ crate::async_usb::on_next_request(&hex!("cc"));
+ }
+ let mut handler = handler();
+ assert_eq!(
+ handler
+ .handle_vendor_command(1, HWW_CMD, &[HWW_REQ_RESET], 0)
+ .unwrap(),
+ vec![HWW_RSP_ACK],
+ );
+ assert!(crate::async_usb::is_idle());
+
+ // A new workflow must not see an input or response left by the previous session.
+ crate::async_usb::spawn(next_request_task, &hex!("aa"));
+ crate::async_usb::spin();
+ assert_eq!(crate::async_usb::take_response().unwrap(), hex!("bb"));
+ assert!(crate::async_usb::waiting_for_next_request());
+ crate::async_usb::on_next_request(&hex!("cc"));
+ crate::async_usb::spin();
+ assert_eq!(crate::async_usb::take_response().unwrap(), hex!("dd"));
+ }
+ }
+
+ #[test]
+ fn test_req_reset_session_discards_final_response() {
+ let _guard = test_guard();
+ crate::async_usb::spawn(ready_task, &[]);
+ crate::async_usb::spin();
+ let mut handler = handler();
+ assert_eq!(
+ handler
+ .handle_vendor_command(1, HWW_CMD, &[HWW_REQ_RESET], 0)
+ .unwrap(),
+ vec![HWW_RSP_ACK],
+ );
+ assert_eq!(
+ crate::async_usb::take_response(),
+ Err(crate::async_usb::CopyResponseErr::NotRunning),
+ );
+ }
+
+ #[test]
+ fn test_req_reset_session_rejects_payload() {
+ let _guard = test_guard();
+ crate::async_usb::spawn(pending_task, &[]);
+ let mut handler = handler();
+ assert_eq!(
+ handler
+ .handle_vendor_command(1, HWW_CMD, &[HWW_REQ_RESET, 0], 0)
+ .unwrap(),
+ vec![HWW_RSP_NACK],
+ );
+ assert!(!crate::async_usb::is_idle());
+ crate::async_usb::cancel();
+ }
+
+ #[test]
+ fn test_req_info_preserves_response_and_timeout() {
+ let _guard = test_guard();
+ crate::async_usb::spawn(next_request_task, &hex!("aa"));
+ crate::async_usb::spin();
+ let mut handler = handler();
+ handler.refresh_timeout(0);
+ let response = handler
+ .handle_vendor_command(1, HWW_CMD, &[HWW_REQ_INFO], 400)
+ .unwrap();
+ assert_eq!(
+ response[0] as usize,
+ crate::version::FIRMWARE_VERSION_SHORT.len()
+ );
+ assert_eq!(handler.deadline_ms, Some(USB_OUTSTANDING_OP_TIMEOUT_MS));
+ assert_eq!(crate::async_usb::take_response().unwrap(), hex!("bb"));
+ assert!(crate::async_usb::waiting_for_next_request());
+ handler.tick(USB_OUTSTANDING_OP_TIMEOUT_MS + 1);
+ assert!(crate::async_usb::is_idle());
+ }
+
#[test]
fn test_req_retry_ack() {
let _guard = test_guard();
### src/usb/usb_processing.c
@@ -104,7 +104,7 @@ typedef struct {
struct usb_processing* blocking_ctx;
/**
* Timeout counter. This is increased every 100ms by a timer,
- * and is reset to 0 every time a new packet is send to one of the
+ * and is reset to 0 every time a new packet, except HWW INFO, is sent to one of the
* underlying stacks. When the timeout counter becomes greater then
* USB_OUTSTANDING_OP_TIMEOUT_TICKS, any outstanding operation is aborted
* and the USB stack is forcefully unlocked.
@@ -276,8 +276,11 @@ static void _usb_arbitrate_packet(struct usb_processing* ctx, const Packet* in_p
_enqueue_frames(ctx, &out_packet);
} else {
_usb_execute_packet(ctx, in_packet);
- /* New packet processed: reset the watchdog timeout. */
- usb_processing_timeout_reset(0);
+ // Discovery must not keep an abandoned operation alive or overwrite an extended timeout.
+ if (ctx != usb_processing_hww() || !hww_request_is_info(in_packet)) {
+ /* New packet processed: reset the watchdog timeout. */
+ usb_processing_timeout_reset(0);
+ }
}
}
#endif
### test/unit-test/CMakeLists.txt
@@ -40,7 +40,7 @@ else()
gestures
""
hww
- "-Wl,--wrap=rust_workflow_u2f_is_active"
+ "-Wl,--wrap=rust_workflow_u2f_is_active,--wrap=rust_hww_reset_session,--wrap=rust_async_usb_cancel,--wrap=rust_async_usb_copy_response"
u2f_state
""
random
### test/unit-test/test_hww.c
@@ -9,44 +9,184 @@
#include <rust/rust.h>
#include <usb/usb_frame.h>
#include <usb/usb_processing.h>
+#include <version.h>
static bool _u2f_workflow_active = false;
+static unsigned int _session_resets = 0;
+static unsigned int _aborts = 0;
+static UsbResponse _pending_response = UsbResponseNotReady;
bool __wrap_rust_workflow_u2f_is_active(void)
{
return _u2f_workflow_active;
}
-static void test_hww_new_request_is_busy_while_u2f_active(void** state)
+void __wrap_rust_hww_reset_session(void)
+{
+ _session_resets++;
+ _pending_response = UsbResponseNack;
+}
+
+void __wrap_rust_async_usb_cancel(void)
+{
+ _aborts++;
+}
+
+UsbResponse __wrap_rust_async_usb_copy_response(buffer_t* out)
{
- (void)state;
+ if (_pending_response == UsbResponseAck) {
+ out->data[0] = 0xaa;
+ out->len = 1;
+ }
+ return _pending_response;
+}
+static int _setup(void** state)
+{
RustUsbReportQueue* queue = rust_usb_report_queue_init();
assert_non_null(queue);
usb_processing_init(queue);
- hww_setup();
+ usb_processing_init_u2f(queue);
+ usb_processing_timeout_reset(0);
+ _u2f_workflow_active = false;
+ _session_resets = 0;
+ _aborts = 0;
+ _pending_response = UsbResponseNotReady;
+ *state = queue;
+ return 0;
+}
- _u2f_workflow_active = true;
- const uint8_t request[] = {0x00, 'h'};
+static int _teardown(void** state)
+{
+ if (usb_processing_locked(usb_processing_hww()) ||
+ usb_processing_locked(usb_processing_u2f())) {
+ usb_processing_unlock();
+ }
+ assert_true(rust_usb_report_queue_free(*state));
+ return 0;
+}
+
+static USB_FRAME _query(void** state, const uint8_t* request, size_t len)
+{
+ RustUsbReportQueue* queue = *state;
const uint32_t cid = 0x12345678;
- assert_true(
- usb_processing_enqueue(usb_processing_hww(), request, sizeof(request), HWW_MSG, cid));
+ assert_true(usb_processing_enqueue(usb_processing_hww(), request, len, HWW_MSG, cid));
usb_processing_process(usb_processing_hww());
USB_FRAME response;
assert_true(rust_usb_report_queue_pull(queue, (uint8_t*)&response));
assert_int_equal(response.cid, cid);
assert_int_equal(response.init.cmd, HWW_MSG);
+ USB_FRAME extra;
+ assert_false(rust_usb_report_queue_pull(queue, (uint8_t*)&extra));
+ return response;
+}
+
+static void test_hww_new_request_is_busy_while_u2f_active(void** state)
+{
+ _u2f_workflow_active = true;
+ const uint8_t request[] = {0x00, 'h'};
+ USB_FRAME response = _query(state, request, sizeof(request));
assert_int_equal(FRAME_MSG_LEN(response), 1);
assert_int_equal(response.init.data[0], 2); // HWW_RSP_BUSY
- assert_false(rust_usb_report_queue_pull(queue, (uint8_t*)&response));
- assert_true(rust_usb_report_queue_free(queue));
+}
+
+static void test_hww_reset_session(void** state)
+{
+ usb_processing_lock(usb_processing_hww());
+ _pending_response = UsbResponseAck;
+ const uint8_t request[] = {3}; // HWW_REQ_RESET
+ for (unsigned int i = 0; i < 2; i++) {
+ USB_FRAME response = _query(state, request, sizeof(request));
+ assert_int_equal(FRAME_MSG_LEN(response), 1);
+ assert_int_equal(response.init.data[0], 0); // HWW_RSP_ACK
+ assert_int_equal(_session_resets, i + 1);
+ assert_false(usb_processing_locked(usb_processing_hww()));
+ }
+ const uint8_t retry[] = {1}; // HWW_REQ_RETRY
+ USB_FRAME response = _query(state, retry, sizeof(retry));
+ assert_int_equal(response.init.data[0], 3); // HWW_RSP_NACK
+}
+
+static void test_hww_reset_session_is_busy_while_u2f_active(void** state)
+{
+ _u2f_workflow_active = true;
+ const uint8_t request[] = {3};
+ USB_FRAME response = _query(state, request, sizeof(request));
+ assert_int_equal(response.init.data[0], 2); // HWW_RSP_BUSY
+ assert_int_equal(_session_resets, 0);
+}
+
+static void test_hww_control_requests_respect_u2f_lock(void** state)
+{
+ usb_processing_lock(usb_processing_u2f());
+ const uint8_t requests[] = {'i', 3};
+ for (size_t i = 0; i < sizeof(requests); i++) {
+ USB_FRAME response = _query(state, &requests[i], 1);
+ assert_int_equal(response.init.data[0], 2); // HWW_RSP_BUSY
+ assert_true(usb_processing_locked(usb_processing_u2f()));
+ }
+ assert_int_equal(_session_resets, 0);
+}
+
+static void test_hww_reset_session_rejects_payload(void** state)
+{
+ const uint8_t request[] = {3, 0};
+ USB_FRAME response = _query(state, request, sizeof(request));
+ assert_int_equal(response.init.data[0], 3); // HWW_RSP_NACK
+ assert_int_equal(_session_resets, 0);
+ usb_processing_lock(usb_processing_hww());
+ response = _query(state, request, sizeof(request));
+ assert_int_equal(response.init.data[0], 2); // HWW_RSP_BUSY
+ assert_true(usb_processing_locked(usb_processing_hww()));
+ assert_int_equal(_session_resets, 0);
+}
+
+static void test_hww_info_preserves_response(void** state)
+{
+ usb_processing_lock(usb_processing_hww());
+ _pending_response = UsbResponseAck;
+ const uint8_t request[] = {'i'};
+ USB_FRAME response = _query(state, request, sizeof(request));
+ const size_t version_len = sizeof(DIGITAL_BITBOX_VERSION_SHORT) - 1;
+ assert_int_equal(FRAME_MSG_LEN(response), version_len + 5);
+ assert_int_equal(response.init.data[0], version_len);
+ assert_memory_equal(response.init.data + 1, DIGITAL_BITBOX_VERSION_SHORT, version_len);
+ assert_true(usb_processing_locked(usb_processing_hww()));
+ assert_int_equal(_aborts, 0);
+ const uint8_t retry[] = {1};
+ response = _query(state, retry, sizeof(retry));
+ assert_int_equal(FRAME_MSG_LEN(response), 2);
+ assert_int_equal(response.init.data[0], 0); // HWW_RSP_ACK
+ assert_int_equal(response.init.data[1], 0xaa);
+ assert_false(usb_processing_locked(usb_processing_hww()));
+}
+
+static void test_hww_info_does_not_refresh_timeout(void** state)
+{
+ usb_processing_lock(usb_processing_hww());
+ usb_processing_timeout_reset(6); // More than 500 ms without polling.
+ const uint8_t request[] = {'i'};
+ USB_FRAME response = _query(state, request, sizeof(request));
+ assert_int_equal(response.init.data[0], sizeof(DIGITAL_BITBOX_VERSION_SHORT) - 1);
+ assert_int_equal(_aborts, 1);
+ assert_false(usb_processing_locked(usb_processing_hww()));
}
int main(void)
{
+ hww_setup();
const struct CMUnitTest tests[] = {
- cmocka_unit_test(test_hww_new_request_is_busy_while_u2f_active),
+ cmocka_unit_test_setup_teardown(
+ test_hww_new_request_is_busy_while_u2f_active, _setup, _teardown),
+ cmocka_unit_test_setup_teardown(test_hww_reset_session, _setup, _teardown),
+ cmocka_unit_test_setup_teardown(
+ test_hww_reset_session_is_busy_while_u2f_active, _setup, _teardown),
+ cmocka_unit_test_setup_teardown(
+ test_hww_control_requests_respect_u2f_lock, _setup, _teardown),
+ cmocka_unit_test_setup_teardown(test_hww_reset_session_rejects_payload, _setup, _teardown),
+ cmocka_unit_test_setup_teardown(test_hww_info_preserves_response, _setup, _teardown),
+ cmocka_unit_test_setup_teardown(test_hww_info_does_not_refresh_timeout, _setup, _teardown),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
### versions.json
@@ -1,5 +1,5 @@
{
- "firmware": "v9.27.2",
+ "firmware": "v9.28.0",
"bootloader": "v1.2.3",
"stage0": 2
}Why this scored 62/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.