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 Jun 24, 2023
1 parent 8cf18d2 commit b9da471
Show file tree
Hide file tree
Showing 6 changed files with 499 additions and 43 deletions.
16 changes: 16 additions & 0 deletions src/apdu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,15 @@ pub enum Ins {
/// Get slot metadata
GetMetadata,

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

/// YubiHSM Auth // Get challenge
GetHostChallenge,

/// YubiHSM Auth // List credentials
ListCredentials,

/// Other/unrecognized instruction codes
Other(u8),
}
Expand All @@ -223,6 +232,10 @@ impl Ins {
Ins::Attest => 0xf9,
Ins::GetSerial => 0xf8,
Ins::GetMetadata => 0xf7,
// Yubihsm auth
Ins::Calculate => 0x03,
Ins::GetHostChallenge => 0x04,
Ins::ListCredentials => 0x05,
Ins::Other(code) => code,
}
}
Expand All @@ -231,6 +244,9 @@ impl Ins {
impl From<u8> for Ins {
fn from(code: u8) -> Self {
match code {
0x03 => Ins::Calculate,
0x04 => Ins::GetHostChallenge,
0x05 => Ins::ListCredentials,
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;
269 changes: 269 additions & 0 deletions src/hsmauth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
//! 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 nom::{
bytes::complete::{tag, take},
combinator::eof,
error::{Error as NumError, ErrorKind},
multi::many0,
number::complete::u8,
IResult,
};
use std::{fmt, 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.
#[derive(Clone)]
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: &Challenge, hsm_challenge: &Challenge) -> Self {
let mut out = Self::zeroed();
out.0[..8].copy_from_slice(host_challenge.as_slice());
out.0[8..].copy_from_slice(hsm_challenge.as_slice());

out
}

fn zeroed() -> Self {
Self([0u8; 16])
}

/// Build `Context` from the provided buffer
pub fn from_buf(buf: [u8; 16]) -> Self {
Self(buf)
}
}

/// Exclusive access to the Hsmauth applet.
pub struct HsmAuth {
client: YubiKey,
}

impl HsmAuth {
pub(crate) fn new(mut client: 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,
)
}

/// Get YubiKey Challenge
pub fn get_challenge(&mut self, label: Label) -> Result<Challenge> {
Transaction::new(&mut self.client.card)?.get_host_challenge(self.client.version, label)
}

/// List credentials
pub fn list_credentials(&mut self) -> Result<Vec<Credential>> {
Transaction::new(&mut self.client.card)?.list_credentials(self.client.version)
}

/// Retun the inner `YubiKey`
pub fn into_inner(mut self) -> Result<YubiKey> {
Transaction::new(&mut self.client.card)?.select_piv_application()?;
Ok(self.client)
}
}

#[derive(Debug)]
/// Algorithm for the credentials
pub enum Algorithm {
/// AES 128 keys
Aes128,
/// EC P256
EcP256,
}

impl fmt::Display for Algorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Algorithm::Aes128 => write!(f, "AES128"),
Algorithm::EcP256 => write!(f, "ECP256"),
}
}
}

impl Algorithm {
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let (input, value) = u8(input)?;
match value {
38 => Ok((input, Algorithm::Aes128)),
39 => Ok((input, Algorithm::EcP256)),
_ => Err(nom::Err::Error(NumError::new(input, ErrorKind::Tag))),
}
}
}

/// YubiHSM Auth credential store in the Application
#[derive(Debug)]
pub struct Credential {
/// Algorithm of the key
pub algorithm: Algorithm,
/// Is touch required when using this credential
pub touch: bool,
/// Remaining attempts to authenticate to this credential
pub remaining_attempts: u8,
/// Label for the credential
pub label: Vec<u8>,
}

impl Credential {
/// Parse a buffer holding a single credential
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let (input, _list) = tag(b"\x72")(input)?;

let (input, buf_len) = u8(input)?;
let buf_len = if buf_len < 3 {
return Err(nom::Err::Error(NumError::new(
input,
ErrorKind::LengthValue,
)));
} else {
(buf_len - 3) as usize
};

let (input, algorithm) = Algorithm::parse(input)?;

let (input, touch) = u8(input)?;
let touch = match touch {
0 => false,
1 => true,
_ => return Err(nom::Err::Error(NumError::new(input, ErrorKind::Tag))),
};

let (input, label) = take(buf_len)(input)?;

let (input, remaining_attempts) = u8(input)?;

Ok((
input,
Credential {
algorithm,
touch,
label: label.to_vec(),
remaining_attempts,
},
))
}

/// Parse a buffer holding a list of `Credential`
pub fn parse_list(input: &[u8]) -> Result<Vec<Self>> {
let (input, credentials) =
many0(Credential::parse)(input).map_err(|_| Error::ParseError)?;
let (_input, _) = eof(input).map_err(|_: nom::Err<NumError<&[u8]>>| Error::ParseError)?;

Ok(credentials)
}
}

/// 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]>,
}

/// Host challenge used for session keys calculation
#[derive(Debug, Clone)]
pub struct Challenge {
buffer: Zeroizing<[u8; Self::SIZE]>,
length: usize,
}

impl Challenge {
/// Challenge used in SCP03 computation
/// Can be as long as a P256 PUBKEY (for SCP11 support).
pub(crate) const SIZE: usize = 65;

/// Returns a slice containing the entire array.
pub fn as_slice(&self) -> &[u8] {
&self.buffer.as_slice()[..self.length]
}

/// Returns true if the challenge has a length of 0.
pub fn is_empty(&self) -> bool {
self.length == 0
}

/// Copy data from provided slice
pub fn copy_from_slice(&mut self, data: &[u8]) -> Result<()> {
self.length = data.len();
if self.length > Self::SIZE {
Err(Error::SizeError)
} else {
self.buffer[..self.length].copy_from_slice(data);
Ok(())
}
}
}

impl Default for Challenge {
fn default() -> Self {
Self {
buffer: Zeroizing::new([0u8; Self::SIZE]),
length: 0,
}
}
}
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
Loading

0 comments on commit b9da471

Please sign in to comment.