Merge remote-tracking branch 'agent/benma-agent/hww-start-session'
What changed, and why it matters
This commit adds a new 'session reset' command to the BitBox02 hardware wallet's USB protocol. It lets the host computer cleanly reset the device connection if a previous operation was interrupted, instead of leaving the device stuck mid-task. The change also fixes a minor timeout-handling issue for the existing INFO command so that merely checking the firmware version no longer keeps an abandoned operation alive. The commit is framed as a robustness improvement, not a security fix, and there is no disclosed vulnerability.
Treat as a normal feature/robustness merge. Review that REQ_RESET cannot be abused to bypass UI confirmation or to reset a U2F workflow (the U2F-active BUSY check appears to cover this). Ensure the new command is authenticated by the same transport rules as CANCEL/RETRY. No immediate security patch is indicated by the commit content.
Security signals we found
New USB control command added to host-wallet protocol
Reset path cancels async task, resets Noise state, unlocks USB processing, and clears output queue
U2F UI ownership check prevents reset from interrupting an active U2F workflow
INFO request timeout behavior changed so version discovery does not refresh/extend operation timeout
Host library gates new command on firmware version >= 9.28.0
Evidence from the diff
The patch introduces HWW_REQ_RESET (value 0x03) alongside existing NEW/RETRY/CANCEL/INFO framing commands. On the firmware side, _reset_session() cancels the current async USB task, resets the Noise handshake state, resets the UI stack, unlocks USB processing if it was locked, and clears the HWW output queue. It returns RSP_BUSY if a U2F workflow currently owns the UI. The Python host library now sends REQ_RESET immediately after version discovery and before attestation/unlock/Noise handshake, but only for firmware >= v9.28.0. The patch also adds hww_request_is_info() and changes usb_processing so that INFO requests do not reset the operation watchdog timeout, preventing version discovery from extending or overwriting an outstanding-operation timeout. Extensive unit tests cover reset behavior, U2F-lock interaction, payload rejection, and INFO timeout preservation.
Changed components
src/hww.c / src/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.pyInspect captured patch +485 / −17
### CHANGELOG.md
@@ -10,6 +10,7 @@ recorded separately.
- Reject malformed microSD backups instead of crashing
- Hold the screen reset pin low until firmware is ready initalize it.
- Cardano: limit xpub requests to 20 keypaths per batch
+- API: add a session reset command for clean host reconnects after interrupted operations
### v9.27.1
- API: include the installed bootloader version in the device info response
### 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"
@@ -598,6 +600,8 @@ def __init__(
else:
self._bitbox_protocol = BitBoxProtocolV1(transport)
+ self._reset_session(transport, self.version)
+
if self.version >= semver.VersionInfo(2, 0, 0):
noise_config.attestation_check(self._perform_attestation())
self._bitbox_protocol.unlock_query()
@@ -694,6 +698,24 @@ def reboot(
return False
return True
+ @staticmethod
+ def _reset_session(transport: TransportLayer, version: semver.VersionInfo) -> None:
+ """Reset the previous session before attestation, unlock and the Noise handshake.
+
+ Skip firmware older than v9.28.0, which does not support session reset.
+ """
+ if version < semver.VersionInfo(9, 28, 0):
+ return
+ cid = transport.generate_cid()
+ while True:
+ response = 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
+
@staticmethod
def get_info(
transport: TransportLayer,
### py/bitbox02/tests/test_session.py
@@ -0,0 +1,101 @@
+# SPDX-License-Identifier: Apache-2.0
+
+"""Tests for starting a fresh host session before regular connection setup."""
+
+import unittest
+from unittest import mock
+
+import semver
+
+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.BitBoxCommonAPI._reset_session( # pylint: disable=protected-access
+ transport, semver.VersionInfo(9, 28, 0)
+ )
+ 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.BitBoxCommonAPI._reset_session( # pylint: disable=protected-access
+ transport, semver.VersionInfo(9, 28, 0)
+ )
+
+
+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 34/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.