refactor(rust/trezor-thp): TransportError sending
What changed, and why it matters
This commit is a code cleanup in Trezor's secure transport layer. It centralizes how error messages are sent and removes an outdated workaround where the device had to manually craft an error packet. The change also makes decryption failures on the device side send a proper error response instead of silently closing the channel. There is no indication this fixes an active security vulnerability; it appears to be a refactoring and robustness improvement.
No immediate security action required. Treat as normal code-quality/robustness refactor. Reviewers may want to confirm that the new SendingError state correctly handles retransmissions and that recoverable errors transition back to Idle safely.
Security signals we found
Refactoring of transport-layer error handling
Decryption failures now explicitly reported to peer via TransportError::DecryptionFailed
Removal of manual error packet construction in favor of centralized state machine
Test assertion un-skipped, indicating expected behavior is now enforced
Evidence from the diff
The refactor introduces a new ChannelState::SendingError state and a send_error() helper, replacing ad-hoc error header construction in device.rs. It removes the ack_received field from PacketInResult::TransportError because transport errors are no longer ACKed. On decryption failure, the device (non-host) role now transitions to SendingError with TransportError::DecryptionFailed rather than immediately failing the channel. A previously skipped test assertion in test_device_locked is now enabled. No CVE, advisory, or vendor security disclosure is present in the provided materials.
Changed components
rust/trezor-thp/src/channel/device.rsrust/trezor-thp/src/channel/host.rsrust/trezor-thp/src/channel/mod.rsrust/trezor-thp/src/channel/test.rsInspect captured patch +36 / −28
diff --git a/rust/trezor-thp/src/channel/device.rs b/rust/trezor-thp/src/channel/device.rs
index fbf891ca..809a2216 100644
--- a/rust/trezor-thp/src/channel/device.rs
+++ b/rust/trezor-thp/src/channel/device.rs
@@ -394,8 +394,7 @@ impl<C: CredentialVerifier, B: Backend> ChannelOpen<C, B> {
/// True if the handshake failed and the object should be discarded.
pub fn handshake_failed(&self) -> bool {
- matches!(self.state, HandshakeState::Failed)
- || matches!(self.channel.state, ChannelState::Failed(_))
+ matches!(self.state, HandshakeState::Failed) || self.channel.is_failed()
}
/// True if the handshake is waiting for device static key to be supplied using
@@ -446,12 +445,7 @@ impl<C: CredentialVerifier, B: Backend> ChannelOpen<C, B> {
if !self.static_key_required() {
return Err(Error::not_ready());
}
- let header = Header::new_error(self.channel.channel_id)?;
- self.internal_buffer.clear();
- let _ = self
- .internal_buffer
- .push(TransportError::DeviceLocked.into());
- self.channel.raw_in(header, &self.internal_buffer)?;
+ self.channel.send_error(TransportError::DeviceLocked);
self.state = HandshakeState::SendingDeviceLocked;
Ok(())
}
@@ -492,10 +486,6 @@ where
return PacketInResult::ignore(Error::malformed_data());
}
if res.got_ack() {
- if matches!(self.state, HandshakeState::SendingDeviceLocked) {
- self.state = HandshakeState::Failed;
- return res;
- }
prepare_zeroed(&mut self.internal_buffer);
}
if res.got_message() {
diff --git a/rust/trezor-thp/src/channel/host.rs b/rust/trezor-thp/src/channel/host.rs
index 37cb1043..7a91310f 100644
--- a/rust/trezor-thp/src/channel/host.rs
+++ b/rust/trezor-thp/src/channel/host.rs
@@ -442,8 +442,7 @@ impl<C: CredentialStore, B: Backend> ChannelOpen<C, B> {
/// True if the handshake failed and the object should be discarded.
pub fn handshake_failed(&self) -> bool {
- matches!(self.state, HandshakeState::Failed)
- || matches!(self.channel.state, ChannelState::Failed(_))
+ matches!(self.state, HandshakeState::Failed) || self.channel.is_failed()
}
/// Finish the handshake.
diff --git a/rust/trezor-thp/src/channel/mod.rs b/rust/trezor-thp/src/channel/mod.rs
index 43c844a4..96e03493 100644
--- a/rust/trezor-thp/src/channel/mod.rs
+++ b/rust/trezor-thp/src/channel/mod.rs
@@ -100,6 +100,9 @@ enum ChannelState<R: Role> {
fragmenter: Fragmenter<R>,
retry: u8,
},
+ /// About to send Transport error, these are not ACKed.
+ /// Transitions to Failed afterwards unless the error is recoverable.
+ SendingError { error: TransportError },
/// In the process of receiving a message, or waiting for the consumer to pick up
/// an assembled message.
Receiving { reassembler: Reassembler<R> },
@@ -171,6 +174,10 @@ impl<R: Role, B: Backend> Channel<R, B> {
}
}
+ pub fn send_error(&mut self, error: TransportError) {
+ self.state = ChannelState::SendingError { error };
+ }
+
fn raw_in(&mut self, header: Header<R>, send_buffer: &[u8]) -> Result<()> {
let ChannelState::Idle = self.state else {
return Err(Error::not_ready());
@@ -255,6 +262,7 @@ impl<R: Role, B: Backend> Channel<R, B> {
// We end up sending reply while the other side is retransmitting.
// Is this recoverable?
ChannelState::Sending { .. } => return Err(Error::malformed_data()),
+ ChannelState::SendingError { .. } => return Err(Error::unexpected_input()),
ChannelState::Failed { .. } => return Err(Error::unexpected_input()),
})
}
@@ -351,9 +359,6 @@ pub enum PacketInResult {
},
/// Peer sent a `TRANSPORT_ERROR` message.
TransportError {
- /// True if the packet contained valid ACK and channel is ready to send next message.
- /// Reserved for ACK piggybacking.
- ack_received: bool,
/// Error sent by the peer.
error: TransportError,
},
@@ -413,10 +418,7 @@ impl PacketInResult {
}
const fn transport_error(e: TransportError) -> Self {
- Self::TransportError {
- ack_received: false,
- error: e,
- }
+ Self::TransportError { error: e }
}
const fn route(channel_id: u16) -> Self {
@@ -444,7 +446,6 @@ impl PacketInResult {
match self {
Self::Accepted { ack_received, .. } => *ack_received,
Self::EnlargeBuffer { ack_received, .. } => *ack_received,
- Self::TransportError { ack_received, .. } => *ack_received,
_ => false,
}
}
@@ -540,7 +541,7 @@ pub trait ChannelIO {
///
/// The message including the application header (session id, message type) is passed in
/// `send_buffer`, occupying first `plaintext_len` bytes. There must be at least 16 more
- /// bytes in the buffer for authentication tag.
+ /// bytes in the buffer for the authentication tag.
///
/// Instead of this function you can use [`Self::message_in_from`] to prepare the send
/// buffer for you.
@@ -608,7 +609,7 @@ pub trait ChannelIO {
impl<R: Role, B: Backend> ChannelIO for Channel<R, B> {
fn packet_in(&mut self, packet_buffer: &[u8], receive_buffer: &mut [u8]) -> PacketInResult {
- if let ChannelState::Failed { .. } = self.state {
+ if self.is_failed() {
return PacketInResult::fail(Error::unexpected_input());
}
let res = PacketInResult::from_result(self.handle_packet(packet_buffer, receive_buffer));
@@ -625,6 +626,16 @@ impl<R: Role, B: Backend> ChannelIO for Channel<R, B> {
Fragmenter::single(header, sb, &[], packet_buffer)?;
return Ok(());
}
+ if let ChannelState::SendingError { error } = self.state {
+ let header = Header::<R>::new_error(self.channel_id)?;
+ Fragmenter::single(header, SyncBits::new(), &[error.into()], packet_buffer)?;
+ if error.is_recoverable() {
+ self.state = ChannelState::Idle;
+ } else {
+ self.state = ChannelState::Failed { error: None };
+ }
+ return Ok(());
+ }
let ChannelState::Sending { fragmenter, .. } = &mut self.state else {
return Err(Error::not_ready());
};
@@ -651,6 +662,7 @@ impl<R: Role, B: Backend> ChannelIO for Channel<R, B> {
}
match &self.state {
ChannelState::Sending { fragmenter, .. } => !fragmenter.is_done(),
+ ChannelState::SendingError { .. } => true,
_ => false,
}
}
@@ -687,8 +699,16 @@ impl<R: Role, B: Backend> ChannelIO for Channel<R, B> {
let receive_buffer = match self.noise()?.decrypt(receive_buffer) {
Ok(plaintext_len) => &receive_buffer[..plaintext_len],
Err(e) => {
- log::error!("[{}] Decryption failed, channel closed.", self.channel_id);
- self.state = ChannelState::Failed { error: None };
+ if R::is_host() {
+ log::error!("[{:04x}] Decryption failed.", self.channel_id);
+ self.state = ChannelState::Failed { error: None };
+ } else {
+ log::error!(
+ "[{:04x}] Decryption failed, sending DECRYPTION_FAILED.",
+ self.channel_id
+ );
+ self.send_error(TransportError::DecryptionFailed);
+ }
return Err(e);
}
};
diff --git a/rust/trezor-thp/src/channel/test.rs b/rust/trezor-thp/src/channel/test.rs
index fe45efc6..400e507e 100644
--- a/rust/trezor-thp/src/channel/test.rs
+++ b/rust/trezor-thp/src/channel/test.rs
@@ -317,8 +317,7 @@ fn test_device_locked() -> Result<()> {
d.send_device_locked()?;
take_turns(&mut h, &mut d)?;
assert!(h.handshake_failed());
- // TODO: either the host needs to ACK the error, or host transitions to ack after sending it
- // assert!(d.handshake_failed());
+ assert!(d.handshake_failed());
Ok(())
}
Why this scored 11/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.