Skip to content

Commit

Permalink
implement yubihsm auth
Browse files Browse the repository at this point in the history
This is used to calculate session keys for Yubico HSM.
  • Loading branch information
baloo committed Mar 19, 2023
1 parent cafb0b2 commit 7f160e4
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 43 deletions.
5 changes: 5 additions & 0 deletions src/apdu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ pub enum Ins {
/// Get slot metadata
GetMetadata,

/// YubiHSM Auth // Calculate session keys
Calculate,

/// Other/unrecognized instruction codes
Other(u8),
}
Expand All @@ -223,6 +226,7 @@ impl Ins {
Ins::Attest => 0xf9,
Ins::GetSerial => 0xf8,
Ins::GetMetadata => 0xf7,
Ins::Calculate => 0x03,
Ins::Other(code) => code,
}
}
Expand All @@ -231,6 +235,7 @@ impl Ins {
impl From<u8> for Ins {
fn from(code: u8) -> Self {
match code {
0x03 => Ins::Calculate,
0x20 => Ins::Verify,
0x24 => Ins::ChangeReference,
0x2c => Ins::ResetRetry,
Expand Down
5 changes: 5 additions & 0 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
115 changes: 115 additions & 0 deletions src/hsmauth.rs
Original file line number Diff line number Diff line change
@@ -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<u8>);

impl FromStr for Label {
type Err = Error;

fn from_str(input: &str) -> Result<Self> {
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<Self> {
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<SessionKeys> {
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]>,
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ mod chuid;
mod config;
mod consts;
mod error;
pub mod hsmauth;
mod metadata;
mod mgm;
#[cfg(feature = "untested")]
Expand Down
138 changes: 98 additions & 40 deletions src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*,
Expand Down Expand Up @@ -61,25 +62,34 @@ 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);
e
})?;

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,
});
}
Expand Down Expand Up @@ -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)?;

Expand All @@ -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()
}
Expand Down Expand Up @@ -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<SessionKeys> {
// 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)
}
}
Loading

0 comments on commit 7f160e4

Please sign in to comment.