diff --git a/Cargo.lock b/Cargo.lock index 4e264d2..0c1521d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "block-buffer" version = "0.11.0-rc.3" @@ -549,7 +555,7 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37cab0be9d04e808a8d8059fa54befcd71dc8b168f9f0c04bdb7e59832abbab4" dependencies = [ - "bitflags", + "bitflags 1.3.2", "pcsc-sys", ] @@ -1094,6 +1100,7 @@ name = "yubikey" version = "0.8.0" dependencies = [ "base16ct", + "bitflags 2.5.0", "der", "des", "ecdsa", diff --git a/Cargo.toml b/Cargo.toml index 15253fc..9f878e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ sha2 = "=0.11.0-pre.4" x509-cert = { version = "=0.3.0-pre.0", features = [ "builder", "hazmat" ] } [dependencies] +bitflags = "2.5.0" der = "=0.8.0-rc.1" des = "=0.9.0-pre.2" elliptic-curve = "=0.14.0-rc.1" diff --git a/examples/change-mode.rs b/examples/change-mode.rs new file mode 100644 index 0000000..94863d9 --- /dev/null +++ b/examples/change-mode.rs @@ -0,0 +1,10 @@ +#![cfg(feature = "untested")] + +use yubikey::{mgm, YubiKey}; + +fn main() { + let yubikey = YubiKey::open().unwrap(); + + let mut mgmt = mgm::Manager::new(yubikey).unwrap(); + mgmt.enable_yubihsm().unwrap(); +} diff --git a/src/apdu.rs b/src/apdu.rs index 890638b..93f8385 100644 --- a/src/apdu.rs +++ b/src/apdu.rs @@ -204,6 +204,15 @@ pub enum Ins { /// Get slot metadata GetMetadata, + /// Management // Read Config + ReadConfig, + + /// Management // Write Config + WriteConfig, + + /// Management // DeviceReset + DeviceReset, + /// Other/unrecognized instruction codes Other(u8), } @@ -229,6 +238,12 @@ impl Ins { Ins::Attest => 0xf9, Ins::GetSerial => 0xf8, Ins::GetMetadata => 0xf7, + + // Management + Ins::ReadConfig => 0x1d, + Ins::WriteConfig => 0x1c, + Ins::DeviceReset => 0x1f, + Ins::Other(code) => code, } } @@ -237,6 +252,11 @@ impl Ins { impl From for Ins { fn from(code: u8) -> Self { match code { + // Management + 0x1d => Ins::ReadConfig, + 0x1c => Ins::WriteConfig, + 0x1f => Ins::DeviceReset, + 0x20 => Ins::Verify, 0x24 => Ins::ChangeReference, 0x2c => Ins::ResetRetry, diff --git a/src/consts.rs b/src/consts.rs index 03242ab..bc40a7d 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] //! Miscellaneous constant values /// YubiKey max buffer size @@ -18,3 +19,19 @@ pub(crate) const TAG_ADMIN_TIMESTAMP: u8 = 0x83; // Protected tags pub(crate) const TAG_PROTECTED_FLAGS_1: u8 = 0x81; pub(crate) const TAG_PROTECTED_MGM: u8 = 0x89; + +// Management +pub(crate) const TAG_USB_SUPPORTED: u8 = 0x01; +pub(crate) const TAG_SERIAL: u8 = 0x02; +pub(crate) const TAG_USB_ENABLED: u8 = 0x03; +pub(crate) const TAG_FORM_FACTOR: u8 = 0x04; +pub(crate) const TAG_VERSION: u8 = 0x05; +pub(crate) const TAG_AUTO_EJECT_TIMEOUT: u8 = 0x06; +pub(crate) const TAG_CHALRESP_TIMEOUT: u8 = 0x07; +pub(crate) const TAG_DEVICE_FLAGS: u8 = 0x08; +pub(crate) const TAG_APP_VERSIONS: u8 = 0x09; +pub(crate) const TAG_CONFIG_LOCK: u8 = 0x0A; +pub(crate) const TAG_UNLOCK: u8 = 0x0B; +pub(crate) const TAG_REBOOT: u8 = 0x0C; +pub(crate) const TAG_NFC_SUPPORTED: u8 = 0x0D; +pub(crate) const TAG_NFC_ENABLED: u8 = 0x0E; diff --git a/src/lib.rs b/src/lib.rs index 89e56da..a41c067 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,7 +51,7 @@ mod config; mod consts; mod error; mod metadata; -mod mgm; +pub mod mgm; #[cfg(feature = "untested")] mod mscmap; #[cfg(feature = "untested")] diff --git a/src/mgm.rs b/src/mgm.rs index 960f49a..b3ef0f3 100644 --- a/src/mgm.rs +++ b/src/mgm.rs @@ -35,20 +35,30 @@ use log::error; use rand_core::{OsRng, RngCore}; use zeroize::Zeroize; -#[cfg(feature = "untested")] -use crate::{ - consts::{TAG_ADMIN_FLAGS_1, TAG_ADMIN_SALT, TAG_PROTECTED_MGM}, - metadata::{AdminData, ProtectedData}, - piv::{ManagementSlotId, SlotAlgorithmId}, - transaction::Transaction, - yubikey::YubiKey, -}; use des::{ cipher::{BlockCipherDecrypt, BlockCipherEncrypt, KeyInit}, TdesEde3, }; #[cfg(feature = "untested")] -use {pbkdf2::pbkdf2_hmac, sha1::Sha1}; +use { + crate::{ + consts::{ + CB_BUF_MAX, TAG_ADMIN_FLAGS_1, TAG_ADMIN_SALT, TAG_AUTO_EJECT_TIMEOUT, + TAG_CHALRESP_TIMEOUT, TAG_CONFIG_LOCK, TAG_DEVICE_FLAGS, TAG_FORM_FACTOR, + TAG_NFC_ENABLED, TAG_NFC_SUPPORTED, TAG_PROTECTED_MGM, TAG_REBOOT, TAG_SERIAL, + TAG_UNLOCK, TAG_USB_ENABLED, TAG_USB_SUPPORTED, TAG_VERSION, + }, + metadata::{AdminData, ProtectedData}, + piv::{ManagementSlotId, SlotAlgorithmId}, + serialization::Tlv, + transaction::Transaction, + yubikey::YubiKey, + Serial, Version, + }, + bitflags::bitflags, + pbkdf2::pbkdf2_hmac, + sha1::Sha1, +}; /// YubiKey MGMT Applet Name #[cfg(feature = "untested")] @@ -413,3 +423,467 @@ impl<'a> TryFrom<&'a [u8]> for MgmKey { Self::new(key_bytes.try_into().map_err(|_| Error::SizeError)?) } } + +/// Manager for the YubiKey +/// Allows to enable applications hosted on the YubiKey +#[cfg(feature = "untested")] +pub struct Manager { + client: YubiKey, +} + +#[cfg(feature = "untested")] +impl Manager { + /// Open the manager applet on the YubiKey + pub fn new(mut client: YubiKey) -> Result { + Transaction::new(&mut client.card)?.select_application( + APPLET_ID, + APPLET_NAME, + "failed selecting YkHSM auth application", + )?; + + Ok(Self { client }) + } + + /// Enable YubiHSM applet + pub fn enable_yubihsm(&mut self) -> Result<()> { + let mut config = Transaction::new(&mut self.client.card)?.read_config()?; + config.config.usb_enabled_apps |= Capability::HSMAUTH; + Transaction::new(&mut self.client.card)?.write_config( + self.client.version, + config.config, + None, + None, + )?; + Ok(()) + } + + /// Enable PIV applet + pub fn enable_piv(&mut self) -> Result<()> { + let mut config = Transaction::new(&mut self.client.card)?.read_config()?; + config.config.usb_enabled_apps |= Capability::PIV; + Transaction::new(&mut self.client.card)?.write_config( + self.client.version, + config.config, + None, + None, + )?; + Ok(()) + } + + /// Disable NFC interface on the device + pub fn disable_nfc(&mut self) -> Result<()> { + let mut config = Transaction::new(&mut self.client.card)?.read_config()?; + config.config.nfc_enabled_apps = Some(Capability::empty()); + Transaction::new(&mut self.client.card)?.write_config( + self.client.version, + config.config, + None, + None, + )?; + Ok(()) + } + + /// Lock configuration of the device + pub fn lock_config( + &mut self, + current_lock: Option, + new_lock: Option, + ) -> Result<()> { + let config = Transaction::new(&mut self.client.card)?.read_config()?; + Transaction::new(&mut self.client.card)?.write_config( + self.client.version, + config.config, + current_lock, + new_lock, + )?; + Ok(()) + } + + /// Read configuration from yubikey + pub fn read_config(&mut self) -> Result { + Transaction::new(&mut self.client.card)?.read_config() + } + + /// Return the inner [`YubiKey`] + pub fn into_inner(mut self) -> Result { + Transaction::new(&mut self.client.card)?.select_piv_application()?; + Ok(self.client) + } +} + +/// secret to lock YubiKey's configuration +pub struct Lock(pub [u8; 16]); + +impl Lock { + /// Clear the locking of configuration + pub const UNLOCKED: Lock = Lock([0; 16]); +} + +/// Configuration for a device +#[derive(Clone, Debug, PartialEq)] +#[cfg(feature = "untested")] +pub struct DeviceConfig { + /// List of applets that are available on the USB interface + pub usb_enabled_apps: Capability, + /// List of applets that are available on the NFC interface. + /// Field will be set to None if the device doesn't support NFC + pub nfc_enabled_apps: Option, + /// When set, the smartcard will automatically eject after the given time. + pub auto_eject_timeout: Option, + /// The timeout when waiting for touch for challenge response. + pub challenge_response_timeout: Option, + /// Configuration flags + pub device_flags: Option, +} + +#[cfg(feature = "untested")] +impl DeviceConfig { + pub(crate) fn as_tlv( + &self, + reboot: bool, + current_lock: Option, + new_lock: Option, + ) -> Result> { + let mut data = [0u8; CB_BUF_MAX]; + let mut len = data.len(); + let mut data_remaining = &mut data[1..]; + + if reboot { + let offset = Tlv::write(data_remaining, TAG_REBOOT, &[])?; + data_remaining = &mut data_remaining[offset..]; + } + + if let Some(lock) = current_lock { + let offset = Tlv::write(data_remaining, TAG_UNLOCK, &lock.0[..])?; + data_remaining = &mut data_remaining[offset..]; + } + + { + let offset = Tlv::write( + data_remaining, + TAG_USB_ENABLED, + &self.usb_enabled_apps.bits().to_be_bytes()[..], + )?; + data_remaining = &mut data_remaining[offset..]; + } + if let Some(nfc_enabled_apps) = self.nfc_enabled_apps { + let offset = Tlv::write( + data_remaining, + TAG_NFC_ENABLED, + &nfc_enabled_apps.bits().to_be_bytes()[..], + )?; + data_remaining = &mut data_remaining[offset..]; + } + if let Some(auto_eject_timeout) = self.auto_eject_timeout { + let offset = Tlv::write( + data_remaining, + TAG_AUTO_EJECT_TIMEOUT, + &auto_eject_timeout.to_be_bytes()[..], + )?; + data_remaining = &mut data_remaining[offset..]; + } + if let Some(challenge_response_timeout) = self.challenge_response_timeout { + let offset = Tlv::write( + data_remaining, + TAG_CHALRESP_TIMEOUT, + &challenge_response_timeout.to_be_bytes()[..], + )?; + data_remaining = &mut data_remaining[offset..]; + } + if let Some(device_flags) = self.device_flags { + let offset = Tlv::write( + data_remaining, + TAG_DEVICE_FLAGS, + &device_flags.bits().to_be_bytes()[..], + )?; + data_remaining = &mut data_remaining[offset..]; + } + + if let Some(lock) = new_lock { + let offset = Tlv::write(data_remaining, TAG_CONFIG_LOCK, &lock.0[..])?; + data_remaining = &mut data_remaining[offset..]; + } + + len -= data_remaining.len(); + data[0] = (len - 1) as u8; + Ok(data[..len].to_vec()) + } + + /// Is the NFC interface active + pub fn nfc_enabled(&self) -> bool { + // It happens that the yubikey will disable the NFC interface entirely if every applet is + // disabled. + self.nfc_enabled_apps + .map(|nfc| !nfc.is_empty()) + .unwrap_or(false) + } +} + +#[cfg(feature = "untested")] +bitflags! { + /// Represents a set of applications. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct Capability: u16 { + /// One Time Password + const OTP =0x01; + /// U2F + const U2F = 0x02; + /// FIDO2 + const FIDO2 = 0x200; + /// OATH + const OATH = 0x20; + /// PIV + const PIV = 0x10; + /// OpenPGP + const OPENPGP = 0x08; + /// HSM Auth + const HSMAUTH = 0x100; + + /// General CCID bit + const GENERAL_CCID = 0x04; + } +} + +/// Device information +/// This represents the configuration and the status of the device +#[cfg(feature = "untested")] +pub struct DeviceInfo { + /// Configuration of the YubiKey + pub config: DeviceConfig, + /// Is the configuration locked with a password? + pub is_locked: bool, +} + +#[cfg(feature = "untested")] +impl DeviceInfo { + pub(crate) fn parse(input: &[u8]) -> Result { + use nom::{ + bytes::complete::take, + combinator::{eof, map}, + multi::fold_many1, + number::complete::{be_u16, be_u32, u8}, + }; + + fn u8_parser(i: &[u8]) -> nom::IResult<&[u8], u8> { + let (i, v) = u8(i)?; + let (i, _) = eof(i)?; + + Ok((i, v)) + } + fn u16_parser(i: &[u8]) -> nom::IResult<&[u8], u16> { + let (i, v) = be_u16(i)?; + let (i, _) = eof(i)?; + + Ok((i, v)) + } + + fn capability_parser(i: &[u8]) -> nom::IResult<&[u8], Capability> { + let (i, v) = map(be_u16, Capability::from_bits_retain)(i)?; + let (i, _) = eof(i)?; + + Ok((i, v)) + } + fn serial_parser(i: &[u8]) -> nom::IResult<&[u8], Serial> { + let (i, v) = map(be_u32, Serial)(i)?; + let (i, _) = eof(i)?; + + Ok((i, v)) + } + + #[derive(Debug, Default)] + struct Info { + usb_supported_apps: Option, + usb_enabled_apps: Option, + nfc_supported_apps: Option, + nfc_enabled_apps: Option, + serial: Option, + form_factor: Option, + version: Option, + auto_eject_timeout: Option, + challenge_response_timeout: Option, + device_flags: Option, + is_locked: bool, + } + + let (input, len) = u8(input).map_err(|_: nom::Err<()>| Error::ParseError)?; + let (input, rest) = take(len)(input).map_err(|_: nom::Err<()>| Error::ParseError)?; + let (_, _) = eof(input).map_err(|_: nom::Err<()>| Error::ParseError)?; + + let out = fold_many1( + |input| Tlv::parse(input).map_err(|_| nom::Err::Error(())), + || Ok(Info::default()), + |acc: Result, tlv| match acc { + Ok(mut config) => { + match tlv.tag { + v if v == TAG_USB_SUPPORTED => { + config.usb_supported_apps = Some( + capability_parser(tlv.value) + .map_err(|_| Error::ParseError)? + .1, + ); + Ok(config) + } + v if v == TAG_USB_ENABLED => { + config.usb_enabled_apps = Some( + capability_parser(tlv.value) + .map_err(|_| Error::ParseError)? + .1, + ); + Ok(config) + } + v if v == TAG_NFC_SUPPORTED => { + config.nfc_supported_apps = Some( + capability_parser(tlv.value) + .map_err(|_| Error::ParseError)? + .1, + ); + Ok(config) + } + v if v == TAG_NFC_ENABLED => { + config.nfc_enabled_apps = Some( + capability_parser(tlv.value) + .map_err(|_| Error::ParseError)? + .1, + ); + Ok(config) + } + v if v == TAG_SERIAL => { + config.serial = + Some(serial_parser(tlv.value).map_err(|_| Error::ParseError)?.1); + Ok(config) + } + v if v == TAG_FORM_FACTOR => { + config.form_factor = Some(FormFactor::parse(tlv.value)?); + Ok(config) + } + v if v == TAG_VERSION => { + config.version = Some(Version::parse(tlv.value)?); + Ok(config) + } + v if v == TAG_AUTO_EJECT_TIMEOUT => { + config.auto_eject_timeout = + Some(u16_parser(tlv.value).map_err(|_| Error::ParseError)?.1); + Ok(config) + } + v if v == TAG_CHALRESP_TIMEOUT => { + config.challenge_response_timeout = + Some(u8_parser(tlv.value).map_err(|_| Error::ParseError)?.1); + Ok(config) + } + v if v == TAG_DEVICE_FLAGS => { + config.device_flags = Some( + DeviceFlags::parse(tlv.value) + .map_err(|_| Error::ParseError)? + .1, + ); + Ok(config) + } + v if v == TAG_CONFIG_LOCK => { + config.is_locked = tlv.value == b"\x01"; + Ok(config) + } + // TODO(baloo): implement config lock + _unsupported => { + // New unsupported tags + Ok(config) + } + } + } + err => err, + }, + )(rest) + .map_err(|_: nom::Err<()>| Error::ParseError)? + .1?; + + let Some(usb_enabled_apps) = out.usb_enabled_apps else { + return Err(Error::ParseError); + }; + + let config = DeviceConfig { + usb_enabled_apps, + nfc_enabled_apps: out.nfc_enabled_apps, + auto_eject_timeout: out.auto_eject_timeout, + challenge_response_timeout: out.challenge_response_timeout, + device_flags: out.device_flags, + }; + + Ok(DeviceInfo { + config, + is_locked: out.is_locked, + }) + } +} + +/// FormFactor of the YubiKey +#[cfg(feature = "untested")] +#[derive(Debug, Copy, Clone, PartialEq)] +#[repr(u8)] +pub enum FormFactor { + /// YubiKey reported an unknown form factor + Unknown = 0x00, + /// Full size USB-A YubiKey + UsbAKeychain = 0x01, + /// Nano USB-A YubiKey + UsbANano = 0x02, + /// Full size USB-C YubiKey + UsbCKeychain = 0x03, + /// Nano USB-C YubiKey + UsbCNano = 0x04, + /// Lightning // USB-C YubiKey + UsbCLightning = 0x05, + /// USB-A YubiKey with a fingerprint reader + UsbABio = 0x06, + /// USB-C YubiKey with a fingerprint reader + UsbCBio = 0x07, + /// Unsupported form factor + Unsupported(u8), +} + +#[cfg(feature = "untested")] +impl FormFactor { + fn parse(input: &[u8]) -> Result { + use nom::{combinator::eof, number::complete::u8}; + + let (i, v) = u8(input).map_err(|_: nom::Err<()>| Error::ParseError)?; + let (_i, _) = eof(i).map_err(|_: nom::Err<()>| Error::ParseError)?; + + Ok(match v { + 0 => Self::Unknown, + 1 => Self::UsbAKeychain, + 2 => Self::UsbANano, + 3 => Self::UsbCKeychain, + 4 => Self::UsbCNano, + 5 => Self::UsbCLightning, + 6 => Self::UsbABio, + 7 => Self::UsbCBio, + v => Self::Unsupported(v), + }) + } +} + +#[cfg(feature = "untested")] +bitflags! { + /// Represents configuration flags. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct DeviceFlags: u8{ + /// Remote wake-up + const REMOTE_WAKEUP = 0x40; + /// Eject + const Eject = 0x80; + } +} + +#[cfg(feature = "untested")] +impl DeviceFlags { + fn parse(i: &[u8]) -> nom::IResult<&[u8], Self> { + use nom::{ + combinator::{eof, map}, + number::complete::u8, + }; + + let (i, v) = map(u8, Self::from_bits_retain)(i)?; + let (i, _) = eof(i)?; + + Ok((i, v)) + } +} diff --git a/src/transaction.rs b/src/transaction.rs index 727f23d..d29af6d 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -15,7 +15,7 @@ use log::{error, trace}; use zeroize::Zeroizing; #[cfg(feature = "untested")] -use crate::mgm::{MgmKey, DES_LEN_3DES}; +use crate::mgm::{DeviceConfig, DeviceInfo, Lock, MgmKey, DES_LEN_3DES}; const CB_PIN_MAX: usize = 8; @@ -60,23 +60,32 @@ impl<'tx> Transaction<'tx> { Ok(recv_buffer) } + /// Select PIV application. + pub fn select_piv_application(&self) -> Result<()> { + self.select_application( + piv::APPLET_ID, + piv::APPLET_NAME, + "failed selecting application", + ) + } + /// Select application. - pub fn select_application(&self) -> Result<()> { + pub fn select_application( + &self, + applet: &[u8], + applet_name: &'static str, + error: &'static str, + ) -> Result<()> { let response = Apdu::new(Ins::SelectApplication) .p1(0x04) - .data(piv::APPLET_ID) + .data(applet) .transmit(self, 0xFF) .inspect_err(|e| error!("failed communicating with card: '{}'", e))?; if !response.is_success() { - error!( - "failed selecting application: {:04x}", - response.status_words().code() - ); + error!("{}: {:04x}", error, response.status_words().code()); return Err(match response.status_words() { - StatusWords::NotFoundError => Error::AppletNotFound { - applet_name: piv::APPLET_NAME, - }, + StatusWords::NotFoundError => Error::AppletNotFound { applet_name }, _ => Error::GenericError, }); } @@ -101,21 +110,11 @@ impl<'tx> Transaction<'tx> { match version.major { // YK4 requires switching to the YK applet to retrieve the serial 4 => { - let sw = Apdu::new(Ins::SelectApplication) - .p1(0x04) - .data(otp::APPLET_ID) - .transmit(self, 0xFF)? - .status_words(); - - if !sw.is_success() { - error!("failed selecting yk application: {:04x}", sw.code()); - return Err(match sw { - StatusWords::NotFoundError => Error::AppletNotFound { - applet_name: otp::APPLET_NAME, - }, - _ => Error::GenericError, - }); - } + self.select_application( + otp::APPLET_ID, + otp::APPLET_NAME, + "failed selecting yk application", + )?; let response = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?; @@ -129,21 +128,11 @@ impl<'tx> Transaction<'tx> { } // reselect the PIV applet - let sw = Apdu::new(Ins::SelectApplication) - .p1(0x04) - .data(piv::APPLET_ID) - .transmit(self, 0xFF)? - .status_words(); - - if !sw.is_success() { - error!("failed selecting application: {:04x}", sw.code()); - return Err(match sw { - StatusWords::NotFoundError => Error::AppletNotFound { - applet_name: piv::APPLET_NAME, - }, - _ => Error::GenericError, - }); - } + self.select_application( + piv::APPLET_ID, + piv::APPLET_NAME, + "failed selecting application", + )?; response.data().try_into() } @@ -524,4 +513,66 @@ impl<'tx> Transaction<'tx> { _ => Err(Error::GenericError), } } + + /// Write configuration to the YubiKey + #[cfg(feature = "untested")] + pub fn write_config( + &mut self, + version: Version, + config: DeviceConfig, + current_lock: Option, + new_lock: Option, + ) -> Result<()> { + if version + < (Version { + major: 5, + minor: 0, + patch: 0, + }) + { + return Err(Error::NotSupported); + } + + let data = config.as_tlv(true, current_lock, new_lock)?; + + let response = Apdu::new(Ins::WriteConfig) + .params(0x00, 0x00) + .data(&data) + .transmit(self, 2)?; + + if !response.is_success() { + error!( + "Unable to write_config: {:04x}", + response.status_words().code() + ); + return Err(Error::GenericError); + } + + Ok(()) + } + + /// Write configuration to the YubiKey + #[cfg(feature = "untested")] + pub fn read_config(&mut self) -> Result { + let mut data = [0u8; CB_BUF_MAX]; + let mut len = data.len(); + let data_remaining = &mut data[..]; + + len -= data_remaining.len(); + let response = Apdu::new(Ins::ReadConfig) + .params(0x00, 0x00) + .data(&data[..len]) + .transmit(self, CB_BUF_MAX + 2)?; + + if !response.is_success() { + error!( + "Unable to read configuration: {:04x}", + response.status_words().code() + ); + return Err(Error::GenericError); + } + + let data = response.data(); + DeviceInfo::parse(data) + } } diff --git a/src/yubikey.rs b/src/yubikey.rs index 6c1c298..6cbaf87 100644 --- a/src/yubikey.rs +++ b/src/yubikey.rs @@ -45,6 +45,7 @@ use log::{error, info}; use pcsc::Card; use rand_core::{OsRng, RngCore}; use std::{ + cmp::{Ord, Ordering}, fmt::{self, Display}, str::FromStr, }; @@ -143,6 +144,22 @@ impl Version { patch: bytes[2], } } + + #[cfg(feature = "untested")] + pub(crate) fn parse(input: &[u8]) -> Result { + use nom::{combinator::eof, number::complete::u8}; + + let (i, major) = u8(input).map_err(|_: nom::Err<()>| Error::ParseError)?; + let (i, minor) = u8(i).map_err(|_: nom::Err<()>| Error::ParseError)?; + let (i, patch) = u8(i).map_err(|_: nom::Err<()>| Error::ParseError)?; + let (_i, _) = eof(i).map_err(|_: nom::Err<()>| Error::ParseError)?; + + Ok(Self { + major, + minor, + patch, + }) + } } impl Display for Version { @@ -151,6 +168,39 @@ impl Display for Version { } } +impl Ord for Version { + fn cmp(&self, other: &Self) -> Ordering { + if self.major > other.major { + return Ordering::Greater; + } + if self.major < other.major { + return Ordering::Less; + } + + if self.minor > other.minor { + return Ordering::Greater; + } + if self.minor < other.minor { + return Ordering::Less; + } + + if self.patch > other.patch { + return Ordering::Greater; + } + if self.patch < other.patch { + return Ordering::Less; + } + + Ordering::Equal + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + /// YubiKey device: primary API for opening a session and performing various operations. /// /// Almost all functionality in this library will require an open session @@ -275,7 +325,7 @@ impl YubiKey { .map(|p| Buffer::new(p.expose_secret().clone())); let txn = Transaction::new(&mut self.card)?; - txn.select_application()?; + txn.select_piv_application()?; if let Some(p) = &pin { txn.verify_pin(p)?; @@ -462,7 +512,7 @@ impl YubiKey { // Force a re-select to unverify, because once verified the spec dictates that // subsequent verify calls will return a "verification not needed" instead of // the number of tries left... - txn.select_application()?; + txn.select_piv_application()?; // WRONG_PIN is expected on successful query. match txn.verify_pin(&[]) { @@ -708,7 +758,7 @@ impl<'a> TryFrom<&'a Reader<'_>> for YubiKey { let mut app_version_serial = || -> Result<(Version, Serial)> { let txn = Transaction::new(&mut card)?; - txn.select_application()?; + txn.select_piv_application()?; let v = txn.get_version()?; let s = txn.get_serial(v)?;