Skip to content
Snippets Groups Projects
Unverified Commit dabbe386 authored by kaczmarczyck's avatar kaczmarczyck Committed by GitHub
Browse files

Merge pull request #75 from kaczmarczyck/HMAC-secret-extension

adding HMAC-secret support
parents 8de99f70 99ae5278
No related branches found
No related tags found
No related merge requests found
...@@ -220,6 +220,78 @@ impl TryFrom<&cbor::Value> for Extensions { ...@@ -220,6 +220,78 @@ impl TryFrom<&cbor::Value> for Extensions {
} }
} }
impl From<Extensions> for cbor::Value {
fn from(extensions: Extensions) -> Self {
cbor_map_btree!(extensions
.0
.into_iter()
.map(|(key, value)| (cbor_text!(key), value))
.collect())
}
}
impl Extensions {
#[cfg(test)]
pub fn new(extension_map: BTreeMap<String, cbor::Value>) -> Self {
Extensions(extension_map)
}
pub fn has_make_credential_hmac_secret(&self) -> Result<bool, Ctap2StatusCode> {
self.0
.get("hmac-secret")
.map(read_bool)
.unwrap_or(Ok(false))
}
pub fn get_assertion_hmac_secret(
&self,
) -> Option<Result<GetAssertionHmacSecretInput, Ctap2StatusCode>> {
self.0
.get("hmac-secret")
.map(GetAssertionHmacSecretInput::try_from)
}
}
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct GetAssertionHmacSecretInput {
pub key_agreement: CoseKey,
pub salt_enc: Vec<u8>,
pub salt_auth: Vec<u8>,
}
impl TryFrom<&cbor::Value> for GetAssertionHmacSecretInput {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: &cbor::Value) -> Result<Self, Ctap2StatusCode> {
let input_map = read_map(cbor_value)?;
let cose_key = read_map(ok_or_missing(input_map.get(&cbor_unsigned!(1)))?)?;
let salt_enc = read_byte_string(ok_or_missing(input_map.get(&cbor_unsigned!(2)))?)?;
let salt_auth = read_byte_string(ok_or_missing(input_map.get(&cbor_unsigned!(3)))?)?;
Ok(Self {
key_agreement: CoseKey(cose_key.clone()),
salt_enc,
salt_auth,
})
}
}
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct GetAssertionHmacSecretOutput(Vec<u8>);
impl From<GetAssertionHmacSecretOutput> for cbor::Value {
fn from(message: GetAssertionHmacSecretOutput) -> cbor::Value {
cbor_bytes!(message.0)
}
}
impl TryFrom<&cbor::Value> for GetAssertionHmacSecretOutput {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: &cbor::Value) -> Result<Self, Ctap2StatusCode> {
Ok(GetAssertionHmacSecretOutput(read_byte_string(cbor_value)?))
}
}
// Even though options are optional, we can use the default if not present. // Even though options are optional, we can use the default if not present.
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct MakeCredentialOptions { pub struct MakeCredentialOptions {
...@@ -314,6 +386,7 @@ pub struct PublicKeyCredentialSource { ...@@ -314,6 +386,7 @@ pub struct PublicKeyCredentialSource {
pub rp_id: String, pub rp_id: String,
pub user_handle: Vec<u8>, // not optional, but nullable pub user_handle: Vec<u8>, // not optional, but nullable
pub other_ui: Option<String>, pub other_ui: Option<String>,
pub cred_random: Option<Vec<u8>>,
} }
impl From<PublicKeyCredentialSource> for cbor::Value { impl From<PublicKeyCredentialSource> for cbor::Value {
...@@ -324,12 +397,17 @@ impl From<PublicKeyCredentialSource> for cbor::Value { ...@@ -324,12 +397,17 @@ impl From<PublicKeyCredentialSource> for cbor::Value {
None => cbor_null!(), None => cbor_null!(),
Some(other_ui) => cbor_text!(other_ui), Some(other_ui) => cbor_text!(other_ui),
}; };
let cred_random = match credential.cred_random {
None => cbor_null!(),
Some(cred_random) => cbor_bytes!(cred_random),
};
cbor_array! { cbor_array! {
credential.credential_id, credential.credential_id,
private_key, private_key,
credential.rp_id, credential.rp_id,
credential.user_handle, credential.user_handle,
other_ui, other_ui,
cred_random,
} }
} }
} }
...@@ -341,7 +419,7 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource { ...@@ -341,7 +419,7 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource {
use cbor::{SimpleValue, Value}; use cbor::{SimpleValue, Value};
let fields = read_array(&cbor_value)?; let fields = read_array(&cbor_value)?;
if fields.len() != 5 { if fields.len() != 6 {
return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR); return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR);
} }
let credential_id = read_byte_string(&fields[0])?; let credential_id = read_byte_string(&fields[0])?;
...@@ -357,6 +435,10 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource { ...@@ -357,6 +435,10 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource {
Value::Simple(SimpleValue::NullValue) => None, Value::Simple(SimpleValue::NullValue) => None,
cbor_value => Some(read_text_string(cbor_value)?), cbor_value => Some(read_text_string(cbor_value)?),
}; };
let cred_random = match &fields[5] {
Value::Simple(SimpleValue::NullValue) => None,
cbor_value => Some(read_byte_string(cbor_value)?),
};
Ok(PublicKeyCredentialSource { Ok(PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey, key_type: PublicKeyCredentialType::PublicKey,
credential_id, credential_id,
...@@ -364,6 +446,7 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource { ...@@ -364,6 +446,7 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource {
rp_id, rp_id,
user_handle, user_handle,
other_ui, other_ui,
cred_random,
}) })
} }
} }
...@@ -993,6 +1076,7 @@ mod test { ...@@ -993,6 +1076,7 @@ mod test {
rp_id: "example.com".to_string(), rp_id: "example.com".to_string(),
user_handle: b"foo".to_vec(), user_handle: b"foo".to_vec(),
other_ui: None, other_ui: None,
cred_random: None,
}; };
assert_eq!( assert_eq!(
...@@ -1005,6 +1089,16 @@ mod test { ...@@ -1005,6 +1089,16 @@ mod test {
..credential ..credential
}; };
assert_eq!(
PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())),
Ok(credential.clone())
);
let credential = PublicKeyCredentialSource {
cred_random: Some(vec![0x00; 32]),
..credential
};
assert_eq!( assert_eq!(
PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())),
Ok(credential) Ok(credential)
......
...@@ -28,9 +28,9 @@ use self::command::{ ...@@ -28,9 +28,9 @@ use self::command::{
AuthenticatorMakeCredentialParameters, Command, AuthenticatorMakeCredentialParameters, Command,
}; };
use self::data_formats::{ use self::data_formats::{
ClientPinSubCommand, CoseKey, PackedAttestationStatement, PublicKeyCredentialDescriptor, ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput, PackedAttestationStatement,
PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity, PublicKeyCredentialDescriptor, PublicKeyCredentialSource, PublicKeyCredentialType,
SignatureAlgorithm, PublicKeyCredentialUserEntity, SignatureAlgorithm,
}; };
use self::hid::ChannelID; use self::hid::ChannelID;
use self::key_material::{AAGUID, ATTESTATION_CERTIFICATE, ATTESTATION_PRIVATE_KEY}; use self::key_material::{AAGUID, ATTESTATION_CERTIFICATE, ATTESTATION_PRIVATE_KEY};
...@@ -81,9 +81,14 @@ const PIN_PADDED_LENGTH: usize = 64; ...@@ -81,9 +81,14 @@ const PIN_PADDED_LENGTH: usize = 64;
// - 32 byte relying party ID hashed with SHA256, // - 32 byte relying party ID hashed with SHA256,
// - 32 byte HMAC-SHA256 over everything else. // - 32 byte HMAC-SHA256 over everything else.
pub const ENCRYPTED_CREDENTIAL_ID_SIZE: usize = 112; pub const ENCRYPTED_CREDENTIAL_ID_SIZE: usize = 112;
// Set this bit when checking user presence.
const UP_FLAG: u8 = 0x01; const UP_FLAG: u8 = 0x01;
// Set this bit when checking user verification.
const UV_FLAG: u8 = 0x04; const UV_FLAG: u8 = 0x04;
// Set this bit when performing attestation.
const AT_FLAG: u8 = 0x40; const AT_FLAG: u8 = 0x40;
// Set this bit when an extension is used.
const ED_FLAG: u8 = 0x80;
pub const TOUCH_TIMEOUT_MS: isize = 30000; pub const TOUCH_TIMEOUT_MS: isize = 30000;
#[cfg(feature = "with_ctap1")] #[cfg(feature = "with_ctap1")]
...@@ -105,6 +110,63 @@ fn check_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> boo ...@@ -105,6 +110,63 @@ fn check_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> boo
) )
} }
// Decrypts the HMAC secret salt(s) that were encrypted with the shared secret.
// The credRandom is used as a secret to HMAC those salts.
// The last step is to re-encrypt the outputs.
pub fn encrypt_hmac_secret_output(
shared_secret: &[u8; 32],
salt_enc: &[u8],
cred_random: &[u8],
) -> Result<Vec<u8>, Ctap2StatusCode> {
if salt_enc.len() != 32 && salt_enc.len() != 64 {
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
}
if cred_random.len() != 32 {
// We are strict here. We need at least 32 byte, but expect exactly 32.
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
}
let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret);
let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key);
// The specification specifically asks for a zero IV.
let iv = [0; 16];
let mut cred_random_secret = [0; 32];
cred_random_secret.clone_from_slice(cred_random);
// Initialization of 4 blocks in any case makes this function more readable.
let mut blocks = [[0u8; 16]; 4];
let block_len = salt_enc.len() / 16;
for i in 0..block_len {
blocks[i].copy_from_slice(&salt_enc[16 * i..16 * (i + 1)]);
}
cbc_decrypt(&aes_dec_key, iv, &mut blocks[..block_len]);
let mut decrypted_salt1 = [0; 32];
decrypted_salt1[..16].clone_from_slice(&blocks[0]);
let output1 = hmac_256::<Sha256>(&cred_random_secret, &decrypted_salt1[..]);
decrypted_salt1[16..].clone_from_slice(&blocks[1]);
for i in 0..2 {
blocks[i].copy_from_slice(&output1[16 * i..16 * (i + 1)]);
}
if block_len == 4 {
let mut decrypted_salt2 = [0; 32];
decrypted_salt2[..16].clone_from_slice(&blocks[2]);
decrypted_salt2[16..].clone_from_slice(&blocks[3]);
let output2 = hmac_256::<Sha256>(&cred_random_secret, &decrypted_salt2[..]);
for i in 0..2 {
blocks[i + 2].copy_from_slice(&output2[16 * i..16 * (i + 1)]);
}
}
cbc_encrypt(&aes_enc_key, iv, &mut blocks[..block_len]);
let mut encrypted_output = Vec::with_capacity(salt_enc.len());
for b in &blocks[..block_len] {
encrypted_output.extend(b);
}
Ok(encrypted_output)
}
// This function is adapted from https://doc.rust-lang.org/nightly/src/core/str/mod.rs.html#2110 // This function is adapted from https://doc.rust-lang.org/nightly/src/core/str/mod.rs.html#2110
// (as of 2020-01-20) and truncates to "max" bytes, not breaking the encoding. // (as of 2020-01-20) and truncates to "max" bytes, not breaking the encoding.
// We change the return value, since we don't need the bool. // We change the return value, since we don't need the bool.
...@@ -261,6 +323,7 @@ where ...@@ -261,6 +323,7 @@ where
rp_id: String::from(""), rp_id: String::from(""),
user_handle: vec![], user_handle: vec![],
other_ui: None, other_ui: None,
cred_random: None,
}) })
} }
...@@ -324,10 +387,10 @@ where ...@@ -324,10 +387,10 @@ where
user, user,
pub_key_cred_params, pub_key_cred_params,
exclude_list, exclude_list,
extensions,
options, options,
pin_uv_auth_param, pin_uv_auth_param,
pin_uv_auth_protocol, pin_uv_auth_protocol,
..
} = make_credential_params; } = make_credential_params;
if let Some(auth_param) = &pin_uv_auth_param { if let Some(auth_param) = &pin_uv_auth_param {
...@@ -362,6 +425,19 @@ where ...@@ -362,6 +425,19 @@ where
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM);
} }
let use_hmac_extension =
extensions.map_or(Ok(false), |e| e.has_make_credential_hmac_secret())?;
if use_hmac_extension && !options.rk {
// The extension is actually supported, but we need resident keys.
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
}
let cred_random = if use_hmac_extension {
Some(self.rng.gen_uniform_u8x32().to_vec())
} else {
None
};
let ed_flag = if use_hmac_extension { ED_FLAG } else { 0 };
let rp_id = rp.rp_id; let rp_id = rp.rp_id;
if let Some(exclude_list) = exclude_list { if let Some(exclude_list) = exclude_list {
for cred_desc in exclude_list { for cred_desc in exclude_list {
...@@ -389,7 +465,7 @@ where ...@@ -389,7 +465,7 @@ where
if !check_pin_auth(&self.pin_uv_auth_token, &client_data_hash, &pin_auth) { if !check_pin_auth(&self.pin_uv_auth_token, &client_data_hash, &pin_auth) {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
} }
UP_FLAG | UV_FLAG | AT_FLAG UP_FLAG | UV_FLAG | AT_FLAG | ed_flag
} }
None => { None => {
if self.persistent_store.pin_hash().is_some() { if self.persistent_store.pin_hash().is_some() {
...@@ -398,7 +474,7 @@ where ...@@ -398,7 +474,7 @@ where
if options.uv { if options.uv {
return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION);
} }
UP_FLAG | AT_FLAG UP_FLAG | AT_FLAG | ed_flag
} }
}; };
...@@ -421,6 +497,7 @@ where ...@@ -421,6 +497,7 @@ where
other_ui: user other_ui: user
.user_display_name .user_display_name
.map(|s| truncate_to_char_boundary(&s, 64).to_string()), .map(|s| truncate_to_char_boundary(&s, 64).to_string()),
cred_random,
}; };
self.persistent_store.store_credential(credential_source)?; self.persistent_store.store_credential(credential_source)?;
random_id random_id
...@@ -441,6 +518,14 @@ where ...@@ -441,6 +518,14 @@ where
None => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR), None => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR),
}; };
auth_data.extend(cose_key); auth_data.extend(cose_key);
if use_hmac_extension {
let extensions = cbor_map! {
"hmac-secret" => true,
};
if !cbor::write(extensions, &mut auth_data) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR);
}
}
let mut signature_data = auth_data.clone(); let mut signature_data = auth_data.clone();
signature_data.extend(client_data_hash); signature_data.extend(client_data_hash);
...@@ -481,10 +566,10 @@ where ...@@ -481,10 +566,10 @@ where
rp_id, rp_id,
client_data_hash, client_data_hash,
allow_list, allow_list,
extensions,
options, options,
pin_uv_auth_param, pin_uv_auth_param,
pin_uv_auth_protocol, pin_uv_auth_protocol,
..
} = get_assertion_params; } = get_assertion_params;
if let Some(auth_param) = &pin_uv_auth_param { if let Some(auth_param) = &pin_uv_auth_param {
...@@ -527,6 +612,15 @@ where ...@@ -527,6 +612,15 @@ where
} }
} }
let get_assertion_hmac_secret_input = match extensions {
Some(extensions) => extensions.get_assertion_hmac_secret().transpose()?,
None => None,
};
if get_assertion_hmac_secret_input.is_some() && !options.up {
// The extension is actually supported, but we need user presence.
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
}
// The user verification bit depends on the existance of PIN auth, whereas // The user verification bit depends on the existance of PIN auth, whereas
// user presence is requested as an option. // user presence is requested as an option.
let mut flags = match pin_uv_auth_param { let mut flags = match pin_uv_auth_param {
...@@ -551,6 +645,9 @@ where ...@@ -551,6 +645,9 @@ where
if options.up { if options.up {
flags |= UP_FLAG; flags |= UP_FLAG;
} }
if get_assertion_hmac_secret_input.is_some() {
flags |= ED_FLAG;
}
let rp_id_hash = Sha256::hash(rp_id.as_bytes()); let rp_id_hash = Sha256::hash(rp_id.as_bytes());
let mut decrypted_credential = None; let mut decrypted_credential = None;
...@@ -590,7 +687,36 @@ where ...@@ -590,7 +687,36 @@ where
self.increment_global_signature_counter(); self.increment_global_signature_counter();
let auth_data = self.generate_auth_data(&rp_id_hash, flags); let mut auth_data = self.generate_auth_data(&rp_id_hash, flags);
// Process extensions.
if let Some(get_assertion_hmac_secret_input) = get_assertion_hmac_secret_input {
let GetAssertionHmacSecretInput {
key_agreement,
salt_enc,
salt_auth,
} = get_assertion_hmac_secret_input;
let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?;
let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk);
// HMAC-secret does the same 16 byte truncated check.
if !check_pin_auth(&shared_secret, &salt_enc, &salt_auth) {
// Again, hard to tell what the correct error code here is.
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
}
let encrypted_output = match &credential.cred_random {
Some(cr) => encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cr)?,
// This is the case if the credential was not created with HMAC-secret.
None => return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION),
};
let extensions = cbor_map! {
"hmac-secret" => encrypted_output,
};
if !cbor::write(extensions, &mut auth_data) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR);
}
}
let mut signature_data = auth_data.clone(); let mut signature_data = auth_data.clone();
signature_data.extend(client_data_hash); signature_data.extend(client_data_hash);
let signature = credential let signature = credential
...@@ -639,7 +765,7 @@ where ...@@ -639,7 +765,7 @@ where
String::from(U2F_VERSION_STRING), String::from(U2F_VERSION_STRING),
String::from(FIDO2_VERSION_STRING), String::from(FIDO2_VERSION_STRING),
], ],
extensions: Some(vec![]), extensions: Some(vec![String::from("hmac-secret")]),
aaguid: *AAGUID, aaguid: *AAGUID,
options: Some(options_map), options: Some(options_map),
max_msg_size: Some(1024), max_msg_size: Some(1024),
...@@ -948,7 +1074,7 @@ where ...@@ -948,7 +1074,7 @@ where
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::data_formats::{ use super::data_formats::{
GetAssertionOptions, MakeCredentialOptions, PublicKeyCredentialRpEntity, Extensions, GetAssertionOptions, MakeCredentialOptions, PublicKeyCredentialRpEntity,
PublicKeyCredentialUserEntity, PublicKeyCredentialUserEntity,
}; };
use super::*; use super::*;
...@@ -970,13 +1096,15 @@ mod test { ...@@ -970,13 +1096,15 @@ mod test {
let mut expected_response = vec![0x00, 0xA6, 0x01]; let mut expected_response = vec![0x00, 0xA6, 0x01];
// The difference here is a longer array of supported versions. // The difference here is a longer array of supported versions.
#[cfg(not(feature = "with_ctap1"))] #[cfg(not(feature = "with_ctap1"))]
expected_response.extend(&[ expected_response.extend(&[0x81, 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30]);
0x81, 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, 0x02, 0x80, 0x03, 0x50,
]);
#[cfg(feature = "with_ctap1")] #[cfg(feature = "with_ctap1")]
expected_response.extend(&[ expected_response.extend(&[
0x82, 0x66, 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32, 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x82, 0x66, 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32, 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F,
0x32, 0x5F, 0x30, 0x02, 0x80, 0x03, 0x50, 0x32, 0x5F, 0x30,
]);
expected_response.extend(&[
0x02, 0x81, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
0x03, 0x50,
]); ]);
expected_response.extend(AAGUID); expected_response.extend(AAGUID);
expected_response.extend(&[ expected_response.extend(&[
...@@ -1130,6 +1258,7 @@ mod test { ...@@ -1130,6 +1258,7 @@ mod test {
rp_id: String::from("example.com"), rp_id: String::from("example.com"),
user_handle: vec![], user_handle: vec![],
other_ui: None, other_ui: None,
cred_random: None,
}; };
assert!(ctap_state assert!(ctap_state
.persistent_store .persistent_store
...@@ -1153,6 +1282,54 @@ mod test { ...@@ -1153,6 +1282,54 @@ mod test {
); );
} }
#[test]
fn test_process_make_credential_hmac_secret() {
let mut rng = ThreadRng256 {};
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let mut extension_map = BTreeMap::new();
extension_map.insert("hmac-secret".to_string(), cbor_bool!(true));
let extensions = Some(Extensions::new(extension_map));
let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = extensions;
let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
match make_credential_response.unwrap() {
ResponseData::AuthenticatorMakeCredential(make_credential_response) => {
let AuthenticatorMakeCredentialResponse {
fmt,
auth_data,
att_stmt,
} = make_credential_response;
// The expected response is split to only assert the non-random parts.
assert_eq!(fmt, "packed");
let mut expected_auth_data = vec![
0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80,
0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2,
0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, 0x00,
];
expected_auth_data.extend(AAGUID);
expected_auth_data.extend(&[0x00, 0x20]);
assert_eq!(
auth_data[0..expected_auth_data.len()],
expected_auth_data[..]
);
let expected_extension_cbor = vec![
0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
0xF5,
];
assert_eq!(
auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()],
expected_extension_cbor[..]
);
assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64);
}
_ => panic!("Invalid response type"),
}
}
#[test] #[test]
fn test_process_make_credential_cancelled() { fn test_process_make_credential_cancelled() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
...@@ -1216,6 +1393,53 @@ mod test { ...@@ -1216,6 +1393,53 @@ mod test {
} }
} }
#[test]
fn test_residential_process_get_assertion_hmac_secret() {
let mut rng = ThreadRng256 {};
let sk = crypto::ecdh::SecKey::gensk(&mut rng);
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let mut extension_map = BTreeMap::new();
extension_map.insert("hmac-secret".to_string(), cbor_bool!(true));
let make_extensions = Some(Extensions::new(extension_map));
let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = make_extensions;
assert!(ctap_state
.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID)
.is_ok());
let pk = sk.genpk();
let hmac_secret_parameters = cbor_map! {
1 => cbor::Value::Map(CoseKey::from(pk).0),
2 => vec![0; 32],
3 => vec![0; 16],
};
let mut extension_map = BTreeMap::new();
extension_map.insert("hmac-secret".to_string(), hmac_secret_parameters);
let get_extensions = Some(Extensions::new(extension_map));
let get_assertion_params = AuthenticatorGetAssertionParameters {
rp_id: String::from("example.com"),
client_data_hash: vec![0xCD],
allow_list: None,
extensions: get_extensions,
options: GetAssertionOptions {
up: false,
uv: false,
},
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let get_assertion_response =
ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID);
assert_eq!(
get_assertion_response,
Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION)
);
}
#[test] #[test]
fn test_process_reset() { fn test_process_reset() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
...@@ -1231,6 +1455,7 @@ mod test { ...@@ -1231,6 +1455,7 @@ mod test {
rp_id: String::from("example.com"), rp_id: String::from("example.com"),
user_handle: vec![], user_handle: vec![],
other_ui: None, other_ui: None,
cred_random: None,
}; };
assert!(ctap_state assert!(ctap_state
.persistent_store .persistent_store
...@@ -1294,4 +1519,32 @@ mod test { ...@@ -1294,4 +1519,32 @@ mod test {
.is_none()); .is_none());
} }
} }
#[test]
fn test_encrypt_hmac_secret_output() {
let shared_secret = [0x55; 32];
let salt_enc = [0x5E; 32];
let cred_random = [0xC9; 32];
let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random);
assert_eq!(output.unwrap().len(), 32);
let salt_enc = [0x5E; 48];
let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random);
assert_eq!(
output,
Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION)
);
let salt_enc = [0x5E; 64];
let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random);
assert_eq!(output.unwrap().len(), 64);
let salt_enc = [0x5E; 32];
let cred_random = [0xC9; 33];
let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random);
assert_eq!(
output,
Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION)
);
}
} }
...@@ -445,6 +445,7 @@ mod test { ...@@ -445,6 +445,7 @@ mod test {
rp_id: String::from(rp_id), rp_id: String::from(rp_id),
user_handle, user_handle,
other_ui: None, other_ui: None,
cred_random: None,
} }
} }
...@@ -613,6 +614,7 @@ mod test { ...@@ -613,6 +614,7 @@ mod test {
rp_id: String::from("example.com"), rp_id: String::from("example.com"),
user_handle: vec![0x00], user_handle: vec![0x00],
other_ui: None, other_ui: None,
cred_random: None,
}; };
assert_eq!(found_credential, Some(expected_credential)); assert_eq!(found_credential, Some(expected_credential));
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment