Skip to content

Commit

Permalink
readers: Use Reader to connect to YubiKey
Browse files Browse the repository at this point in the history
Removes the legacy API inherited from `yubico-piv-tool` and uses
the `reader` module exclusively for selecting and opening the PC/SC
reader.
  • Loading branch information
tarcieri committed Dec 2, 2019
1 parent 589ca3d commit f4d6ff1
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 95 deletions.
16 changes: 11 additions & 5 deletions src/readers.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//! Support for enumerating available readers
use crate::error::Error;
use crate::{error::Error, yubikey::YubiKey};
use std::{
borrow::Cow,
convert::TryInto,
ffi::CStr,
sync::{Arc, Mutex},
};
Expand All @@ -20,7 +21,8 @@ pub struct 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> {
let ctx = pcsc::Context::establish(pcsc::Scope::System)?;
let reader_names = vec![0u8; ctx.list_readers_len()?];
Expand Down Expand Up @@ -74,9 +76,13 @@ impl<'ctx> Reader<'ctx> {
self.name.to_string_lossy()
}

/// Open the given reader
// TODO(tarcieri): return a `YubiKey` here rather than a `pcsc::Card`
pub fn open(&self) -> Result<pcsc::Card, Error> {
/// Open a connection to this reader, returning a `YubiKey` if successful
pub fn open(&self) -> Result<YubiKey, 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();
Ok(ctx.connect(self.name, pcsc::ShareMode::Shared, pcsc::Protocols::T1)?)
}
Expand Down
174 changes: 85 additions & 89 deletions src/yubikey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,22 @@ use crate::{
serialization::*,
Buffer, ObjectId,
};
use crate::{consts::*, error::Error, transaction::Transaction};
use crate::{
consts::*,
error::Error,
readers::{Reader, Readers},
transaction::Transaction,
};
#[cfg(feature = "untested")]
use getrandom::getrandom;
use log::{error, info, warn};
use pcsc::{Card, Context};
use pcsc::Card;
#[cfg(feature = "untested")]
use secrecy::ExposeSecret;
use std::fmt::{self, Display};
use std::{
convert::TryFrom,
fmt::{self, Display},
};
#[cfg(feature = "untested")]
use std::{
convert::TryInto,
Expand Down Expand Up @@ -130,96 +138,28 @@ pub struct YubiKey {
}

impl YubiKey {
/// Open a connection to a YubiKey, optionally giving the name
/// (needed if e.g. there are multiple YubiKeys connected).
pub fn open(name: Option<&[u8]>) -> Result<YubiKey, Error> {
let context = Context::establish(pcsc::Scope::System)?;
let mut card = Self::connect(&context, 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;
/// Open a connection to a YubiKey.
///
/// Returns an error if there is more than one YubiKey detected.
///
/// 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<YubiKey, Error> {
let mut readers = Readers::open()?;
let mut reader_iter = readers.iter()?;

if let Some(reader) = reader_iter.next() {
if reader_iter.next().is_some() {
error!("multiple YubiKeys detected!");
return Err(Error::PcscError { inner: None });
}

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
})?;
return reader.open();
}

let yubikey = YubiKey {
card,
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 })
error!("no YubiKey detected!");
Err(Error::GenericError)
}

/// Reconnect to a YubiKey
Expand Down Expand Up @@ -818,3 +758,59 @@ impl YubiKey {
}
}
}

impl<'a> TryFrom<&'a Reader<'_>> for YubiKey {
type Error = Error;

fn try_from(reader: &'a Reader<'_>) -> Result<YubiKey, 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)
}
}
2 changes: 1 addition & 1 deletion tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ fn connect() {
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.serial());
}

0 comments on commit f4d6ff1

Please sign in to comment.