yubikey_management/
yubikey.rs

1// SPDX-FileCopyrightText: 2023 Heiko Schaefer <heiko@schaefer.name>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use std::collections::{HashMap, HashSet, LinkedList};
5use std::fmt::{Display, Formatter};
6
7use card_backend::SmartcardError;
8use iso7816_tlv::simple::Tlv;
9use regex::Regex;
10
11pub(crate) const AID_MGR: &[u8] = &[0xA0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17];
12pub(crate) const AID_OTP: &[u8] = &[0xA0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01];
13
14/// YubiKey firmware version
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct Version {
17    major: u8,
18    minor: u8,
19    patch: u8,
20}
21
22impl Version {
23    pub fn major(&self) -> u8 {
24        self.major
25    }
26
27    pub fn minor(&self) -> u8 {
28        self.minor
29    }
30
31    pub fn patch(&self) -> u8 {
32        self.patch
33    }
34}
35
36impl Display for Version {
37    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
38        f.write_fmt(format_args!("{}.{}.{}", self.major, self.minor, self.patch))
39    }
40}
41
42const VERSION_STRING_PATTERN: &str = r"\b(?P<major>\d+).(?P<minor>\d).(?P<patch>\d)\b";
43
44impl TryFrom<String> for Version {
45    type Error = SmartcardError;
46
47    fn try_from(value: String) -> Result<Self, Self::Error> {
48        let re = Regex::new(VERSION_STRING_PATTERN).unwrap();
49
50        if let Some(caps) = re.captures(&value) {
51            Ok(Version {
52                major: caps[1].parse::<u8>().unwrap(),
53                minor: caps[2].parse::<u8>().unwrap(),
54                patch: caps[3].parse::<u8>().unwrap(),
55            })
56        } else {
57            Err(SmartcardError::Error(format!(
58                "Unexpected version string: '{}'",
59                value
60            )))
61        }
62    }
63}
64
65impl From<[u8; 3]> for Version {
66    fn from(value: [u8; 3]) -> Self {
67        Self {
68            major: value[0],
69            minor: value[1],
70            patch: value[2],
71        }
72    }
73}
74
75/// Applications on a YubiKey (e.g. OTP, PIV, OpenPGP card, ...)
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub enum Application {
78    Otp,
79    U2F,
80    Openpgp,
81    Piv,
82    Oath,
83    HsmAuth,
84    Fido2,
85}
86
87impl From<Application> for u16 {
88    fn from(value: Application) -> Self {
89        match value {
90            Application::Otp => 0x01,
91            Application::U2F => 0x02,
92            Application::Openpgp => 0x08,
93            Application::Piv => 0x10,
94            Application::Oath => 0x20,
95            Application::HsmAuth => 0x100,
96            Application::Fido2 => 0x200,
97        }
98    }
99}
100
101pub(crate) fn applications_to_bitmask(apps: HashSet<Application>) -> u16 {
102    let mut bitmask = 0;
103    apps.iter().for_each(|&a| bitmask |= u16::from(a));
104
105    bitmask
106}
107
108fn applications(value: Option<u16>) -> LinkedList<Application> {
109    let mut list = LinkedList::new();
110
111    if let Some(value) = value {
112        for a in [
113            Application::Otp,
114            Application::U2F,
115            Application::Openpgp,
116            Application::Piv,
117            Application::Oath,
118            Application::HsmAuth,
119            Application::Fido2,
120        ] {
121            if value & u16::from(a) != 0 {
122                list.push_back(a)
123            }
124        }
125    }
126
127    list
128}
129
130/// YubiKey form factor
131#[derive(Debug, Clone, Copy, PartialEq)]
132pub enum FormFactor {
133    Unknown,
134    UsbAKeychain,
135    UsbANano,
136    UsbCKeychain,
137    UsbCNano,
138    UsbCLightning,
139    UsbABio,
140    UsbCBio,
141}
142
143impl From<Option<u8>> for FormFactor {
144    fn from(value: Option<u8>) -> Self {
145        match value {
146            Some(0x01) => FormFactor::UsbAKeychain,
147            Some(0x02) => FormFactor::UsbANano,
148            Some(0x03) => FormFactor::UsbCKeychain,
149            Some(0x04) => FormFactor::UsbCNano,
150            Some(0x05) => FormFactor::UsbCLightning,
151            Some(0x06) => FormFactor::UsbABio,
152            Some(0x07) => FormFactor::UsbCBio,
153            _ => FormFactor::Unknown,
154        }
155    }
156}
157
158const TAG_USB_SUPPORTED: u8 = 0x01;
159const TAG_SERIAL: u8 = 0x02;
160pub(crate) const TAG_USB_ENABLED: u8 = 0x03;
161const TAG_FORM_FACTOR: u8 = 0x04;
162const TAG_VERSION: u8 = 0x05;
163const TAG_AUTO_EJECT_TIMEOUT: u8 = 0x06;
164const TAG_CHALRESP_TIMEOUT: u8 = 0x07;
165const TAG_DEVICE_FLAGS: u8 = 0x08;
166// const TAG_APP_VERSIONS: u8 = 0x09;
167const TAG_CONFIG_LOCK: u8 = 0x0A;
168// const TAG_UNLOCK: u8 = 0x0B;
169// const TAG_REBOOT: u8 = 0x0C;
170const TAG_NFC_SUPPORTED: u8 = 0x0D;
171pub(crate) const TAG_NFC_ENABLED: u8 = 0x0E;
172
173/// Metadata about a YubiKey device, as returned by [`YkManagement::read_config()`]
174#[derive(Debug, PartialEq)]
175pub struct DeviceInfo {
176    usb_supported: LinkedList<Application>,
177    serial: Option<u32>,
178    usb_enabled: LinkedList<Application>,
179    form_factor: FormFactor,
180    version: Option<Version>,
181    auto_eject_timeout: Option<u16>,
182    chalresp_timeout: Option<u8>,
183    device_flags: Option<u8>,
184    // app_versions, // FIXME: what type of data is in that field?
185    config_lock: Option<u8>,
186    // unlock, // FIXME: what type of data is in that field?
187    // reboot, // FIXME: what type of data is in that field?
188    nfc_supported: LinkedList<Application>,
189    nfc_enabled: LinkedList<Application>,
190}
191
192impl DeviceInfo {
193    /// YubiKey serial, if available
194    pub fn serial(&self) -> Option<u32> {
195        self.serial
196    }
197
198    /// YubiKey version, if available
199    pub fn version(&self) -> Option<Version> {
200        self.version
201    }
202
203    /// YubiKey device form factor
204    pub fn form_factor(&self) -> FormFactor {
205        self.form_factor
206    }
207
208    /// Applications that are supported on the USB interface
209    pub fn usb_supported(&self) -> HashSet<Application> {
210        self.usb_supported.iter().cloned().collect()
211    }
212
213    /// Applications that are enabled on the USB interface
214    pub fn usb_enabled(&self) -> HashSet<Application> {
215        self.usb_enabled.iter().cloned().collect()
216    }
217
218    /// Applications that are supported on the NFC interface
219    pub fn nfc_supported(&self) -> HashSet<Application> {
220        self.nfc_supported.iter().cloned().collect()
221    }
222
223    /// Applications that are enabled on the NFC interface
224    pub fn nfc_enabled(&self) -> HashSet<Application> {
225        self.nfc_enabled.iter().cloned().collect()
226    }
227}
228
229impl Display for DeviceInfo {
230    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
231        if let Some(serial) = self.serial {
232            f.write_fmt(format_args!("Serial {}\n", serial))?;
233        }
234        if let Some(version) = self.version {
235            f.write_fmt(format_args!("Version {}\n", version))?;
236        }
237        f.write_fmt(format_args!("Form factor: {:?}\n", self.form_factor))?;
238
239        f.write_fmt(format_args!("USB supported: {:?}\n", self.usb_supported))?;
240        f.write_fmt(format_args!("USB enabled: {:?}\n", self.usb_enabled))?;
241        f.write_fmt(format_args!("NFC supported: {:?}\n", self.nfc_supported))?;
242        f.write_fmt(format_args!("NFC enabled: {:?}\n", self.nfc_enabled))?;
243
244        if let Some(auto_eject_timeout) = self.auto_eject_timeout {
245            f.write_fmt(format_args!(
246                "Auto eject timeout: {:?}\n",
247                auto_eject_timeout
248            ))?;
249        }
250
251        if let Some(chalresp_timeout) = self.chalresp_timeout {
252            f.write_fmt(format_args!("ChalResp timeout: {:?}\n", chalresp_timeout))?;
253        }
254
255        if let Some(device_flags) = self.device_flags {
256            f.write_fmt(format_args!("Device flags: {:?}\n", device_flags))?;
257        }
258
259        if let Some(config_lock) = self.config_lock {
260            f.write_fmt(format_args!("Config lock: {:?}\n", config_lock))?;
261        }
262
263        // app_versions ?
264        // unlock ?
265        // reboot ?
266
267        Ok(())
268    }
269}
270
271impl TryFrom<&[u8]> for DeviceInfo {
272    type Error = SmartcardError;
273
274    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
275        // First byte is length
276        assert_eq!(value[0] as usize, value.len() - 1);
277
278        let mut map = HashMap::new();
279
280        let mut remaining: &[u8] = &value[1..];
281        while !remaining.is_empty() {
282            let (r, rest) = Tlv::parse(remaining);
283            remaining = rest;
284
285            let tag: u8 = r.as_ref().unwrap().tag().into();
286            let value = r.as_ref().unwrap().value().to_vec();
287
288            map.insert(tag, value);
289        }
290
291        // println!("{:#x?}", map);
292
293        fn to_applications(val: Option<&Vec<u8>>) -> LinkedList<Application> {
294            applications(val.map(|x| match x.len() {
295                1 => x[0] as u16,
296                2 => u16::from_be_bytes([x[0], x[1]]),
297                _ => unimplemented!(),
298            }))
299        }
300
301        Ok(DeviceInfo {
302            usb_supported: to_applications(map.get(&TAG_USB_SUPPORTED)),
303            serial: map
304                .get(&TAG_SERIAL)
305                .map(|x| u32::from_be_bytes(x[0..4].try_into().unwrap())),
306            usb_enabled: to_applications(map.get(&TAG_USB_ENABLED)),
307            form_factor: map.get(&TAG_FORM_FACTOR).map(|x| x[0]).into(),
308            version: map.get(&TAG_VERSION).map(|x| [x[0], x[1], x[2]].into()),
309            auto_eject_timeout: map
310                .get(&TAG_AUTO_EJECT_TIMEOUT)
311                .map(|x| u16::from_be_bytes(x[0..2].try_into().unwrap())),
312            chalresp_timeout: map.get(&TAG_CHALRESP_TIMEOUT).map(|x| x[0]),
313            device_flags: map.get(&TAG_DEVICE_FLAGS).map(|x| x[0]),
314            // app_versions: , // FIXME
315            config_lock: map.get(&TAG_CONFIG_LOCK).map(|x| x[0]),
316            // unlock: , // FIXME
317            // reboot: , // FIXME
318            nfc_supported: to_applications(map.get(&TAG_NFC_SUPPORTED)),
319            nfc_enabled: to_applications(map.get(&TAG_NFC_ENABLED)),
320        })
321    }
322}
323
324#[cfg(test)]
325mod test {
326    use std::collections::LinkedList;
327
328    use hex_literal::hex;
329
330    use crate::yubikey::{DeviceInfo, FormFactor, Version};
331    use crate::Application;
332
333    #[test]
334    fn test_config() {
335        let config = hex!("2e0102023f0302023b020400f46eec04010105030502070602000007010f0801000d02023f0e02023b0a01000f0100");
336
337        let di: DeviceInfo = (&config[..]).try_into().unwrap();
338
339        let expected = DeviceInfo {
340            serial: Some(16019180),
341            version: Some(Version {
342                major: 5,
343                minor: 2,
344                patch: 7,
345            }),
346            form_factor: FormFactor::UsbAKeychain,
347            auto_eject_timeout: Some(0),
348            chalresp_timeout: Some(15),
349            device_flags: Some(0),
350            config_lock: Some(0),
351
352            usb_supported: LinkedList::from([
353                Application::Otp,
354                Application::U2F,
355                Application::Openpgp,
356                Application::Piv,
357                Application::Oath,
358                Application::Fido2,
359            ]),
360            usb_enabled: LinkedList::from([
361                Application::Otp,
362                Application::U2F,
363                Application::Openpgp,
364                Application::Piv,
365                Application::Oath,
366                Application::Fido2,
367            ]),
368
369            nfc_supported: LinkedList::from([
370                Application::Otp,
371                Application::U2F,
372                Application::Openpgp,
373                Application::Piv,
374                Application::Oath,
375                Application::Fido2,
376            ]),
377            nfc_enabled: LinkedList::from([
378                Application::Otp,
379                Application::U2F,
380                Application::Openpgp,
381                Application::Piv,
382                Application::Oath,
383                Application::Fido2,
384            ]),
385        };
386
387        assert_eq!(di, expected);
388    }
389}