fix(trezor-client): handle button request during eth sign data streaming
What changed, and why it matters
This commit fixes a bug in the Rust Trezor client library used to talk to Trezor hardware wallets. When signing a large Ethereum transaction, the device may ask the user to confirm by sending a 'button request' in the middle of streaming the transaction data. Previously, the client did not handle that mid-stream request, so the signing process could hang or fail. The fix wraps each streamed chunk's response with the existing interaction handler so button requests are processed correctly. A new example and a regression test for a 10 KB transaction are added.
Users of the rust trezor-client library should upgrade to a version containing this commit if they sign large Ethereum transactions. Application developers should ensure they use the updated interaction-handling path and run the new regression test against emulators.
Security signals we found
Denial-of-service / hang in client library during large Ethereum transaction signing
Missing handling of ButtonRequest inside a multi-message protocol flow
Regression test added for large (10 kB) Ethereum transaction signing
Fix pattern: wrap streamed response in existing interaction handler
Evidence from the diff
In rust/trezor-client/src/client/ethereum.rs, both ethereum_sign_tx and ethereum_sign_tx_eip1559 stream transaction data to the firmware in 1024-byte chunks via EthereumTxAck and expect EthereumTxRequest responses. Before this patch, the raw self.call(…)? result was used directly, ignoring possible ButtonRequest messages injected by the firmware during streaming. The patch wraps each response in handle_interaction(…)?, the same helper used elsewhere to dispatch button/confirmation prompts. A new send_message helper is added for fire-and-forget debug-link messages, and a test with a 10 KB payload plus auto-approving debug-link thread is introduced to prevent regression.
Changed components
rust/trezor-client/src/client/ethereum.rsrust/trezor-client/src/client/mod.rsrust/trezor-client/examples/eth_sign_tx_large.rsrust/trezor-client/src/lib.rsInspect captured patch +123 / −13
diff --git a/rust/trezor-client/examples/eth_sign_tx_large.rs b/rust/trezor-client/examples/eth_sign_tx_large.rs
new file mode 100644
index 00000000..43f24191
--- /dev/null
+++ b/rust/trezor-client/examples/eth_sign_tx_large.rs
@@ -0,0 +1,23 @@
+fn main() {
+ tracing_subscriber::fmt().with_max_level(tracing::Level::TRACE).init();
+
+ // init with debugging
+ let mut trezor = trezor_client::unique(false).unwrap();
+ trezor.init_device(None).unwrap();
+
+ let signature = trezor
+ .ethereum_sign_tx(
+ // default ETH path
+ vec![44 + (1 << 31), 60 + (1 << 31), 0 + (1 << 31), 0, 0],
+ vec![],
+ vec![],
+ vec![],
+ "".to_string(),
+ vec![],
+ // 10kB of empty data
+ vec![0; 10 * 1024],
+ Some(1u64),
+ )
+ .unwrap();
+ println!("Signature: {:?}", signature);
+}
diff --git a/rust/trezor-client/src/client/ethereum.rs b/rust/trezor-client/src/client/ethereum.rs
index 12ac0ea0..09328a75 100644
--- a/rust/trezor-client/src/client/ethereum.rs
+++ b/rust/trezor-client/src/client/ethereum.rs
@@ -92,7 +92,9 @@ impl Trezor {
let mut ack = protos::EthereumTxAck::new();
ack.set_data_chunk(data.splice(..std::cmp::min(1024, data.len()), []).collect());
- resp = self.call(ack, Box::new(|_, m: protos::EthereumTxRequest| Ok(m)))?.ok()?;
+ resp = handle_interaction(
+ self.call(ack, Box::new(|_, m: protos::EthereumTxRequest| Ok(m)))?,
+ )?;
}
convert_signature(&resp, chain_id)
@@ -147,7 +149,9 @@ impl Trezor {
let mut ack = protos::EthereumTxAck::new();
ack.set_data_chunk(data.splice(..std::cmp::min(1024, data.len()), []).collect());
- resp = self.call(ack, Box::new(|_, m: protos::EthereumTxRequest| Ok(m)))?.ok()?
+ resp = handle_interaction(
+ self.call(ack, Box::new(|_, m: protos::EthereumTxRequest| Ok(m)))?,
+ )?;
}
convert_signature(&resp, chain_id)
diff --git a/rust/trezor-client/src/client/mod.rs b/rust/trezor-client/src/client/mod.rs
index 6dd54026..d7266d5c 100644
--- a/rust/trezor-client/src/client/mod.rs
+++ b/rust/trezor-client/src/client/mod.rs
@@ -58,6 +58,14 @@ impl Trezor {
self.transport.read_message().map_err(Error::TransportReceiveMessage)
}
+ /// Sends a message without waiting for a response.
+ /// Useful for fire-and-forget debug messages such as `DebugLinkDecision`, which the
+ /// firmware may not reply to.
+ pub fn send_message<S: TrezorMessage>(&mut self, message: S) -> Result<()> {
+ let proto_msg = ProtoMessage(S::MESSAGE_TYPE, message.write_to_bytes()?);
+ self.transport.write_message(proto_msg).map_err(Error::TransportSendMessage)
+ }
+
/// Sends a message and returns a TrezorResponse with either the expected response message,
/// a failure or an interaction request.
/// This method is only exported for users that want to expand the features of this library
diff --git a/rust/trezor-client/src/lib.rs b/rust/trezor-client/src/lib.rs
index cd8b7ebb..27ea83d5 100644
--- a/rust/trezor-client/src/lib.rs
+++ b/rust/trezor-client/src/lib.rs
@@ -136,11 +136,25 @@ pub fn unique(debug: bool) -> Result<Trezor> {
#[cfg(test)]
mod tests {
- use serial_test::serial;
- use std::str::FromStr;
+ use std::{
+ str::FromStr,
+ sync::{
+ atomic::{AtomicBool, Ordering},
+ Arc,
+ },
+ thread,
+ };
- use crate::{client::handle_interaction, protos::IdentityType};
use bitcoin::{bip32::DerivationPath, hex::FromHex};
+ use serial_test::serial;
+
+ use crate::{
+ client::handle_interaction,
+ protos::{
+ debug_link_decision::DebugButton, DebugLinkDecision, DebugLinkGetState, DebugLinkState,
+ IdentityType,
+ },
+ };
use super::*;
@@ -155,6 +169,40 @@ mod tests {
emulator
}
+ /// Spawn a background thread that continuously sends a "YES" decision to the debug link.
+ fn with_auto_approve<F, T>(f: F) -> T
+ where
+ F: FnOnce() -> T,
+ {
+ let mut debuglink = find_devices(true)
+ .into_iter()
+ .find(|t| t.model == Model::TrezorEmulator)
+ .expect("No debug emulator found")
+ .connect()
+ .expect("Failed to connect to debug emulator");
+
+ let stop = Arc::new(AtomicBool::new(false));
+ let stop_clone = stop.clone();
+
+ let mut req = DebugLinkDecision::new();
+ req.set_button(DebugButton::YES);
+
+ thread::scope(|scope| {
+ scope.spawn(move || {
+ while !stop_clone.load(Ordering::Relaxed) {
+ // DebugLinkDecision has no response; send fire-and-forget then poll
+ // state (which returns when the firmware has processed the decision).
+ let _ = debuglink.send_message(req.clone());
+ let _ = debuglink
+ .call(DebugLinkGetState::new(), Box::new(|_, m: DebugLinkState| Ok(m)));
+ }
+ });
+ let res = f();
+ stop.store(true, Ordering::Relaxed);
+ res
+ })
+ }
+
#[test]
#[serial]
fn test_emulator_find() {
@@ -189,12 +237,9 @@ mod tests {
assert_eq!(address.ok().unwrap().to_string(), "mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q");
}
- #[ignore]
#[test]
#[serial]
fn test_ecdh_shared_secret() {
- tracing_subscriber::fmt().with_max_level(tracing::Level::TRACE).init();
-
let mut emulator = init_emulator();
assert_eq!(emulator.features().expect("Failed to get features").label(), "SLIP-0014");
@@ -208,11 +253,14 @@ mod tests {
let peer_public_key = Vec::from_hex("0407f2c6e5becf3213c1d07df0cfbe8e39f70a8c643df7575e5c56859ec52c45ca950499c019719dae0fda04248d851e52cf9d66eeb211d89a77be40de22b6c89d").unwrap();
let curve_name = "secp256k1".to_owned();
- let response = handle_interaction(
- emulator
- .get_ecdh_session_key(ident, peer_public_key, curve_name)
- .expect("Failed to get ECDH shared secret"),
- )
+
+ let response = with_auto_approve(|| {
+ handle_interaction(
+ emulator
+ .get_ecdh_session_key(ident, peer_public_key, curve_name)
+ .expect("Failed to get ECDH shared secret"),
+ )
+ })
.unwrap();
let expected_session_key = Vec::from_hex("048125883b086746244b0d2c548860ecc723346e14c87e51dc7ba32791bc780d132dbd814fbee77134f318afac6ad6db3c5334efe6a8798628a1038195b96e82e2").unwrap();
@@ -223,4 +271,31 @@ mod tests {
.unwrap();
assert_eq!(response.public_key(), &expected_public_key);
}
+
+ #[test]
+ #[serial]
+ fn test_ethereum_sign_tx_large() {
+ let mut emulator = init_emulator();
+ assert_eq!(emulator.features().expect("Failed to get features").label(), "SLIP-0014");
+
+ let signature = with_auto_approve(|| {
+ emulator.ethereum_sign_tx(
+ // default ETH path
+ vec![44 + (1 << 31), 60 + (1 << 31), 0 + (1 << 31), 0, 0],
+ vec![],
+ vec![],
+ vec![],
+ "".to_string(),
+ vec![],
+ // 10kB of empty data
+ vec![0; 10 * 1024],
+ Some(1u64),
+ )
+ })
+ .unwrap();
+
+ assert_eq!(signature.r.len(), 32);
+ assert_eq!(signature.s.len(), 32);
+ assert_eq!(signature.v, 38);
+ }
}
Why this scored 45/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.