Skip to main content

netget/server/usb/smartcard/
ccid.rs

1//! USB CCID (Chip Card Interface Device, USB class 0x0B) message layer.
2//!
3//! CCID rev 1.1 defines a fixed 10-byte header followed by `dwLength` bytes of payload:
4//!
5//! ```text
6//! | 0        | 1..5             | 5     | 6    | 7 | 8 | 9 | 10.. |
7//! | bMessage | dwLength (LE)    | bSlot | bSeq | message-specific | abData |
8//! ```
9//!
10//! Every `PC_to_RDR_*` command is answered by exactly one `RDR_to_PC_*` response carrying
11//! the **same `bSeq`**, which is how the host pairs them up.
12//!
13//! ## Hostile input
14//!
15//! Everything parsed here arrives on a bulk OUT endpoint from whoever attached over USB/IP.
16//! `CcidCommand::parse` bounds-checks the header, refuses a `dwLength` larger than the
17//! `dwMaxCCIDMessageLength` this reader advertises, and refuses a truncated payload. Nothing
18//! indexes without checking first and nothing panics: a panic inside a URB callback would be
19//! silent while the server still reported `Running`.
20
21use anyhow::{bail, Result};
22
23/// PC_to_RDR message types (host → reader).
24pub mod command {
25    pub const ICC_POWER_ON: u8 = 0x62;
26    pub const ICC_POWER_OFF: u8 = 0x63;
27    pub const GET_SLOT_STATUS: u8 = 0x65;
28    pub const XFR_BLOCK: u8 = 0x6F;
29    pub const GET_PARAMETERS: u8 = 0x6C;
30    pub const RESET_PARAMETERS: u8 = 0x6D;
31    pub const SET_PARAMETERS: u8 = 0x61;
32    pub const ESCAPE: u8 = 0x6B;
33    pub const ICC_CLOCK: u8 = 0x6E;
34    pub const T0_APDU: u8 = 0x6A;
35    pub const ABORT: u8 = 0x72;
36    pub const SET_DATA_RATE_AND_CLOCK_FREQUENCY: u8 = 0x73;
37}
38
39/// RDR_to_PC message types (reader → host).
40pub mod response {
41    pub const DATA_BLOCK: u8 = 0x80;
42    pub const SLOT_STATUS: u8 = 0x81;
43    pub const PARAMETERS: u8 = 0x82;
44    pub const ESCAPE: u8 = 0x83;
45    pub const DATA_RATE_AND_CLOCK_FREQUENCY: u8 = 0x84;
46    /// Interrupt-endpoint notification of a slot state change.
47    pub const NOTIFY_SLOT_CHANGE: u8 = 0x50;
48}
49
50/// `bmICCStatus`, the low two bits of `bStatus`.
51pub mod icc_status {
52    pub const PRESENT_ACTIVE: u8 = 0;
53    pub const PRESENT_INACTIVE: u8 = 1;
54    pub const NOT_PRESENT: u8 = 2;
55}
56
57/// `bmCommandStatus`, the high two bits of `bStatus`.
58pub mod command_status {
59    pub const PROCESSED: u8 = 0;
60    pub const FAILED: u8 = 1;
61}
62
63/// `bError` values used by this reader (CCID rev 1.1 table 6.2-2).
64pub mod error {
65    /// The command in `bMessageType` is not supported.
66    pub const CMD_NOT_SUPPORTED: u8 = 0x00;
67    /// No answer from the card — used here for "no card" and "card not powered".
68    pub const ICC_MUTE: u8 = 0xFE;
69    /// No error.
70    pub const NONE: u8 = 0x00;
71}
72
73/// Fixed CCID header length.
74pub const HEADER_LEN: usize = 10;
75
76/// `dwMaxCCIDMessageLength` advertised in the class descriptor, and therefore the largest
77/// message this reader will parse or emit. 271 = 10-byte header + 261-byte short APDU, the
78/// conventional value for a short-APDU-level reader.
79pub const MAX_MESSAGE_LEN: usize = 271;
80
81/// Largest `abData` payload that fits in `MAX_MESSAGE_LEN`.
82pub const MAX_PAYLOAD_LEN: usize = MAX_MESSAGE_LEN - HEADER_LEN;
83
84/// Compose `bStatus` from the ICC status and the command status.
85pub fn status_byte(icc: u8, command: u8) -> u8 {
86    (icc & 0x03) | ((command & 0x03) << 6)
87}
88
89/// A parsed `PC_to_RDR_*` message.
90#[derive(Debug, Clone)]
91pub struct CcidCommand {
92    pub message_type: u8,
93    pub slot: u8,
94    /// Sequence number; the response must echo it.
95    pub seq: u8,
96    /// Bytes 7, 8 and 9 — message-specific (`bBWI`/`wLevelParameter` for XfrBlock,
97    /// `bPowerSelect` for IccPowerOn, `bProtocolNum` for SetParameters, …).
98    pub params: [u8; 3],
99    pub data: Vec<u8>,
100}
101
102impl CcidCommand {
103    /// Parse one CCID message, rejecting every truncated or oversized form.
104    pub fn parse(bytes: &[u8]) -> Result<Self> {
105        if bytes.len() < HEADER_LEN {
106            bail!(
107                "CCID message too short: {} byte(s), need at least {}",
108                bytes.len(),
109                HEADER_LEN
110            );
111        }
112
113        let declared = u32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]) as usize;
114        if declared > MAX_PAYLOAD_LEN {
115            bail!(
116                "CCID dwLength={} exceeds the advertised maximum payload of {} byte(s)",
117                declared,
118                MAX_PAYLOAD_LEN
119            );
120        }
121        if bytes.len() < HEADER_LEN + declared {
122            bail!(
123                "CCID message truncated: dwLength={} but only {} payload byte(s) present",
124                declared,
125                bytes.len() - HEADER_LEN
126            );
127        }
128
129        Ok(Self {
130            message_type: bytes[0],
131            slot: bytes[5],
132            seq: bytes[6],
133            params: [bytes[7], bytes[8], bytes[9]],
134            data: bytes[HEADER_LEN..HEADER_LEN + declared].to_vec(),
135        })
136    }
137
138    /// Human-readable message name, for logs and status lines.
139    pub fn name(&self) -> &'static str {
140        match self.message_type {
141            command::ICC_POWER_ON => "PC_to_RDR_IccPowerOn",
142            command::ICC_POWER_OFF => "PC_to_RDR_IccPowerOff",
143            command::GET_SLOT_STATUS => "PC_to_RDR_GetSlotStatus",
144            command::XFR_BLOCK => "PC_to_RDR_XfrBlock",
145            command::GET_PARAMETERS => "PC_to_RDR_GetParameters",
146            command::RESET_PARAMETERS => "PC_to_RDR_ResetParameters",
147            command::SET_PARAMETERS => "PC_to_RDR_SetParameters",
148            command::ESCAPE => "PC_to_RDR_Escape",
149            command::ICC_CLOCK => "PC_to_RDR_IccClock",
150            command::T0_APDU => "PC_to_RDR_T0APDU",
151            command::ABORT => "PC_to_RDR_Abort",
152            command::SET_DATA_RATE_AND_CLOCK_FREQUENCY => "PC_to_RDR_SetDataRateAndClockFrequency",
153            _ => "PC_to_RDR_Unknown",
154        }
155    }
156}
157
158/// Build a CCID message with the standard 10-byte header.
159///
160/// A payload longer than `MAX_PAYLOAD_LEN` cannot be framed as a single message this reader
161/// claims to support, so it is refused rather than silently truncated.
162fn build(message_type: u8, slot: u8, seq: u8, params: [u8; 3], payload: &[u8]) -> Result<Vec<u8>> {
163    if payload.len() > MAX_PAYLOAD_LEN {
164        bail!(
165            "CCID payload of {} byte(s) exceeds dwMaxCCIDMessageLength ({} byte(s) of payload)",
166            payload.len(),
167            MAX_PAYLOAD_LEN
168        );
169    }
170    let mut message = Vec::with_capacity(HEADER_LEN + payload.len());
171    message.push(message_type);
172    message.extend_from_slice(&(payload.len() as u32).to_le_bytes());
173    message.push(slot);
174    message.push(seq);
175    message.extend_from_slice(&params);
176    message.extend_from_slice(payload);
177    Ok(message)
178}
179
180/// `RDR_to_PC_DataBlock` — the answer to IccPowerOn (carrying the ATR) and to XfrBlock
181/// (carrying the response APDU).
182pub fn data_block(slot: u8, seq: u8, icc: u8, cmd: u8, err: u8, payload: &[u8]) -> Result<Vec<u8>> {
183    build(
184        response::DATA_BLOCK,
185        slot,
186        seq,
187        [status_byte(icc, cmd), err, 0x00],
188        payload,
189    )
190}
191
192/// `RDR_to_PC_SlotStatus` — the answer to GetSlotStatus, IccPowerOff, Abort, and to any
193/// command that failed.
194pub fn slot_status(slot: u8, seq: u8, icc: u8, cmd: u8, err: u8) -> Vec<u8> {
195    // A slot status message has no payload, so `build` cannot exceed the maximum.
196    build(
197        response::SLOT_STATUS,
198        slot,
199        seq,
200        [status_byte(icc, cmd), err, 0x00],
201        &[],
202    )
203    .expect("slot status has an empty payload")
204}
205
206/// `RDR_to_PC_Parameters` carrying the default T=0 parameter block.
207pub fn parameters_t0(slot: u8, seq: u8, icc: u8) -> Vec<u8> {
208    // ISO 7816-3 defaults: Fi/Di = 0x11 (372/1), no convention change, guard time 0,
209    // WI = 10, clock stop not supported.
210    const T0_PARAMETERS: [u8; 5] = [0x11, 0x00, 0x00, 0x0A, 0x00];
211    build(
212        response::PARAMETERS,
213        slot,
214        seq,
215        [
216            status_byte(icc, command_status::PROCESSED),
217            error::NONE,
218            0x00,
219        ],
220        &T0_PARAMETERS,
221    )
222    .expect("T=0 parameter block is 5 bytes")
223}
224
225/// `RDR_to_PC_Escape` refusing a vendor-specific command.
226pub fn escape_unsupported(slot: u8, seq: u8, icc: u8) -> Vec<u8> {
227    build(
228        response::ESCAPE,
229        slot,
230        seq,
231        [
232            status_byte(icc, command_status::FAILED),
233            error::CMD_NOT_SUPPORTED,
234            0x00,
235        ],
236        &[],
237    )
238    .expect("escape response has an empty payload")
239}
240
241/// `RDR_to_PC_NotifySlotChange` for the single slot this reader has.
242///
243/// Two bytes: the message type and a bitmap in which bit 0 is the slot's current state
244/// (1 = card present) and bit 1 marks that the state changed since the last notification.
245pub fn notify_slot_change(card_present: bool) -> Vec<u8> {
246    let mut bitmap = 0b10; // "changed" flag for slot 0
247    if card_present {
248        bitmap |= 0b01;
249    }
250    vec![response::NOTIFY_SLOT_CHANGE, bitmap]
251}
252
253/// The CCID class-specific descriptor (bDescriptorType 0x21), inserted between the interface
254/// descriptor and the endpoint descriptors. This is what tells the host driver that the
255/// interface is a card reader and what it can do.
256pub fn class_descriptor() -> Vec<u8> {
257    let mut descriptor = Vec::with_capacity(54);
258    descriptor.push(54); // bLength
259    descriptor.push(0x21); // bDescriptorType (CCID)
260    descriptor.extend_from_slice(&0x0110u16.to_le_bytes()); // bcdCCID 1.10
261    descriptor.push(0x00); // bMaxSlotIndex: one slot
262    descriptor.push(0x07); // bVoltageSupport: 5.0V, 3.0V, 1.8V
263    descriptor.extend_from_slice(&0x0000_0003u32.to_le_bytes()); // dwProtocols: T=0 and T=1
264    descriptor.extend_from_slice(&3_580u32.to_le_bytes()); // dwDefaultClock (kHz)
265    descriptor.extend_from_slice(&3_580u32.to_le_bytes()); // dwMaximumClock (kHz)
266    descriptor.push(0x00); // bNumClockSupported: default only
267    descriptor.extend_from_slice(&9_600u32.to_le_bytes()); // dwDataRate (bps)
268    descriptor.extend_from_slice(&9_600u32.to_le_bytes()); // dwMaxDataRate (bps)
269    descriptor.push(0x00); // bNumDataRatesSupported: default only
270    descriptor.extend_from_slice(&254u32.to_le_bytes()); // dwMaxIFSD
271    descriptor.extend_from_slice(&0u32.to_le_bytes()); // dwSynchProtocols: none
272    descriptor.extend_from_slice(&0u32.to_le_bytes()); // dwMechanical: no special features
273                                                       // dwFeatures 0x000204BA: automatic parameter/voltage/frequency/baud/PPS/IFSD handling
274                                                       // plus 0x00020000, "short APDU level exchange" — the host hands us whole APDUs, which
275                                                       // is exactly the granularity the handler answers at.
276    descriptor.extend_from_slice(&0x0002_04BAu32.to_le_bytes());
277    descriptor.extend_from_slice(&(MAX_MESSAGE_LEN as u32).to_le_bytes()); // dwMaxCCIDMessageLength
278    descriptor.push(0x00); // bClassGetResponse: echo
279    descriptor.push(0x00); // bClassEnvelope: echo
280    descriptor.extend_from_slice(&0u16.to_le_bytes()); // wLcdLayout: no display
281    descriptor.push(0x00); // bPINSupport: no PIN pad
282    descriptor.push(0x01); // bMaxCCIDBusySlots
283    descriptor
284}