diff --git a/README.md b/README.md index 5dd7bc9..9fb818d 100644 --- a/README.md +++ b/README.md @@ -112,15 +112,16 @@ To trace every message sent to/from the card i.e. the raw Application Protocol Data Unit (APDU) messages, use the `trace` log level: ``` +running 1 test [INFO yubikey_piv::yubikey] trying to connect to reader 'Yubico YubiKey OTP+FIDO+CCID' [INFO yubikey_piv::yubikey] connected to 'Yubico YubiKey OTP+FIDO+CCID' successfully -[TRACE yubikey_piv::apdu] >>> APDU { cla: 0, ins: 164, p1: 4, p2: 0, lc: 5, data: [160, 0, 0, 3, 8] } +[TRACE yubikey_piv::apdu] >>> APDU { cla: 0, ins: SelectApplication, p1: 4, p2: 0, data: [160, 0, 0, 3, 8] } [TRACE yubikey_piv::transaction] >>> [0, 164, 4, 0, 5, 160, 0, 0, 3, 8] [TRACE yubikey_piv::apdu] <<< Response { status_words: Success, data: [97, 17, 79, 6, 0, 0, 16, 0, 1, 0, 121, 7, 79, 5, 160, 0, 0, 3, 8] } -[TRACE yubikey_piv::apdu] >>> APDU { cla: 0, ins: 253, p1: 0, p2: 0, lc: 0, data: [] } +[TRACE yubikey_piv::apdu] >>> APDU { cla: 0, ins: GetVersion, p1: 0, p2: 0, data: [] } [TRACE yubikey_piv::transaction] >>> [0, 253, 0, 0, 0] [TRACE yubikey_piv::apdu] <<< Response { status_words: Success, data: [5, 1, 2] } -[TRACE yubikey_piv::apdu] >>> APDU { cla: 0, ins: 248, p1: 0, p2: 0, lc: 0, data: [] } +[TRACE yubikey_piv::apdu] >>> APDU { cla: 0, ins: GetSerial, p1: 0, p2: 0, data: [] } [TRACE yubikey_piv::transaction] >>> [0, 248, 0, 0, 0] [TRACE yubikey_piv::apdu] <<< Response { status_words: Success, data: [0, 115, 0, 178] } test connect ... ok diff --git a/src/apdu.rs b/src/apdu.rs index 2ddf77a..b3e8e13 100644 --- a/src/apdu.rs +++ b/src/apdu.rs @@ -32,7 +32,6 @@ use crate::{error::Error, transaction::Transaction, Buffer}; use log::trace; -use std::fmt::{self, Debug}; use zeroize::{Zeroize, Zeroizing}; /// Maximum amount of command data that can be included in an APDU @@ -41,13 +40,13 @@ const APDU_DATA_MAX: usize = 0xFF; /// Application Protocol Data Unit (APDU). /// /// These messages are packets used to communicate with the YubiKey. -#[derive(Clone)] +#[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct APDU { /// Instruction class: indicates the type of command (e.g. inter-industry or proprietary) cla: u8, /// Instruction code: indicates the specific command (e.g. "write data") - ins: u8, + ins: Ins, /// Instruction parameter 1 for the command (e.g. offset into file at which to write the data) p1: u8, @@ -61,10 +60,10 @@ pub(crate) struct APDU { impl APDU { /// Create a new APDU with the given instruction code - pub fn new(ins: u8) -> Self { + pub fn new(ins: impl Into) -> Self { Self { cla: 0, - ins, + ins: ins.into(), p1: 0, p2: 0, data: vec![], @@ -123,7 +122,7 @@ impl APDU { pub fn to_bytes(&self) -> Buffer { let mut bytes = Vec::with_capacity(5 + self.data.len()); bytes.push(self.cla); - bytes.push(self.ins); + bytes.push(self.ins.code()); bytes.push(self.p1); bytes.push(self.p2); bytes.push(self.data.len() as u8); @@ -132,21 +131,6 @@ impl APDU { } } -impl Debug for APDU { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "APDU {{ cla: {}, ins: {}, p1: {}, p2: {}, lc: {}, data: {:?} }}", - self.cla, - self.ins, - self.p1, - self.p2, - self.data.len(), - self.data.as_slice() - ) - } -} - impl Drop for APDU { fn drop(&mut self) { self.zeroize(); @@ -155,16 +139,125 @@ impl Drop for APDU { impl Zeroize for APDU { fn zeroize(&mut self) { - self.cla.zeroize(); - self.ins.zeroize(); - self.p1.zeroize(); - self.p2.zeroize(); + // Only `data` may contain secrets self.data.zeroize(); } } +/// APDU instruction codes +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Ins { + /// Verify + Verify, + + /// Change reference + ChangeReference, + + /// Reset retry + ResetRetry, + + /// Generate asymmetric + GenerateAsymmetric, + + /// Authenticate + Authenticate, + + /// Get data + GetData, + + /// Put data + PutData, + + /// Select application + SelectApplication, + + /// Get response APDU + GetResponseApdu, + + // Yubico vendor specific instructions + // + /// Set MGM key + SetMgmKey, + + /// Import key + ImportKey, + + /// Get version + GetVersion, + + /// Reset device + Reset, + + /// Set PIN retries + SetPinRetries, + + /// Generate attestation certificate for asymmetric key + Attest, + + /// Get device serial + GetSerial, + + /// Other/unrecognized instruction codes + Other(u8), +} + +impl Ins { + /// Get the code that corresponds to this instruction + pub fn code(self) -> u8 { + match self { + Ins::Verify => 0x20, + Ins::ChangeReference => 0x24, + Ins::ResetRetry => 0x2c, + Ins::GenerateAsymmetric => 0x47, + Ins::Authenticate => 0x87, + Ins::GetData => 0xcb, + Ins::PutData => 0xdb, + Ins::SelectApplication => 0xa4, + Ins::GetResponseApdu => 0xc0, + Ins::SetMgmKey => 0xff, + Ins::ImportKey => 0xfe, + Ins::GetVersion => 0xfd, + Ins::Reset => 0xfb, + Ins::SetPinRetries => 0xfa, + Ins::Attest => 0xf9, + Ins::GetSerial => 0xf8, + Ins::Other(code) => code, + } + } +} + +impl From for Ins { + fn from(code: u8) -> Self { + match code { + 0x20 => Ins::Verify, + 0x24 => Ins::ChangeReference, + 0x2c => Ins::ResetRetry, + 0x47 => Ins::GenerateAsymmetric, + 0x87 => Ins::Authenticate, + 0xcb => Ins::GetData, + 0xdb => Ins::PutData, + 0xa4 => Ins::SelectApplication, + 0xc0 => Ins::GetResponseApdu, + 0xff => Ins::SetMgmKey, + 0xfe => Ins::ImportKey, + 0xfd => Ins::GetVersion, + 0xfb => Ins::Reset, + 0xfa => Ins::SetPinRetries, + 0xf9 => Ins::Attest, + 0xf8 => Ins::GetSerial, + code => Ins::Other(code), + } + } +} + +impl From for u8 { + fn from(ins: Ins) -> u8 { + ins.code() + } +} + /// APDU responses -#[derive(Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct Response { /// Status words status_words: StatusWords, @@ -239,6 +332,7 @@ impl From> for Response { impl Zeroize for Response { fn zeroize(&mut self) { + // Only `data` may contain secrets self.data.zeroize(); } } diff --git a/src/consts.rs b/src/consts.rs index 39b1c35..749a2a2 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -136,26 +136,6 @@ pub const YKPIV_CCCID_SIZE: usize = 14; pub const YKPIV_CERTINFO_UNCOMPRESSED: u8 = 0; pub const YKPIV_CERTINFO_GZIP: u8 = 1; -pub const YKPIV_INS_VERIFY: u8 = 0x20; -pub const YKPIV_INS_CHANGE_REFERENCE: u8 = 0x24; -pub const YKPIV_INS_RESET_RETRY: u8 = 0x2c; -pub const YKPIV_INS_GENERATE_ASYMMETRIC: u8 = 0x47; -pub const YKPIV_INS_AUTHENTICATE: u8 = 0x87; -pub const YKPIV_INS_GET_DATA: u8 = 0xcb; -pub const YKPIV_INS_PUT_DATA: u8 = 0xdb; -pub const YKPIV_INS_SELECT_APPLICATION: u8 = 0xa4; -pub const YKPIV_INS_GET_RESPONSE_APDU: u8 = 0xc0; - -// Yubico vendor specific instructions -// -pub const YKPIV_INS_SET_MGMKEY: u8 = 0xff; -pub const YKPIV_INS_IMPORT_KEY: u8 = 0xfe; -pub const YKPIV_INS_GET_VERSION: u8 = 0xfd; -pub const YKPIV_INS_RESET: u8 = 0xfb; -pub const YKPIV_INS_SET_PIN_RETRIES: u8 = 0xfa; -pub const YKPIV_INS_ATTEST: u8 = 0xf9; -pub const YKPIV_INS_GET_SERIAL: u8 = 0xf8; - pub const YKPIV_KEY_AUTHENTICATION: u8 = 0x9a; pub const YKPIV_KEY_CARDMGM: u8 = 0x9b; pub const YKPIV_KEY_SIGNATURE: u8 = 0x9c; diff --git a/src/key.rs b/src/key.rs index 7083b6e..8c07e15 100644 --- a/src/key.rs +++ b/src/key.rs @@ -38,7 +38,7 @@ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use crate::{ - apdu::StatusWords, + apdu::{Ins, StatusWords}, certificate::{self, Certificate}, consts::*, error::Error, @@ -199,7 +199,7 @@ pub fn generate( touch_policy: u8, ) -> Result { let mut in_data = [0u8; 11]; - let mut templ = [0, YKPIV_INS_GENERATE_ASYMMETRIC, 0, 0]; + let mut templ = [0, Ins::GenerateAsymmetric.code(), 0, 0]; let setting_roca: settings::BoolValue; if yubikey.device_model() == DEVTYPE_YK4 diff --git a/src/transaction.rs b/src/transaction.rs index 4f227c5..1bc1cf6 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,9 +1,14 @@ //! YubiKey PC/SC transactions -use crate::{apdu::APDU, consts::*, error::Error, yubikey::*}; +use crate::{ + apdu::{Ins, APDU}, + error::Error, + yubikey::*, +}; #[cfg(feature = "untested")] use crate::{ apdu::{Response, StatusWords}, + consts::*, mgm::MgmKey, serialization::*, Buffer, ObjectId, @@ -60,7 +65,7 @@ impl<'tx> Transaction<'tx> { /// Select application. pub fn select_application(&self) -> Result<(), Error> { - let response = APDU::new(YKPIV_INS_SELECT_APPLICATION) + let response = APDU::new(Ins::SelectApplication) .p1(0x04) .data(&AID) .transmit(self, 0xFF) @@ -83,7 +88,7 @@ impl<'tx> Transaction<'tx> { /// Get the version of the PIV application installed on the YubiKey pub fn get_version(&self) -> Result { // get version from device - let response = APDU::new(YKPIV_INS_GET_VERSION).transmit(self, 261)?; + let response = APDU::new(Ins::GetVersion).transmit(self, 261)?; if !response.is_success() { return Err(Error::GenericError); @@ -93,11 +98,7 @@ impl<'tx> Transaction<'tx> { return Err(Error::SizeError); } - Ok(Version { - major: response.data()[0], - minor: response.data()[1], - patch: response.data()[2], - }) + Ok(Version::new(response.data()[..3].try_into().unwrap())) } /// Get YubiKey device serial number @@ -106,7 +107,7 @@ impl<'tx> Transaction<'tx> { let response = if version.major < 5 { // get serial from neo/yk4 devices using the otp applet - let sw = APDU::new(YKPIV_INS_SELECT_APPLICATION) + let sw = APDU::new(Ins::SelectApplication) .p1(0x04) .data(&yk_applet) .transmit(self, 0xFF)? @@ -128,7 +129,7 @@ impl<'tx> Transaction<'tx> { } // reselect the PIV applet - let sw = APDU::new(YKPIV_INS_SELECT_APPLICATION) + let sw = APDU::new(Ins::SelectApplication) .p1(0x04) .data(&AID) .transmit(self, 0xFF)? @@ -142,7 +143,7 @@ impl<'tx> Transaction<'tx> { resp } else { // get serial from yk5 and later devices using the f8 command - let resp = APDU::new(YKPIV_INS_GET_SERIAL).transmit(self, 0xFF)?; + let resp = APDU::new(Ins::GetSerial).transmit(self, 0xFF)?; if !resp.is_success() { error!( @@ -169,7 +170,7 @@ impl<'tx> Transaction<'tx> { return Err(Error::SizeError); } - let response = APDU::new(YKPIV_INS_VERIFY) + let response = APDU::new(Ins::Verify) .params(0x00, 0x80) .data(pin) .transmit(self, 261)?; @@ -185,7 +186,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 templ = [0, Ins::ChangeReference.code(), 0, 0x80]; let mut indata = Zeroizing::new([0u8; 16]); if current_pin.len() > CB_PIN_MAX || new_pin.len() > CB_PIN_MAX { @@ -193,7 +194,7 @@ impl<'tx> Transaction<'tx> { } if action == CHREF_ACT_UNBLOCK_PIN { - templ[1] = YKPIV_INS_RESET_RETRY; + templ[1] = Ins::ResetRetry.code(); } else if action == CHREF_ACT_CHANGE_PUK { templ[3] = 0x81; } @@ -259,7 +260,7 @@ impl<'tx> Transaction<'tx> { data[2] = DES_LEN_3DES as u8; data[3..3 + DES_LEN_3DES].copy_from_slice(new_key.as_ref()); - let status_words = APDU::new(YKPIV_INS_SET_MGMKEY) + let status_words = APDU::new(Ins::SetMgmKey) .params(0xff, p2) .data(&data) .transmit(self, 261)? @@ -290,7 +291,7 @@ impl<'tx> Transaction<'tx> { ) -> Result<(), Error> { let in_len = sign_in.len(); let mut indata = [0u8; 1024]; - let templ = [0, YKPIV_INS_AUTHENTICATE, algorithm, key]; + let templ = [0, Ins::Authenticate.code(), algorithm, key]; let mut len: usize = 0; match algorithm { @@ -454,7 +455,7 @@ impl<'tx> Transaction<'tx> { sw & 0xff ); - let response = APDU::new(YKPIV_INS_GET_RESPONSE_APDU).transmit(self, 261)?; + let response = APDU::new(Ins::GetResponseApdu).transmit(self, 261)?; sw = response.status_words().code(); if sw != StatusWords::Success.code() && (sw >> 8 != 0x61) { @@ -481,7 +482,7 @@ impl<'tx> Transaction<'tx> { #[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]; + let templ = [0, Ins::GetData.code(), 0x3f, 0xff]; let mut inlen = indata.len(); let indata_remaining = set_object(object_id, &mut indata); @@ -524,7 +525,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]; + let templ = [0, Ins::PutData.code(), 0x3f, 0xff]; // TODO(tarcieri): replace with vector let mut data = [0u8; CB_BUF_MAX]; diff --git a/src/yubikey.rs b/src/yubikey.rs index 6327ec0..96bd829 100644 --- a/src/yubikey.rs +++ b/src/yubikey.rs @@ -35,7 +35,7 @@ #[cfg(feature = "untested")] use crate::{ - apdu::{StatusWords, APDU}, + apdu::{Ins, StatusWords, APDU}, key::SlotId, metadata, mgm::MgmKey, @@ -99,6 +99,17 @@ pub struct Version { 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. /// @@ -270,7 +281,7 @@ impl YubiKey { let txn = self.begin_transaction()?; // get a challenge from the card - let challenge = APDU::new(YKPIV_INS_AUTHENTICATE) + let challenge = APDU::new(Ins::Authenticate) .params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM) .data(&[0x7c, 0x02, 0x80, 0x00]) .transmit(&txn, 261)?; @@ -299,7 +310,7 @@ impl YubiKey { let mut challenge = [0u8; 8]; challenge.copy_from_slice(&data[14..22]); - let authentication = APDU::new(YKPIV_INS_AUTHENTICATE) + let authentication = APDU::new(Ins::Authenticate) .params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM) .data(&data) .transmit(&txn, 261)?; @@ -324,7 +335,7 @@ impl YubiKey { pub fn deauthenticate(&mut self) -> Result<(), Error> { let txn = self.begin_transaction()?; - let status_words = APDU::new(YKPIV_INS_SELECT_APPLICATION) + let status_words = APDU::new(Ins::SelectApplication) .p1(0x04) .data(MGMT_AID) .transmit(&txn, 255)? @@ -422,7 +433,7 @@ impl YubiKey { let templ = [ 0, - YKPIV_INS_SET_PIN_RETRIES, + Ins::SetPinRetries.code(), pin_tries as u8, puk_tries as u8, ]; @@ -644,7 +655,7 @@ impl YubiKey { let mut key_data = Zeroizing::new(vec![0u8; 1024]); let mut in_ptr: *mut u8 = key_data.as_mut_ptr(); - let templ = [0, YKPIV_INS_IMPORT_KEY, algorithm, key]; + let templ = [0, Ins::ImportKey.code(), algorithm, key]; let mut elem_len: u32 = 0; let mut params: [*const u8; 5] = [ptr::null(); 5]; let mut lens = [0usize; 5]; @@ -795,7 +806,7 @@ impl YubiKey { /// #[cfg(feature = "untested")] pub fn attest(&mut self, key: SlotId) -> Result { - let templ = [0, YKPIV_INS_ATTEST, key, 0]; + let templ = [0, Ins::Attest.code(), key, 0]; let txn = self.begin_transaction()?; let response = txn.transfer_data(&templ, &[], CB_OBJ_MAX)?; @@ -819,7 +830,7 @@ impl YubiKey { pub fn get_auth_challenge(&mut self) -> Result<[u8; 8], Error> { let txn = self.begin_transaction()?; - let response = APDU::new(YKPIV_INS_AUTHENTICATE) + let response = APDU::new(Ins::Authenticate) .params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM) .data(&[0x7c, 0x02, 0x81, 0x00]) .transmit(&txn, 261)?; @@ -844,7 +855,7 @@ impl YubiKey { let txn = self.begin_transaction()?; // send the response to the card and a challenge of our own. - let status_words = APDU::new(YKPIV_INS_AUTHENTICATE) + let status_words = APDU::new(Ins::Authenticate) .params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM) .data(&data) .transmit(&txn, 261)? @@ -864,7 +875,7 @@ impl YubiKey { /// 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 templ = [0, Ins::Reset.code(), 0, 0]; let txn = self.begin_transaction()?; let status_words = txn.transfer_data(&templ, &[], 255)?.status_words();