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}