feat(rust/trezor-thp): secure channel layer: device side
What changed, and why it matters
This commit adds the device-side implementation of a new secure communication layer (THP) for Trezor hardware wallets. It introduces code that handles encrypted channels, pairing handshakes, and legacy protocol fallback. The change is a feature addition rather than a documented security fix, and the diff itself does not show an exploitable vulnerability. However, it is a large, security-critical change to firmware that will protect future host-device communication.
Treat this as a high-risk feature commit requiring focused review of the Noise handshake state machine, key masking correctness, credential verification interface, and legacy v1 fallback behavior. Run the new test suite and perform a security audit of the device-side handshake before release. No immediate patch is indicated by the diff alone.
Security signals we found
New cryptographic handshake implementation (Noise_XX) on device side
Private key masking before Noise handshake using ephemeral key hash
Channel ID validation and broadcast/non-broadcast separation
Legacy codec v1 fallback response hardcoded as constant
Error classification changes: DeviceLocked no longer recoverable, continuation packets treated as malformed data
Dependency bump for trezor/noise-rust to revision 1bb08dfdfc11160b760b8d28c09677ed72fb1fff
Extensive new test coverage for packet loss, damage, replay, and channel-id wraparound
Evidence from the diff
The commit implements the device-side secure channel for the Trezor THP (Trezor Host Protocol) layer in Rust. It adds a device multiplexer (device::Mux), device handshake state machine (device::ChannelOpen), Noise_XX responder logic, credential verification hooks, and extensive unit tests. It also refactors the host side to remove the separate ChannelPairing phase and updates the noise-rust dependency revision. Several error-handling paths were tightened (e.g., continuation packets now return MalformedData instead of UnexpectedInput, and DeviceLocked is no longer marked recoverable). The code is defensive: it validates channel IDs, checks handshake message lengths, masks the static private key with an ephemeral-derived hash before use, and ignores malformed broadcast packets.
Changed components
rust/trezor-thp/src/channel/device.rsrust/trezor-thp/src/channel/host.rsrust/trezor-thp/src/channel/noise.rsrust/trezor-thp/src/channel/mod.rsrust/trezor-thp/src/credential.rsrust/trezor-thp/src/error.rsrust/trezor-thp/src/fragment.rsrust/trezor-thp/src/header.rsrust/trezor-thp/Cargo.tomlInspect captured patch +1788 / −185
diff --git a/rust/trezor-thp/Cargo.toml b/rust/trezor-thp/Cargo.toml
index 99d72e87..cd6c12d8 100644
--- a/rust/trezor-thp/Cargo.toml
+++ b/rust/trezor-thp/Cargo.toml
@@ -17,12 +17,13 @@ default-features = false
version = "0.2.0"
default-features = false
git = "https://github.com/trezor/noise-rust"
-rev = "916ce4dc25b0dc830832a51f6f37021e2214e4c7"
+rev = "1bb08dfdfc11160b760b8d28c09677ed72fb1fff"
[dev-dependencies]
env_logger = "0.11.8"
getrandom = "0.3.4"
hex = "0.4.3"
+test-case = "3.3.1"
# regenerate examples/host-cli/pb after upgrading protobuf
protobuf = "=3.7.2"
protobuf-codegen = "=3.7.2"
@@ -36,4 +37,4 @@ version = "0.6.2"
default-features = false
features = ["use-x25519", "use-aes-256-gcm", "use-sha2"]
git = "https://github.com/trezor/noise-rust"
-rev = "916ce4dc25b0dc830832a51f6f37021e2214e4c7"
+rev = "1bb08dfdfc11160b760b8d28c09677ed72fb1fff"
diff --git a/rust/trezor-thp/examples/host-cli/client.rs b/rust/trezor-thp/examples/host-cli/client.rs
index 0c20d0a7..f5f4b5e0 100644
--- a/rust/trezor-thp/examples/host-cli/client.rs
+++ b/rust/trezor-thp/examples/host-cli/client.rs
@@ -18,6 +18,7 @@ const MESSAGE_TYPE_BUTTONACK: u16 = 27;
pub struct Client<C> {
pub channel: Buffered<C>,
+ pub device_properties: Vec<u8>,
socket: UdpSocket,
emu_addr: SocketAddr,
}
@@ -32,6 +33,7 @@ where
channel.set_packet_len(PACKET_LEN);
Client {
channel,
+ device_properties: Vec::new(),
socket: UdpSocket::bind("127.0.0.1:0").unwrap(),
emu_addr,
}
@@ -42,6 +44,7 @@ impl<C: ChannelIO> Client<C> {
pub fn map<D>(self, func: impl FnOnce(C) -> D) -> Client<D> {
Client {
channel: self.channel.map(|c| Ok(func(c))).unwrap(),
+ device_properties: self.device_properties,
socket: self.socket,
emu_addr: self.emu_addr,
}
diff --git a/rust/trezor-thp/examples/host-cli/main.rs b/rust/trezor-thp/examples/host-cli/main.rs
index 0d77510b..b6b1c181 100644
--- a/rust/trezor-thp/examples/host-cli/main.rs
+++ b/rust/trezor-thp/examples/host-cli/main.rs
@@ -7,7 +7,7 @@ use protobuf::Message;
use trezor_thp::{
Backend, Channel, Host,
- channel::host::{ChannelOpen, ChannelPairing, Mux},
+ channel::host::{ChannelOpen, Mux},
credential::{CredentialStore, NullCredentialStore},
};
@@ -29,6 +29,8 @@ impl Backend for RustCrypto {
}
}
+type HostChannel = Channel<Host, RustCrypto>;
+
fn do_allocation<C>(client: &mut Client<Mux<C, RustCrypto>>)
where
C: CredentialStore,
@@ -40,17 +42,18 @@ fn do_handshake<C>(client: &mut Client<ChannelOpen<C, RustCrypto>>)
where
C: CredentialStore,
{
+ client.device_properties = client.channel.device_properties().into();
let device_properties =
- ThpDeviceProperties::parse_from_bytes(client.channel.device_properties()).unwrap();
+ ThpDeviceProperties::parse_from_bytes(&client.device_properties).unwrap();
log::debug!("Device properties: {:?}.", device_properties);
// Handshake should finish within 2 request-response cycles.
client.call(0, &[]);
client.call(0, &[]);
}
-fn do_pairing(client: &mut Client<ChannelPairing<RustCrypto>>) {
+fn do_pairing(client: &mut Client<HostChannel>) {
let device_properties =
- ThpDeviceProperties::parse_from_bytes(client.channel.device_properties()).unwrap();
+ ThpDeviceProperties::parse_from_bytes(&client.device_properties).unwrap();
let mut pairing_methods = Vec::new();
for p in &device_properties.pairing_methods {
@@ -67,7 +70,7 @@ fn do_pairing(client: &mut Client<ChannelPairing<RustCrypto>>) {
}
}
-fn do_pairing_skip(client: &mut Client<ChannelPairing<RustCrypto>>) {
+fn do_pairing_skip(client: &mut Client<HostChannel>) {
let mut pairing_request = ThpPairingRequest::new();
pairing_request.set_host_name("localhost".into());
pairing_request.set_app_name("trezor-thp/examples".into());
@@ -118,9 +121,6 @@ pub fn main() -> std::io::Result<()> {
let mut client = client.map(|c| c.complete().unwrap());
do_pairing(&mut client);
- assert!(client.channel.pairing_done());
- let mut client = client.map(|c| c.complete().unwrap());
-
do_ping(&mut client);
Ok(())
}
diff --git a/rust/trezor-thp/examples/transport-level-ping.rs b/rust/trezor-thp/examples/transport-level-ping.rs
index ce81f1b4..2d116b49 100644
--- a/rust/trezor-thp/examples/transport-level-ping.rs
+++ b/rust/trezor-thp/examples/transport-level-ping.rs
@@ -1,15 +1,28 @@
use std::env;
+use std::io::ErrorKind;
use std::net::{SocketAddr, UdpSocket};
use std::str::FromStr;
+use std::time::Duration;
use trezor_thp::{
- Host,
- fragment::{Fragmenter, Reassembler},
- header::{Header, SyncBits},
+ Backend, channel::buffered::ChannelExt, channel::host::Mux, credential::NullCredentialStore,
};
+struct RustCrypto;
+
+impl Backend for RustCrypto {
+ type DH = trezor_noise_rust_crypto::X25519;
+ type Cipher = trezor_noise_rust_crypto::Aes256Gcm;
+ type Hash = trezor_noise_rust_crypto::Sha256;
+
+ fn random_bytes(dest: &mut [u8]) {
+ getrandom::fill(dest).unwrap();
+ }
+}
+
const REPEAT: u8 = 1;
const PACKET_LEN: usize = 64;
+const READ_TIMEOUT: Duration = Duration::from_secs(2);
pub fn main() -> std::io::Result<()> {
let port_str = env::args().nth(1).unwrap_or("21324".to_string());
@@ -17,24 +30,30 @@ pub fn main() -> std::io::Result<()> {
let emu_addr = SocketAddr::from(([127, 0, 0, 1], port));
let socket = UdpSocket::bind("127.0.0.1:0")?;
- let mut sockbuf = [0u8; PACKET_LEN];
- let mut reply_data = [0u8; PACKET_LEN];
-
- for i in 0..REPEAT {
- let nonce = [i; 8]; // not a good nonce
- let sb = SyncBits::new(); // no ABP for ping
- Fragmenter::<Host>::single(Header::new_ping(), sb, &nonce, &mut sockbuf).unwrap();
- socket.send_to(&sockbuf, &emu_addr)?;
-
- let (reply_len, _src_addr) = socket.recv_from(&mut sockbuf).unwrap();
- assert!(reply_len > 0);
- let (header, payload) =
- Reassembler::<Host>::single(&sockbuf[..reply_len], &mut reply_data).unwrap();
-
- if header.is_pong() && payload == &nonce {
+ let mut recvbuf = [0u8; PACKET_LEN];
+ let mut mux = Mux::<_, RustCrypto>::new(NullCredentialStore).into_buffered();
+ mux.set_packet_len(PACKET_LEN);
+
+ for _i in 0..REPEAT {
+ mux.ping();
+ let packet = mux.packet_out().unwrap();
+ socket.send_to(&packet, &emu_addr)?;
+ socket.set_read_timeout(Some(READ_TIMEOUT))?;
+ match socket.recv_from(&mut recvbuf) {
+ Ok(_) => {}
+ Err(e) if matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => {
+ println!("Ping timeout");
+ return Ok(());
+ }
+ Err(e) => {
+ return Err(e);
+ }
+ };
+ let res = mux.packet_in(&recvbuf);
+ if res.got_pong() {
println!("Pong OK");
} else {
- println!("Invalid reply {}", hex::encode(sockbuf));
+ println!("Invalid reply {}", hex::encode(recvbuf));
break;
}
}
diff --git a/rust/trezor-thp/src/channel/device.rs b/rust/trezor-thp/src/channel/device.rs
new file mode 100644
index 00000000..cc3bee09
--- /dev/null
+++ b/rust/trezor-thp/src/channel/device.rs
@@ -0,0 +1,556 @@
+use heapless;
+
+use crate::{
+ Backend, Channel, ChannelIO, Device, Error,
+ alternating_bit::SyncBits,
+ channel::{
+ ChannelState, Nonce, PRIVKEY_LEN, PacketInResult, PairingState, noise::NoiseHandshake,
+ },
+ credential::CredentialVerifier,
+ error::TransportError,
+ fragment::{Fragmenter, Reassembler},
+ header::{
+ BROADCAST_CHANNEL_ID, HandshakeMessage, Header, MAX_CHANNEL_ID, MIN_CHANNEL_ID,
+ channel_id_valid, parse_cb_channel,
+ },
+ util::prepare_zeroed,
+};
+
+use core::marker::PhantomData;
+
+// 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;
+// As long as `packet_out` is called soon after `packet_in` there shouldn't be an accumulation
+// of outgoing messages. However there still can be >1 during normal operation, e.g. when we're
+// responding to PING at the same time the application requests sending an error.
+const BROADCAST_OUTGOING_QUEUE_LEN: usize = 8;
+// "?##" + Failure message type + msg_size + msg_data (code = "Failure_InvalidProtocol")
+const CODEC_V1_RESPONSE: &[u8] = b"?##\x00\x03\x00\x00\x00\x02\x08\x11";
+
+/// Maps packets to channels. Handles broadcast channel messages, notably channel allocation.
+/// Every packet interface on the device needs to have one Mux. Event loop should pass every
+/// incoming packet to [`Mux::packet_in`] in order to determine what to do with it.
+/// Single packet only. Does not keep track of opened channels.
+pub struct Mux<C, B> {
+ // is_locked: bool,
+ next_channel_id: u16,
+ cred_verif: C,
+ outgoing: heapless::Deque<MuxOutgoing, BROADCAST_OUTGOING_QUEUE_LEN>,
+ new_channel: Option<(u16, Nonce)>,
+ _phantom: PhantomData<B>,
+}
+
+enum MuxOutgoing {
+ Error(u16, TransportError),
+ Pong(Nonce),
+ CodecV1Response,
+}
+
+impl MuxOutgoing {
+ pub fn to_str(&self) -> &'static str {
+ match self {
+ Self::Error(_, _) => "transport_error",
+ Self::Pong(_) => "pong",
+ Self::CodecV1Response => "codec_v1_response",
+ }
+ }
+}
+
+impl<C, B> Mux<C, B>
+where
+ C: CredentialVerifier,
+ B: Backend,
+{
+ pub fn new(cred_verif: C) -> Self {
+ // Use random starting id to avoid giving out the number of channels allocated since boot.
+ let next_channel_id = random_channel_id::<B>();
+ Self {
+ next_channel_id,
+ cred_verif,
+ outgoing: heapless::Deque::new(),
+ new_channel: None,
+ _phantom: PhantomData,
+ }
+ }
+
+ /// Create new [`ChannelOpen`] when channel allocation request is pending.
+ pub fn channel_alloc(&mut self) -> Result<ChannelOpen<C, B>, Error> {
+ let Some((channel_id, nonce)) = self.new_channel.take() else {
+ return Err(Error::not_ready());
+ };
+ ChannelOpen::<C, B>::new(channel_id, nonce, self.cred_verif.clone())
+ }
+
+ /// Returns `true` if there is channel allocation request pending.
+ pub fn channel_alloc_ready(&self) -> bool {
+ self.new_channel.is_some()
+ }
+
+ /// Enqueue `TransportError::TransportBusy` for given channel id. Event loop should call this whenever it
+ /// gets packet for existing channel but cannot currently process it, for example because there is no
+ /// available receive buffer. Host is supposed to try again later.
+ pub fn send_transport_busy(&mut self, channel_id: u16) -> Result<(), Error> {
+ self.enqueue(MuxOutgoing::Error(
+ channel_id,
+ TransportError::TransportBusy,
+ ))
+ }
+
+ /// Enqueue TransportError::UnallocatedChannel for given channel id. Event loop should call this
+ /// method whenever it gets [`PacketInResult::Route`] result for a channel that does not exist (anymore).
+ pub fn send_unallocated_channel(&mut self, channel_id: u16) -> Result<(), Error> {
+ self.enqueue(MuxOutgoing::Error(
+ channel_id,
+ TransportError::UnallocatedChannel,
+ ))
+ }
+
+ fn enqueue(&mut self, outgoing: MuxOutgoing) -> Result<(), Error> {
+ self.outgoing.push_back(outgoing).map_err(|o| {
+ log::warn!(
+ "Broadcast channel outgoing queue full, dropped {}.",
+ o.to_str()
+ );
+ Error::not_ready()
+ })
+ }
+
+ fn handle_broadcast(&mut self, packet: &[u8]) -> Result<Option<u16>, Error> {
+ let (header, payload) = Reassembler::<Device>::single_inplace(packet)?;
+ match header {
+ Header::Ping if payload.len() == Nonce::LEN => {
+ let (nonce, _rest) = Nonce::parse(payload).unwrap();
+ self.enqueue(MuxOutgoing::Pong(nonce)).map(|_| None)
+ }
+ Header::ChannelAllocationRequest if payload.len() == Nonce::LEN => {
+ let (nonce, _rest) = Nonce::parse(payload)?;
+ let channel_id = self.next_channel_id;
+ self.next_channel_id += 1;
+ if self.next_channel_id > MAX_CHANNEL_ID {
+ log::debug!("Channel id max value reached, wrapping around.");
+ self.next_channel_id = MIN_CHANNEL_ID;
+ }
+ if self.new_channel.is_some() {
+ log::warn!("Dropping previous channel allocation request.");
+ }
+ self.new_channel = Some((channel_id, nonce));
+ Ok(Some(channel_id))
+ }
+ // No Header::TransportError for broadcast.
+ _ => {
+ log::debug!(
+ "Broadcast channel: ignoring packet with control byte {}.",
+ packet[0]
+ );
+ Err(Error::malformed_data())
+ }
+ }
+ }
+
+ fn handle_v1(&mut self, packet: &[u8]) -> PacketInResult {
+ match Header::<Device>::parse(packet) {
+ Ok((
+ Header::CodecV1Request {
+ is_continuation: false,
+ },
+ _,
+ )) => {
+ let res = self
+ .enqueue(MuxOutgoing::CodecV1Response)
+ .map(|_| PacketInResult::accept(false));
+ return PacketInResult::from_result(res);
+ }
+ Ok((Header::CodecV1Request { .. }, _)) => {
+ log::debug!("Ignoring v1 continuation.");
+ }
+ _ => {
+ log::error!("Malformed v1 packet.");
+ }
+ };
+ PacketInResult::ignore(Error::malformed_data())
+ }
+
+ #[cfg(test)]
+ pub(crate) fn set_next_channel_id(&mut self, channel_id: u16) {
+ assert!(channel_id_valid(channel_id));
+ self.next_channel_id = channel_id;
+ }
+}
+
+impl<C, B> ChannelIO for Mux<C, B>
+where
+ C: CredentialVerifier,
+ B: Backend,
+{
+ fn packet_in(&mut self, packet_buffer: &[u8], _receive_buffer: &mut [u8]) -> PacketInResult {
+ let Ok((cb, channel_id, _rest)) = parse_cb_channel(packet_buffer) else {
+ // parse_cb_channel already writes to log
+ return PacketInResult::ignore(Error::malformed_data());
+ };
+ if cb.is_codec_v1() {
+ return self.handle_v1(packet_buffer);
+ }
+ if !channel_id_valid(channel_id) {
+ log::warn!("Invalid channel id {}.", channel_id);
+ return PacketInResult::ignore(Error::malformed_data());
+ }
+ if channel_id != BROADCAST_CHANNEL_ID {
+ return PacketInResult::route(channel_id);
+ }
+ PacketInResult::from_result(self.handle_broadcast(packet_buffer).map(|r| {
+ r.map_or_else(
+ || PacketInResult::accept(false),
+ PacketInResult::channel_allocation,
+ )
+ }))
+ }
+
+ fn packet_in_ready(&self) -> bool {
+ !self.outgoing.is_full()
+ }
+
+ fn packet_out(&mut self, packet_buffer: &mut [u8], _send_buffer: &[u8]) -> Result<(), Error> {
+ let op = self.outgoing.pop_front().ok_or_else(Error::not_ready)?;
+ let sb = SyncBits::new();
+ match op {
+ MuxOutgoing::Error(channel_id, transport_error) => Fragmenter::<Device>::single(
+ Header::new_error(channel_id)?,
+ sb,
+ &[u8::from(transport_error)],
+ packet_buffer,
+ ),
+ MuxOutgoing::Pong(nonce) => Fragmenter::<Device>::single(
+ Header::new_pong(),
+ sb,
+ nonce.as_slice(),
+ packet_buffer,
+ ),
+ MuxOutgoing::CodecV1Response => {
+ let (response, zeros) = packet_buffer
+ .split_at_mut_checked(CODEC_V1_RESPONSE.len())
+ .ok_or_else(Error::insufficient_buffer)?;
+ response.copy_from_slice(CODEC_V1_RESPONSE);
+ zeros.fill(0);
+ Ok(())
+ }
+ }
+ }
+
+ fn packet_out_ready(&self) -> bool {
+ !self.outgoing.is_empty()
+ }
+
+ fn message_in(&mut self, _plaintext_len: usize, _send_buffer: &mut [u8]) -> Result<(), Error> {
+ Ok(())
+ }
+
+ fn message_in_ready(&self) -> bool {
+ false
+ }
+
+ 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 {
+ false
+ }
+
+ fn message_retransmit(&mut self) -> Result<(), Error> {
+ Ok(())
+ }
+}
+
+#[derive(Copy, Clone)]
+enum HandshakeState {
+ SendingChannelResponse,
+ StaticKeyRequired { try_to_unlock: bool },
+ SendingInitiationResponse,
+ SendingCompletionResponse { pairing_state: PairingState },
+ SendingDeviceLocked,
+ Failed,
+}
+
+/// Channel in the handshake phase. Perform [`ChannelIO`] with empty messages until
+/// [`ChannelOpen::handshake_done`] is true, then call [`ChannelOpen::complete`].
+/// Please note that this object also handles sending ChannelAllocationResponse
+/// which is a broadcast message, which are normally handled by [`Mux`].
+pub struct ChannelOpen<C: CredentialVerifier, B: Backend> {
+ channel: Channel<Device, B>,
+ state: HandshakeState,
+ noise: NoiseHandshake<Device, B>,
+ internal_buffer: heapless::Vec<u8, INTERNAL_BUFFER_LEN>,
+ cred_verif: C,
+}
+
+impl<C: CredentialVerifier, B: Backend> ChannelOpen<C, B> {
+ fn new(channel_id: u16, nonce: Nonce, cred_verif: C) -> Result<Self, Error> {
+ let mut internal_buffer = heapless::Vec::new();
+ internal_buffer
+ .extend_from_slice(nonce.as_slice())
+ .map_err(|_| Error::insufficient_buffer())?;
+ internal_buffer
+ .extend_from_slice(&channel_id.to_be_bytes())
+ .map_err(|_| Error::insufficient_buffer())?;
+ internal_buffer
+ .extend_from_slice(cred_verif.device_properties())
+ .map_err(|_| Error::insufficient_buffer())?;
+
+ // Sending `channel_allocation_response` on broadcast channel.
+ let mut channel = Channel::new(channel_id);
+ channel.raw_in(
+ Header::new_channel_response(&internal_buffer)?,
+ &internal_buffer,
+ )?;
+ Ok(Self {
+ channel,
+ state: HandshakeState::SendingChannelResponse,
+ noise: NoiseHandshake::prepare_responder(cred_verif.device_properties()),
+ internal_buffer,
+ cred_verif,
+ })
+ }
+
+ 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()) {
+ (HandshakeState::SendingChannelResponse, Some(HandshakeMessage::InitiationRequest)) => {
+ let try_to_unlock = self.noise.read_initiation_request(&self.internal_buffer)?;
+ self.state = HandshakeState::StaticKeyRequired { try_to_unlock };
+ }
+ (
+ HandshakeState::SendingInitiationResponse,
+ Some(HandshakeMessage::CompletionRequest),
+ ) => {
+ let pairing_state = self.send_completion_response()?;
+ self.state = HandshakeState::SendingCompletionResponse { pairing_state };
+ }
+ _ => {
+ log::error!("[{}] Unexpected handshake state.", self.channel_id());
+ return Err(Error::unexpected_input());
+ }
+ }
+ Ok(())
+ }
+
+ fn send_initiation_response(
+ &mut self,
+ static_privkey: &[u8; PRIVKEY_LEN],
+ ) -> Result<(), Error> {
+ prepare_zeroed(&mut self.internal_buffer);
+ let msg = self
+ .noise
+ .write_initiation_response(static_privkey, &mut self.internal_buffer)?;
+ let header = Header::new_handshake(
+ self.channel.channel_id,
+ HandshakeMessage::InitiationResponse,
+ msg,
+ )?;
+ self.channel.raw_in(header, msg)?;
+ let len = msg.len();
+ self.internal_buffer.truncate(len);
+ Ok(())
+ }
+
+ fn send_completion_response(&mut self) -> Result<PairingState, Error> {
+ let payload = self.internal_buffer.clone();
+ prepare_zeroed(&mut self.internal_buffer);
+ let (nc, ps, msg) = self.noise.write_completion_response(
+ &payload,
+ &self.cred_verif,
+ &mut self.internal_buffer,
+ )?;
+ self.channel.noise = Some(nc);
+ let header = Header::new_handshake(
+ self.channel.channel_id,
+ HandshakeMessage::CompletionResponse,
+ msg,
+ )?;
+ self.channel.raw_in(header, msg)?;
+ let len = msg.len();
+ self.internal_buffer.truncate(len);
+ Ok(ps)
+ }
+
+ pub fn pairing_state(&self) -> Option<PairingState> {
+ match self.state {
+ HandshakeState::SendingCompletionResponse { pairing_state } => Some(pairing_state),
+ _ => None,
+ }
+ }
+
+ /// True if handshake finished and [`ChannelOpen::complete()`] can be called.
+ pub fn handshake_done(&self) -> bool {
+ // Done only after peer acknowledges completion response.
+ matches!(self.state, HandshakeState::SendingCompletionResponse { .. })
+ && matches!(self.channel.state, ChannelState::Idle)
+ }
+
+ /// 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(_))
+ }
+
+ /// True if the handshake is waiting for device static key to be supplied using
+ /// [`ChannelOpen::set_static_key()`]. If the key is not available, handshake should be
+ /// aborted using [`ChannelOpen::send_device_locked()`].
+ pub fn static_key_required(&self) -> bool {
+ matches!(self.state, HandshakeState::StaticKeyRequired { .. })
+ }
+
+ /// Finish the handshake and transition into the pairing/credential/appdata phase.
+ ///
+ /// Please note that the returned [`Channel`] is in the [Pairing phase] (state `TP0`). The peers
+ /// must exchange protobuf messages as described in the [Pairing phase] and [Credential phase]
+ /// section of THP spec. Only after `ThpMessageType_ThpEndResponse` is sent from the device to
+ /// the host can regular application messages be transported. The library does not track whether
+ /// the channel is in the pairing, credential, or application transport phase and it is the
+ /// responsibility of the application to separate these message contexts.
+ ///
+ /// [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 fn complete(self) -> Result<Channel<Device, B>, Error> {
+ if self.channel.noise.is_none() {
+ return Err(Error::unexpected_input());
+ }
+ log::debug!("Handshake complete.");
+ Ok(match self.state {
+ HandshakeState::SendingCompletionResponse { .. } => self.channel,
+ _ => return Err(Error::unexpected_input()),
+ })
+ }
+
+ pub fn channel_id(&self) -> u16 {
+ self.channel.channel_id
+ }
+
+ /// Notify host that handshake cannot proceed because device static key is not available.
+ pub fn send_device_locked(&mut self) -> Result<(), Error> {
+ 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.state = HandshakeState::SendingDeviceLocked;
+ Ok(())
+ }
+
+ /// Set static private key to be used for handshake. The key is not stored and can be
+ /// disposed of after returning from this function.
+ pub fn set_static_key(&mut self, static_privkey: &[u8; PRIVKEY_LEN]) -> Result<(), Error> {
+ if !self.static_key_required() {
+ return Err(Error::not_ready());
+ }
+ self.send_initiation_response(static_privkey)?;
+ self.state = HandshakeState::SendingInitiationResponse;
+ Ok(())
+ }
+}
+
+impl<C, B> ChannelIO for ChannelOpen<C, B>
+where
+ C: CredentialVerifier,
+ B: Backend,
+{
+ fn packet_in(&mut self, packet_buffer: &[u8], _receive_buffer: &mut [u8]) -> PacketInResult {
+ let res = self
+ .channel
+ .packet_in(packet_buffer, &mut self.internal_buffer);
+ if let PacketInResult::EnlargeBuffer { buffer_size, .. } = res {
+ log::error!(
+ "[{}] Payload length {} exceeds handshake limit.",
+ self.channel_id(),
+ buffer_size
+ );
+ // Possibly damaged length field, ignore continuations.
+ self.channel.state = ChannelState::Idle;
+ 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() {
+ let handled = self.incoming_internal();
+ if let Err(e) = handled {
+ if e == Error::InvalidChecksum {
+ return PacketInResult::ignore(e);
+ } else {
+ self.state = HandshakeState::Failed;
+ return PacketInResult::fail(e);
+ }
+ }
+ if let HandshakeState::StaticKeyRequired { try_to_unlock } = self.state {
+ return PacketInResult::HandshakeKeyRequired { try_to_unlock };
+ }
+ }
+ res
+ }
+
+ fn packet_out(&mut self, packet_buffer: &mut [u8], _send_buffer: &[u8]) -> Result<(), Error> {
+ self.channel
+ .packet_out(packet_buffer, &self.internal_buffer)?;
+ // Do not wait for ack - `channel_allocation_response` is sent over broadcast channel.
+ if matches!(self.state, HandshakeState::SendingChannelResponse)
+ && matches!(self.channel.state, ChannelState::Idle)
+ {
+ prepare_zeroed(&mut self.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> {
+ 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()
+ }
+}
+
+fn random_channel_id<B: Backend>() -> u16 {
+ let mut bytes = [0u8, 0u8];
+ for _i in 0..16 {
+ B::random_bytes(&mut bytes);
+ let channel_id = u16::from_be_bytes(bytes);
+ if channel_id_valid(channel_id) && channel_id != BROADCAST_CHANNEL_ID {
+ return channel_id;
+ }
+ }
+ panic!("Cannot generate random channel id.");
+}
diff --git a/rust/trezor-thp/src/channel/host.rs b/rust/trezor-thp/src/channel/host.rs
index 9ff716dc..b945e091 100644
--- a/rust/trezor-thp/src/channel/host.rs
+++ b/rust/trezor-thp/src/channel/host.rs
@@ -2,12 +2,13 @@ use heapless;
use crate::{
Backend, Channel, ChannelIO, Error, Host,
+ alternating_bit::SyncBits,
channel::{ChannelState, Nonce, PacketInResult, PairingState, noise::NoiseHandshake},
credential::CredentialStore,
fragment::{Fragmenter, Reassembler},
header::{
- BROADCAST_CHANNEL_ID, HandshakeMessage, Header, SyncBits, channel_id_valid,
- parse_cb_channel, parse_u16,
+ BROADCAST_CHANNEL_ID, HandshakeMessage, Header, channel_id_valid, parse_cb_channel,
+ parse_u16,
},
util::prepare_zeroed,
};
@@ -20,7 +21,6 @@ use core::marker::PhantomData;
// - 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
enum AllocationState {
None,
@@ -43,6 +43,16 @@ enum AllocationState {
},
}
+#[derive(PartialEq, Eq)]
+enum PingState {
+ /// Ping not requested, pong not expected.
+ None,
+ /// Application requested ping to be sent.
+ SendingPing,
+ /// Ping was sent, awaiting pong.
+ AwaitingPong(Nonce),
+}
+
/// Handles broadcast channel messages, notably channel allocation requests.
/// Because host often only needs a single channel, you can throw away the Mux
/// after allocating one, if you don't need the keep-alive functionality.
@@ -50,7 +60,7 @@ pub struct Mux<C, B> {
cred_store: C,
internal_buffer: heapless::Vec<u8, MAX_DEVICE_PROPERTIES_LEN>,
channel_allocation: AllocationState,
- ping: Option<(bool, Nonce)>,
+ ping: PingState,
_phantom: PhantomData<B>,
}
@@ -66,17 +76,17 @@ where
cred_store,
internal_buffer,
channel_allocation: AllocationState::None,
- ping: None,
+ ping: PingState::None,
_phantom: PhantomData,
}
}
/// Enqueue a keep-alive message.
pub fn ping(&mut self) {
- if self.ping.is_some() {
+ if !matches!(self.ping, PingState::None) {
log::warn!("Dropping previous ping attempt.");
}
- self.ping = Some((false, Nonce::random::<B>()));
+ self.ping = PingState::SendingPing;
}
/// Enqueue channel allocation request.
@@ -124,11 +134,11 @@ where
Header::Pong => {
let (_header, payload) = Reassembler::<Host>::single_inplace(packet)?;
let (nonce, _rest) = Nonce::parse(payload)?;
- if Some((true, nonce)) != self.ping {
+ if PingState::AwaitingPong(nonce) != self.ping {
log::warn!("Ignoring PONG with invalid nonce.");
return Err(Error::malformed_data());
}
- self.ping = None;
+ self.ping = PingState::None;
Ok(PacketInResult::pong())
}
Header::ChannelAllocationResponse { .. } => {
@@ -216,7 +226,7 @@ where
B: Backend,
{
fn packet_in(&mut self, packet_buffer: &[u8], _receive_buffer: &mut [u8]) -> PacketInResult {
- let Ok((_cb, channel_id, _rest)) = parse_cb_channel(packet_buffer) else {
+ let Ok((cb, channel_id, _rest)) = parse_cb_channel(packet_buffer) else {
// parse_cb_channel already writes to log
return PacketInResult::ignore(Error::malformed_data());
};
@@ -224,7 +234,7 @@ where
log::warn!("Invalid channel id {}.", channel_id);
return PacketInResult::ignore(Error::malformed_data());
}
- if channel_id != BROADCAST_CHANNEL_ID {
+ if channel_id != BROADCAST_CHANNEL_ID && !cb.is_codec_v1() {
return PacketInResult::route(channel_id);
}
PacketInResult::from_result(self.handle_broadcast(packet_buffer))
@@ -247,7 +257,7 @@ where
try_to_unlock,
nonce,
};
- } else if let Some((false, _)) = self.ping {
+ } else if let PingState::SendingPing = self.ping {
let nonce = Nonce::random::<B>();
Fragmenter::<Host>::single(
Header::new_ping(),
@@ -255,7 +265,7 @@ where
nonce.as_slice(),
packet_buffer,
)?;
- self.ping = Some((true, nonce));
+ self.ping = PingState::AwaitingPong(nonce);
} else {
return Err(Error::not_ready());
}
@@ -263,7 +273,7 @@ where
}
fn packet_out_ready(&self) -> bool {
- let send_ping = matches!(self.ping, Some((false, _)));
+ let send_ping = matches!(self.ping, PingState::SendingPing);
let send_channel_allocation = matches!(
self.channel_allocation,
AllocationState::SendingRequest { .. }
@@ -298,11 +308,11 @@ where
#[derive(Copy, Clone)]
enum HandshakeState {
/// `HH1`.
- SentInitiationRequest,
+ SendingInitiationRequest,
/// `HH2`.
- SentCompletionRequest,
+ SendingCompletionRequest,
/// `HP0`.
- Finished(PairingState),
+ Finished { pairing_state: PairingState },
/// Handshake cannot be finished.
Failed,
}
@@ -313,9 +323,7 @@ enum HandshakeState {
/// - perform [`ChannelIO`] with empty messages until [`PacketInResult::ChannelAllocation`] is returned
/// - calling [`Mux::channel_alloc`] to obtain [`ChannelOpen`]
/// - 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`]
+/// - call [`ChannelOpen::complete`] to obtain [`Channel`]
pub struct ChannelOpen<C: CredentialStore, B: Backend> {
channel: Channel<Host, B>,
state: HandshakeState,
@@ -337,7 +345,7 @@ impl<C: CredentialStore, B: Backend> ChannelOpen<C, B> {
let mut internal_buffer = heapless::Vec::new();
prepare_zeroed(&mut internal_buffer);
- let (hss, msg) = NoiseHandshake::initiation_request(
+ let (hss, msg) = NoiseHandshake::write_initiation_request(
&device_properties,
try_to_unlock,
&mut internal_buffer,
@@ -349,7 +357,7 @@ impl<C: CredentialStore, B: Backend> ChannelOpen<C, B> {
internal_buffer.truncate(len);
let res = Self {
channel,
- state: HandshakeState::SentInitiationRequest,
+ state: HandshakeState::SendingInitiationRequest,
noise: hss,
internal_buffer,
device_properties,
@@ -363,13 +371,19 @@ impl<C: CredentialStore, B: Backend> ChannelOpen<C, B> {
self.internal_buffer.truncate(len);
match (self.state, header.handshake_phase()) {
- (HandshakeState::SentInitiationRequest, Some(HandshakeMessage::InitiationResponse)) => {
+ (
+ HandshakeState::SendingInitiationRequest,
+ Some(HandshakeMessage::InitiationResponse),
+ ) => {
self.continue_handshake()?;
- self.state = HandshakeState::SentCompletionRequest;
+ self.state = HandshakeState::SendingCompletionRequest;
}
- (HandshakeState::SentCompletionRequest, Some(HandshakeMessage::CompletionResponse)) => {
- let device_state = self.finish_handshake()?;
- self.state = HandshakeState::Finished(device_state);
+ (
+ HandshakeState::SendingCompletionRequest,
+ Some(HandshakeMessage::CompletionResponse),
+ ) => {
+ let pairing_state = self.finish_handshake()?;
+ self.state = HandshakeState::Finished { pairing_state };
}
_ => {
log::error!("Unexpected handshake state.");
@@ -385,7 +399,7 @@ impl<C: CredentialStore, B: Backend> ChannelOpen<C, B> {
self.internal_buffer
.resize(self.internal_buffer.capacity(), 0u8)
.unwrap();
- let (nc, msg) = self.noise.completion_request(
+ let (nc, msg) = self.noise.write_completion_request(
&mut self.cred_store,
&mut self.internal_buffer,
payload_len,
@@ -413,29 +427,43 @@ impl<C: CredentialStore, B: Backend> ChannelOpen<C, B> {
self.device_properties.as_slice()
}
+ /// Returns pairing state if handshake finished, or None otherwise.
+ pub fn pairing_state(&self) -> Option<PairingState> {
+ match self.state {
+ HandshakeState::Finished { pairing_state } => Some(pairing_state),
+ _ => None,
+ }
+ }
+
/// True if handshake finished and [`ChannelOpen::complete()`] can be called.
pub fn handshake_done(&self) -> bool {
- matches!(self.state, HandshakeState::Finished(_))
+ self.pairing_state().is_some()
}
/// True if the handshake failed and the object should be discarded.
pub fn handshake_failed(&self) -> bool {
matches!(self.state, HandshakeState::Failed)
- }
-
- /// Transition into the pairing phase.
- pub fn complete(self) -> Result<ChannelPairing<B>, Error> {
+ || matches!(self.channel.state, ChannelState::Failed(_))
+ }
+
+ /// Finish the handshake.
+ ///
+ /// Please note that the returned [`Channel`] is in the [Pairing phase] (state `HP0`). The peers
+ /// must exchange protobuf messages as described in the [Pairing phase] and [Credential phase]
+ /// section of THP spec. Only after `ThpMessageType_ThpEndResponse` is sent from the device to
+ /// the host can regular application messages be transported. The library does not track whether
+ /// the channel is in the pairing, credential, or application transport phase and it is the
+ /// responsibility of the application to separate these message contexts.
+ ///
+ /// [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 fn complete(self) -> Result<Channel<Host, B>, Error> {
if self.channel.noise.is_none() {
return Err(Error::unexpected_input());
}
log::debug!("Handshake complete.");
Ok(match self.state {
- HandshakeState::Finished(ps) => ChannelPairing {
- channel: self.channel,
- device_properties: self.device_properties,
- pairing_state: ps,
- is_finished: false,
- },
+ HandshakeState::Finished { .. } => self.channel,
_ => return Err(Error::unexpected_input()),
})
}
@@ -462,7 +490,7 @@ where
);
// Possibly damaged length field, ignore continuations.
self.channel.state = ChannelState::Idle;
- return PacketInResult::ignore(Error::MalformedData);
+ return PacketInResult::ignore(Error::malformed_data());
}
if res.got_ack() {
prepare_zeroed(&mut self.internal_buffer);
@@ -470,7 +498,9 @@ where
if res.got_message() {
let handled = self.incoming_internal();
if let Err(e) = handled {
- if e != Error::InvalidChecksum {
+ if e == Error::InvalidChecksum {
+ return PacketInResult::ignore(e);
+ } else {
self.state = HandshakeState::Failed;
return PacketInResult::fail(e);
}
@@ -512,88 +542,3 @@ where
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::unexpected_input());
- }
- 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]) -> PacketInResult {
- 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::malformed_data());
- }
- 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
index 3ddee90c..a4cf8008 100644
--- a/rust/trezor-thp/src/channel/mod.rs
+++ b/rust/trezor-thp/src/channel/mod.rs
@@ -1,7 +1,10 @@
#[cfg(feature = "use_std")]
pub mod buffered;
+pub mod device;
pub mod host;
mod noise;
+#[cfg(test)]
+mod test;
use crate::{
Error, Role,
@@ -11,8 +14,10 @@ use crate::{
header::{BROADCAST_CHANNEL_ID, Header, NONCE_LEN, parse_cb_channel, parse_u16},
};
-pub use noise::Backend;
-use noise::{HANDSHAKE_HASH_LEN, NoiseCiphers, TAG_LEN};
+use noise::NoiseCiphers;
+pub use noise::{
+ Backend, Cipher, DH, HANDSHAKE_HASH_LEN, Hash, PRIVKEY_LEN, PUBKEY_LEN, TAG_LEN, U8Array,
+};
const APP_HEADER_LEN: usize = 3; // session id (1) + message type (2)
@@ -264,6 +269,13 @@ impl<R: Role, B: Backend> Channel<R, B> {
let is_done = r.is_done();
let payload_len = r.header().payload_len();
let enlarge = (usize::from(payload_len) > receive_buffer.len()).then_some(payload_len);
+ if enlarge.is_some() {
+ log::debug!(
+ "Message is larger ({}) than receive buffer ({}), requesting reallocation.",
+ payload_len,
+ receive_buffer.len()
+ );
+ }
self.state = ChannelState::Receiving(r);
Ok((is_done, enlarge))
}
@@ -313,9 +325,17 @@ pub enum PacketInResult {
channel_id: u16,
},
/// Channel allocation request/response was received. Event loop should call
- /// [`Mux::channel_alloc`] to create new channel object. Only [`device::Mux`] and[`host::Mux`]
+ /// [`Mux::channel_alloc`] to create new channel object. Only [`device::Mux`] and [`host::Mux`]
/// return this variant. There is no queue, do it before processing the next packet.
+ ///
+ /// Please note that the library does not keep track of allocated ids, that is left to
+ /// the application. By creating a lot of new channels an attacker can obtain an id that
+ /// was issued earlier. If there is existing channel with such id it should be destroyed.
+ /// (This is only relevant on the device side.)
ChannelAllocation { channel_id: u16 },
+ /// Call [`device::ChannelOpen::set_static_key`] or [`device::ChannelOpen::send_device_locked`].
+ /// Only [`device::ChannelOpen`] returns this variant.
+ HandshakeKeyRequired { try_to_unlock: bool },
}
impl PacketInResult {
@@ -544,7 +564,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(_e) = self.state {
- return PacketInResult::fail(Error::UnexpectedInput);
+ return PacketInResult::fail(Error::unexpected_input());
}
let res = PacketInResult::from_result(self.handle_packet(packet_buffer, receive_buffer));
if let PacketInResult::Failed { .. } = res {
diff --git a/rust/trezor-thp/src/channel/noise.rs b/rust/trezor-thp/src/channel/noise.rs
index b015b751..09fd1867 100644
--- a/rust/trezor-thp/src/channel/noise.rs
+++ b/rust/trezor-thp/src/channel/noise.rs
@@ -1,12 +1,27 @@
-use trezor_noise_protocol::{
- Cipher, CipherState, DH, HandshakeState, Hash, U8Array, patterns::noise_xx,
-};
+//! Implementation of the Noise XX patern. See https://noiseprotocol.org/
+//! Host is the initiator, device is the responder.
+//!
+//! -> e # initiation request
+//! <- e, ee, s, es # initiation response
+//! -> s, se # completion request
+//! <- (pairing_state) # completion response - technically not part of the handshake as secure channel is established at this point
+
+use trezor_noise_protocol::{CipherState, HandshakeState, patterns::noise_xx};
-use crate::{Error, Host, Role, credential::CredentialStore, util::prepare_zeroed};
+pub use trezor_noise_protocol::{Cipher, DH, Hash, U8Array};
+
+use crate::{
+ Device, Error, Host, Role,
+ channel::PairingState,
+ credential::{CredentialStore, CredentialVerifier},
+ util::prepare_zeroed,
+};
use core::marker::PhantomData;
pub const HANDSHAKE_HASH_LEN: usize = 32;
+pub const PRIVKEY_LEN: usize = 32;
+pub const PUBKEY_LEN: usize = 32;
pub const TAG_LEN: usize = 16;
pub const MAX_KEY_AND_CREDENTIAL_LEN: usize = 128;
@@ -15,8 +30,11 @@ pub const MAX_KEY_AND_CREDENTIAL_LEN: usize = 128;
/// 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 {
+ /// Implementation of AES-256-GCM.
type Cipher: Cipher;
+ /// Implementation of X25519.
type DH: DH;
+ /// Implementation of SHA-256.
type Hash: Hash;
/// Fill entire destination with random bytes.
@@ -60,7 +78,7 @@ impl<B: Backend> NoiseCiphers<B> {
}
impl<B: Backend> NoiseHandshake<Host, B> {
- pub fn start_pairing<'a>(
+ pub fn write_initiation_request<'a>(
device_properties: &[u8],
try_to_unlock: bool,
dest: &'a mut [u8],
@@ -85,7 +103,7 @@ impl<B: Backend> NoiseHandshake<Host, B> {
Ok((new, dest))
}
- pub fn complete_pairing<'a>(
+ pub fn write_completion_request<'a>(
&mut self,
cred_store: &mut impl CredentialStore,
buffer: &'a mut [u8],
@@ -151,3 +169,121 @@ impl<B: Backend> NoiseHandshake<Host, B> {
Ok((new_key, buf))
}
}
+
+impl<B: Backend> NoiseHandshake<Device, B> {
+ pub fn prepare_responder(device_properties: &[u8]) -> Self {
+ let hss = HandshakeState::new(
+ noise_xx(),
+ /*is_initiator=*/ false,
+ /*prologue=*/ device_properties,
+ /*s=*/ None,
+ /*e=*/ None,
+ /*re=*/ None,
+ /*rs=*/ None,
+ );
+ NoiseHandshake {
+ hss,
+ _phantom: PhantomData,
+ }
+ }
+
+ pub fn read_initiation_request(&mut self, incoming: &[u8]) -> Result<bool, Error> {
+ const PAYLOAD_LEN: usize = 1;
+ if incoming.len() != self.hss.get_next_message_overhead() + PAYLOAD_LEN {
+ log::error!("Unexpected message length during handshake.");
+ return Err(Error::malformed_data());
+ }
+ let mut payload = [0u8; PAYLOAD_LEN];
+ self.hss.read_message(incoming, &mut payload)?;
+ let try_to_unlock = payload[0] & 0x01 == 0x01;
+ Ok(try_to_unlock)
+ }
+
+ pub fn write_initiation_response<'a>(
+ &mut self,
+ static_privkey: &[u8; PRIVKEY_LEN],
+ dest: &'a mut [u8],
+ ) -> Result<&'a [u8], Error> {
+ let mk = Self::mask_key(static_privkey);
+ self.hss.set_s(mk.static_privkey);
+ self.hss.set_s_mask(mk.mask);
+ self.hss.set_e(mk.ephemeral_privkey);
+ let len = self.hss.get_next_message_overhead();
+ let dest = dest.get_mut(..len).ok_or_else(Error::insufficient_buffer)?;
+ self.hss.write_message(/*payload*/ &[], dest)?; // no outgoing payload
+ Ok(dest)
+ }
+
+ pub fn write_completion_response<'a>(
+ &mut self,
+ incoming: &[u8],
+ cred_verifier: &impl CredentialVerifier,
+ dest: &'a mut [u8],
+ ) -> Result<(NoiseCiphers<B>, PairingState, &'a [u8]), Error> {
+ let overhead_len = self.hss.get_next_message_overhead();
+ if incoming.len() < overhead_len {
+ log::error!("Unexpected message length during handshake.");
+ return Err(Error::malformed_data());
+ }
+ let cred_len = incoming.len().saturating_sub(overhead_len);
+ let mut cred = heapless::Vec::<u8, MAX_KEY_AND_CREDENTIAL_LEN>::new();
+ cred.resize(cred_len, 0u8)
+ .map_err(|_| Error::insufficient_buffer())?;
+ self.hss.read_message(incoming, &mut cred)?;
+ if !self.hss.completed() {
+ log::error!("Handshake not completed.");
+ return Err(Error::crypto_error());
+ }
+
+ let remote_static_pubkey = self.hss.get_rs().ok_or_else(Error::crypto_error)?;
+ let (decrypt, encrypt) = self.hss.get_ciphers();
+ let mut handshake_hash = [0u8; HANDSHAKE_HASH_LEN];
+ handshake_hash.copy_from_slice(self.hss.get_hash());
+ let mut nc = NoiseCiphers {
+ encrypt,
+ decrypt,
+ handshake_hash,
+ };
+ let pairing_state = cred_verifier.verify(remote_static_pubkey.as_slice(), &cred);
+ let payload = &[pairing_state as u8];
+ let plaintext_len = payload.len();
+ let dest = dest
+ .get_mut(..plaintext_len + TAG_LEN)
+ .ok_or_else(Error::insufficient_buffer)?;
+ dest[0..plaintext_len].copy_from_slice(payload);
+ nc.encrypt(dest, plaintext_len)?;
+ Ok((nc, pairing_state, dest))
+ }
+
+ fn mask_key(static_privkey: &[u8; PRIVKEY_LEN]) -> MaskKeyResult<B>
+ where
+ DHPrivKey<B>: U8Array,
+ {
+ let static_privkey = <DHPrivKey<B> as U8Array>::from_slice(static_privkey);
+ let static_pubkey = <B::DH as DH>::pubkey(&static_privkey);
+ let ephemeral_privkey = <B::DH as DH>::genkey();
+ let ephemeral_pubkey = <B::DH as DH>::pubkey(&ephemeral_privkey);
+ let mask = hash_of_two::<B>(static_pubkey.as_slice(), ephemeral_pubkey.as_slice());
+ let mask = <DHPrivKey<B> as U8Array>::from_slice(&mask);
+ MaskKeyResult {
+ static_privkey,
+ ephemeral_privkey,
+ mask,
+ }
+ }
+}
+
+struct MaskKeyResult<B: Backend> {
+ static_privkey: DHPrivKey<B>,
+ ephemeral_privkey: DHPrivKey<B>,
+ mask: DHPrivKey<B>,
+}
+
+fn hash_of_two<B: Backend>(in1: &[u8], in2: &[u8]) -> [u8; PRIVKEY_LEN] {
+ let mut res = [0u8; PRIVKEY_LEN];
+ let mut h = <B::Hash as Default>::default();
+ h.input(in1);
+ h.input(in2);
+ res.copy_from_slice(h.result().as_slice());
+ res
+}
diff --git a/rust/trezor-thp/src/channel/test.rs b/rust/trezor-thp/src/channel/test.rs
new file mode 100644
index 00000000..44281349
--- /dev/null
+++ b/rust/trezor-thp/src/channel/test.rs
@@ -0,0 +1,911 @@
+use std::collections::{HashSet, VecDeque};
+use std::ops::{Deref, DerefMut};
+use std::sync::Once;
+
+use super::super::crc32;
+use super::buffered::{Buffered, ChannelExt};
+use super::*;
+use crate::{
+ Device, Host,
+ credential::{CredentialVerifier, NullCredentialStore},
+ header::{BROADCAST_CHANNEL_ID, MAX_CHANNEL_ID, MIN_CHANNEL_ID},
+};
+
+use test_case::{test_case, test_matrix};
+
+struct RustCrypto;
+
+impl Backend for RustCrypto {
+ type DH = trezor_noise_rust_crypto::X25519;
+ type Cipher = trezor_noise_rust_crypto::Aes256Gcm;
+ type Hash = trezor_noise_rust_crypto::Sha256;
+
+ fn random_bytes(dest: &mut [u8]) {
+ getrandom::fill(dest).unwrap();
+ }
+}
+
+type Packet = Vec<u8>;
+type AppMsg = (u8, u16, Vec<u8>);
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+enum Direction {
+ HostToDevice,
+ DeviceToHost,
+}
+
+use Direction::*;
+
+type TakeTurnsResult = Option<(Direction, AppMsg)>;
+
+const DEFAULT_PACKET_LEN: usize = 64;
+static SETUP: Once = Once::new();
+const DEVICE_KEY: &[u8; PRIVKEY_LEN] = &[0u8; PRIVKEY_LEN];
+
+fn setup() {
+ SETUP.call_once(|| {
+ env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "info"));
+ })
+}
+
+/// Uses all-zero privkey, verification always fails.
+#[derive(Clone)]
+pub struct TestCredentialVerifier;
+
+impl CredentialVerifier for TestCredentialVerifier {
+ fn verify(&self, _remote_static_pubkey: &[u8], _credential: &[u8]) -> PairingState {
+ PairingState::Unpaired
+ }
+
+ fn device_properties(&self) -> &[u8] {
+ // internal_model: Some("T2W1"), model_variant: Some(0), protocol_version_major: Some(2),
+ // protocol_version_minor: Some(0), pairing_methods: [CodeEntry, SkipPairing]
+ b"\x0a\x04\x54\x32\x57\x31\x10\x00\x18\x02\x20\x00\x28\x02\x28\x01"
+ }
+}
+
+pub struct WithKey<C: CredentialVerifier, B: Backend> {
+ channel: super::device::ChannelOpen<C, B>,
+ static_key: [u8; PRIVKEY_LEN],
+}
+
+impl<C: CredentialVerifier, B: Backend> WithKey<C, B> {
+ fn unwrap(self) -> super::device::ChannelOpen<C, B> {
+ self.channel
+ }
+}
+
+impl<C: CredentialVerifier, B: Backend> ChannelIO for WithKey<C, B> {
+ fn packet_in(&mut self, packet_buffer: &[u8], receive_buffer: &mut [u8]) -> PacketInResult {
+ let pir = self.channel.packet_in(packet_buffer, receive_buffer);
+ if matches!(pir, PacketInResult::HandshakeKeyRequired { .. }) {
+ self.channel.set_static_key(&self.static_key).unwrap();
+ return PacketInResult::Accepted {
+ ack_received: false,
+ message_ready: false,
+ pong: false,
+ };
+ }
+ pir
+ }
+
+ fn packet_in_ready(&self) -> bool {
+ self.channel.packet_in_ready()
+ }
+
+ fn packet_out(&mut self, packet_buffer: &mut [u8], send_buffer: &[u8]) -> Result<()> {
+ 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<()> {
+ 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])> {
+ self.channel.message_out(receive_buffer)
+ }
+
+ fn message_out_ready(&self) -> bool {
+ self.channel.message_out_ready()
+ }
+
+ fn message_retransmit(&mut self) -> Result<()> {
+ self.channel.message_retransmit()
+ }
+}
+
+impl<C: CredentialVerifier, B: Backend> Deref for WithKey<C, B> {
+ type Target = super::device::ChannelOpen<C, B>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.channel
+ }
+}
+
+impl<C: CredentialVerifier, B: Backend> DerefMut for WithKey<C, B> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.channel
+ }
+}
+
+pub trait WithKeyExt<C: CredentialVerifier, B: Backend>: Sized {
+ fn with_key(self, key: &[u8; PRIVKEY_LEN]) -> WithKey<C, B>;
+}
+
+impl<C: CredentialVerifier, B: Backend> WithKeyExt<C, B> for super::device::ChannelOpen<C, B> {
+ fn with_key(self, key: &[u8; PRIVKEY_LEN]) -> WithKey<C, B> {
+ WithKey {
+ channel: self,
+ static_key: *key,
+ }
+ }
+}
+
+fn take_turns_mutate<C1, C2, F>(
+ host: &mut Buffered<C1>,
+ device: &mut Buffered<C2>,
+ mut func: F,
+) -> Result<TakeTurnsResult>
+where
+ C1: ChannelIO,
+ C2: ChannelIO,
+ F: FnMut(Direction, &mut VecDeque<Packet>),
+{
+ let mut msg = TakeTurnsResult::None;
+ let mut wire = VecDeque::<Packet>::new();
+ while host.packet_out_ready() || device.packet_out_ready() {
+ while host.packet_out_ready() {
+ wire.push_back(host.packet_out()?);
+ }
+ if !wire.is_empty() {
+ func(HostToDevice, &mut wire);
+ }
+ for packet in wire.iter() {
+ log::trace!("> {}", hex::encode(packet));
+ device.packet_in(packet).check_failed()?;
+ }
+ if device.message_out_ready() {
+ assert!(msg.is_none());
+ msg = Some((HostToDevice, device.message_out()?));
+ }
+ wire.clear();
+ while device.packet_out_ready() {
+ wire.push_back(device.packet_out()?);
+ }
+ if !wire.is_empty() {
+ func(DeviceToHost, &mut wire);
+ }
+ for packet in wire.iter() {
+ log::trace!("< {}", hex::encode(packet));
+ host.packet_in(packet).check_failed()?;
+ }
+ if host.message_out_ready() {
+ assert!(msg.is_none());
+ msg = Some((DeviceToHost, host.message_out()?));
+ }
+ wire.clear();
+ }
+ Ok(msg)
+}
+
+fn take_turns<C1, C2>(host: &mut Buffered<C1>, device: &mut Buffered<C2>) -> Result<TakeTurnsResult>
+where
+ C1: ChannelIO,
+ C2: ChannelIO,
+{
+ take_turns_mutate(host, device, |_, _| {})
+}
+
+impl Direction {
+ fn send_nocheck<C1, C2>(
+ self,
+ host: &mut Buffered<C1>,
+ device: &mut Buffered<C2>,
+ sid: u8,
+ mty: u16,
+ message: &[u8],
+ ) -> Result<TakeTurnsResult>
+ where
+ C1: ChannelIO,
+ C2: ChannelIO,
+ {
+ if self == HostToDevice {
+ host.message_in(sid, mty, message)?;
+ } else {
+ device.message_in(sid, mty, message)?;
+ }
+ take_turns(host, device)
+ }
+
+ fn send<C1, C2>(
+ self,
+ host: &mut Buffered<C1>,
+ device: &mut Buffered<C2>,
+ sid: u8,
+ mty: u16,
+ message: &[u8],
+ ) -> Result<()>
+ where
+ C1: ChannelIO,
+ C2: ChannelIO,
+ {
+ let msg = self.send_nocheck(host, device, sid, mty, message)?;
+ assert_eq!(msg, Some((self, (sid, mty, message.into()))));
+ Ok(())
+ }
+
+ fn send_noresult<C1: ChannelIO, C2: ChannelIO>(
+ self,
+ host: &mut Buffered<C1>,
+ device: &mut Buffered<C2>,
+ message: &[u8],
+ ) -> Result<()> {
+ let msg = self.send_nocheck(host, device, 10, 100, message)?;
+ assert_eq!(msg, None);
+ Ok(())
+ }
+}
+
+#[test]
+fn test_open() -> Result<()> {
+ setup();
+
+ let (mut hm, mut dm) = create_mux();
+ // channel allocation
+ hm.request_channel(false);
+ take_turns(&mut hm, &mut dm)?;
+ let mut d = dm.channel_alloc()?.with_key(DEVICE_KEY).into_buffered();
+ take_turns(&mut hm, &mut d)?;
+ let mut h = hm.channel_alloc()?.into_buffered();
+
+ // handshake
+ assert!(!h.handshake_done());
+ assert!(!d.handshake_done());
+ take_turns(&mut h, &mut d)?;
+ assert!(h.handshake_done());
+ assert!(d.handshake_done());
+ let mut h = h.map(|h| h.complete())?;
+ let mut d = d.map(|d| d.unwrap().complete())?;
+
+ // pairing
+ 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")?;
+
+ // application messaging
+ HostToDevice.send(&mut h, &mut d, 0, 1234, b"Ping")?;
+ DeviceToHost.send(&mut h, &mut d, 0, 5678, b"Pong")?;
+ HostToDevice.send(&mut h, &mut d, 1, 9999, &[0u8; 999])?;
+ DeviceToHost.send(&mut h, &mut d, 1, 9998, &[9u8; 666])?;
+ Ok(())
+}
+
+#[test]
+fn test_device_locked() -> Result<()> {
+ setup();
+
+ let (mut hm, mut dm) = create_mux();
+ // channel allocation
+ hm.request_channel(false);
+ take_turns(&mut hm, &mut dm)?;
+ let mut d = dm.channel_alloc()?.into_buffered();
+ take_turns(&mut hm, &mut d)?;
+ let mut h = hm.channel_alloc()?.into_buffered();
+
+ // handshake
+ assert!(!h.handshake_done());
+ assert!(!d.handshake_done());
+ take_turns(&mut h, &mut d)?;
+ assert!(d.static_key_required());
+ assert!(!d.handshake_failed());
+
+ // static key not available
+ 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());
+ Ok(())
+}
+
+fn create_mux() -> (
+ Buffered<host::Mux<NullCredentialStore, RustCrypto>>,
+ Buffered<device::Mux<TestCredentialVerifier, RustCrypto>>,
+) {
+ let mut hm = host::Mux::<_, RustCrypto>::new(NullCredentialStore).into_buffered();
+ hm.set_packet_len(DEFAULT_PACKET_LEN);
+ let mut dm = device::Mux::<_, RustCrypto>::new(TestCredentialVerifier).into_buffered();
+ dm.set_packet_len(DEFAULT_PACKET_LEN);
+ (hm, dm)
+}
+
+fn open_channel(
+ packet_len: usize,
+) -> Result<(
+ Buffered<Channel<Host, RustCrypto>>,
+ Buffered<Channel<Device, RustCrypto>>,
+)> {
+ let (mut hm, mut dm) = create_mux();
+ hm.set_packet_len(packet_len);
+ dm.set_packet_len(packet_len);
+ hm.request_channel(false);
+ take_turns(&mut hm, &mut dm)?;
+ let mut d = dm.channel_alloc()?.with_key(DEVICE_KEY).into_buffered();
+ take_turns(&mut d, &mut hm)?;
+ let mut h = hm.channel_alloc()?.into_buffered();
+ take_turns(&mut h, &mut d)?;
+ let h = h.map(|h| h.complete())?;
+ let d = d.map(|d| d.unwrap().complete())?;
+
+ Ok((h, d))
+}
+
+#[test_case(10, 0; "empty messages")]
+#[test_case(30, 1; "short messages")]
+#[test_case(20, 2000; "medium messages")]
+#[test_case(5, 59976; "huge messages")]
+#[test_case(1000, 100; "lots of messages")]
+fn test_messages(count: usize, length: usize) -> Result<()> {
+ setup();
+ let (mut h, mut d) = open_channel(DEFAULT_PACKET_LEN)?;
+ let payload = vec![42u8; length];
+ for _i in 0..count {
+ HostToDevice.send(&mut h, &mut d, 10, 100, &payload)?;
+ DeviceToHost.send(&mut h, &mut d, 10, 100, &payload)?;
+ }
+ Ok(())
+}
+
+#[test_case(17; "tiny")]
+#[test_case(256; "medium")]
+#[test_case(1500; "large")]
+fn test_packet_length(packet_len: usize) -> Result<()> {
+ setup();
+ let (mut h, mut d) = open_channel(packet_len)?;
+ let payload = vec![69u8; 3200];
+ for _i in 0..10 {
+ HostToDevice.send(&mut h, &mut d, 10, 100, &payload)?;
+ DeviceToHost.send(&mut h, &mut d, 10, 100, &payload)?;
+ }
+ Ok(())
+}
+
+#[test]
+fn test_one_device_multiple_hosts() -> Result<()> {
+ const NHOSTS: usize = 4;
+ setup();
+ let mut dm = device::Mux::<_, RustCrypto>::new(TestCredentialVerifier).into_buffered();
+ dm.set_packet_len(DEFAULT_PACKET_LEN);
+
+ let mut device_chans = Vec::<Buffered<Channel<Device, RustCrypto>>>::new();
+ let mut host_chans = Vec::<Buffered<Channel<Host, RustCrypto>>>::new();
+
+ // open channels
+ for _i in 0..NHOSTS {
+ let mut hm = host::Mux::<_, RustCrypto>::new(NullCredentialStore).into_buffered();
+ hm.set_packet_len(DEFAULT_PACKET_LEN);
+ hm.request_channel(false);
+ take_turns(&mut hm, &mut dm)?;
+ let mut d = dm.channel_alloc()?.with_key(DEVICE_KEY).into_buffered();
+ take_turns(&mut d, &mut hm)?;
+ let mut h = hm.channel_alloc()?.into_buffered();
+ take_turns(&mut h, &mut d)?;
+ let h = h.map(|h| h.complete())?;
+ let d = d.map(|d| d.unwrap().complete())?;
+ device_chans.push(d);
+ host_chans.push(h);
+ }
+
+ // check that channel ids are distinct
+ let device_ids: HashSet<u16> = device_chans.iter().map(|c| c.channel_id()).collect();
+ assert_eq!(device_ids.len(), NHOSTS);
+ let host_ids: HashSet<u16> = host_chans.iter().map(|c| c.channel_id()).collect();
+ assert_eq!(host_ids.len(), NHOSTS);
+
+ // normal operation
+ let payload = vec![67u8; 160];
+ for i in 0..NHOSTS {
+ HostToDevice.send(&mut host_chans[i], &mut device_chans[i], 10, 100, &payload)?;
+ DeviceToHost.send(&mut host_chans[i], &mut device_chans[i], 10, 100, &payload)?;
+ }
+
+ // channel mismatch, packets should be ignored
+ for i in 0..NHOSTS {
+ HostToDevice.send_noresult(
+ &mut host_chans[i],
+ &mut device_chans[(i + 1) % NHOSTS],
+ &payload,
+ )?;
+ DeviceToHost.send_noresult(
+ &mut host_chans[(i + 1) % NHOSTS],
+ &mut device_chans[i],
+ &payload,
+ )?;
+ }
+
+ Ok(())
+}
+
+fn lose_nth(dir: Direction, i: usize) -> impl FnMut(Direction, &mut VecDeque<Packet>) {
+ let mut index = Some(i);
+ return move |cur_dir: Direction, wire: &mut VecDeque<Packet>| {
+ if dir != cur_dir {
+ return;
+ }
+ if let Some(i) = index {
+ if i < wire.len() {
+ let packet = wire.remove(i).unwrap();
+ log::trace!("drop {}", hex::encode(packet));
+ index = None;
+ } else {
+ index = Some(i.checked_sub(wire.len()).unwrap());
+ }
+ }
+ };
+}
+
+#[test]
+fn test_packet_loss_alloc() -> Result<()> {
+ setup();
+
+ let (mut hm, mut dm) = create_mux();
+ // channel allocation request lost
+ hm.request_channel(false);
+ take_turns_mutate(&mut hm, &mut dm, lose_nth(HostToDevice, 0))?;
+ assert!(!dm.channel_alloc_ready());
+
+ // channel allocation response lost
+ hm.request_channel(false);
+ take_turns(&mut hm, &mut dm)?;
+ let mut d = dm.channel_alloc()?.into_buffered();
+ take_turns_mutate(&mut hm, &mut d, lose_nth(DeviceToHost, 0))?;
+ assert!(!hm.channel_alloc_ready());
+
+ // successful allocation
+ hm.request_channel(false);
+ take_turns(&mut hm, &mut dm)?;
+ let mut d = dm.channel_alloc()?.into_buffered();
+ take_turns(&mut hm, &mut d)?;
+ let mut _h = hm.channel_alloc()?.into_buffered();
+ Ok(())
+}
+
+#[test_case(HostToDevice, 0; "handshake_init_request")]
+//#[test_case(HostToDevice, 1; "handshake_init_response_ack")]
+#[test_case(HostToDevice, 2; "handshake_completion_request")]
+#[test_case(HostToDevice, 3; "handshake_completion_request_cont")]
+//#[test_case(HostToDevice, 4, "handshake_completion_response_ack")]
+//#[test_case(DeviceToHost, 0, "handshake_init_request_ack")]
+#[test_case(DeviceToHost, 1; "handshake_init_response")]
+#[test_case(DeviceToHost, 2; "handshake_init_response_cont")]
+//#[test_case(DeviceToHost, 3; "handshake_completion_request_ack")]
+#[test_case(DeviceToHost, 4; "handshake_completion_response")]
+fn test_packet_loss_handshake(dir: Direction, lost_index: usize) -> Result<()> {
+ setup();
+
+ let (mut hm, mut dm) = create_mux();
+ // channel allocation
+ hm.request_channel(false);
+ take_turns(&mut hm, &mut dm)?;
+ let mut d = dm.channel_alloc()?.with_key(DEVICE_KEY).into_buffered();
+ take_turns(&mut hm, &mut d)?;
+ let mut h = hm.channel_alloc()?.into_buffered();
+
+ // handshake
+ take_turns_mutate(&mut h, &mut d, lose_nth(dir, lost_index))?;
+ if dir == DeviceToHost {
+ assert!(!h.handshake_done());
+ d.message_retransmit()?;
+ } else {
+ assert!(!d.handshake_done());
+ h.message_retransmit()?;
+ }
+ take_turns(&mut h, &mut d)?;
+ let mut h = h.map(|h| h.complete())?;
+ let mut d = d.map(|d| d.unwrap().complete())?;
+
+ // pairing
+ HostToDevice.send(&mut h, &mut d, 0, 1010, b"ThpSelectMethod with SkipParing")?;
+ DeviceToHost.send(&mut h, &mut d, 0, 1019, b"ThpEndResponse means done")?;
+
+ // application messaging
+ HostToDevice.send(&mut h, &mut d, 0, 1234, b"Ping")?;
+ DeviceToHost.send(&mut h, &mut d, 0, 5678, b"Pong")?;
+ Ok(())
+}
+
+fn damage_nth(
+ dir: Direction,
+ packet_index: usize,
+ byte_index: usize,
+) -> impl FnMut(Direction, &mut VecDeque<Packet>) {
+ let mut index = Some(packet_index);
+ return move |cur_dir: Direction, wire: &mut VecDeque<Packet>| {
+ if dir != cur_dir {
+ return;
+ }
+ if let Some(pi) = index {
+ if pi < wire.len() {
+ let packet = &mut wire[pi];
+ // only way to damage continuations which are 1XXXXXXX (X = any)
+ packet[byte_index] ^= 0b1000_0000;
+ log::trace!("damaged {}", hex::encode(packet));
+ index = None;
+ } else {
+ index = Some(pi.checked_sub(wire.len()).unwrap());
+ }
+ }
+ };
+}
+
+#[test_case(0; "cb")]
+#[test_case(1; "cid")]
+#[test_case(3; "len")]
+#[test_case(5; "data")]
+fn test_packet_damage_alloc(byte_index: usize) -> Result<()> {
+ setup();
+
+ let (mut hm, mut dm) = create_mux();
+ // channel allocation request lost
+ hm.request_channel(false);
+ take_turns_mutate(&mut hm, &mut dm, damage_nth(HostToDevice, 0, byte_index))?;
+ assert!(!dm.channel_alloc_ready());
+
+ // channel allocation response lost
+ hm.request_channel(false);
+ take_turns(&mut hm, &mut dm)?;
+ let mut d = dm.channel_alloc()?.into_buffered();
+ take_turns_mutate(&mut hm, &mut d, damage_nth(DeviceToHost, 0, byte_index))?;
+ assert!(!hm.channel_alloc_ready());
+
+ // successful allocation
+ hm.request_channel(false);
+ take_turns(&mut hm, &mut dm)?;
+ let mut d = dm.channel_alloc()?.into_buffered();
+ take_turns(&mut hm, &mut d)?;
+ let mut _h = hm.channel_alloc()?.into_buffered();
+ Ok(())
+}
+
+#[test_matrix(
+ [HostToDevice, DeviceToHost],
+ [0, 1, 2, 3, 4],
+ [0, 1, 3, 6]
+)]
+fn test_packet_damage_handshake(
+ dir: Direction,
+ packet_index: usize,
+ byte_index: usize,
+) -> Result<()> {
+ setup();
+
+ // Don't test ACKs for now.
+ let skip = [
+ (HostToDevice, 1),
+ (HostToDevice, 4),
+ (DeviceToHost, 0),
+ (DeviceToHost, 3),
+ ];
+ if skip.contains(&(dir, packet_index)) {
+ log::warn!("Skipping test case");
+ return Ok(());
+ }
+
+ let (mut hm, mut dm) = create_mux();
+ // channel allocation
+ hm.request_channel(false);
+ take_turns(&mut hm, &mut dm)?;
+ let mut d = dm.channel_alloc()?.with_key(DEVICE_KEY).into_buffered();
+ take_turns(&mut hm, &mut d)?;
+ let mut h = hm.channel_alloc()?.into_buffered();
+
+ // handshake
+ take_turns_mutate(&mut h, &mut d, damage_nth(dir, packet_index, byte_index))?;
+ if dir == DeviceToHost {
+ assert!(!h.handshake_done());
+ d.message_retransmit()?;
+ } else {
+ assert!(!d.handshake_done());
+ h.message_retransmit()?;
+ }
+ take_turns(&mut h, &mut d)?;
+ let mut h = h.map(|h| h.complete())?;
+ let mut d = d.map(|d| d.unwrap().complete())?;
+
+ // pairing
+ HostToDevice.send(&mut h, &mut d, 0, 1010, b"ThpSelectMethod with SkipParing")?;
+ DeviceToHost.send(&mut h, &mut d, 0, 1019, b"ThpEndResponse means done")?;
+
+ // application messaging
+ HostToDevice.send(&mut h, &mut d, 0, 1234, b"Ping")?;
+ DeviceToHost.send(&mut h, &mut d, 0, 5678, b"Pong")?;
+ Ok(())
+}
+
+#[test]
+fn test_packet_damage_application() -> Result<()> {
+ setup();
+
+ let (mut h, mut d) = open_channel(DEFAULT_PACKET_LEN)?;
+ h.message_in(0, 0, b"the quick brown fox jumps over the lazy dog")?;
+ let res = take_turns_mutate(&mut h, &mut d, damage_nth(HostToDevice, 0, 16));
+ assert_eq!(res, Err(Error::InvalidChecksum));
+ h.message_retransmit().unwrap();
+ take_turns(&mut h, &mut d)?;
+
+ d.message_in(0, 0, b"the quick lazy dog crawls under the brown fox")?;
+ let res = take_turns_mutate(&mut h, &mut d, damage_nth(DeviceToHost, 0, 50));
+ assert_eq!(res, Err(Error::InvalidChecksum));
+ d.message_retransmit().unwrap();
+ take_turns(&mut h, &mut d)?;
+
+ Ok(())
+}
+
+#[test]
+fn test_codec_v1() -> Result<()> {
+ setup();
+
+ fn assert_ignored<C: ChannelIO>(channel: &mut Buffered<C>, packet: &[u8]) {
+ let pir = channel.packet_in(&packet);
+ assert!(matches!(pir, PacketInResult::Ignored { .. }));
+ assert!(!channel.packet_out_ready());
+ }
+
+ let v1_init = "3f23230001000000080a0470696e671001000000000000000000000000000000\
+ 0000000000000000000000000000000000000000000000000000000000000000";
+ let v1_cont = "3f61616161616161616161616161616161616161616141414141414141414141\
+ 4141414141414141414141414141414141414141414141414141414141414141";
+ let v1_init = hex::decode(v1_init).unwrap();
+ let v1_cont = hex::decode(v1_cont).unwrap();
+
+ // broadcast handling
+ let (mut hm, mut dm) = create_mux();
+ // device::Mux shoud respond
+ let pir = dm.packet_in(&v1_init);
+ assert!(matches!(
+ pir,
+ PacketInResult::Accepted {
+ ack_received: false,
+ message_ready: false,
+ pong: false
+ }
+ ));
+ let response = dm.packet_out().unwrap();
+ assert!(response.starts_with(b"?##"));
+ assert!(!dm.packet_out_ready());
+ // in other cases the packet should be ignored
+ assert_ignored(&mut dm, &v1_cont);
+ assert_ignored(&mut hm, &v1_init);
+ assert_ignored(&mut hm, &v1_cont);
+
+ // non-broadcast handling
+ let (mut h, mut d) = open_channel(DEFAULT_PACKET_LEN)?;
+ assert_ne!(h.channel_id, 0x2323);
+ assert_ne!(d.channel_id, 0x2323);
+ assert_ignored(&mut d, &v1_init);
+ assert_ignored(&mut d, &v1_cont);
+ assert_ignored(&mut h, &v1_init);
+ assert_ignored(&mut h, &v1_cont);
+
+ // non-broadcast handling, channel_id == "##"
+ h.channel_id = 0x2323;
+ d.channel_id = 0x2323;
+ assert_ignored(&mut d, &v1_init);
+ assert_ignored(&mut d, &v1_cont);
+ assert_ignored(&mut h, &v1_init);
+ assert_ignored(&mut h, &v1_cont);
+
+ Ok(())
+}
+
+#[test]
+fn test_ping() -> Result<()> {
+ setup();
+
+ let (mut hm, mut dm) = create_mux();
+ // host->device ping
+ hm.ping();
+ let ping_packet = hm.packet_out()?;
+ let pir = dm.packet_in(&ping_packet);
+ assert!(matches!(
+ pir,
+ PacketInResult::Accepted {
+ ack_received: false,
+ message_ready: false,
+ pong: false
+ }
+ ));
+ let pong_packet = dm.packet_out()?;
+ let pir = hm.packet_in(&pong_packet);
+ assert!(matches!(
+ pir,
+ PacketInResult::Accepted {
+ ack_received: false,
+ message_ready: false,
+ pong: true
+ }
+ ));
+ assert!(pir.got_pong());
+ // duplicates are ignored
+ let pir = hm.packet_in(&pong_packet);
+ assert!(matches!(pir, PacketInResult::Ignored { .. }));
+ assert!(!pir.got_pong());
+
+ // device->host ping is not implemented
+ let pir = hm.packet_in(&ping_packet);
+ assert!(matches!(pir, PacketInResult::Ignored { .. }));
+
+ // non-broadcast channels ignore ping
+ let (mut h, mut d) = open_channel(DEFAULT_PACKET_LEN)?;
+ let pir = h.packet_in(&ping_packet);
+ assert!(matches!(pir, PacketInResult::Ignored { .. }));
+ let pir = d.packet_in(&ping_packet);
+ assert!(matches!(pir, PacketInResult::Ignored { .. }));
+
+ Ok(())
+}
+
+#[test]
+fn test_invalid_channel_id() -> Result<()> {
+ setup();
+
+ const INVALID: &[u16] = &[
+ MIN_CHANNEL_ID - 1,
+ MAX_CHANNEL_ID + 1,
+ BROADCAST_CHANNEL_ID - 1,
+ ];
+
+ fn make_packet(channel_id: u16) -> Vec<u8> {
+ let mut res = Vec::new();
+ res.push(0x04);
+ res.push((channel_id >> 8) as u8);
+ res.push((channel_id & 0xff) as u8);
+ res.extend_from_slice(&[0x00, 0x10]);
+ res.resize(DEFAULT_PACKET_LEN, 0x00);
+ res
+ }
+
+ let (mut hm, mut dm) = create_mux();
+ // muxes return Route(cid) for valid non-broadcast channel
+ let pir = dm.packet_in(&make_packet(66));
+ assert_eq!(pir, PacketInResult::Route { channel_id: 66 });
+ let pir = hm.packet_in(&make_packet(66));
+ assert_eq!(pir, PacketInResult::Route { channel_id: 66 });
+
+ // muxes return error for invalid channel ids
+ for channel_id in INVALID {
+ assert_eq!(
+ dm.packet_in(&make_packet(*channel_id)),
+ PacketInResult::Ignored {
+ error: Error::MalformedData
+ }
+ );
+ assert_eq!(
+ hm.packet_in(&make_packet(*channel_id)),
+ PacketInResult::Ignored {
+ error: Error::MalformedData
+ }
+ );
+ }
+
+ // normal channels return error for invalid ids
+ let (mut h, mut d) = open_channel(DEFAULT_PACKET_LEN)?;
+ for channel_id in INVALID {
+ assert_eq!(
+ d.packet_in(&make_packet(*channel_id)),
+ PacketInResult::Ignored {
+ error: Error::MalformedData
+ }
+ );
+ assert_eq!(
+ h.packet_in(&make_packet(*channel_id)),
+ PacketInResult::Ignored {
+ error: Error::MalformedData
+ }
+ );
+ }
+
+ Ok(())
+}
+
+// NOTE: does not work with CRC across packet boundary.
+fn recompute_crc(wire: &mut VecDeque<Packet>) {
+ let payload_len: usize = u16::from_be_bytes([wire[0][3], wire[0][4]]).into();
+ let mut crc_start = 5 + payload_len - crc32::CHECKSUM_LEN;
+ let mut buf = Vec::new();
+ for p in wire.iter() {
+ buf.extend_from_slice(p);
+ }
+ buf.truncate(crc_start);
+ let checksum = crc32::digest(&buf);
+ for i in 0..wire.len() {
+ if crc_start > wire[i].len() {
+ crc_start -= wire[i].len()
+ } else {
+ wire[i][crc_start..crc_start + crc32::CHECKSUM_LEN].copy_from_slice(&checksum);
+ break;
+ }
+ }
+}
+
+fn damage_nth_fix_crc(
+ dir: Direction,
+ packet_index: usize,
+ byte_index: usize,
+) -> impl FnMut(Direction, &mut VecDeque<Packet>) {
+ let mut index = Some(packet_index);
+ return move |cur_dir: Direction, wire: &mut VecDeque<Packet>| {
+ if dir != cur_dir {
+ return;
+ }
+ let Some(pi) = index else {
+ return;
+ };
+ if pi < wire.len() {
+ // only way to damage continuations which are 1XXXXXXX (X = any)
+ wire[pi][byte_index] ^= 0b1000_0000;
+ recompute_crc(wire);
+ log::trace!("damaged+crc {}", hex::encode(&wire[pi]));
+ index = None;
+ } else {
+ index = Some(pi.checked_sub(wire.len()).unwrap());
+ }
+ };
+}
+
+#[test]
+fn test_invalid_tag() -> Result<()> {
+ setup();
+
+ let (mut h, mut d) = open_channel(DEFAULT_PACKET_LEN)?;
+ h.message_in(0, 404, b"hello world hello world hello")
+ .unwrap();
+ let res = take_turns_mutate(&mut h, &mut d, damage_nth_fix_crc(HostToDevice, 0, 10));
+ assert_eq!(res, Err(Error::CryptoError));
+
+ let (mut h, mut d) = open_channel(DEFAULT_PACKET_LEN)?;
+ d.message_in(0, 403, b"hello world hello world hello")
+ .unwrap();
+ let res = take_turns_mutate(&mut h, &mut d, damage_nth_fix_crc(DeviceToHost, 0, 20));
+ assert_eq!(res, Err(Error::CryptoError));
+
+ Ok(())
+}
+
+#[test]
+fn test_channel_id_wraparound() -> Result<()> {
+ setup();
+
+ fn alloc_test(
+ hm: &mut Buffered<host::Mux<NullCredentialStore, RustCrypto>>,
+ dm: &mut Buffered<device::Mux<TestCredentialVerifier, RustCrypto>>,
+ expected_id: u16,
+ ) -> Result<()> {
+ hm.request_channel(false);
+ take_turns(hm, dm)?;
+ let mut d = dm.channel_alloc()?.into_buffered();
+ take_turns(hm, &mut d)?;
+ let h = hm.channel_alloc()?.into_buffered();
+ assert_eq!(h.channel_id(), expected_id);
+ Ok(())
+ }
+
+ let (mut hm, mut dm) = create_mux();
+ dm.set_next_channel_id(MAX_CHANNEL_ID - 1);
+ alloc_test(&mut hm, &mut dm, MAX_CHANNEL_ID - 1)?;
+ alloc_test(&mut hm, &mut dm, MAX_CHANNEL_ID)?;
+ alloc_test(&mut hm, &mut dm, MIN_CHANNEL_ID)?;
+ alloc_test(&mut hm, &mut dm, MIN_CHANNEL_ID + 1)?;
+
+ Ok(())
+}
diff --git a/rust/trezor-thp/src/credential.rs b/rust/trezor-thp/src/credential.rs
index 86fbfb0c..c6b6fc76 100644
--- a/rust/trezor-thp/src/credential.rs
+++ b/rust/trezor-thp/src/credential.rs
@@ -1,7 +1,7 @@
-pub const CREDENTIAL_PRIVKEY_LENGTH: usize = 32;
+use crate::channel::{PRIVKEY_LEN, PairingState};
pub struct FoundCredential<'a> {
- pub local_static_privkey: &'a [u8; CREDENTIAL_PRIVKEY_LENGTH],
+ pub local_static_privkey: &'a [u8; PRIVKEY_LEN],
pub auth_credential: &'a [u8],
}
@@ -33,3 +33,12 @@ impl CredentialStore for NullCredentialStore {
None
}
}
+
+/// Device-side credential handling.
+pub trait CredentialVerifier: Clone {
+ /// Validate given protobuf-encoded credential.
+ fn verify(&self, remote_static_pubkey: &[u8], credential: &[u8]) -> PairingState;
+
+ /// Return protobuf-encoded device properties to be used in `ChannelAllocationResponse` message.
+ fn device_properties(&self) -> &[u8];
+}
diff --git a/rust/trezor-thp/src/error.rs b/rust/trezor-thp/src/error.rs
index 59aa2b97..4c4cbbd6 100644
--- a/rust/trezor-thp/src/error.rs
+++ b/rust/trezor-thp/src/error.rs
@@ -20,10 +20,7 @@ pub enum TransportError {
impl TransportError {
pub fn is_recoverable(&self) -> bool {
- matches!(
- self,
- TransportError::TransportBusy | TransportError::DeviceLocked
- )
+ matches!(self, TransportError::TransportBusy)
}
}
@@ -79,7 +76,6 @@ impl From<NoiseError> for Error {
fn from(val: NoiseError) -> Self {
match val.kind() {
NoiseErrorKind::DH | NoiseErrorKind::Decryption => Self::crypto_error(),
- NoiseErrorKind::NeedPSK => panic!(),
NoiseErrorKind::TooShort => Self::malformed_data(),
}
}
diff --git a/rust/trezor-thp/src/fragment.rs b/rust/trezor-thp/src/fragment.rs
index cba52045..5bbff26c 100644
--- a/rust/trezor-thp/src/fragment.rs
+++ b/rust/trezor-thp/src/fragment.rs
@@ -117,20 +117,18 @@ impl<R: Role> Reassembler<R> {
pub fn new(input: &[u8], buffer: &mut [u8]) -> Result<Self> {
let (header, after_header) = Header::parse(input)?;
if header.is_continuation() {
- return Err(Error::unexpected_input());
+ return Err(Error::malformed_data());
}
- let payload_len = header.payload_len().into();
- if buffer.len() < payload_len {
+ let nbytes = after_header.len(); // Header::parse strips padding
+ let payload_len: usize = header.payload_len().into();
+ if buffer.len() < nbytes {
return Err(Error::insufficient_buffer());
}
+ buffer[..nbytes].copy_from_slice(after_header);
let mut checksum = Crc32::new();
checksum.update(&input[..header.header_len()]);
-
- let nbytes = after_header.len(); // Header::parse strips padding
- buffer[..nbytes].copy_from_slice(after_header);
-
let checksum_bytes = payload_len.saturating_sub(CHECKSUM_LEN).min(nbytes);
checksum.update(&after_header[..checksum_bytes]);
@@ -217,7 +215,7 @@ impl<R: Role> Reassembler<R> {
pub fn single_inplace(buffer: &[u8]) -> Result<(Header<R>, &[u8])> {
let (header, after_header) = Header::parse(buffer)?;
if header.is_continuation() {
- return Err(Error::unexpected_input());
+ return Err(Error::malformed_data());
}
let payload_len: usize = header.payload_len().into();
if payload_len != after_header.len() {
@@ -233,7 +231,7 @@ impl<R: Role> Reassembler<R> {
let received_checksum = *after_header
.last_chunk::<CHECKSUM_LEN>()
- .ok_or_else(Error::invalid_checksum)?;
+ .ok_or_else(Error::malformed_data)?;
if computed_checksum != received_checksum {
return Err(Error::invalid_checksum());
}
diff --git a/rust/trezor-thp/src/header.rs b/rust/trezor-thp/src/header.rs
index e8ea9d56..efe2e910 100644
--- a/rust/trezor-thp/src/header.rs
+++ b/rust/trezor-thp/src/header.rs
@@ -1,5 +1,5 @@
use crate::Role;
-pub use crate::alternating_bit::SyncBits;
+use crate::alternating_bit::SyncBits;
use crate::control_byte::{self, ControlByte};
use crate::crc32;
use crate::error::{Error, Result};
@@ -10,8 +10,8 @@ const CHECKSUM_LEN: u16 = crc32::CHECKSUM_LEN as u16;
pub const NONCE_LEN: u16 = 8;
const MAX_PAYLOAD_LEN: u16 = 60000;
-const MIN_CHANNEL_ID: u16 = 0x0001;
-const MAX_CHANNEL_ID: u16 = 0xFFEF;
+pub const MIN_CHANNEL_ID: u16 = 0x0001;
+pub const MAX_CHANNEL_ID: u16 = 0xFFEF;
pub const BROADCAST_CHANNEL_ID: u16 = 0xFFFF;
/// Represents packet header, i.e. control byte, channel id and possibly payload length.
@@ -60,6 +60,7 @@ pub enum HandshakeMessage {
CompletionResponse,
}
+/// Check whether channel id is valid. Please note this also includes broadcast channel.
pub const fn channel_id_valid(channel_id: u16) -> bool {
(MIN_CHANNEL_ID <= channel_id && channel_id <= MAX_CHANNEL_ID)
|| channel_id == BROADCAST_CHANNEL_ID
@@ -363,6 +364,14 @@ impl<R: Role> Header<R> {
Self::Pong
}
+ pub const fn new_codec1_request(is_continuation: bool) -> Self {
+ Self::CodecV1Request { is_continuation }
+ }
+
+ pub const fn new_codec1_response() -> Self {
+ Self::CodecV1Response
+ }
+
pub const fn is_continuation(&self) -> bool {
matches!(self, Self::Continuation { .. })
}
diff --git a/rust/trezor-thp/src/lib.rs b/rust/trezor-thp/src/lib.rs
index b998fdf6..56d62752 100644
--- a/rust/trezor-thp/src/lib.rs
+++ b/rust/trezor-thp/src/lib.rs
@@ -8,7 +8,7 @@ mod control_byte;
mod crc32;
pub mod credential;
pub mod error;
-pub mod fragment;
+mod fragment;
pub mod header;
mod util;
Why this scored 28/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.