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
This commit is contained in:
str4d
2023-02-12 17:20:34 +00:00
committed by GitHub
parent 10241230b3
commit d55079f9a6
8 changed files with 99 additions and 31 deletions
+10
View File
@@ -8,6 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- `YubiKey::disconnect` - `YubiKey::disconnect`
- `impl Debug for {Context, YubiKey}` - `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) ## 0.7.0 (2022-11-14)
### Added ### Added
+9
View File
@@ -45,6 +45,12 @@ pub enum Error {
/// Applet error /// Applet error
AppletError, 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 /// Argument error
ArgumentError, ArgumentError,
@@ -125,6 +131,9 @@ impl Error {
match self { match self {
Error::AlgorithmError => f.write_str("algorithm error"), Error::AlgorithmError => f.write_str("algorithm error"),
Error::AppletError => f.write_str("applet 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::ArgumentError => f.write_str("argument error"),
Error::AuthenticationError => f.write_str("authentication error"), Error::AuthenticationError => f.write_str("authentication error"),
Error::GenericError => f.write_str("generic error"), Error::GenericError => f.write_str("generic error"),
+1
View File
@@ -49,6 +49,7 @@ mod mgm;
mod mscmap; mod mscmap;
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
mod msroots; mod msroots;
mod otp;
pub mod piv; pub mod piv;
mod policy; mod policy;
pub mod reader; pub mod reader;
+10
View File
@@ -48,6 +48,16 @@ use des::{
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
use {hmac::Hmac, pbkdf2::pbkdf2, sha1::Sha1}; 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.
///
/// <https://developers.yubico.com/PIV/Introduction/Admin_access.html>
#[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; pub(crate) const ADMIN_FLAGS_1_PROTECTED_MGM: u8 = 0x02;
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
+5
View File
@@ -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];
+6
View File
@@ -73,6 +73,12 @@ use {
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
use zeroize::Zeroizing; 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_POINTP256: usize = 65;
const CB_ECC_POINTP384: usize = 97; const CB_ECC_POINTP384: usize = 97;
+23 -13
View File
@@ -5,7 +5,8 @@ use crate::{
apdu::{Apdu, Ins, StatusWords}, apdu::{Apdu, Ins, StatusWords},
consts::{CB_BUF_MAX, CB_OBJ_MAX}, consts::{CB_BUF_MAX, CB_OBJ_MAX},
error::{Error, Result}, error::{Error, Result},
piv::{AlgorithmId, SlotId}, otp,
piv::{self, AlgorithmId, SlotId},
serialization::*, serialization::*,
yubikey::*, yubikey::*,
Buffer, ObjectId, Buffer, ObjectId,
@@ -16,12 +17,6 @@ use zeroize::Zeroizing;
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
use crate::mgm::{MgmKey, DES_LEN_3DES}; 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; const CB_PIN_MAX: usize = 8;
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
@@ -69,7 +64,7 @@ impl<'tx> Transaction<'tx> {
pub fn select_application(&self) -> Result<()> { pub fn select_application(&self) -> Result<()> {
let response = Apdu::new(Ins::SelectApplication) let response = Apdu::new(Ins::SelectApplication)
.p1(0x04) .p1(0x04)
.data(PIV_AID) .data(piv::APPLET_ID)
.transmit(self, 0xFF) .transmit(self, 0xFF)
.map_err(|e| { .map_err(|e| {
error!("failed communicating with card: '{}'", e); error!("failed communicating with card: '{}'", e);
@@ -81,7 +76,12 @@ impl<'tx> Transaction<'tx> {
"failed selecting application: {:04x}", "failed selecting application: {:04x}",
response.status_words().code() 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(()) Ok(())
@@ -110,13 +110,18 @@ impl<'tx> Transaction<'tx> {
4 => { 4 => {
let sw = Apdu::new(Ins::SelectApplication) let sw = Apdu::new(Ins::SelectApplication)
.p1(0x04) .p1(0x04)
.data(YK_AID) .data(otp::APPLET_ID)
.transmit(self, 0xFF)? .transmit(self, 0xFF)?
.status_words(); .status_words();
if !sw.is_success() { if !sw.is_success() {
error!("failed selecting yk application: {:04x}", sw.code()); 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)?; let response = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?;
@@ -133,13 +138,18 @@ impl<'tx> Transaction<'tx> {
// reselect the PIV applet // reselect the PIV applet
let sw = Apdu::new(Ins::SelectApplication) let sw = Apdu::new(Ins::SelectApplication)
.p1(0x04) .p1(0x04)
.data(PIV_AID) .data(piv::APPLET_ID)
.transmit(self, 0xFF)? .transmit(self, 0xFF)?
.status_words(); .status_words();
if !sw.is_success() { if !sw.is_success() {
error!("failed selecting application: {:04x}", sw.code()); 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() response.data().try_into()
+27 -10
View File
@@ -55,6 +55,7 @@ use {
apdu::StatusWords, apdu::StatusWords,
consts::{TAG_ADMIN_FLAGS_1, TAG_ADMIN_TIMESTAMP}, consts::{TAG_ADMIN_FLAGS_1, TAG_ADMIN_TIMESTAMP},
metadata::AdminData, metadata::AdminData,
mgm,
transaction::ChangeRefAction, transaction::ChangeRefAction,
Buffer, ObjectId, Buffer, ObjectId,
}, },
@@ -71,12 +72,6 @@ pub(crate) const ALGO_3DES: u8 = 0x03;
/// Card management key /// Card management key
pub(crate) const KEY_CARDMGM: u8 = 0x9b; pub(crate) const KEY_CARDMGM: u8 = 0x9b;
/// MGMT Applet ID.
///
/// <https://developers.yubico.com/PIV/Introduction/Admin_access.html>
#[cfg(feature = "untested")]
const MGMT_AID: [u8; 8] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17];
const TAG_DYN_AUTH: u8 = 0x7c; const TAG_DYN_AUTH: u8 = 0x7c;
/// Cached YubiKey PIN. /// Cached YubiKey PIN.
@@ -387,7 +382,7 @@ impl YubiKey {
let status_words = Apdu::new(Ins::SelectApplication) let status_words = Apdu::new(Ins::SelectApplication)
.p1(0x04) .p1(0x04)
.data(MGMT_AID) .data(mgm::APPLET_ID)
.transmit(&txn, 255)? .transmit(&txn, 255)?
.status_words(); .status_words();
@@ -396,7 +391,12 @@ impl YubiKey {
"Failed selecting mgmt application: {:04x}", "Failed selecting mgmt application: {:04x}",
status_words.code() status_words.code()
); );
return Err(Error::GenericError); return Err(match status_words {
StatusWords::NotFoundError => Error::AppletNotFound {
applet_name: mgm::APPLET_NAME,
},
_ => Error::GenericError,
});
} }
Ok(()) Ok(())
@@ -682,15 +682,30 @@ impl<'a> TryFrom<&'a Reader<'_>> for YubiKey {
info!("connected to reader: {}", reader.name()); info!("connected to reader: {}", reader.name());
let (version, serial) = { let mut app_version_serial = || -> Result<(Version, Serial)> {
let txn = Transaction::new(&mut card)?; let txn = Transaction::new(&mut card)?;
txn.select_application()?; txn.select_application()?;
let v = txn.get_version()?; let v = txn.get_version()?;
let s = txn.get_serial(v)?; let s = txn.get_serial(v)?;
(v, s) Ok((v, s))
}; };
match app_version_serial() {
Err(e) => {
error!("Could not use reader: {}", e);
// 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 { let yubikey = YubiKey {
card, card,
name: String::from(reader.name()), name: String::from(reader.name()),
@@ -701,4 +716,6 @@ impl<'a> TryFrom<&'a Reader<'_>> for YubiKey {
Ok(yubikey) Ok(yubikey)
} }
}
}
} }