c30cf5b83a
Adds an off-by-default test that the `YubiKey::verify_pin` function works, and removes it from `untested` gating.
791 lines
25 KiB
Rust
791 lines
25 KiB
Rust
//! YubiKey-related types and communication support
|
|
|
|
// Adapted from yubico-piv-tool:
|
|
// <https://github.com/Yubico/yubico-piv-tool/>
|
|
//
|
|
// Copyright (c) 2014-2016 Yubico AB
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are
|
|
// met:
|
|
//
|
|
// * Redistributions of source code must retain the above copyright
|
|
// notice, this list of conditions and the following disclaimer.
|
|
//
|
|
// * Redistributions in binary form must reproduce the above
|
|
// copyright notice, this list of conditions and the following
|
|
// disclaimer in the documentation and/or other materials provided
|
|
// with the distribution.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
#![allow(non_snake_case, non_upper_case_globals)]
|
|
#![allow(clippy::too_many_arguments, clippy::missing_safety_doc)]
|
|
|
|
#[cfg(feature = "untested")]
|
|
use crate::{
|
|
apdu::{Ins, StatusWords, APDU},
|
|
key::{AlgorithmId, SlotId},
|
|
metadata,
|
|
mgm::MgmKey,
|
|
policy::{PinPolicy, TouchPolicy},
|
|
serialization::*,
|
|
Buffer, ObjectId,
|
|
};
|
|
use crate::{
|
|
consts::*,
|
|
error::Error,
|
|
readers::{Reader, Readers},
|
|
transaction::Transaction,
|
|
};
|
|
#[cfg(feature = "untested")]
|
|
use getrandom::getrandom;
|
|
use log::{error, info, warn};
|
|
use pcsc::Card;
|
|
#[cfg(feature = "untested")]
|
|
use secrecy::ExposeSecret;
|
|
use std::{
|
|
convert::TryFrom,
|
|
fmt::{self, Display},
|
|
};
|
|
#[cfg(feature = "untested")]
|
|
use std::{
|
|
convert::TryInto,
|
|
time::{SystemTime, UNIX_EPOCH},
|
|
};
|
|
#[cfg(feature = "untested")]
|
|
use zeroize::Zeroizing;
|
|
|
|
/// PIV Application ID
|
|
pub const AID: [u8; 5] = [0xa0, 0x00, 0x00, 0x03, 0x08];
|
|
|
|
/// MGMT Application ID.
|
|
/// <https://developers.yubico.com/PIV/Introduction/Admin_access.html>
|
|
pub const MGMT_AID: [u8; 8] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17];
|
|
|
|
/// Cached YubiKey PIN
|
|
pub type CachedPin = secrecy::SecretVec<u8>;
|
|
|
|
/// YubiKey Serial Number
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
|
pub struct Serial(pub u32);
|
|
|
|
impl From<u32> for Serial {
|
|
fn from(num: u32) -> Serial {
|
|
Serial(num)
|
|
}
|
|
}
|
|
|
|
impl From<Serial> for u32 {
|
|
fn from(serial: Serial) -> u32 {
|
|
serial.0
|
|
}
|
|
}
|
|
|
|
impl Display for Serial {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.0)
|
|
}
|
|
}
|
|
|
|
/// YubiKey Version
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
pub struct Version {
|
|
/// Major version component
|
|
pub major: u8,
|
|
|
|
/// Minor version component
|
|
pub minor: u8,
|
|
|
|
/// Patch version component
|
|
pub patch: u8,
|
|
}
|
|
|
|
impl Version {
|
|
/// Parse a version from bytes
|
|
pub fn new(bytes: [u8; 3]) -> Version {
|
|
Version {
|
|
major: bytes[0],
|
|
minor: bytes[1],
|
|
patch: bytes[2],
|
|
}
|
|
}
|
|
}
|
|
|
|
/// YubiKey Device: this is the primary API for opening a session and
|
|
/// performing various operations.
|
|
///
|
|
/// Almost all functionality in this library will require an open session
|
|
/// with a YubiKey which is represented by this type.
|
|
// TODO(tarcieri): reduce coupling to internal fields via `pub(crate)`
|
|
#[cfg_attr(not(feature = "untested"), allow(dead_code))]
|
|
pub struct YubiKey {
|
|
pub(crate) card: Card,
|
|
pub(crate) pin: Option<CachedPin>,
|
|
pub(crate) is_neo: bool,
|
|
pub(crate) version: Version,
|
|
pub(crate) serial: Serial,
|
|
}
|
|
|
|
impl YubiKey {
|
|
/// 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<Self, 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 });
|
|
}
|
|
|
|
return reader.open();
|
|
}
|
|
|
|
error!("no YubiKey detected!");
|
|
Err(Error::GenericError)
|
|
}
|
|
|
|
/// Reconnect to a YubiKey
|
|
#[cfg(feature = "untested")]
|
|
pub fn reconnect(&mut self) -> Result<(), Error> {
|
|
info!("trying to reconnect to current reader");
|
|
|
|
self.card.reconnect(
|
|
pcsc::ShareMode::Shared,
|
|
pcsc::Protocols::T1,
|
|
pcsc::Disposition::ResetCard,
|
|
)?;
|
|
|
|
let pin = self
|
|
.pin
|
|
.as_ref()
|
|
.map(|p| Buffer::new(p.expose_secret().clone()));
|
|
|
|
let txn = Transaction::new(&mut self.card)?;
|
|
txn.select_application()?;
|
|
|
|
if let Some(p) = &pin {
|
|
txn.verify_pin(p)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Begin a transaction.
|
|
pub(crate) fn begin_transaction(&mut self) -> Result<Transaction<'_>, Error> {
|
|
// TODO(tarcieri): reconnect support
|
|
Ok(Transaction::new(&mut self.card)?)
|
|
}
|
|
|
|
/// Get the YubiKey's PIV application version.
|
|
///
|
|
/// This always uses the cached version queried when the key is initialized.
|
|
pub fn version(&mut self) -> Version {
|
|
self.version
|
|
}
|
|
|
|
/// Get YubiKey device serial number.
|
|
///
|
|
/// This always uses the cached version queried when the key is initialized.
|
|
pub fn serial(&mut self) -> Serial {
|
|
self.serial
|
|
}
|
|
|
|
/// Get YubiKey device model
|
|
// TODO(tarcieri): use an emum for this
|
|
#[cfg(feature = "untested")]
|
|
pub fn device_model(&self) -> u32 {
|
|
if self.is_neo {
|
|
DEVTYPE_NEOr3
|
|
} else {
|
|
// TODO(tarcieri): YK5?
|
|
DEVTYPE_YK4
|
|
}
|
|
}
|
|
|
|
/// Authenticate to the card using the provided management key (MGM).
|
|
#[cfg(feature = "untested")]
|
|
pub fn authenticate(&mut self, mgm_key: MgmKey) -> Result<(), Error> {
|
|
let txn = self.begin_transaction()?;
|
|
|
|
const TAG_DYN_AUTH: u8 = 0x7c;
|
|
|
|
// get a challenge from the card
|
|
let challenge = APDU::new(Ins::Authenticate)
|
|
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
|
|
.data(&[TAG_DYN_AUTH, 0x02, 0x80, 0x00])
|
|
.transmit(&txn, 261)?;
|
|
|
|
if !challenge.is_success() || challenge.data().len() < 12 {
|
|
return Err(Error::AuthenticationError);
|
|
}
|
|
|
|
// send a response to the cards challenge and a challenge of our own.
|
|
let response = mgm_key.decrypt(challenge.data()[4..12].try_into().unwrap());
|
|
|
|
let mut data = [0u8; 22];
|
|
data[0] = TAG_DYN_AUTH;
|
|
data[1] = 20; // 2 + 8 + 2 +8
|
|
data[2] = 0x80;
|
|
data[3] = 8;
|
|
data[4..12].copy_from_slice(&response);
|
|
data[12] = 0x81;
|
|
data[13] = 8;
|
|
|
|
if getrandom(&mut data[14..22]).is_err() {
|
|
error!("failed getting randomness for authentication");
|
|
return Err(Error::RandomnessError);
|
|
}
|
|
|
|
let mut challenge = [0u8; 8];
|
|
challenge.copy_from_slice(&data[14..22]);
|
|
|
|
let authentication = APDU::new(Ins::Authenticate)
|
|
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
|
|
.data(&data)
|
|
.transmit(&txn, 261)?;
|
|
|
|
if !authentication.is_success() {
|
|
return Err(Error::AuthenticationError);
|
|
}
|
|
|
|
// compare the response from the card with our challenge
|
|
let response = mgm_key.encrypt(&challenge);
|
|
|
|
use subtle::ConstantTimeEq;
|
|
if response.ct_eq(&authentication.data()[4..12]).unwrap_u8() != 1 {
|
|
return Err(Error::AuthenticationError);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Deauthenticate
|
|
#[cfg(feature = "untested")]
|
|
pub fn deauthenticate(&mut self) -> Result<(), Error> {
|
|
let txn = self.begin_transaction()?;
|
|
|
|
let status_words = APDU::new(Ins::SelectApplication)
|
|
.p1(0x04)
|
|
.data(MGMT_AID)
|
|
.transmit(&txn, 255)?
|
|
.status_words();
|
|
|
|
if !status_words.is_success() {
|
|
error!(
|
|
"Failed selecting mgmt application: {:04x}",
|
|
status_words.code()
|
|
);
|
|
return Err(Error::GenericError);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Sign data using a PIV key
|
|
#[cfg(feature = "untested")]
|
|
pub fn sign_data(
|
|
&mut self,
|
|
raw_in: &[u8],
|
|
algorithm: AlgorithmId,
|
|
key: SlotId,
|
|
) -> Result<Buffer, Error> {
|
|
let txn = self.begin_transaction()?;
|
|
|
|
// don't attempt to reselect in crypt operations to avoid problems with PIN_ALWAYS
|
|
txn.authenticated_command(raw_in, algorithm, key, false)
|
|
}
|
|
|
|
/// Decrypt data using a PIV key
|
|
#[cfg(feature = "untested")]
|
|
pub fn decrypt_data(
|
|
&mut self,
|
|
input: &[u8],
|
|
algorithm: AlgorithmId,
|
|
key: SlotId,
|
|
) -> Result<Buffer, Error> {
|
|
let txn = self.begin_transaction()?;
|
|
|
|
// don't attempt to reselect in crypt operations to avoid problems with PIN_ALWAYS
|
|
txn.authenticated_command(input, algorithm, key, true)
|
|
}
|
|
|
|
/// Verify device PIN.
|
|
pub fn verify_pin(&mut self, pin: &[u8]) -> Result<(), Error> {
|
|
{
|
|
let txn = self.begin_transaction()?;
|
|
txn.verify_pin(pin)?;
|
|
}
|
|
|
|
if !pin.is_empty() {
|
|
self.pin = Some(CachedPin::new(pin.into()))
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the number of PIN retries
|
|
#[cfg(feature = "untested")]
|
|
pub fn get_pin_retries(&mut self) -> Result<u8, Error> {
|
|
let txn = self.begin_transaction()?;
|
|
|
|
// Force a re-select to unverify, because once verified the spec dictates that
|
|
// subsequent verify calls will return a "verification not needed" instead of
|
|
// the number of tries left...
|
|
txn.select_application()?;
|
|
|
|
// WRONG_PIN is expected on successful query.
|
|
match txn.verify_pin(&[]) {
|
|
Ok(()) => Ok(0), // TODO(tarcieri): verify this matches `yubico-piv-tool`
|
|
Err(Error::WrongPin { tries }) => Ok(tries),
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
/// Set the number of PIN retries
|
|
#[cfg(feature = "untested")]
|
|
pub fn set_pin_retries(&mut self, pin_tries: u8, puk_tries: u8) -> Result<(), Error> {
|
|
// Special case: if either retry count is 0, it's a successful no-op
|
|
if pin_tries == 0 || puk_tries == 0 {
|
|
return Ok(());
|
|
}
|
|
|
|
let txn = self.begin_transaction()?;
|
|
|
|
let templ = [0, Ins::SetPinRetries.code(), pin_tries, puk_tries];
|
|
|
|
let status_words = txn.transfer_data(&templ, &[], 255)?.status_words();
|
|
|
|
match status_words {
|
|
StatusWords::Success => Ok(()),
|
|
StatusWords::AuthBlockedError => Err(Error::AuthenticationError),
|
|
StatusWords::SecurityStatusError => Err(Error::AuthenticationError),
|
|
_ => Err(Error::GenericError),
|
|
}
|
|
}
|
|
|
|
/// Change the Personal Identification Number (PIN).
|
|
///
|
|
/// The default PIN code is 123456
|
|
#[cfg(feature = "untested")]
|
|
pub fn change_pin(&mut self, current_pin: &[u8], new_pin: &[u8]) -> Result<(), Error> {
|
|
{
|
|
let txn = self.begin_transaction()?;
|
|
txn.change_pin(CHREF_ACT_CHANGE_PIN, current_pin, new_pin)?;
|
|
}
|
|
|
|
if !new_pin.is_empty() {
|
|
self.pin = Some(CachedPin::new(new_pin.into()));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Set PIN last changed
|
|
#[cfg(feature = "untested")]
|
|
pub fn set_pin_last_changed(yubikey: &mut YubiKey) -> Result<(), Error> {
|
|
let mut data = [0u8; CB_BUF_MAX];
|
|
let max_size = yubikey.obj_size_max();
|
|
let txn = yubikey.begin_transaction()?;
|
|
|
|
let buffer = metadata::read(&txn, TAG_ADMIN)?;
|
|
let mut cb_data = buffer.len();
|
|
data[..cb_data].copy_from_slice(&buffer);
|
|
|
|
// TODO(tarcieri): double check this is little endian
|
|
let tnow = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs()
|
|
.to_le_bytes();
|
|
|
|
metadata::set_item(
|
|
&mut data,
|
|
&mut cb_data,
|
|
CB_OBJ_MAX,
|
|
TAG_ADMIN_TIMESTAMP,
|
|
&tnow,
|
|
)
|
|
.map_err(|e| {
|
|
error!("could not set pin timestamp, err = {}", e);
|
|
e
|
|
})?;
|
|
|
|
metadata::write(&txn, TAG_ADMIN, &data, max_size).map_err(|e| {
|
|
error!("could not write admin data, err = {}", e);
|
|
e
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Change the PIN Unblocking Key (PUK). PUKs are codes for resetting
|
|
/// lost/forgotten PINs, or devices that have become blocked because of too
|
|
/// many failed attempts.
|
|
///
|
|
/// The PUK is part of the PIV standard that the YubiKey follows.
|
|
///
|
|
/// The default PUK code is 12345678.
|
|
#[cfg(feature = "untested")]
|
|
pub fn change_puk(&mut self, current_puk: &[u8], new_puk: &[u8]) -> Result<(), Error> {
|
|
let txn = self.begin_transaction()?;
|
|
txn.change_pin(CHREF_ACT_CHANGE_PUK, current_puk, new_puk)
|
|
}
|
|
|
|
/// Block PUK: permanently prevent the PIN from becoming unblocked
|
|
#[cfg(feature = "untested")]
|
|
pub fn block_puk(yubikey: &mut YubiKey) -> Result<(), Error> {
|
|
let mut puk = [0x30, 0x42, 0x41, 0x44, 0x46, 0x30, 0x30, 0x44];
|
|
let mut tries_remaining: i32 = -1;
|
|
let mut flags = [0];
|
|
|
|
let max_size = yubikey.obj_size_max();
|
|
let txn = yubikey.begin_transaction()?;
|
|
|
|
while tries_remaining != 0 {
|
|
// 2 -> change puk
|
|
let res = txn.change_pin(CHREF_ACT_CHANGE_PUK, &puk, &puk);
|
|
|
|
match res {
|
|
Ok(()) => puk[0] += 1,
|
|
Err(Error::WrongPin { tries }) => {
|
|
tries_remaining = tries as i32;
|
|
continue;
|
|
}
|
|
Err(e) => {
|
|
// depending on the firmware, tries may not be set to zero when the PUK is blocked,
|
|
// instead, the return code will be PIN_LOCKED and tries will be unset
|
|
if e != Error::PinLocked {
|
|
continue;
|
|
}
|
|
tries_remaining = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Ok(data) = metadata::read(&txn, TAG_ADMIN) {
|
|
if let Ok(item) = metadata::get_item(&data, TAG_ADMIN_FLAGS_1) {
|
|
if item.len() == flags.len() {
|
|
flags.copy_from_slice(item)
|
|
} else {
|
|
error!(
|
|
"admin flags exist, but are incorrect size: {} (expected {})",
|
|
item.len(),
|
|
flags.len()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
flags[0] |= ADMIN_FLAGS_1_PUK_BLOCKED;
|
|
let mut data = [0u8; CB_BUF_MAX];
|
|
let mut cb_data: usize = data.len();
|
|
|
|
if metadata::set_item(
|
|
&mut data,
|
|
&mut cb_data,
|
|
CB_OBJ_MAX,
|
|
TAG_ADMIN_FLAGS_1,
|
|
&flags,
|
|
)
|
|
.is_ok()
|
|
{
|
|
if metadata::write(&txn, TAG_ADMIN, &data[..cb_data], max_size).is_err() {
|
|
error!("could not write admin metadata");
|
|
}
|
|
} else {
|
|
error!("could not set admin flags");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Unblock a Personal Identification Number (PIN) using a previously
|
|
/// configured PIN Unblocking Key (PUK).
|
|
#[cfg(feature = "untested")]
|
|
pub fn unblock_pin(&mut self, puk: &[u8], new_pin: &[u8]) -> Result<(), Error> {
|
|
let txn = self.begin_transaction()?;
|
|
txn.change_pin(CHREF_ACT_UNBLOCK_PIN, puk, new_pin)
|
|
}
|
|
|
|
/// Fetch an object from the YubiKey
|
|
#[cfg(feature = "untested")]
|
|
pub fn fetch_object(&mut self, object_id: ObjectId) -> Result<Buffer, Error> {
|
|
let txn = self.begin_transaction()?;
|
|
txn.fetch_object(object_id)
|
|
}
|
|
|
|
/// Save an object
|
|
#[cfg(feature = "untested")]
|
|
pub fn save_object(&mut self, object_id: ObjectId, indata: &mut [u8]) -> Result<(), Error> {
|
|
let txn = self.begin_transaction()?;
|
|
txn.save_object(object_id, indata)
|
|
}
|
|
|
|
/// Import a private encryption or signing key into the YubiKey
|
|
// TODO(tarcieri): refactor this into separate methods per key type
|
|
#[cfg(feature = "untested")]
|
|
pub fn import_private_key(
|
|
&mut self,
|
|
key: SlotId,
|
|
algorithm: AlgorithmId,
|
|
p: Option<&[u8]>,
|
|
q: Option<&[u8]>,
|
|
dp: Option<&[u8]>,
|
|
dq: Option<&[u8]>,
|
|
qinv: Option<&[u8]>,
|
|
ec_data: Option<&[u8]>,
|
|
pin_policy: PinPolicy,
|
|
touch_policy: TouchPolicy,
|
|
) -> Result<(), Error> {
|
|
let mut key_data = Zeroizing::new(vec![0u8; 1024]);
|
|
let templ = [0, Ins::ImportKey.code(), algorithm.into(), key.into()];
|
|
|
|
let (elem_len, params, param_tag) = match algorithm {
|
|
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => match (p, q, dp, dq, qinv) {
|
|
(Some(p), Some(q), Some(dp), Some(dq), Some(qinv)) => {
|
|
if p.len() + q.len() + dp.len() + dq.len() + qinv.len() >= key_data.len() {
|
|
return Err(Error::SizeError);
|
|
}
|
|
|
|
(
|
|
match algorithm {
|
|
AlgorithmId::Rsa1024 => 64,
|
|
AlgorithmId::Rsa2048 => 128,
|
|
_ => unreachable!(),
|
|
},
|
|
vec![p, q, dp, dq, qinv],
|
|
0x01,
|
|
)
|
|
}
|
|
_ => return Err(Error::GenericError),
|
|
},
|
|
AlgorithmId::EccP256 | AlgorithmId::EccP384 => match ec_data {
|
|
Some(ec_data) => {
|
|
if ec_data.len() >= key_data.len() {
|
|
// This can never be true, but check to be explicit.
|
|
return Err(Error::SizeError);
|
|
}
|
|
|
|
(
|
|
match algorithm {
|
|
AlgorithmId::EccP256 => 32,
|
|
AlgorithmId::EccP384 => 48,
|
|
_ => unreachable!(),
|
|
},
|
|
vec![ec_data],
|
|
0x06,
|
|
)
|
|
}
|
|
_ => return Err(Error::GenericError),
|
|
},
|
|
};
|
|
|
|
let mut offset = 0;
|
|
|
|
for (i, param) in params.into_iter().enumerate() {
|
|
key_data[offset] = param_tag + i as u8;
|
|
offset += 1;
|
|
|
|
offset += set_length(&mut key_data[offset..], elem_len);
|
|
|
|
let padding = elem_len - param.len();
|
|
let remaining = key_data.len() - offset;
|
|
|
|
if padding > remaining {
|
|
return Err(Error::AlgorithmError);
|
|
}
|
|
|
|
for b in &mut key_data[offset..offset + padding] {
|
|
*b = 0;
|
|
}
|
|
offset += padding;
|
|
key_data[offset..offset + param.len()].copy_from_slice(param);
|
|
offset += param.len();
|
|
}
|
|
|
|
offset += pin_policy.write(&mut key_data[offset..]);
|
|
offset += touch_policy.write(&mut key_data[offset..]);
|
|
|
|
let txn = self.begin_transaction()?;
|
|
|
|
let status_words = txn
|
|
.transfer_data(&templ, &key_data[..offset], 256)?
|
|
.status_words();
|
|
|
|
match status_words {
|
|
StatusWords::Success => Ok(()),
|
|
StatusWords::SecurityStatusError => Err(Error::AuthenticationError),
|
|
_ => Err(Error::GenericError),
|
|
}
|
|
}
|
|
|
|
/// Generate an attestation certificate for a stored key.
|
|
/// <https://developers.yubico.com/PIV/Introduction/PIV_attestation.html>
|
|
#[cfg(feature = "untested")]
|
|
pub fn attest(&mut self, key: SlotId) -> Result<Buffer, Error> {
|
|
let templ = [0, Ins::Attest.code(), key.into(), 0];
|
|
let txn = self.begin_transaction()?;
|
|
let response = txn.transfer_data(&templ, &[], CB_OBJ_MAX)?;
|
|
|
|
if !response.is_success() {
|
|
if response.status_words() == StatusWords::NotSupportedError {
|
|
return Err(Error::NotSupported);
|
|
} else {
|
|
return Err(Error::GenericError);
|
|
}
|
|
}
|
|
|
|
if response.data()[0] != 0x30 {
|
|
return Err(Error::GenericError);
|
|
}
|
|
|
|
Ok(Buffer::new(response.data().into()))
|
|
}
|
|
|
|
/// Get an auth challenge
|
|
#[cfg(feature = "untested")]
|
|
pub fn get_auth_challenge(&mut self) -> Result<[u8; 8], Error> {
|
|
let txn = self.begin_transaction()?;
|
|
|
|
let response = APDU::new(Ins::Authenticate)
|
|
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
|
|
.data(&[0x7c, 0x02, 0x81, 0x00])
|
|
.transmit(&txn, 261)?;
|
|
|
|
if !response.is_success() {
|
|
return Err(Error::AuthenticationError);
|
|
}
|
|
|
|
Ok(response.data()[4..12].try_into().unwrap())
|
|
}
|
|
|
|
/// Verify an auth response
|
|
#[cfg(feature = "untested")]
|
|
pub fn verify_auth_response(&mut self, response: [u8; 8]) -> Result<(), Error> {
|
|
let mut data = [0u8; 12];
|
|
data[0] = 0x7c;
|
|
data[1] = 0x0a;
|
|
data[2] = 0x82;
|
|
data[3] = 0x08;
|
|
data[4..12].copy_from_slice(&response);
|
|
|
|
let txn = self.begin_transaction()?;
|
|
|
|
// send the response to the card and a challenge of our own.
|
|
let status_words = APDU::new(Ins::Authenticate)
|
|
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
|
|
.data(&data)
|
|
.transmit(&txn, 261)?
|
|
.status_words();
|
|
|
|
if !status_words.is_success() {
|
|
return Err(Error::AuthenticationError);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Reset YubiKey.
|
|
///
|
|
/// WARNING: this is a destructive operation which will destroy all keys!
|
|
///
|
|
/// The reset function is only available when both pins are blocked.
|
|
#[cfg(feature = "untested")]
|
|
pub fn reset_device(&mut self) -> Result<(), Error> {
|
|
let templ = [0, Ins::Reset.code(), 0, 0];
|
|
let txn = self.begin_transaction()?;
|
|
let status_words = txn.transfer_data(&templ, &[], 255)?.status_words();
|
|
|
|
if !status_words.is_success() {
|
|
return Err(Error::GenericError);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get max object size supported by this device
|
|
#[cfg(feature = "untested")]
|
|
pub(crate) fn obj_size_max(&self) -> usize {
|
|
if self.is_neo {
|
|
CB_OBJ_MAX_NEO
|
|
} else {
|
|
CB_OBJ_MAX
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|