Skip to main content

netget/server/usb/smartcard/
handler.rs

1//! USB CCID interface handler — the `usbip::UsbInterfaceHandler` for a virtual card reader.
2//!
3//! `usbip` 0.9 ships handlers for HID and CDC only; there is no CCID handler, so the whole of
4//! this is written here against the CCID rev 1.1 specification.
5//!
6//! ## What is answered where
7//!
8//! `handle_urb` is **synchronous** — it cannot await an LLM call. So the split is:
9//!
10//! | Command | Answered by |
11//! |---|---|
12//! | `IccPowerOn` | here, from the ATR the handler configured at startup |
13//! | `IccPowerOff`, `GetSlotStatus`, `Abort`, `IccClock`, `T0APDU` | here |
14//! | `GetParameters`, `SetParameters`, `ResetParameters` | here, with the default T=0 block |
15//! | `Escape` | here, refused (`CMD_NOT_SUPPORTED`) |
16//! | `XfrBlock` (an APDU) | forwarded on a channel; `mod.rs` raises the event and queues the answer |
17//!
18//! Nothing about *what the card says* is decided here. The ATR and every response APDU come
19//! from the handler (script, static or LLM) via `mod.rs`.
20//!
21//! ## Bulk IN framing
22//!
23//! Responses are queued as whole CCID messages. A bulk IN URB takes at most
24//! `min(wMaxPacketSize, transfer_buffer_length)` bytes; a message longer than that is split
25//! and the remainder stays at the front of the queue, so a 64-byte-packet host reassembles
26//! it exactly as it would from real hardware. Two queued messages are never merged into one
27//! transfer, because a host parses one CCID message per transfer.
28
29use std::collections::VecDeque;
30use std::sync::{Arc, Mutex};
31use tokio::sync::mpsc;
32use tracing::{debug, trace, warn};
33
34use super::ccid::{self, CcidCommand};
35
36/// Card state shared between the connection task (which the handler cannot reach) and every
37/// live USB/IP session. One reader, one slot, one card: sessions deliberately share it.
38#[derive(Debug)]
39pub struct CardState {
40    /// Answer To Reset returned by `IccPowerOn`. Set by the `set_atr` action.
41    pub atr: Vec<u8>,
42    /// Whether a card is in the slot. Set by the `set_card_present` action.
43    pub card_present: bool,
44}
45
46/// An `XfrBlock` waiting for an answer from the handler.
47#[derive(Debug)]
48pub struct PendingApdu {
49    pub slot: u8,
50    pub seq: u8,
51    pub apdu: Vec<u8>,
52}
53
54/// The single slot this reader exposes.
55pub const SLOT: u8 = 0;
56
57/// A virtual USB CCID card reader.
58///
59/// `Debug` is required by `usbip::UsbInterfaceHandler` as of 0.9.
60#[derive(Debug)]
61pub struct UsbCcidHandler {
62    card: Arc<Mutex<CardState>>,
63    /// Whole CCID response messages waiting for a bulk IN URB.
64    tx_queue: VecDeque<Vec<u8>>,
65    /// `XfrBlock` payloads handed to the connection task, which raises the APDU event.
66    apdu_tx: mpsc::UnboundedSender<PendingApdu>,
67    /// Whether the host has powered the card up. `XfrBlock` before `IccPowerOn` is refused,
68    /// as a real reader refuses it.
69    powered: bool,
70    /// Set when the card is inserted or removed, cleared by the next interrupt IN URB.
71    slot_change_pending: bool,
72}
73
74impl UsbCcidHandler {
75    pub fn new(card: Arc<Mutex<CardState>>, apdu_tx: mpsc::UnboundedSender<PendingApdu>) -> Self {
76        Self {
77            card,
78            tx_queue: VecDeque::new(),
79            apdu_tx,
80            powered: false,
81            slot_change_pending: true,
82        }
83    }
84
85    /// The endpoints a CCID interface needs: interrupt IN for slot-change notifications and a
86    /// bulk pair for commands and responses. 64-byte packets, as a full-speed reader has.
87    pub fn endpoints() -> Vec<usbip::UsbEndpoint> {
88        vec![
89            usbip::UsbEndpoint {
90                address: 0x81, // interrupt IN
91                attributes: usbip::EndpointAttributes::Interrupt as u8,
92                max_packet_size: 8,
93                interval: 16,
94            },
95            usbip::UsbEndpoint {
96                address: 0x82, // bulk IN
97                attributes: usbip::EndpointAttributes::Bulk as u8,
98                max_packet_size: 64,
99                interval: 0,
100            },
101            usbip::UsbEndpoint {
102                address: 0x02, // bulk OUT
103                attributes: usbip::EndpointAttributes::Bulk as u8,
104                max_packet_size: 64,
105                interval: 0,
106            },
107        ]
108    }
109
110    /// Queue an `RDR_to_PC_DataBlock` carrying a response APDU. Called by the connection task
111    /// once the handler has answered.
112    pub fn queue_apdu_response(&mut self, slot: u8, seq: u8, response: &[u8]) {
113        let icc = self.icc_status();
114        match ccid::data_block(
115            slot,
116            seq,
117            icc,
118            ccid::command_status::PROCESSED,
119            ccid::error::NONE,
120            response,
121        ) {
122            Ok(message) => {
123                trace!(
124                    "CCID queueing RDR_to_PC_DataBlock seq={} with {} response byte(s)",
125                    seq,
126                    response.len()
127                );
128                self.tx_queue.push_back(message);
129            }
130            Err(e) => {
131                // The answer cannot be framed as one CCID message. Report a card failure
132                // rather than truncating it into something the host would misread.
133                warn!("CCID response for seq={} cannot be framed: {}", seq, e);
134                self.tx_queue.push_back(ccid::slot_status(
135                    slot,
136                    seq,
137                    icc,
138                    ccid::command_status::FAILED,
139                    ccid::error::ICC_MUTE,
140                ));
141            }
142        }
143    }
144
145    /// Note that the card was inserted or removed, so the next interrupt IN URB reports it.
146    /// Removing the card also drops the power state, exactly as pulling one out would.
147    pub fn note_slot_change(&mut self) {
148        self.slot_change_pending = true;
149        if !self.card_present() {
150            self.powered = false;
151        }
152    }
153
154    /// Number of CCID messages still waiting for a bulk IN URB. Used by diagnostics.
155    #[allow(dead_code)]
156    pub fn pending_responses(&self) -> usize {
157        self.tx_queue.len()
158    }
159
160    fn card_present(&self) -> bool {
161        match self.card.lock() {
162            Ok(card) => card.card_present,
163            Err(poisoned) => poisoned.into_inner().card_present,
164        }
165    }
166
167    fn atr(&self) -> Vec<u8> {
168        match self.card.lock() {
169            Ok(card) => card.atr.clone(),
170            Err(poisoned) => poisoned.into_inner().atr.clone(),
171        }
172    }
173
174    /// `bmICCStatus` for the current slot.
175    fn icc_status(&self) -> u8 {
176        if !self.card_present() {
177            ccid::icc_status::NOT_PRESENT
178        } else if self.powered {
179            ccid::icc_status::PRESENT_ACTIVE
180        } else {
181            ccid::icc_status::PRESENT_INACTIVE
182        }
183    }
184
185    /// Refuse a command, reporting the current slot state.
186    fn fail(&self, seq: u8, slot: u8, error: u8) -> Vec<u8> {
187        ccid::slot_status(
188            slot,
189            seq,
190            self.icc_status(),
191            ccid::command_status::FAILED,
192            error,
193        )
194    }
195
196    /// Handle one CCID message that arrived on the bulk OUT endpoint.
197    ///
198    /// Returns the response to queue, or `None` when the answer will come later (`XfrBlock`).
199    fn handle_ccid_command(&mut self, command: &CcidCommand) -> Option<Vec<u8>> {
200        let seq = command.seq;
201        let slot = command.slot;
202
203        // One slot only: anything else is addressed to hardware that does not exist.
204        if slot != SLOT {
205            warn!("CCID command for slot {} but only slot 0 exists", slot);
206            return Some(self.fail(seq, slot, ccid::error::ICC_MUTE));
207        }
208
209        match command.message_type {
210            ccid::command::ICC_POWER_ON => {
211                if !self.card_present() {
212                    debug!("CCID IccPowerOn refused: no card in the slot");
213                    return Some(self.fail(seq, slot, ccid::error::ICC_MUTE));
214                }
215                self.powered = true;
216                let atr = self.atr();
217                debug!("CCID IccPowerOn: answering with a {}-byte ATR", atr.len());
218                Some(
219                    ccid::data_block(
220                        slot,
221                        seq,
222                        ccid::icc_status::PRESENT_ACTIVE,
223                        ccid::command_status::PROCESSED,
224                        ccid::error::NONE,
225                        &atr,
226                    )
227                    .unwrap_or_else(|e| {
228                        // An ATR longer than a CCID message is a configuration error, not a
229                        // host error: refuse the power-on rather than send a bad frame.
230                        warn!("CCID ATR cannot be framed: {}", e);
231                        ccid::slot_status(
232                            slot,
233                            seq,
234                            ccid::icc_status::PRESENT_INACTIVE,
235                            ccid::command_status::FAILED,
236                            ccid::error::ICC_MUTE,
237                        )
238                    }),
239                )
240            }
241            ccid::command::ICC_POWER_OFF => {
242                self.powered = false;
243                debug!("CCID IccPowerOff");
244                Some(ccid::slot_status(
245                    slot,
246                    seq,
247                    self.icc_status(),
248                    ccid::command_status::PROCESSED,
249                    ccid::error::NONE,
250                ))
251            }
252            ccid::command::GET_SLOT_STATUS | ccid::command::ABORT | ccid::command::ICC_CLOCK => {
253                Some(ccid::slot_status(
254                    slot,
255                    seq,
256                    self.icc_status(),
257                    ccid::command_status::PROCESSED,
258                    ccid::error::NONE,
259                ))
260            }
261            ccid::command::T0_APDU => Some(ccid::slot_status(
262                slot,
263                seq,
264                self.icc_status(),
265                ccid::command_status::PROCESSED,
266                ccid::error::NONE,
267            )),
268            ccid::command::GET_PARAMETERS
269            | ccid::command::SET_PARAMETERS
270            | ccid::command::RESET_PARAMETERS => {
271                // The class descriptor advertises automatic parameter negotiation, so the
272                // reader always reports the ISO 7816-3 defaults rather than storing what the
273                // host asked for.
274                Some(ccid::parameters_t0(slot, seq, self.icc_status()))
275            }
276            ccid::command::ESCAPE => {
277                debug!("CCID Escape refused: no vendor-specific commands are supported");
278                Some(ccid::escape_unsupported(slot, seq, self.icc_status()))
279            }
280            ccid::command::XFR_BLOCK => {
281                if !self.card_present() {
282                    debug!("CCID XfrBlock refused: no card in the slot");
283                    return Some(self.fail(seq, slot, ccid::error::ICC_MUTE));
284                }
285                if !self.powered {
286                    debug!("CCID XfrBlock refused: card has not been powered on");
287                    return Some(self.fail(seq, slot, ccid::error::ICC_MUTE));
288                }
289                if command.data.is_empty() {
290                    debug!("CCID XfrBlock carried no APDU");
291                    return Some(self.fail(seq, slot, ccid::error::ICC_MUTE));
292                }
293
294                if self
295                    .apdu_tx
296                    .send(PendingApdu {
297                        slot,
298                        seq,
299                        apdu: command.data.clone(),
300                    })
301                    .is_err()
302                {
303                    // The connection task is gone, so nothing will ever answer this APDU.
304                    // Fail the command instead of leaving the host waiting on its own timeout.
305                    warn!("CCID XfrBlock dropped: the connection task has exited");
306                    return Some(self.fail(seq, slot, ccid::error::ICC_MUTE));
307                }
308                trace!(
309                    "CCID XfrBlock seq={} forwarded ({} APDU byte(s))",
310                    seq,
311                    command.data.len()
312                );
313                None
314            }
315            other => {
316                debug!("CCID message type {:#04x} is not supported", other);
317                Some(self.fail(seq, slot, ccid::error::CMD_NOT_SUPPORTED))
318            }
319        }
320    }
321
322    /// Handle a class-specific control transfer addressed to this interface.
323    fn handle_control(&mut self, setup: usbip::SetupPacket) -> Vec<u8> {
324        // CCID class requests: ABORT (0x01), GET_CLOCK_FREQUENCIES (0x02), GET_DATA_RATES
325        // (0x03). The class descriptor reports "default only" for both clock and data rate,
326        // so the two GET requests correctly return an empty list, and ABORT is acknowledged.
327        // Answering an unknown request with an error would abort the whole USB/IP session,
328        // so unknown requests are acknowledged empty too.
329        debug!(
330            "CCID control request type={:#04x} request={:#04x} value={:#06x}",
331            setup.request_type, setup.request, setup.value
332        );
333        Vec::new()
334    }
335}
336
337impl usbip::UsbInterfaceHandler for UsbCcidHandler {
338    fn handle_urb(
339        &mut self,
340        _interface: &usbip::UsbInterface,
341        ep: usbip::UsbEndpoint,
342        transfer_buffer_length: u32,
343        setup: usbip::SetupPacket,
344        req: &[u8],
345    ) -> std::result::Result<Vec<u8>, std::io::Error> {
346        if ep.is_ep0() {
347            return Ok(self.handle_control(setup));
348        }
349
350        match ep.direction() {
351            usbip::Direction::Out => {
352                if req.is_empty() {
353                    return Ok(Vec::new());
354                }
355                trace!("CCID <- {} ({} bytes)", hex::encode_upper(req), req.len());
356
357                match CcidCommand::parse(req) {
358                    Ok(command) => {
359                        trace!("CCID command {} seq={}", command.name(), command.seq);
360                        if let Some(response) = self.handle_ccid_command(&command) {
361                            self.tx_queue.push_back(response);
362                        }
363                    }
364                    Err(e) => {
365                        // A malformed message has no trustworthy bSeq, so there is nothing to
366                        // answer. Log it and drop it rather than guessing.
367                        warn!("CCID message from the host could not be parsed: {}", e);
368                    }
369                }
370                Ok(Vec::new())
371            }
372            usbip::Direction::In => {
373                if ep.attributes == usbip::EndpointAttributes::Interrupt as u8 {
374                    if self.slot_change_pending {
375                        self.slot_change_pending = false;
376                        let present = self.card_present();
377                        debug!("CCID NotifySlotChange: card_present={}", present);
378                        return Ok(ccid::notify_slot_change(present));
379                    }
380                    return Ok(Vec::new());
381                }
382
383                // Bulk IN: hand over the next queued message, split across URBs if the host's
384                // transfer is smaller than the message.
385                let limit = (ep.max_packet_size as usize).min(transfer_buffer_length as usize);
386                if limit == 0 {
387                    return Ok(Vec::new());
388                }
389                let Some(mut message) = self.tx_queue.pop_front() else {
390                    return Ok(Vec::new());
391                };
392                if message.len() > limit {
393                    let remainder = message.split_off(limit);
394                    self.tx_queue.push_front(remainder);
395                }
396                trace!(
397                    "CCID -> {} ({} bytes)",
398                    hex::encode_upper(&message),
399                    message.len()
400                );
401                Ok(message)
402            }
403        }
404    }
405
406    fn get_class_specific_descriptor(&self) -> Vec<u8> {
407        ccid::class_descriptor()
408    }
409
410    fn as_any(&mut self) -> &mut dyn std::any::Any {
411        self
412    }
413}