adding HMAC-secret support
This commit is contained in:
@@ -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
|
||||||
|
.iter()
|
||||||
|
.map(|(key, value)| (cbor_text!(key), value.clone()))
|
||||||
|
.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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
rp_id,
|
rp_id,
|
||||||
user_handle,
|
user_handle,
|
||||||
other_ui,
|
other_ui,
|
||||||
|
cred_random,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 {
|
|||||||
..credential
|
..credential
|
||||||
};
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())),
|
||||||
|
Ok(credential.clone())
|
||||||
|
);
|
||||||
|
|
||||||
|
let credential = PublicKeyCredentialSource {
|
||||||
|
cred_random: Some([0x00; 32].to_vec()),
|
||||||
|
..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)
|
||||||
|
|||||||
282
src/ctap/mod.rs
282
src/ctap/mod.rs
@@ -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};
|
||||||
@@ -84,6 +84,7 @@ pub const ENCRYPTED_CREDENTIAL_ID_SIZE: usize = 112;
|
|||||||
const UP_FLAG: u8 = 0x01;
|
const UP_FLAG: u8 = 0x01;
|
||||||
const UV_FLAG: u8 = 0x04;
|
const UV_FLAG: u8 = 0x04;
|
||||||
const AT_FLAG: u8 = 0x40;
|
const AT_FLAG: u8 = 0x40;
|
||||||
|
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 +106,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: Vec<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 +319,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 +383,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 +421,22 @@ where
|
|||||||
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM);
|
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let use_hmac_extension = if let Some(extensions) = extensions {
|
||||||
|
extensions.has_make_credential_hmac_secret()?
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
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 +464,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 +473,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 +496,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 +517,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 +565,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 +611,16 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let get_assertion_hmac_secret_input = if let Some(extensions) = extensions {
|
||||||
|
extensions.get_assertion_hmac_secret().transpose()?
|
||||||
|
} else {
|
||||||
|
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
|
|||||||
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,37 @@ 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 = if let Some(cred_random) = &credential.cred_random {
|
||||||
|
encrypt_hmac_secret_output(&shared_secret, salt_enc, cred_random)?
|
||||||
|
} else {
|
||||||
|
// This happens because the credential was not created with HMAC-secret.
|
||||||
|
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 +766,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 +1075,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 +1097,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 +1259,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 +1283,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 +1394,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 +1456,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 +1520,32 @@ mod test {
|
|||||||
.is_none());
|
.is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encrypt_hmac_secret_output() {
|
||||||
|
let shared_secret = [0x55; 32];
|
||||||
|
let salt_enc = vec![0x5E; 32];
|
||||||
|
let cred_random = vec![0xC9; 32];
|
||||||
|
let output = encrypt_hmac_secret_output(&shared_secret, salt_enc, &cred_random);
|
||||||
|
assert_eq!(output.unwrap().len(), 32);
|
||||||
|
|
||||||
|
let salt_enc = vec![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 = vec![0x5E; 64];
|
||||||
|
let output = encrypt_hmac_secret_output(&shared_secret, salt_enc, &cred_random);
|
||||||
|
assert_eq!(output.unwrap().len(), 64);
|
||||||
|
|
||||||
|
let salt_enc = vec![0x5E; 32];
|
||||||
|
let cred_random = vec![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 {
|
|||||||
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 {
|
|||||||
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));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user