What changed, and why it matters
This firmware update fixes several security issues in the BitBox02 hardware wallet. The most important change is that when signing Bitcoin transactions, the device now verifies that the previous transaction output's script (the 'pubkey script') matches the keypath the wallet is being asked to sign with. This closes a gap where a malicious or buggy host app could ask the wallet to sign an input while claiming it belongs to a different address/key than it actually does. The update also hardens the U2F/FIDO workflow so that an attacker cannot swap or replay a different request while the user is still confirming one on screen, and it prevents Bluetooth pairing prompts from popping up on top of other wallet tasks. The changelog simply says 'Security improvements', so the vendor has not publicly detailed the exact vulnerabilities.
Treat this as a security update and recommend users upgrade to v9.26.5. If a security advisory is later published, cross-reference the exact CVEs and attack scenarios. Review whether the pubkey-script validation change needs to be flagged for downstream wallet software that streams prevtx data, because mismatches will now fail signing.
Security signals we found
Bitcoin signing now validates prevtx pubkey_script against derived script from keypath/script config
New unit test explicitly rejects keypath/script mismatch for silent payment inputs
U2F pending APDU is stored and compared to prevent request swapping during user confirmation
U2F workflow guard prevents spawning overlapping unlock/confirm workflows
BLE pairing prompt blocked when HWW workflow owns the screen
CHANGELOG labels the release with 'Security improvements' but gives no CVE or details
Evidence from the diff
The merge commit contains three main security-relevant hardening patches: (1) In src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs, handle_prevtx() now receives and validates the expected pubkey_script derived from the input’s keypath/script-config against the streamed previous transaction output. A new test test_silent_payment_rejects_input_keypath_mismatch confirms that mismatches are rejected. (2) In src/u2f.c and src/u2f/u2f_app.c, U2F state handling is reworked: the pending APDU is stored and compared byte-for-byte on continuation, the workflow guard prevents concurrent U2F workflows, and the response CID is taken from the current request rather than stored state. (3) In src/da14531/da14531_handler.c, BLE pairing confirmation is suppressed when a HWW workflow owns the screen (usb_processing_locked(usb_processing_hww())), and a custom component cleanup callback prevents stale component pointers. Supporting test files and Python demo tooling updates are included.
Changed components
Bitcoin transaction signing (src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs)U2F/FIDO state machine (src/u2f.c, src/u2f/u2f_app.c, src/u2f/u2f_app.h)U2F C API workflow guards (src/rust/bitbox02-rust-c/src/u2f_c_api.rs, u2f_c_api_stubs.rs)Bluetooth LE pairing handler (src/da14531/da14531_handler.c)Python demo/test tooling (py/send_message.py, py/requirements.txt)Inspect captured patch +974 / −279
### CHANGELOG.md
@@ -11,6 +11,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
- Display long transaction and swap amounts in full instead of truncating them
### v9.26.5
+- Security improvements
- Fixed a crash when listing many backups over Bluetooth
- Fix unexpected NACK responses on iOS due to slow securechip operations
### py/requirements.txt
@@ -2,6 +2,7 @@
tzlocal>=1.5,<2.0
types-tzlocal
bitbox02
+bech32>=1.2.0
requests
types-requests
rlp
### py/send_message.py
@@ -6,6 +6,7 @@
# pylint: disable=too-many-lines
import argparse
+import hashlib
import socket
import pprint
import sys
@@ -18,6 +19,7 @@
import os
import requests
+import base58
import hid
import semver
from tzlocal import get_localzone
@@ -46,6 +48,13 @@
except ModuleNotFoundError:
pass
+try:
+ # Optional bech32 dependency only needed for Bitcoin signing demos.
+ # pylint: disable=import-error
+ import bech32
+except ModuleNotFoundError:
+ pass
+
def eprint(*args: Any, **kwargs: Any) -> None:
"""
@@ -87,69 +96,110 @@ def ask_user(
def _btc_demo_inputs_outputs(
+ device: bitbox02.BitBox02,
bip44_account: int,
+ coin: "bitbox02.btc.BTCCoin.V" = bitbox02.btc.BTC,
+ script_configs: Optional[Sequence[bitbox02.btc.BTCScriptConfigWithKeypath]] = None,
) -> Tuple[List[bitbox02.BTCInputType], List[bitbox02.BTCOutputType]]:
"""
Returns a sample btc tx.
"""
- inputs: List[bitbox02.BTCInputType] = [
- {
- "prev_out_hash": binascii.unhexlify(
- "c58b7e3f1200e0c0ec9a5e81e925baface2cc1d4715514f2d8205be2508b48ee"
+
+ def address_to_pkscript(address: str) -> bytes:
+ lowercase_address = address.lower()
+ if lowercase_address.startswith(("bc1", "tb1", "bcrt1", "ltc1", "tltc1")):
+ separator_pos = lowercase_address.rfind("1")
+ assert separator_pos > 0
+ witness_version, witness_program = bech32.decode(
+ lowercase_address[:separator_pos], address
+ )
+ assert witness_version == 0
+ assert witness_program is not None
+ return bytes([0, len(witness_program)]) + bytes(witness_program)
+
+ decoded = base58.b58decode_check(address)
+ assert len(decoded) == 21
+ return b"\xa9\x14" + decoded[1:] + b"\x87"
+
+ def make_prev_tx(
+ pubkey_script: bytes,
+ ) -> Tuple[bytes, Any]:
+ version = 1
+ locktime = 0
+ value = int(1e8 * 0.60005)
+ prev_out_hash = b"11111111111111111111111111111111"
+ prev_out_index = 0
+ signature_script = b"some signature script"
+ sequence = 0xFFFFFFFF
+ prev_tx = {
+ "version": version,
+ "locktime": locktime,
+ "inputs": [
+ {
+ "prev_out_hash": prev_out_hash,
+ "prev_out_index": prev_out_index,
+ "signature_script": signature_script,
+ "sequence": sequence,
+ }
+ ],
+ "outputs": [{"value": value, "pubkey_script": pubkey_script}],
+ }
+ serialized = (
+ version.to_bytes(4, "little")
+ + b"\x01"
+ + prev_out_hash
+ + prev_out_index.to_bytes(4, "little")
+ + bytes([len(signature_script)])
+ + signature_script
+ + sequence.to_bytes(4, "little")
+ + b"\x01"
+ + value.to_bytes(8, "little")
+ + bytes([len(pubkey_script)])
+ + pubkey_script
+ + locktime.to_bytes(4, "little")
+ )
+ return hashlib.sha256(hashlib.sha256(serialized).digest()).digest(), prev_tx
+
+ if script_configs is None:
+ script_configs = [
+ bitbox02.btc.BTCScriptConfigWithKeypath(
+ script_config=bitbox02.btc.BTCScriptConfig(
+ simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH
+ ),
+ keypath=[84 + HARDENED, 0 + HARDENED, bip44_account],
),
- "prev_out_index": 0,
- "prev_out_value": int(1e8 * 0.60005),
- "sequence": 0xFFFFFFFF,
- "keypath": [84 + HARDENED, 0 + HARDENED, bip44_account, 0, 0],
- "script_config_index": 0,
- "prev_tx": {
- "version": 1,
- "locktime": 0,
- "inputs": [
- {
- "prev_out_hash": b"11111111111111111111111111111111",
- "prev_out_index": 0,
- "signature_script": b"some signature script",
- "sequence": 0xFFFFFFFF,
- }
- ],
- "outputs": [
- {
- "value": int(1e8 * 0.60005),
- "pubkey_script": b"some pubkey script",
- }
- ],
- },
- },
- {
- "prev_out_hash": binascii.unhexlify(
- "c58b7e3f1200e0c0ec9a5e81e925baface2cc1d4715514f2d8205be2508b48ee"
+ bitbox02.btc.BTCScriptConfigWithKeypath(
+ script_config=bitbox02.btc.BTCScriptConfig(
+ simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH
+ ),
+ keypath=[49 + HARDENED, 0 + HARDENED, bip44_account],
),
- "prev_out_index": 0,
- "prev_out_value": int(1e8 * 0.60005),
- "sequence": 0xFFFFFFFF,
- "keypath": [49 + HARDENED, 0 + HARDENED, bip44_account, 0, 1],
- "script_config_index": 1,
- "prev_tx": {
- "version": 1,
- "locktime": 0,
- "inputs": [
- {
- "prev_out_hash": b"11111111111111111111111111111111",
- "prev_out_index": 0,
- "signature_script": b"some signature script",
- "sequence": 0xFFFFFFFF,
- }
- ],
- "outputs": [
- {
- "value": int(1e8 * 0.60005),
- "pubkey_script": b"some pubkey script",
- }
- ],
- },
- },
- ]
+ ]
+ assert len(script_configs) in (1, 2)
+
+ inputs: List[bitbox02.BTCInputType] = []
+ for input_index in range(2):
+ script_config_index = input_index if len(script_configs) == 2 else 0
+ script_config = script_configs[script_config_index]
+ keypath = list(script_config.keypath) + [0, input_index]
+ address = device.btc_address(
+ coin=coin,
+ keypath=keypath,
+ script_config=script_config.script_config,
+ display=False,
+ )
+ prev_out_hash, prev_tx = make_prev_tx(address_to_pkscript(address))
+ inputs.append(
+ {
+ "prev_out_hash": prev_out_hash,
+ "prev_out_index": 0,
+ "prev_out_value": int(1e8 * 0.60005),
+ "sequence": 0xFFFFFFFF,
+ "keypath": keypath,
+ "script_config_index": script_config_index,
+ "prev_tx": prev_tx,
+ }
+ )
outputs: List[bitbox02.BTCOutputType] = [
bitbox02.BTCOutputInternal(
keypath=[84 + HARDENED, 0 + HARDENED, bip44_account, 1, 0],
@@ -518,7 +568,7 @@ def _sign_btc_normal(
) -> None:
# pylint: disable=no-member
bip44_account: int = 0 + HARDENED
- inputs, outputs = _btc_demo_inputs_outputs(bip44_account)
+ inputs, outputs = _btc_demo_inputs_outputs(self._device, bip44_account)
sigs = self._device.btc_sign(
bitbox02.btc.BTC,
[
@@ -548,7 +598,7 @@ def _sign_btc_send_to_self_same_account(
) -> None:
# pylint: disable=no-member
bip44_account: int = 0 + HARDENED
- inputs, outputs = _btc_demo_inputs_outputs(bip44_account)
+ inputs, outputs = _btc_demo_inputs_outputs(self._device, bip44_account)
outputs[1] = bitbox02.BTCOutputInternal(
keypath=[84 + HARDENED, 0 + HARDENED, bip44_account, 0, 0],
value=int(1e8 * 0.2),
@@ -583,7 +633,7 @@ def _sign_btc_send_to_self_different_account(
) -> None:
# pylint: disable=no-member
bip44_account: int = 0 + HARDENED
- inputs, outputs = _btc_demo_inputs_outputs(bip44_account)
+ inputs, outputs = _btc_demo_inputs_outputs(self._device, bip44_account)
outputs[1] = bitbox02.BTCOutputInternal(
keypath=[84 + HARDENED, 0 + HARDENED, 1 + HARDENED, 0, 0],
value=int(1e8 * 0.2),
@@ -624,7 +674,7 @@ def _sign_btc_send_to_self_different_account(
def _sign_btc_high_fee(self) -> None:
# pylint: disable=no-member
bip44_account: int = 0 + HARDENED
- inputs, outputs = _btc_demo_inputs_outputs(bip44_account)
+ inputs, outputs = _btc_demo_inputs_outputs(self._device, bip44_account)
outputs[1].value = int(1e8 * 0.18)
sigs = self._device.btc_sign(
bitbox02.btc.BTC,
@@ -651,7 +701,7 @@ def _sign_btc_high_fee(self) -> None:
def _sign_btc_multiple_changes(self) -> None:
# pylint: disable=no-member
bip44_account: int = 0 + HARDENED
- inputs, outputs = _btc_demo_inputs_outputs(bip44_account)
+ inputs, outputs = _btc_demo_inputs_outputs(self._device, bip44_account)
# Add a change output.
outputs.append(
bitbox02.BTCOutputInternal(
@@ -685,7 +735,7 @@ def _sign_btc_multiple_changes(self) -> None:
def _sign_btc_locktime_rbf(self) -> None:
# pylint: disable=no-member
bip44_account: int = 0 + HARDENED
- inputs, outputs = _btc_demo_inputs_outputs(bip44_account)
+ inputs, outputs = _btc_demo_inputs_outputs(self._device, bip44_account)
inputs[0]["sequence"] = 0xFFFFFFFF - 2
sigs = self._device.btc_sign(
bitbox02.btc.BTC,
@@ -713,7 +763,7 @@ def _sign_btc_locktime_rbf(self) -> None:
def _sign_btc_taproot_inputs(self) -> None:
# pylint: disable=no-member
bip44_account: int = 0 + HARDENED
- inputs, outputs = _btc_demo_inputs_outputs(bip44_account)
+ inputs, outputs = _btc_demo_inputs_outputs(self._device, bip44_account)
for inp in inputs:
inp["keypath"] = [86 + HARDENED] + list(inp["keypath"][1:])
inp["prev_tx"] = None
@@ -742,7 +792,7 @@ def _sign_btc_taproot_inputs(self) -> None:
def _sign_btc_taproot_output(self) -> None:
# pylint: disable=no-member
bip44_account: int = 0 + HARDENED
- inputs, outputs = _btc_demo_inputs_outputs(bip44_account)
+ inputs, outputs = _btc_demo_inputs_outputs(self._device, bip44_account)
assert isinstance(outputs[1], bitbox02.BTCOutputExternal)
outputs[1].type = bitbox02.btc.P2TR
outputs[1].payload = bytes.fromhex(
@@ -773,22 +823,25 @@ def _sign_btc_taproot_output(self) -> None:
def _sign_btc_policy(self) -> None:
bip44_account: int = 0 + HARDENED
account_keypath = [48 + HARDENED, 1 + HARDENED, bip44_account, 3 + HARDENED]
- inputs, outputs = _btc_demo_inputs_outputs(bip44_account)
- for i, inp in enumerate(inputs):
- inp["keypath"] = account_keypath + [0, i]
- inp["script_config_index"] = 0
+ coin = bitbox02.btc.TBTC
+ script_configs = [
+ bitbox02.btc.BTCScriptConfigWithKeypath(
+ script_config=self._btc_policy_config(coin),
+ keypath=account_keypath,
+ ),
+ ]
+ inputs, outputs = _btc_demo_inputs_outputs(
+ self._device,
+ bip44_account,
+ coin=coin,
+ script_configs=script_configs,
+ )
assert isinstance(outputs[0], bitbox02.BTCOutputInternal)
outputs[0].keypath = account_keypath + [1, 0]
- coin = bitbox02.btc.TBTC
sigs = self._device.btc_sign(
coin,
- [
- bitbox02.btc.BTCScriptConfigWithKeypath(
- script_config=self._btc_policy_config(coin),
- keypath=account_keypath,
- ),
- ],
+ script_configs,
inputs=inputs,
outputs=outputs,
)
@@ -801,7 +854,7 @@ def _sign_btc_op_return(
) -> None:
# pylint: disable=no-member
bip44_account: int = 0 + HARDENED
- inputs, outputs = _btc_demo_inputs_outputs(bip44_account)
+ inputs, outputs = _btc_demo_inputs_outputs(self._device, bip44_account)
outputs.append(
bitbox02.BTCOutputExternal(
output_type=bitbox02.btc.OP_RETURN,
### src/da14531/da14531_handler.c
@@ -7,9 +7,11 @@
#include "memory/memory_shared.h"
#include "screen.h"
#include "ui/screen_stack.h"
+#include "ui/ui_util.h"
#include "usb/class/usb_size.h"
#include "usb/usb_frame.h"
#include "usb/usb_packet.h"
+#include "usb/usb_processing.h"
#include <rust/rust.h>
#include <ui/components/confirm.h>
#include <ui/components/ui_images.h>
@@ -37,22 +39,41 @@ struct pairing_callback {
static struct pairing_callback _ble_pairing_callback_data;
-static void _ble_pairing_callback(bool ok, void* param)
+static void _ble_pairing_cleanup(component_t* component)
{
- struct pairing_callback* data = (struct pairing_callback*)param;
+ if (_ble_pairing_component == component) {
+ _ble_pairing_component = NULL;
+ }
+ ui_util_component_cleanup(component);
+}
+static const component_functions_t _ble_pairing_component_functions = {
+ .cleanup = _ble_pairing_cleanup,
+ .render = ui_util_component_render_subcomponents,
+ .on_event = NULL,
+};
+
+static void _ble_pairing_respond(const uint8_t* key, struct RustByteQueue* queue, bool ok)
+{
uint8_t payload[18] = {0};
payload[0] = CTRL_CMD_TK_CONFIRM;
- memcpy(&payload[1], &data->key[0], sizeof(data->key));
+ memcpy(&payload[1], key, sizeof(_ble_pairing_callback_data.key));
payload[17] = ok ? 1 : 0; /* 1 yes, 0 no */
uint8_t tmp[12 + sizeof(payload) * 2];
uint16_t len = da14531_protocol_format(
&tmp[0], sizeof(tmp), DA14531_PROTOCOL_PACKET_TYPE_CTRL_DATA, payload, sizeof(payload));
ASSERT(len <= sizeof(tmp));
for (int i = 0; i < len; i++) {
- rust_bytequeue_put(data->queue, tmp[i]);
+ rust_bytequeue_put(queue, tmp[i]);
}
+}
+
+static void _ble_pairing_callback(bool ok, void* param)
+{
+ struct pairing_callback* data = (struct pairing_callback*)param;
+
+ _ble_pairing_respond(data->key, data->queue, ok);
ui_screen_stack_pop();
_ble_pairing_component = NULL;
@@ -145,6 +166,12 @@ static void _ctrl_handler(const struct da14531_ctrl_frame* frame, struct RustByt
break;
}
#if !defined(BOOTLOADER)
+ // A running HWW task can own a Rust-backed screen. Do not overlay it: if its watchdog
+ // cancels the task, the Rust screen owner assumes that its component is still on top.
+ if (usb_processing_locked(usb_processing_hww())) {
+ _ble_pairing_respond(&frame->cmd_data[0], queue, false);
+ break;
+ }
memcpy(
&(_ble_pairing_callback_data.key)[0],
&frame->cmd_data[0],
@@ -163,6 +190,7 @@ static void _ctrl_handler(const struct da14531_ctrl_frame* frame, struct RustByt
};
_ble_pairing_component = confirm_create(
&confirm_params, _ble_pairing_callback, (void*)&_ble_pairing_callback_data);
+ _ble_pairing_component->f = &_ble_pairing_component_functions;
ui_screen_stack_push(_ble_pairing_component);
#else
memcpy(
### src/rust/bitbox02-rust-c/src/u2f_c_api.rs
@@ -34,9 +34,11 @@ static BITBOX02_HAL: GroundedCell<crate::HalImpl> = GroundedCell::const_init();
struct ActiveWorkflowGuard;
impl ActiveWorkflowGuard {
- fn new() -> Self {
- ACTIVE_WORKFLOW_COUNT.fetch_add(1, Ordering::Relaxed);
- Self
+ fn try_new() -> Option<Self> {
+ ACTIVE_WORKFLOW_COUNT
+ .compare_exchange(0, 1, Ordering::Relaxed, Ordering::Relaxed)
+ .ok()
+ .map(|_| Self)
}
}
@@ -56,6 +58,21 @@ fn next_task_token() -> u32 {
NEXT_TASK_TOKEN.fetch_add(1, Ordering::Relaxed)
}
+/// # Safety
+/// Must be called from the same single-threaded, non-reentrant execution context as all other
+/// U2F workflow C API calls.
+unsafe fn try_start_workflow() -> Option<ActiveWorkflowGuard> {
+ let guard = ActiveWorkflowGuard::try_new()?;
+ unsafe {
+ if !matches!(UNLOCK_STATE.get().as_ref().unwrap(), TaskState::Nothing)
+ || !matches!(CONFIRM_STATE.get().as_ref().unwrap(), TaskState::Nothing)
+ {
+ return None;
+ }
+ }
+ Some(guard)
+}
+
/// # Safety
/// Must not be called concurrently or reentrantly with other operations that mutate unlock
/// workflow state in this module.
@@ -91,9 +108,11 @@ unsafe fn complete_confirm(token: u32, result: Result<(), UserAbort>) {
/// 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() {
+pub unsafe extern "C" fn rust_workflow_spawn_unlock() -> bool {
+ let Some(active_workflow_guard) = (unsafe { try_start_workflow() }) else {
+ return false;
+ };
let token = next_task_token();
- let active_workflow_guard = ActiveWorkflowGuard::new();
unsafe {
UNLOCK_STATE.get().write(TaskState::Running(token));
}
@@ -104,6 +123,7 @@ pub unsafe extern "C" fn rust_workflow_spawn_unlock() {
};
unsafe { complete_unlock(token, result) };
}));
+ true
}
/// # Safety
@@ -116,11 +136,13 @@ pub unsafe extern "C" fn rust_workflow_spawn_unlock() {
pub unsafe extern "C" fn rust_workflow_spawn_confirm(
title: *const core::ffi::c_char,
body: *const core::ffi::c_char,
-) {
+) -> bool {
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 Some(active_workflow_guard) = (unsafe { try_start_workflow() }) else {
+ return false;
+ };
let token = next_task_token();
- let active_workflow_guard = ActiveWorkflowGuard::new();
unsafe {
CONFIRM_STATE.get().write(TaskState::Running(token));
}
@@ -143,6 +165,7 @@ pub unsafe extern "C" fn rust_workflow_spawn_confirm(
};
unsafe { complete_confirm(token, result) };
}));
+ true
}
/// Returns true if there was a result.
@@ -207,19 +230,23 @@ mod tests {
use super::*;
#[test]
- fn test_rust_workflow_u2f_is_active() {
+ fn test_try_start_workflow() {
assert!(!rust_workflow_u2f_is_active());
- let first_guard = ActiveWorkflowGuard::new();
- assert!(rust_workflow_u2f_is_active());
-
- {
- let _second_guard = ActiveWorkflowGuard::new();
- assert!(rust_workflow_u2f_is_active());
- }
+ let first_guard = unsafe { try_start_workflow() }.unwrap();
assert!(rust_workflow_u2f_is_active());
+ assert!(unsafe { try_start_workflow() }.is_none());
drop(first_guard);
assert!(!rust_workflow_u2f_is_active());
+
+ unsafe {
+ UNLOCK_STATE.get().write(TaskState::ResultAvailable(Ok(())));
+ }
+ assert!(unsafe { try_start_workflow() }.is_none());
+ assert!(!rust_workflow_u2f_is_active());
+ unsafe {
+ UNLOCK_STATE.get().write(TaskState::Nothing);
+ }
}
}
### src/rust/bitbox02-rust-c/src/u2f_c_api_stubs.rs
@@ -1,7 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
#[unsafe(no_mangle)]
-pub unsafe extern "C" fn rust_workflow_spawn_unlock() {}
+pub unsafe extern "C" fn rust_workflow_spawn_unlock() -> bool {
+ true
+}
#[unsafe(no_mangle)]
pub extern "C" fn rust_workflow_u2f_is_active() -> bool {
@@ -12,7 +14,7 @@ pub extern "C" fn rust_workflow_u2f_is_active() -> bool {
pub unsafe extern "C" fn rust_workflow_spawn_confirm(
_title: *const core::ffi::c_char,
_body: *const core::ffi::c_char,
-) {
+) -> bool {
panic!("unused");
}
### src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
@@ -340,10 +340,12 @@ async fn sighash_script(
}
/// Stream an input's previous transaction and verify that the prev_out_hash in the input matches
-/// the hash of the previous transaction, as well as that the amount provided in the input is correct.
+/// the hash of the previous transaction, as well as that the amount and scriptPubKey of the
+/// previous output are correct.
async fn handle_prevtx(
input_index: u32,
input: &pb::BtcSignInputRequest,
+ expected_pubkey_script: &[u8],
num_inputs: u32,
progress_component: &mut impl Progress,
next_response: &mut NextResponse,
@@ -404,10 +406,12 @@ async fn handle_prevtx(
let prevtx_output =
get_prevtx_output(input_index, prevtx_output_index, next_response).await?;
- if prevtx_output_index == input.prev_out_index
- && input.prev_out_value != prevtx_output.value
- {
- return Err(Error::InvalidInput);
+ if prevtx_output_index == input.prev_out_index {
+ if input.prev_out_value != prevtx_output.value
+ || expected_pubkey_script != prevtx_output.pubkey_script.as_slice()
+ {
+ return Err(Error::InvalidInput);
+ }
}
hasher.update(prevtx_output.value.to_le_bytes());
hasher.update(serialize(&VarInt(prevtx_output.pubkey_script.len() as u64)));
@@ -675,10 +679,10 @@ impl<'a> TryFrom<&'a ValidatedScriptConfigWithKeypath<'a>>
///
/// The hash_prevout and hash_sequence and total_in are accumulated in inputs_pass1.
///
-/// For each input in pass1, the input's prevtx is streamed to compute and compare the prevOutHash
-/// and input amount. This only happens if the script_configs in the init request contain
-/// non-taproot (legacy and v0 segwit) configs. If all inputs are taproot, this step is not needed
-/// as the input amounts and pubkey scripts are committed to in the signature hash. With
+/// For each input in pass1, the input's prevtx is streamed to compute and compare the prevOutHash,
+/// input amount and pubkey script. This only happens if the script_configs in the init request
+/// contain non-taproot (legacy and v0 segwit) configs. If all inputs are taproot, this step is not
+/// needed as the input amounts and pubkey scripts are committed to in the signature hash. With
/// SIGHASH_ALL/SIGHASH_DEFAULT, it would technically be enough if there was only one taproot input
/// to skip streaming the previous transactions, even if there are non-taproot inputs (every input
/// commits to the taproot inputs presence, and the taproot input commits to all amounts and pubkey
@@ -822,6 +826,7 @@ async fn _process(
handle_prevtx(
input_index,
&tx_input,
+ pk_script.as_slice(),
request.num_inputs,
progress_component.as_mut().unwrap(),
&mut next_response,
@@ -1396,6 +1401,83 @@ mod tests {
payment_request: Option<pb::BtcPaymentRequestRequest>,
}
+ /// Computes the transaction hash of the previous transaction in an input test fixture.
+ fn prevtx_hash(input: &TxInput) -> Vec<u8> {
+ let mut hasher = Sha256::new();
+ hasher.update(input.prevtx_version.to_le_bytes());
+ hasher.update(serialize(&VarInt(input.prevtx_inputs.len() as u64)));
+ for prevtx_input in input.prevtx_inputs.iter() {
+ hasher.update(prevtx_input.prev_out_hash.as_slice());
+ hasher.update(prevtx_input.prev_out_index.to_le_bytes());
+ hasher.update(serialize(&VarInt(
+ prevtx_input.signature_script.len() as u64
+ )));
+ hasher.update(prevtx_input.signature_script.as_slice());
+ hasher.update(prevtx_input.sequence.to_le_bytes());
+ }
+ hasher.update(serialize(&VarInt(input.prevtx_outputs.len() as u64)));
+ for prevtx_output in input.prevtx_outputs.iter() {
+ hasher.update(prevtx_output.value.to_le_bytes());
+ hasher.update(serialize(&VarInt(prevtx_output.pubkey_script.len() as u64)));
+ hasher.update(prevtx_output.pubkey_script.as_slice());
+ }
+ hasher.update(input.prevtx_locktime.to_le_bytes());
+ Sha256::digest(hasher.finalize()).to_vec()
+ }
+
+ /// Sets the selected previous outputs to scripts derived from the signing request and updates
+ /// their transaction hashes. The mock keystore must be unlocked, and the HAL must contain any
+ /// required multisig or policy registrations.
+ async fn set_prevout_scripts(
+ hal: &mut impl crate::hal::Hal,
+ transaction: &alloc::rc::Rc<core::cell::RefCell<Transaction>>,
+ init_request: &pb::BtcSignInitRequest,
+ ) {
+ let (coin_params, num_inputs) = {
+ let transaction = transaction.borrow();
+ (
+ super::super::params::get(transaction.coin),
+ transaction.inputs.len(),
+ )
+ };
+ let validated_script_configs =
+ validate_script_configs(hal, coin_params, &init_request.script_configs)
+ .await
+ .unwrap();
+ let mut xpub_cache = Bip32XpubCache::new(Compute::Once);
+ for input_index in 0..num_inputs {
+ let (script_config_index, keypath) = {
+ let transaction = transaction.borrow();
+ let input = &transaction.inputs[input_index];
+ (
+ input.input.script_config_index as usize,
+ input.input.keypath.clone(),
+ )
+ };
+ let script_config = validated_script_configs.get(script_config_index).unwrap();
+ let script =
+ common::Payload::from(hal, &mut xpub_cache, coin_params, &keypath, script_config)
+ .await
+ .unwrap()
+ .pk_script(coin_params)
+ .unwrap();
+
+ let mut transaction = transaction.borrow_mut();
+ let input = &mut transaction.inputs[input_index];
+ input.prevtx_outputs[input.input.prev_out_index as usize].pubkey_script = script;
+ input.input.prev_out_hash = prevtx_hash(input);
+ }
+ }
+
+ /// Creates the default transaction fixture with previous-output scripts matching its inputs.
+ /// The mock keystore must be unlocked before calling this helper.
+ async fn new_transaction(coin: pb::BtcCoin) -> alloc::rc::Rc<core::cell::RefCell<Transaction>> {
+ let transaction = alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(coin)));
+ let init_request = transaction.borrow().init_request();
+ set_prevout_scripts(&mut TestingHal::new(), &transaction, &init_request).await;
+ transaction
+ }
+
impl Transaction {
/// An arbitrary test transaction with some inputs and outputs.
fn new(coin: pb::BtcCoin) -> Self {
@@ -1451,7 +1533,8 @@ mod tests {
},
pb::BtcPrevTxOutputRequest {
value: 1010000000, // btc 10.1
- pubkey_script: b"pubkey script 2".to_vec(),
+ // Overwritten with the derived pubkey script before signing.
+ pubkey_script: b"placeholder".to_vec(),
},
],
prevtx_locktime: 0,
@@ -1484,7 +1567,8 @@ mod tests {
}],
prevtx_outputs: vec![pb::BtcPrevTxOutputRequest {
value: 1020000000, // btc 10.2
- pubkey_script: b"pubkey script".to_vec(),
+ // Overwritten with the derived pubkey script before signing.
+ pubkey_script: b"placeholder".to_vec(),
}],
prevtx_locktime: 87654,
host_nonce: None,
@@ -1589,7 +1673,8 @@ mod tests {
}],
prevtx_outputs: vec![pb::BtcPrevTxOutputRequest {
value: 100000, // btc 0.001
- pubkey_script: b"pubkey script".to_vec(),
+ // Overwritten with the derived pubkey script before signing.
+ pubkey_script: b"placeholder".to_vec(),
}],
prevtx_locktime: 0,
host_nonce: None,
@@ -2009,7 +2094,8 @@ mod tests {
PREVTX_REQUESTED = 0;
}
- let transaction = alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(coin)));
+ mock_unlocked();
+ let transaction = new_transaction(coin).await;
let tx = transaction.clone();
*crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() =
@@ -2021,7 +2107,6 @@ mod tests {
Ok(tx.borrow().make_host_request(response))
}));
- mock_unlocked();
let mut init_request = transaction.borrow().init_request();
init_request.format_unit = format_unit as _;
@@ -2126,7 +2211,7 @@ mod tests {
assert_eq!(
next.signature,
hex!(
- "2e084a0a5f9babb35df6ec3a89720bcfc088d4ba6aee47973c55fec3b3ddaa6007c7b11c8b5a1a6820ca74a85aeb4cf545c1b3375370f44f24d53d61fe676e4c"
+ "2738445c66add4a23ba457ddd678bc9ea3dab27030fa6a73b31c1aac7893c9aa0dd848ab8d27ff660a1f23213e0156937c9d46c261a3f809beae93760d2bce72"
)
);
}
@@ -2145,9 +2230,8 @@ mod tests {
/// Test that receiving an unexpected message from the host results in an invalid state error.
#[async_test::test]
pub async fn test_invalid_state() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
let tx = transaction.clone();
static mut COUNTER: u32 = 0;
*crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() =
@@ -2167,6 +2251,7 @@ mod tests {
/// Test signing if all inputs are of type P2WPKH-P2SH.
#[async_test::test]
pub async fn test_script_type_p2wpkh_p2sh() {
+ mock_unlocked();
let transaction =
alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
for input in transaction.borrow_mut().inputs.iter_mut() {
@@ -2177,9 +2262,6 @@ mod tests {
output.keypath[0] = 49 + HARDENED;
}
}
-
- mock_host_responder(transaction.clone());
- mock_unlocked();
let mut init_request = transaction.borrow().init_request();
init_request.script_configs[0] = pb::BtcScriptConfigWithKeypath {
script_config: Some(pb::BtcScriptConfig {
@@ -2189,14 +2271,16 @@ mod tests {
}),
keypath: vec![49 + HARDENED, 0 + HARDENED, 10 + HARDENED],
};
+ set_prevout_scripts(&mut TestingHal::new(), &transaction, &init_request).await;
+ mock_host_responder(transaction.clone());
let result = process(&mut TestingHal::new(), &init_request).await;
match result {
Ok(Response::BtcSignNext(next)) => {
assert!(next.has_signature);
assert_eq!(
next.signature,
hex!(
- "3a4618f6163c1d553bebc2c6ac08866d9f027ca663eea743658bb0581c4233a432984ccaeb52044f70474794c55446a5d823e1fb969a39132f7da230d2dd3375"
+ "157bca77b0af6618659c78bf4d7a758ac520e6088a7e648f5fb44fefeecca54c0dcc128a6a1141b2786e8618f12ecd618e81f87205fe456de4ce3d368e89d936"
)
);
}
@@ -2207,8 +2291,8 @@ mod tests {
/// Test signing if all inputs are of type P2TR.
#[async_test::test]
pub async fn test_script_type_p2tr() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
for input in transaction.borrow_mut().inputs.iter_mut() {
input.input.keypath[0] = 86 + HARDENED;
}
@@ -2230,7 +2314,6 @@ mod tests {
Ok(tx.borrow().make_host_request(response))
}));
- mock_unlocked();
let mut init_request = transaction.borrow().init_request();
init_request.script_configs[0] = pb::BtcScriptConfigWithKeypath {
script_config: Some(pb::BtcScriptConfig {
@@ -2247,7 +2330,7 @@ mod tests {
assert_eq!(
next.signature,
hex!(
- "87f00346f53b11e03175eaf5254e3aec432bfd9585465c83fb483e8ddaf0893b0460d764711b4adf5865419d06116abc174b1578372e11fcd41cd2db18c306a7"
+ "c62342dc6a5e438b18cfc6b58d07394ec3dd4bb1687d520ac2576b219e2e45987e922a8d488a4ae058cda1a40e64835de2c2c796fa5ad579659fa7e3106b1a57"
)
);
}
@@ -2260,11 +2343,25 @@ mod tests {
/// inputs should be streamed in this case.
#[async_test::test]
pub async fn test_script_type_p2tr_mixed() {
+ mock_unlocked();
let transaction =
alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
transaction.borrow_mut().inputs[0].input.script_config_index = 1;
transaction.borrow_mut().inputs[0].input.keypath[0] = 86 + HARDENED;
+ let mut init_request = transaction.borrow().init_request();
+ init_request
+ .script_configs
+ .push(pb::BtcScriptConfigWithKeypath {
+ script_config: Some(pb::BtcScriptConfig {
+ config: Some(pb::btc_script_config::Config::SimpleType(
+ SimpleType::P2tr as _,
+ )),
+ }),
+ keypath: vec![86 + HARDENED, 0 + HARDENED, 10 + HARDENED],
+ });
+ set_prevout_scripts(&mut TestingHal::new(), &transaction, &init_request).await;
+
let tx = transaction.clone();
// Check that previous transactions are streamed, as not all input are taproot.
static mut PREVTX_REQUESTED: u32 = 0;
@@ -2277,18 +2374,6 @@ mod tests {
Ok(tx.borrow().make_host_request(response))
}));
- mock_unlocked();
- let mut init_request = transaction.borrow().init_request();
- init_request
- .script_configs
- .push(pb::BtcScriptConfigWithKeypath {
- script_config: Some(pb::BtcScriptConfig {
- config: Some(pb::btc_script_config::Config::SimpleType(
- SimpleType::P2tr as _,
- )),
- }),
- keypath: vec![86 + HARDENED, 0 + HARDENED, 10 + HARDENED],
- });
assert!(process(&mut TestingHal::new(), &init_request).await.is_ok());
assert_eq!(
unsafe { PREVTX_REQUESTED },
@@ -2301,13 +2386,13 @@ mod tests {
/// spend them.
#[async_test::test]
pub async fn test_spend_high_address_index() {
+ mock_unlocked();
let transaction =
alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
transaction.borrow_mut().inputs[0].input.keypath[4] = 100000;
-
- mock_host_responder(transaction.clone());
- mock_unlocked();
let init_request = transaction.borrow().init_request();
+ set_prevout_scripts(&mut TestingHal::new(), &transaction, &init_request).await;
+ mock_host_responder(transaction.clone());
let result = process(&mut TestingHal::new(), &init_request).await;
assert!(result.is_ok());
}
@@ -2338,6 +2423,8 @@ mod tests {
WrongInputValue,
// input's prevtx hash does not match input's prevOutHash
WrongPrevoutHash,
+ // selected prevtx output's script does not match the input keypath
+ WrongPrevoutScript,
// input's prev_out_index too high
WrongPrevoutIndex,
// no inputs in prevtx
@@ -2357,12 +2444,13 @@ mod tests {
TestCase::WrongOutputValue,
TestCase::WrongInputValue,
TestCase::WrongPrevoutHash,
+ TestCase::WrongPrevoutScript,
TestCase::WrongPrevoutIndex,
TestCase::PrevTxNoInputs,
TestCase::PrevTxNoOutputs,
] {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
match value {
TestCase::WrongCoinInput => {
transaction.borrow_mut().inputs[0].input.keypath[1] = 1 + HARDENED;
@@ -2402,6 +2490,13 @@ mod tests {
TestCase::WrongPrevoutHash => {
transaction.borrow_mut().inputs[0].input.prev_out_hash[0] += 1;
}
+ TestCase::WrongPrevoutScript => {
+ let mut transaction = transaction.borrow_mut();
+ let input = &mut transaction.inputs[0];
+ let prev_out_index = input.input.prev_out_index as usize;
+ input.prevtx_outputs[prev_out_index].pubkey_script[0] ^= 1;
+ input.input.prev_out_hash = prevtx_hash(input);
+ }
TestCase::WrongPrevoutIndex => {
let mut tx = transaction.borrow_mut();
tx.inputs[0].input.prev_out_index = tx.inputs[0].prevtx_outputs.len() as _;
@@ -2414,7 +2509,6 @@ mod tests {
}
}
mock_host_responder(transaction.clone());
- mock_unlocked();
let init_request = transaction.borrow().init_request();
let result = process(&mut TestingHal::new(), &init_request).await;
assert_eq!(result, Err(Error::InvalidInput));
@@ -2424,12 +2518,11 @@ mod tests {
/// Test signing with mixed input types.
#[async_test::test]
pub async fn test_mixed_inputs() {
+ mock_unlocked();
let transaction =
alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
transaction.borrow_mut().inputs[0].input.script_config_index = 1;
transaction.borrow_mut().inputs[0].input.keypath[0] = 49 + HARDENED;
- mock_host_responder(transaction.clone());
- mock_unlocked();
let mut init_request = transaction.borrow().init_request();
init_request
.script_configs
@@ -2441,13 +2534,15 @@ mod tests {
}),
keypath: vec![49 + HARDENED, 0 + HARDENED, 10 + HARDENED],
});
+ set_prevout_scripts(&mut TestingHal::new(), &transaction, &init_request).await;
+ mock_host_responder(transaction.clone());
assert!(process(&mut TestingHal::new(), &init_request).await.is_ok());
}
#[async_test::test]
async fn test_user_aborts() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
mock_host_responder(transaction.clone());
// We go through all possible user confirmations and abort one of them at a time.
let total_confirmations = transaction.borrow().total_confirmations;
@@ -2524,13 +2619,11 @@ mod tests {
confirm: Some("Locktime on block:\n10\n"),
},
] {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(test_case.coin)));
+ mock_unlocked();
+ let transaction = new_transaction(test_case.coin).await;
transaction.borrow_mut().inputs[0].input.sequence = test_case.sequence;
mock_host_responder(transaction.clone());
- mock_unlocked();
-
let mut init_request = transaction.borrow().init_request();
init_request.locktime = test_case.locktime;
@@ -2557,13 +2650,12 @@ mod tests {
// Test a transaction with an unusually high fee.
#[async_test::test]
async fn test_high_fee_warning() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
transaction.borrow_mut().outputs[1].value = 1034567890;
// One more confirmation for the high fee warning.
transaction.borrow_mut().total_confirmations += 1;
mock_host_responder(transaction.clone());
- mock_unlocked();
let init_request = transaction.borrow().init_request();
let total_confirmations = transaction.borrow().total_confirmations;
@@ -2590,13 +2682,12 @@ mod tests {
// active on Litecoin yet.
#[async_test::test]
async fn test_p2tr_output() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
transaction.borrow_mut().outputs[0].r#type = pb::BtcOutputType::P2tr as _;
transaction.borrow_mut().outputs[0].payload =
hex!("a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c").to_vec();
mock_host_responder(transaction.clone());
- mock_unlocked();
let mut mock_hal = TestingHal::new();
let init_request = transaction.borrow().init_request();
@@ -2617,7 +2708,7 @@ mod tests {
assert_eq!(
next.signature,
hex!(
- "8f1e0e8f98d36db1196264f1a300fae317f1508d2c489fbbd660e048c4529c612f59576c86a26ffa476d97351e469ef6ed2784aecb71053a5166775ccb4d7b9b"
+ "63d8442f327c59f50baacfa64726d01d6325042e9c79a03be14504a52cfbb1cc0e9a0b7ce419b6448e280bab107abb969b5eb4c149399f23e3c479d37e99ac31"
)
);
}
@@ -2627,6 +2718,7 @@ mod tests {
#[async_test::test]
async fn test_silent_payment_output() {
+ mock_unlocked();
let transaction =
alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
@@ -2643,6 +2735,18 @@ mod tests {
Some(pb::btc_sign_output_request::SilentPayment {
address: "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv".into(),
});
+ let mut init_request = transaction.borrow().init_request();
+ init_request
+ .script_configs
+ .push(pb::BtcScriptConfigWithKeypath {
+ script_config: Some(pb::BtcScriptConfig {
+ config: Some(pb::btc_script_config::Config::SimpleType(
+ pb::btc_script_config::SimpleType::P2tr as _,
+ )),
+ }),
+ keypath: vec![86 + HARDENED, 0 + HARDENED, 10 + HARDENED],
+ });
+ set_prevout_scripts(&mut TestingHal::new(), &transaction, &init_request).await;
let tx = transaction.clone();
*crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() =
Some(Box::new(move |response: Response| {
@@ -2652,25 +2756,12 @@ mod tests {
assert_eq!(
next.generated_output_pkscript,
hex!(
- "51207b9101d60c6461ff3e18f0832e7f1e952084205062d7e0b7b08812c264cfe713"
+ "5120d93dcc0bfa6bd3f53dff16446ed4a6687272fd00966d462c269e4ec11ed98f9e"
)
);
}
Ok(tx.borrow().make_host_request(response))
}));
- mock_unlocked();
-
- let mut init_request = transaction.borrow().init_request();
- init_request
- .script_configs
- .push(pb::BtcScriptConfigWithKeypath {
- script_config: Some(pb::BtcScriptConfig {
- config: Some(pb::btc_script_config::Config::SimpleType(
- pb::btc_script_config::SimpleType::P2tr as _,
- )),
- }),
- keypath: vec![86 + HARDENED, 0 + HARDENED, 10 + HARDENED],
- });
let mut mock_hal = TestingHal::new();
assert!(process(&mut mock_hal, &init_request).await.is_ok());
@@ -2684,10 +2775,68 @@ mod tests {
);
}
+ #[async_test::test]
+ async fn test_silent_payment_rejects_input_keypath_mismatch() {
+ for (simple_type, purpose) in [
+ (SimpleType::P2wpkh, 84 + HARDENED),
+ (SimpleType::P2wpkhP2sh, 49 + HARDENED),
+ ] {
+ mock_unlocked();
+ let transaction =
+ alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+
+ for input in transaction.borrow_mut().inputs.iter_mut() {
+ input.input.keypath[0] = purpose;
+ }
+ transaction.borrow_mut().outputs[0].r#type = pb::BtcOutputType::Unknown as _;
+ transaction.borrow_mut().outputs[0].payload = vec![];
+ transaction.borrow_mut().outputs[0].silent_payment =
+ Some(pb::btc_sign_output_request::SilentPayment {
+ address: "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv".into(),
+ });
+
+ let mut init_request = transaction.borrow().init_request();
+ init_request.script_configs[0] = pb::BtcScriptConfigWithKeypath {
+ script_config: Some(pb::BtcScriptConfig {
+ config: Some(pb::btc_script_config::Config::SimpleType(simple_type as _)),
+ }),
+ keypath: vec![purpose, 0 + HARDENED, 10 + HARDENED],
+ };
+ set_prevout_scripts(&mut TestingHal::new(), &transaction, &init_request).await;
+
+ let tx = transaction.clone();
+ let first_input_seen = alloc::rc::Rc::new(core::cell::Cell::new(false));
+ let first_input_seen_callback = first_input_seen.clone();
+ *crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() =
+ Some(Box::new(move |response: Response| {
+ let next = extract_next(&response);
+ let next_type = NextType::try_from(next.r#type).unwrap();
+ let index = next.index;
+ let mut request = tx.borrow().make_host_request(response);
+ if next_type == NextType::Input
+ && index == 0
+ && !first_input_seen_callback.replace(true)
+ {
+ match &mut request {
+ Request::BtcSignInput(input) => input.keypath[4] += 1,
+ _ => panic!("wrong request type"),
+ }
+ }
+ Ok(request)
+ }));
+
+ assert_eq!(
+ process(&mut TestingHal::new(), &init_request).await,
+ Err(Error::InvalidInput)
+ );
+ assert!(first_input_seen.get());
+ }
+ }
+
#[async_test::test]
async fn test_silent_payment_rejects_ours_output() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
{
let mut tx = transaction.borrow_mut();
@@ -2703,7 +2852,6 @@ mod tests {
}
mock_host_responder(transaction.clone());
- mock_unlocked();
let init_request = transaction.borrow().init_request();
assert_eq!(
@@ -2715,11 +2863,10 @@ mod tests {
// Test an output that is sending to the same account, but is not a change output by keypath.
#[async_test::test]
async fn test_self_send_non_change_output_same_account() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
transaction.borrow_mut().outputs[5].keypath[3] = 0;
mock_host_responder(transaction.clone());
- mock_unlocked();
let mut mock_hal = TestingHal::new();
let init_request = transaction.borrow().init_request();
@@ -2743,7 +2890,7 @@ mod tests {
assert_eq!(
next.signature,
hex!(
- "e115d7d2d2b7ef068e7b89de83ec791744d46b8bae8a5931a73ef644c0db01cf2f2e2a02797a29a181fe74ea1f5d2bcaba4d70e0e7742412a680fd62957a90f7"
+ "1d4cab467bb114fd62fa803407d514980e5b18f45a66a34d31078129627d2e892894a8035b67781bae22bd8b326c3c2de7555b0bd9509a2d0adb4971ca34ae1e"
)
);
}
@@ -2754,14 +2901,13 @@ mod tests {
// Test an output that is sending to another account of our keystore.
#[async_test::test]
async fn test_self_send_different_account() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
const DIFFERENT_ACCOUNT: u32 = 20 + HARDENED;
transaction.borrow_mut().outputs[5].keypath[2] = DIFFERENT_ACCOUNT;
transaction.borrow_mut().outputs[5].keypath[3] = 0;
transaction.borrow_mut().outputs[5].output_script_config_index = Some(0);
mock_host_responder(transaction.clone());
- mock_unlocked();
let coin = transaction.borrow().coin;
let mut init_request = transaction.borrow().init_request();
init_request.output_script_configs = vec![pb::BtcScriptConfigWithKeypath {
@@ -2792,8 +2938,8 @@ mod tests {
/// Exercise the antiklepto protocol
#[async_test::test]
async fn test_antiklepto() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
let host_nonce = hex!("abababababababababababababababababababababababababababababababab");
// The host nonce commitment value does not impact this test, but an invalid commitment
// would fail the antiklepto signature check on the host. The host check is skipped here and
@@ -2808,7 +2954,6 @@ mod tests {
.input
.host_nonce_commitment = Some(host_nonce_commitment);
mock_host_responder(transaction.clone());
- mock_unlocked();
let init_request = transaction.borrow().init_request();
let result = process(&mut TestingHal::new(), &init_request).await;
match result {
@@ -2819,7 +2964,7 @@ mod tests {
assert_eq!(
next.signature,
hex!(
- "2e6de654626ee912bf2e0cf5a56749891aa98956d40e29e38b8a644d5c62cfcc44e7729284ff30f9248cd70a5457b0e2324e7c473f6600432acdc8d92fb16766"
+ "07b9c8df1b71fc841d5a379a9f568faffc4f7311e74a7ab3ec1f8f1b0d1e8b1234ded6bacb2532b653f0c1f3039fd348a86d1663db763b7e2c445fe9988c3f60"
)
);
}
@@ -2830,8 +2975,8 @@ mod tests {
/// The sum of the inputs in the 2nd pass can't be higher than in the first for all inputs.
#[async_test::test]
async fn test_input_sum_changes() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
static mut PASS2_INPUT_REQUESTS_COUNTER: u32 = 0;
*crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() = {
let tx = transaction.clone();
@@ -2861,7 +3006,6 @@ mod tests {
Ok(tx.make_host_request(response))
}))
};
- mock_unlocked();
let init_request = transaction.borrow().init_request();
let result = process(&mut TestingHal::new(), &init_request).await;
assert_eq!(result, Err(Error::InvalidInput));
@@ -2874,8 +3018,8 @@ mod tests {
/// inputs in the first pass.
#[async_test::test]
async fn test_input_sum_last_mismatch() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
static mut PASS2_INPUT_REQUESTS_COUNTER: u32 = 0;
*crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() = {
let tx = transaction.clone();
@@ -2899,7 +3043,6 @@ mod tests {
Ok(tx.make_host_request(response))
}))
};
- mock_unlocked();
let init_request = transaction.borrow().init_request();
let result = process(&mut TestingHal::new(), &init_request).await;
assert_eq!(result, Err(Error::InvalidInput));
@@ -2914,8 +3057,8 @@ mod tests {
/// Outgoing sum overflows.
#[async_test::test]
async fn test_overflow_output_out() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
*crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() = {
let tx = transaction.clone();
Some(Box::new(move |response: Response| {
@@ -2934,7 +3077,6 @@ mod tests {
}
}))
};
- mock_unlocked();
let init_request = transaction.borrow().init_request();
let result = process(&mut TestingHal::new(), &init_request).await;
assert_eq!(result, Err(Error::InvalidInput));
@@ -2943,8 +3085,8 @@ mod tests {
/// Outgoing change overflows.
#[async_test::test]
async fn test_overflow_output_ours() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
*crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() = {
let tx = transaction.clone();
Some(Box::new(move |response: Response| {
@@ -2963,7 +3105,6 @@ mod tests {
}
}))
};
- mock_unlocked();
let init_request = transaction.borrow().init_request();
let result = process(&mut TestingHal::new(), &init_request).await;
assert_eq!(result, Err(Error::InvalidInput));
@@ -3028,14 +3169,15 @@ mod tests {
}
};
+ set_prevout_scripts(&mut mock_hal, &transaction, &init_request).await;
let result = process(&mut mock_hal, &init_request).await;
match result {
Ok(Response::BtcSignNext(next)) => {
assert!(next.has_signature);
assert_eq!(
next.signature,
hex!(
- "1bee37e9123fd37fb8be2dd253ea810a021302e14962f46eeea979d96ffb4c6769d007de360f50e1de378de48e7a9fc79c47245b360daf27647529c92e86b203"
+ "5f4ff35de020fdf55813f1bab8eda330440199329c02fbb9aa58292fda61c35873c44c9328d29e3f6f6a1dd20d6863aa0ad956cb8bcfbe3b023f16b99d4b89ee"
)
);
}
@@ -3197,14 +3339,15 @@ mod tests {
contains_silent_payment_outputs: false,
}
};
+ set_prevout_scripts(&mut mock_hal, &transaction, &init_request).await;
let result = process(&mut mock_hal, &init_request).await;
match result {
Ok(Response::BtcSignNext(next)) => {
assert!(next.has_signature);
assert_eq!(
next.signature,
hex!(
- "a72342869a29b02433faae2ac5c49f033effd3a6b60623878ef7bf8b14dee2a03a76511b37baf15e707507f48b10cdf5a8f30b0ada4da22a38a5476f69911d8e"
+ "e80ddbc806ba13e34239af24668267e6aae4a8522f56a9ee2c94496088da614614ddd85f80f15d87fed85086dce56521b3c32fc7f809483cdd1a75c36bb43bde"
)
);
}
@@ -3281,14 +3424,15 @@ mod tests {
contains_silent_payment_outputs: false,
}
};
+ set_prevout_scripts(&mut mock_hal, &transaction, &init_request).await;
let result = process(&mut mock_hal, &init_request).await;
match result {
Ok(Response::BtcSignNext(next)) => {
assert!(next.has_signature);
assert_eq!(
next.signature,
hex!(
- "dbed8b1aefbdcfd7f3e6d9dff5ec83c5ed77cad7278b06c5f4d33072f300c2d613d166171c54d202415b5344a92d4f6f9b36ac314dc93e18bdcf6135de4d11bf"
+ "b6fc8262c84c39a8ea9ec9e59378f5290838835909f20ab6f9ae9eb4c35ad3fb06a05407f1297694d26976f3d1ae5e8bad0130dc3fd2d7ca02e653d9f5aeeb03"
)
);
}
@@ -3355,14 +3499,15 @@ mod tests {
let init_request = transaction
.borrow()
.init_request_policy(policy, keypath_account);
+ set_prevout_scripts(&mut mock_hal, &transaction, &init_request).await;
let result = process(&mut mock_hal, &init_request).await;
match result {
Ok(Response::BtcSignNext(next)) => {
assert!(next.has_signature);
assert_eq!(
next.signature,
hex!(
- "5736b8eec7594ad906daf8d3fac64d58aed35fc50726b0ed6d5fb1c8019fcab0606ced7d09bc9a75fadf5ba45cc95dc15fb6796997466739a9f6383bd159dae4"
+ "bef93371f3c8fdf8c844a09b0bf0a3414767f68ba87d07ff3441a03c681cab887c337e3a982ecfdc84a8c15f0ebf50ae4ae92772f56431142fbe21ee8878a96c"
)
);
}
@@ -3679,14 +3824,15 @@ mod tests {
let init_request = transaction
.borrow()
.init_request_policy(policy, keypath_account);
+ set_prevout_scripts(&mut mock_hal, &transaction, &init_request).await;
let result = process(&mut mock_hal, &init_request).await;
match result {
Ok(Response::BtcSignNext(next)) => {
assert!(next.has_signature);
assert_eq!(
next.signature,
hex!(
- "1c6b5465859db7dbd88f174d07a9df416d6dfa1e742903989584cd72e989d141485ad9d712df2852a6500e06856404959c010d5254353d11ab3167377ed4ee88"
+ "2454a136a45a0af77632475a901bad2324123ed65cf9c9ec3ba2847a65e749b0718a0d1968b524739bee4e38aa119924733ee09d016093b32b6861eb57ba0e5f"
)
);
}
@@ -3813,8 +3959,8 @@ mod tests {
#[async_test::test]
pub async fn test_payment_request() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
// Attach second output to a payment request.
{
@@ -3845,7 +3991,6 @@ mod tests {
}
mock_host_responder(transaction.clone());
- mock_unlocked();
let init_request = transaction.borrow().init_request();
let mut mock_hal = TestingHal::new();
@@ -3907,8 +4052,8 @@ mod tests {
#[async_test::test]
pub async fn test_payment_request_rejects_ours_output() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
{
let mut tx = transaction.borrow_mut();
@@ -3940,7 +4085,6 @@ mod tests {
}
mock_host_responder(transaction.clone());
- mock_unlocked();
let init_request = transaction.borrow().init_request();
assert_eq!(
@@ -4026,8 +4170,8 @@ mod tests {
#[async_test::test]
pub async fn test_swap_payment_request() {
// End-to-end swap signing: swap screens appear, then the regular BTC confirmations continue.
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
{
let mut tx = transaction.borrow_mut();
@@ -4115,9 +4259,8 @@ mod tests {
#[async_test::test]
pub async fn test_swap_payment_request_unsupported_source_coin() {
// Swap UI is restricted to BTC/LTC source accounts; other BTC-like coins must fail early.
- let transaction = alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(
- pb::BtcCoin::Tbtc,
- )));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Tbtc).await;
{
let mut tx = transaction.borrow_mut();
@@ -4162,8 +4305,8 @@ mod tests {
#[async_test::test]
async fn test_op_return() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
// Attach OP_RETURN output
{
@@ -4177,7 +4320,6 @@ mod tests {
}
mock_host_responder(transaction.clone());
- mock_unlocked();
let init_request = transaction.borrow().init_request();
let mut mock_hal = TestingHal::new();
@@ -4189,7 +4331,7 @@ mod tests {
assert_eq!(
next.signature,
hex!(
- "f49c71b89ec3510ebebae9aff9f967ad9bb6cc0c4cddbdf851f97e47e9922646622459e522b0751fa246e49a8e48417344a5384a9f68c1c85cd03804b35e1e1e"
+ "6c3c1156a5ce8c3382d81f1858649c9f595d4e251df361c90b7498f3907d756c7aded53e9eb668c81b50607b35e04127cd6eabd3dc4e36538bae283d89900ad0"
),
);
}
@@ -4201,8 +4343,8 @@ mod tests {
#[async_test::test]
async fn test_op_return_nonascii() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
// Attach OP_RETURN output
{
@@ -4216,7 +4358,6 @@ mod tests {
}
mock_host_responder(transaction.clone());
- mock_unlocked();
let init_request = transaction.borrow().init_request();
let mut mock_hal = TestingHal::new();
@@ -4232,8 +4373,8 @@ mod tests {
#[async_test::test]
async fn test_op_return_fail_nonzero_value() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
// Attach OP_RETURN output
{
@@ -4247,7 +4388,6 @@ mod tests {
}
mock_host_responder(transaction.clone());
- mock_unlocked();
let init_request = transaction.borrow().init_request();
let mut mock_hal = TestingHal::new();
@@ -4259,8 +4399,8 @@ mod tests {
#[async_test::test]
async fn test_op_return_rejects_ours_output() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
{
let mut tx = transaction.borrow_mut();
@@ -4273,7 +4413,6 @@ mod tests {
}
mock_host_responder(transaction.clone());
- mock_unlocked();
let init_request = transaction.borrow().init_request();
assert_eq!(
@@ -4284,8 +4423,8 @@ mod tests {
#[async_test::test]
async fn test_op_return_rejects_silent_payment_output() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
{
let mut tx = transaction.borrow_mut();
@@ -4298,7 +4437,6 @@ mod tests {
}
mock_host_responder(transaction.clone());
- mock_unlocked();
let init_request = transaction.borrow().init_request();
assert_eq!(
@@ -4309,8 +4447,8 @@ mod tests {
#[async_test::test]
async fn test_op_return_rejects_payment_request_output() {
- let transaction =
- alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+ mock_unlocked();
+ let transaction = new_transaction(pb::BtcCoin::Btc).await;
{
let mut tx = transaction.borrow_mut();
@@ -4324,7 +4462,6 @@ mod tests {
}
mock_host_responder(transaction.clone());
- mock_unlocked();
let init_request = transaction.borrow().init_request();
assert_eq!(
### src/u2f.c
@@ -2,6 +2,7 @@
#include "u2f.h"
#include "u2f/u2f_app.h"
+#include <stddef.h>
#include <stdio.h>
#include <string.h>
@@ -39,6 +40,8 @@ typedef struct {
#define U2F_KEYHANDLE_LEN (U2F_NONCE_LENGTH + SHA256_LEN)
#define SHA256_LEN 32
#define HMAC_SHA256_LEN 32
+#define U2F_AUTHENTICATE_REQ_LEN (offsetof(U2F_AUTHENTICATE_REQ, keyHandle) + U2F_KEYHANDLE_LEN)
+#define U2F_MAX_PENDING_APDU_LEN (sizeof(USB_APDU) + U2F_AUTHENTICATE_REQ_LEN)
#if (U2F_EC_KEY_SIZE != SHA256_LEN) || (U2F_EC_KEY_SIZE != U2F_NONCE_LENGTH)
#error "Incorrect macro values for u2f"
@@ -60,13 +63,13 @@ typedef enum {
typedef struct {
/**
- * CID and last command are used to check that we eventually respond to the right command after
- * the user has confirmed on screen. U2F is normally stateless between transactions, but since
- * we have a display and Yes/No input we can ask the user on the first request and later respond
- * that the user is present only if the user confirmed specifically that app-id.
+ * Last command and pending APDU are used to check that we eventually respond to the exact
+ * request that the user confirmed on screen. The transport CID is deliberately not part of the
+ * pending request, as U2F clients can use a new CID for every transaction.
*/
- uint32_t cid;
uint8_t last_cmd;
+ uint8_t pending_apdu[U2F_MAX_PENDING_APDU_LEN];
+ size_t pending_apdu_len;
/**
* Keeps track of which part of a registration we're currently in.
*/
@@ -133,6 +136,29 @@ static void _clear_state(void)
{
_state.reg = U2F_REGISTER_IDLE;
_state.auth = U2F_AUTHENTICATE_IDLE;
+ _state.pending_apdu_len = 0;
+}
+
+static size_t _apdu_len(const USB_APDU* apdu)
+{
+ return sizeof(USB_APDU) + APDU_LEN(*apdu);
+}
+
+static void _set_pending_apdu(const USB_APDU* apdu)
+{
+ const size_t len = _apdu_len(apdu);
+ if (len > sizeof(_state.pending_apdu)) {
+ Abort("U2F pending APDU too large");
+ }
+ memcpy(_state.pending_apdu, apdu, len);
+ _state.pending_apdu_len = len;
+}
+
+static bool _is_pending_apdu(const USB_APDU* apdu)
+{
+ const size_t len = _apdu_len(apdu);
+ return len == _state.pending_apdu_len && len <= sizeof(_state.pending_apdu) &&
+ MEMEQ(apdu, _state.pending_apdu, len);
}
static component_t* _nudge_label = NULL;
@@ -198,35 +224,33 @@ static void _stop_refresh_webpage_screen(void)
*
* @return Unlock success status:
* * ASYNC_OP_TRUE if the BB02 is unlocked;
- * * ASYNC_OP_NOT_READY if the BB02 wasn't unlocked, but is now
- * ("Refresh webpage" screen was started).
- * * ASYNC_OP_FALSE if the BB02 couldn't be unlocked.
+ * * ASYNC_OP_NOT_READY if an unlock workflow was started;
+ * * ASYNC_OP_FALSE if another U2F workflow is already active.
*/
-static bool _unlock_if_locked(void)
+static async_op_result_t _unlock_if_locked(void)
{
if (rust_keystore_is_locked()) {
- rust_workflow_spawn_unlock();
- return false;
+ return rust_workflow_spawn_unlock() ? ASYNC_OP_NOT_READY : ASYNC_OP_FALSE;
}
/* Pop the "refresh webpage" screen if any */
_stop_refresh_webpage_screen();
- return true;
+ return ASYNC_OP_TRUE;
}
static uint32_t _next_cid(void)
{
+ uint32_t cid;
do {
- _state.cid = (random_byte_mcu() << 0) + (random_byte_mcu() << 8) +
- (random_byte_mcu() << 16) + (random_byte_mcu() << 24);
- } while (_state.cid == 0 || _state.cid == U2FHID_CID_BROADCAST);
- return _state.cid;
+ cid = ((uint32_t)random_byte_mcu() << 0) | ((uint32_t)random_byte_mcu() << 8) |
+ ((uint32_t)random_byte_mcu() << 16) | ((uint32_t)random_byte_mcu() << 24);
+ } while (cid == 0 || cid == U2FHID_CID_BROADCAST);
+ return cid;
}
static void _fill_message(const uint8_t* data, const uint32_t len, Packet* out_packet)
{
util_zero(out_packet->data_addr, sizeof(out_packet->data_addr));
memcpy(out_packet->data_addr, data, len);
- out_packet->cid = _state.cid;
out_packet->cmd = U2FHID_MSG;
out_packet->len = len;
}
@@ -360,8 +384,7 @@ static int _sig_to_der(const uint8_t* sig, uint8_t* der)
*/
static void _assert_unlocked(void)
{
- bool was_unlocked = _unlock_if_locked();
- if (!was_unlocked) {
+ if (_unlock_if_locked() != ASYNC_OP_TRUE) {
Abort("Bad BB02 lock status after refresh");
}
}
@@ -376,7 +399,7 @@ static void _assert_unlocked(void)
*/
static uint16_t _register_sanity_check_req(const USB_APDU* apdu)
{
- if (APDU_LEN(*apdu) < U2F_KEYHANDLE_LEN) { // actual size could vary
+ if (APDU_LEN(*apdu) != sizeof(U2F_REGISTER_REQ)) {
return U2F_SW_WRONG_LENGTH;
}
@@ -390,10 +413,13 @@ static uint16_t _register_sanity_check_req(const USB_APDU* apdu)
/**
* Starts the registration "confirm" screen.
*/
-static void _register_start_confirm(const uint8_t* app_id)
+static void _register_start_confirm(const USB_APDU* apdu)
{
- _state.reg = U2F_REGISTER_CONFIRMING;
- u2f_app_confirm_start(U2F_APP_REGISTER, app_id);
+ const U2F_REGISTER_REQ* reg_request = (const U2F_REGISTER_REQ*)apdu->data;
+ if (u2f_app_confirm_start(U2F_APP_REGISTER, reg_request->appId)) {
+ _set_pending_apdu(apdu);
+ _state.reg = U2F_REGISTER_CONFIRMING;
+ }
}
/**
@@ -402,29 +428,31 @@ static void _register_start_confirm(const uint8_t* app_id)
*/
static void _register_start(const USB_APDU* apdu, Packet* out_packet)
{
- const U2F_REGISTER_REQ* reg_request = (const U2F_REGISTER_REQ*)apdu->data;
uint16_t req_error = _register_sanity_check_req(apdu);
if (req_error) {
_clear_state();
_error(req_error, out_packet);
return;
}
- // If it fails to unlock it will call _unlock()
- bool is_unlocked = _unlock_if_locked();
- if (!is_unlocked) {
+ async_op_result_t unlock_result = _unlock_if_locked();
+ if (unlock_result == ASYNC_OP_NOT_READY) {
_state.reg = U2F_REGISTER_UNLOCKING;
- } else {
- _register_start_confirm(reg_request->appId);
+ } else if (unlock_result == ASYNC_OP_TRUE) {
+ _register_start_confirm(apdu);
}
_error(U2F_SW_CONDITIONS_NOT_SATISFIED, out_packet);
}
static void _register_wait_refresh(const USB_APDU* apdu, Packet* out_packet)
{
_assert_unlocked();
- const U2F_REGISTER_REQ* reg_request = (const U2F_REGISTER_REQ*)apdu->data;
- _register_start_confirm(reg_request->appId);
+ uint16_t req_error = _register_sanity_check_req(apdu);
+ if (req_error) {
+ _error(req_error, out_packet);
+ return;
+ }
+ _register_start_confirm(apdu);
_error(U2F_SW_CONDITIONS_NOT_SATISFIED, out_packet);
}
@@ -516,7 +544,13 @@ static void _register_continue(const USB_APDU* apdu, Packet* out_packet)
*/
static uint16_t _authenticate_sanity_check_req(const USB_APDU* apdu)
{
- if (APDU_LEN(*apdu) < U2F_KEYHANDLE_LEN) { // actual size could vary
+ const uint32_t len = APDU_LEN(*apdu);
+ if (len < offsetof(U2F_AUTHENTICATE_REQ, keyHandle)) {
+ return U2F_SW_WRONG_LENGTH;
+ }
+ const U2F_AUTHENTICATE_REQ* auth_request = (const U2F_AUTHENTICATE_REQ*)apdu->data;
+ if (auth_request->keyHandleLength != U2F_KEYHANDLE_LEN ||
+ len != offsetof(U2F_AUTHENTICATE_REQ, keyHandle) + auth_request->keyHandleLength) {
return U2F_SW_WRONG_LENGTH;
}
@@ -557,8 +591,11 @@ static uint16_t _authenticate_start_confirm(const USB_APDU* apdu)
if (key_error) {
return key_error;
}
+ if (!u2f_app_confirm_start(U2F_APP_AUTHENTICATE, auth_request->appId)) {
+ return U2F_SW_CONDITIONS_NOT_SATISFIED;
+ }
+ _set_pending_apdu(apdu);
_state.auth = U2F_AUTHENTICATE_CONFIRMING;
- u2f_app_confirm_start(U2F_APP_AUTHENTICATE, auth_request->appId);
return 0;
}
@@ -571,13 +608,16 @@ static void _authenticate_start(const USB_APDU* apdu, Packet* out_packet)
return;
}
- // If it fails to unlock it will call _unlock()
- bool is_unlocked = _unlock_if_locked();
- if (!is_unlocked) {
+ async_op_result_t unlock_result = _unlock_if_locked();
+ if (unlock_result == ASYNC_OP_NOT_READY) {
_state.auth = U2F_AUTHENTICATE_UNLOCKING;
_error(U2F_SW_CONDITIONS_NOT_SATISFIED, out_packet);
return;
}
+ if (unlock_result == ASYNC_OP_FALSE) {
+ _error(U2F_SW_CONDITIONS_NOT_SATISFIED, out_packet);
+ return;
+ }
uint16_t key_error = _authenticate_start_confirm(apdu);
if (key_error) {
_clear_state();
@@ -590,6 +630,11 @@ static void _authenticate_start(const USB_APDU* apdu, Packet* out_packet)
static void _authenticate_wait_refresh(const USB_APDU* apdu, Packet* out_packet)
{
_assert_unlocked();
+ uint16_t req_error = _authenticate_sanity_check_req(apdu);
+ if (req_error) {
+ _error(req_error, out_packet);
+ return;
+ }
uint16_t key_error = _authenticate_start_confirm(apdu);
if (key_error) {
_clear_state();
@@ -606,16 +651,16 @@ static void _authenticate_continue(const USB_APDU* apdu, Packet* out_packet)
uint8_t mac[HMAC_SHA256_LEN];
uint8_t sig[64] = {0};
U2F_AUTHENTICATE_SIG_STR sig_base;
+ // Keep the outstanding confirmation on malformed retries so a valid retry can still consume
+ // its result.
uint16_t req_error = _authenticate_sanity_check_req(apdu);
if (req_error) {
- _clear_state();
_error(req_error, out_packet);
return;
}
uint16_t key_error = _authenticate_verify_key_valid(apdu);
if (key_error) {
- _clear_state();
_error(key_error, out_packet);
return;
}
@@ -816,8 +861,7 @@ static void _cmd_authenticate(const Packet* in_packet, Packet* out_packet)
{
const USB_APDU* apdu = (const USB_APDU*)in_packet->data_addr;
/* Sanity-check our state. */
- if (_state.auth != U2F_AUTHENTICATE_IDLE &&
- (_state.last_cmd != U2F_AUTHENTICATE || _state.cid != in_packet->cid)) {
+ if (_state.auth != U2F_AUTHENTICATE_IDLE && _state.last_cmd != U2F_AUTHENTICATE) {
util_log("u2f: ERROR authenticate invalid state");
_clear_state();
return;
@@ -848,15 +892,14 @@ static void _cmd_msg(const Packet* in_packet, Packet* out_packet, const size_t m
{
(void)max_out_len;
- // By default always use the recieved cid
- _state.cid = in_packet->cid;
-
const USB_APDU* apdu = (const USB_APDU*)in_packet->data_addr;
if ((APDU_LEN(*apdu) + sizeof(USB_APDU)) > in_packet->len) {
return;
}
+ // The CID routes only this response; workflow ownership is bound to the pending APDU.
+ out_packet->cid = in_packet->cid;
usb_processing_lock(usb_processing_u2f());
if (apdu->cla != 0) {
@@ -866,10 +909,20 @@ static void _cmd_msg(const Packet* in_packet, Packet* out_packet, const size_t m
switch (apdu->ins) {
case U2F_REGISTER:
+ if (_state.auth != U2F_AUTHENTICATE_IDLE ||
+ (_state.reg == U2F_REGISTER_CONFIRMING && !_is_pending_apdu(apdu))) {
+ _error(U2F_SW_CONDITIONS_NOT_SATISFIED, out_packet);
+ return;
+ }
_cmd_register(in_packet, out_packet);
_state.last_cmd = apdu->ins;
break;
case U2F_AUTHENTICATE:
+ if (_state.reg != U2F_REGISTER_IDLE ||
+ (_state.auth == U2F_AUTHENTICATE_CONFIRMING && !_is_pending_apdu(apdu))) {
+ _error(U2F_SW_CONDITIONS_NOT_SATISFIED, out_packet);
+ return;
+ }
_cmd_authenticate(in_packet, out_packet);
_state.last_cmd = apdu->ins;
break;
@@ -892,8 +945,8 @@ bool u2f_blocking_request_can_go_through(const Packet* in_packet)
void u2f_blocked_req_error(Packet* out_packet, const Packet* in_packet)
{
- (void)in_packet;
_error(U2F_SW_CONDITIONS_NOT_SATISFIED, out_packet);
+ out_packet->cid = in_packet->cid;
}
static void _process_register_wait_unlock(void)
### src/u2f/u2f_app.c
@@ -35,7 +35,7 @@ static bool _is_app_id_bogus(const uint8_t* app_id)
MEMEQ(app_id, APPID_BOGUS_FIREFOX, U2F_APPID_SIZE);
}
-void u2f_app_confirm_start(enum u2f_app_confirm_t type, const uint8_t* app_id)
+bool u2f_app_confirm_start(enum u2f_app_confirm_t type, const uint8_t* app_id)
{
char app_string[100] = {0};
const char* title;
@@ -58,9 +58,12 @@ void u2f_app_confirm_start(enum u2f_app_confirm_t type, const uint8_t* app_id)
default:
Abort("u2f_app_confirm: Internal error");
}
+ if (!rust_workflow_spawn_confirm(title, app_string)) {
+ return false;
+ }
_state.outstanding_confirm = type;
memcpy(_state.app_id, app_id, 32);
- rust_workflow_spawn_confirm(title, app_string);
+ return true;
}
async_op_result_t u2f_app_confirm_retry(enum u2f_app_confirm_t type, const uint8_t* app_id)
### src/u2f/u2f_app.h
@@ -26,10 +26,9 @@ enum u2f_app_confirm_t {
*
* @param[in] type show registration or authentication screen.
* @param[in] app_id U2F app ID to identify the website.
- * @param[out] result true if the user accepts, false for rejection.
- * @return Ready if result is ready, NotReady otherwise
+ * @return true if the confirmation workflow was started, false if another U2F workflow is active.
*/
-void u2f_app_confirm_start(enum u2f_app_confirm_t type, const uint8_t* app_id);
+bool u2f_app_confirm_start(enum u2f_app_confirm_t type, const uint8_t* app_id);
/**
* Polls an outstanding confirmation for completion.
### test/unit-test/CMakeLists.txt
@@ -33,10 +33,14 @@ else()
""
cleanup
"-Wl,--wrap=util_cleanup_32"
+ da14531_handler
+ "-Wl,--wrap=confirm_create"
gestures
""
hww
"-Wl,--wrap=rust_workflow_u2f_is_active"
+ u2f_state
+ ""
random
"-Wl,--wrap=rand,--wrap=rust_sha256"
screen_process
@@ -101,6 +105,11 @@ else()
${CMAKE_SOURCE_DIR}/external/asf4-drivers/hal/utils/include
)
endif()
+ if(TEST_NAME STREQUAL "u2f_state")
+ target_include_directories(${EXE} PRIVATE
+ ${CMAKE_SOURCE_DIR}/external/asf4-drivers/hal/utils/include
+ )
+ endif()
if(TEST_NAME STREQUAL "stage0_descriptor")
target_sources(${EXE} PRIVATE ${CMAKE_SOURCE_DIR}/src/bootloader/stage0/stage0_descriptor.c)
endif()
### test/unit-test/test_da14531_handler.c
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <cmocka.h>
+
+#include <da14531/da14531.h>
+#include <da14531/da14531_handler.h>
+#include <da14531/da14531_protocol.h>
+#include <rust/rust.h>
+#include <ui/components/confirm.h>
+#include <usb/usb_processing.h>
+
+#include <string.h>
+
+component_t* __wrap_confirm_create(
+ const confirm_params_t* params,
+ void (*callback)(bool, void*),
+ void* callback_param)
+{
+ (void)params;
+ (void)callback;
+ (void)callback_param;
+ fail_msg("pairing confirmation created during an HWW workflow");
+ return NULL;
+}
+
+static void test_pairing_code_rejected_during_hww_workflow(void** state)
+{
+ (void)state;
+
+ const uint8_t key[] = {0x12, 0x34, 0x56, 0x78};
+ uint8_t frame_buf[sizeof(struct da14531_protocol_frame) + 1 + sizeof(key)] = {0};
+ struct da14531_protocol_frame* frame = (struct da14531_protocol_frame*)frame_buf;
+ frame->type = DA14531_PROTOCOL_PACKET_TYPE_CTRL_DATA;
+ frame->payload_length = 1 + sizeof(key);
+ frame->payload[0] = CTRL_CMD_PAIRING_CODE;
+ memcpy(&frame->payload[1], key, sizeof(key));
+
+ struct RustByteQueue* queue = rust_bytequeue_init(64);
+ assert_non_null(queue);
+ usb_processing_lock(usb_processing_hww());
+ da14531_handler(frame, queue);
+ usb_processing_unlock();
+
+ uint8_t response_payload[18] = {0};
+ response_payload[0] = CTRL_CMD_TK_CONFIRM;
+ memcpy(&response_payload[1], key, sizeof(key));
+ uint8_t expected[12 + sizeof(response_payload) * 2] = {0};
+ const uint16_t expected_len = da14531_protocol_format(
+ expected,
+ sizeof(expected),
+ DA14531_PROTOCOL_PACKET_TYPE_CTRL_DATA,
+ response_payload,
+ sizeof(response_payload));
+ assert_int_equal(rust_bytequeue_num(queue), expected_len);
+ for (uint16_t i = 0; i < expected_len; i++) {
+ uint8_t actual;
+ assert_true(rust_bytequeue_get(queue, &actual));
+ assert_int_equal(actual, expected[i]);
+ }
+ assert_true(rust_bytequeue_free(queue));
+}
+
+int main(void)
+{
+ const struct CMUnitTest tests[] = {
+ cmocka_unit_test(test_pairing_code_rejected_during_hww_workflow),
+ };
+ return cmocka_run_group_tests(tests, NULL, NULL);
+}
### test/unit-test/test_u2f_state.c
@@ -0,0 +1,310 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <cmocka.h>
+
+#include <string.h>
+
+#include "u2f.c"
+
+static void _set_confirming_state(uint8_t instruction, const USB_APDU* pending_apdu)
+{
+ memset(&_state, 0, sizeof(_state));
+ _state.last_cmd = instruction;
+ _set_pending_apdu(pending_apdu);
+ if (instruction == U2F_REGISTER) {
+ _state.reg = U2F_REGISTER_CONFIRMING;
+ } else {
+ _state.auth = U2F_AUTHENTICATE_CONFIRMING;
+ }
+}
+
+static Packet _message(uint8_t instruction, uint32_t cid)
+{
+ Packet packet = {
+ .len = sizeof(USB_APDU),
+ .cmd = U2FHID_MSG,
+ .cid = cid,
+ };
+ USB_APDU* apdu = (USB_APDU*)packet.data_addr;
+ apdu->ins = instruction;
+ return packet;
+}
+
+static Packet _register_message(uint32_t cid)
+{
+ Packet packet = {
+ .len = sizeof(USB_APDU) + sizeof(U2F_REGISTER_REQ),
+ .cmd = U2FHID_MSG,
+ .cid = cid,
+ };
+ USB_APDU* apdu = (USB_APDU*)packet.data_addr;
+ apdu->ins = U2F_REGISTER;
+ apdu->p1 = U2F_AUTH_ENFORCE;
+ apdu->lc3 = sizeof(U2F_REGISTER_REQ);
+ U2F_REGISTER_REQ* request = (U2F_REGISTER_REQ*)apdu->data;
+ memset(request->challenge, 0x11, sizeof(request->challenge));
+ memset(request->appId, 0x22, sizeof(request->appId));
+ return packet;
+}
+
+static Packet _authenticate_message(uint32_t cid)
+{
+ Packet packet = {
+ .len = sizeof(USB_APDU) + U2F_AUTHENTICATE_REQ_LEN,
+ .cmd = U2FHID_MSG,
+ .cid = cid,
+ };
+ USB_APDU* apdu = (USB_APDU*)packet.data_addr;
+ apdu->ins = U2F_AUTHENTICATE;
+ apdu->p1 = U2F_AUTH_ENFORCE;
+ apdu->lc3 = U2F_AUTHENTICATE_REQ_LEN;
+ U2F_AUTHENTICATE_REQ* request = (U2F_AUTHENTICATE_REQ*)apdu->data;
+ memset(request->challenge, 0x33, sizeof(request->challenge));
+ memset(request->appId, 0x44, sizeof(request->appId));
+ request->keyHandleLength = U2F_KEYHANDLE_LEN;
+ memset(request->keyHandle, 0x55, request->keyHandleLength);
+ return packet;
+}
+
+static void test_authenticate_bad_retry_preserves_confirmation(void** state)
+{
+ (void)state;
+
+ Packet pending_packet = _authenticate_message(0x12345678);
+ _set_confirming_state(U2F_AUTHENTICATE, (const USB_APDU*)pending_packet.data_addr);
+
+ USB_APDU apdu = {
+ .cla = 0,
+ .ins = U2F_AUTHENTICATE,
+ };
+ Packet out_packet = {.cid = 0x12345678};
+ _authenticate_continue(&apdu, &out_packet);
+
+ assert_int_equal(_state.last_cmd, U2F_AUTHENTICATE);
+ assert_int_equal(_state.auth, U2F_AUTHENTICATE_CONFIRMING);
+ assert_true(_is_pending_apdu((const USB_APDU*)pending_packet.data_addr));
+ assert_int_equal(out_packet.cid, 0x12345678);
+ assert_int_equal(out_packet.len, 2);
+ assert_int_equal(out_packet.data_addr[0], U2F_SW_WRONG_LENGTH >> 8);
+ assert_int_equal(out_packet.data_addr[1], U2F_SW_WRONG_LENGTH & 0xff);
+}
+
+static void test_request_length_validation(void** state)
+{
+ (void)state;
+
+ Packet register_packet = _register_message(0x12345678);
+ USB_APDU* register_apdu = (USB_APDU*)register_packet.data_addr;
+ register_apdu->lc3--;
+ assert_int_equal(_register_sanity_check_req(register_apdu), U2F_SW_WRONG_LENGTH);
+ register_apdu->lc3 += 2;
+ assert_int_equal(_register_sanity_check_req(register_apdu), U2F_SW_WRONG_LENGTH);
+
+ Packet authenticate_packet = _authenticate_message(0x12345678);
+ USB_APDU* authenticate_apdu = (USB_APDU*)authenticate_packet.data_addr;
+ U2F_AUTHENTICATE_REQ* authenticate_request = (U2F_AUTHENTICATE_REQ*)authenticate_apdu->data;
+
+ authenticate_apdu->lc3 = offsetof(U2F_AUTHENTICATE_REQ, keyHandle) - 1;
+ assert_int_equal(_authenticate_sanity_check_req(authenticate_apdu), U2F_SW_WRONG_LENGTH);
+ authenticate_apdu->lc3 = U2F_AUTHENTICATE_REQ_LEN;
+ authenticate_request->keyHandleLength--;
+ assert_int_equal(_authenticate_sanity_check_req(authenticate_apdu), U2F_SW_WRONG_LENGTH);
+}
+
+static void test_same_request_can_continue_on_new_cid(void** state)
+{
+ (void)state;
+
+ const uint32_t owner_cid = 0x12345678;
+ const uint32_t other_cid = 0x87654321;
+ Packet pending_packet = _register_message(owner_cid);
+ const USB_APDU* pending_apdu = (const USB_APDU*)pending_packet.data_addr;
+ _set_confirming_state(U2F_REGISTER, pending_apdu);
+
+ Packet in_packet = pending_packet;
+ in_packet.cid = other_cid;
+ assert_true(_is_pending_apdu((const USB_APDU*)in_packet.data_addr));
+ Packet out_packet = {0};
+ _cmd_msg(&in_packet, &out_packet, sizeof(out_packet.data_addr));
+
+ assert_int_equal(_state.last_cmd, U2F_REGISTER);
+ assert_int_equal(_state.reg, U2F_REGISTER_CONFIRMING);
+ assert_true(_is_pending_apdu(pending_apdu));
+ assert_int_equal(out_packet.cid, other_cid);
+ assert_int_equal(out_packet.len, 2);
+ assert_int_equal(out_packet.data_addr[0], U2F_SW_CONDITIONS_NOT_SATISFIED >> 8);
+ assert_int_equal(out_packet.data_addr[1], U2F_SW_CONDITIONS_NOT_SATISFIED & 0xff);
+ usb_processing_unlock();
+}
+
+static void test_changed_request_cannot_continue_workflow(void** state)
+{
+ (void)state;
+
+ Packet pending_packet = _authenticate_message(0x12345678);
+ const USB_APDU* pending_apdu = (const USB_APDU*)pending_packet.data_addr;
+ _set_confirming_state(U2F_AUTHENTICATE, pending_apdu);
+
+ Packet changed_packet = pending_packet;
+ changed_packet.cid = 0x87654321;
+ USB_APDU* changed_apdu = (USB_APDU*)changed_packet.data_addr;
+ U2F_AUTHENTICATE_REQ* changed_request = (U2F_AUTHENTICATE_REQ*)changed_apdu->data;
+
+ changed_apdu->p1 ^= 1;
+ assert_false(_is_pending_apdu(changed_apdu));
+ changed_apdu->p1 ^= 1;
+ changed_request->challenge[0] ^= 1;
+ assert_false(_is_pending_apdu(changed_apdu));
+ changed_request->challenge[0] ^= 1;
+ changed_request->appId[0] ^= 1;
+ assert_false(_is_pending_apdu(changed_apdu));
+ changed_request->appId[0] ^= 1;
+ changed_request->keyHandle[0] ^= 1;
+ assert_false(_is_pending_apdu(changed_apdu));
+ changed_request->keyHandle[0] ^= 1;
+ changed_apdu->lc3--;
+ assert_false(_is_pending_apdu(changed_apdu));
+ changed_apdu->lc3++;
+ assert_true(_is_pending_apdu(changed_apdu));
+
+ changed_request->challenge[0] ^= 1;
+ Packet out_packet = {0};
+ _cmd_msg(&changed_packet, &out_packet, sizeof(out_packet.data_addr));
+
+ assert_int_equal(_state.last_cmd, U2F_AUTHENTICATE);
+ assert_int_equal(_state.auth, U2F_AUTHENTICATE_CONFIRMING);
+ assert_true(_is_pending_apdu(pending_apdu));
+ assert_int_equal(out_packet.cid, changed_packet.cid);
+ assert_int_equal(out_packet.len, 2);
+ assert_int_equal(out_packet.data_addr[0], U2F_SW_CONDITIONS_NOT_SATISFIED >> 8);
+ assert_int_equal(out_packet.data_addr[1], U2F_SW_CONDITIONS_NOT_SATISFIED & 0xff);
+ usb_processing_unlock();
+}
+
+static void test_cross_command_cannot_continue_workflow(void** state)
+{
+ (void)state;
+
+ Packet pending_packet = _authenticate_message(0x12345678);
+ const USB_APDU* pending_apdu = (const USB_APDU*)pending_packet.data_addr;
+ _set_confirming_state(U2F_AUTHENTICATE, pending_apdu);
+
+ Packet in_packet = _register_message(0x87654321);
+ Packet out_packet = {0};
+ _cmd_msg(&in_packet, &out_packet, sizeof(out_packet.data_addr));
+
+ assert_int_equal(_state.last_cmd, U2F_AUTHENTICATE);
+ assert_int_equal(_state.auth, U2F_AUTHENTICATE_CONFIRMING);
+ assert_true(_is_pending_apdu(pending_apdu));
+ assert_int_equal(out_packet.cid, in_packet.cid);
+ assert_int_equal(out_packet.data_addr[0], U2F_SW_CONDITIONS_NOT_SATISFIED >> 8);
+ assert_int_equal(out_packet.data_addr[1], U2F_SW_CONDITIONS_NOT_SATISFIED & 0xff);
+ usb_processing_unlock();
+}
+
+static void test_stateless_request_preserves_workflow(void** state)
+{
+ (void)state;
+
+ const uint32_t other_cid = 0x87654321;
+ Packet pending_packet = _authenticate_message(0x12345678);
+ const USB_APDU* pending_apdu = (const USB_APDU*)pending_packet.data_addr;
+ _set_confirming_state(U2F_AUTHENTICATE, pending_apdu);
+
+ Packet in_packet = _message(U2F_VERSION, other_cid);
+ Packet out_packet = {0};
+ _cmd_msg(&in_packet, &out_packet, sizeof(out_packet.data_addr));
+
+ assert_int_equal(_state.last_cmd, U2F_AUTHENTICATE);
+ assert_int_equal(_state.auth, U2F_AUTHENTICATE_CONFIRMING);
+ assert_true(_is_pending_apdu(pending_apdu));
+ assert_int_equal(out_packet.cid, other_cid);
+ usb_processing_unlock();
+}
+
+static void test_malformed_message_preserves_workflow(void** state)
+{
+ (void)state;
+
+ Packet pending_packet = _authenticate_message(0x12345678);
+ const USB_APDU* pending_apdu = (const USB_APDU*)pending_packet.data_addr;
+ _set_confirming_state(U2F_AUTHENTICATE, pending_apdu);
+
+ Packet in_packet = _message(U2F_VERSION, 0x87654321);
+ ((USB_APDU*)in_packet.data_addr)->lc3 = 1;
+ Packet out_packet = {0};
+ _cmd_msg(&in_packet, &out_packet, sizeof(out_packet.data_addr));
+
+ assert_int_equal(_state.last_cmd, U2F_AUTHENTICATE);
+ assert_int_equal(_state.auth, U2F_AUTHENTICATE_CONFIRMING);
+ assert_true(_is_pending_apdu(pending_apdu));
+ assert_int_equal(out_packet.len, 0);
+}
+
+static void test_init_preserves_workflow(void** state)
+{
+ (void)state;
+
+ Packet pending_packet = _authenticate_message(0x12345678);
+ const USB_APDU* pending_apdu = (const USB_APDU*)pending_packet.data_addr;
+ _set_confirming_state(U2F_AUTHENTICATE, pending_apdu);
+
+ Packet in_packet = {
+ .len = sizeof(U2FHID_INIT_REQ),
+ .cmd = U2FHID_INIT,
+ .cid = U2FHID_CID_BROADCAST,
+ };
+ Packet out_packet = {0};
+ _cmd_init(&in_packet, &out_packet, sizeof(out_packet.data_addr));
+
+ const U2FHID_INIT_RESP* response = (const U2FHID_INIT_RESP*)out_packet.data_addr;
+ assert_int_not_equal(response->cid, 0);
+ assert_int_not_equal(response->cid, U2FHID_CID_BROADCAST);
+ assert_int_equal(_state.last_cmd, U2F_AUTHENTICATE);
+ assert_int_equal(_state.auth, U2F_AUTHENTICATE_CONFIRMING);
+ assert_true(_is_pending_apdu(pending_apdu));
+ assert_int_equal(out_packet.cid, U2FHID_CID_BROADCAST);
+ usb_processing_unlock();
+}
+
+static void test_blocked_error_uses_request_cid(void** state)
+{
+ (void)state;
+
+ const uint32_t owner_cid = 0x12345678;
+ const uint32_t request_cid = 0x87654321;
+ Packet pending_packet = _authenticate_message(owner_cid);
+ const USB_APDU* pending_apdu = (const USB_APDU*)pending_packet.data_addr;
+ _set_confirming_state(U2F_AUTHENTICATE, pending_apdu);
+
+ Packet in_packet = _message(U2F_AUTHENTICATE, request_cid);
+ Packet out_packet = {0};
+ u2f_blocked_req_error(&out_packet, &in_packet);
+
+ assert_int_equal(_state.last_cmd, U2F_AUTHENTICATE);
+ assert_int_equal(_state.auth, U2F_AUTHENTICATE_CONFIRMING);
+ assert_true(_is_pending_apdu(pending_apdu));
+ assert_int_equal(out_packet.cid, request_cid);
+ assert_int_equal(out_packet.len, 2);
+ assert_int_equal(out_packet.data_addr[0], U2F_SW_CONDITIONS_NOT_SATISFIED >> 8);
+ assert_int_equal(out_packet.data_addr[1], U2F_SW_CONDITIONS_NOT_SATISFIED & 0xff);
+}
+
+int main(void)
+{
+ const struct CMUnitTest tests[] = {
+ cmocka_unit_test(test_authenticate_bad_retry_preserves_confirmation),
+ cmocka_unit_test(test_request_length_validation),
+ cmocka_unit_test(test_same_request_can_continue_on_new_cid),
+ cmocka_unit_test(test_changed_request_cannot_continue_workflow),
+ cmocka_unit_test(test_cross_command_cannot_continue_workflow),
+ cmocka_unit_test(test_stateless_request_preserves_workflow),
+ cmocka_unit_test(test_malformed_message_preserves_workflow),
+ cmocka_unit_test(test_init_preserves_workflow),
+ cmocka_unit_test(test_blocked_error_uses_request_cid),
+ };
+ return cmocka_run_group_tests(tests, NULL, NULL);
+}Why this scored 70/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.