From d55079f9a6beea7e8d4d5943917179c1e3a87f8f Mon Sep 17 00:00:00 2001 From: str4d Date: Sun, 12 Feb 2023 17:20:34 +0000 Subject: [PATCH] Enable library users to detect if a smart card doesn't support PIV (#476) * Enable library users to detect if a smart card doesn't support PIV Closes iqlusioninc/yubikey.rs#456. * Avoid resetting the card if we fail to select PIV or fetch version/serial --- CHANGELOG.md | 10 +++++++++ src/error.rs | 9 ++++++++ src/lib.rs | 1 + src/mgm.rs | 10 +++++++++ src/otp.rs | 5 +++++ src/piv.rs | 6 ++++++ src/transaction.rs | 36 +++++++++++++++++++------------ src/yubikey.rs | 53 ++++++++++++++++++++++++++++++---------------- 8 files changed, 99 insertions(+), 31 deletions(-) create mode 100644 src/otp.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c5845bd..e88bbf56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `YubiKey::disconnect` - `impl Debug for {Context, YubiKey}` +- `Error::AppletNotFound` + +### Changed +- `Reader::open` now returns `Error::AppletNotFound` instead of `Error::Generic` + if the PIV applet is not present on the device. This is returned by non-PIV + virtual smart cards like Windows Hello for Business, as well as some smart + card readers when no card is present. +- `Reader::open` now avoids resetting the card if an error occurs (equivalent to + calling `YubiKey::disconnect(pcsc::Disposition::LeaveCard)` if `Reader::open` + succeeds). ## 0.7.0 (2022-11-14) ### Added diff --git a/src/error.rs b/src/error.rs index 86d39ad6..cf759c99 100644 --- a/src/error.rs +++ b/src/error.rs @@ -45,6 +45,12 @@ pub enum Error { /// Applet error AppletError, + /// We tried to select an applet that could not be found. + AppletNotFound { + /// Human-readable name of the applet. + applet_name: &'static str, + }, + /// Argument error ArgumentError, @@ -125,6 +131,9 @@ impl Error { match self { Error::AlgorithmError => f.write_str("algorithm error"), Error::AppletError => f.write_str("applet error"), + Error::AppletNotFound { applet_name } => { + f.write_str(&format!("{} applet not found", applet_name)) + } Error::ArgumentError => f.write_str("argument error"), Error::AuthenticationError => f.write_str("authentication error"), Error::GenericError => f.write_str("generic error"), diff --git a/src/lib.rs b/src/lib.rs index f3782c25..571712ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,7 @@ mod mgm; mod mscmap; #[cfg(feature = "untested")] mod msroots; +mod otp; pub mod piv; mod policy; pub mod reader; diff --git a/src/mgm.rs b/src/mgm.rs index b1a9c145..78e829a5 100644 --- a/src/mgm.rs +++ b/src/mgm.rs @@ -48,6 +48,16 @@ use des::{ #[cfg(feature = "untested")] use {hmac::Hmac, pbkdf2::pbkdf2, sha1::Sha1}; +/// YubiKey MGMT Applet Name +#[cfg(feature = "untested")] +pub(crate) const APPLET_NAME: &str = "YubiKey MGMT"; + +/// MGMT Applet ID. +/// +/// +#[cfg(feature = "untested")] +pub(crate) const APPLET_ID: &[u8] = &[0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17]; + pub(crate) const ADMIN_FLAGS_1_PROTECTED_MGM: u8 = 0x02; #[cfg(feature = "untested")] diff --git a/src/otp.rs b/src/otp.rs new file mode 100644 index 00000000..394b7a7f --- /dev/null +++ b/src/otp.rs @@ -0,0 +1,5 @@ +/// YubiKey OTP Applet Name +pub(crate) const APPLET_NAME: &str = "YubiKey OTP"; + +/// YubiKey OTP Applet ID. Needed to query serial on YK4. +pub(crate) const APPLET_ID: &[u8] = &[0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01]; diff --git a/src/piv.rs b/src/piv.rs index 037957ae..78ba16d3 100644 --- a/src/piv.rs +++ b/src/piv.rs @@ -73,6 +73,12 @@ use { #[cfg(feature = "untested")] use zeroize::Zeroizing; +/// PIV Applet Name +pub(crate) const APPLET_NAME: &str = "PIV"; + +/// PIV Applet ID +pub(crate) const APPLET_ID: &[u8] = &[0xa0, 0x00, 0x00, 0x03, 0x08]; + const CB_ECC_POINTP256: usize = 65; const CB_ECC_POINTP384: usize = 97; diff --git a/src/transaction.rs b/src/transaction.rs index 379aa9d4..a66c797b 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -5,7 +5,8 @@ use crate::{ apdu::{Apdu, Ins, StatusWords}, consts::{CB_BUF_MAX, CB_OBJ_MAX}, error::{Error, Result}, - piv::{AlgorithmId, SlotId}, + otp, + piv::{self, AlgorithmId, SlotId}, serialization::*, yubikey::*, Buffer, ObjectId, @@ -16,12 +17,6 @@ use zeroize::Zeroizing; #[cfg(feature = "untested")] use crate::mgm::{MgmKey, DES_LEN_3DES}; -/// PIV Applet ID -const PIV_AID: [u8; 5] = [0xa0, 0x00, 0x00, 0x03, 0x08]; - -/// YubiKey OTP Applet ID. Needed to query serial on YK4. -const YK_AID: [u8; 8] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01]; - const CB_PIN_MAX: usize = 8; #[cfg(feature = "untested")] @@ -69,7 +64,7 @@ impl<'tx> Transaction<'tx> { pub fn select_application(&self) -> Result<()> { let response = Apdu::new(Ins::SelectApplication) .p1(0x04) - .data(PIV_AID) + .data(piv::APPLET_ID) .transmit(self, 0xFF) .map_err(|e| { error!("failed communicating with card: '{}'", e); @@ -81,7 +76,12 @@ impl<'tx> Transaction<'tx> { "failed selecting application: {:04x}", response.status_words().code() ); - return Err(Error::GenericError); + return Err(match response.status_words() { + StatusWords::NotFoundError => Error::AppletNotFound { + applet_name: piv::APPLET_NAME, + }, + _ => Error::GenericError, + }); } Ok(()) @@ -110,13 +110,18 @@ impl<'tx> Transaction<'tx> { 4 => { let sw = Apdu::new(Ins::SelectApplication) .p1(0x04) - .data(YK_AID) + .data(otp::APPLET_ID) .transmit(self, 0xFF)? .status_words(); if !sw.is_success() { error!("failed selecting yk application: {:04x}", sw.code()); - return Err(Error::GenericError); + return Err(match sw { + StatusWords::NotFoundError => Error::AppletNotFound { + applet_name: otp::APPLET_NAME, + }, + _ => Error::GenericError, + }); } let response = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?; @@ -133,13 +138,18 @@ impl<'tx> Transaction<'tx> { // reselect the PIV applet let sw = Apdu::new(Ins::SelectApplication) .p1(0x04) - .data(PIV_AID) + .data(piv::APPLET_ID) .transmit(self, 0xFF)? .status_words(); if !sw.is_success() { error!("failed selecting application: {:04x}", sw.code()); - return Err(Error::GenericError); + return Err(match sw { + StatusWords::NotFoundError => Error::AppletNotFound { + applet_name: piv::APPLET_NAME, + }, + _ => Error::GenericError, + }); } response.data().try_into() diff --git a/src/yubikey.rs b/src/yubikey.rs index 94507b2a..39ef400b 100644 --- a/src/yubikey.rs +++ b/src/yubikey.rs @@ -55,6 +55,7 @@ use { apdu::StatusWords, consts::{TAG_ADMIN_FLAGS_1, TAG_ADMIN_TIMESTAMP}, metadata::AdminData, + mgm, transaction::ChangeRefAction, Buffer, ObjectId, }, @@ -71,12 +72,6 @@ pub(crate) const ALGO_3DES: u8 = 0x03; /// Card management key pub(crate) const KEY_CARDMGM: u8 = 0x9b; -/// MGMT Applet ID. -/// -/// -#[cfg(feature = "untested")] -const MGMT_AID: [u8; 8] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17]; - const TAG_DYN_AUTH: u8 = 0x7c; /// Cached YubiKey PIN. @@ -387,7 +382,7 @@ impl YubiKey { let status_words = Apdu::new(Ins::SelectApplication) .p1(0x04) - .data(MGMT_AID) + .data(mgm::APPLET_ID) .transmit(&txn, 255)? .status_words(); @@ -396,7 +391,12 @@ impl YubiKey { "Failed selecting mgmt application: {:04x}", status_words.code() ); - return Err(Error::GenericError); + return Err(match status_words { + StatusWords::NotFoundError => Error::AppletNotFound { + applet_name: mgm::APPLET_NAME, + }, + _ => Error::GenericError, + }); } Ok(()) @@ -682,23 +682,40 @@ impl<'a> TryFrom<&'a Reader<'_>> for YubiKey { info!("connected to reader: {}", reader.name()); - let (version, serial) = { + let mut app_version_serial = || -> Result<(Version, Serial)> { let txn = Transaction::new(&mut card)?; txn.select_application()?; let v = txn.get_version()?; let s = txn.get_serial(v)?; - (v, s) + Ok((v, s)) }; - let yubikey = YubiKey { - card, - name: String::from(reader.name()), - pin: None, - version, - serial, - }; + match app_version_serial() { + Err(e) => { + error!("Could not use reader: {}", e); - Ok(yubikey) + // We were unable to use the card, so we've effectively only connected as + // a side-effect of determining this. Avoid disrupting its internal state + // any further (e.g. preserve the PIN cache of whatever applet is selected + // currently). + if let Err((_, e)) = card.disconnect(pcsc::Disposition::LeaveCard) { + error!("Failed to disconnect gracefully from card: {}", e); + } + + Err(e) + } + Ok((version, serial)) => { + let yubikey = YubiKey { + card, + name: String::from(reader.name()), + pin: None, + version, + serial, + }; + + Ok(yubikey) + } + } } }