openpgp_card/ocard/data/
historical.rs

1// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! 6 Historical Bytes
5
6use std::convert::TryFrom;
7
8use crate::ocard::data::{CardCapabilities, CardServiceData, HistoricalBytes};
9use crate::Error;
10
11impl CardCapabilities {
12    pub fn command_chaining(&self) -> bool {
13        self.command_chaining
14    }
15
16    pub fn extended_lc_le(&self) -> bool {
17        self.extended_lc_le
18    }
19
20    pub fn extended_length_information(&self) -> bool {
21        self.extended_length_information
22    }
23}
24
25impl From<[u8; 3]> for CardCapabilities {
26    fn from(data: [u8; 3]) -> Self {
27        let byte3 = data[2];
28
29        let command_chaining = byte3 & 0x80 != 0;
30        let extended_lc_le = byte3 & 0x40 != 0;
31        let extended_length_information = byte3 & 0x20 != 0;
32
33        Self {
34            command_chaining,
35            extended_lc_le,
36            extended_length_information,
37        }
38    }
39}
40
41impl From<u8> for CardServiceData {
42    fn from(data: u8) -> Self {
43        let select_by_full_df_name = data & 0x80 != 0;
44        let select_by_partial_df_name = data & 0x40 != 0;
45        let dos_available_in_ef_dir = data & 0x20 != 0;
46        let dos_available_in_ef_atr_info = data & 0x10 != 0;
47        let access_services = [data & 0x8 != 0, data & 0x4 != 0, data & 0x2 != 0];
48        let mf = data & 0x1 != 0;
49
50        Self {
51            select_by_full_df_name,
52            select_by_partial_df_name,
53            dos_available_in_ef_dir,
54            dos_available_in_ef_atr_info,
55            access_services,
56            mf,
57        }
58    }
59}
60
61/// Split a compact-tlv "TL" (tag + length) into a 4-bit 'tag' and 4-bit
62/// 'length'.
63///
64/// The COMPACT-TLV format has a Tag in the first nibble of a byte (bit
65/// 5-8) and a length in the second nibble (bit 1-4).
66fn split_tl(tl: u8) -> (u8, u8) {
67    let tag = (tl & 0xf0) >> 4;
68    let len = tl & 0x0f;
69
70    (tag, len)
71}
72
73impl HistoricalBytes {
74    pub fn card_capabilities(&self) -> Option<&CardCapabilities> {
75        self.cc.as_ref()
76    }
77
78    pub fn card_service_data(&self) -> Option<&CardServiceData> {
79        self.csd.as_ref()
80    }
81}
82
83impl TryFrom<&[u8]> for HistoricalBytes {
84    type Error = crate::Error;
85
86    fn try_from(mut data: &[u8]) -> Result<Self, Self::Error> {
87        // workaround-hack for "ledger" with zero-padded historical bytes
88        #[allow(clippy::expect_used)] // the suffix strip expect is ok
89        if data.ends_with(&[0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]) {
90            data = data
91                .strip_suffix(&[0x0, 0x0, 0x0, 0x0, 0x0])
92                .expect("checked");
93        }
94
95        // workaround-hack for "ledger": fix status indicator byte 7
96        if data == [0x0, 0x31, 0xc5, 0x73, 0xc0, 0x1, 0x80, 0x7, 0x90, 0x0] {
97            data = &[0x0, 0x31, 0xc5, 0x73, 0xc0, 0x1, 0x80, 0x5, 0x90, 0x0];
98        }
99
100        let len = data.len();
101
102        if len < 4 {
103            // historical bytes cannot be this short
104
105            return Err(Error::ParseError(format!(
106                "Historical bytes too short ({len} bytes), must be >= 4"
107            )));
108        }
109
110        if data[0] != 0 {
111            // The OpenPGP application assumes a category indicator byte
112            // set to '00' (o-card 3.4.1, pg 44)
113
114            return Err(Error::ParseError(
115                "Unexpected category indicator in historical bytes".into(),
116            ));
117        }
118
119        // category indicator byte
120        let cib = data[0];
121
122        // Card service data byte
123        let mut csd = None;
124
125        // Card capabilities
126        let mut cc = None;
127
128        // get information from "COMPACT-TLV data objects" [ISO 12.1.1.2]
129        let mut ctlv = data[1..len - 3].to_vec();
130        while !ctlv.is_empty() {
131            let (t, l) = split_tl(ctlv[0]);
132
133            // ctlv must still contain at least 1 + l bytes
134            // (1 byte for the tl, plus `l` bytes of data for this ctlv)
135            // (e.g. len = 4 -> tl + 3byte data)
136            if ctlv.len() < (1 + l as usize) {
137                return Err(Error::ParseError(format!(
138                    "Illegal length value in Historical Bytes TL {} len {} l {}",
139                    ctlv[0],
140                    ctlv.len(),
141                    l
142                )));
143            }
144
145            match (t, l) {
146                (0x3, 0x1) => {
147                    csd = Some(ctlv[1]);
148                    ctlv.drain(0..2);
149                }
150                (0x7, 0x3) => {
151                    cc = Some([ctlv[1], ctlv[2], ctlv[3]]);
152                    ctlv.drain(0..4);
153                }
154                (_, _) => {
155                    // Log other unexpected CTLV entries.
156
157                    // (e.g. yubikey neo returns historical bytes as:
158                    // "[0, 73, 0, 0, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]")
159                    log::trace!("historical bytes: ignored (tag {}, len {})", t, l);
160                    ctlv.drain(0..(l as usize + 1));
161                }
162            }
163        }
164
165        // status indicator byte
166        let sib = match data[len - 3] {
167            0 => {
168                // Card does not offer life cycle management, commands
169                // TERMINATE DF and ACTIVATE FILE are not supported
170                0
171            }
172            3 => {
173                // Initialisation state
174                // OpenPGP application can be reset to default values with
175                // an ACTIVATE FILE command
176                3
177            }
178            5 => {
179                // Operational state (activated)
180                // Card supports life cycle management, commands TERMINATE
181                // DF and ACTIVATE FILE are available
182                5
183            }
184            _ => {
185                return Err(Error::ParseError(
186                    "unexpected status indicator in historical bytes".into(),
187                ));
188            }
189        };
190
191        // Ignore final two (status) bytes:
192        // according to the spec, they 'normally' show [0x90, 0x0] - but
193        // YubiKey Neo shows [0x0, 0x0].
194        // It's unclear if these status bytes are ever useful to process?
195
196        let cc = cc.map(CardCapabilities::from);
197        let csd = csd.map(CardServiceData::from);
198
199        Ok(Self { cib, csd, cc, sib })
200    }
201}
202
203#[cfg(test)]
204mod test {
205    use std::convert::TryInto;
206
207    use super::*;
208
209    #[test]
210    fn test_split_tl() {
211        assert_eq!(split_tl(0x31), (3, 1));
212        assert_eq!(split_tl(0x73), (7, 3));
213        assert_eq!(split_tl(0x00), (0, 0));
214        assert_eq!(split_tl(0xff), (0xf, 0xf));
215    }
216
217    #[test]
218    fn test_gnuk() -> Result<(), Error> {
219        // gnuk 1.2 stable
220        let data: &[u8] = &[0x0, 0x31, 0x84, 0x73, 0x80, 0x1, 0x80, 0x5, 0x90, 0x0];
221        let hist: HistoricalBytes = data.try_into()?;
222
223        assert_eq!(
224            hist,
225            HistoricalBytes {
226                cib: 0,
227                csd: Some(CardServiceData {
228                    select_by_full_df_name: true,
229                    select_by_partial_df_name: false,
230                    dos_available_in_ef_dir: false,
231                    dos_available_in_ef_atr_info: false,
232                    access_services: [false, true, false,],
233                    mf: false,
234                }),
235                cc: Some(CardCapabilities {
236                    command_chaining: true,
237                    extended_lc_le: false,
238                    extended_length_information: false,
239                }),
240                sib: 5
241            }
242        );
243
244        Ok(())
245    }
246
247    #[test]
248    fn test_floss34() -> Result<(), Error> {
249        // floss shop openpgp smartcard 3.4
250        let data: &[u8] = &[0x0, 0x31, 0xf5, 0x73, 0xc0, 0x1, 0x60, 0x5, 0x90, 0x0];
251        let hist: HistoricalBytes = data.try_into()?;
252
253        assert_eq!(
254            hist,
255            HistoricalBytes {
256                cib: 0,
257                csd: Some(CardServiceData {
258                    select_by_full_df_name: true,
259                    select_by_partial_df_name: true,
260                    dos_available_in_ef_dir: true,
261                    dos_available_in_ef_atr_info: true,
262                    access_services: [false, true, false,],
263                    mf: true,
264                },),
265                cc: Some(CardCapabilities {
266                    command_chaining: false,
267                    extended_lc_le: true,
268                    extended_length_information: true,
269                },),
270                sib: 5,
271            }
272        );
273
274        Ok(())
275    }
276
277    #[test]
278    fn test_yk5() -> Result<(), Error> {
279        // yubikey 5
280        let data: &[u8] = &[0x0, 0x73, 0x0, 0x0, 0xe0, 0x5, 0x90, 0x0];
281        let hist: HistoricalBytes = data.try_into()?;
282
283        assert_eq!(
284            hist,
285            HistoricalBytes {
286                cib: 0,
287                csd: None,
288                cc: Some(CardCapabilities {
289                    command_chaining: true,
290                    extended_lc_le: true,
291                    extended_length_information: true,
292                },),
293                sib: 5,
294            }
295        );
296
297        Ok(())
298    }
299
300    #[test]
301    fn test_yk4() -> Result<(), Error> {
302        // yubikey 4
303        let data: &[u8] = &[0x0, 0x73, 0x0, 0x0, 0x80, 0x5, 0x90, 0x0];
304        let hist: HistoricalBytes = data.try_into()?;
305
306        assert_eq!(
307            hist,
308            HistoricalBytes {
309                cib: 0,
310                csd: None,
311                cc: Some(CardCapabilities {
312                    command_chaining: true,
313                    extended_lc_le: false,
314                    extended_length_information: false,
315                },),
316                sib: 5,
317            }
318        );
319
320        Ok(())
321    }
322
323    #[test]
324    fn test_yk_neo() -> Result<(), Error> {
325        // yubikey neo
326        let data: &[u8] = &[
327            0x0, 0x73, 0x0, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
328        ];
329        let hist: HistoricalBytes = data.try_into()?;
330
331        assert_eq!(
332            hist,
333            HistoricalBytes {
334                cib: 0,
335                csd: None,
336                cc: Some(CardCapabilities {
337                    command_chaining: true,
338                    extended_lc_le: false,
339                    extended_length_information: false
340                }),
341                sib: 0
342            }
343        );
344
345        Ok(())
346    }
347
348    #[test]
349    fn test_ledger_nano_s() -> Result<(), Error> {
350        let data: &[u8] = &[
351            0x0, 0x31, 0xc5, 0x73, 0xc0, 0x1, 0x80, 0x7, 0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
352        ];
353        let hist: HistoricalBytes = data.try_into()?;
354
355        assert_eq!(
356            hist,
357            HistoricalBytes {
358                cib: 0,
359                csd: Some(CardServiceData {
360                    select_by_full_df_name: true,
361                    select_by_partial_df_name: true,
362                    dos_available_in_ef_dir: false,
363                    dos_available_in_ef_atr_info: false,
364                    access_services: [false, true, false],
365                    mf: true
366                }),
367                cc: Some(CardCapabilities {
368                    command_chaining: true,
369                    extended_lc_le: false,
370                    extended_length_information: false
371                }),
372                sib: 5
373            }
374        );
375
376        Ok(())
377    }
378}