Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Feb 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -397,7 +392,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 @@ -406,7 +401,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 @@ -692,23 +692,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)
}
}
}
}