use aesni::{Aes128, BlockCipher};
use aesni::block_cipher_trait::generic_array::GenericArray;
use aesni::block_cipher_trait::generic_array::typenum::U16;
use block_modes::{BlockMode, BlockModeIv, Cbc};
use block_modes::block_padding::Iso7816;
use byteorder::{BigEndian, ByteOrder};
use clear_on_drop::clear::Clear;
use cmac::Cmac;
use cmac::crypto_mac::Mac as CryptoMac;
use failure::Error;
use super::kdf;
use super::{Challenge, CommandMessage, CommandType, Context, Cryptogram, ResponseMessage,
SecureChannelError, StaticKeys, CRYPTOGRAM_SIZE, KEY_SIZE, MAC_SIZE};
#[cfg(feature = "mockhsm")]
use super::ResponseCode;
const AES_BLOCK_SIZE: usize = 16;
type Aes128Cbc = Cbc<Aes128, Iso7816>;
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct Id(u8);
impl Id {
pub fn new(id: u8) -> Result<Self, Error> {
if id > MAX_ID.0 {
fail!(
SecureChannelError::ProtocolError,
"session ID exceeds the maximum allowed: {} (max {})",
id,
MAX_ID.0
);
}
Ok(Id(id))
}
pub fn succ(&self) -> Result<Self, Error> {
Self::new(self.0 + 1)
}
pub fn to_u8(&self) -> u8 {
self.0
}
}
pub const MAX_ID: Id = Id(16);
pub const MAX_COMMANDS_PER_SESSION: u32 = 0x10_0000;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SecurityLevel {
NoSecurityLevel,
Authenticated,
Terminated,
}
pub(crate) struct Channel {
id: Id,
counter: u32,
security_level: SecurityLevel,
context: Context,
enc_key: [u8; KEY_SIZE],
mac_key: [u8; KEY_SIZE],
rmac_key: [u8; KEY_SIZE],
mac_chaining_value: [u8; MAC_SIZE * 2],
}
impl Channel {
pub fn new(
id: Id,
static_keys: &StaticKeys,
host_challenge: &Challenge,
card_challenge: &Challenge,
) -> Self {
let context = Context::from_challenges(host_challenge, card_challenge);
let enc_key = derive_key(&static_keys.enc_key, 0b100, &context);
let mac_key = derive_key(&static_keys.mac_key, 0b110, &context);
let rmac_key = derive_key(&static_keys.mac_key, 0b111, &context);
let mac_chaining_value = [0u8; MAC_SIZE * 2];
Self {
id,
counter: 0,
security_level: SecurityLevel::NoSecurityLevel,
context,
enc_key,
mac_key,
rmac_key,
mac_chaining_value,
}
}
pub fn card_cryptogram(&self) -> Cryptogram {
let mut result_bytes = [0u8; CRYPTOGRAM_SIZE];
kdf::derive(&self.mac_key, 0, &self.context, &mut result_bytes);
let result = Cryptogram::from_slice(&result_bytes);
result_bytes.clear();
result
}
pub fn host_cryptogram(&self) -> Cryptogram {
let mut result_bytes = [0u8; CRYPTOGRAM_SIZE];
kdf::derive(&self.mac_key, 1, &self.context, &mut result_bytes);
let result = Cryptogram::from_slice(&result_bytes);
result_bytes.clear();
result
}
pub fn command_with_mac(
&mut self,
command_type: CommandType,
command_data: &[u8],
) -> Result<CommandMessage, Error> {
if self.counter >= MAX_COMMANDS_PER_SESSION {
self.terminate();
fail!(
SecureChannelError::SessionLimitReached,
"max of {} commands per session exceeded",
MAX_COMMANDS_PER_SESSION
);
}
let mut mac = Cmac::<Aes128>::new_varkey(self.mac_key.as_ref()).unwrap();
mac.input(&self.mac_chaining_value);
mac.input(&[command_type.to_u8()]);
let mut length = [0u8; 2];
BigEndian::write_u16(&mut length, (1 + command_data.len() + MAC_SIZE) as u16);
mac.input(&length);
mac.input(&[self.id.to_u8()]);
mac.input(command_data);
let tag = mac.result().code();
self.mac_chaining_value.copy_from_slice(tag.as_slice());
Ok(CommandMessage::new_with_mac(
command_type,
self.id,
command_data,
&tag,
)?)
}
pub fn authenticate_session(&mut self) -> Result<CommandMessage, Error> {
assert_eq!(self.security_level, SecurityLevel::NoSecurityLevel);
assert_eq!(self.mac_chaining_value, [0u8; MAC_SIZE * 2]);
let host_cryptogram = self.host_cryptogram();
self.command_with_mac(CommandType::AuthSession, host_cryptogram.as_slice())
}
pub fn finish_authenticate_session(&mut self, response: &ResponseMessage) -> Result<(), Error> {
if !response.data.is_empty() {
self.terminate();
fail!(
SecureChannelError::ProtocolError,
"expected empty response data (got {}-bytes)",
response.data.len(),
);
}
self.security_level = SecurityLevel::Authenticated;
self.counter = 1;
Ok(())
}
pub fn encrypt_command(&mut self, command: CommandMessage) -> Result<CommandMessage, Error> {
assert_eq!(self.security_level, SecurityLevel::Authenticated);
let mut message: Vec<u8> = command.into();
let pos = message.len();
message.extend_from_slice(&[0u8; AES_BLOCK_SIZE]);
let cipher = Aes128::new_varkey(&self.enc_key).unwrap();
let icv = compute_icv(&cipher, self.counter);
let cbc_encryptor = Aes128Cbc::new(cipher, &icv);
let ciphertext = cbc_encryptor.encrypt_pad(&mut message, pos).unwrap();
self.command_with_mac(CommandType::SessionMessage, ciphertext)
}
pub fn decrypt_response(
&mut self,
encrypted_response: ResponseMessage,
) -> Result<ResponseMessage, Error> {
assert_eq!(self.security_level, SecurityLevel::Authenticated);
let cipher = Aes128::new_varkey(&self.enc_key).unwrap();
let icv = compute_icv(&cipher, self.counter);
self.verify_response_mac(&encrypted_response)?;
let cipher = Aes128::new_varkey(&self.enc_key).unwrap();
let cbc_decryptor = Aes128Cbc::new(cipher, &icv);
let mut response_message = encrypted_response.data;
let response_len = cbc_decryptor
.decrypt_pad(&mut response_message)
.map_err(|e| {
self.terminate();
err!(
SecureChannelError::ProtocolError,
"error decrypting response: {:?}",
e
)
})?
.len();
response_message.truncate(response_len);
let mut decrypted_response = ResponseMessage::parse(response_message)?;
decrypted_response.session_id = encrypted_response.session_id;
Ok(decrypted_response)
}
pub fn verify_response_mac(&mut self, response: &ResponseMessage) -> Result<(), Error> {
assert_eq!(self.security_level, SecurityLevel::Authenticated);
let session_id = response.session_id.ok_or_else(|| {
self.terminate();
err!(
SecureChannelError::ProtocolError,
"no session ID in response"
)
})?;
assert_eq!(self.id, session_id, "session ID mismatch: {:?}", session_id);
let mut mac = Cmac::<Aes128>::new_varkey(self.rmac_key.as_ref()).unwrap();
mac.input(&self.mac_chaining_value);
mac.input(&[response.code.to_u8()]);
let mut length = [0u8; 2];
BigEndian::write_u16(&mut length, response.len() as u16);
mac.input(&length);
mac.input(&[session_id.to_u8()]);
mac.input(&response.data);
if response
.mac
.as_ref()
.expect("missing R-MAC tag!")
.verify(&mac.result().code())
.is_err()
{
self.terminate();
fail!(SecureChannelError::VerifyFailed, "R-MAC mismatch!");
}
self.increment_counter();
Ok(())
}
#[cfg(feature = "mockhsm")]
pub fn verify_authenticate_session(
&mut self,
command: &CommandMessage,
) -> Result<ResponseMessage, Error> {
assert_eq!(self.security_level, SecurityLevel::NoSecurityLevel);
assert_eq!(self.mac_chaining_value, [0u8; MAC_SIZE * 2]);
if command.data.len() != CRYPTOGRAM_SIZE {
self.terminate();
fail!(
SecureChannelError::ProtocolError,
"expected {}-byte command data (got {})",
CRYPTOGRAM_SIZE,
command.data.len()
);
}
let expected_host_cryptogram = self.host_cryptogram();
let actual_host_cryptogram = Cryptogram::from_slice(&command.data);
if expected_host_cryptogram != actual_host_cryptogram {
self.terminate();
fail!(
SecureChannelError::VerifyFailed,
"host cryptogram mismatch!"
);
}
self.verify_command_mac(command)?;
self.security_level = SecurityLevel::Authenticated;
self.counter = 1;
Ok(ResponseMessage::success(CommandType::AuthSession, vec![]))
}
#[cfg(feature = "mockhsm")]
pub fn decrypt_command(
&mut self,
encrypted_command: CommandMessage,
) -> Result<CommandMessage, Error> {
assert_eq!(self.security_level, SecurityLevel::Authenticated);
let cipher = Aes128::new_varkey(&self.enc_key).unwrap();
let icv = compute_icv(&cipher, self.counter);
self.verify_command_mac(&encrypted_command)?;
let cipher = Aes128::new_varkey(&self.enc_key).unwrap();
let cbc_decryptor = Aes128Cbc::new(cipher, &icv);
let mut command_data = encrypted_command.data;
let command_len = cbc_decryptor
.decrypt_pad(&mut command_data)
.map_err(|e| {
self.terminate();
err!(
SecureChannelError::ProtocolError,
"error decrypting command: {:?}",
e
)
})?
.len();
command_data.truncate(command_len);
let mut decrypted_command = CommandMessage::parse(command_data)?;
decrypted_command.session_id = encrypted_command.session_id;
Ok(decrypted_command)
}
#[cfg(feature = "mockhsm")]
pub fn verify_command_mac(&mut self, command: &CommandMessage) -> Result<(), Error> {
assert_eq!(
command.session_id.unwrap(),
self.id,
"session ID mismatch: {:?}",
command.session_id
);
let mut mac = Cmac::<Aes128>::new_varkey(self.mac_key.as_ref()).unwrap();
mac.input(&self.mac_chaining_value);
mac.input(&[command.command_type.to_u8()]);
let mut length = [0u8; 2];
BigEndian::write_u16(&mut length, command.len() as u16);
mac.input(&length);
mac.input(&[command.session_id.unwrap().to_u8()]);
mac.input(&command.data);
let tag = mac.result().code();
if command
.mac
.as_ref()
.expect("missing C-MAC tag!")
.verify(&tag)
.is_err()
{
self.terminate();
fail!(SecureChannelError::VerifyFailed, "C-MAC mismatch!");
}
self.mac_chaining_value.copy_from_slice(tag.as_slice());
Ok(())
}
#[cfg(feature = "mockhsm")]
pub fn encrypt_response(
&mut self,
response: ResponseMessage,
) -> Result<ResponseMessage, Error> {
assert_eq!(self.security_level, SecurityLevel::Authenticated);
let mut message: Vec<u8> = response.into();
let pos = message.len();
message.extend_from_slice(&[0u8; AES_BLOCK_SIZE]);
let cipher = Aes128::new_varkey(&self.enc_key).unwrap();
let icv = compute_icv(&cipher, self.counter);
let cbc_encryptor = Aes128Cbc::new(cipher, &icv);
let ct_len = cbc_encryptor.encrypt_pad(&mut message, pos).unwrap().len();
message.truncate(ct_len);
self.response_with_mac(ResponseCode::Success(CommandType::SessionMessage), message)
}
#[cfg(feature = "mockhsm")]
pub fn response_with_mac<T>(
&mut self,
code: ResponseCode,
response_data: T,
) -> Result<ResponseMessage, Error>
where
T: Into<Vec<u8>>,
{
assert_eq!(self.security_level, SecurityLevel::Authenticated);
let body = response_data.into();
let mut mac = Cmac::<Aes128>::new_varkey(self.rmac_key.as_ref()).unwrap();
mac.input(&self.mac_chaining_value);
mac.input(&[code.to_u8()]);
let mut length = [0u8; 2];
BigEndian::write_u16(&mut length, (1 + body.len() + MAC_SIZE) as u16);
mac.input(&length);
mac.input(&[self.id.to_u8()]);
mac.input(&body);
self.increment_counter();
Ok(ResponseMessage::new_with_mac(
code,
self.id,
body,
&mac.result().code(),
))
}
fn increment_counter(&mut self) {
self.counter = self.counter.checked_add(1).unwrap_or_else(|| {
panic!("session counter overflowed!");
});
}
fn terminate(&mut self) {
self.security_level = SecurityLevel::Terminated;
self.enc_key.clear();
self.mac_key.clear();
self.rmac_key.clear();
}
}
impl Drop for Channel {
fn drop(&mut self) {
self.terminate();
}
}
fn derive_key(
parent_key: &[u8; KEY_SIZE],
derivation_constant: u8,
context: &Context,
) -> [u8; KEY_SIZE] {
let mut key = [0u8; KEY_SIZE];
kdf::derive(parent_key, derivation_constant, context, &mut key);
key
}
fn compute_icv(cipher: &Aes128, counter: u32) -> GenericArray<u8, U16> {
let mut icv = GenericArray::clone_from_slice(&[0u8; AES_BLOCK_SIZE]);
BigEndian::write_u32(&mut icv.as_mut_slice()[12..], counter);
cipher.encrypt_block(&mut icv);
icv
}
#[cfg(all(test, feature = "mockhsm"))]
mod tests {
use securechannel::{Challenge, Channel, CommandMessage, CommandType, ResponseMessage,
SessionId, StaticKeys};
const PASSWORD: &[u8] = b"password";
const SALT: &[u8] = b"Yubico";
const PBKDF_ITERATIONS: usize = 10000;
const HOST_CHALLENGE: &[u8] = &[0u8; 8];
const CARD_CHALLENGE: &[u8] = &[0u8; 8];
const COMMAND_TYPE: CommandType = CommandType::Echo;
const COMMAND_DATA: &[u8] = b"Hello, world!";
#[test]
fn happy_path_test() {
let static_keys = StaticKeys::derive_from_password(PASSWORD, SALT, PBKDF_ITERATIONS);
let host_challenge = Challenge::from_slice(HOST_CHALLENGE);
let card_challenge = Challenge::from_slice(CARD_CHALLENGE);
let session_id = SessionId::new(0).unwrap();
let mut host_channel =
Channel::new(session_id, &static_keys, &host_challenge, &card_challenge);
let mut card_channel =
Channel::new(session_id, &static_keys, &host_challenge, &card_challenge);
let auth_command = host_channel.authenticate_session().unwrap();
let auth_response = card_channel
.verify_authenticate_session(&auth_command)
.unwrap();
host_channel
.finish_authenticate_session(&auth_response)
.unwrap();
let command_ciphertext = host_channel
.encrypt_command(CommandMessage::new(COMMAND_TYPE, Vec::from(COMMAND_DATA)).unwrap())
.unwrap();
let decrypted_command = card_channel.decrypt_command(command_ciphertext).unwrap();
let response_ciphertext = card_channel
.encrypt_response(ResponseMessage::success(
decrypted_command.command_type,
decrypted_command.data,
))
.unwrap();
let decrypted_response = host_channel.decrypt_response(response_ciphertext).unwrap();
assert_eq!(decrypted_response.command().unwrap(), COMMAND_TYPE);
assert_eq!(&decrypted_response.data[..], COMMAND_DATA);
}
}