Merge pull request #30 from tarcieri/untested-feature

Add `untested` Cargo feature for untested functionality
This commit is contained in:
Tony Arcieri
2019-11-25 15:16:56 -08:00
committed by GitHub
8 changed files with 146 additions and 64 deletions
+16
View File
@@ -59,6 +59,14 @@ jobs:
command: test
args: --release
- name: Run cargo build --all-features
uses: actions-rs/cargo@v1
env:
RUSTFLAGS: -D warnings
with:
command: build
args: --all-features
test:
name: Test Suite
strategy:
@@ -88,6 +96,14 @@ jobs:
command: test
args: --release
- name: Run cargo build --all-features
uses: actions-rs/cargo@v1
env:
RUSTFLAGS: -D warnings
with:
command: build
args: --all-features
fmt:
name: Rustfmt
runs-on: ubuntu-latest
+13 -5
View File
@@ -7,7 +7,6 @@ application providing general-purpose public-key signing and encryption
with hardware-backed private keys for RSA (2048/1024) and ECC (P-256/P-384)
algorithms (e.g, PKCS#1v1.5, ECDSA)
"""
authors = ["Tony Arcieri <bascule@gmail.com>", "Yubico AB"]
edition = "2018"
license = "BSD-2-Clause"
@@ -16,16 +15,25 @@ readme = "README.md"
categories = ["api-bindings", "cryptography", "hardware-support"]
keywords = ["ccid", "ecdsa", "rsa", "piv", "yubikey"]
[badges]
maintenance = { status = "experimental" }
[dependencies]
des = "0.3"
getrandom = "0.1"
hmac = "0.7"
hmac = { version = "0.7", optional = true }
log = "0.4"
pbkdf2 = "0.3"
pbkdf2 = { version = "0.3", optional = true }
pcsc = "2"
sha-1 = "0.8"
subtle = "2"
sha-1 = { version = "0.8", optional = true }
subtle = { version = "2", optional = true }
zeroize = "1"
[dev-dependencies]
env_logger = "0.7"
[features]
untested = ["hmac", "pbkdf2", "sha-1", "subtle"]
[package.metadata.docs.rs]
all-features = true
+5 -2
View File
@@ -80,8 +80,11 @@ Legend:
| 🚧 | Testing and validation in progress |
| ⚠️ | Untested support |
NOTE: Commands marked ⚠️ have not been properly tested and may contain bugs or
not work at all.
NOTE: Commands marked ⚠️ are disabled by default as they have have not been properly tested and may contain bugs or
not work at all. USE AT YOUR OWN RISK!
Enable the `untested` feature in your `Cargo.toml` to enable features marked ⚠️
above.
## Testing
+2
View File
@@ -71,6 +71,7 @@ impl APDU {
}
/// Set this APDU's class
#[cfg(feature = "untested")]
pub fn cla(&mut self, value: u8) -> &mut Self {
self.cla = value;
self
@@ -83,6 +84,7 @@ impl APDU {
}
/// Set both parameters for this APDU
#[cfg(feature = "untested")]
pub fn params(&mut self, p1: u8, p2: u8) -> &mut Self {
self.p1 = p1;
self.p2 = p2;
+16 -2
View File
@@ -40,7 +40,7 @@
//! code from upstream [yubico-piv-tool] has been translated into Rust
//! presenting a safe interface, much of it is still untested.
//!
//! Please see the project's README.md for a complete status.
//! Please see the [project's README.md for a complete status][status].
//!
//! ## History
//!
@@ -83,6 +83,7 @@
//! [YubiKey NEO]: https://support.yubico.com/support/solutions/articles/15000006494-yubikey-neo
//! [YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4
//! [YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
//! [status]: https://github.com/tarcieri/yubikey-piv.rs#status
//! [yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
//! [Corrode]: https://github.com/jameysharp/corrode
//! [piv-tool-guide]: https://www.yubico.com/wp-content/uploads/2016/05/Yubico_PIV_Tool_Command_Line_Guide_en.pdf
@@ -134,24 +135,37 @@
)]
mod apdu;
#[cfg(feature = "untested")]
pub mod cccid;
#[cfg(feature = "untested")]
pub mod certificate;
#[cfg(feature = "untested")]
pub mod chuid;
#[cfg(feature = "untested")]
pub mod config;
pub mod consts;
#[cfg(feature = "untested")]
pub mod container;
pub mod error;
#[cfg(feature = "untested")]
pub mod key;
#[cfg(feature = "untested")]
mod metadata;
#[cfg(feature = "untested")]
pub mod mgm;
#[cfg(feature = "untested")]
pub mod msroots;
mod response;
#[cfg(feature = "untested")]
mod serialization;
#[cfg(feature = "untested")]
pub mod settings;
mod transaction;
pub mod yubikey;
pub use self::{key::Key, mgm::MgmKey, yubikey::YubiKey};
#[cfg(feature = "untested")]
pub use self::{key::Key, mgm::MgmKey};
pub use yubikey::YubiKey;
/// Algorithm identifiers
// TODO(tarcieri): make this an enum
+3
View File
@@ -64,6 +64,7 @@ impl Response {
}
/// Create a new response from the given status words and buffer
#[cfg(feature = "untested")]
pub fn new(status_words: StatusWords, buffer: Buffer) -> Response {
Response {
status_words,
@@ -77,6 +78,7 @@ impl Response {
}
/// Get the raw [`StatusWords`] code for this response.
#[cfg(feature = "untested")]
pub fn code(&self) -> u32 {
self.status_words.code()
}
@@ -92,6 +94,7 @@ impl Response {
}
/// Consume this response, returning its buffer
#[cfg(feature = "untested")]
pub fn into_buffer(self) -> Buffer {
self.buffer
}
+13 -6
View File
@@ -1,17 +1,17 @@
//! YubiKey PC/SC transactions
use crate::{apdu::APDU, consts::*, error::Error, yubikey::*, Buffer};
#[cfg(feature = "untested")]
use crate::{
apdu::APDU,
consts::*,
error::Error,
mgm::MgmKey,
response::{Response, StatusWords},
serialization::*,
yubikey::*,
Buffer, ObjectId,
ObjectId,
};
use log::{error, trace};
use std::{convert::TryInto, ptr};
use std::convert::TryInto;
#[cfg(feature = "untested")]
use std::ptr;
use zeroize::Zeroizing;
/// Exclusive transaction with the YubiKey's PC/SC card.
@@ -164,6 +164,7 @@ impl<'tx> Transaction<'tx> {
}
/// Verify device PIN.
#[cfg(feature = "untested")]
pub fn verify_pin(&self, pin: &[u8]) -> Result<(), Error> {
// TODO(tarcieri): allow unpadded (with `0xFF`) PIN shorter than CB_PIN_MAX?
if pin.len() != CB_PIN_MAX {
@@ -184,6 +185,7 @@ impl<'tx> Transaction<'tx> {
}
/// Change the PIN
#[cfg(feature = "untested")]
pub fn change_pin(&self, action: i32, current_pin: &[u8], new_pin: &[u8]) -> Result<(), Error> {
let mut templ = [0, YKPIV_INS_CHANGE_REFERENCE, 0, 0x80];
let mut indata = Zeroizing::new([0u8; 16]);
@@ -243,6 +245,7 @@ impl<'tx> Transaction<'tx> {
}
/// Set the management key (MGM).
#[cfg(feature = "untested")]
pub fn set_mgm_key(&self, new_key: &MgmKey, touch: Option<u8>) -> Result<(), Error> {
let p2 = match touch.unwrap_or_default() {
0 => 0xff,
@@ -276,6 +279,7 @@ impl<'tx> Transaction<'tx> {
/// This is the common backend for all public key encryption and signing
/// operations.
// TODO(tarcieri): refactor this to be less gross/coupled.
#[cfg(feature = "untested")]
#[allow(clippy::too_many_arguments)]
pub(crate) fn authenticated_command(
&self,
@@ -392,6 +396,7 @@ impl<'tx> Transaction<'tx> {
/// messages into smaller APDU-sized messages (using the provided APDU
/// template to construct them), and then sending those via
/// [`Transaction::transmit`].
#[cfg(feature = "untested")]
pub fn transfer_data(
&self,
templ: &[u8],
@@ -475,6 +480,7 @@ impl<'tx> Transaction<'tx> {
}
/// Fetch an object
#[cfg(feature = "untested")]
pub fn fetch_object(&self, object_id: ObjectId) -> Result<Buffer, Error> {
let mut indata = [0u8; 5];
let templ = [0, YKPIV_INS_GET_DATA, 0x3f, 0xff];
@@ -518,6 +524,7 @@ impl<'tx> Transaction<'tx> {
}
/// Save an object
#[cfg(feature = "untested")]
pub fn save_object(&self, object_id: ObjectId, indata: &[u8]) -> Result<(), Error> {
let templ = [0, YKPIV_INS_PUT_DATA, 0x3f, 0xff];
+78 -49
View File
@@ -33,19 +33,24 @@
#![allow(non_snake_case, non_upper_case_globals)]
#![allow(clippy::too_many_arguments, clippy::missing_safety_doc)]
#[cfg(feature = "untested")]
use crate::{
apdu::APDU, consts::*, error::Error, key::SlotId, metadata, mgm::MgmKey, response::StatusWords,
serialization::*, transaction::Transaction, Buffer, ObjectId,
apdu::APDU, key::SlotId, metadata, mgm::MgmKey, response::StatusWords, serialization::*,
ObjectId,
};
use crate::{consts::*, error::Error, transaction::Transaction, Buffer};
#[cfg(feature = "untested")]
use getrandom::getrandom;
use log::{error, info, warn};
use pcsc::{Card, Context};
use std::fmt::{self, Display};
#[cfg(feature = "untested")]
use std::{
convert::TryInto,
fmt::{self, Display},
ptr, slice,
time::{SystemTime, UNIX_EPOCH},
};
#[cfg(feature = "untested")]
use zeroize::Zeroizing;
/// PIV Application ID
@@ -96,6 +101,7 @@ pub struct Version {
/// 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<Buffer>,
@@ -198,6 +204,7 @@ impl YubiKey {
}
/// Reconnect to a YubiKey
#[cfg(feature = "untested")]
pub fn reconnect(&mut self) -> Result<(), Error> {
info!("trying to reconnect to current reader");
@@ -221,12 +228,40 @@ impl YubiKey {
}
/// Begin a transaction.
#[cfg(feature = "untested")]
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()?;
@@ -280,7 +315,30 @@ impl YubiKey {
Ok(())
}
/// Deauthenticate
#[cfg(feature = "untested")]
pub fn deauthenticate(&mut self) -> Result<(), Error> {
let txn = self.begin_transaction()?;
let status_words = APDU::new(YKPIV_INS_SELECT_APPLICATION)
.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],
@@ -296,6 +354,7 @@ impl YubiKey {
}
/// Decrypt data using a PIV key
#[cfg(feature = "untested")]
pub fn decrypt_data(
&mut self,
input: &[u8],
@@ -310,21 +369,8 @@ impl YubiKey {
txn.authenticated_command(input, out, out_len, algorithm, key, true)
}
/// 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
}
/// Verify device PIN.
#[cfg(feature = "untested")]
pub fn verify_pin(&mut self, pin: &[u8]) -> Result<(), Error> {
{
let txn = self.begin_transaction()?;
@@ -339,6 +385,7 @@ impl YubiKey {
}
/// Get the number of PIN retries
#[cfg(feature = "untested")]
pub fn get_pin_retries(&mut self) -> Result<u32, Error> {
let txn = self.begin_transaction()?;
@@ -356,6 +403,7 @@ impl YubiKey {
}
/// Set the number of PIN retries
#[cfg(feature = "untested")]
pub fn set_pin_retries(&mut self, pin_tries: usize, puk_tries: usize) -> Result<(), Error> {
// Special case: if either retry count is 0, it's a successful no-op
if pin_tries == 0 || puk_tries == 0 {
@@ -388,6 +436,7 @@ impl YubiKey {
/// 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()?;
@@ -402,6 +451,7 @@ impl YubiKey {
}
/// 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();
@@ -445,12 +495,14 @@ impl YubiKey {
/// 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;
@@ -519,18 +571,21 @@ impl YubiKey {
/// 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)
@@ -538,6 +593,7 @@ impl YubiKey {
/// 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,
@@ -733,6 +789,7 @@ impl YubiKey {
/// 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, YKPIV_INS_ATTEST, key, 0];
let txn = self.begin_transaction()?;
@@ -754,6 +811,7 @@ impl YubiKey {
}
/// Get an auth challenge
#[cfg(feature = "untested")]
pub fn get_auth_challenge(&mut self) -> Result<[u8; 8], Error> {
let txn = self.begin_transaction()?;
@@ -770,6 +828,7 @@ impl YubiKey {
}
/// 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;
@@ -794,43 +853,12 @@ impl YubiKey {
Ok(())
}
/// Deauthenticate
pub fn deauthenticate(&mut self) -> Result<(), Error> {
let txn = self.begin_transaction()?;
let status_words = APDU::new(YKPIV_INS_SELECT_APPLICATION)
.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(())
}
/// Get YubiKey device model
// TODO(tarcieri): use an emum for this
pub fn device_model(&self) -> u32 {
if self.is_neo {
DEVTYPE_NEOr3
} else {
// TODO(tarcieri): YK5?
DEVTYPE_YK4
}
}
/// 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, YKPIV_INS_RESET, 0, 0];
let txn = self.begin_transaction()?;
@@ -844,6 +872,7 @@ impl YubiKey {
}
/// 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