diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e5445ac..1fadffb 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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 diff --git a/Cargo.toml b/Cargo.toml index a31db29..9094a9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 ", "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 diff --git a/README.md b/README.md index b1bb897..82317d2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/apdu.rs b/src/apdu.rs index ffa52eb..47945b5 100644 --- a/src/apdu.rs +++ b/src/apdu.rs @@ -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; diff --git a/src/lib.rs b/src/lib.rs index 6d16e62..776896a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 diff --git a/src/response.rs b/src/response.rs index 2b80953..8743bf0 100644 --- a/src/response.rs +++ b/src/response.rs @@ -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 } diff --git a/src/transaction.rs b/src/transaction.rs index 62e2910..5f9beea 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -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) -> 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 { 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]; diff --git a/src/yubikey.rs b/src/yubikey.rs index 968e444..22cfeba 100644 --- a/src/yubikey.rs +++ b/src/yubikey.rs @@ -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, @@ -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, 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 { 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 { 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. /// + #[cfg(feature = "untested")] pub fn attest(&mut self, key: SlotId) -> Result { 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