diff --git a/src/apdu.rs b/src/apdu.rs index d8942a32..d09a61dc 100644 --- a/src/apdu.rs +++ b/src/apdu.rs @@ -198,6 +198,9 @@ pub enum Ins { /// Get slot metadata GetMetadata, + /// YubiHSM Auth // Calculate session keys + Calculate, + /// Other/unrecognized instruction codes Other(u8), } @@ -223,6 +226,7 @@ impl Ins { Ins::Attest => 0xf9, Ins::GetSerial => 0xf8, Ins::GetMetadata => 0xf7, + Ins::Calculate => 0x03, Ins::Other(code) => code, } } @@ -231,6 +235,7 @@ impl Ins { impl From for Ins { fn from(code: u8) -> Self { match code { + 0x03 => Ins::Calculate, 0x20 => Ins::Verify, 0x24 => Ins::ChangeReference, 0x2c => Ins::ResetRetry, diff --git a/src/consts.rs b/src/consts.rs index 03242ab8..5e06591b 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -18,3 +18,8 @@ pub(crate) const TAG_ADMIN_TIMESTAMP: u8 = 0x83; // Protected tags pub(crate) const TAG_PROTECTED_FLAGS_1: u8 = 0x81; pub(crate) const TAG_PROTECTED_MGM: u8 = 0x89; + +// YubiHSM Auth +pub(crate) const TAG_LABEL: u8 = 0x71; +pub(crate) const TAG_PW: u8 = 0x73; +pub(crate) const TAG_CONTEXT: u8 = 0x77; diff --git a/src/hsmauth.rs b/src/hsmauth.rs new file mode 100644 index 00000000..20d7c4d9 --- /dev/null +++ b/src/hsmauth.rs @@ -0,0 +1,115 @@ +//! YubiHSM Auth protocol +//! +//! YubiHSM Auth is a YubiKey CCID application that stores the long-lived +//! credentials used to establish secure sessions with a YubiHSM 2. The secure +//! session protocol is based on Secure Channel Protocol 3 (SCP03). +use crate::{ + error::{Error, Result}, + transaction::Transaction, + YubiKey, +}; +use std::str::FromStr; +use zeroize::Zeroizing; + +/// Yubikey HSM Auth Applet ID +pub(crate) const APPLET_ID: &[u8] = &[0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x07, 0x01]; +/// Yubikey HSM Auth Applet Name +pub(crate) const APPLET_NAME: &str = "YubiHSM"; + +/// AES key size in bytes. SCP03 theoretically supports other key sizes, but +/// the YubiHSM 2 does not. Since this crate is somewhat specialized to the `YubiHSM 2` (at least for now) +/// we hardcode to 128-bit for simplicity. +pub(crate) const KEY_SIZE: usize = 16; + +/// Password to authenticate to the Yubikey HSM Auth Applet has a max length of 16 +pub(crate) const PW_LEN: usize = 16; + +/// Label associated with a secret on the Yubikey. +pub struct Label(pub(crate) Vec); + +impl FromStr for Label { + type Err = Error; + + fn from_str(input: &str) -> Result { + let buf = input.as_bytes(); + + if (1..=64).contains(&buf.len()) { + Ok(Self(buf.to_vec())) + } else { + Err(Error::ParseError) + } + } +} + +/// [`Context`] holds the various challenges used for the authentication. +/// +/// This is used as part of the key derivation for the session keys. +pub struct Context(pub(crate) [u8; 16]); + +impl Context { + /// Creates a [`Context`] from its components + pub fn new(host_challenge: [u8; 8], hsm_challenge: [u8; 8]) -> Self { + let mut out = Self::zeroed(); + out.0[..8].copy_from_slice(&host_challenge[..]); + out.0[8..].copy_from_slice(&hsm_challenge[..]); + + out + } + + fn zeroed() -> Self { + Self([0u8; 16]) + } +} + +/// Exclusive access to the Hsmauth applet. +pub struct HsmAuth<'y> { + client: &'y mut YubiKey, +} + +impl<'y> HsmAuth<'y> { + pub(crate) fn new(client: &'y mut YubiKey) -> Result { + Transaction::new(&mut client.card)?.select_application( + APPLET_ID, + APPLET_NAME, + "failed selecting YkHSM auth application", + )?; + + Ok(Self { client }) + } + + /// Calculate session key with the specified key. + pub fn calculate( + &mut self, + label: Label, + context: Context, + password: &[u8], + ) -> Result { + Transaction::new(&mut self.client.card)?.calculate( + self.client.version, + label, + context, + password, + ) + } +} + +impl<'y> Drop for HsmAuth<'y> { + fn drop(&mut self) { + // Revert to PIV application on drop + Transaction::new(&mut self.client.card) + .unwrap() + .select_piv_application() + .unwrap() + } +} + +/// The sessions keys after negociation via SCP03. +#[derive(Default, Debug)] +pub struct SessionKeys { + /// Session encryption key (S-ENC) + pub enc_key: Zeroizing<[u8; KEY_SIZE]>, + /// Session Command MAC key (S-MAC) + pub mac_key: Zeroizing<[u8; KEY_SIZE]>, + /// Session Respose MAC key (S-RMAC) + pub rmac_key: Zeroizing<[u8; KEY_SIZE]>, +} diff --git a/src/lib.rs b/src/lib.rs index 571712ee..1fada138 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,7 @@ mod chuid; mod config; mod consts; mod error; +pub mod hsmauth; mod metadata; mod mgm; #[cfg(feature = "untested")] diff --git a/src/transaction.rs b/src/transaction.rs index 13987416..56dac31d 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -3,8 +3,9 @@ use crate::{ apdu::Response, apdu::{Apdu, Ins, StatusWords}, - consts::{CB_BUF_MAX, CB_OBJ_MAX}, + consts::{CB_BUF_MAX, CB_OBJ_MAX, TAG_CONTEXT, TAG_LABEL, TAG_PW}, error::{Error, Result}, + hsmauth::{self, Context, Label, SessionKeys}, otp, piv::{self, AlgorithmId, SlotId}, serialization::*, @@ -61,10 +62,24 @@ impl<'tx> Transaction<'tx> { } /// Select application. - pub fn select_application(&self) -> Result<()> { + pub fn select_piv_application(&self) -> Result<()> { + self.select_application( + piv::APPLET_ID, + piv::APPLET_NAME, + "failed selecting application", + ) + } + + /// Select application. + pub(crate) fn select_application( + &self, + applet: &[u8], + applet_name: &'static str, + error: &'static str, + ) -> Result<()> { let response = Apdu::new(Ins::SelectApplication) .p1(0x04) - .data(piv::APPLET_ID) + .data(applet) .transmit(self, 0xFF) .map_err(|e| { error!("failed communicating with card: '{}'", e); @@ -72,14 +87,9 @@ impl<'tx> Transaction<'tx> { })?; if !response.is_success() { - error!( - "failed selecting application: {:04x}", - response.status_words().code() - ); + error!("{}: {:04x}", error, response.status_words().code()); return Err(match response.status_words() { - StatusWords::NotFoundError => Error::AppletNotFound { - applet_name: piv::APPLET_NAME, - }, + StatusWords::NotFoundError => Error::AppletNotFound { applet_name }, _ => Error::GenericError, }); } @@ -108,21 +118,11 @@ impl<'tx> Transaction<'tx> { match version.major { // YK4 requires switching to the YK applet to retrieve the serial 4 => { - let sw = Apdu::new(Ins::SelectApplication) - .p1(0x04) - .data(otp::APPLET_ID) - .transmit(self, 0xFF)? - .status_words(); - - if !sw.is_success() { - error!("failed selecting yk application: {:04x}", sw.code()); - return Err(match sw { - StatusWords::NotFoundError => Error::AppletNotFound { - applet_name: otp::APPLET_NAME, - }, - _ => Error::GenericError, - }); - } + self.select_application( + otp::APPLET_ID, + otp::APPLET_NAME, + "failed selecting yk application", + )?; let response = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?; @@ -136,21 +136,11 @@ impl<'tx> Transaction<'tx> { } // reselect the PIV applet - let sw = Apdu::new(Ins::SelectApplication) - .p1(0x04) - .data(piv::APPLET_ID) - .transmit(self, 0xFF)? - .status_words(); - - if !sw.is_success() { - error!("failed selecting application: {:04x}", sw.code()); - return Err(match sw { - StatusWords::NotFoundError => Error::AppletNotFound { - applet_name: piv::APPLET_NAME, - }, - _ => Error::GenericError, - }); - } + self.select_application( + piv::APPLET_ID, + piv::APPLET_NAME, + "failed selecting application", + )?; response.data().try_into() } @@ -515,4 +505,72 @@ impl<'tx> Transaction<'tx> { _ => Err(Error::GenericError), } } + + /// Get AES-128 session keys + /// + /// Get the SCP03 session keys from an AES-128 credential. + pub fn calculate( + &mut self, + version: Version, + label: Label, + context: Context, + password: &[u8], + ) -> Result { + // YubiHSM was introduced by firmware 5.4.3 + // https://docs.yubico.com/yesdk/users-manual/application-yubihsm-auth/yubihsm-auth-overview.html + if version + < (Version { + major: 5, + minor: 4, + patch: 3, + }) + { + return Err(Error::NotSupported); + } + + let mut data = [0u8; CB_BUF_MAX]; + let mut len = data.len(); + let mut data_remaining = &mut data[..]; + + let offset = Tlv::write(data_remaining, TAG_LABEL, &label.0)?; + data_remaining = &mut data_remaining[offset..]; + + let offset = Tlv::write(data_remaining, TAG_CONTEXT, &context.0)?; + data_remaining = &mut data_remaining[offset..]; + + let mut password = password.to_vec(); + password.resize(hsmauth::PW_LEN, 0); + + let offset = Tlv::write(data_remaining, TAG_PW, &password)?; + data_remaining = &mut data_remaining[offset..]; + len -= data_remaining.len(); + + let response = Apdu::new(Ins::Calculate) + .params(0x00, 0x00) + .data(&data[..len]) + .transmit(self, (hsmauth::KEY_SIZE * 3) + 2)?; + + if !response.is_success() { + error!( + "failed calculating the session secret: {:04x}", + response.status_words().code() + ); + return Err(Error::GenericError); + } + + let data = response.data(); + + let mut session_keys = SessionKeys::default(); + session_keys + .enc_key + .copy_from_slice(&data[..hsmauth::KEY_SIZE]); + session_keys + .mac_key + .copy_from_slice(&data[hsmauth::KEY_SIZE..hsmauth::KEY_SIZE * 2]); + session_keys + .rmac_key + .copy_from_slice(&data[hsmauth::KEY_SIZE * 2..]); + + Ok(session_keys) + } } diff --git a/src/yubikey.rs b/src/yubikey.rs index 6a699e31..c3c21bad 100644 --- a/src/yubikey.rs +++ b/src/yubikey.rs @@ -36,6 +36,7 @@ use crate::{ chuid::ChuId, config::Config, error::{Error, Result}, + hsmauth::HsmAuth, mgm::MgmKey, piv, reader::{Context, Reader}, @@ -45,6 +46,7 @@ use log::{error, info}; use pcsc::{Card, Disposition}; use rand_core::{OsRng, RngCore}; use std::{ + cmp::{Ord, Ordering}, fmt::{self, Display}, str::FromStr, }; @@ -145,6 +147,39 @@ impl Version { } } +impl Ord for Version { + fn cmp(&self, other: &Self) -> Ordering { + if self.major > other.major { + return Ordering::Greater; + } + if self.major < other.major { + return Ordering::Less; + } + + if self.minor > other.minor { + return Ordering::Greater; + } + if self.minor < other.minor { + return Ordering::Less; + } + + if self.patch > other.patch { + return Ordering::Greater; + } + if self.patch < other.patch { + return Ordering::Less; + } + + Ordering::Equal + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl Display for Version { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}.{}.{}", self.major, self.minor, self.patch) @@ -259,7 +294,7 @@ impl YubiKey { .map(|p| Buffer::new(p.expose_secret().clone())); let txn = Transaction::new(&mut self.card)?; - txn.select_application()?; + txn.select_piv_application()?; if let Some(p) = &pin { txn.verify_pin(p)?; @@ -444,7 +479,7 @@ impl YubiKey { // 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()?; + txn.select_piv_application()?; // WRONG_PIN is expected on successful query. match txn.verify_pin(&[]) { @@ -690,6 +725,11 @@ impl YubiKey { Ok(()) } + + /// Creates a client for the YubiHSM AUth + pub fn hsmauth(&mut self) -> Result> { + HsmAuth::new(self) + } } impl<'a> TryFrom<&'a Reader<'_>> for YubiKey { @@ -705,7 +745,7 @@ impl<'a> TryFrom<&'a Reader<'_>> for YubiKey { let mut app_version_serial = || -> Result<(Version, Serial)> { let txn = Transaction::new(&mut card)?; - txn.select_application()?; + txn.select_piv_application()?; let v = txn.get_version()?; let s = txn.get_serial(v)?;