feat(rust/trezor-thp): secure channel layer
What changed, and why it matters
This commit adds a new secure communication layer to the Trezor hardware wallet's Rust protocol library. It introduces encrypted channels, handshake logic, and credential handling. The change is a feature implementation rather than a documented security fix, but it touches sensitive cryptography and transport code, so correctness matters for device security.
Treat this as a high-risk feature addition requiring careful review. Verify the Noise_XX handshake implementation against the THP specification, audit buffer sizing and reassembly logic for overflows, review credential lookup and static key handling, and ensure the new error/ACK state machine cannot be driven into inconsistent states by a malicious peer. Run fuzzing and protocol conformance tests before release.
Security signals we found
New cryptographic handshake and encrypted transport code added
Header constructors now validate channel IDs and payload lengths (previously marked FIXME)
Noise_XX pattern used with ephemeral and static DH keys
Credential lookup based on masked static pubkey and ephemeral pubkey
Buffer size checks added in several places (InsufficientBuffer errors)
No unsafe code; crate forbids unsafe_code
No changelog entry and no explicit security disclosure in commit message
Evidence from the diff
The commit implements the Trezor Host Protocol (THP) secure channel in the rust/trezor-thp crate. It adds Noise_XX handshake support via trezor-noise-protocol, host-side channel opening, encrypted transport framing, reassembly, ACK/retransmit logic, and a CredentialStore trait. Several previously unchecked header constructors now validate channel IDs and payload lengths. The diff is large (+1416/-91) and adds new code paths for parsing, fragmentation, and cryptographic state management.
Changed components
rust/trezor-thprust/trezor-thp/src/channel/host.rsrust/trezor-thp/src/channel/mod.rsrust/trezor-thp/src/channel/noise.rsrust/trezor-thp/src/header.rsrust/trezor-thp/src/fragment.rsrust/trezor-thp/src/credential.rsrust/trezor-thp/src/error.rsInspect captured patch +1416 / −91
diff --git a/rust/trezor-thp/Cargo.toml b/rust/trezor-thp/Cargo.toml
index 7aa4da0c..d88d7c93 100644
--- a/rust/trezor-thp/Cargo.toml
+++ b/rust/trezor-thp/Cargo.toml
@@ -6,6 +6,15 @@ edition = "2024"
[dependencies]
log = "0.4.29"
+[dependencies.heapless]
+version = "0.9.2"
+default-features = false
+
+[dependencies.trezor-noise-protocol]
+version = "0.2.0"
+default-features = false
+git = "https://github.com/trezor/noise-rust"
+rev = "916ce4dc25b0dc830832a51f6f37021e2214e4c7"
+
[dev-dependencies]
-heapless = { version = "0.9.2", default-features = false }
hex = "0.4.3"
diff --git a/rust/trezor-thp/README.md b/rust/trezor-thp/README.md
new file mode 100644
index 00000000..41877384
--- /dev/null
+++ b/rust/trezor-thp/README.md
@@ -0,0 +1,47 @@
+# trezor-thp
+
+Rust library for *Trezor Host Protocol*. THP facilitates communication between an application
+on a host computer and a [Trezor] cryptocurrency wallet.
+
+To learn more about THP, please see the [full specification][THP-spec].
+
+## Design
+
+The goal of the library is to be used by both bare-metal firmware and desktop applications.
+
+- Implements both the Host and the Device (Trezor) side.
+- Usable on `no_std` and without `core::alloc`.
+- I/O-free to make integration into any kind of event loop possible.
+- Usable with any protobuf and cryptography libraries.
+- Minimal dependencies.
+
+Due to these requirements, the crate is very low-level - it provides a library of components that
+you need to assemble to get a high-level abstraction of THP sessions.
+
+Crates that provide a higher-level interface:
+- [trezor-client](https://crates.io/crates/trezor-client) (work in progress)
+
+## Examples
+
+The examples assume Trezor emulator available through UDP.
+
+```console
+cargo run --example ping-emulator
+cargo run --example host-cli
+```
+
+## Features
+
+None yet.
+
+## Other implementations
+
+- [trezorlib](https://github.com/trezor/trezor-firmware/tree/main/python/src/trezorlib/)
+- [Suite/Connect](https://github.com/trezor/trezor-suite/tree/develop/packages/protocol/src/protocol-thp/)
+
+## Credits
+
+- [Trezor](https://github.com/trezor/trezor-firmware)
+
+[THP-spec]: https://docs.trezor.io/trezor-firmware/common/thp/specification.html
+[Trezor]: https://trezor.io/
diff --git a/rust/trezor-thp/examples/ping-emulator.rs b/rust/trezor-thp/examples/ping-emulator.rs
index 3ec07778..ce81f1b4 100644
--- a/rust/trezor-thp/examples/ping-emulator.rs
+++ b/rust/trezor-thp/examples/ping-emulator.rs
@@ -4,9 +4,8 @@ use std::str::FromStr;
use trezor_thp::{
Host,
- alternating_bit::SyncBits,
fragment::{Fragmenter, Reassembler},
- header::Header,
+ header::{Header, SyncBits},
};
const REPEAT: u8 = 1;
diff --git a/rust/trezor-thp/src/alternating_bit.rs b/rust/trezor-thp/src/alternating_bit.rs
index 39d3554b..8a5a1536 100644
--- a/rust/trezor-thp/src/alternating_bit.rs
+++ b/rust/trezor-thp/src/alternating_bit.rs
@@ -1,4 +1,7 @@
-use crate::control_byte::{ACK_BIT, SEQ_BIT};
+use crate::{
+ control_byte::{ACK_BIT, SEQ_BIT},
+ error::Error,
+};
#[derive(Clone, Copy)]
pub struct SyncBits(u8);
@@ -39,6 +42,15 @@ impl From<u8> for SyncBits {
}
}
+impl TryFrom<&[u8]> for SyncBits {
+ type Error = Error;
+
+ fn try_from(bytes: &[u8]) -> Result<Self, Error> {
+ let first_byte = bytes.first().ok_or(Error::MalformedData)?;
+ Ok(Self::from(*first_byte))
+ }
+}
+
impl From<SyncBits> for u8 {
fn from(sb: SyncBits) -> Self {
sb.0
@@ -52,7 +64,7 @@ impl Default for SyncBits {
}
/// Alternating Bit Protocol state for a single channel.
-#[cfg_attr(any(test, debug_assertions), derive(Debug, PartialEq))]
+#[cfg_attr(any(test, debug_assertions), derive(Debug, PartialEq, Eq))]
pub struct ChannelSync {
/// If true we are waiting for an ACK and cannot send further messages.
can_send: bool,
@@ -128,6 +140,7 @@ impl ChannelSync {
/// Serialize for storage.
/// can_send_bit | sync_receive_bit | sync_send_bit | ack_piggybacking | rfu(4)
+ #[cfg(test)]
pub fn to_u8(&self) -> u8 {
let mut res = 0u8;
if self.can_send {
@@ -146,6 +159,7 @@ impl ChannelSync {
}
/// Deserialize from storage.
+ #[cfg(test)]
pub fn from_u8(val: u8) -> Self {
Self {
can_send: (val & 0x80 != 0),
@@ -238,10 +252,10 @@ mod test {
#[test]
fn test_serialize_rt() {
- for cs in 0..1 {
- for sr in 0..1 {
- for ss in 0..1 {
- for ap in 0..1 {
+ for cs in 0..=1 {
+ for sr in 0..=1 {
+ for ss in 0..=1 {
+ for ap in 0..=1 {
let orig = ChannelSync {
can_send: cs != 0,
sync_receive: sr != 0,
diff --git a/rust/trezor-thp/src/channel/host.rs b/rust/trezor-thp/src/channel/host.rs
new file mode 100644
index 00000000..e06eb534
--- /dev/null
+++ b/rust/trezor-thp/src/channel/host.rs
@@ -0,0 +1,358 @@
+use heapless;
+
+use crate::{
+ Backend, Channel, ChannelIO, Error, Host,
+ channel::{ChannelState, Nonce, PacketInResult, PairingState, noise::NoiseHandshake},
+ credential::CredentialStore,
+ header::{BROADCAST_CHANNEL_ID, HandshakeMessage, Header, parse_u16},
+};
+
+use core::ops::ControlFlow;
+
+// Must fit any of:
+// - device_properties + overhead
+// - 2 DH keys + 2 AEAD tags (2*32+2*16=96) + overhead
+// - DH key + credential + 2 AEAD tags + overhead
+const INTERNAL_BUFFER_LEN: usize = 192;
+const MAX_DEVICE_PROPERTIES_LEN: usize = 128;
+const MESSAGE_TYPE_END_RESPONSE: u16 = 1019; // ThpMessageType_ThpEndResponse
+
+#[derive(Copy, Clone)]
+enum HostHandshakeState {
+ /// `HH0`.
+ SentChannelRequest(Nonce),
+ /// `HH1`.
+ SentInitiationRequest,
+ /// `HH2`.
+ SentCompletionRequest,
+ /// `HP0`.
+ Finished(PairingState),
+ /// Handshake cannot be finished.
+ Failed,
+}
+
+/// Open a [`Channel`] from the host side.
+///
+/// - start by calling [`ChannelOpen::new`]
+/// - perform [`ChannelIO`] with empty messages until [`ChannelOpen::handshake_done`]
+/// - call [`ChannelOpen::complete`] to obtain [`ChannelPairing`]
+/// - perform [`ChannelIO`] until [`ChannelPairing::pairing_done`]
+/// - call [`ChannelPairing::complete`] to get established [`Channel`]
+pub struct ChannelOpen<C: CredentialStore, B: Backend> {
+ channel: Channel<Host, B>,
+ state: HostHandshakeState,
+ noise: Option<NoiseHandshake<Host, B>>,
+ internal_buffer: heapless::Vec<u8, INTERNAL_BUFFER_LEN>,
+ device_properties: heapless::Vec<u8, MAX_DEVICE_PROPERTIES_LEN>,
+ cred_store: C,
+ try_to_unlock: bool,
+}
+
+impl<C: CredentialStore, B: Backend> ChannelOpen<C, B> {
+ pub fn new(try_to_unlock: bool, cred_store: C) -> Result<Self, Error> {
+ let nonce = Nonce::random::<B>();
+ let internal_buffer = heapless::Vec::from_slice(nonce.as_slice()).unwrap();
+ let mut channel = Channel::new(BROADCAST_CHANNEL_ID);
+ channel.raw_in(Header::new_channel_request(), &internal_buffer)?;
+ let res = Self {
+ channel,
+ state: HostHandshakeState::SentChannelRequest(nonce),
+ noise: None,
+ internal_buffer,
+ device_properties: heapless::Vec::new(),
+ cred_store,
+ try_to_unlock,
+ };
+ Ok(res)
+ }
+
+ pub fn is_broadcast(&self) -> bool {
+ self.channel.is_broadcast()
+ }
+
+ fn incoming_internal(&mut self) -> Result<(), Error> {
+ let (header, len) = self.channel.raw_out(&self.internal_buffer)?;
+ self.internal_buffer.truncate(len);
+
+ match (self.state, header.handshake_phase()) {
+ (HostHandshakeState::SentChannelRequest(nonce), _)
+ if header.is_channel_allocation_response() =>
+ {
+ if self.get_channel(&nonce)?.is_continue() {
+ log::debug!("Got channel id {}.", self.channel.channel_id);
+ self.start_handshake()?;
+ self.state = HostHandshakeState::SentInitiationRequest;
+ }
+ }
+ (
+ HostHandshakeState::SentInitiationRequest,
+ Some(HandshakeMessage::InitiationResponse),
+ ) => {
+ self.continue_handshake()?;
+ self.state = HostHandshakeState::SentCompletionRequest;
+ }
+ (
+ HostHandshakeState::SentCompletionRequest,
+ Some(HandshakeMessage::CompletionResponse),
+ ) => {
+ let device_state = self.finish_handshake()?;
+ self.state = HostHandshakeState::Finished(device_state);
+ }
+ _ => {
+ log::error!("Unexpected handshake state.");
+ return Err(Error::UnexpectedInput);
+ }
+ }
+ Ok(())
+ }
+
+ fn get_channel(&mut self, expected_nonce: &Nonce) -> Result<ControlFlow<(), ()>, Error> {
+ let (nonce, payload) = Nonce::parse(&self.internal_buffer)?;
+ if nonce != *expected_nonce {
+ log::warn!("Received non matching channel request nonce.");
+ return Ok(ControlFlow::Break(()));
+ };
+ let (cid, device_properties) = parse_u16(payload)?;
+ self.channel.channel_id = cid;
+ self.device_properties =
+ heapless::Vec::from_slice(device_properties).map_err(|_| Error::InsufficientBuffer)?;
+ Ok(ControlFlow::Continue(()))
+ }
+
+ fn start_handshake(&mut self) -> Result<(), Error> {
+ self.zero_internal_buffer();
+ let (hss, msg) = NoiseHandshake::start_pairing(
+ &self.device_properties,
+ self.try_to_unlock,
+ &mut self.internal_buffer,
+ )?;
+ let header = Header::new_handshake(
+ self.channel.channel_id,
+ HandshakeMessage::InitiationRequest,
+ msg,
+ )?;
+ self.noise = Some(hss);
+ self.channel.raw_in(header, msg)?;
+ let len = msg.len();
+ self.internal_buffer.truncate(len);
+ Ok(())
+ }
+
+ fn continue_handshake(&mut self) -> Result<(), Error> {
+ let payload_len = self.internal_buffer.len();
+ // Buffer used both for input and output - pad with zeros.
+ self.internal_buffer
+ .resize(self.internal_buffer.capacity(), 0u8)
+ .unwrap();
+ let noise = self.noise.as_mut().ok_or(Error::UnexpectedInput)?;
+ let (nc, msg) =
+ noise.complete_pairing(&mut self.cred_store, &mut self.internal_buffer, payload_len)?;
+ self.channel.noise = Some(nc);
+ self.noise = None;
+ let header = Header::new_handshake(
+ self.channel.channel_id,
+ HandshakeMessage::CompletionRequest,
+ msg,
+ )?;
+ self.channel.raw_in(header, msg)?;
+ let len = msg.len();
+ self.internal_buffer.truncate(len);
+ Ok(())
+ }
+
+ fn finish_handshake(&mut self) -> Result<PairingState, Error> {
+ let payload = &mut self.internal_buffer;
+ let len = self.channel.noise()?.decrypt(payload.as_mut_slice())?;
+ payload.truncate(len); // assumes tag at the end
+ PairingState::try_from(payload.as_slice())
+ }
+
+ fn zero_internal_buffer(&mut self) {
+ self.internal_buffer.clear();
+ self.internal_buffer
+ .resize(self.internal_buffer.capacity(), 0u8)
+ .unwrap();
+ }
+
+ /// True if handshake finished and [`ChannelOpen::complete()`] can be called.
+ pub fn handshake_done(&self) -> bool {
+ matches!(self.state, HostHandshakeState::Finished(_))
+ }
+
+ /// True if the handshake failed and the object should be discarded.
+ pub fn handshake_failed(&self) -> bool {
+ matches!(self.state, HostHandshakeState::Failed)
+ }
+
+ /// Transition into the pairing phase.
+ pub fn complete(self) -> Result<ChannelPairing<B>, Error> {
+ if self.is_broadcast() || self.channel.noise.is_none() {
+ return Err(Error::UnexpectedInput);
+ }
+ log::debug!("Handshake complete.");
+ Ok(match self.state {
+ HostHandshakeState::Finished(ps) => ChannelPairing {
+ channel: self.channel,
+ device_properties: self.device_properties,
+ pairing_state: ps,
+ is_finished: false,
+ },
+ _ => return Err(Error::UnexpectedInput),
+ })
+ }
+}
+
+impl<C, B> ChannelIO for ChannelOpen<C, B>
+where
+ C: CredentialStore,
+ B: Backend,
+{
+ fn packet_in(
+ &mut self,
+ packet_buffer: &[u8],
+ _receive_buffer: &mut [u8],
+ ) -> Result<PacketInResult, Error> {
+ let res = self
+ .channel
+ .packet_in(packet_buffer, &mut self.internal_buffer)?;
+ if res.got_ack() {
+ self.zero_internal_buffer();
+ }
+ if res.got_message() {
+ let handled = self.incoming_internal();
+ if handled.is_err() {
+ self.state = HostHandshakeState::Failed;
+ }
+ handled?;
+ }
+ Ok(res)
+ }
+
+ fn packet_out(&mut self, packet_buffer: &mut [u8], _send_buffer: &[u8]) -> Result<(), Error> {
+ self.channel
+ .packet_out(packet_buffer, &self.internal_buffer)?;
+
+ if self.channel.is_broadcast() && matches!(self.channel.state, ChannelState::Idle) {
+ // no ack/retransmits on broadcast
+ self.zero_internal_buffer();
+ }
+ Ok(())
+ }
+
+ fn packet_out_ready(&self) -> bool {
+ self.channel.packet_out_ready()
+ }
+
+ fn message_in(&mut self, _plaintext_len: usize, _send_buffer: &mut [u8]) -> Result<(), Error> {
+ // Messages from application are ignored during the handshake.
+ Ok(())
+ }
+
+ fn message_in_ready(&self) -> bool {
+ !(self.handshake_done() || self.handshake_failed())
+ }
+
+ fn message_out<'a>(
+ &mut self,
+ receive_buffer: &'a mut [u8],
+ ) -> Result<(u8, u16, &'a [u8]), Error> {
+ Ok((0, 0, &receive_buffer[..0]))
+ }
+
+ fn message_out_ready(&self) -> bool {
+ self.channel.message_out_ready()
+ }
+
+ fn message_retransmit(&mut self) -> Result<(), Error> {
+ self.channel.message_retransmit()
+ }
+}
+
+/// Channel in the pairing or credentials phase.
+///
+/// Application must use this object to exchange protobuf messages as described in
+/// the [Pairing phase] and [Credential phase] section of THP spec. After
+/// `ThpMessageType_ThpEndResponse` is received, call [`ChannelPairing::complete`]
+/// to obtain a [`Channel`].
+///
+/// Corresponds to states `HP0`..`HC2` in the spec.
+///
+/// [Pairing phase]: https://docs.trezor.io/trezor-firmware/common/thp/specification.html#pairing-phase
+/// [Credential phase]: https://docs.trezor.io/trezor-firmware/common/thp/specification.html#credential-phase
+pub struct ChannelPairing<B: Backend> {
+ channel: Channel<Host, B>,
+ device_properties: heapless::Vec<u8, MAX_DEVICE_PROPERTIES_LEN>,
+ pairing_state: PairingState,
+ is_finished: bool, // true after Trezor sends ThpEndResponse, should we block sending messages afterwards?
+}
+
+impl<B: Backend> ChannelPairing<B> {
+ pub fn pairing_done(&self) -> bool {
+ self.is_finished
+ }
+
+ pub fn device_properties(&self) -> &[u8] {
+ self.device_properties.as_slice()
+ }
+
+ pub fn pairing_state(&self) -> PairingState {
+ self.pairing_state
+ }
+
+ pub fn complete(self) -> Result<Channel<Host, B>, Error> {
+ if !self.is_finished {
+ return Err(Error::UnexpectedInput);
+ }
+ log::debug!("Pairing and credentials complete, begin application level transport.");
+ Ok(self.channel)
+ }
+}
+
+impl<B: Backend> ChannelIO for ChannelPairing<B> {
+ fn packet_in(
+ &mut self,
+ packet_buffer: &[u8],
+ receive_buffer: &mut [u8],
+ ) -> Result<PacketInResult, Error> {
+ self.channel.packet_in(packet_buffer, receive_buffer)
+ }
+
+ fn packet_out(&mut self, packet_buffer: &mut [u8], send_buffer: &[u8]) -> Result<(), Error> {
+ self.channel.packet_out(packet_buffer, send_buffer)
+ }
+
+ fn packet_out_ready(&self) -> bool {
+ self.channel.packet_out_ready()
+ }
+
+ fn message_in(&mut self, plaintext_len: usize, send_buffer: &mut [u8]) -> Result<(), Error> {
+ self.channel.message_in(plaintext_len, send_buffer)
+ }
+
+ fn message_in_ready(&self) -> bool {
+ self.channel.message_in_ready()
+ }
+
+ fn message_out<'a>(
+ &mut self,
+ receive_buffer: &'a mut [u8],
+ ) -> Result<(u8, u16, &'a [u8]), Error> {
+ let (sid, message_type, message) = self.channel.message_out(receive_buffer)?;
+ if sid != 0 {
+ log::error!("Invalid session id in pairing phase.");
+ return Err(Error::MalformedData);
+ }
+ if message_type == MESSAGE_TYPE_END_RESPONSE {
+ self.is_finished = true;
+ }
+ Ok((0, message_type, message))
+ }
+
+ fn message_out_ready(&self) -> bool {
+ self.channel.message_out_ready()
+ }
+
+ fn message_retransmit(&mut self) -> Result<(), Error> {
+ self.channel.message_retransmit()
+ }
+}
diff --git a/rust/trezor-thp/src/channel/mod.rs b/rust/trezor-thp/src/channel/mod.rs
new file mode 100644
index 00000000..a3435356
--- /dev/null
+++ b/rust/trezor-thp/src/channel/mod.rs
@@ -0,0 +1,538 @@
+pub mod host;
+mod noise;
+
+use crate::{
+ Error, Role,
+ alternating_bit::{ChannelSync, SyncBits},
+ error::{Result, TransportError},
+ fragment::{Fragmenter, Reassembler},
+ header::{BROADCAST_CHANNEL_ID, Header, NONCE_LEN, parse_cb_channel, parse_u16},
+};
+
+pub use noise::Backend;
+use noise::{HANDSHAKE_HASH_LEN, NoiseCiphers, TAG_LEN};
+
+const APP_HEADER_LEN: usize = 3; // session id (1) + message type (2)
+
+/// Used during channel allocation on broadcast channel.
+#[derive(Copy, Clone, PartialEq, Eq)]
+struct Nonce([u8; NONCE_LEN as _]);
+
+impl Nonce {
+ pub const LEN: usize = NONCE_LEN as _;
+
+ pub fn random<B: Backend>() -> Self {
+ let mut bytes = [0u8; Self::LEN];
+ B::random_bytes(&mut bytes);
+ Self(bytes)
+ }
+
+ pub fn parse(bytes: &[u8]) -> Result<(Self, &[u8])> {
+ bytes
+ .split_first_chunk::<{ Nonce::LEN }>()
+ .map(|(n, p)| (Nonce(*n), p))
+ .ok_or(Error::MalformedData)
+ }
+
+ pub fn as_slice(&self) -> &[u8] {
+ &self.0
+ }
+}
+
+/// Sent by device after successful handshake to indicate whether pairing is required.
+#[repr(u8)]
+#[derive(Copy, Clone)]
+pub enum PairingState {
+ Unpaired = 0,
+ Paired = 1,
+ PairedAutoconnect = 2,
+}
+
+impl PairingState {
+ pub fn is_paired(&self) -> bool {
+ !matches!(self, Self::Unpaired)
+ }
+}
+
+impl TryFrom<&[u8]> for PairingState {
+ type Error = Error;
+
+ fn try_from(bytes: &[u8]) -> Result<Self> {
+ Ok(match bytes {
+ [0] => Self::Unpaired,
+ [1] => Self::Paired,
+ [2] => Self::PairedAutoconnect,
+ _ => return Err(Error::MalformedData),
+ })
+ }
+}
+
+/// Is the channel currently sending or receiving a message?
+enum ChannelState<R: Role> {
+ /// Ready to send or receive.
+ Idle,
+ /// In the process of sending a message, or waiting for ACK.
+ Sending(Fragmenter<R>),
+ /// In the process of receiving a message, or waiting for the consumer to pick up
+ /// an assembled message.
+ Receiving(Reassembler<R>),
+ /// Channel is inoperable.
+ /// None: local failure
+ /// Some: error message received from other side
+ Failed(Option<TransportError>),
+}
+
+/// THP channel with established secure layer.
+///
+/// There is no constructor, to obtain a channel please use [`host::ChannelOpen`]
+/// or `device::ChannelOpen`.
+/// For actually sending and receiving messages please see [`ChannelIO`].
+pub struct Channel<R: Role, B: Backend> {
+ channel_id: u16,
+ sync: ChannelSync,
+ noise: Option<NoiseCiphers<B>>,
+ send_ack: Option<SyncBits>,
+ state: ChannelState<R>,
+}
+
+impl<R: Role, B: Backend> Channel<R, B> {
+ fn new(channel_id: u16) -> Self {
+ Self {
+ channel_id,
+ sync: ChannelSync::new(),
+ noise: None,
+ send_ack: None,
+ state: ChannelState::Idle,
+ }
+ }
+
+ fn noise(&mut self) -> Result<&mut NoiseCiphers<B>> {
+ self.noise.as_mut().ok_or(Error::UnexpectedInput)
+ }
+
+ fn is_broadcast(&self) -> bool {
+ self.channel_id == BROADCAST_CHANNEL_ID
+ }
+
+ pub fn channel_id(&self) -> u16 {
+ self.channel_id
+ }
+
+ pub fn handshake_hash(&self) -> &[u8; HANDSHAKE_HASH_LEN] {
+ self.noise.as_ref().unwrap().handshake_hash()
+ }
+
+ fn raw_in(&mut self, header: Header<R>, send_buffer: &[u8]) -> Result<()> {
+ let ChannelState::Idle = self.state else {
+ return Err(Error::NotReady);
+ };
+ let sb = if self.is_broadcast() {
+ SyncBits::new()
+ } else {
+ self.sync.send_start().ok_or(Error::NotReady)?
+ };
+ let frag = Fragmenter::new(header, sb, send_buffer)?;
+ self.state = ChannelState::Sending(frag);
+ Ok(())
+ }
+
+ fn raw_out(&mut self, receive_buffer: &[u8]) -> Result<(Header<R>, usize)> {
+ let has_cid = !self.is_broadcast();
+ let ChannelState::Receiving(r) = &mut self.state else {
+ return Err(Error::NotReady);
+ };
+ if !r.is_done() {
+ return Err(Error::NotReady);
+ }
+ let len = match r.verify(receive_buffer) {
+ Ok(len) => len,
+ Err(e) => {
+ log::warn!(
+ "[{}] Reassembled message with invalid digest.",
+ self.channel_id
+ );
+ return Err(e);
+ }
+ };
+ if has_cid {
+ self.send_ack = Some(self.sync.receive_acknowledge());
+ }
+ let header = r.header();
+ self.state = ChannelState::Idle;
+ Ok((header, len))
+ }
+
+ fn handle_ack(&mut self, packet_buffer: &[u8]) -> Result<PacketInResult> {
+ if self.is_broadcast() {
+ // Ignore ACKs on broadcast channel.
+ return PacketInResult::nothing();
+ }
+ if matches!(self.state, ChannelState::Sending(_)) {
+ let sb = SyncBits::try_from(packet_buffer)?;
+ self.sync.send_mark_delivered(sb);
+ if self.sync.can_send() {
+ self.state = ChannelState::Idle;
+ return PacketInResult::ack();
+ }
+ }
+ log::warn!("[{}] Unexpected ACK.", self.channel_id);
+ PacketInResult::nothing()
+ }
+
+ fn handle_error(&mut self, packet_buffer: &[u8]) -> Result<PacketInResult> {
+ let mut err_buf = [0u8; 16];
+ if let Ok((header, payload)) = Reassembler::<R>::single(packet_buffer, &mut err_buf) {
+ if let Ok(te) = TransportError::try_from(payload) {
+ if header.is_error() {
+ log::error!("[{}] Peer sent an error: {}.", self.channel_id, te as u8);
+ if !te.is_recoverable() {
+ self.state = ChannelState::Failed(Some(te));
+ }
+ return PacketInResult::transport_error(te);
+ }
+ }
+ }
+ log::error!("[{}] Peer sent unknown error.", self.channel_id);
+ self.state = ChannelState::Failed(None);
+ Err(Error::MalformedData)
+ }
+
+ fn handle_init(
+ &mut self,
+ packet_buffer: &[u8],
+ receive_buffer: &mut [u8],
+ ) -> Result<PacketInResult> {
+ let sb = SyncBits::try_from(packet_buffer)?;
+ if !self.is_broadcast() && !self.sync.receive_start(sb) {
+ // Bad sync bit, drop this packet and continuations.
+ log::debug!("[{}] Bad sync bit, ignoring packet.", self.channel_id);
+ self.state = ChannelState::Idle;
+ return PacketInResult::nothing();
+ }
+ receive_buffer.fill(0);
+ let r = Reassembler::new(packet_buffer, receive_buffer)?;
+ let is_done = r.is_done();
+ self.state = ChannelState::Receiving(r);
+ PacketInResult::message(is_done)
+ }
+}
+
+/// Whether channel state changed after calling [`ChannelIO::packet_in`].
+pub struct PacketInResult {
+ ack_received: bool,
+ message_ready: bool,
+ error: Option<TransportError>,
+ // enlarge_buffer: Option<usize>,
+}
+
+impl PacketInResult {
+ const fn new(ack_received: bool, message_ready: bool) -> Self {
+ Self {
+ ack_received,
+ message_ready,
+ error: None,
+ }
+ }
+
+ const fn nothing() -> Result<Self> {
+ Ok(Self::new(false, false))
+ }
+
+ const fn ack() -> Result<Self> {
+ Ok(Self::new(true, false))
+ }
+
+ const fn message(is_done: bool) -> Result<Self> {
+ Ok(Self::new(false, is_done))
+ }
+
+ const fn transport_error(e: TransportError) -> Result<Self> {
+ Ok(Self {
+ ack_received: false,
+ message_ready: false,
+ error: Some(e),
+ })
+ }
+
+ /// True if the received packet was valid ACK.
+ pub const fn got_ack(&self) -> bool {
+ self.ack_received
+ }
+
+ /// True if the received packet was the last fragment of incoming message.
+ /// Event loop should call [`ChannelIO::message_out`]. The message is
+ /// not guaranteed to be valid.
+ pub const fn got_message(&self) -> bool {
+ self.message_ready
+ }
+
+ pub const fn got_error(&self) -> bool {
+ self.error.is_some()
+ }
+}
+
+/// Trait for communicating over THP channel.
+///
+/// As we have distinct types for channels in different phases of being established,
+/// they share this trait for doing I/O.
+///
+/// A channel is an object that consumes (USB, BLE, UDP) packets and produces application
+/// messages (usually protobuf encoded), and also consumes such messages to produce packets.
+/// It also needs to be notified when a messages takes too long to send and should to be
+/// retransmitted.
+///
+/// To give the application freedom in handling precious buffers used during message
+/// fragmentation and reassembly, channel does not keep ownership of them, instead they need
+/// to be passed along every call.
+pub trait ChannelIO {
+ /// Session ID (1B) + message type (2B) + AEAD tag (16B).
+ const BUFFER_OVERHEAD: usize = APP_HEADER_LEN + TAG_LEN;
+
+ /// Pass incoming packet into a channel.
+ ///
+ /// Please note the caller should first check whether channel ID matches.
+ ///
+ /// If [`PacketInResult::got_message()`] of the returned value evaluates to true the application
+ /// should call [`ChannelIO::packet_out`].
+ fn packet_in(
+ &mut self,
+ packet_buffer: &[u8],
+ receive_buffer: &mut [u8],
+ ) -> Result<PacketInResult>;
+
+ /// Is channel ready to accept incoming packet?
+ ///
+ /// Only provided for completeness as the channel is always ready to drop unexpected packets.
+ fn packet_in_ready(&self) -> bool {
+ true
+ }
+
+ /// Write outgoing packet to `packet_buffer`. Returns [`Error::NotReady`] if there isn't one.
+ fn packet_out(&mut self, packet_buffer: &mut [u8], send_buffer: &[u8]) -> Result<()>;
+
+ /// Is channel ready to send a packet?
+ fn packet_out_ready(&self) -> bool;
+
+ /// Submit prepared send buffer to encrypt and fragment into packets.
+ ///
+ /// 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.
+ ///
+ /// Instead of this function you can use [`Self::message_in_from`] to prepare the send
+ /// buffer for you.
+ ///
+ /// Returns [`Error::NotReady`] if the channel hasn't finished sending the previous message
+ /// (did not send all fragments or did not receive valid ACK), or if the channel is
+ /// currently receiving.
+ fn message_in(&mut self, plaintext_len: usize, send_buffer: &mut [u8]) -> Result<()>;
+
+ /// Is channel ready to send next message?
+ ///
+ /// After calling [`Self::message_in`] this method returns `false` until valid ACK is received
+ /// from the other side.
+ fn message_in_ready(&self) -> bool;
+
+ /// Pick up reassembled and decrypted message. The channel will send ACK packet afterwards.
+ ///
+ /// Returns [`Error::NotReady`] if there is no reassembled message.
+ ///
+ /// Returns [`Error::InvalidChecksum`] if message integrity failed. Usually the other side
+ /// will retransmit the message.
+ fn message_out<'a>(&mut self, receive_buffer: &'a mut [u8]) -> Result<(u8, u16, &'a [u8])>;
+
+ /// Is there a reassembled incoming message ready?
+ ///
+ /// You can use [`PacketInResult::got_message`] instead of this method.
+ ///
+ /// Please note the incoming message may not be valid.
+ fn message_out_ready(&self) -> bool; // unused
+
+ /// Retransmit message previously submitted using [`Self::message_in`]. Does nothing if
+ /// ACK was already received.
+ fn message_retransmit(&mut self) -> Result<()>;
+
+ /// Submit message for channel to encrypt and fragment into packets.
+ ///
+ /// The length of `send_buffer` must be at least [`Self::BUFFER_OVERHEAD`] more
+ /// than the message length.
+ ///
+ /// Returns [`Error::NotReady`] if the channel hasn't finished sending the previous message
+ /// (did not send all fragments or did not receive valid ACK), or if the channel is
+ /// currently receiving.
+ fn message_in_from(
+ &mut self,
+ session_id: u8,
+ message_type: u16,
+ message: &[u8],
+ send_buffer: &mut [u8],
+ ) -> Result<()> {
+ if !self.message_in_ready() {
+ return Err(Error::NotReady);
+ }
+ let plaintext_len = message.len() + APP_HEADER_LEN;
+ if send_buffer.len() < plaintext_len {
+ return Err(Error::InsufficientBuffer);
+ }
+ send_buffer[0] = session_id;
+ send_buffer[1..3].copy_from_slice(&message_type.to_be_bytes());
+ send_buffer[3..plaintext_len].copy_from_slice(message);
+ self.message_in(plaintext_len, send_buffer)
+ }
+}
+
+impl<R: Role, B: Backend> ChannelIO for Channel<R, B> {
+ fn packet_in(
+ &mut self,
+ packet_buffer: &[u8],
+ receive_buffer: &mut [u8],
+ ) -> Result<PacketInResult> {
+ if let ChannelState::Failed(_e) = self.state {
+ return PacketInResult::nothing();
+ }
+ let (cb, channel_id, _rest) = parse_cb_channel(packet_buffer)?;
+ if channel_id != self.channel_id {
+ log::warn!(
+ "[{}] Invalid channel {}, ignoring.",
+ self.channel_id,
+ channel_id
+ );
+ return PacketInResult::nothing();
+ }
+ if cb.is_ack() {
+ return self.handle_ack(packet_buffer);
+ } else if cb.is_error() {
+ return self.handle_error(packet_buffer);
+ }
+ let is_cont = cb.is_continuation();
+ if !(is_cont
+ || cb.is_channel_allocation_request()
+ || cb.is_channel_allocation_response()
+ || cb.is_handshake()
+ || cb.is_encrypted_transport())
+ {
+ log::warn!(
+ "[{}] Invalid control byte {}.",
+ self.channel_id,
+ u8::from(cb)
+ );
+ return PacketInResult::nothing();
+ }
+ match &mut self.state {
+ // First fragment.
+ ChannelState::Receiving(_) | ChannelState::Idle if !is_cont => {
+ self.handle_init(packet_buffer, receive_buffer)
+ }
+ // Continuation fragments.
+ ChannelState::Receiving(r) => {
+ r.update(packet_buffer, receive_buffer)?;
+ PacketInResult::message(r.is_done())
+ }
+ // Ignore unexpected continuations.
+ ChannelState::Idle => PacketInResult::nothing(),
+ // Might possibly happen when we've sent an ACK and it got lost.
+ // We end up sending reply while the other side is retransmitting.
+ // Is this recoverable?
+ ChannelState::Sending(_) => PacketInResult::nothing(),
+ ChannelState::Failed(_e) => unreachable!(),
+ }
+ }
+
+ fn packet_out(&mut self, packet_buffer: &mut [u8], send_buffer: &[u8]) -> Result<()> {
+ // Send pending ACK.
+ if let Some(sb) = self.send_ack.take() {
+ let header = Header::<R>::new_ack(self.channel_id)?;
+ Fragmenter::single(header, sb, &[], packet_buffer)?;
+ return Ok(());
+ }
+ let ChannelState::Sending(f) = &mut self.state else {
+ return Err(Error::NotReady);
+ };
+ let written = f.next(send_buffer, packet_buffer)?;
+ if !written {
+ return Err(Error::NotReady);
+ }
+ if f.is_done() {
+ if self.is_broadcast() {
+ // No ACKs without channel ID, assume delivered.
+ self.state = ChannelState::Idle;
+ } else {
+ self.sync.send_finish();
+ }
+ }
+ Ok(())
+ }
+
+ fn packet_out_ready(&self) -> bool {
+ if self.send_ack.is_some() {
+ return true;
+ }
+ match &self.state {
+ ChannelState::Sending(f) => !f.is_done(),
+ _ => false,
+ }
+ }
+
+ fn message_in(&mut self, plaintext_len: usize, send_buffer: &mut [u8]) -> Result<()> {
+ if !self.message_in_ready() {
+ return Err(Error::NotReady);
+ }
+ let encrypted_len = plaintext_len + TAG_LEN;
+ if send_buffer.len() < encrypted_len {
+ return Err(Error::InsufficientBuffer);
+ }
+ self.noise()?.encrypt(send_buffer, plaintext_len)?;
+ let header = Header::new_encrypted(self.channel_id, &send_buffer[..encrypted_len])?;
+ self.raw_in(header, send_buffer)
+ }
+
+ fn message_in_ready(&self) -> bool {
+ matches!(self.state, ChannelState::Idle)
+ }
+
+ fn message_out<'a>(&mut self, receive_buffer: &'a mut [u8]) -> Result<(u8, u16, &'a [u8])> {
+ let (header, len) = self.raw_out(receive_buffer)?;
+ let receive_buffer = &mut receive_buffer[..len];
+
+ if !header.is_encrypted() {
+ log::error!(
+ "[{}] Invalid message type, expecting EncryptedTransport.",
+ self.channel_id
+ );
+ return Err(Error::MalformedData);
+ }
+
+ 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(None);
+ return Err(e);
+ }
+ };
+ if receive_buffer.len() < APP_HEADER_LEN {
+ log::error!("[{}] Incoming message too short.", self.channel_id);
+ // fails on the next two lines
+ }
+ let (session_id, rest) = receive_buffer.split_first().ok_or(Error::MalformedData)?;
+ let (message_type, rest) = parse_u16(rest)?;
+ Ok((*session_id, message_type, rest))
+ }
+
+ fn message_out_ready(&self) -> bool {
+ match &self.state {
+ ChannelState::Receiving(r) => r.is_done(),
+ _ => false,
+ }
+ }
+
+ fn message_retransmit(&mut self) -> Result<()> {
+ let ChannelState::Sending(f) = &mut self.state else {
+ log::warn!("[{}] Nothing to retransmit.", self.channel_id);
+ return Ok(());
+ };
+ log::debug!("[{}] Retransmitting message.", self.channel_id);
+ f.reset();
+ Ok(())
+ }
+}
diff --git a/rust/trezor-thp/src/channel/noise.rs b/rust/trezor-thp/src/channel/noise.rs
new file mode 100644
index 00000000..283945aa
--- /dev/null
+++ b/rust/trezor-thp/src/channel/noise.rs
@@ -0,0 +1,151 @@
+use trezor_noise_protocol::{
+ Cipher, CipherState, DH, HandshakeState, Hash, U8Array, patterns::noise_xx,
+};
+
+use crate::{Error, Host, Role, credential::CredentialStore};
+
+use core::marker::PhantomData;
+
+pub const HANDSHAKE_HASH_LEN: usize = 32;
+pub const TAG_LEN: usize = 16;
+pub const MAX_KEY_AND_CREDENTIAL_LEN: usize = 128;
+
+/// Cryptography backend trait.
+///
+/// Please see [`trezor-noise-rust-crypto` and `trezor-noise-ring`](http://github.com/trezor/noise-rust)
+/// for implementations based on rust-crypto and ring.
+pub trait Backend {
+ type Cipher: Cipher;
+ type DH: DH;
+ type Hash: Hash;
+
+ /// Fill entire destination with random bytes.
+ fn random_bytes(dest: &mut [u8]);
+}
+
+type DHPrivKey<B> = <<B as Backend>::DH as DH>::Key;
+type DHPubKey<B> = <<B as Backend>::DH as DH>::Pubkey;
+
+pub struct NoiseHandshake<R: Role, B: Backend> {
+ hss: HandshakeState<B::DH, B::Cipher, B::Hash>,
+ _phantom: PhantomData<R>,
+}
+pub struct NoiseCiphers<B: Backend> {
+ encrypt: CipherState<B::Cipher>,
+ decrypt: CipherState<B::Cipher>,
+ handshake_hash: [u8; HANDSHAKE_HASH_LEN],
+}
+
+impl<B: Backend> NoiseCiphers<B> {
+ pub fn encrypt(&mut self, in_out: &mut [u8], plaintext_len: usize) -> Result<(), Error> {
+ if in_out.len() < plaintext_len + TAG_LEN {
+ return Err(Error::InsufficientBuffer);
+ }
+ self.encrypt.encrypt_ad_in_place(&[], in_out, plaintext_len);
+ Ok(())
+ }
+
+ pub fn decrypt(&mut self, in_out: &mut [u8]) -> Result<usize, Error> {
+ if in_out.len() < TAG_LEN {
+ return Err(Error::MalformedData);
+ }
+ self.decrypt
+ .decrypt_ad_in_place(&[], in_out, in_out.len())
+ .map_err(|()| Error::CryptoError)
+ }
+
+ pub fn handshake_hash(&self) -> &[u8; HANDSHAKE_HASH_LEN] {
+ &self.handshake_hash
+ }
+}
+
+impl<B: Backend> NoiseHandshake<Host, B> {
+ pub fn start_pairing<'a>(
+ device_properties: &[u8],
+ try_to_unlock: bool,
+ dest: &'a mut [u8],
+ ) -> Result<(Self, &'a [u8]), Error> {
+ let payload = &[u8::from(try_to_unlock)];
+ let mut hss = HandshakeState::new(
+ noise_xx(),
+ /*is_initiator=*/ true,
+ /*prologue=*/ device_properties,
+ /*s=*/ None, // will be set later based on cred_lookup
+ /*e=*/ None, // random
+ /*re=*/ None,
+ /*rs=*/ None,
+ );
+ let len = hss.get_next_message_overhead() + payload.len();
+ let dest = dest.get_mut(..len).ok_or(Error::InsufficientBuffer)?;
+ hss.write_message(payload, dest)?;
+ let new = NoiseHandshake {
+ hss,
+ _phantom: PhantomData,
+ };
+ Ok((new, dest))
+ }
+
+ pub fn complete_pairing<'a>(
+ &mut self,
+ cred_store: &mut impl CredentialStore,
+ buffer: &'a mut [u8],
+ incoming_len: usize,
+ ) -> Result<(NoiseCiphers<B>, &'a [u8]), Error> {
+ if incoming_len != self.hss.get_next_message_overhead() {
+ log::error!("Unexpected message length during handshake.");
+ return Err(Error::MalformedData);
+ }
+ let incoming = buffer
+ .get(..incoming_len)
+ .ok_or(Error::InsufficientBuffer)?;
+ self.hss.read_message(incoming, &mut [])?;
+
+ // Look up static key based on remote keys, or generate a new one.
+ let remote_static_key = self.hss.get_rs().ok_or(Error::CryptoError)?;
+ let remote_ephemeral_key = self.hss.get_re().ok_or(Error::CryptoError)?;
+ let (local_static, pairing_credential) =
+ Self::credential_from_store(cred_store, &remote_ephemeral_key, &remote_static_key)?;
+ self.hss.set_s(local_static);
+
+ buffer.fill(0);
+ let len = self.hss.get_next_message_overhead() + pairing_credential.len();
+ let dest = buffer.get_mut(..len).ok_or(Error::InsufficientBuffer)?;
+ self.hss
+ .write_message(pairing_credential.as_slice(), dest)?;
+ if !self.hss.completed() {
+ log::error!("Handshake not completed.");
+ return Err(Error::CryptoError);
+ }
+ let (encrypt, decrypt) = self.hss.get_ciphers();
+ let mut handshake_hash = [0u8; HANDSHAKE_HASH_LEN];
+ handshake_hash.copy_from_slice(self.hss.get_hash());
+ let nc = NoiseCiphers {
+ encrypt,
+ decrypt,
+ handshake_hash,
+ };
+ Ok((nc, dest))
+ }
+
+ fn credential_from_store(
+ cs: &impl CredentialStore,
+ re: &DHPubKey<B>,
+ rs: &DHPubKey<B>,
+ ) -> Result<(DHPrivKey<B>, heapless::Vec<u8, MAX_KEY_AND_CREDENTIAL_LEN>), Error>
+ where
+ DHPrivKey<B>: U8Array,
+ {
+ let mut buf = heapless::Vec::new();
+ buf.resize(buf.capacity(), 0u8).unwrap();
+ let result = cs.lookup(re.as_slice(), rs.as_slice(), buf.as_mut_slice());
+ if let Some(found) = result {
+ let found_key = <DHPrivKey<B> as U8Array>::from_slice(found.local_static_privkey);
+ let found_credential = heapless::Vec::from_slice(found.auth_credential)
+ .map_err(|_| Error::InsufficientBuffer)?;
+ return Ok((found_key, found_credential));
+ }
+ buf.clear();
+ let new_key = <B::DH as DH>::genkey();
+ Ok((new_key, buf))
+ }
+}
diff --git a/rust/trezor-thp/src/control_byte.rs b/rust/trezor-thp/src/control_byte.rs
index 0b227e8b..6313fcaf 100644
--- a/rust/trezor-thp/src/control_byte.rs
+++ b/rust/trezor-thp/src/control_byte.rs
@@ -1,4 +1,4 @@
-use crate::alternating_bit::SyncBits;
+use crate::{alternating_bit::SyncBits, error::Error};
pub const CODEC_V1: u8 = 0x3F;
pub const CONTINUATION_PACKET: u8 = 0x80;
@@ -72,6 +72,13 @@ impl ControlByte {
pub const fn is_channel_allocation_response(&self) -> bool {
self.0 == CHANNEL_ALLOCATION_RES
}
+
+ pub const fn is_handshake(&self) -> bool {
+ self.0 & DATA_MASK == HANDSHAKE_INIT_REQ
+ || self.0 & DATA_MASK == HANDSHAKE_INIT_RES
+ || self.0 & DATA_MASK == HANDSHAKE_COMP_REQ
+ || self.0 & DATA_MASK == HANDSHAKE_COMP_RES
+ }
}
// Note consider TryFrom + validation
@@ -81,6 +88,15 @@ impl From<u8> for ControlByte {
}
}
+impl TryFrom<&[u8]> for ControlByte {
+ type Error = Error;
+
+ fn try_from(bytes: &[u8]) -> Result<Self, Error> {
+ let first_byte = bytes.first().ok_or(Error::MalformedData)?;
+ Ok(Self::from(*first_byte))
+ }
+}
+
impl From<ControlByte> for u8 {
fn from(cb: ControlByte) -> Self {
cb.0
diff --git a/rust/trezor-thp/src/credential.rs b/rust/trezor-thp/src/credential.rs
new file mode 100644
index 00000000..f690dea4
--- /dev/null
+++ b/rust/trezor-thp/src/credential.rs
@@ -0,0 +1,34 @@
+pub const CREDENTIAL_PRIVKEY_LENGTH: usize = 32;
+
+pub struct FoundCredential<'a> {
+ pub local_static_privkey: &'a [u8; CREDENTIAL_PRIVKEY_LENGTH],
+ pub auth_credential: &'a [u8],
+}
+
+/// Host-side credential store.
+/// Basically a set of (`remote_static_pubkey`, `local_static_privkey`, `auth_credential`).
+pub trait CredentialStore {
+ /// Find a record such that `remote_static_pubkey` satisfies
+ /// `masked_static_pubkey == X25519(SHA-256(remote_static_pubkey || ephemeral_pubkey), remote_static_pubkey)`.
+ /// If found, write `local_static_privkey` and `auth_credential` to `dest` and return the written subslices.
+ fn lookup<'a>(
+ &self,
+ ephemeral_pubkey: &[u8],
+ masked_static_pubkey: &[u8],
+ dest: &'a mut [u8],
+ ) -> Option<FoundCredential<'a>>;
+}
+
+/// Never finds a matching credential.
+pub struct NullCredentialStore;
+
+impl CredentialStore for NullCredentialStore {
+ fn lookup<'a>(
+ &self,
+ _ephemeral: &[u8],
+ _masked_static: &[u8],
+ _dest: &'a mut [u8],
+ ) -> Option<FoundCredential<'a>> {
+ None
+ }
+}
diff --git a/rust/trezor-thp/src/error.rs b/rust/trezor-thp/src/error.rs
index ef2864b1..e7b484e5 100644
--- a/rust/trezor-thp/src/error.rs
+++ b/rust/trezor-thp/src/error.rs
@@ -1,13 +1,32 @@
+use trezor_noise_protocol::{Error as NoiseError, ErrorKind as NoiseErrorKind};
+
+/// Payload of `transport_error` message type (0x42).
#[cfg_attr(any(test, debug_assertions), derive(Debug))]
-#[derive(Clone, Copy, PartialEq)]
+#[derive(Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum TransportError {
+ /// Issued by the recipient when the transport layer is busy reassembling a message
+ /// on another channel.
TransportBusy = 1,
+ /// Issued by Trezor in response to a message that has a channel identifier which
+ /// is not allocated.
UnallocatedChannel = 2,
+ /// Issued by Trezor in response to a message that has an invalid authentication tag.
DecryptionFailed = 3,
+ /// Issued by Trezor in response to handshake messages (`HandshakeInitiationRequest`,
+ /// `HandshakeContinuationRequest``) received while the device is locked.
DeviceLocked = 5,
}
+impl TransportError {
+ pub fn is_recoverable(&self) -> bool {
+ matches!(
+ self,
+ TransportError::TransportBusy | TransportError::DeviceLocked
+ )
+ }
+}
+
impl TryFrom<u8> for TransportError {
type Error = Error;
@@ -22,18 +41,44 @@ impl TryFrom<u8> for TransportError {
}
}
+impl TryFrom<&[u8]> for TransportError {
+ type Error = Error;
+
+ fn try_from(val: &[u8]) -> Result<Self> {
+ val.first()
+ .ok_or(Error::MalformedData)
+ .and_then(|b| TransportError::try_from(*b))
+ }
+}
+
#[cfg_attr(any(test, debug_assertions), derive(Debug))]
+#[derive(PartialEq, Eq)]
pub enum Error {
/// Numeric field has forbidden value.
OutOfBounds,
/// Invalid data/operation from crate user.
UnexpectedInput,
+ /// Channel is not ready to send another message, or there is no message ready to be passed
+ /// to the caller.
+ NotReady,
/// Invalid data from the wire.
MalformedData,
/// Checksum doesn't match.
- InvalidDigest,
+ InvalidChecksum,
/// Provided buffer is too small.
InsufficientBuffer,
+ /// Secure channel cryptography failed.
+ CryptoError,
+}
+
+impl From<NoiseError> for Error {
+ fn from(val: NoiseError) -> Self {
+ match val.kind() {
+ NoiseErrorKind::DH | NoiseErrorKind::Decryption => Self::CryptoError,
+ NoiseErrorKind::NeedPSK => panic!(),
+ NoiseErrorKind::TooShort => Self::MalformedData,
+ }
+ }
}
pub type Result<T> = core::result::Result<T, Error>;
diff --git a/rust/trezor-thp/src/fragment.rs b/rust/trezor-thp/src/fragment.rs
index f51850ec..044c1415 100644
--- a/rust/trezor-thp/src/fragment.rs
+++ b/rust/trezor-thp/src/fragment.rs
@@ -29,7 +29,7 @@ impl<R: Role> Fragmenter<R> {
}
pub fn next(&mut self, payload: &[u8], dest: &mut [u8]) -> Result<bool> {
- const MIN_PACKET_SIZE: usize = Header::<crate::Device>::new_ack(0).header_len() + 1;
+ const MIN_PACKET_SIZE: usize = Header::<crate::Device>::new_ping().header_len() + 1;
if dest.len() < MIN_PACKET_SIZE {
return Err(Error::InsufficientBuffer);
}
@@ -49,7 +49,7 @@ impl<R: Role> Fragmenter<R> {
self.checksum.update(&dest[..header_len]);
header_len
} else {
- let cont_header = Header::<R>::new_continuation(self.header.channel_id());
+ let cont_header = Header::<R>::new_continuation(self.header.channel_id())?;
cont_header
.to_bytes(SyncBits::new(), dest)
.ok_or(Error::UnexpectedInput)?
@@ -84,6 +84,12 @@ impl<R: Role> Fragmenter<R> {
payload_done && crc_done
}
+ pub fn reset(&mut self) {
+ self.offset = 0;
+ self.checksum = Crc32::new();
+ self.crc_offset = 0;
+ }
+
/// Shortcut to serialize packet into buffer known to be large enough.
pub fn single(header: Header<R>, sb: SyncBits, payload: &[u8], dest: &mut [u8]) -> Result<()> {
let mut fragmenter = Self::new(header, sb, payload)?;
@@ -132,12 +138,19 @@ impl<R: Role> Reassembler<R> {
pub fn update(&mut self, input: &[u8], buffer: &mut [u8]) -> Result<()> {
let (header, after_header) = Header::<R>::parse(input)?;
if !header.is_continuation() {
- log::error!("Unexpected continuation.");
+ log::error!(
+ "[{}] Unexpected initiation packet.",
+ self.header.channel_id()
+ );
return Err(Error::UnexpectedInput);
}
if header.channel_id() != self.header.channel_id() {
- log::error!("Unexpected channel id.");
+ log::error!(
+ "[{}] Unexpected channel id {}.",
+ self.header.channel_id(),
+ header.channel_id()
+ );
return Err(Error::OutOfBounds);
}
@@ -163,18 +176,18 @@ impl<R: Role> Reassembler<R> {
pub fn verify(&self, buffer: &[u8]) -> Result<usize> {
if !self.is_done() {
- return Err(Error::InvalidDigest);
+ return Err(Error::UnexpectedInput);
}
let computed_checksum = self.checksum.finalize();
let length_no_checksum =
usize::from(self.header.payload_len()).saturating_sub(CHECKSUM_LEN);
let received_checksum = *buffer
.get(length_no_checksum..)
- .ok_or(Error::InvalidDigest)?
+ .ok_or(Error::InvalidChecksum)?
.first_chunk::<CHECKSUM_LEN>()
- .ok_or(Error::InvalidDigest)?;
+ .ok_or(Error::InvalidChecksum)?;
if computed_checksum != received_checksum {
- return Err(Error::InvalidDigest);
+ return Err(Error::InvalidChecksum);
}
Ok(length_no_checksum)
}
@@ -247,8 +260,8 @@ mod test {
for i in 0..DATA.len() {
let source = &DATA[..i];
- let channel_id = i as u16;
- let header = Header::<Host>::new_encrypted(channel_id, source);
+ let channel_id = 1 + i as u16;
+ let header = Header::<Host>::new_encrypted(channel_id, source).unwrap();
let packets = fragment(header, SyncBits::new(), source, PACKET_SIZE);
// println!("message len: {}, packets: {}", i, packets.len());
@@ -314,7 +327,7 @@ mod test {
#[test]
fn test_write_empty_payload() {
- let header = Header::<Device>::new_encrypted(CHANNEL_ID, &[]);
+ let header = Header::<Device>::new_encrypted(CHANNEL_ID, &[]).unwrap();
let packets = fragment(header, SyncBits::new(), &[], PACKET_LEN);
assert_eq!(packets.len(), 1);
assert_eq!(hex::encode(&packets[0]), EMPTY_PAYLOAD_EXPECTED);
@@ -323,7 +336,7 @@ mod test {
#[test]
fn test_write_short_payload() {
let data = &[0x07];
- let header = Header::<Device>::new_encrypted(CHANNEL_ID, data);
+ let header = Header::<Device>::new_encrypted(CHANNEL_ID, data).unwrap();
let packets = fragment(header, SyncBits::new(), data, PACKET_LEN);
assert_eq!(packets.len(), 1);
assert_eq!(hex::encode(&packets[0]), SHORT_PAYLOAD_EXPECTED);
@@ -332,7 +345,7 @@ mod test {
#[test]
fn test_write_longer_payload() {
let data: Vec<u8, 256> = (0..=255).collect();
- let header = Header::<Device>::new_encrypted(CHANNEL_ID, &data);
+ let header = Header::<Device>::new_encrypted(CHANNEL_ID, &data).unwrap();
let packets = fragment(header, SyncBits::new(), &data, PACKET_LEN);
assert_eq!(packets.len(), LONGER_PAYLOAD_EXPECTED.len());
packets
@@ -346,7 +359,7 @@ mod test {
let data: Vec<u8, 2048> = (0..2048u16)
.map(|n| u8::try_from(n & 0xff).unwrap())
.collect();
- let header = Header::<Device>::new_encrypted(CHANNEL_ID, &data);
+ let header = Header::<Device>::new_encrypted(CHANNEL_ID, &data).unwrap();
let packets = fragment(header, SyncBits::new(), &data, PACKET_LEN);
assert_eq!(packets.len(), EVEN_LONGER_PAYLOADS_EXPECTED.len());
packets
diff --git a/rust/trezor-thp/src/header.rs b/rust/trezor-thp/src/header.rs
index bd613b65..f4fa57e5 100644
--- a/rust/trezor-thp/src/header.rs
+++ b/rust/trezor-thp/src/header.rs
@@ -1,5 +1,5 @@
use crate::Role;
-use crate::alternating_bit::SyncBits;
+pub use crate::alternating_bit::SyncBits;
use crate::control_byte::{self, ControlByte};
use crate::crc32;
use crate::error::{Error, Result};
@@ -7,16 +7,17 @@ use crate::error::{Error, Result};
use core::marker::PhantomData;
const CHECKSUM_LEN: u16 = crc32::CHECKSUM_LEN as u16;
-const NONCE_LEN: u16 = 8;
+pub const NONCE_LEN: u16 = 8;
const MAX_PAYLOAD_LEN: u16 = 60000;
+const MIN_CHANNEL_ID: u16 = 0x0001;
const MAX_CHANNEL_ID: u16 = 0xFFEF;
-const BROADCAST_CHANNEL_ID: u16 = 0xFFFF;
+pub const BROADCAST_CHANNEL_ID: u16 = 0xFFFF;
/// Represents packet header, i.e. control byte, channel id and possibly payload length.
/// Please note that `seq_bit` and `ack_bit` which are also part of the header are handled separately.
#[cfg_attr(any(test, debug_assertions), derive(Debug))]
-#[derive(Clone, PartialEq)]
+#[derive(Clone, PartialEq, Eq)]
pub enum Header<R: Role> {
Continuation {
channel_id: u16,
@@ -50,7 +51,8 @@ pub enum Header<R: Role> {
}
#[cfg_attr(any(test, debug_assertions), derive(Debug))]
-#[derive(Clone, PartialEq)]
+#[derive(Clone, Copy, PartialEq, Eq)]
+#[repr(u8)]
pub enum HandshakeMessage {
InitiationRequest,
InitiationResponse,
@@ -59,16 +61,27 @@ pub enum HandshakeMessage {
}
pub const fn channel_id_valid(channel_id: u16) -> bool {
- channel_id <= MAX_CHANNEL_ID || channel_id == BROADCAST_CHANNEL_ID
+ (MIN_CHANNEL_ID <= channel_id && channel_id <= MAX_CHANNEL_ID)
+ || channel_id == BROADCAST_CHANNEL_ID
}
-fn parse_u16(buffer: &[u8]) -> Result<(u16, &[u8])> {
- let (bytes, rest) = buffer
- .split_first_chunk::<2>()
- .ok_or(Error::MalformedData)?;
+pub(crate) fn parse_u16(buffer: &[u8]) -> Result<(u16, &[u8])> {
+ let Some((bytes, rest)) = buffer.split_first_chunk::<2>() else {
+ log::error!("Packet too short.");
+ return Err(Error::MalformedData);
+ };
Ok((u16::from_be_bytes(*bytes), rest))
}
+pub(crate) fn parse_cb_channel(buffer: &[u8]) -> Result<(ControlByte, u16, &[u8])> {
+ let Some((cb, rest)) = buffer.split_first() else {
+ log::error!("Packet is empty.");
+ return Err(Error::MalformedData);
+ };
+ let (channel_id, rest) = parse_u16(rest)?;
+ Ok((ControlByte::from(*cb), channel_id, rest))
+}
+
impl<R: Role> Header<R> {
const INIT_LEN: usize = 5;
const CONT_LEN: usize = 3;
@@ -76,20 +89,16 @@ impl<R: Role> Header<R> {
/// Parse header from a byte slice. Return remaining subslice on success.
/// Note: sync bits are discarded and need to be obtained from input buffer separately.
pub fn parse(buffer: &[u8]) -> Result<(Self, &[u8])> {
- let Some((first_byte, rest)) = buffer.split_first() else {
- log::error!("Packet too short.");
- return Err(Error::MalformedData);
- };
- let cb = ControlByte::from(*first_byte);
+ let (cb, channel_id, rest) = parse_cb_channel(buffer)?;
if cb.is_codec_v1() {
if R::is_host() {
return Ok((Header::CodecV1Response, &[]));
} else {
- let is_continuation = !matches!(rest.get(..2), Some(b"##"));
+ // v1 initiation packets are prefixed ?##
+ let is_continuation = channel_id != 0x2323;
return Ok((Header::CodecV1Request { is_continuation }, &[]));
}
}
- let (channel_id, rest) = parse_u16(rest)?;
if !channel_id_valid(channel_id) {
log::error!("Invalid channel id {}.", channel_id);
return Err(Error::OutOfBounds);
@@ -105,38 +114,29 @@ impl<R: Role> Header<R> {
// strip padding if there is any
let without_padding = rest.len().min(payload_len.into());
let rest = &rest[..without_padding];
- if let Some(fixed) = Self::parse_fixed(cb, channel_id, payload_len)? {
- return Ok((fixed, rest));
- }
- if let Some(phase) = HandshakeMessage::from_u8::<R>(cb.into()) {
- return Ok((
- Self::Handshake {
- phase,
- channel_id,
- payload_len,
- },
- rest,
- ));
- }
- if cb.is_encrypted_transport() {
- return Ok((
- Self::Encrypted {
- channel_id,
- payload_len,
- },
- rest,
- ));
+ // check for single-packet messages, validate length
+ let mut header: Option<_> = Self::parse_single(cb, channel_id, payload_len)?;
+ if channel_id == BROADCAST_CHANNEL_ID {
+ // fragmentable, broadcast-only messages
+ header = header.or_else(|| Self::parse_broadcast(cb, payload_len));
+ } else {
+ // fragmentable, unicast-only messages
+ header = header.or_else(|| Self::parse_unicast(cb, channel_id, payload_len));
}
- if R::is_host() && channel_id == BROADCAST_CHANNEL_ID && cb.is_channel_allocation_response()
- {
- return Ok((Self::ChannelAllocationResponse { payload_len }, rest));
+ if let Some(header) = header {
+ return Ok((header, rest));
}
- log::error!("Unknown control byte {}.", u8::from(cb));
+ log::error!(
+ "Unknown header: ({}, {}, {}).",
+ u8::from(cb),
+ channel_id,
+ payload_len
+ );
Err(Error::MalformedData)
}
- fn parse_fixed(cb: ControlByte, channel_id: u16, payload_len: u16) -> Result<Option<Self>> {
- let res = if cb.is_ack() {
+ fn parse_single(cb: ControlByte, channel_id: u16, payload_len: u16) -> Result<Option<Self>> {
+ let res = if cb.is_ack() && channel_id != BROADCAST_CHANNEL_ID {
Self::Ack {
channel_id,
_phantom: PhantomData,
@@ -164,7 +164,31 @@ impl<R: Role> Header<R> {
}
}
- fn control_byte(&self, sync_bits: SyncBits) -> Option<ControlByte> {
+ fn parse_broadcast(cb: ControlByte, payload_len: u16) -> Option<Self> {
+ if R::is_host() && cb.is_channel_allocation_response() {
+ return Some(Self::ChannelAllocationResponse { payload_len });
+ }
+ None
+ }
+
+ fn parse_unicast(cb: ControlByte, channel_id: u16, payload_len: u16) -> Option<Self> {
+ if let Some(phase) = HandshakeMessage::from_u8::<R>(cb.into()) {
+ return Some(Self::Handshake {
+ phase,
+ channel_id,
+ payload_len,
+ });
+ }
+ if cb.is_encrypted_transport() {
+ return Some(Self::Encrypted {
+ channel_id,
+ payload_len,
+ });
+ }
+ None
+ }
+
+ pub(crate) fn control_byte(&self, sync_bits: SyncBits) -> Option<ControlByte> {
let cb = match self {
Self::Continuation { .. } => control_byte::CONTINUATION_PACKET,
Self::Ack { .. } => control_byte::ACK_MESSAGE,
@@ -255,27 +279,76 @@ impl<R: Role> Header<R> {
}
}
- pub const fn new_continuation(channel_id: u16) -> Self {
- Self::Continuation { channel_id }
+ fn validate_len(payload: &[u8]) -> Result<u16> {
+ if let Ok(payload_len) = u16::try_from(payload.len()) {
+ let with_crc = payload_len.saturating_add(CHECKSUM_LEN);
+ if with_crc <= MAX_PAYLOAD_LEN {
+ return Ok(with_crc);
+ }
+ }
+ log::error!("Cannot construct: message too long {}.", payload.len());
+ Err(Error::UnexpectedInput)
}
- pub const fn new_encrypted(channel_id: u16, payload: &[u8]) -> Self {
- // FIXME validate payload length, channel id?
- Self::Encrypted {
- channel_id,
- payload_len: payload.len() as u16 + CHECKSUM_LEN,
+ fn validate_channel(channel_id: u16) -> Result<u16> {
+ if !channel_id_valid(channel_id) {
+ log::error!("Cannot construct: invalid channel id {}.", channel_id);
+ return Err(Error::UnexpectedInput);
}
+ Ok(channel_id)
}
- pub const fn new_error(channel_id: u16) -> Self {
- Self::TransportError { channel_id }
+ fn validate_channel_unicast(channel_id: u16) -> Result<u16> {
+ let channel_id = Self::validate_channel(channel_id)?;
+ if channel_id == BROADCAST_CHANNEL_ID {
+ log::error!("Cannot construct: illegal broadcast.");
+ return Err(Error::UnexpectedInput);
+ }
+ Ok(channel_id)
}
- pub const fn new_ack(channel_id: u16) -> Self {
- Self::Ack {
- channel_id,
+ pub fn new_continuation(channel_id: u16) -> Result<Self> {
+ Ok(Self::Continuation {
+ channel_id: Self::validate_channel(channel_id)?,
+ })
+ }
+
+ pub const fn new_channel_request() -> Self {
+ Self::ChannelAllocationRequest
+ }
+
+ pub fn new_channel_response(payload: &[u8]) -> Result<Self> {
+ Ok(Self::ChannelAllocationResponse {
+ payload_len: Self::validate_len(payload)?,
+ })
+ }
+
+ pub fn new_handshake(channel_id: u16, phase: HandshakeMessage, payload: &[u8]) -> Result<Self> {
+ Ok(Self::Handshake {
+ phase,
+ channel_id: Self::validate_channel_unicast(channel_id)?,
+ payload_len: Self::validate_len(payload)?,
+ })
+ }
+
+ pub fn new_encrypted(channel_id: u16, payload: &[u8]) -> Result<Self> {
+ Ok(Self::Encrypted {
+ channel_id: Self::validate_channel_unicast(channel_id)?,
+ payload_len: Self::validate_len(payload)?,
+ })
+ }
+
+ pub fn new_error(channel_id: u16) -> Result<Self> {
+ Ok(Self::TransportError {
+ channel_id: Self::validate_channel(channel_id)?,
+ })
+ }
+
+ pub fn new_ack(channel_id: u16) -> Result<Self> {
+ Ok(Self::Ack {
+ channel_id: Self::validate_channel_unicast(channel_id)?,
_phantom: PhantomData,
- }
+ })
}
pub const fn new_ping() -> Self {
@@ -309,6 +382,25 @@ impl<R: Role> Header<R> {
pub const fn is_ack(&self) -> bool {
matches!(self, Self::Ack { .. })
}
+
+ pub const fn is_error(&self) -> bool {
+ matches!(self, Self::TransportError { .. })
+ }
+
+ pub const fn is_channel_allocation_request(&self) -> bool {
+ matches!(self, Self::ChannelAllocationRequest)
+ }
+
+ pub const fn is_channel_allocation_response(&self) -> bool {
+ matches!(self, Self::ChannelAllocationResponse { .. })
+ }
+
+ pub const fn handshake_phase(&self) -> Option<HandshakeMessage> {
+ match self {
+ Self::Handshake { phase, .. } => Some(*phase),
+ _ => None,
+ }
+ }
}
impl HandshakeMessage {
@@ -400,8 +492,13 @@ mod test {
},
),
("8012345678", Header::Continuation { channel_id: 0x1234 }),
- ("201337000463061764", Header::new_ack(0x1337)),
- ("20ffff000460132e1c", Header::new_ack(BROADCAST_CHANNEL_ID)),
+ (
+ "201337000463061764",
+ Header::Ack {
+ channel_id: 0x1337,
+ _phantom: PhantomData,
+ },
+ ),
(
"041337000457479ea0",
Header::Encrypted {
diff --git a/rust/trezor-thp/src/lib.rs b/rust/trezor-thp/src/lib.rs
index 77c069eb..b9460e4c 100644
--- a/rust/trezor-thp/src/lib.rs
+++ b/rust/trezor-thp/src/lib.rs
@@ -1,21 +1,25 @@
-//! Trezor Host Protocol implementation in Rust.
-
+#![doc = include_str!("../README.md")]
#![no_std]
#![forbid(unsafe_code)]
-pub mod alternating_bit;
+mod alternating_bit;
+pub mod channel;
mod control_byte;
mod crc32;
+pub mod credential;
pub mod error;
pub mod fragment;
pub mod header;
+pub use channel::{Backend, Channel, ChannelIO};
+pub use error::Error;
+
pub trait Role: Clone + PartialEq {
fn is_host() -> bool;
}
#[cfg_attr(any(test, debug_assertions), derive(Debug))]
-#[derive(Clone, PartialEq)]
+#[derive(Clone, PartialEq, Eq)]
pub struct Host;
impl Role for Host {
@@ -25,7 +29,7 @@ impl Role for Host {
}
#[cfg_attr(any(test, debug_assertions), derive(Debug))]
-#[derive(Clone, PartialEq)]
+#[derive(Clone, PartialEq, Eq)]
pub struct Device;
impl Role for Device {
Why this scored 35/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.