diff --git a/Cargo.lock b/Cargo.lock index 617a1da..983ccff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -887,6 +887,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.3", +] + [[package]] name = "version_check" version = "0.9.3" @@ -998,6 +1007,7 @@ dependencies = [ "sha2", "subtle", "subtle-encoding", + "uuid", "x509", "x509-parser", "zeroize", diff --git a/Cargo.toml b/Cargo.toml index 4783934..7bfa1f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ sha-1 = "0.9" sha2 = "0.9" subtle = "2" subtle-encoding = "0.5" +uuid = { version = "0.8", features = ["v4"] } x509 = "0.2" x509-parser = "0.9" zeroize = "1" @@ -57,3 +58,4 @@ untested = [] [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/README.md b/README.md index eb31580..2c89393 100644 --- a/README.md +++ b/README.md @@ -53,41 +53,6 @@ an experimental stage and may still contain high-severity issues. USE AT YOUR OWN RISK! -## Status - -This project is a largely incomplete work-in-progress. So far the only -functionality which has actually been tested is connecting to Yubikeys. - -If you're interested helping test functionality, the table below documents -the current status of the project and relevant GitHub issues for various -functions of the YubiKey: - -| | Module | Issue | Description | -|----|---------------|-------|-------------| -| 🚧 | `yubikey` | [#20] | Core functionality: auth, keys, PIN/PUK, encrypt, sign, attest | -| 🚧 | `cccid` | [#21] | Cardholder Capability Container (CCC) IDs | -| 🚧️ | `certificate` | [#22] | Certificates for stored keys | -| 🚧 | `chuid` | [#23] | Cardholder Unique Identifier (CHUID) | -| ✅️ | `config` | [#24] | Support for reading on-key configuration | -| 🚧 | `key` | [#26] | Crypto key management: list, generate, import | -| 🚧 | `mgm` | [#26] | Management Key (MGM) support: set, get, derive | -| ⚠️ | `mscmap` | [#25] | MS Container Map Records | -| ⚠️ | `msroots` | [#28] | `msroots` file: PKCS#7 formatted certificate store for enterprise trusted roots | - -Legend: - -| | Description | -|----|------------------------------------| -| ✅ | Working | -| 🚧 | Testing and validation in progress | -| ⚠️ | Untested support | - -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 To run the full test suite, you'll need a connected YubiKey NEO/4/5 device in diff --git a/src/cccid.rs b/src/cccid.rs index 65ec99e..cae7135 100644 --- a/src/cccid.rs +++ b/src/cccid.rs @@ -32,15 +32,12 @@ use crate::{Error, Result, YubiKey}; use getrandom::getrandom; -use std::fmt::{self, Debug, Display}; +use std::{ + fmt::{self, Debug, Display}, + str, +}; use subtle_encoding::hex; -/// CCCID size -pub const CCCID_SIZE: usize = 14; - -/// CCC size -pub const CCC_SIZE: usize = 51; - /// CCCID offset const CCC_ID_OFFS: usize = 9; @@ -62,28 +59,34 @@ const CCC_TMPL: &[u8] = &[ 0x00, 0xfe, 0x00, ]; -/// Cardholder Capability Container (CCC) Identifier Card ID -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub struct CardId(pub [u8; CCCID_SIZE]); +/// Cardholder Capability Container (CCC) Identifier Card ID. +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub struct CardId(pub [u8; Self::BYTE_SIZE]); impl CardId { + /// CCCID size in bytes + pub const BYTE_SIZE: usize = 14; + /// Generate a random CCC Card ID pub fn generate() -> Result { - let mut id = [0u8; CCCID_SIZE]; + let mut id = [0u8; Self::BYTE_SIZE]; getrandom(&mut id).map_err(|_| Error::RandomnessError)?; Ok(Self(id)) } } -/// Cardholder Capability Container (CCC) Identifier -#[derive(Copy, Clone)] -pub struct Ccc(pub [u8; CCC_SIZE]); +/// Cardholder Capability Container (CCC) Identifier. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct Ccc(pub [u8; Self::BYTE_SIZE]); impl Ccc { + /// CCC size in bytes + pub const BYTE_SIZE: usize = 51; + /// Return CardId component of CCC pub fn card_id(&self) -> Result { - let mut cccid = [0u8; CCCID_SIZE]; - cccid.copy_from_slice(&self.0[CCC_ID_OFFS..(CCC_ID_OFFS + CCCID_SIZE)]); + let mut cccid = [0u8; CardId::BYTE_SIZE]; + cccid.copy_from_slice(&self.0[CCC_ID_OFFS..(CCC_ID_OFFS + CardId::BYTE_SIZE)]); Ok(CardId(cccid)) } @@ -96,8 +99,8 @@ impl Ccc { return Err(Error::GenericError); } - let mut ccc = [0u8; CCC_SIZE]; - ccc.copy_from_slice(&response[0..CCC_SIZE]); + let mut ccc = [0u8; Self::BYTE_SIZE]; + ccc.copy_from_slice(&response[0..Self::BYTE_SIZE]); Ok(Self(ccc)) } @@ -112,18 +115,8 @@ impl Ccc { } } -impl Debug for Ccc { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "CCC({:?})", &self.0[..]) - } -} - impl Display for Ccc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - String::from_utf8(hex::encode(&self.0[..])).unwrap() - ) + write!(f, "{}", str::from_utf8(&hex::encode(&self.0[..])).unwrap()) } } diff --git a/src/certificate.rs b/src/certificate.rs index 8776253..feefe42 100644 --- a/src/certificate.rs +++ b/src/certificate.rs @@ -1,4 +1,4 @@ -//! YubiKey Certificates +//! X.509 certificate support. // Adapted from yubico-piv-tool: // diff --git a/src/chuid.rs b/src/chuid.rs index 92c3dc8..22f65ff 100644 --- a/src/chuid.rs +++ b/src/chuid.rs @@ -31,21 +31,13 @@ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use crate::{Error, Result, YubiKey}; -use getrandom::getrandom; -use std::fmt::{self, Debug, Display}; +use std::{ + convert::TryInto, + fmt::{self, Debug, Display}, + str, +}; use subtle_encoding::hex; - -/// CHUID size -pub const CHUID_SIZE: usize = 59; - -/// CARDID size -pub const CARDID_SIZE: usize = 16; - -/// FASC-N component size -pub const FASCN_SIZE: usize = 25; - -/// Expiration size -pub const EXPIRATION_SIZE: usize = 8; +use uuid::Uuid; /// FASC-N offset const CHUID_FASCN_OFFS: usize = 2; @@ -81,46 +73,38 @@ const CHUID_TMPL: &[u8] = &[ 0x30, 0x33, 0x30, 0x30, 0x31, 0x30, 0x31, 0x3e, 0x00, 0xfe, 0x00, ]; -/// Cardholder Unique Identifier (CHUID) Card UUID/GUID value +/// Cardholder Unique Identifier (CHUID). #[derive(Copy, Clone, Debug)] -pub struct Uuid(pub [u8; CARDID_SIZE]); - -impl Uuid { - /// Generate a random Cardholder Unique Identifier (CHUID) UUID - pub fn generate() -> Result { - let mut id = [0u8; CARDID_SIZE]; - getrandom(&mut id).map_err(|_| Error::RandomnessError)?; - Ok(Self(id)) - } -} - -/// Cardholder Unique Identifier (CHUID) -#[derive(Copy, Clone)] -pub struct ChuId(pub [u8; CHUID_SIZE]); +pub struct ChuId(pub [u8; Self::BYTE_SIZE]); impl ChuId { + /// CHUID size in bytes + pub const BYTE_SIZE: usize = 59; + + /// FASC-N component size + pub const FASCN_SIZE: usize = 25; + + /// Expiration size + pub const EXPIRATION_SIZE: usize = 8; + /// Return FASC-N component of CHUID - pub fn fascn(&self) -> Result<[u8; FASCN_SIZE]> { - let mut fascn = [0u8; FASCN_SIZE]; - fascn.copy_from_slice(&self.0[CHUID_FASCN_OFFS..(CHUID_FASCN_OFFS + FASCN_SIZE)]); - Ok(fascn) + pub fn fascn(&self) -> [u8; Self::FASCN_SIZE] { + self.0[CHUID_FASCN_OFFS..(CHUID_FASCN_OFFS + Self::FASCN_SIZE)] + .try_into() + .unwrap() } /// Return Card UUID/GUID component of CHUID - pub fn uuid(&self) -> Result<[u8; CARDID_SIZE]> { - let mut uuid = [0u8; CARDID_SIZE]; - uuid.copy_from_slice(&self.0[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + CARDID_SIZE)]); - Ok(uuid) + pub fn uuid(&self) -> Uuid { + Uuid::from_slice(&self.0[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + 16)]).unwrap() } /// Return expiration date component of CHUID // TODO(tarcieri): parse expiration? - pub fn expiration(&self) -> Result<[u8; EXPIRATION_SIZE]> { - let mut expiration = [0u8; EXPIRATION_SIZE]; - expiration.copy_from_slice( - &self.0[CHUID_EXPIRATION_OFFS..(CHUID_EXPIRATION_OFFS + EXPIRATION_SIZE)], - ); - Ok(expiration) + pub fn expiration(&self) -> [u8; Self::EXPIRATION_SIZE] { + self.0[CHUID_EXPIRATION_OFFS..(CHUID_EXPIRATION_OFFS + Self::EXPIRATION_SIZE)] + .try_into() + .unwrap() } /// Get Cardholder Unique Identifier (CHUID) @@ -132,17 +116,15 @@ impl ChuId { return Err(Error::GenericError); } - let mut chuid = [0u8; CHUID_SIZE]; - chuid.copy_from_slice(&response[0..CHUID_SIZE]); - let retval = ChuId { 0: chuid }; - Ok(retval) + Ok(ChuId(response[..Self::BYTE_SIZE].try_into().unwrap())) } /// Set Cardholder Unique Identifier (CHUID) #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn set(&self, yubikey: &mut YubiKey) -> Result<()> { let mut buf = CHUID_TMPL.to_vec(); - buf[0..self.0.len()].copy_from_slice(&self.0); + buf[..Self::BYTE_SIZE].copy_from_slice(&self.0); let txn = yubikey.begin_transaction()?; txn.save_object(OBJ_CHUID, &buf) @@ -151,16 +133,6 @@ impl ChuId { impl Display for ChuId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - String::from_utf8(hex::encode(&self.0[..])).unwrap() - ) - } -} - -impl Debug for ChuId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "CHUID({:?})", &self.0[..]) + write!(f, "{}", str::from_utf8(&hex::encode(&self.0[..])).unwrap()) } } diff --git a/src/config.rs b/src/config.rs index 850e447..7f366fd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -46,28 +46,28 @@ use std::{ const CB_ADMIN_TIMESTAMP: usize = 0x04; const PROTECTED_FLAGS_1_PUK_NOBLOCK: u8 = 0x01; -/// Config +/// YubiKey configuration. #[derive(Copy, Clone, Debug)] pub struct Config { /// Protected data available - protected_data_available: bool, + pub protected_data_available: bool, /// PUK blocked - puk_blocked: bool, + pub puk_blocked: bool, /// No block on upgrade - puk_noblock_on_upgrade: bool, + pub puk_noblock_on_upgrade: bool, /// PIN last changed - pin_last_changed: Option, + pub pin_last_changed: Option, /// MGM type - mgm_type: MgmType, + pub mgm_type: MgmType, } impl Config { - /// Get YubiKey config - pub fn get(yubikey: &mut YubiKey) -> Result { + /// Get YubiKey config. + pub(crate) fn get(yubikey: &mut YubiKey) -> Result { let mut config = Config { protected_data_available: false, puk_blocked: false, @@ -129,9 +129,7 @@ impl Config { if protected_data.get_item(TAG_PROTECTED_MGM).is_ok() { if config.mgm_type != MgmType::Protected { - error!( - "conflicting types of mgm key administration configured: protected MGM exists" - ); + error!("conflicting MGM key types: protected MGM exists"); } // Always favor protected MGM diff --git a/src/error.rs b/src/error.rs index 3d21ea7..d891b5a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -35,7 +35,7 @@ use std::fmt::{self, Display}; /// Result type with [`Error`]. pub type Result = core::result::Result; -/// Kinds of errors +/// Kinds of errors. #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum Error { diff --git a/src/key.rs b/src/key.rs index bc7726c..ca93851 100644 --- a/src/key.rs +++ b/src/key.rs @@ -350,10 +350,13 @@ pub const SLOTS: [SlotId; 24] = [ pub enum AlgorithmId { /// 1024-bit RSA. Rsa1024, + /// 2048-bit RSA. Rsa2048, + /// ECDSA with the NIST P256 curve. EccP256, + /// ECDSA with the NIST P384 curve. EccP384, } @@ -390,6 +393,7 @@ impl AlgorithmId { } #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] fn get_elem_len(self) -> usize { match self { AlgorithmId::Rsa1024 => 64, @@ -400,6 +404,7 @@ impl AlgorithmId { } #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] fn get_param_tag(self) -> u8 { match self { AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => 0x01, @@ -453,8 +458,7 @@ impl Key { } } -/// Generate key -#[allow(clippy::cognitive_complexity)] +/// Generate new key. pub fn generate( yubikey: &mut YubiKey, slot: SlotId, @@ -473,7 +477,7 @@ pub fn generate( const SZ_ROCA_BLOCK_ADMIN: &str = "was blocked due to an administrator configuration setting."; const SZ_ROCA_DEFAULT: &str = "was permitted by default, but is not recommended. The default behavior will change in a future Yubico release."; - let setting_roca: settings::ConfigValue; + let setting_roca: settings::SettingValue; match algorithm { AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => { @@ -481,17 +485,17 @@ pub fn generate( && (yubikey.version.minor < 3 || yubikey.version.minor == 3 && (yubikey.version.patch < 5)) { - setting_roca = settings::ConfigValue::get(SZ_SETTING_ROCA, true); + setting_roca = settings::SettingValue::get(SZ_SETTING_ROCA, true); let psz_msg = match setting_roca.source { - settings::Source::User => { + settings::SettingSource::User => { if setting_roca.value { SZ_ROCA_ALLOW_USER } else { SZ_ROCA_BLOCK_USER } } - settings::Source::Admin => { + settings::SettingSource::Admin => { if setting_roca.value { SZ_ROCA_ALLOW_ADMIN } else { @@ -660,6 +664,7 @@ pub fn generate( } #[cfg(feature = "untested")] +#[cfg_attr(docsrs, doc(cfg(feature = "untested")))] fn write_key( yubikey: &mut YubiKey, slot: SlotId, @@ -708,6 +713,7 @@ fn write_key( /// The key data that makes up an RSA key. #[cfg(feature = "untested")] +#[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub struct RsaKeyData { /// The secret prime `p`. p: Buffer, @@ -769,6 +775,7 @@ impl RsaKeyData { /// /// Errors if `algorithm` isn't `AlgorithmId::Rsa1024` or `AlgorithmId::Rsa2048`. #[cfg(feature = "untested")] +#[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn import_rsa_key( yubikey: &mut YubiKey, slot: SlotId, @@ -803,6 +810,7 @@ pub fn import_rsa_key( /// /// Errors if `algorithm` isn't `AlgorithmId::EccP256` or ` AlgorithmId::EccP384`. #[cfg(feature = "untested")] +#[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn import_ecc_key( yubikey: &mut YubiKey, slot: SlotId, @@ -828,8 +836,10 @@ pub fn import_ecc_key( } /// Generate an attestation certificate for a stored key. +/// /// #[cfg(feature = "untested")] +#[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn attest(yubikey: &mut YubiKey, key: SlotId) -> Result { let templ = [0, Ins::Attest.code(), key.into(), 0]; let txn = yubikey.begin_transaction()?; @@ -850,7 +860,7 @@ pub fn attest(yubikey: &mut YubiKey, key: SlotId) -> Result { Ok(Buffer::new(response.data().into())) } -/// Sign data using a PIV key +/// Sign data using a PIV key. pub fn sign_data( yubikey: &mut YubiKey, raw_in: &[u8], @@ -863,8 +873,9 @@ pub fn sign_data( txn.authenticated_command(raw_in, algorithm, key, false) } -/// Decrypt data using a PIV key +/// Decrypt data using a PIV key. #[cfg(feature = "untested")] +#[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn decrypt_data( yubikey: &mut YubiKey, input: &[u8], diff --git a/src/lib.rs b/src/lib.rs index e2946df..81dc54e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,14 +34,6 @@ //! //! NOTE: RSASSA-PSS signatures and RSA-OAEP encryption may be supportable (TBD) //! -//! ## Status -//! -//! This is a work-in-progress effort, and while much of the library-level -//! 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][status]. -//! //! ## History //! //! This library is a Rust translation of the [yubico-piv-tool] utility by @@ -83,7 +75,6 @@ //! [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/iqlusioninc/yubikey.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 @@ -121,6 +112,7 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#![cfg_attr(docsrs, feature(doc_cfg))] #![doc( html_logo_url = "https://raw.githubusercontent.com/iqlusioninc/yubikey.rs/main/img/logo.png", html_root_url = "https://docs.rs/yubikey/0.4.0-pre" @@ -129,34 +121,44 @@ #![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)] mod apdu; -pub mod cccid; +mod cccid; pub mod certificate; -pub mod chuid; -pub mod config; -pub mod error; +mod chuid; +mod config; +mod error; pub mod key; mod metadata; -pub mod mgm; +mod mgm; #[cfg(feature = "untested")] -pub mod mscmap; +mod mscmap; #[cfg(feature = "untested")] -pub mod msroots; -pub mod policy; +mod msroots; +mod policy; pub mod readers; mod serialization; -pub mod settings; +mod settings; mod transaction; -pub mod yubikey; +mod yubikey; -pub use self::{ +pub use crate::{ + cccid::{CardId, Ccc}, + chuid::ChuId, + config::Config, error::{Error, Result}, key::Key, - mgm::MgmKey, + mgm::{MgmKey, MgmType}, + policy::{PinPolicy, TouchPolicy}, readers::Readers, - yubikey::{Serial, YubiKey}, + settings::{SettingSource, SettingValue}, + yubikey::{CachedPin, Serial, Version, YubiKey}, }; -/// Object identifiers +#[cfg(feature = "untested")] +pub use crate::{mscmap::MsContainer, msroots::MsRoots}; + +pub use uuid::Uuid; + +/// Object identifiers: handles to particular objects stored on a YubiKey. pub type ObjectId = u32; /// Buffer type (self-zeroizing byte vector) diff --git a/src/mgm.rs b/src/mgm.rs index 2c176d4..d347325 100644 --- a/src/mgm.rs +++ b/src/mgm.rs @@ -73,7 +73,7 @@ pub(crate) const DES_LEN_3DES: usize = DES_LEN_DES * 3; #[cfg(feature = "untested")] const ITER_MGM_PBKDF2: u32 = 10000; -/// Management Key (MGM) key types (manual/derived/protected) +/// Management Key (MGM) key types (manual/derived/protected). #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum MgmType { /// Manual @@ -132,6 +132,7 @@ impl MgmKey { /// Get derived management key (MGM) #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn get_derived(yubikey: &mut YubiKey, pin: &[u8]) -> Result { let txn = yubikey.begin_transaction()?; @@ -157,6 +158,7 @@ impl MgmKey { /// Get protected management key (MGM) #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn get_protected(yubikey: &mut YubiKey) -> Result { let txn = yubikey.begin_transaction()?; @@ -187,6 +189,7 @@ impl MgmKey { /// /// This will wipe any metadata related to derived and PIN-protected management keys. #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn set_default(yubikey: &mut YubiKey) -> Result<()> { MgmKey::default().set_manual(yubikey, false) } @@ -198,6 +201,7 @@ impl MgmKey { /// /// This will wipe any metadata related to derived and PIN-protected management keys. #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn set_manual(&self, yubikey: &mut YubiKey, require_touch: bool) -> Result<()> { let txn = yubikey.begin_transaction()?; @@ -257,6 +261,7 @@ impl MgmKey { /// /// This enables key management operations to be performed with access to the PIN. #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn set_protected(&self, yubikey: &mut YubiKey) -> Result<()> { let txn = yubikey.begin_transaction()?; diff --git a/src/mscmap.rs b/src/mscmap.rs index 63b0def..1716996 100644 --- a/src/mscmap.rs +++ b/src/mscmap.rs @@ -1,7 +1,4 @@ -//! MS Container Map Records -//! -//! These appear(?) to be defined in Microsoft's Smart Card Minidriver Specification: -//! +//! MS Container Map Records. // Adapted from yubico-piv-tool: // @@ -37,46 +34,53 @@ use crate::{key::SlotId, serialization::*, Error, Result, YubiKey, CB_OBJ_MAX}; use log::error; use std::convert::{TryFrom, TryInto}; -/// Container name length -const CONTAINER_NAME_LEN: usize = 40; - -/// Container record length: 27 = 80 + 1 + 1 + 2 + 1 + 1 + 1 + 20 -const CONTAINER_REC_LEN: usize = (2 * CONTAINER_NAME_LEN) + 27; - const OBJ_MSCMAP: u32 = 0x005f_ff10; const TAG_MSCMAP: u8 = 0x81; -/// MS Container Map(?) Records +/// MS Container Map records. +/// +/// Defined in Microsoft's Smart Card Minidriver Specification: +/// +#[cfg_attr(docsrs, doc(cfg(feature = "untested")))] #[derive(Clone, Debug)] -pub struct Container { - /// Container name - pub name: [u16; CONTAINER_NAME_LEN], +pub struct MsContainer { + /// Container name. + pub name: [u16; Self::NAME_LEN], - /// Card slot + /// Card slot. pub slot: SlotId, - /// Key spec + /// Key spec. pub key_spec: u8, - /// Key size in bits + /// Key size in bits. pub key_size_bits: u16, - /// Flags + /// Flags. pub flags: u8, - /// PIN ID + /// PIN ID. pub pin_id: u8, - /// Associated ECHD(?) container (typo of "ecdh" perhaps?) + /// Associated ECHD container. pub associated_echd_container: u8, - /// Cert fingerprint - pub cert_fingerprint: [u8; 20], + /// Cert fingerprint. + pub cert_fingerprint: [u8; Self::CERT_FINGERPRINT_LEN], } -impl Container { - /// Read MS Container Map records +impl MsContainer { + /// Container name length in UTF-16 chars. + const NAME_LEN: usize = 40; + + /// Container record length: 27 = 80 + 1 + 1 + 2 + 1 + 1 + 1 + 20 + const REC_LEN: usize = (2 * Self::NAME_LEN) + 27; + + /// Length of a certificate fingerprint. + const CERT_FINGERPRINT_LEN: usize = 20; + + /// Read MS Container Map records. pub fn read_mscmap(yubikey: &mut YubiKey) -> Result> { let txn = yubikey.begin_transaction()?; let response = txn.fetch_object(OBJ_MSCMAP)?; @@ -95,8 +99,8 @@ impl Container { return Err(Error::InvalidObject); } - for chunk in tlv.value.chunks_exact(CONTAINER_REC_LEN) { - containers.push(Container::new(chunk)?); + for chunk in tlv.value.chunks_exact(Self::REC_LEN) { + containers.push(MsContainer::new(chunk)?); } Ok(containers) @@ -105,7 +109,7 @@ impl Container { /// Write MS Container Map records. pub fn write_mscmap(yubikey: &mut YubiKey, containers: &[Self]) -> Result<()> { let n_containers = containers.len(); - let data_len = n_containers * CONTAINER_REC_LEN; + let data_len = n_containers * Self::REC_LEN; let txn = yubikey.begin_transaction()?; @@ -115,7 +119,7 @@ impl Container { let mut buf = [0u8; CB_OBJ_MAX]; let offset = Tlv::write_as(&mut buf, TAG_MSCMAP, data_len, |buf| { - for (i, chunk) in buf.chunks_exact_mut(CONTAINER_REC_LEN).enumerate() { + for (i, chunk) in buf.chunks_exact_mut(Self::REC_LEN).enumerate() { chunk.copy_from_slice(&containers[i].to_bytes()); } })?; @@ -123,19 +127,19 @@ impl Container { txn.save_object(OBJ_MSCMAP, &buf[..offset]) } - /// Parse a container record from a byte slice + /// Parse a container record from a byte slice. pub fn new(bytes: &[u8]) -> Result { - if bytes.len() != CONTAINER_REC_LEN { + if bytes.len() != Self::REC_LEN { error!( "couldn't parse PIV container: expected {}-bytes, got {}-bytes", - CONTAINER_REC_LEN, + Self::REC_LEN, bytes.len() ); return Err(Error::ParseError); } - let mut name = [0u16; CONTAINER_NAME_LEN]; - let name_bytes_len = CONTAINER_NAME_LEN * 2; + let mut name = [0u16; Self::NAME_LEN]; + let name_bytes_len = Self::NAME_LEN * 2; for (i, chunk) in bytes[..name_bytes_len].chunks_exact(2).enumerate() { name[i] = u16::from_le_bytes(chunk.try_into().unwrap()); @@ -144,7 +148,7 @@ impl Container { let mut cert_fingerprint = [0u8; 20]; cert_fingerprint.copy_from_slice(&bytes[(bytes.len() - 20)..]); - Ok(Container { + Ok(Self { name, slot: bytes[name_bytes_len].try_into()?, key_spec: bytes[name_bytes_len + 1], @@ -160,17 +164,17 @@ impl Container { }) } - /// Parse the container name as a UTF-16 string + /// Parse the container name as a UTF-16 string. pub fn parse_name(&self) -> Result { String::from_utf16(&self.name).map_err(|_| Error::ParseError) } - /// Serialize a container record as a byte size - pub fn to_bytes(&self) -> [u8; CONTAINER_REC_LEN] { + /// Serialize a container record as a byte size. + pub fn to_bytes(&self) -> [u8; Self::REC_LEN] { // TODO(tarcieri): use array instead of `Vec` - let mut bytes = Vec::with_capacity(CONTAINER_REC_LEN); + let mut bytes = Vec::with_capacity(Self::REC_LEN); - for i in 0..CONTAINER_NAME_LEN { + for i in 0..Self::NAME_LEN { bytes.extend_from_slice(&self.name[i].to_le_bytes()); } @@ -185,7 +189,7 @@ impl Container { } } -impl<'a> TryFrom<&'a [u8]> for Container { +impl<'a> TryFrom<&'a [u8]> for MsContainer { type Error = Error; fn try_from(bytes: &'a [u8]) -> Result { diff --git a/src/msroots.rs b/src/msroots.rs index 6c8a027..f1c86af 100644 --- a/src/msroots.rs +++ b/src/msroots.rs @@ -1,11 +1,4 @@ -//! `msroots`: PKCS#7 formatted certificate store for enterprise trusted roots. -//! -//! This `msroots` file contains a bag of certificates with empty content and -//! an empty signature, allowing an enterprise root certificate truststore to -//! be written to and read from a YubiKey. -//! -//! For more information, see: -//! +//! PKCS#7 formatted certificate store for enterprise trusted roots. // Adapted from yubico-piv-tool: // @@ -53,7 +46,15 @@ const OBJ_MSROOTS5: u32 = 0x005f_ff15; const TAG_MSROOTS_END: u8 = 0x82; const TAG_MSROOTS_MID: u8 = 0x83; -/// `msroots` file: PKCS#7-formatted certificate store for enterprise trust roots +/// PKCS#7-formatted certificate store for enterprise trust roots. +/// +/// The `msroots` file contains a bag of certificates with empty content and +/// an empty signature, allowing an enterprise root certificate truststore to +/// be written to and read from a YubiKey. +/// +/// For more information, see: +/// +#[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub struct MsRoots(Vec); impl MsRoots { diff --git a/src/policy.rs b/src/policy.rs index f891743..0e13543 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -3,8 +3,9 @@ use crate::{serialization::Tlv, Result}; /// Specifies how often the PIN needs to be entered for access to the credential in a -/// given slot. This policy must be set upon key generation or importation, and cannot be -/// changed later. +/// given slot. +/// +/// This policy must be set when keys are generated or imported, and cannot be changed later. #[derive(Clone, Copy, Debug, PartialEq)] pub enum PinPolicy { /// Use the default PIN policy for the slot. See the slot's documentation for details. @@ -46,8 +47,9 @@ impl PinPolicy { } /// Specifies under what conditions a physical touch on the metal contact is required, in -/// addition to the [`PinPolicy`]. This policy must be set upon key generation or -/// importation, and cannot be changed later. +/// addition to the [`PinPolicy`]. +/// +/// This policy must be set when keys are generated or imported, and cannot be changed later. #[derive(Clone, Copy, Debug, PartialEq)] pub enum TouchPolicy { /// Use the default touch policy for the slot. diff --git a/src/readers.rs b/src/readers.rs index 7daf89b..8423d18 100644 --- a/src/readers.rs +++ b/src/readers.rs @@ -1,4 +1,4 @@ -//! Support for enumerating available readers +//! Support for enumerating available PC/SC card readers. use crate::{Result, YubiKey}; use std::{ @@ -32,7 +32,7 @@ impl Readers { }) } - /// Iterate over the available readers + /// Iterate over the available readers. pub fn iter(&mut self) -> Result> { let Self { ctx, reader_names } = self; @@ -54,7 +54,7 @@ impl Readers { } } -/// An individual connected reader +/// An individual connected PC/SC card reader. pub struct Reader<'ctx> { /// Name of this reader name: &'ctx CStr, @@ -64,24 +64,24 @@ pub struct Reader<'ctx> { } impl<'ctx> Reader<'ctx> { - /// Create a new reader from its name and context + /// Create a new reader from its name and context. fn new(name: &'ctx CStr, ctx: Arc>) -> Self { // TODO(tarcieri): open devices, determine they're YubiKeys, get serial? Self { name, ctx } } - /// Get this reader's name + /// Get this reader's name. pub fn name(&self) -> Cow<'_, str> { // TODO(tarcieri): is lossy ok here? try to avoid lossiness? self.name.to_string_lossy() } - /// Open a connection to this reader, returning a `YubiKey` if successful + /// Open a connection to this reader, returning a `YubiKey` if successful. pub fn open(&self) -> Result { self.try_into() } - /// Connect to this reader, returning its `pcsc::Card` + /// Connect to this reader, returning its `pcsc::Card`. pub(crate) fn connect(&self) -> Result { let ctx = self.ctx.lock().unwrap(); Ok(ctx.connect(self.name, pcsc::ShareMode::Shared, pcsc::Protocols::T1)?) diff --git a/src/settings.rs b/src/settings.rs index 264cf74..518616e 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -40,43 +40,48 @@ use std::{ io::{BufRead, BufReader}, }; -/// Source of how a setting was configured +/// Source of how a setting was configured. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum Source { - /// User-specified setting +pub enum SettingSource { + /// User-specified setting: sourced via `YUBIKEY_PIV_*` environment vars. User, - /// Admin-specified setting + /// Admin-specified setting: sourced via the `/etc/yubico/yubikeypiv.conf` + /// configuration file. Admin, - /// Default setting + /// Default setting. Default, } -impl Default for Source { +impl Default for SettingSource { fn default() -> Self { Self::Default } } -/// Setting booleans +/// Setting booleans: configuration values sourced from a file or the environment. +/// +/// These can be configured globally in `/etc/yubico/yubikeypiv.conf` by a +/// system administrator, or by the local user via `YUBIKEY_PIV_*` environment +/// variables. #[derive(Copy, Clone, Debug)] -pub struct ConfigValue { +pub struct SettingValue { /// Boolean value pub value: bool, /// Source of the configuration setting (user, admin, or default) - pub source: Source, + pub source: SettingSource, } -impl ConfigValue { - /// Get a [`BoolValue`] value by name. +impl SettingValue { + /// Get a [`SettingValue`] value by name. pub fn get(key: &str, default: bool) -> Self { Self::from_file(key) .or_else(|| Self::from_env(key)) .unwrap_or(Self { value: default, - source: Source::Default, + source: SettingSource::Default, }) } @@ -104,8 +109,8 @@ impl ConfigValue { }; if name == key { - return Some(ConfigValue { - source: Source::Admin, + return Some(SettingValue { + source: SettingSource::Admin, value: value == "1" || value == "true", }); } @@ -119,18 +124,18 @@ impl ConfigValue { fn from_env(key: &str) -> Option { env::var(format!("YUBIKEY_PIV_{}", key)) .ok() - .map(|value| ConfigValue { - source: Source::User, + .map(|value| SettingValue { + source: SettingSource::User, value: value == "1" || value == "true", }) } } -impl Default for ConfigValue { +impl Default for SettingValue { fn default() -> Self { Self { value: false, - source: Source::default(), + source: SettingSource::default(), } } } diff --git a/src/yubikey.rs b/src/yubikey.rs index 44469e0..e6e1494 100644 --- a/src/yubikey.rs +++ b/src/yubikey.rs @@ -70,10 +70,10 @@ pub(crate) const KEY_CARDMGM: u8 = 0x9b; const TAG_DYN_AUTH: u8 = 0x7c; -/// Cached YubiKey PIN +/// Cached YubiKey PIN. pub type CachedPin = secrecy::SecretVec; -/// YubiKey Serial Number +/// YubiKey serial number. #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] pub struct Serial(pub u32); @@ -103,7 +103,7 @@ impl Display for Serial { } } -/// YubiKey Version +/// YubiKey version. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Version { /// Major version component @@ -133,8 +133,7 @@ impl Display for Version { } } -/// YubiKey Device: this is the primary API for opening a session and -/// performing various operations. +/// YubiKey device: 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. @@ -203,8 +202,9 @@ impl YubiKey { Err(Error::NotFound) } - /// Reconnect to a YubiKey + /// Reconnect to a YubiKey. #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn reconnect(&mut self) -> Result<()> { info!("trying to reconnect to current reader"); @@ -235,7 +235,7 @@ impl YubiKey { Transaction::new(&mut self.card) } - /// Get the name of the associated PC/SC card reader + /// Get the name of the associated PC/SC card reader. pub fn name(&self) -> &str { &self.name } @@ -259,12 +259,12 @@ impl YubiKey { Config::get(self) } - /// Get CHUID + /// Get Cardholder Unique Identifier (CHUID). pub fn chuid(&mut self) -> Result { ChuId::get(self) } - /// Get CCCID + /// Get Cardholder Capability Container (CCC) Identifier. pub fn cccid(&mut self) -> Result { Ccc::get(self) } @@ -325,6 +325,7 @@ impl YubiKey { /// Deauthenticate #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn deauthenticate(&mut self) -> Result<()> { let txn = self.begin_transaction()?; @@ -359,7 +360,7 @@ impl YubiKey { Ok(()) } - /// Get the number of PIN retries + /// Get the number of PIN retries. pub fn get_pin_retries(&mut self) -> Result { let txn = self.begin_transaction()?; @@ -376,8 +377,9 @@ impl YubiKey { } } - /// Set the number of PIN retries + /// Set the number of PIN retries. #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn set_pin_retries(&mut self, pin_tries: u8, puk_tries: u8) -> Result<()> { // Special case: if either retry count is 0, it's a successful no-op if pin_tries == 0 || puk_tries == 0 { @@ -400,8 +402,9 @@ impl YubiKey { /// Change the Personal Identification Number (PIN). /// - /// The default PIN code is 123456 + /// The default PIN code is `123456`. #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn change_pin(&mut self, current_pin: &[u8], new_pin: &[u8]) -> Result<()> { { let txn = self.begin_transaction()?; @@ -415,8 +418,9 @@ impl YubiKey { Ok(()) } - /// Set PIN last changed + /// Set PIN last changed. #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn set_pin_last_changed(yubikey: &mut YubiKey) -> Result<()> { let txn = yubikey.begin_transaction()?; @@ -450,15 +454,17 @@ impl YubiKey { /// /// The PUK is part of the PIV standard that the YubiKey follows. /// - /// The default PUK code is 12345678. + /// The default PUK code is `12345678`. #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn change_puk(&mut self, current_puk: &[u8], new_puk: &[u8]) -> Result<()> { let txn = self.begin_transaction()?; txn.change_ref(ChangeRefAction::ChangePuk, current_puk, new_puk) } - /// Block PUK: permanently prevent the PIN from becoming unblocked + /// Block PUK: permanently prevent the PIN from becoming unblocked. #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn block_puk(yubikey: &mut YubiKey) -> Result<()> { let mut puk = [0x30, 0x42, 0x41, 0x44, 0x46, 0x30, 0x30, 0x44]; let mut tries_remaining: i32 = -1; @@ -488,24 +494,23 @@ impl YubiKey { } // Attempt to set the "PUK blocked" flag in admin data. - - let mut admin_data = if let Ok(admin_data) = AdminData::read(&txn) { - if let Ok(item) = admin_data.get_item(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() - ); + let mut admin_data = AdminData::read(&txn) + .map(|data| { + if let Ok(item) = data.get_item(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() + ); + } } - } - admin_data - } else { - AdminData::default() - }; + data + }) + .unwrap_or_default(); flags[0] |= ADMIN_FLAGS_1_PUK_BLOCKED; @@ -523,27 +528,31 @@ impl YubiKey { /// Unblock a Personal Identification Number (PIN) using a previously /// configured PIN Unblocking Key (PUK). #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn unblock_pin(&mut self, puk: &[u8], new_pin: &[u8]) -> Result<()> { let txn = self.begin_transaction()?; txn.change_ref(ChangeRefAction::UnblockPin, puk, new_pin) } - /// Fetch an object from the YubiKey + /// Fetch an object from the YubiKey. #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(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 + /// Save an object. #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn save_object(&mut self, object_id: ObjectId, indata: &mut [u8]) -> Result<()> { let txn = self.begin_transaction()?; txn.save_object(object_id, indata) } - /// Get an auth challenge + /// Get an auth challenge. #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn get_auth_challenge(&mut self) -> Result<[u8; 8]> { let txn = self.begin_transaction()?; @@ -559,8 +568,9 @@ impl YubiKey { Ok(response.data()[4..12].try_into().unwrap()) } - /// Verify an auth response + /// Verify an auth response. #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn verify_auth_response(&mut self, response: [u8; 8]) -> Result<()> { let mut data = [0u8; 12]; data[0] = 0x7c; @@ -591,6 +601,7 @@ impl YubiKey { /// /// The reset function is only available when both pins are blocked. #[cfg(feature = "untested")] + #[cfg_attr(docsrs, doc(cfg(feature = "untested")))] pub fn reset_device(&mut self) -> Result<()> { let templ = [0, Ins::Reset.code(), 0, 0]; let txn = self.begin_transaction()?; diff --git a/tests/integration.rs b/tests/integration.rs index ae05e5d..a36ef51 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -17,8 +17,7 @@ use x509::RelativeDistinguishedName; use yubikey::{ certificate::{Certificate, PublicKeyInfo}, key::{self, AlgorithmId, Key, RetiredSlotId, SlotId}, - policy::{PinPolicy, TouchPolicy}, - Error, MgmKey, YubiKey, + Error, MgmKey, PinPolicy, TouchPolicy, YubiKey, }; lazy_static! {