Skip to main content

netget/server/nfc/
apdu.rs

1//! ISO 7816-4 command APDU parsing for the virtual NFC Forum Type 4 tag.
2//!
3//! An NFC Type 4 tag speaks ISO 7816-4 APDUs over ISO-DEP, so the same parser
4//! that a contact smart card needs applies verbatim to a contactless tag.
5//!
6//! ## Command APDU layout
7//!
8//! ```text
9//! | CLA | INS | P1 | P2 | [Lc] | [Data] | [Le] |
10//! ```
11//!
12//! Both the short form (1-byte Lc/Le) and the extended form (`00` marker plus a
13//! 2-byte Lc/Le) are accepted.
14//!
15//! ## Hostile input
16//!
17//! Every field these bytes come from is attacker-controlled: the reader on the
18//! other end of the socket writes them. Nothing here indexes without first
19//! checking the length, and nothing panics — a malformed APDU is an `Err` that
20//! the caller answers with a status word, never an unwind inside a connection
21//! task (which would be silent while the server still reported `Running`).
22
23use anyhow::{bail, Result};
24
25/// ISO 7816-4 instruction bytes the tag can name in an event.
26pub mod ins {
27    pub const SELECT: u8 = 0xA4;
28    pub const READ_BINARY: u8 = 0xB0;
29    pub const READ_RECORD: u8 = 0xB2;
30    pub const UPDATE_BINARY: u8 = 0xD6;
31    pub const UPDATE_RECORD: u8 = 0xDC;
32    pub const VERIFY: u8 = 0x20;
33    pub const GET_RESPONSE: u8 = 0xC0;
34    pub const GET_DATA: u8 = 0xCA;
35    pub const PUT_DATA: u8 = 0xDA;
36    pub const ENVELOPE: u8 = 0xC2;
37    pub const INTERNAL_AUTHENTICATE: u8 = 0x88;
38    pub const EXTERNAL_AUTHENTICATE: u8 = 0x82;
39    pub const GET_CHALLENGE: u8 = 0x84;
40}
41
42/// `P1` value of SELECT meaning "select by DF name", i.e. by application ID.
43pub const SELECT_P1_BY_AID: u8 = 0x04;
44
45/// Status word returned when the handler produced no usable response.
46///
47/// `6F00` is ISO 7816-4 "no precise diagnosis": a card-side failure. It is the
48/// deliberate fail-*closed* answer, and is structurally distinct from a status
49/// word the model chose itself (`6982`, `6A82`, …), so an LLM outage can never
50/// be mistaken for the model approving something.
51pub const SW_NO_PRECISE_DIAGNOSIS: (u8, u8) = (0x6F, 0x00);
52
53/// Status word for an APDU that could not be parsed at all.
54pub const SW_WRONG_LENGTH: (u8, u8) = (0x67, 0x00);
55
56/// Status word returned when the backend is saturated and a retry may succeed.
57///
58/// `6400` is ISO 7816-4 "execution error, state of non-volatile memory unchanged":
59/// the card did not do anything, and the reader may send the command again. It is
60/// kept distinct from [`SW_NO_PRECISE_DIAGNOSIS`] so a transient overload is not
61/// recorded by the reader as a permanent card fault — the same reason HTTP answers
62/// 503 rather than 500.
63pub const SW_EXECUTION_ERROR_STATE_UNCHANGED: (u8, u8) = (0x64, 0x00);
64
65/// A parsed ISO 7816-4 command APDU.
66#[derive(Debug, Clone)]
67pub struct ApduCommand {
68    pub cla: u8,
69    pub ins: u8,
70    pub p1: u8,
71    pub p2: u8,
72    /// Command data field (`Lc` bytes). Empty for case 1 and case 2 APDUs.
73    pub data: Vec<u8>,
74    /// Expected response length. `None` when the command carries no `Le`.
75    /// A wire value of `00` means the maximum (256 short / 65536 extended).
76    pub le: Option<u32>,
77}
78
79impl ApduCommand {
80    /// Parse a command APDU, rejecting every truncated or inconsistent form.
81    pub fn parse(bytes: &[u8]) -> Result<Self> {
82        if bytes.len() < 4 {
83            bail!("APDU too short: {} byte(s), need at least 4", bytes.len());
84        }
85
86        let cla = bytes[0];
87        let ins = bytes[1];
88        let p1 = bytes[2];
89        let p2 = bytes[3];
90        let body = &bytes[4..];
91
92        let (data, le) = match body.len() {
93            // Case 1: header only.
94            0 => (Vec::new(), None),
95            // Case 2S: a single Le byte.
96            1 => (Vec::new(), Some(expand_le_short(body[0]))),
97            _ if body[0] != 0 => {
98                // Short form with a data field.
99                let lc = body[0] as usize;
100                let data_end = 1 + lc;
101                if body.len() < data_end {
102                    bail!(
103                        "APDU truncated: Lc={} but only {} byte(s) follow",
104                        lc,
105                        body.len() - 1
106                    );
107                }
108                let data = body[1..data_end].to_vec();
109                match body.len() - data_end {
110                    0 => (data, None),
111                    1 => (data, Some(expand_le_short(body[data_end]))),
112                    extra => bail!("APDU has {} trailing byte(s) after Lc/Data/Le", extra),
113                }
114            }
115            // Extended form: leading 00 marker followed by a 2-byte length.
116            _ => {
117                if body.len() < 3 {
118                    bail!(
119                        "APDU truncated: extended-length marker needs 3 byte(s), got {}",
120                        body.len()
121                    );
122                }
123                let extended = u16::from_be_bytes([body[1], body[2]]);
124                if body.len() == 3 {
125                    // Case 2E: extended Le only.
126                    return Ok(Self {
127                        cla,
128                        ins,
129                        p1,
130                        p2,
131                        data: Vec::new(),
132                        le: Some(expand_le_extended(extended)),
133                    });
134                }
135                let lc = extended as usize;
136                if lc == 0 {
137                    bail!("APDU has an extended-length marker with Lc=0 and trailing bytes");
138                }
139                let data_end = 3 + lc;
140                if body.len() < data_end {
141                    bail!(
142                        "APDU truncated: extended Lc={} but only {} byte(s) follow",
143                        lc,
144                        body.len() - 3
145                    );
146                }
147                let data = body[3..data_end].to_vec();
148                match body.len() - data_end {
149                    0 => (data, None),
150                    2 => (
151                        data,
152                        Some(expand_le_extended(u16::from_be_bytes([
153                            body[data_end],
154                            body[data_end + 1],
155                        ]))),
156                    ),
157                    extra => bail!(
158                        "APDU has {} trailing byte(s) after extended Lc/Data/Le",
159                        extra
160                    ),
161                }
162            }
163        };
164
165        Ok(Self {
166            cla,
167            ins,
168            p1,
169            p2,
170            data,
171            le,
172        })
173    }
174
175    /// Human-readable instruction name, so the model is not asked to memorise
176    /// instruction bytes.
177    pub fn ins_name(&self) -> &'static str {
178        match self.ins {
179            ins::SELECT => "SELECT",
180            ins::READ_BINARY => "READ_BINARY",
181            ins::READ_RECORD => "READ_RECORD",
182            ins::UPDATE_BINARY => "UPDATE_BINARY",
183            ins::UPDATE_RECORD => "UPDATE_RECORD",
184            ins::VERIFY => "VERIFY",
185            ins::GET_RESPONSE => "GET_RESPONSE",
186            ins::GET_DATA => "GET_DATA",
187            ins::PUT_DATA => "PUT_DATA",
188            ins::ENVELOPE => "ENVELOPE",
189            ins::INTERNAL_AUTHENTICATE => "INTERNAL_AUTHENTICATE",
190            ins::EXTERNAL_AUTHENTICATE => "EXTERNAL_AUTHENTICATE",
191            ins::GET_CHALLENGE => "GET_CHALLENGE",
192            _ => "UNKNOWN",
193        }
194    }
195
196    /// True for `SELECT` by DF name (AID) — the command a reader uses to pick
197    /// an application on the tag. Raised as `nfc_tag_selected`; every other
198    /// APDU, including SELECT by file identifier, is `nfc_apdu_received`.
199    pub fn is_select_by_aid(&self) -> bool {
200        self.ins == ins::SELECT && self.p1 == SELECT_P1_BY_AID && !self.data.is_empty()
201    }
202}
203
204/// A short `Le` of `00` means 256 bytes, not zero.
205fn expand_le_short(value: u8) -> u32 {
206    if value == 0 {
207        256
208    } else {
209        value as u32
210    }
211}
212
213/// An extended `Le` of `0000` means 65536 bytes, not zero.
214fn expand_le_extended(value: u16) -> u32 {
215    if value == 0 {
216        65536
217    } else {
218        value as u32
219    }
220}
221
222/// A response APDU: optional data followed by the two status bytes.
223#[derive(Debug, Clone)]
224pub struct ApduResponse {
225    pub data: Vec<u8>,
226    pub sw1: u8,
227    pub sw2: u8,
228}
229
230impl ApduResponse {
231    pub fn new(data: Vec<u8>, sw1: u8, sw2: u8) -> Self {
232        Self { data, sw1, sw2 }
233    }
234
235    /// The fail-closed response used when no handler produced one.
236    pub fn card_error() -> Self {
237        Self::new(
238            Vec::new(),
239            SW_NO_PRECISE_DIAGNOSIS.0,
240            SW_NO_PRECISE_DIAGNOSIS.1,
241        )
242    }
243
244    /// The fail-closed response for an internal (backend) failure.
245    ///
246    /// The two [`crate::utils::WireFailure`] categories map onto two different status
247    /// words — `6400` for a saturated backend (retryable), `6F00` for everything else —
248    /// so the reader can tell "try again" from "this card is broken". Nothing derived
249    /// from the error itself reaches the wire: the status word is a category, and an
250    /// APDU response carries no free-text field to leak one into.
251    pub fn for_wire_failure(failure: crate::utils::WireFailure) -> Self {
252        let (sw1, sw2) = match failure {
253            crate::utils::WireFailure::Overloaded => SW_EXECUTION_ERROR_STATE_UNCHANGED,
254            crate::utils::WireFailure::Unavailable => SW_NO_PRECISE_DIAGNOSIS,
255        };
256        Self::new(Vec::new(), sw1, sw2)
257    }
258
259    pub fn status_word(&self) -> String {
260        format!("{:02X}{:02X}", self.sw1, self.sw2)
261    }
262
263    pub fn into_bytes(self) -> Vec<u8> {
264        let mut bytes = self.data;
265        bytes.push(self.sw1);
266        bytes.push(self.sw2);
267        bytes
268    }
269}