Skip to content

Commit

Permalink
Enable library users to detect if a smart card doesn't support PIV (#476
Browse files Browse the repository at this point in the history
)

* Enable library users to detect if a smart card doesn't support PIV

Closes #456.

* Avoid resetting the card if we fail to select PIV or fetch version/serial
  • Loading branch information
str4d authored Feb 12, 2023
1 parent 1024123 commit d55079f
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 31 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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"),
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ mod mgm;
mod mscmap;
#[cfg(feature = "untested")]
mod msroots;
mod otp;
pub mod piv;
mod policy;
pub mod reader;
Expand Down
10 changes: 10 additions & 0 deletions src/mgm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
/// <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;

#[cfg(feature = "untested")]
Expand Down
5 changes: 5 additions & 0 deletions src/otp.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions src/piv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
36 changes: 23 additions & 13 deletions src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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")]
Expand Down Expand Up @@ -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);
Expand All @@ -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(())
Expand Down Expand Up @@ -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)?;
Expand All @@ -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()
Expand Down
53 changes: 35 additions & 18 deletions src/yubikey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ use {
apdu::StatusWords,
consts::{TAG_ADMIN_FLAGS_1, TAG_ADMIN_TIMESTAMP},
metadata::AdminData,
mgm,
transaction::ChangeRefAction,
Buffer, ObjectId,
},
Expand All @@ -71,12 +72,6 @@ pub(crate) const ALGO_3DES: u8 = 0x03;
/// Card management key
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;

/// Cached YubiKey PIN.
Expand Down Expand Up @@ -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();

Expand All @@ -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(())
Expand Down Expand Up @@ -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)
}
}
}
}

0 comments on commit d55079f

Please sign in to comment.