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}