refactor(rust/trezor-thp): keep track if channel finished pairing/credentials
What changed, and why it matters
This commit is a straightforward internal code refactor in the Trezor hardware wallet's Rust transport-handshake protocol (THP) library. It replaces a simple pairing-state field with a richer 'phase' enum that tracks whether the channel is still in the pairing/credential setup stage or has moved to encrypted application messaging. The change adds helper methods so the application can explicitly mark pairing as finished. There is no direct security fix here; it is a structural improvement that makes state tracking clearer and less error-prone for callers.
No immediate security action required. Treat as normal code-quality refactor. Review downstream callers of `Channel::phase()`/`end_pairing()` to ensure they correctly invoke `end_pairing()` after pairing/credential exchange, since the library now relies on the application to mark the transition to encrypted transport.
Security signals we found
Refactor only: no vulnerability patch, no bounds-check changes, no cryptographic algorithm changes
Adds explicit lifecycle phase tracking to prevent callers from misidentifying the channel's setup state
Default phase uses least-privileged `Unpaired` value as a defensive default
No CVE, advisory, or vendor security disclosure referenced in commit or supplied materials
Evidence from the diff
The patch changes Channel in rust/trezor-thp from storing a bare PairingState to storing a new Phase enum with two variants: PairingCredential { handshake_pairing_state } and EncryptedTransport. ChannelOpen::complete() now initializes phase to PairingCredential carrying the handshake-derived pairing state, and a new end_pairing() method transitions the channel to EncryptedTransport. Public API handshake_pairing_state() is replaced by phase() and is_encrypted_transport(). Tests are updated to assert the phase transitions. The commit message explicitly calls this a refactor and the code comments describe the design intent.
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 +63 / −11
diff --git a/rust/trezor-thp/src/channel/device.rs b/rust/trezor-thp/src/channel/device.rs
index ed5350e2..fb2a2b58 100644
--- a/rust/trezor-thp/src/channel/device.rs
+++ b/rust/trezor-thp/src/channel/device.rs
@@ -5,7 +5,7 @@ use crate::{
alternating_bit::SyncBits,
channel::{
HANDSHAKE_BUFFER_DTH_LEN, HANDSHAKE_BUFFER_HTD_LEN, MAX_DEVICE_PROPERTIES_LEN, Nonce,
- PRIVKEY_LEN, PacketInResult, PairingState, ReceiveState, SendState,
+ PRIVKEY_LEN, PacketInResult, PairingState, Phase, ReceiveState, SendState,
noise::NoiseHandshake,
},
control_byte::ControlByte,
@@ -442,7 +442,9 @@ impl<C: CredentialVerifier, B: Backend> ChannelOpen<C, B> {
log::debug!("[{:04x}] Handshake complete.", self.channel_id());
Ok(match self.state {
HandshakeState::SendingCompletionResponse { pairing_state } => {
- self.channel.pairing_state = pairing_state;
+ self.channel.phase = Phase::PairingCredential {
+ handshake_pairing_state: pairing_state,
+ };
self.channel
}
_ => return Err(Error::unexpected_input()),
diff --git a/rust/trezor-thp/src/channel/host.rs b/rust/trezor-thp/src/channel/host.rs
index 7d629ddf..4ab7312f 100644
--- a/rust/trezor-thp/src/channel/host.rs
+++ b/rust/trezor-thp/src/channel/host.rs
@@ -5,7 +5,7 @@ use crate::{
alternating_bit::SyncBits,
channel::{
HANDSHAKE_BUFFER_DTH_LEN, HANDSHAKE_BUFFER_HTD_LEN, MAX_ALLOC_RESPONSE_LEN,
- MAX_DEVICE_PROPERTIES_LEN, Nonce, PacketInResult, PairingState, ReceiveState,
+ MAX_DEVICE_PROPERTIES_LEN, Nonce, PacketInResult, PairingState, Phase, ReceiveState,
noise::NoiseHandshake,
},
control_byte::ControlByte,
@@ -482,7 +482,9 @@ impl<C: CredentialStore, B: Backend> ChannelOpen<C, B> {
log::debug!("[{:04x}] Handshake complete.", self.channel_id());
Ok(match self.state {
HandshakeState::Finished { pairing_state } => {
- self.channel.pairing_state = pairing_state;
+ self.channel.phase = Phase::PairingCredential {
+ handshake_pairing_state: pairing_state,
+ };
self.channel
}
_ => return Err(Error::unexpected_input()),
diff --git a/rust/trezor-thp/src/channel/mod.rs b/rust/trezor-thp/src/channel/mod.rs
index 6417970c..a316c1a4 100644
--- a/rust/trezor-thp/src/channel/mod.rs
+++ b/rust/trezor-thp/src/channel/mod.rs
@@ -137,6 +137,36 @@ enum ReceiveState<R: Role> {
Failed,
}
+/// Whether pairing and credential phase has finished.
+///
+/// After channel is allocated, it goes through several phases until application
+/// messages can be securely exchanged.
+/// 1. Handshake phase
+/// 2. Pairing phase
+/// 3. Credential phase
+/// 4. Encrypted transport phase
+///
+/// Channel in handshake phase has distinct type and a [`ChannelOpen::complete()`]
+/// method to obtain channel in pairing or credential phase. In pairing and credential
+/// phase peers exchange protobuf messages, and by exchanging `EndRequest` and `EndResponse`,
+/// transition to encrypted transport phase is indicated. While application messages in
+/// encrypted transport phase can also use protobuf messages, their meaning is generally
+/// different than in the other phases.
+/// Application can use this enum to distinguish the context.
+#[repr(u8)]
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum Phase {
+ /// Channel is in pairing or credential phase, depending on the outcome of the handshake
+ /// phase.
+ PairingCredential {
+ handshake_pairing_state: PairingState,
+ },
+ /// Channel is in encrypted transport phase. Because this library doesn't understand
+ /// protobuf, application must call [`Channel::end_pairing`] to indicate successful
+ /// end of pairing/credential phase.
+ EncryptedTransport,
+}
+
/// THP channel with established secure layer.
///
/// There is no constructor, to obtain a channel please use [`host::Mux`]
@@ -149,7 +179,7 @@ pub struct Channel<R: Role, B: Backend> {
send_ack: Option<SyncBits>,
send_state: SendState<R>,
receive_state: ReceiveState<R>,
- pairing_state: PairingState,
+ phase: Phase,
}
impl<R: Role, B: Backend> Channel<R, B> {
@@ -161,7 +191,11 @@ impl<R: Role, B: Backend> Channel<R, B> {
send_ack: None,
send_state: SendState::Idle,
receive_state: ReceiveState::Idle,
- pairing_state: PairingState::Unpaired,
+ // ChannelOpen must set this when returning Channel.
+ // Use the least privileged value as a default.
+ phase: Phase::PairingCredential {
+ handshake_pairing_state: PairingState::Unpaired,
+ },
}
}
@@ -177,11 +211,21 @@ impl<R: Role, B: Backend> Channel<R, B> {
self.noise.as_ref().unwrap().handshake_hash()
}
- /// Returns the channel pairing state at the end of the handshake.
- /// This is read-only attribute to inform the application whether it needs to perform
- /// pairing, or can directly transition to encrypted transport state.
- pub fn handshake_pairing_state(&self) -> PairingState {
- self.pairing_state
+ /// Returns channel establishment state.
+ pub fn phase(&self) -> Phase {
+ self.phase
+ }
+
+ /// True if pairing/credential phase has finished and channel can be used to exchange
+ /// application messages.
+ pub fn is_encrypted_transport(&self) -> bool {
+ matches!(self.phase, Phase::EncryptedTransport)
+ }
+
+ /// Application should call this whenever transitioning to the encrypted transport
+ /// state.
+ pub fn end_pairing(&mut self) {
+ self.phase = Phase::EncryptedTransport;
}
pub fn remote_static_pubkey(&self) -> &[u8; PUBKEY_LEN] {
diff --git a/rust/trezor-thp/src/channel/test.rs b/rust/trezor-thp/src/channel/test.rs
index d01b6d9f..496fd10c 100644
--- a/rust/trezor-thp/src/channel/test.rs
+++ b/rust/trezor-thp/src/channel/test.rs
@@ -396,10 +396,14 @@ fn test_open() -> Result<()> {
let mut d = d.map(|d| d.unwrap().complete())?;
// pairing
+ assert!(!h.is_encrypted_transport() && !d.is_encrypted_transport());
HostToDevice.send(&mut h, &mut d, 0, 1008, b"ThpPairingRequest placeholder")?;
DeviceToHost.send(&mut h, &mut d, 0, 1009, b"ThpPairingResponse placeholder")?;
HostToDevice.send(&mut h, &mut d, 0, 1010, b"ThpSelectMethod with SkipParing")?;
DeviceToHost.send(&mut h, &mut d, 0, 1019, b"ThpEndResponse means done")?;
+ h.end_pairing();
+ d.end_pairing();
+ assert!(h.is_encrypted_transport() && d.is_encrypted_transport());
// application messaging
HostToDevice.send(&mut h, &mut d, 0, 1234, b"Ping")?;
Why this scored 12/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.