Skip to main content

auc_tool/
transport.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::mpsc::{self, Receiver, SyncSender, TryRecvError};
5use std::thread;
6use std::time::{Duration, Instant};
7
8use anyhow::{Context, Result, anyhow, bail};
9use passless_uhid::{DeviceIdentity, RawUhidDevice, UhidEvent};
10use rand::RngExt as _;
11use soft_fido2_transport::{ChannelManager, Cmd, Message, Packet};
12
13use crate::authenticator::{AuthenticatorEngine, PresenceGate};
14use crate::vault::Vault;
15
16const CTAPHID_BROADCAST_CID: u32 = 0xffff_ffff;
17const CTAPHID_MAX_MESSAGE_SIZE: usize = 7609;
18const INITIAL_PAYLOAD_SIZE: usize = 57;
19const CONTINUATION_PAYLOAD_SIZE: usize = 59;
20const MAX_ALLOCATED_CHANNELS: usize = 64;
21const KEEPALIVE_INTERVAL: Duration = Duration::from_millis(100);
22const POLL_INTERVAL: Duration = Duration::from_millis(2);
23const CAPABILITY_WINK: u8 = 0x01;
24const CAPABILITY_CBOR: u8 = 0x04;
25const CAPABILITY_NMSG: u8 = 0x08;
26const KEEPALIVE_PROCESSING: u8 = 0x01;
27const KEEPALIVE_UP_NEEDED: u8 = 0x02;
28
29pub fn run_uhid(
30    vault: Vault,
31    presence: PresenceGate,
32    device_present: Arc<AtomicBool>,
33) -> Result<()> {
34    let identity = DeviceIdentity::new(
35        "auc software authenticator",
36        "auc/uhid",
37        vault.device_unique_name()?,
38        0x1209,
39        0xa0c0,
40        0x0001,
41    );
42    let device = RawUhidDevice::create(identity).context("failed to create auc UHID device")?;
43    device
44        .set_nonblocking(true)
45        .context("failed to make auc UHID descriptor nonblocking")?;
46    let _presence = DevicePresence::new(Arc::clone(&device_present));
47    let engine = AuthenticatorEngine::new(vault, presence.clone())?;
48    let (work_tx, work_rx) = mpsc::sync_channel(1);
49    let (result_tx, result_rx) = mpsc::sync_channel(1);
50    thread::Builder::new()
51        .name("auc-ctap".to_string())
52        .spawn(move || command_worker(engine, work_rx, result_tx))
53        .context("failed to start auc CTAP worker")?;
54    TransportLoop::new(UhidEndpoint::new(device), presence, work_tx, result_rx).run()
55}
56
57struct DevicePresence {
58    present: Arc<AtomicBool>,
59}
60
61impl DevicePresence {
62    fn new(present: Arc<AtomicBool>) -> Self {
63        present.store(true, Ordering::Release);
64        Self { present }
65    }
66}
67
68impl Drop for DevicePresence {
69    fn drop(&mut self) {
70        self.present.store(false, Ordering::Release);
71    }
72}
73
74struct CtapWork {
75    channel: u32,
76    request: Vec<u8>,
77}
78
79struct CtapResult {
80    channel: u32,
81    response: Result<Vec<u8>>,
82}
83
84fn command_worker(
85    mut engine: AuthenticatorEngine,
86    work: Receiver<CtapWork>,
87    results: SyncSender<CtapResult>,
88) {
89    while let Ok(work) = work.recv() {
90        let result = CtapResult {
91            channel: work.channel,
92            response: engine.handle(&work.request),
93        };
94        if results.send(result).is_err() {
95            break;
96        }
97    }
98}
99
100trait HidEndpoint {
101    fn read_event(&mut self) -> Result<Option<EndpointEvent>>;
102    fn write_packet(&mut self, packet: &[u8; 64]) -> Result<()>;
103}
104
105enum EndpointEvent {
106    Packet([u8; 64]),
107    Disconnected,
108}
109
110struct UhidEndpoint {
111    device: RawUhidDevice,
112}
113
114impl UhidEndpoint {
115    fn new(device: RawUhidDevice) -> Self {
116        Self { device }
117    }
118}
119
120impl HidEndpoint for UhidEndpoint {
121    fn read_event(&mut self) -> Result<Option<EndpointEvent>> {
122        loop {
123            match self.device.read_event()? {
124                Some(UhidEvent::Output { data, .. }) if data.len() == 65 && data[0] == 0 => {
125                    return Ok(Some(EndpointEvent::Packet(
126                        data[1..]
127                            .try_into()
128                            .expect("validated FIDO report has 64 bytes"),
129                    )));
130                }
131                Some(UhidEvent::Output { data, .. }) if data.len() == 64 => {
132                    return Ok(Some(EndpointEvent::Packet(
133                        data.as_slice()
134                            .try_into()
135                            .expect("validated FIDO report has 64 bytes"),
136                    )));
137                }
138                Some(UhidEvent::Close) => return Ok(Some(EndpointEvent::Disconnected)),
139                Some(UhidEvent::GetReport { id, .. }) => {
140                    self.device
141                        .send_get_report_reply(id, libc::EIO as u16, &[])?;
142                }
143                Some(UhidEvent::SetReport { id, .. }) => {
144                    self.device.send_set_report_reply(id, libc::EIO as u16)?;
145                }
146                Some(_) => {}
147                None => return Ok(None),
148            }
149        }
150    }
151
152    fn write_packet(&mut self, packet: &[u8; 64]) -> Result<()> {
153        self.device.write_packet(packet).map_err(Into::into)
154    }
155}
156
157struct ActiveCtap {
158    channel: u32,
159    cancelled: bool,
160    last_keepalive: Instant,
161}
162
163struct ChannelLock {
164    channel: u32,
165    expires: Instant,
166}
167
168struct TransportLoop<E> {
169    endpoint: E,
170    assembly: ChannelManager,
171    allocated: HashMap<u32, Instant>,
172    lock: Option<ChannelLock>,
173    active: Option<ActiveCtap>,
174    presence: PresenceGate,
175    work: SyncSender<CtapWork>,
176    results: Receiver<CtapResult>,
177}
178
179impl<E: HidEndpoint> TransportLoop<E> {
180    fn new(
181        endpoint: E,
182        presence: PresenceGate,
183        work: SyncSender<CtapWork>,
184        results: Receiver<CtapResult>,
185    ) -> Self {
186        Self {
187            endpoint,
188            assembly: ChannelManager::new(),
189            allocated: HashMap::new(),
190            lock: None,
191            active: None,
192            presence,
193            work,
194            results,
195        }
196    }
197
198    fn run(mut self) -> Result<()> {
199        loop {
200            match self.endpoint.read_event()? {
201                Some(EndpointEvent::Packet(packet)) => {
202                    self.process_packet(Packet::from_bytes(packet))?;
203                }
204                Some(EndpointEvent::Disconnected) => self.cancel_active(),
205                None => {}
206            }
207            self.process_result()?;
208            self.send_keepalive()?;
209            thread::sleep(POLL_INTERVAL);
210        }
211    }
212
213    fn process_packet(&mut self, packet: Packet) -> Result<()> {
214        if !self.packet_channel_is_valid(&packet) {
215            return self.write_error(packet.cid(), TransportError::InvalidChannel);
216        }
217        let channel = packet.cid();
218        if packet.is_init()
219            && packet
220                .payload_len()
221                .is_some_and(|length| usize::from(length) > CTAPHID_MAX_MESSAGE_SIZE)
222        {
223            return self.write_error(channel, TransportError::InvalidLength);
224        }
225        if let Some(last_used) = self.allocated.get_mut(&channel) {
226            *last_used = Instant::now();
227        }
228        match self.assembly.process_packet(packet) {
229            Ok(Some(message)) => self.process_message(message),
230            Ok(None) => Ok(()),
231            Err(error) => self.write_error(channel, TransportError::from_soft(error)),
232        }
233    }
234
235    fn packet_channel_is_valid(&self, packet: &Packet) -> bool {
236        if packet.cid() == CTAPHID_BROADCAST_CID {
237            return packet.is_init() && packet.cmd() == Some(Cmd::Init);
238        }
239        self.allocated.contains_key(&packet.cid())
240    }
241
242    fn process_message(&mut self, message: Message) -> Result<()> {
243        self.expire_lock();
244        if self
245            .lock
246            .as_ref()
247            .is_some_and(|lock| lock.channel != message.cid && message.cmd != Cmd::Init)
248        {
249            return self.write_error(message.cid, TransportError::ChannelBusy);
250        }
251        match message.cmd {
252            Cmd::Init => self.handle_init(message),
253            Cmd::Ping => self.write_message(Message::new(
254                message.cid,
255                Cmd::Ping,
256                message.data,
257                Some(CTAPHID_MAX_MESSAGE_SIZE),
258            )),
259            Cmd::Wink if message.data.is_empty() => self.write_message(Message::new(
260                message.cid,
261                Cmd::Wink,
262                Vec::new(),
263                Some(CTAPHID_MAX_MESSAGE_SIZE),
264            )),
265            Cmd::Wink => self.write_error(message.cid, TransportError::InvalidLength),
266            Cmd::Lock => self.handle_lock(message),
267            Cmd::Cancel => {
268                if !message.data.is_empty() {
269                    return self.write_error(message.cid, TransportError::InvalidLength);
270                }
271                self.cancel(message.cid);
272                Ok(())
273            }
274            Cmd::Cbor => self.start_cbor(message),
275            Cmd::Msg | Cmd::Keepalive | Cmd::Error => {
276                self.write_error(message.cid, TransportError::InvalidCommand)
277            }
278            _ => self.write_error(message.cid, TransportError::InvalidCommand),
279        }
280    }
281
282    fn handle_init(&mut self, message: Message) -> Result<()> {
283        if message.data.len() != 8 {
284            return self.write_error(message.cid, TransportError::InvalidLength);
285        }
286        if message.cid != CTAPHID_BROADCAST_CID {
287            self.cancel(message.cid);
288        }
289        let channel = if message.cid == CTAPHID_BROADCAST_CID {
290            let Some(channel) = self.allocate_channel() else {
291                return self.write_error(message.cid, TransportError::ChannelBusy);
292            };
293            channel
294        } else {
295            message.cid
296        };
297        let mut data = Vec::with_capacity(17);
298        data.extend_from_slice(&message.data);
299        data.extend_from_slice(&channel.to_be_bytes());
300        data.push(2);
301        data.push(
302            env!("CARGO_PKG_VERSION_MAJOR")
303                .parse()
304                .context("auc major version does not fit the CTAPHID version field")?,
305        );
306        data.push(
307            env!("CARGO_PKG_VERSION_MINOR")
308                .parse()
309                .context("auc minor version does not fit the CTAPHID version field")?,
310        );
311        data.push(
312            env!("CARGO_PKG_VERSION_PATCH")
313                .parse()
314                .context("auc patch version does not fit the CTAPHID version field")?,
315        );
316        data.push(CAPABILITY_WINK | CAPABILITY_CBOR | CAPABILITY_NMSG);
317        self.write_message(Message::new(
318            message.cid,
319            Cmd::Init,
320            data,
321            Some(CTAPHID_MAX_MESSAGE_SIZE),
322        ))
323    }
324
325    fn allocate_channel(&mut self) -> Option<u32> {
326        if self.allocated.len() >= MAX_ALLOCATED_CHANNELS {
327            let protected_active = self.active.as_ref().map(|active| active.channel);
328            let protected_lock = self.lock.as_ref().map(|lock| lock.channel);
329            let (&oldest, _) = self
330                .allocated
331                .iter()
332                .filter(|(channel, _)| {
333                    Some(**channel) != protected_active && Some(**channel) != protected_lock
334                })
335                .min_by_key(|(_, last_used)| *last_used)?;
336            self.allocated.remove(&oldest);
337            self.assembly.cancel_channel(oldest);
338        }
339        loop {
340            let channel: u32 = rand::rng().random();
341            if channel != 0
342                && channel != CTAPHID_BROADCAST_CID
343                && !self.allocated.contains_key(&channel)
344            {
345                self.allocated.insert(channel, Instant::now());
346                return Some(channel);
347            }
348        }
349    }
350
351    fn handle_lock(&mut self, message: Message) -> Result<()> {
352        let Some(&seconds) = message.data.first().filter(|_| message.data.len() == 1) else {
353            return self.write_error(message.cid, TransportError::InvalidLength);
354        };
355        if seconds > 10 {
356            return self.write_error(message.cid, TransportError::InvalidParameter);
357        }
358        if seconds == 0 {
359            if self
360                .lock
361                .as_ref()
362                .is_some_and(|lock| lock.channel == message.cid)
363            {
364                self.lock = None;
365            }
366        } else {
367            self.lock = Some(ChannelLock {
368                channel: message.cid,
369                expires: Instant::now() + Duration::from_secs(seconds.into()),
370            });
371        }
372        self.write_message(Message::new(
373            message.cid,
374            Cmd::Lock,
375            Vec::new(),
376            Some(CTAPHID_MAX_MESSAGE_SIZE),
377        ))
378    }
379
380    fn start_cbor(&mut self, message: Message) -> Result<()> {
381        if message.data.len() > CTAPHID_MAX_MESSAGE_SIZE {
382            return self.write_error(message.cid, TransportError::InvalidLength);
383        }
384        if self.active.is_some() {
385            return self.write_error(message.cid, TransportError::ChannelBusy);
386        }
387        self.presence.begin_command(message.cid)?;
388        if self
389            .work
390            .send(CtapWork {
391                channel: message.cid,
392                request: message.data,
393            })
394            .is_err()
395        {
396            self.presence.finish_command(message.cid);
397            bail!("auc CTAP worker exited unexpectedly");
398        }
399        self.active = Some(ActiveCtap {
400            channel: message.cid,
401            cancelled: false,
402            last_keepalive: Instant::now(),
403        });
404        Ok(())
405    }
406
407    fn cancel(&mut self, channel: u32) {
408        self.assembly.cancel_channel(channel);
409        if let Some(active) = self
410            .active
411            .as_mut()
412            .filter(|active| active.channel == channel)
413        {
414            active.cancelled = true;
415            self.presence.cancel(channel);
416        }
417    }
418
419    fn cancel_active(&mut self) {
420        if let Some(active) = self.active.as_mut() {
421            active.cancelled = true;
422            self.presence.cancel(active.channel);
423        }
424        self.assembly.clear();
425    }
426
427    fn process_result(&mut self) -> Result<()> {
428        match self.results.try_recv() {
429            Ok(result) => {
430                let active = self
431                    .active
432                    .take()
433                    .ok_or_else(|| anyhow!("auc CTAP worker returned without an active command"))?;
434                if result.channel != active.channel {
435                    bail!("auc CTAP worker returned a mismatched channel");
436                }
437                let cancelled = active.cancelled || self.presence.is_cancelled(active.channel);
438                self.presence.finish_command(active.channel);
439                if cancelled {
440                    return Ok(());
441                }
442                match result.response {
443                    Ok(response) => self.write_message(Message::new(
444                        active.channel,
445                        Cmd::Cbor,
446                        response,
447                        Some(CTAPHID_MAX_MESSAGE_SIZE),
448                    )),
449                    Err(error) => {
450                        eprintln!("auc CTAP command failed: {error:#}");
451                        self.write_error(active.channel, TransportError::Other)
452                    }
453                }
454            }
455            Err(TryRecvError::Empty) => Ok(()),
456            Err(TryRecvError::Disconnected) => bail!("auc CTAP worker disconnected"),
457        }
458    }
459
460    fn send_keepalive(&mut self) -> Result<()> {
461        let Some(active) = self.active.as_mut() else {
462            return Ok(());
463        };
464        if active.cancelled || active.last_keepalive.elapsed() < KEEPALIVE_INTERVAL {
465            return Ok(());
466        }
467        let channel = active.channel;
468        active.last_keepalive = Instant::now();
469        let status = if self.presence.is_waiting(channel) {
470            KEEPALIVE_UP_NEEDED
471        } else {
472            KEEPALIVE_PROCESSING
473        };
474        self.write_message(Message::new(
475            channel,
476            Cmd::Keepalive,
477            vec![status],
478            Some(CTAPHID_MAX_MESSAGE_SIZE),
479        ))
480    }
481
482    fn expire_lock(&mut self) {
483        if self
484            .lock
485            .as_ref()
486            .is_some_and(|lock| Instant::now() >= lock.expires)
487        {
488            self.lock = None;
489        }
490    }
491
492    fn write_message(&mut self, message: Message) -> Result<()> {
493        for packet in fragment_message(&message)? {
494            self.endpoint.write_packet(&packet)?;
495        }
496        Ok(())
497    }
498
499    fn write_error(&mut self, channel: u32, error: TransportError) -> Result<()> {
500        self.endpoint
501            .write_packet(Packet::new_error(channel, error.soft()).as_bytes())
502    }
503}
504
505fn fragment_message(message: &Message) -> Result<Vec<[u8; 64]>> {
506    let limit = message.max_msg_size.unwrap_or(CTAPHID_MAX_MESSAGE_SIZE);
507    if message.data.len() > limit || message.data.len() > CTAPHID_MAX_MESSAGE_SIZE {
508        bail!("auc CTAPHID response exceeds the protocol limit");
509    }
510    let mut packets = Vec::with_capacity(
511        1 + message
512            .data
513            .len()
514            .saturating_sub(INITIAL_PAYLOAD_SIZE)
515            .div_ceil(CONTINUATION_PAYLOAD_SIZE),
516    );
517    let mut initial = [0_u8; 64];
518    initial[..4].copy_from_slice(&message.cid.to_be_bytes());
519    initial[4] = message.cmd.to_u8_init();
520    initial[5..7].copy_from_slice(&(message.data.len() as u16).to_be_bytes());
521    let initial_length = message.data.len().min(INITIAL_PAYLOAD_SIZE);
522    initial[7..7 + initial_length].copy_from_slice(&message.data[..initial_length]);
523    packets.push(initial);
524    for (sequence, chunk) in message.data[initial_length..]
525        .chunks(CONTINUATION_PAYLOAD_SIZE)
526        .enumerate()
527    {
528        let sequence = u8::try_from(sequence)
529            .ok()
530            .filter(|sequence| *sequence <= 127)
531            .ok_or_else(|| anyhow!("auc CTAPHID response requires too many packets"))?;
532        let mut continuation = [0_u8; 64];
533        continuation[..4].copy_from_slice(&message.cid.to_be_bytes());
534        continuation[4] = sequence;
535        continuation[5..5 + chunk.len()].copy_from_slice(chunk);
536        packets.push(continuation);
537    }
538    Ok(packets)
539}
540
541#[derive(Clone, Copy)]
542enum TransportError {
543    InvalidCommand,
544    InvalidParameter,
545    InvalidLength,
546    InvalidSequence,
547    MessageTimeout,
548    ChannelBusy,
549    InvalidChannel,
550    Other,
551}
552
553impl TransportError {
554    fn from_soft(error: soft_fido2_transport::Error) -> Self {
555        match error {
556            soft_fido2_transport::Error::InvalidSequence => Self::InvalidSequence,
557            soft_fido2_transport::Error::InvalidChannel => Self::InvalidChannel,
558            soft_fido2_transport::Error::InvalidCommand => Self::InvalidCommand,
559            soft_fido2_transport::Error::InvalidPacket
560            | soft_fido2_transport::Error::FragmentationError => Self::InvalidSequence,
561            soft_fido2_transport::Error::MessageTooLarge => Self::InvalidLength,
562            soft_fido2_transport::Error::Timeout => Self::MessageTimeout,
563            soft_fido2_transport::Error::ChannelBusy => Self::ChannelBusy,
564            _ => Self::Other,
565        }
566    }
567
568    fn soft(self) -> soft_fido2_transport::ctaphid::ErrorCode {
569        use soft_fido2_transport::ctaphid::ErrorCode;
570
571        match self {
572            Self::InvalidCommand => ErrorCode::InvalidCmd,
573            Self::InvalidParameter => ErrorCode::InvalidPar,
574            Self::InvalidLength => ErrorCode::InvalidLen,
575            Self::InvalidSequence => ErrorCode::InvalidSeq,
576            Self::MessageTimeout => ErrorCode::MsgTimeout,
577            Self::ChannelBusy => ErrorCode::ChannelBusy,
578            Self::InvalidChannel => ErrorCode::InvalidChannel,
579            Self::Other => ErrorCode::Other,
580        }
581    }
582}
583
584#[cfg(test)]
585mod tests {
586    use std::collections::VecDeque;
587
588    use super::*;
589
590    #[derive(Default)]
591    struct MockEndpoint {
592        incoming: VecDeque<EndpointEvent>,
593        outgoing: Vec<[u8; 64]>,
594    }
595
596    impl HidEndpoint for MockEndpoint {
597        fn read_event(&mut self) -> Result<Option<EndpointEvent>> {
598            Ok(self.incoming.pop_front())
599        }
600
601        fn write_packet(&mut self, packet: &[u8; 64]) -> Result<()> {
602            self.outgoing.push(*packet);
603            Ok(())
604        }
605    }
606
607    fn transport() -> (
608        TransportLoop<MockEndpoint>,
609        Receiver<CtapWork>,
610        SyncSender<CtapResult>,
611    ) {
612        let (work_tx, work_rx) = mpsc::sync_channel(1);
613        let (result_tx, result_rx) = mpsc::sync_channel(1);
614        (
615            TransportLoop::new(
616                MockEndpoint::default(),
617                PresenceGate::new(),
618                work_tx,
619                result_rx,
620            ),
621            work_rx,
622            result_tx,
623        )
624    }
625
626    fn process_message(transport: &mut TransportLoop<MockEndpoint>, message: &Message) {
627        for packet in fragment_message(message).unwrap() {
628            transport
629                .process_packet(Packet::from_bytes(packet))
630                .unwrap();
631        }
632    }
633
634    #[test]
635    fn ctaphid_fragmentation_round_trips_at_the_protocol_limit() {
636        let message = Message::new(
637            0xdead_beef,
638            Cmd::Cbor,
639            vec![0xa5; CTAPHID_MAX_MESSAGE_SIZE],
640            Some(CTAPHID_MAX_MESSAGE_SIZE),
641        );
642        let packets = fragment_message(&message)
643            .unwrap()
644            .into_iter()
645            .map(Packet::from_bytes)
646            .collect::<Vec<_>>();
647        assert_eq!(
648            Message::from_packets(&packets, Some(CTAPHID_MAX_MESSAGE_SIZE)).unwrap(),
649            message
650        );
651    }
652
653    #[test]
654    fn transport_errors_map_to_specific_ctaphid_codes() {
655        use soft_fido2_transport::ctaphid::ErrorCode;
656
657        assert_eq!(TransportError::InvalidLength.soft(), ErrorCode::InvalidLen);
658        assert_eq!(TransportError::ChannelBusy.soft(), ErrorCode::ChannelBusy);
659        assert_eq!(
660            TransportError::InvalidChannel.soft(),
661            ErrorCode::InvalidChannel
662        );
663    }
664
665    #[test]
666    fn broadcast_init_allocates_a_channel_and_echoes_the_nonce() {
667        let (mut transport, _, _) = transport();
668        let nonce = vec![0x5a; 8];
669        process_message(
670            &mut transport,
671            &Message::new(
672                CTAPHID_BROADCAST_CID,
673                Cmd::Init,
674                nonce.clone(),
675                Some(CTAPHID_MAX_MESSAGE_SIZE),
676            ),
677        );
678        let response = Message::from_packets(
679            &transport
680                .endpoint
681                .outgoing
682                .iter()
683                .copied()
684                .map(Packet::from_bytes)
685                .collect::<Vec<_>>(),
686            Some(CTAPHID_MAX_MESSAGE_SIZE),
687        )
688        .unwrap();
689
690        assert_eq!(response.cmd, Cmd::Init);
691        assert_eq!(&response.data[..8], nonce);
692        let channel = u32::from_be_bytes(response.data[8..12].try_into().unwrap());
693        assert!(transport.allocated.contains_key(&channel));
694        assert_eq!(
695            response.data[16],
696            CAPABILITY_WINK | CAPABILITY_CBOR | CAPABILITY_NMSG
697        );
698    }
699
700    #[test]
701    fn cancel_discards_the_eventual_cbor_worker_result() {
702        let (mut transport, work, results) = transport();
703        let channel = 0xdead_beef;
704        transport.allocated.insert(channel, Instant::now());
705        process_message(
706            &mut transport,
707            &Message::new(
708                channel,
709                Cmd::Cbor,
710                vec![0x04],
711                Some(CTAPHID_MAX_MESSAGE_SIZE),
712            ),
713        );
714        assert_eq!(work.recv().unwrap().channel, channel);
715        process_message(
716            &mut transport,
717            &Message::new(
718                channel,
719                Cmd::Cancel,
720                Vec::new(),
721                Some(CTAPHID_MAX_MESSAGE_SIZE),
722            ),
723        );
724        results
725            .send(CtapResult {
726                channel,
727                response: Ok(vec![0x00]),
728            })
729            .unwrap();
730        transport.process_result().unwrap();
731
732        assert!(transport.endpoint.outgoing.is_empty());
733        assert!(transport.active.is_none());
734        assert!(!transport.presence.has_pending_touch());
735    }
736
737    #[test]
738    fn oversized_initial_frame_is_rejected_before_assembly() {
739        let (mut transport, _, _) = transport();
740        let channel = 0xdead_beef;
741        transport.allocated.insert(channel, Instant::now());
742        let mut bytes = [0_u8; 64];
743        bytes[..4].copy_from_slice(&channel.to_be_bytes());
744        bytes[4] = Cmd::Cbor.to_u8_init();
745        bytes[5..7].copy_from_slice(&((CTAPHID_MAX_MESSAGE_SIZE + 1) as u16).to_be_bytes());
746
747        transport.process_packet(Packet::from_bytes(bytes)).unwrap();
748        let response = Message::from_packets(
749            &[Packet::from_bytes(transport.endpoint.outgoing[0])],
750            Some(CTAPHID_MAX_MESSAGE_SIZE),
751        )
752        .unwrap();
753        assert_eq!(response.cmd, Cmd::Error);
754        assert_eq!(
755            response.data,
756            vec![soft_fido2_transport::ctaphid::ErrorCode::InvalidLen as u8]
757        );
758    }
759
760    #[test]
761    fn channel_eviction_preserves_the_active_and_locked_channels() {
762        let (mut transport, _, _) = transport();
763        let now = Instant::now();
764        for channel in 1..=MAX_ALLOCATED_CHANNELS as u32 {
765            transport
766                .allocated
767                .insert(channel, now + Duration::from_millis(channel.into()));
768        }
769        transport.active = Some(ActiveCtap {
770            channel: 1,
771            cancelled: false,
772            last_keepalive: now,
773        });
774        transport.lock = Some(ChannelLock {
775            channel: 2,
776            expires: now + Duration::from_secs(10),
777        });
778
779        let allocated = transport.allocate_channel().unwrap();
780        assert!(transport.allocated.contains_key(&1));
781        assert!(transport.allocated.contains_key(&2));
782        assert!(!transport.allocated.contains_key(&3));
783        assert!(transport.allocated.contains_key(&allocated));
784        assert_eq!(transport.allocated.len(), MAX_ALLOCATED_CHANNELS);
785    }
786}