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}