readers: Use Reader to connect to YubiKey

Removes the legacy API inherited from `yubico-piv-tool` and uses
the `reader` module exclusively for selecting and opening the PC/SC
reader.
This commit is contained in:
Tony Arcieri
2019-12-02 10:05:17 -08:00
parent 589ca3de12
commit 9ce2ffe938
3 changed files with 96 additions and 94 deletions
+11 -5
View File
@@ -1,8 +1,9 @@
//! Support for enumerating available readers //! Support for enumerating available readers
use crate::error::Error; use crate::{error::Error, yubikey::YubiKey};
use std::{ use std::{
borrow::Cow, borrow::Cow,
convert::TryInto,
ffi::CStr, ffi::CStr,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
@@ -20,7 +21,8 @@ pub struct Readers {
} }
impl Readers { impl Readers {
/// Open a PC/SC context /// Open a PC/SC context, which can be used to enumerate available PC/SC
/// readers (which can be used to connect to YubiKeys).
pub fn open() -> Result<Self, Error> { pub fn open() -> Result<Self, Error> {
let ctx = pcsc::Context::establish(pcsc::Scope::System)?; let ctx = pcsc::Context::establish(pcsc::Scope::System)?;
let reader_names = vec![0u8; ctx.list_readers_len()?]; let reader_names = vec![0u8; ctx.list_readers_len()?];
@@ -74,9 +76,13 @@ impl<'ctx> Reader<'ctx> {
self.name.to_string_lossy() self.name.to_string_lossy()
} }
/// Open the given reader /// Open a connection to this reader, returning a `YubiKey` if successful
// TODO(tarcieri): return a `YubiKey` here rather than a `pcsc::Card` pub fn open(&self) -> Result<YubiKey, Error> {
pub fn open(&self) -> Result<pcsc::Card, Error> { self.try_into()
}
/// Connect to this reader, returning its `pcsc::Card`
pub(crate) fn connect(&self) -> Result<pcsc::Card, Error> {
let ctx = self.ctx.lock().unwrap(); let ctx = self.ctx.lock().unwrap();
Ok(ctx.connect(self.name, pcsc::ShareMode::Shared, pcsc::Protocols::T1)?) Ok(ctx.connect(self.name, pcsc::ShareMode::Shared, pcsc::Protocols::T1)?)
} }
+84 -88
View File
@@ -42,14 +42,22 @@ use crate::{
serialization::*, serialization::*,
Buffer, ObjectId, Buffer, ObjectId,
}; };
use crate::{consts::*, error::Error, transaction::Transaction}; use crate::{
consts::*,
error::Error,
readers::{Reader, Readers},
transaction::Transaction,
};
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
use getrandom::getrandom; use getrandom::getrandom;
use log::{error, info, warn}; use log::{error, info, warn};
use pcsc::{Card, Context}; use pcsc::Card;
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
use std::fmt::{self, Display}; use std::{
convert::TryFrom,
fmt::{self, Display},
};
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
use std::{ use std::{
convert::TryInto, convert::TryInto,
@@ -130,96 +138,28 @@ pub struct YubiKey {
} }
impl YubiKey { impl YubiKey {
/// Open a connection to a YubiKey, optionally giving the name /// Open a connection to a YubiKey.
/// (needed if e.g. there are multiple YubiKeys connected). ///
pub fn open(name: Option<&[u8]>) -> Result<YubiKey, Error> { /// Returns an error if there is more than one YubiKey detected.
let context = Context::establish(pcsc::Scope::System)?; ///
let mut card = Self::connect(&context, name)?; /// If you need to operate in environments with more than one YubiKey
/// attached to the same system, use [`yubikey_piv::Readers`] to select
/// from the available PC/SC readers connected.
pub fn open() -> Result<Self, Error> {
let mut readers = Readers::open()?;
let mut reader_iter = readers.iter()?;
let mut is_neo = false; if let Some(reader) = reader_iter.next() {
let version: Version; if reader_iter.next().is_some() {
let serial: Serial; error!("multiple YubiKeys detected!");
return Err(Error::PcscError { inner: None });
{
let txn = Transaction::new(&mut card)?;
let mut atr_buf = [0; CB_ATR_MAX];
let atr = txn.get_attribute(pcsc::Attribute::AtrString, &mut atr_buf)?;
if atr == YKPIV_ATR_NEO_R3 {
is_neo = true;
} }
txn.select_application()?; return reader.open();
// now that the PIV application is selected, retrieve the version
// and serial number. Previously the NEO/YK4 required switching
// to the yk applet to retrieve the serial, YK5 implements this
// as a PIV applet command. Unfortunately, this change requires
// that we retrieve the version number first, so that get_serial
// can determine how to get the serial number, which for the NEO/Yk4
// will result in another selection of the PIV applet.
version = txn.get_version().map_err(|e| {
warn!("failed to retrieve version: '{}'", e);
e
})?;
serial = txn.get_serial(version).map_err(|e| {
warn!("failed to retrieve serial number: '{}'", e);
e
})?;
} }
let yubikey = YubiKey { error!("no YubiKey detected!");
card, Err(Error::GenericError)
pin: None,
is_neo,
version,
serial,
};
Ok(yubikey)
}
/// Connect to a YubiKey PC/SC card.
fn connect(context: &Context, name: Option<&[u8]>) -> Result<Card, Error> {
// ensure PC/SC context is valid
context.is_valid()?;
let buffer_len = context.list_readers_len()?;
let mut buffer = vec![0u8; buffer_len];
for reader in context.list_readers(&mut buffer)? {
if let Some(wanted) = name {
if reader.to_bytes() != wanted {
warn!(
"skipping reader '{}' since it doesn't match '{}'",
reader.to_string_lossy(),
String::from_utf8_lossy(wanted)
);
continue;
}
}
info!("trying to connect to reader '{}'", reader.to_string_lossy());
match context.connect(reader, pcsc::ShareMode::Shared, pcsc::Protocols::T1) {
Ok(card) => {
info!("connected to '{}' successfully", reader.to_string_lossy());
return Ok(card);
}
Err(err) => {
error!(
"skipping '{}' due to connection error: {}",
reader.to_string_lossy(),
err
);
}
}
}
error!("error: no usable reader found");
Err(Error::PcscError { inner: None })
} }
/// Reconnect to a YubiKey /// Reconnect to a YubiKey
@@ -818,3 +758,59 @@ impl YubiKey {
} }
} }
} }
impl<'a> TryFrom<&'a Reader<'_>> for YubiKey {
type Error = Error;
fn try_from(reader: &'a Reader<'_>) -> Result<Self, Error> {
let mut card = reader.connect().map_err(|e| {
error!("error connecting to reader '{}': {}", reader.name(), e);
e
})?;
info!("connected to reader: {}", reader.name());
let mut is_neo = false;
let version: Version;
let serial: Serial;
{
let txn = Transaction::new(&mut card)?;
let mut atr_buf = [0; CB_ATR_MAX];
let atr = txn.get_attribute(pcsc::Attribute::AtrString, &mut atr_buf)?;
if atr == YKPIV_ATR_NEO_R3 {
is_neo = true;
}
txn.select_application()?;
// now that the PIV application is selected, retrieve the version
// and serial number. Previously the NEO/YK4 required switching
// to the yk applet to retrieve the serial, YK5 implements this
// as a PIV applet command. Unfortunately, this change requires
// that we retrieve the version number first, so that get_serial
// can determine how to get the serial number, which for the NEO/Yk4
// will result in another selection of the PIV applet.
version = txn.get_version().map_err(|e| {
warn!("failed to retrieve version: '{}'", e);
e
})?;
serial = txn.get_serial(version).map_err(|e| {
warn!("failed to retrieve serial number: '{}'", e);
e
})?;
}
let yubikey = YubiKey {
card,
pin: None,
is_neo,
version,
serial,
};
Ok(yubikey)
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ fn connect() {
env_logger::builder().format_timestamp(None).init(); env_logger::builder().format_timestamp(None).init();
} }
let mut yubikey = YubiKey::open(None).unwrap(); let mut yubikey = YubiKey::open().unwrap();
dbg!(&yubikey.version()); dbg!(&yubikey.version());
dbg!(&yubikey.serial()); dbg!(&yubikey.serial());
} }