yubikey_management/
lib.rs

1// SPDX-FileCopyrightText: 2023 Heiko Schaefer <heiko@schaefer.name>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Configuration of Yubikey devices, via their "management" application.
5//! Allows enabling and disabling of applications on a YubiKey device,
6//! separately for the USB and NFC interface.
7//!
8//! This crate is in an early development stage, and still very incomplete.
9//!
10//! See <https://github.com/Yubico/yubikey-manager/blob/main/yubikit/management.py>
11
12mod apdu;
13mod command;
14mod yubikey;
15mod yubikey4;
16
17use std::collections::HashSet;
18
19use card_backend::{CardBackend, CardTransaction, SmartcardError};
20
21use crate::apdu::{Command, Expect, RawResponse, Response};
22use crate::yubikey::{applications_to_bitmask, AID_MGR, AID_OTP, TAG_NFC_ENABLED, TAG_USB_ENABLED};
23pub use crate::yubikey::{Application, DeviceInfo, FormFactor, Version};
24
25const BUF_SIZE: usize = 256;
26
27enum Interface {
28    Usb,
29    Nfc,
30}
31
32/// A YubiKey card, with the MANAGEMENT application selected
33pub struct YkManagement {
34    card: Box<dyn CardBackend + Sync + Send>,
35    version: Version,
36}
37
38impl YkManagement {
39    fn send(
40        tx: &mut (dyn CardTransaction + Sync + Send),
41        cmd: Command,
42        response: bool,
43    ) -> Result<Response, SmartcardError> {
44        let exp = match response {
45            true => Expect::Some,
46            false => Expect::Empty,
47        };
48
49        let send = cmd.serialize(false, exp)?;
50
51        log::trace!("send: {:x?}", send);
52
53        let res = tx
54            .transmit(&send, BUF_SIZE)
55            .map_err(|e| SmartcardError::Error(format!("{e:?}")))?;
56
57        log::trace!("received: {:x?}", res);
58
59        let raw = RawResponse::try_from(res)?;
60        raw.try_into()
61    }
62
63    /// Takes a `CardBackend` and turns it into a `YkManagement`, by calling
64    /// `SELECT` on the management application of the card.
65    ///
66    /// (The YubiKey NEO is handled differently, and the OTP application
67    /// is SELECTed)
68    pub fn select(mut card: Box<dyn CardBackend + Sync + Send>) -> Result<Self, SmartcardError> {
69        let version = {
70            let mut tx = card.transaction(None)?;
71
72            let cmd = command::select(AID_MGR.into());
73            let resp = YkManagement::send(&mut *tx, cmd, true)?;
74
75            let mut data = resp.as_ref();
76
77            // YubiKey Edge incorrectly appends SW twice.
78            if data[data.len() - 2..] == [0x90, 0x00] {
79                // Drop the extra [0x90, 0x00]
80                data = &data[0..data.len() - 2];
81            }
82
83            let s = String::from_utf8_lossy(data);
84            let version = Version::try_from(s.to_string())?;
85
86            // For YubiKey NEO, we use the OTP application for further commands
87            if version.major() == 3 {
88                // Workaround: "de-select" on NEO, otherwise it gets stuck.
89                let _resp = YkManagement::send(
90                    &mut *tx,
91                    Command::new(0xa4, 0x04, 0x00, 0x08, vec![]),
92                    false,
93                );
94                // UNCLEAR: This returns "6e00" (just like in ykman).
95                // What is this command supposed to do?
96                //
97                // Without this command, the next "select" operation does
98                // indeed fail. -> We ignore the Error in `_resp`.
99
100                // Select OTP application
101                let cmd = command::select(AID_OTP.into());
102                let _resp = YkManagement::send(&mut *tx, cmd, true)?;
103
104                // FIXME: get and use version from OTP application?
105            }
106
107            version
108        };
109
110        Ok(Self { card, version })
111    }
112
113    /// The YubiKey Version of the management application
114    pub fn version(&self) -> Version {
115        self.version
116    }
117
118    /// Reads the device config, using the 'INS_READ_CONFIG' command.
119    /// Results are parsed into a `DeviceInfo` struct, but presented
120    /// as returned by the card, without additional post-processing.
121    pub fn read_config(&mut self) -> Result<DeviceInfo, SmartcardError> {
122        log::debug!("read_config");
123
124        // def read_device_info(self) -> DeviceInfo:
125        //     require_version(self.version, (4, 1, 0))
126        // return DeviceInfo.parse(self.backend.read_config(), self.version)
127
128        let data = YkManagement::send(
129            &mut *self.card.transaction(None)?,
130            command::read_config(),
131            true,
132        )
133        .map(move |r| r.as_ref().to_vec())?;
134
135        DeviceInfo::try_from(data.as_slice())
136    }
137
138    /// Configure enabled applications, on YubiKey 4 devices
139    pub(crate) fn set_mode(&mut self, apps: HashSet<Application>) -> Result<(), SmartcardError> {
140        log::debug!("set_mode");
141
142        if self.version.major() != 4 {
143            unimplemented!();
144        }
145
146        let interface = yubikey4::usb_applications_to_interface(apps);
147        log::trace!("  interface: {:?}", interface);
148
149        let mode = yubikey4::interfaces_to_mode(interface);
150        log::trace!("  mode: {}", mode);
151
152        let cmd = command::set_mode_device_config(vec![mode, 0x00, 0x00, 0x00]);
153
154        YkManagement::send(&mut *self.card.transaction(None)?, cmd, false)?;
155
156        Ok(())
157    }
158
159    /// Configure enabled applications, on YubiKey 5 devices
160    pub(crate) fn write_config(
161        &mut self,
162        apps: HashSet<Application>,
163        iface: Interface,
164    ) -> Result<(), SmartcardError> {
165        log::debug!("write_config_usb");
166
167        if self.version.major() != 5 {
168            unimplemented!();
169        }
170
171        let bitmask: u16 = applications_to_bitmask(apps);
172        let bitmask = bitmask.to_be_bytes();
173
174        let cmd = command::write_config(vec![
175            0x04, // len
176            match iface {
177                Interface::Usb => TAG_USB_ENABLED,
178                Interface::Nfc => TAG_NFC_ENABLED,
179            },
180            0x02, // len
181            bitmask[0],
182            bitmask[1],
183        ]);
184
185        YkManagement::send(&mut *self.card.transaction(None)?, cmd, false)?;
186
187        Ok(())
188    }
189
190    fn change_application_set(apps: &mut HashSet<Application>, change: &[Application], on: bool) {
191        if on {
192            // add apps to current configuration
193            change.iter().for_each(|x| {
194                apps.insert(*x);
195            });
196        } else {
197            // remove apps from current configuration
198            change.iter().for_each(|x| {
199                apps.remove(x);
200            });
201        }
202    }
203
204    /// Change the set of enabled applications on the USB interface:
205    /// If `on` is true, the applications in `change` are enabled.
206    /// If `on` is false, the applications in `change` are disabled.
207    ///
208    /// This functionality is available for the YubiKey 4 and YubiKey 5.
209    pub fn applications_change_usb(
210        &mut self,
211        change: &[Application],
212        on: bool,
213    ) -> Result<(), SmartcardError> {
214        log::debug!("applications_change_usb, change: {:?}, on: {}", change, on);
215
216        let cfg = self.read_config()?;
217
218        let mut apps = cfg.usb_enabled();
219        Self::change_application_set(&mut apps, change, on);
220
221        match self.version.major() {
222            5 => self.write_config(apps, Interface::Usb)?,
223            4 => self.set_mode(apps)?,
224            _ => unimplemented!(),
225        };
226
227        Ok(())
228    }
229
230    /// Change the set of enabled applications on the NFC interface:
231    /// If `on` is true, the applications in `change` are enabled.
232    /// If `on` is false, the applications in `change` are disabled.
233    ///
234    /// This functionality is available for the YubiKey 5.
235    pub fn applications_change_nfc(
236        &mut self,
237        change: &[Application],
238        on: bool,
239    ) -> Result<(), SmartcardError> {
240        log::debug!("applications_change_nfc, change: {:?}, on: {}", change, on);
241
242        let cfg = self.read_config()?;
243
244        let mut apps = cfg.nfc_enabled();
245        Self::change_application_set(&mut apps, change, on);
246
247        match self.version.major() {
248            5 => self.write_config(apps, Interface::Nfc)?,
249            _ => unimplemented!(),
250        };
251
252        Ok(())
253    }
254}