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:
@@ -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
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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
@@ -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")]
|
||||||
|
|||||||
@@ -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];
|
||||||
@@ -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
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user