diff --git a/README.md b/README.md index 28093fc..6e68ba1 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,10 @@ a few things you can personalize: 4. Depending on your available flash storage, choose an appropriate maximum number of supported residential keys and number of pages in `ctap/storage.rs`. +5. Change the default level for the credProtect extension in `ctap/mod.rs`. + When changing the default, resident credentials become undiscoverable without + user verification. This helps privacy, but can make usage less comfortable + for credentials that need less protection. ### 3D printed enclosure diff --git a/reproducible/reference_binaries_macos-10.15.sha256sum b/reproducible/reference_binaries_macos-10.15.sha256sum index 3b0dabe..0228740 100644 --- a/reproducible/reference_binaries_macos-10.15.sha256sum +++ b/reproducible/reference_binaries_macos-10.15.sha256sum @@ -1,9 +1,9 @@ 1003863864e06553e730eec6df4bf8d30c99f697ef9380efdc35eba679b4db78 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin -7ffc52ea6bfd1c3fde3398da4e894b5659770a74b466e052b4c3999436f9d78e target/nrf52840dk_merged.hex +022268c93fa8bbd9e54e082982b87c10a0e7c0486704de8219d1bb374304636a target/nrf52840dk_merged.hex 88f00a5e1dae6ab3f7571c254ac75f5f3e29ebea7f3ca46c16cfdc3708e804fc third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin -a0cd9144582b616a51d4f097713cbd697d418c19d031906f58fc630d7286ed85 target/nrf52840_dongle_merged.hex +8d68ecc700527789b8edf318f0872ca8fc3b72fa73236f4e06bec89a3682fcf8 target/nrf52840_dongle_merged.hex 1bc69b48a2c48da55db8b322902e1fe3f2e095c0dd8517db28837d86e0addc85 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin -5879d90971a7429e8890ce4a5db694499f391ffd7c6707c6820538ee8126ff5f target/nrf52840_dongle_dfu_merged.hex +af5465e4209914aaf74ee878d03e883a717827119e47b9295aa279ee21f0c5f4 target/nrf52840_dongle_dfu_merged.hex f38ee31d3a09e7e11848e78b5318f95517b6dcd076afcb37e6e3d3e5e9995cc7 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin -a4e7451174ee75a27acfb9bdd3c977f5cf3e756e40219b706c97eab3a21c7ac0 target/nrf52840_mdk_dfu_merged.hex -f364a923a4c56b5bbba8b590c8c296b29f4448f3117cedf433d4752867fac6ef target/tab/ctap2.tab +23603386a615e4e8cb2173c5ce4762110e6cbb979efdbb6e8bef9bc1e3988de4 target/nrf52840_mdk_dfu_merged.hex +c2cbcc28b835934be4c3d3e3c5bdaba642a5811d760c1d2cb73d26b6474e4219 target/tab/ctap2.tab diff --git a/reproducible/reference_binaries_ubuntu-18.04.sha256sum b/reproducible/reference_binaries_ubuntu-18.04.sha256sum index c597106..666eecd 100644 --- a/reproducible/reference_binaries_ubuntu-18.04.sha256sum +++ b/reproducible/reference_binaries_ubuntu-18.04.sha256sum @@ -1,9 +1,9 @@ c182bb4902fff51b2f56810fc2a27df3646cd66ba21359162354d53445623ab8 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin -9624888f52510e1e7a13681a959ecb9dd0e325b3856422b48d28abadc6546211 target/nrf52840dk_merged.hex +d8b62ece387a77cc21f2c10a5f5d65d0d57bf4739b47fd86d2c9ecdd90fbfd7e target/nrf52840dk_merged.hex 0a9929ba8fa57e8a502a49fc7c53177397202e1b11f4c7c3cb6ed68b2b99dd46 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin -31b41cc1010c621765a4385ecd678950ddb2e1eaa11e0efaa9df818a1abfd022 target/nrf52840_dongle_merged.hex +380de1a910b4d9eeb0c814b11b074b2e66334968cc99a4bd34d52a1fce3c5a79 target/nrf52840_dongle_merged.hex cca9086c9149c607589b23ffa599a5e4c26db7c20bd3700b79528bd3a5df991d third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin -0a9c92d56b02b42c7d783606f7711c474fc73518a32b9c7e244c078011a67e6d target/nrf52840_dongle_dfu_merged.hex +4edd988b3e37991f1e58fc520e41f7666f8ae3e8d3993e1bb2fb71657a71fa50 target/nrf52840_dongle_dfu_merged.hex 8857488ba6a69e366f0da229bbfc012a2ad291d3a88d9494247d600c10bb19b7 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin -a5fb5ebcf475f88be0273a4679975bcfee72014102a6191370a80120ca287f11 target/nrf52840_mdk_dfu_merged.hex -7940a87663cf40941ea8c50ad1d99abf2ccbcacfcd157c1b0449dd3ed78e180e target/tab/ctap2.tab +a51aba1cd12e55aa33fd9017af406583ebf14e1c690295b15cf147713dfe2561 target/nrf52840_mdk_dfu_merged.hex +40b413a8b645b4b47fae62a4311acb12cb0c57faff2757e45c18d9e5d441e52d target/tab/ctap2.tab diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 19132bc..d6dd0fa 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -13,10 +13,10 @@ // limitations under the License. use super::data_formats::{ - ok_or_missing, read_array, read_byte_string, read_map, read_text_string, read_unsigned, - ClientPinSubCommand, CoseKey, Extensions, GetAssertionOptions, MakeCredentialOptions, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, + extract_array, extract_byte_string, extract_map, extract_text_string, extract_unsigned, + ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, GetAssertionOptions, + MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialDescriptor, + PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::status_code::Ctap2StatusCode; use alloc::string::String; @@ -113,7 +113,7 @@ pub struct AuthenticatorMakeCredentialParameters { pub user: PublicKeyCredentialUserEntity, pub pub_key_cred_params: Vec, pub exclude_list: Option>, - pub extensions: Option, + pub extensions: Option, // Even though options are optional, we can use the default if not present. pub options: MakeCredentialOptions, pub pin_uv_auth_param: Option>, @@ -124,30 +124,32 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - let param_map = read_map(&cbor_value)?; + let mut param_map = extract_map(cbor_value)?; - let client_data_hash = read_byte_string(ok_or_missing(param_map.get(&cbor_unsigned!(1)))?)?; + let client_data_hash = + extract_byte_string(ok_or_missing(param_map.remove(&cbor_unsigned!(1)))?)?; let rp = PublicKeyCredentialRpEntity::try_from(ok_or_missing( - param_map.get(&cbor_unsigned!(2)), + param_map.remove(&cbor_unsigned!(2)), )?)?; let user = PublicKeyCredentialUserEntity::try_from(ok_or_missing( - param_map.get(&cbor_unsigned!(3)), + param_map.remove(&cbor_unsigned!(3)), )?)?; - let cred_param_vec = read_array(ok_or_missing(param_map.get(&cbor_unsigned!(4)))?)?; + let cred_param_vec = extract_array(ok_or_missing(param_map.remove(&cbor_unsigned!(4)))?)?; let pub_key_cred_params = cred_param_vec - .iter() + .into_iter() .map(PublicKeyCredentialParameter::try_from) .collect::, Ctap2StatusCode>>()?; - let exclude_list = match param_map.get(&cbor_unsigned!(5)) { + let exclude_list = match param_map.remove(&cbor_unsigned!(5)) { Some(entry) => { - let exclude_list_vec = read_array(entry)?; + let exclude_list_vec = extract_array(entry)?; + let list_len = MAX_CREDENTIAL_COUNT_IN_LIST.unwrap_or(exclude_list_vec.len()); let exclude_list = exclude_list_vec - .iter() - .take(MAX_CREDENTIAL_COUNT_IN_LIST.unwrap_or(exclude_list_vec.len())) + .into_iter() + .take(list_len) .map(PublicKeyCredentialDescriptor::try_from) .collect::, Ctap2StatusCode>>()?; Some(exclude_list) @@ -156,11 +158,11 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { }; let extensions = param_map - .get(&cbor_unsigned!(6)) - .map(Extensions::try_from) + .remove(&cbor_unsigned!(6)) + .map(MakeCredentialExtensions::try_from) .transpose()?; - let options = match param_map.get(&cbor_unsigned!(7)) { + let options = match param_map.remove(&cbor_unsigned!(7)) { Some(entry) => MakeCredentialOptions::try_from(entry)?, None => MakeCredentialOptions { rk: false, @@ -169,13 +171,13 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { }; let pin_uv_auth_param = param_map - .get(&cbor_unsigned!(8)) - .map(read_byte_string) + .remove(&cbor_unsigned!(8)) + .map(extract_byte_string) .transpose()?; let pin_uv_auth_protocol = param_map - .get(&cbor_unsigned!(9)) - .map(read_unsigned) + .remove(&cbor_unsigned!(9)) + .map(extract_unsigned) .transpose()?; Ok(AuthenticatorMakeCredentialParameters { @@ -197,7 +199,7 @@ pub struct AuthenticatorGetAssertionParameters { pub rp_id: String, pub client_data_hash: Vec, pub allow_list: Option>, - pub extensions: Option, + pub extensions: Option, // Even though options are optional, we can use the default if not present. pub options: GetAssertionOptions, pub pin_uv_auth_param: Option>, @@ -208,18 +210,20 @@ impl TryFrom for AuthenticatorGetAssertionParameters { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - let param_map = read_map(&cbor_value)?; + let mut param_map = extract_map(cbor_value)?; - let rp_id = read_text_string(ok_or_missing(param_map.get(&cbor_unsigned!(1)))?)?; + let rp_id = extract_text_string(ok_or_missing(param_map.remove(&cbor_unsigned!(1)))?)?; - let client_data_hash = read_byte_string(ok_or_missing(param_map.get(&cbor_unsigned!(2)))?)?; + let client_data_hash = + extract_byte_string(ok_or_missing(param_map.remove(&cbor_unsigned!(2)))?)?; - let allow_list = match param_map.get(&cbor_unsigned!(3)) { + let allow_list = match param_map.remove(&cbor_unsigned!(3)) { Some(entry) => { - let allow_list_vec = read_array(entry)?; + let allow_list_vec = extract_array(entry)?; + let list_len = MAX_CREDENTIAL_COUNT_IN_LIST.unwrap_or(allow_list_vec.len()); let allow_list = allow_list_vec - .iter() - .take(MAX_CREDENTIAL_COUNT_IN_LIST.unwrap_or(allow_list_vec.len())) + .into_iter() + .take(list_len) .map(PublicKeyCredentialDescriptor::try_from) .collect::, Ctap2StatusCode>>()?; Some(allow_list) @@ -228,11 +232,11 @@ impl TryFrom for AuthenticatorGetAssertionParameters { }; let extensions = param_map - .get(&cbor_unsigned!(4)) - .map(Extensions::try_from) + .remove(&cbor_unsigned!(4)) + .map(GetAssertionExtensions::try_from) .transpose()?; - let options = match param_map.get(&cbor_unsigned!(5)) { + let options = match param_map.remove(&cbor_unsigned!(5)) { Some(entry) => GetAssertionOptions::try_from(entry)?, None => GetAssertionOptions { up: true, @@ -241,13 +245,13 @@ impl TryFrom for AuthenticatorGetAssertionParameters { }; let pin_uv_auth_param = param_map - .get(&cbor_unsigned!(6)) - .map(read_byte_string) + .remove(&cbor_unsigned!(6)) + .map(extract_byte_string) .transpose()?; let pin_uv_auth_protocol = param_map - .get(&cbor_unsigned!(7)) - .map(read_unsigned) + .remove(&cbor_unsigned!(7)) + .map(extract_unsigned) .transpose()?; Ok(AuthenticatorGetAssertionParameters { @@ -276,32 +280,32 @@ impl TryFrom for AuthenticatorClientPinParameters { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - let param_map = read_map(&cbor_value)?; + let mut param_map = extract_map(cbor_value)?; - let pin_protocol = read_unsigned(ok_or_missing(param_map.get(&cbor_unsigned!(1)))?)?; + let pin_protocol = extract_unsigned(ok_or_missing(param_map.remove(&cbor_unsigned!(1)))?)?; let sub_command = - ClientPinSubCommand::try_from(ok_or_missing(param_map.get(&cbor_unsigned!(2)))?)?; + ClientPinSubCommand::try_from(ok_or_missing(param_map.remove(&cbor_unsigned!(2)))?)?; let key_agreement = param_map - .get(&cbor_unsigned!(3)) - .map(read_map) + .remove(&cbor_unsigned!(3)) + .map(extract_map) .transpose()? - .map(|x| CoseKey(x.clone())); + .map(|x| CoseKey(x)); let pin_auth = param_map - .get(&cbor_unsigned!(4)) - .map(read_byte_string) + .remove(&cbor_unsigned!(4)) + .map(extract_byte_string) .transpose()?; let new_pin_enc = param_map - .get(&cbor_unsigned!(5)) - .map(read_byte_string) + .remove(&cbor_unsigned!(5)) + .map(extract_byte_string) .transpose()?; let pin_hash_enc = param_map - .get(&cbor_unsigned!(6)) - .map(read_byte_string) + .remove(&cbor_unsigned!(6)) + .map(extract_byte_string) .transpose()?; Ok(AuthenticatorClientPinParameters { diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index d55121e..33dec12 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -14,7 +14,7 @@ use super::status_code::Ctap2StatusCode; use alloc::collections::BTreeMap; -use alloc::string::{String, ToString}; +use alloc::string::String; use alloc::vec::Vec; use core::convert::TryFrom; use crypto::{ecdh, ecdsa}; @@ -27,19 +27,19 @@ pub struct PublicKeyCredentialRpEntity { pub rp_icon: Option, } -impl TryFrom<&cbor::Value> for PublicKeyCredentialRpEntity { +impl TryFrom for PublicKeyCredentialRpEntity { type Error = Ctap2StatusCode; - fn try_from(cbor_value: &cbor::Value) -> Result { - let rp_map = read_map(cbor_value)?; - let rp_id = read_text_string(ok_or_missing(rp_map.get(&cbor_text!("id")))?)?; + fn try_from(cbor_value: cbor::Value) -> Result { + let mut rp_map = extract_map(cbor_value)?; + let rp_id = extract_text_string(ok_or_missing(rp_map.remove(&cbor_text!("id")))?)?; let rp_name = rp_map - .get(&cbor_text!("name")) - .map(read_text_string) + .remove(&cbor_text!("name")) + .map(extract_text_string) .transpose()?; let rp_icon = rp_map - .get(&cbor_text!("icon")) - .map(read_text_string) + .remove(&cbor_text!("icon")) + .map(extract_text_string) .transpose()?; Ok(Self { rp_id, @@ -58,23 +58,23 @@ pub struct PublicKeyCredentialUserEntity { pub user_icon: Option, } -impl TryFrom<&cbor::Value> for PublicKeyCredentialUserEntity { +impl TryFrom for PublicKeyCredentialUserEntity { type Error = Ctap2StatusCode; - fn try_from(cbor_value: &cbor::Value) -> Result { - let user_map = read_map(cbor_value)?; - let user_id = read_byte_string(ok_or_missing(user_map.get(&cbor_text!("id")))?)?; + fn try_from(cbor_value: cbor::Value) -> Result { + let mut user_map = extract_map(cbor_value)?; + let user_id = extract_byte_string(ok_or_missing(user_map.remove(&cbor_text!("id")))?)?; let user_name = user_map - .get(&cbor_text!("name")) - .map(read_text_string) + .remove(&cbor_text!("name")) + .map(extract_text_string) .transpose()?; let user_display_name = user_map - .get(&cbor_text!("displayName")) - .map(read_text_string) + .remove(&cbor_text!("displayName")) + .map(extract_text_string) .transpose()?; let user_icon = user_map - .get(&cbor_text!("icon")) - .map(read_text_string) + .remove(&cbor_text!("icon")) + .map(extract_text_string) .transpose()?; Ok(Self { user_id, @@ -117,11 +117,11 @@ impl From for cbor::Value { } } -impl TryFrom<&cbor::Value> for PublicKeyCredentialType { +impl TryFrom for PublicKeyCredentialType { type Error = Ctap2StatusCode; - fn try_from(cbor_value: &cbor::Value) -> Result { - let cred_type_string = read_text_string(cbor_value)?; + fn try_from(cbor_value: cbor::Value) -> Result { + let cred_type_string = extract_text_string(cbor_value)?; match &cred_type_string[..] { "public-key" => Ok(PublicKeyCredentialType::PublicKey), _ => Ok(PublicKeyCredentialType::Unknown), @@ -137,16 +137,17 @@ pub struct PublicKeyCredentialParameter { pub alg: SignatureAlgorithm, } -impl TryFrom<&cbor::Value> for PublicKeyCredentialParameter { +impl TryFrom for PublicKeyCredentialParameter { type Error = Ctap2StatusCode; - fn try_from(cbor_value: &cbor::Value) -> Result { - let cred_param_map = read_map(cbor_value)?; + fn try_from(cbor_value: cbor::Value) -> Result { + let mut cred_param_map = extract_map(cbor_value)?; let cred_type = PublicKeyCredentialType::try_from(ok_or_missing( - cred_param_map.get(&cbor_text!("type")), + cred_param_map.remove(&cbor_text!("type")), + )?)?; + let alg = SignatureAlgorithm::try_from(ok_or_missing( + cred_param_map.remove(&cbor_text!("alg")), )?)?; - let alg = - SignatureAlgorithm::try_from(ok_or_missing(cred_param_map.get(&cbor_text!("alg")))?)?; Ok(Self { cred_type, alg }) } } @@ -155,13 +156,13 @@ impl From for cbor::Value { fn from(cred_param: PublicKeyCredentialParameter) -> Self { cbor_map_options! { "type" => cred_param.cred_type, - "alg" => cred_param.alg as i64, + "alg" => cred_param.alg, } } } // https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] pub enum AuthenticatorTransport { Usb, Nfc, @@ -181,11 +182,11 @@ impl From for cbor::Value { } } -impl TryFrom<&cbor::Value> for AuthenticatorTransport { +impl TryFrom for AuthenticatorTransport { type Error = Ctap2StatusCode; - fn try_from(cbor_value: &cbor::Value) -> Result { - let transport_string = read_text_string(cbor_value)?; + fn try_from(cbor_value: cbor::Value) -> Result { + let transport_string = extract_text_string(cbor_value)?; match &transport_string[..] { "usb" => Ok(AuthenticatorTransport::Usb), "nfc" => Ok(AuthenticatorTransport::Nfc), @@ -197,30 +198,29 @@ impl TryFrom<&cbor::Value> for AuthenticatorTransport { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] pub struct PublicKeyCredentialDescriptor { pub key_type: PublicKeyCredentialType, pub key_id: Vec, pub transports: Option>, } -impl TryFrom<&cbor::Value> for PublicKeyCredentialDescriptor { +impl TryFrom for PublicKeyCredentialDescriptor { type Error = Ctap2StatusCode; - fn try_from(cbor_value: &cbor::Value) -> Result { - let cred_desc_map = read_map(cbor_value)?; + fn try_from(cbor_value: cbor::Value) -> Result { + let mut cred_desc_map = extract_map(cbor_value)?; let key_type = PublicKeyCredentialType::try_from(ok_or_missing( - cred_desc_map.get(&cbor_text!("type")), + cred_desc_map.remove(&cbor_text!("type")), )?)?; - let key_id = read_byte_string(ok_or_missing(cred_desc_map.get(&cbor_text!("id")))?)?; - let transports = match cred_desc_map.get(&cbor_text!("transports")) { + let key_id = extract_byte_string(ok_or_missing(cred_desc_map.remove(&cbor_text!("id")))?)?; + let transports = match cred_desc_map.remove(&cbor_text!("transports")) { Some(exclude_entry) => { - let transport_vec = read_array(exclude_entry)?; + let transport_vec = extract_array(exclude_entry)?; let transports = transport_vec - .iter() + .into_iter() .map(AuthenticatorTransport::try_from) - .collect::, Ctap2StatusCode>>( - )?; + .collect::, Ctap2StatusCode>>()?; Some(transports) } None => None, @@ -243,97 +243,72 @@ impl From for cbor::Value { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] -pub struct Extensions(BTreeMap); +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +pub struct MakeCredentialExtensions { + pub hmac_secret: bool, + pub cred_protect: Option, +} -impl TryFrom<&cbor::Value> for Extensions { +impl TryFrom for MakeCredentialExtensions { type Error = Ctap2StatusCode; - fn try_from(cbor_value: &cbor::Value) -> Result { - let mut extensions = BTreeMap::new(); - for (extension_key, extension_value) in read_map(cbor_value)? { - if let cbor::KeyType::TextString(extension_key_string) = extension_key { - extensions.insert(extension_key_string.to_string(), extension_value.clone()); - } else { - return Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE); - } - } - Ok(Extensions(extensions)) + fn try_from(cbor_value: cbor::Value) -> Result { + let mut extensions_map = extract_map(cbor_value)?; + let hmac_secret = extensions_map + .remove(&cbor_text!("hmac-secret")) + .map_or(Ok(false), extract_bool)?; + let cred_protect = extensions_map + .remove(&cbor_text!("credProtect")) + .map(CredentialProtectionPolicy::try_from) + .transpose()?; + Ok(Self { + hmac_secret, + cred_protect, + }) } } -impl From for cbor::Value { - fn from(extensions: Extensions) -> Self { - cbor_map_btree!(extensions - .0 - .into_iter() - .map(|(key, value)| (cbor_text!(key), value)) - .collect()) - } +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +pub struct GetAssertionExtensions { + pub hmac_secret: Option, } -impl Extensions { - #[cfg(test)] - pub fn new(extension_map: BTreeMap) -> Self { - Extensions(extension_map) - } +impl TryFrom for GetAssertionExtensions { + type Error = Ctap2StatusCode; - pub fn has_make_credential_hmac_secret(&self) -> Result { - self.0 - .get("hmac-secret") - .map(read_bool) - .unwrap_or(Ok(false)) - } - - pub fn get_assertion_hmac_secret( - &self, - ) -> Option> { - self.0 - .get("hmac-secret") + fn try_from(cbor_value: cbor::Value) -> Result { + let mut extensions_map = extract_map(cbor_value)?; + let hmac_secret = extensions_map + .remove(&cbor_text!("hmac-secret")) .map(GetAssertionHmacSecretInput::try_from) + .transpose()?; + Ok(Self { hmac_secret }) } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] pub struct GetAssertionHmacSecretInput { pub key_agreement: CoseKey, pub salt_enc: Vec, pub salt_auth: Vec, } -impl TryFrom<&cbor::Value> for GetAssertionHmacSecretInput { +impl TryFrom for GetAssertionHmacSecretInput { type Error = Ctap2StatusCode; - fn try_from(cbor_value: &cbor::Value) -> Result { - 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)))?)?; + fn try_from(cbor_value: cbor::Value) -> Result { + let mut input_map = extract_map(cbor_value)?; + let cose_key = extract_map(ok_or_missing(input_map.remove(&cbor_unsigned!(1)))?)?; + let salt_enc = extract_byte_string(ok_or_missing(input_map.remove(&cbor_unsigned!(2)))?)?; + let salt_auth = extract_byte_string(ok_or_missing(input_map.remove(&cbor_unsigned!(3)))?)?; Ok(Self { - key_agreement: CoseKey(cose_key.clone()), + key_agreement: CoseKey(cose_key), salt_enc, salt_auth, }) } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] -pub struct GetAssertionHmacSecretOutput(Vec); - -impl From 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 { - Ok(GetAssertionHmacSecretOutput(read_byte_string(cbor_value)?)) - } -} - // Even though options are optional, we can use the default if not present. #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct MakeCredentialOptions { @@ -341,22 +316,22 @@ pub struct MakeCredentialOptions { pub uv: bool, } -impl TryFrom<&cbor::Value> for MakeCredentialOptions { +impl TryFrom for MakeCredentialOptions { type Error = Ctap2StatusCode; - fn try_from(cbor_value: &cbor::Value) -> Result { - let options_map = read_map(cbor_value)?; - let rk = match options_map.get(&cbor_text!("rk")) { - Some(options_entry) => read_bool(options_entry)?, + fn try_from(cbor_value: cbor::Value) -> Result { + let mut options_map = extract_map(cbor_value)?; + let rk = match options_map.remove(&cbor_text!("rk")) { + Some(options_entry) => extract_bool(options_entry)?, None => false, }; - if let Some(options_entry) = options_map.get(&cbor_text!("up")) { - if !read_bool(options_entry)? { + if let Some(options_entry) = options_map.remove(&cbor_text!("up")) { + if !extract_bool(options_entry)? { return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); } } - let uv = match options_map.get(&cbor_text!("uv")) { - Some(options_entry) => read_bool(options_entry)?, + let uv = match options_map.remove(&cbor_text!("uv")) { + Some(options_entry) => extract_bool(options_entry)?, None => false, }; Ok(Self { rk, uv }) @@ -369,22 +344,22 @@ pub struct GetAssertionOptions { pub uv: bool, } -impl TryFrom<&cbor::Value> for GetAssertionOptions { +impl TryFrom for GetAssertionOptions { type Error = Ctap2StatusCode; - fn try_from(cbor_value: &cbor::Value) -> Result { - let options_map = read_map(cbor_value)?; - if let Some(options_entry) = options_map.get(&cbor_text!("rk")) { + fn try_from(cbor_value: cbor::Value) -> Result { + let mut options_map = extract_map(cbor_value)?; + if let Some(options_entry) = options_map.remove(&cbor_text!("rk")) { // This is only for returning the correct status code. - read_bool(options_entry)?; + extract_bool(options_entry)?; return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); } - let up = match options_map.get(&cbor_text!("up")) { - Some(options_entry) => read_bool(options_entry)?, + let up = match options_map.remove(&cbor_text!("up")) { + Some(options_entry) => extract_bool(options_entry)?, None => true, }; - let uv = match options_map.get(&cbor_text!("uv")) { - Some(options_entry) => read_bool(options_entry)?, + let uv = match options_map.remove(&cbor_text!("uv")) { + Some(options_entry) => extract_bool(options_entry)?, None => false, }; Ok(Self { up, uv }) @@ -421,17 +396,50 @@ pub enum SignatureAlgorithm { Unknown = 0, } -impl TryFrom<&cbor::Value> for SignatureAlgorithm { +impl From for cbor::Value { + fn from(alg: SignatureAlgorithm) -> Self { + (alg as i64).into() + } +} + +impl TryFrom for SignatureAlgorithm { type Error = Ctap2StatusCode; - fn try_from(cbor_value: &cbor::Value) -> Result { - match read_integer(cbor_value)? { + fn try_from(cbor_value: cbor::Value) -> Result { + match extract_integer(cbor_value)? { ecdsa::PubKey::ES256_ALGORITHM => Ok(SignatureAlgorithm::ES256), _ => Ok(SignatureAlgorithm::Unknown), } } } +#[derive(Clone, Copy, PartialEq, PartialOrd)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +pub enum CredentialProtectionPolicy { + UserVerificationOptional = 0x01, + UserVerificationOptionalWithCredentialIdList = 0x02, + UserVerificationRequired = 0x03, +} + +impl From for cbor::Value { + fn from(policy: CredentialProtectionPolicy) -> Self { + (policy as i64).into() + } +} + +impl TryFrom for CredentialProtectionPolicy { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + match extract_integer(cbor_value)? { + 0x01 => Ok(CredentialProtectionPolicy::UserVerificationOptional), + 0x02 => Ok(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList), + 0x03 => Ok(CredentialProtectionPolicy::UserVerificationRequired), + _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), + } + } +} + // https://www.w3.org/TR/webauthn/#public-key-credential-source // // Note that we only use the WebAuthn definition as an example. This data-structure is not specified @@ -448,6 +456,7 @@ pub struct PublicKeyCredentialSource { pub user_handle: Vec, // not optional, but nullable pub other_ui: Option, pub cred_random: Option>, + pub cred_protect_policy: Option, } // We serialize credentials for the persistent storage using CBOR maps. Each field of a credential @@ -459,6 +468,7 @@ enum PublicKeyCredentialSourceField { UserHandle = 3, OtherUi = 4, CredRandom = 5, + CredProtectPolicy = 6, // When a field is removed, its tag should be reserved and not used for new fields. We document // those reserved tags below. // Reserved tags: none. @@ -481,7 +491,8 @@ impl From for cbor::Value { RpId => Some(credential.rp_id), UserHandle => Some(credential.user_handle), OtherUi => credential.other_ui, - CredRandom => credential.cred_random + CredRandom => credential.cred_random, + CredProtectPolicy => credential.cred_protect_policy, } } } @@ -509,6 +520,10 @@ impl TryFrom for PublicKeyCredentialSource { .remove(&CredRandom.into()) .map(extract_byte_string) .transpose()?; + let cred_protect_policy = map + .remove(&CredProtectPolicy.into()) + .map(CredentialProtectionPolicy::try_from) + .transpose()?; // We don't return whether there were unknown fields in the CBOR value. This means that // deserialization is not injective. In particular deserialization is only an inverse of // serialization at a given version of OpenSK. This is not a problem because: @@ -527,14 +542,24 @@ impl TryFrom for PublicKeyCredentialSource { user_handle, other_ui, cred_random, + cred_protect_policy, }) } } +impl PublicKeyCredentialSource { + // Relying parties do not need to provide the credential ID in an allow_list if true. + pub fn is_discoverable(&self) -> bool { + self.cred_protect_policy.is_none() + || self.cred_protect_policy + == Some(CredentialProtectionPolicy::UserVerificationOptional) + } +} + // TODO(kaczmarczyck) we could decide to split this data type up // It depends on the algorithm though, I think. // So before creating a mess, this is my workaround. -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] pub struct CoseKey(pub BTreeMap); // This is the algorithm specifier that is supposed to be used in a COSE key @@ -574,23 +599,24 @@ impl TryFrom for ecdh::PubKey { type Error = Ctap2StatusCode; fn try_from(cose_key: CoseKey) -> Result { - let key_type = read_integer(ok_or_missing(cose_key.0.get(&cbor_int!(1)))?)?; + let mut cose_map = cose_key.0; + let key_type = extract_integer(ok_or_missing(cose_map.remove(&cbor_int!(1)))?)?; if key_type != EC2_KEY_TYPE { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } - let algorithm = read_integer(ok_or_missing(cose_key.0.get(&cbor_int!(3)))?)?; + let algorithm = extract_integer(ok_or_missing(cose_map.remove(&cbor_int!(3)))?)?; if algorithm != ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } - let curve = read_integer(ok_or_missing(cose_key.0.get(&cbor_int!(-1)))?)?; + let curve = extract_integer(ok_or_missing(cose_map.remove(&cbor_int!(-1)))?)?; if curve != P_256_CURVE { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } - let x_bytes = read_byte_string(ok_or_missing(cose_key.0.get(&cbor_int!(-2)))?)?; + let x_bytes = extract_byte_string(ok_or_missing(cose_map.remove(&cbor_int!(-2)))?)?; if x_bytes.len() != ecdh::NBYTES { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } - let y_bytes = read_byte_string(ok_or_missing(cose_key.0.get(&cbor_int!(-3)))?)?; + let y_bytes = extract_byte_string(ok_or_missing(cose_map.remove(&cbor_int!(-3)))?)?; if y_bytes.len() != ecdh::NBYTES { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } @@ -627,11 +653,11 @@ impl From for cbor::Value { } } -impl TryFrom<&cbor::Value> for ClientPinSubCommand { +impl TryFrom for ClientPinSubCommand { type Error = Ctap2StatusCode; - fn try_from(cbor_value: &cbor::Value) -> Result { - let subcommand_int = read_unsigned(cbor_value)?; + fn try_from(cbor_value: cbor::Value) -> Result { + let subcommand_int = extract_unsigned(cbor_value)?; match subcommand_int { 0x01 => Ok(ClientPinSubCommand::GetPinRetries), 0x02 => Ok(ClientPinSubCommand::GetKeyAgreement), @@ -646,74 +672,49 @@ impl TryFrom<&cbor::Value> for ClientPinSubCommand { } } -pub(super) fn read_unsigned(cbor_value: &cbor::Value) -> Result { +pub(super) fn extract_unsigned(cbor_value: cbor::Value) -> Result { match cbor_value { - cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => Ok(*unsigned), + cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => Ok(unsigned), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } -pub(super) fn read_integer(cbor_value: &cbor::Value) -> Result { +pub(super) fn extract_integer(cbor_value: cbor::Value) -> Result { match cbor_value { cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => { - if *unsigned <= core::i64::MAX as u64 { - Ok(*unsigned as i64) + if unsigned <= core::i64::MAX as u64 { + Ok(unsigned as i64) } else { Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE) } } - cbor::Value::KeyValue(cbor::KeyType::Negative(signed)) => Ok(*signed), + cbor::Value::KeyValue(cbor::KeyType::Negative(signed)) => Ok(signed), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } -pub fn read_byte_string(cbor_value: &cbor::Value) -> Result, Ctap2StatusCode> { - match cbor_value { - cbor::Value::KeyValue(cbor::KeyType::ByteString(byte_string)) => Ok(byte_string.to_vec()), - _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), - } -} - -fn extract_byte_string(cbor_value: cbor::Value) -> Result, Ctap2StatusCode> { +pub fn extract_byte_string(cbor_value: cbor::Value) -> Result, Ctap2StatusCode> { match cbor_value { cbor::Value::KeyValue(cbor::KeyType::ByteString(byte_string)) => Ok(byte_string), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } -pub(super) fn read_text_string(cbor_value: &cbor::Value) -> Result { - match cbor_value { - cbor::Value::KeyValue(cbor::KeyType::TextString(text_string)) => { - Ok(text_string.to_string()) - } - _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), - } -} - -fn extract_text_string(cbor_value: cbor::Value) -> Result { +pub(super) fn extract_text_string(cbor_value: cbor::Value) -> Result { match cbor_value { cbor::Value::KeyValue(cbor::KeyType::TextString(text_string)) => Ok(text_string), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } -pub(super) fn read_array(cbor_value: &cbor::Value) -> Result<&Vec, Ctap2StatusCode> { +pub(super) fn extract_array(cbor_value: cbor::Value) -> Result, Ctap2StatusCode> { match cbor_value { cbor::Value::Array(array) => Ok(array), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } -pub(super) fn read_map( - cbor_value: &cbor::Value, -) -> Result<&BTreeMap, Ctap2StatusCode> { - match cbor_value { - cbor::Value::Map(map) => Ok(map), - _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), - } -} - -fn extract_map( +pub(super) fn extract_map( cbor_value: cbor::Value, ) -> Result, Ctap2StatusCode> { match cbor_value { @@ -722,7 +723,7 @@ fn extract_map( } } -pub(super) fn read_bool(cbor_value: &cbor::Value) -> Result { +pub(super) fn extract_bool(cbor_value: cbor::Value) -> Result { match cbor_value { cbor::Value::Simple(cbor::SimpleValue::FalseValue) => Ok(false), cbor::Value::Simple(cbor::SimpleValue::TrueValue) => Ok(true), @@ -742,192 +743,192 @@ mod test { use crypto::rng256::{Rng256, ThreadRng256}; #[test] - fn test_read_unsigned() { - assert_eq!(read_unsigned(&cbor_int!(123)), Ok(123)); + fn test_extract_unsigned() { + assert_eq!(extract_unsigned(cbor_int!(123)), Ok(123)); assert_eq!( - read_unsigned(&cbor_bool!(true)), + extract_unsigned(cbor_bool!(true)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_unsigned(&cbor_text!("foo")), + extract_unsigned(cbor_text!("foo")), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_unsigned(&cbor_bytes_lit!(b"bar")), + extract_unsigned(cbor_bytes_lit!(b"bar")), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_unsigned(&cbor_array![]), + extract_unsigned(cbor_array![]), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_unsigned(&cbor_map! {}), + extract_unsigned(cbor_map! {}), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); } #[test] - fn test_read_unsigned_limits() { + fn test_extract_unsigned_limits() { assert_eq!( - read_unsigned(&cbor_unsigned!(std::u64::MAX)), + extract_unsigned(cbor_unsigned!(std::u64::MAX)), Ok(std::u64::MAX) ); assert_eq!( - read_unsigned(&cbor_unsigned!((std::i64::MAX as u64) + 1)), + extract_unsigned(cbor_unsigned!((std::i64::MAX as u64) + 1)), Ok((std::i64::MAX as u64) + 1) ); assert_eq!( - read_unsigned(&cbor_int!(std::i64::MAX)), + extract_unsigned(cbor_int!(std::i64::MAX)), Ok(std::i64::MAX as u64) ); - assert_eq!(read_unsigned(&cbor_int!(123)), Ok(123)); - assert_eq!(read_unsigned(&cbor_int!(1)), Ok(1)); - assert_eq!(read_unsigned(&cbor_int!(0)), Ok(0)); + assert_eq!(extract_unsigned(cbor_int!(123)), Ok(123)); + assert_eq!(extract_unsigned(cbor_int!(1)), Ok(1)); + assert_eq!(extract_unsigned(cbor_int!(0)), Ok(0)); assert_eq!( - read_unsigned(&cbor_int!(-1)), + extract_unsigned(cbor_int!(-1)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_unsigned(&cbor_int!(-123)), + extract_unsigned(cbor_int!(-123)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_unsigned(&cbor_int!(std::i64::MIN)), + extract_unsigned(cbor_int!(std::i64::MIN)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); } #[test] - fn test_read_integer() { - assert_eq!(read_integer(&cbor_int!(123)), Ok(123)); - assert_eq!(read_integer(&cbor_int!(-123)), Ok(-123)); + fn test_extract_integer() { + assert_eq!(extract_integer(cbor_int!(123)), Ok(123)); + assert_eq!(extract_integer(cbor_int!(-123)), Ok(-123)); assert_eq!( - read_integer(&cbor_bool!(true)), + extract_integer(cbor_bool!(true)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_integer(&cbor_text!("foo")), + extract_integer(cbor_text!("foo")), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_integer(&cbor_bytes_lit!(b"bar")), + extract_integer(cbor_bytes_lit!(b"bar")), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_integer(&cbor_array![]), + extract_integer(cbor_array![]), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_integer(&cbor_map! {}), + extract_integer(cbor_map! {}), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); } #[test] - fn test_read_integer_limits() { + fn test_extract_integer_limits() { assert_eq!( - read_integer(&cbor_unsigned!(std::u64::MAX)), + extract_integer(cbor_unsigned!(std::u64::MAX)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_integer(&cbor_unsigned!((std::i64::MAX as u64) + 1)), + extract_integer(cbor_unsigned!((std::i64::MAX as u64) + 1)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); - assert_eq!(read_integer(&cbor_int!(std::i64::MAX)), Ok(std::i64::MAX)); - assert_eq!(read_integer(&cbor_int!(123)), Ok(123)); - assert_eq!(read_integer(&cbor_int!(1)), Ok(1)); - assert_eq!(read_integer(&cbor_int!(0)), Ok(0)); - assert_eq!(read_integer(&cbor_int!(-1)), Ok(-1)); - assert_eq!(read_integer(&cbor_int!(-123)), Ok(-123)); - assert_eq!(read_integer(&cbor_int!(std::i64::MIN)), Ok(std::i64::MIN)); + assert_eq!(extract_integer(cbor_int!(std::i64::MAX)), Ok(std::i64::MAX)); + assert_eq!(extract_integer(cbor_int!(123)), Ok(123)); + assert_eq!(extract_integer(cbor_int!(1)), Ok(1)); + assert_eq!(extract_integer(cbor_int!(0)), Ok(0)); + assert_eq!(extract_integer(cbor_int!(-1)), Ok(-1)); + assert_eq!(extract_integer(cbor_int!(-123)), Ok(-123)); + assert_eq!(extract_integer(cbor_int!(std::i64::MIN)), Ok(std::i64::MIN)); } #[test] - fn test_read_byte_string() { + fn test_extract_byte_string() { assert_eq!( - read_byte_string(&cbor_int!(123)), + extract_byte_string(cbor_int!(123)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_byte_string(&cbor_bool!(true)), + extract_byte_string(cbor_bool!(true)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_byte_string(&cbor_text!("foo")), + extract_byte_string(cbor_text!("foo")), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); - assert_eq!(read_byte_string(&cbor_bytes_lit!(b"")), Ok(Vec::new())); + assert_eq!(extract_byte_string(cbor_bytes_lit!(b"")), Ok(Vec::new())); assert_eq!( - read_byte_string(&cbor_bytes_lit!(b"bar")), + extract_byte_string(cbor_bytes_lit!(b"bar")), Ok(b"bar".to_vec()) ); assert_eq!( - read_byte_string(&cbor_array![]), + extract_byte_string(cbor_array![]), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_byte_string(&cbor_map! {}), + extract_byte_string(cbor_map! {}), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); } #[test] - fn test_read_text_string() { + fn test_extract_text_string() { assert_eq!( - read_text_string(&cbor_int!(123)), + extract_text_string(cbor_int!(123)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_text_string(&cbor_bool!(true)), + extract_text_string(cbor_bool!(true)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); - assert_eq!(read_text_string(&cbor_text!("")), Ok(String::new())); + assert_eq!(extract_text_string(cbor_text!("")), Ok(String::new())); assert_eq!( - read_text_string(&cbor_text!("foo")), + extract_text_string(cbor_text!("foo")), Ok(String::from("foo")) ); assert_eq!( - read_text_string(&cbor_bytes_lit!(b"bar")), + extract_text_string(cbor_bytes_lit!(b"bar")), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_text_string(&cbor_array![]), + extract_text_string(cbor_array![]), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_text_string(&cbor_map! {}), + extract_text_string(cbor_map! {}), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); } #[test] - fn test_read_array() { + fn test_extract_array() { assert_eq!( - read_array(&cbor_int!(123)), + extract_array(cbor_int!(123)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_array(&cbor_bool!(true)), + extract_array(cbor_bool!(true)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_array(&cbor_text!("foo")), + extract_array(cbor_text!("foo")), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_array(&cbor_bytes_lit!(b"bar")), + extract_array(cbor_bytes_lit!(b"bar")), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); - assert_eq!(read_array(&cbor_array![]), Ok(&Vec::new())); + assert_eq!(extract_array(cbor_array![]), Ok(Vec::new())); assert_eq!( - read_array(&cbor_array![ + extract_array(cbor_array![ 123, cbor_null!(), "foo", cbor_array![], cbor_map! {}, ]), - Ok(&vec![ + Ok(vec![ cbor_int!(123), cbor_null!(), cbor_text!("foo"), @@ -936,41 +937,41 @@ mod test { ]) ); assert_eq!( - read_array(&cbor_map! {}), + extract_array(cbor_map! {}), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); } #[test] - fn test_read_map() { + fn test_extract_map() { assert_eq!( - read_map(&cbor_int!(123)), + extract_map(cbor_int!(123)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_map(&cbor_bool!(true)), + extract_map(cbor_bool!(true)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_map(&cbor_text!("foo")), + extract_map(cbor_text!("foo")), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_map(&cbor_bytes_lit!(b"bar")), + extract_map(cbor_bytes_lit!(b"bar")), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_map(&cbor_array![]), + extract_map(cbor_array![]), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); - assert_eq!(read_map(&cbor_map! {}), Ok(&BTreeMap::new())); + assert_eq!(extract_map(cbor_map! {}), Ok(BTreeMap::new())); assert_eq!( - read_map(&cbor_map! { + extract_map(cbor_map! { 1 => cbor_false!(), "foo" => b"bar", b"bin" => -42, }), - Ok(&[ + Ok([ (cbor_unsigned!(1), cbor_false!()), (cbor_text!("foo"), cbor_bytes_lit!(b"bar")), (cbor_bytes_lit!(b"bin"), cbor_int!(-42)), @@ -982,27 +983,27 @@ mod test { } #[test] - fn test_read_bool() { + fn test_extract_bool() { assert_eq!( - read_bool(&cbor_int!(123)), + extract_bool(cbor_int!(123)), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); - assert_eq!(read_bool(&cbor_bool!(true)), Ok(true)); - assert_eq!(read_bool(&cbor_bool!(false)), Ok(false)); + assert_eq!(extract_bool(cbor_bool!(true)), Ok(true)); + assert_eq!(extract_bool(cbor_bool!(false)), Ok(false)); assert_eq!( - read_bool(&cbor_text!("foo")), + extract_bool(cbor_text!("foo")), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_bool(&cbor_bytes_lit!(b"bar")), + extract_bool(cbor_bytes_lit!(b"bar")), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_bool(&cbor_array![]), + extract_bool(cbor_array![]), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); assert_eq!( - read_bool(&cbor_map! {}), + extract_bool(cbor_map! {}), Err(CTAP2_ERR_CBOR_UNEXPECTED_TYPE) ); } @@ -1014,7 +1015,7 @@ mod test { "name" => "Example", "icon" => "example.com/icon.png", }; - let rp_entity = PublicKeyCredentialRpEntity::try_from(&cbor_rp_entity); + let rp_entity = PublicKeyCredentialRpEntity::try_from(cbor_rp_entity); let expected_rp_entity = PublicKeyCredentialRpEntity { rp_id: "example.com".to_string(), rp_name: Some("Example".to_string()), @@ -1031,7 +1032,7 @@ mod test { "displayName" => "bar", "icon" => "example.com/foo/icon.png", }; - let user_entity = PublicKeyCredentialUserEntity::try_from(&cbor_user_entity); + let user_entity = PublicKeyCredentialUserEntity::try_from(cbor_user_entity.clone()); let expected_user_entity = PublicKeyCredentialUserEntity { user_id: vec![0x1D, 0x1D, 0x1D, 0x1D], user_name: Some("foo".to_string()), @@ -1045,39 +1046,70 @@ mod test { #[test] fn test_from_into_public_key_credential_type() { - let cbor_credential_type = cbor_text!("public-key"); - let credential_type = PublicKeyCredentialType::try_from(&cbor_credential_type); + let cbor_credential_type: cbor::Value = cbor_text!("public-key"); + let credential_type = PublicKeyCredentialType::try_from(cbor_credential_type.clone()); let expected_credential_type = PublicKeyCredentialType::PublicKey; assert_eq!(credential_type, Ok(expected_credential_type)); let created_cbor: cbor::Value = credential_type.unwrap().into(); assert_eq!(created_cbor, cbor_credential_type); - let cbor_unknown_type = cbor_text!("unknown-type"); - let unknown_type = PublicKeyCredentialType::try_from(&cbor_unknown_type); + let cbor_unknown_type: cbor::Value = cbor_text!("unknown-type"); + let unknown_type = PublicKeyCredentialType::try_from(cbor_unknown_type); let expected_unknown_type = PublicKeyCredentialType::Unknown; assert_eq!(unknown_type, Ok(expected_unknown_type)); } #[test] fn test_from_into_signature_algorithm() { - let cbor_signature_algorithm = cbor_int!(ecdsa::PubKey::ES256_ALGORITHM); - let signature_algorithm = SignatureAlgorithm::try_from(&cbor_signature_algorithm); + let cbor_signature_algorithm: cbor::Value = cbor_int!(ecdsa::PubKey::ES256_ALGORITHM); + let signature_algorithm = SignatureAlgorithm::try_from(cbor_signature_algorithm.clone()); let expected_signature_algorithm = SignatureAlgorithm::ES256; assert_eq!(signature_algorithm, Ok(expected_signature_algorithm)); - let created_cbor: cbor::Value = cbor_int!(signature_algorithm.unwrap() as i64); + let created_cbor: cbor::Value = signature_algorithm.unwrap().into(); assert_eq!(created_cbor, cbor_signature_algorithm); - let cbor_unknown_algorithm = cbor_int!(-1); - let unknown_algorithm = SignatureAlgorithm::try_from(&cbor_unknown_algorithm); + let cbor_unknown_algorithm: cbor::Value = cbor_int!(-1); + let unknown_algorithm = SignatureAlgorithm::try_from(cbor_unknown_algorithm); let expected_unknown_algorithm = SignatureAlgorithm::Unknown; assert_eq!(unknown_algorithm, Ok(expected_unknown_algorithm)); } + #[test] + fn test_cred_protection_policy_order() { + assert!( + CredentialProtectionPolicy::UserVerificationOptional + < CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList + ); + assert!( + CredentialProtectionPolicy::UserVerificationOptional + < CredentialProtectionPolicy::UserVerificationRequired + ); + assert!( + CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList + < CredentialProtectionPolicy::UserVerificationRequired + ); + } + + #[test] + fn test_from_into_cred_protection_policy() { + let cbor_policy: cbor::Value = CredentialProtectionPolicy::UserVerificationOptional.into(); + let policy = CredentialProtectionPolicy::try_from(cbor_policy.clone()); + let expected_policy = CredentialProtectionPolicy::UserVerificationOptional; + assert_eq!(policy, Ok(expected_policy)); + let created_cbor: cbor::Value = policy.unwrap().into(); + assert_eq!(created_cbor, cbor_policy); + + let cbor_policy_error: cbor::Value = cbor_int!(-1); + let policy_error = CredentialProtectionPolicy::try_from(cbor_policy_error); + let expected_error = Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE); + assert_eq!(policy_error, expected_error); + } + #[test] fn test_from_into_authenticator_transport() { - let cbor_authenticator_transport = cbor_text!("usb"); + let cbor_authenticator_transport: cbor::Value = cbor_text!("usb"); let authenticator_transport = - AuthenticatorTransport::try_from(&cbor_authenticator_transport); + AuthenticatorTransport::try_from(cbor_authenticator_transport.clone()); let expected_authenticator_transport = AuthenticatorTransport::Usb; assert_eq!( authenticator_transport, @@ -1094,7 +1126,7 @@ mod test { "alg" => ecdsa::PubKey::ES256_ALGORITHM, }; let credential_parameter = - PublicKeyCredentialParameter::try_from(&cbor_credential_parameter); + PublicKeyCredentialParameter::try_from(cbor_credential_parameter.clone()); let expected_credential_parameter = PublicKeyCredentialParameter { cred_type: PublicKeyCredentialType::PublicKey, alg: SignatureAlgorithm::ES256, @@ -1112,7 +1144,7 @@ mod test { "transports" => cbor_array!["usb"], }; let credential_descriptor = - PublicKeyCredentialDescriptor::try_from(&cbor_credential_descriptor); + PublicKeyCredentialDescriptor::try_from(cbor_credential_descriptor.clone()); let expected_credential_descriptor = PublicKeyCredentialDescriptor { key_type: PublicKeyCredentialType::PublicKey, key_id: vec![0x2D, 0x2D, 0x2D, 0x2D], @@ -1124,44 +1156,21 @@ mod test { } #[test] - fn test_from_into_extensions() { - let cbor_extensions = cbor_map! { - "the_answer" => 42, - }; - let extensions = Extensions::try_from(&cbor_extensions); - let mut expected_extensions = Extensions(BTreeMap::new()); - expected_extensions - .0 - .insert("the_answer".to_string(), cbor_int!(42)); - assert_eq!(extensions, Ok(expected_extensions)); - let created_cbor: cbor::Value = extensions.unwrap().into(); - assert_eq!(created_cbor, cbor_extensions); - } - - #[test] - fn test_from_into_get_assertion_hmac_secret_output() { - let cbor_output = cbor_bytes![vec![0xC0; 32]]; - let output = GetAssertionHmacSecretOutput::try_from(&cbor_output); - let expected_output = GetAssertionHmacSecretOutput(vec![0xC0; 32]); - assert_eq!(output, Ok(expected_output)); - let created_cbor: cbor::Value = output.unwrap().into(); - assert_eq!(created_cbor, cbor_output); - } - - #[test] - fn test_hmac_secret_extension() { + fn test_from_make_credential_extensions() { let cbor_extensions = cbor_map! { "hmac-secret" => true, + "credProtect" => CredentialProtectionPolicy::UserVerificationRequired, }; - let extensions = Extensions::try_from(&cbor_extensions).unwrap(); - assert!(extensions.has_make_credential_hmac_secret().unwrap()); - - let cbor_extensions = cbor_map! { - "hmac-secret" => false, + let extensions = MakeCredentialExtensions::try_from(cbor_extensions); + let expected_extensions = MakeCredentialExtensions { + hmac_secret: true, + cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), }; - let extensions = Extensions::try_from(&cbor_extensions).unwrap(); - assert!(!extensions.has_make_credential_hmac_secret().unwrap()); + assert_eq!(extensions, Ok(expected_extensions)); + } + #[test] + fn test_from_get_assertion_extensions() { let mut rng = ThreadRng256 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let pk = sk.genpk(); @@ -1170,17 +1179,19 @@ mod test { "hmac-secret" => cbor_map! { 1 => cbor::Value::Map(cose_key.0.clone()), 2 => vec![0x02; 32], - 3 => vec![0x03; 32], + 3 => vec![0x03; 16], }, }; - let extensions = Extensions::try_from(&cbor_extensions).unwrap(); - let get_assertion_input = extensions.get_assertion_hmac_secret(); + let extensions = GetAssertionExtensions::try_from(cbor_extensions); let expected_input = GetAssertionHmacSecretInput { key_agreement: cose_key, salt_enc: vec![0x02; 32], - salt_auth: vec![0x03; 32], + salt_auth: vec![0x03; 16], }; - assert_eq!(get_assertion_input, Some(Ok(expected_input))); + let expected_extensions = GetAssertionExtensions { + hmac_secret: Some(expected_input), + }; + assert_eq!(extensions, Ok(expected_extensions)); } #[test] @@ -1189,7 +1200,7 @@ mod test { "rk" => true, "uv" => false, }; - let make_options = MakeCredentialOptions::try_from(&cbor_make_options); + let make_options = MakeCredentialOptions::try_from(cbor_make_options); let expected_make_options = MakeCredentialOptions { rk: true, uv: false, @@ -1203,7 +1214,7 @@ mod test { "up" => true, "uv" => false, }; - let get_assertion = GetAssertionOptions::try_from(&cbor_get_assertion); + let get_assertion = GetAssertionOptions::try_from(cbor_get_assertion); let expected_get_assertion = GetAssertionOptions { up: true, uv: false, @@ -1242,8 +1253,8 @@ mod test { #[test] fn test_from_into_client_pin_sub_command() { - let cbor_sub_command = cbor_int!(0x01); - let sub_command = ClientPinSubCommand::try_from(&cbor_sub_command); + let cbor_sub_command: cbor::Value = cbor_int!(0x01); + let sub_command = ClientPinSubCommand::try_from(cbor_sub_command.clone()); let expected_sub_command = ClientPinSubCommand::GetPinRetries; assert_eq!(sub_command, Ok(expected_sub_command)); let created_cbor: cbor::Value = sub_command.unwrap().into(); @@ -1261,6 +1272,7 @@ mod test { user_handle: b"foo".to_vec(), other_ui: None, cred_random: None, + cred_protect_policy: None, }; assert_eq!( @@ -1283,6 +1295,16 @@ mod test { ..credential }; + assert_eq!( + PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), + Ok(credential.clone()) + ); + + let credential = PublicKeyCredentialSource { + cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationOptional), + ..credential + }; + assert_eq!( PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), Ok(credential) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index bd1db51..7edc79b 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -32,9 +32,10 @@ use self::command::{ #[cfg(feature = "with_ctap2_1")] use self::data_formats::AuthenticatorTransport; use self::data_formats::{ - ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput, PackedAttestationStatement, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, - PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, + ClientPinSubCommand, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput, + PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, + PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity, + SignatureAlgorithm, }; use self::hid::ChannelID; use self::response::{ @@ -108,6 +109,10 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa cred_type: PublicKeyCredentialType::PublicKey, alg: SignatureAlgorithm::ES256, }; +// You can change this value to one of the following for more privacy. +// - Some(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList) +// - Some(CredentialProtectionPolicy::UserVerificationRequired) +const DEFAULT_CRED_PROTECT: Option = None; fn check_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> bool { if pin_auth.len() != PIN_AUTH_LENGTH { @@ -334,6 +339,7 @@ where user_handle: vec![], other_ui: None, cred_random: None, + cred_protect_policy: None, }) } @@ -427,30 +433,42 @@ where 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 (use_hmac_extension, cred_protect_policy) = if let Some(extensions) = extensions { + let mut cred_protect = extensions.cred_protect; + if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + < DEFAULT_CRED_PROTECT + .unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + { + cred_protect = DEFAULT_CRED_PROTECT; + } + (extensions.hmac_secret, cred_protect) + } else { + (false, None) + }; + let cred_random = if use_hmac_extension { + if !options.rk { + // The extension is actually supported, but we need resident keys. + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); + } Some(self.rng.gen_uniform_u8x32().to_vec()) } else { None }; - let ed_flag = if use_hmac_extension { ED_FLAG } else { 0 }; + // TODO(kaczmarczyck) unsolicited output for default credProtect level + let has_extension_output = use_hmac_extension || cred_protect_policy.is_some(); let rp_id = rp.rp_id; if let Some(exclude_list) = exclude_list { for cred_desc in exclude_list { if self .persistent_store - .find_credential(&rp_id, &cred_desc.key_id) + .find_credential(&rp_id, &cred_desc.key_id, pin_uv_auth_param.is_none()) .is_some() { // Perform this check, so bad actors can't brute force exclude_list - // without user interaction. Discard the user presence check's outcome. - let _ = (self.check_user_presence)(cid); + // without user interaction. + (self.check_user_presence)(cid)?; return Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED); } } @@ -458,6 +476,7 @@ where // MakeCredential always requires user presence. // User verification depends on the PIN auth inputs, which are checked here. + let ed_flag = if has_extension_output { ED_FLAG } else { 0 }; let flags = match pin_uv_auth_param { Some(pin_auth) => { if self.persistent_store.pin_hash().is_none() { @@ -500,6 +519,7 @@ where .user_display_name .map(|s| truncate_to_char_boundary(&s, 64).to_string()), cred_random, + cred_protect_policy, }; self.persistent_store.store_credential(credential_source)?; random_id @@ -520,11 +540,13 @@ where None => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR), }; auth_data.extend(cose_key); - if use_hmac_extension { - let extensions = cbor_map! { - "hmac-secret" => true, + if has_extension_output { + let hmac_secret_output = if use_hmac_extension { Some(true) } else { None }; + let extensions_output = cbor_map_options! { + "hmac-secret" => hmac_secret_output, + "credProtect" => cred_protect_policy, }; - if !cbor::write(extensions, &mut auth_data) { + if !cbor::write(extensions_output, &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); } } @@ -621,17 +643,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 { + let hmac_secret_input = extensions.map(|e| e.hmac_secret).flatten(); + if 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 - // user presence is requested as an option. + // The user verification bit depends on the existance of PIN auth, since we do + // not support internal UV. User presence is requested as an option. + let has_uv = pin_uv_auth_param.is_some(); let mut flags = match pin_uv_auth_param { Some(pin_auth) => { if self.persistent_store.pin_hash().is_none() { @@ -654,7 +674,7 @@ where if options.up { flags |= UP_FLAG; } - if get_assertion_hmac_secret_input.is_some() { + if hmac_secret_input.is_some() { flags |= ED_FLAG; } @@ -663,11 +683,14 @@ where let credentials = if let Some(allow_list) = allow_list { let mut found_credentials = vec![]; for allowed_credential in allow_list { - match self - .persistent_store - .find_credential(&rp_id, &allowed_credential.key_id) - { - Some(credential) => found_credentials.push(credential), + match self.persistent_store.find_credential( + &rp_id, + &allowed_credential.key_id, + !has_uv, + ) { + Some(credential) => { + found_credentials.push(credential); + } None => { if decrypted_credential.is_none() { decrypted_credential = self @@ -679,7 +702,7 @@ where found_credentials } else { // TODO(kaczmarczyck) use GetNextAssertion - self.persistent_store.filter_credential(&rp_id) + self.persistent_store.filter_credential(&rp_id, !has_uv) }; let credential = if let Some(credential) = credentials.first() { @@ -698,12 +721,12 @@ where 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 { + if let Some(hmac_secret_input) = hmac_secret_input { let GetAssertionHmacSecretInput { key_agreement, salt_enc, salt_auth, - } = get_assertion_hmac_secret_input; + } = 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. @@ -718,10 +741,10 @@ where None => return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION), }; - let extensions = cbor_map! { + let extensions_output = cbor_map! { "hmac-secret" => encrypted_output, }; - if !cbor::write(extensions, &mut auth_data) { + if !cbor::write(extensions_output, &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); } } @@ -791,6 +814,7 @@ where transports: Some(vec![AuthenticatorTransport::Usb]), #[cfg(feature = "with_ctap2_1")] algorithms: Some(vec![ES256_CRED_PARAM]), + default_cred_protect: DEFAULT_CRED_PROTECT, #[cfg(feature = "with_ctap2_1")] firmware_version: None, }, @@ -1095,8 +1119,8 @@ where #[cfg(test)] mod test { use super::data_formats::{ - Extensions, GetAssertionOptions, MakeCredentialOptions, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, + GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, + MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::*; use crypto::rng256::ThreadRng256; @@ -1179,6 +1203,32 @@ mod test { } } + fn create_make_credential_parameters_with_exclude_list( + excluded_credential_id: &[u8], + ) -> AuthenticatorMakeCredentialParameters { + let excluded_credential_descriptor = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: excluded_credential_id.to_vec(), + transports: None, + }; + let exclude_list = Some(vec![excluded_credential_descriptor]); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.exclude_list = exclude_list; + make_credential_params + } + + fn create_make_credential_parameters_with_cred_protect_policy( + policy: CredentialProtectionPolicy, + ) -> AuthenticatorMakeCredentialParameters { + let extensions = Some(MakeCredentialExtensions { + hmac_secret: false, + cred_protect: Some(policy), + }); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + make_credential_params + } + #[test] fn test_residential_process_make_credential() { let mut rng = ThreadRng256 {}; @@ -1277,46 +1327,93 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); let excluded_credential_id = vec![0x01, 0x23, 0x45, 0x67]; + let make_credential_params = + create_make_credential_parameters_with_exclude_list(&excluded_credential_id); let excluded_credential_source = PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, - credential_id: excluded_credential_id.clone(), + credential_id: excluded_credential_id, private_key: excluded_private_key, rp_id: String::from("example.com"), user_handle: vec![], other_ui: None, cred_random: None, + cred_protect_policy: None, }; assert!(ctap_state .persistent_store .store_credential(excluded_credential_source) .is_ok()); - let excluded_credential_descriptor = PublicKeyCredentialDescriptor { - key_type: PublicKeyCredentialType::PublicKey, - key_id: excluded_credential_id, - transports: None, - }; - let exclude_list = Some(vec![excluded_credential_descriptor]); - let mut make_credential_params = create_minimal_make_credential_parameters(); - make_credential_params.exclude_list = exclude_list; let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); - assert_eq!( make_credential_response, Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED) ); } + #[test] + fn test_process_make_credential_credential_with_cred_protect() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + + let test_policy = CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList; + let make_credential_params = + create_make_credential_parameters_with_cred_protect_policy(test_policy); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert!(make_credential_response.is_ok()); + + let stored_credential = ctap_state + .persistent_store + .filter_credential("example.com", false) + .pop() + .unwrap(); + let credential_id = stored_credential.credential_id; + assert_eq!(stored_credential.cred_protect_policy, Some(test_policy)); + + let make_credential_params = + create_make_credential_parameters_with_exclude_list(&credential_id); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED) + ); + + let test_policy = CredentialProtectionPolicy::UserVerificationRequired; + let make_credential_params = + create_make_credential_parameters_with_cred_protect_policy(test_policy); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert!(make_credential_response.is_ok()); + + let stored_credential = ctap_state + .persistent_store + .filter_credential("example.com", false) + .pop() + .unwrap(); + let credential_id = stored_credential.credential_id; + assert_eq!(stored_credential.cred_protect_policy, Some(test_policy)); + + let make_credential_params = + create_make_credential_parameters_with_exclude_list(&credential_id); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert!(make_credential_response.is_ok()); + } + #[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 extensions = Some(MakeCredentialExtensions { + hmac_secret: true, + cred_protect: None, + }); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; let make_credential_response = @@ -1426,9 +1523,10 @@ mod test { 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 make_extensions = Some(MakeCredentialExtensions { + hmac_secret: true, + cred_protect: None, + }); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = make_extensions; assert!(ctap_state @@ -1436,15 +1534,15 @@ mod test { .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 hmac_secret_input = GetAssertionHmacSecretInput { + key_agreement: CoseKey::from(pk), + salt_enc: vec![0x02; 32], + salt_auth: vec![0x03; 16], }; - let mut extension_map = BTreeMap::new(); - extension_map.insert("hmac-secret".to_string(), hmac_secret_parameters); + let get_extensions = Some(GetAssertionExtensions { + hmac_secret: Some(hmac_secret_input), + }); - let get_extensions = Some(Extensions::new(extension_map)); let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], @@ -1466,6 +1564,106 @@ mod test { ); } + #[test] + fn test_residential_process_get_assertion_with_cred_protect() { + let mut rng = ThreadRng256 {}; + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let credential_id = rng.gen_uniform_u8x32().to_vec(); + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + + let cred_desc = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: credential_id.clone(), + transports: None, // You can set USB as a hint here. + }; + let credential = PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id: credential_id.clone(), + private_key: private_key.clone(), + rp_id: String::from("example.com"), + user_handle: vec![0x00], + other_ui: None, + cred_random: None, + cred_protect_policy: Some( + CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, + ), + }; + assert!(ctap_state + .persistent_store + .store_credential(credential) + .is_ok()); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions: None, + 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_NO_CREDENTIALS), + ); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: Some(vec![cred_desc.clone()]), + extensions: None, + 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!(get_assertion_response.is_ok()); + + let credential = PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id, + private_key, + rp_id: String::from("example.com"), + user_handle: vec![0x00], + other_ui: None, + cred_random: None, + cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), + }; + assert!(ctap_state + .persistent_store + .store_credential(credential) + .is_ok()); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: Some(vec![cred_desc]), + extensions: None, + 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_NO_CREDENTIALS), + ); + } + #[test] fn test_process_reset() { let mut rng = ThreadRng256 {}; @@ -1482,6 +1680,7 @@ mod test { user_handle: vec![], other_ui: None, cred_random: None, + cred_protect_policy: None, }; assert!(ctap_state .persistent_store diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 389b82d..2a33a6d 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -15,7 +15,7 @@ #[cfg(feature = "with_ctap2_1")] use super::data_formats::{AuthenticatorTransport, PublicKeyCredentialParameter}; use super::data_formats::{ - CoseKey, PackedAttestationStatement, PublicKeyCredentialDescriptor, + CoseKey, CredentialProtectionPolicy, PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, }; use alloc::collections::BTreeMap; @@ -119,6 +119,7 @@ pub struct AuthenticatorGetInfoResponse { pub transports: Option>, #[cfg(feature = "with_ctap2_1")] pub algorithms: Option>, + pub default_cred_protect: Option, #[cfg(feature = "with_ctap2_1")] pub firmware_version: Option, } @@ -137,6 +138,7 @@ impl From for cbor::Value { max_credential_id_length, transports, algorithms, + default_cred_protect, firmware_version, } = get_info_response; @@ -159,6 +161,7 @@ impl From for cbor::Value { 0x08 => max_credential_id_length, 0x09 => transports.map(|vec| cbor_array_vec!(vec)), 0x0A => algorithms.map(|vec| cbor_array_vec!(vec)), + 0x0C => default_cred_protect.map(|p| p as u64), 0x0E => firmware_version, } } @@ -172,6 +175,7 @@ impl From for cbor::Value { options, max_msg_size, pin_protocols, + default_cred_protect, } = get_info_response; let options_cbor: Option = options.map(|options| { @@ -189,6 +193,7 @@ impl From for cbor::Value { 0x04 => options_cbor, 0x05 => max_msg_size, 0x06 => pin_protocols.map(|vec| cbor_array_vec!(vec)), + 0x0C => default_cred_protect.map(|p| p as u64), } } } @@ -290,6 +295,7 @@ mod test { transports: None, #[cfg(feature = "with_ctap2_1")] algorithms: None, + default_cred_protect: None, #[cfg(feature = "with_ctap2_1")] firmware_version: None, }; @@ -318,6 +324,7 @@ mod test { max_credential_id_length: Some(256), transports: Some(vec![AuthenticatorTransport::Usb]), algorithms: Some(vec![ES256_CRED_PARAM]), + default_cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), firmware_version: Some(0), }; let response_cbor: Option = @@ -333,6 +340,7 @@ mod test { 0x08 => 256, 0x09 => cbor_array_vec![vec!["usb"]], 0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]], + 0x0C => CredentialProtectionPolicy::UserVerificationRequired as u64, 0x0E => 0, }; assert_eq!(response_cbor, Some(expected_cbor)); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 452bff6..5613b19 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::crypto::rng256::Rng256; -use crate::ctap::data_formats::PublicKeyCredentialSource; +use crate::ctap::data_formats::{CredentialProtectionPolicy, PublicKeyCredentialSource}; use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::{key_material, PIN_AUTH_LENGTH, USE_BATCH_ATTESTATION}; use alloc::string::String; @@ -228,6 +228,7 @@ impl PersistentStore { &self, rp_id: &str, credential_id: &[u8], + check_cred_protect: bool, ) -> Option { let key = Key::Credential { rp_id: Some(rp_id.into()), @@ -238,7 +239,16 @@ impl PersistentStore { debug_assert_eq!(entry.tag, TAG_CREDENTIAL); let result = deserialize_credential(entry.data); debug_assert!(result.is_some()); - result + if check_cred_protect + && result.as_ref().map_or(false, |cred| { + cred.cred_protect_policy + == Some(CredentialProtectionPolicy::UserVerificationRequired) + }) + { + None + } else { + result + } } pub fn store_credential( @@ -270,7 +280,11 @@ impl PersistentStore { Ok(()) } - pub fn filter_credential(&self, rp_id: &str) -> Vec { + pub fn filter_credential( + &self, + rp_id: &str, + check_cred_protect: bool, + ) -> Vec { self.store .find_all(&Key::Credential { rp_id: Some(rp_id.into()), @@ -283,6 +297,7 @@ impl PersistentStore { debug_assert!(credential.is_some()); credential }) + .filter(|cred| !check_cred_protect || cred.is_discoverable()) .collect() } @@ -550,6 +565,7 @@ mod test { user_handle, other_ui: None, cred_random: None, + cred_protect_policy: None, } } @@ -634,7 +650,7 @@ mod test { .is_ok()); assert_eq!(persistent_store.count_credentials(), 1); assert_eq!( - &persistent_store.filter_credential("example.com"), + &persistent_store.filter_credential("example.com", false), &[expected_credential] ); @@ -682,7 +698,7 @@ mod test { .store_credential(credential_source2) .is_ok()); - let filtered_credentials = persistent_store.filter_credential("example.com"); + let filtered_credentials = persistent_store.filter_credential("example.com", false); assert_eq!(filtered_credentials.len(), 2); assert!( (filtered_credentials[0].credential_id == id0 @@ -692,6 +708,30 @@ mod test { ); } + #[test] + fn test_filter_with_cred_protect() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + assert_eq!(persistent_store.count_credentials(), 0); + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let credential = PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id: rng.gen_uniform_u8x32().to_vec(), + private_key, + rp_id: String::from("example.com"), + user_handle: vec![0x00], + other_ui: None, + cred_random: None, + cred_protect_policy: Some( + CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, + ), + }; + assert!(persistent_store.store_credential(credential).is_ok()); + + let no_credential = persistent_store.filter_credential("example.com", true); + assert_eq!(no_credential, vec![]); + } + #[test] fn test_find() { let mut rng = ThreadRng256 {}; @@ -708,9 +748,9 @@ mod test { .store_credential(credential_source1) .is_ok()); - let no_credential = persistent_store.find_credential("another.example.com", &id0); + let no_credential = persistent_store.find_credential("another.example.com", &id0, false); assert_eq!(no_credential, None); - let found_credential = persistent_store.find_credential("example.com", &id0); + let found_credential = persistent_store.find_credential("example.com", &id0, false); let expected_credential = PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, credential_id: id0, @@ -719,10 +759,33 @@ mod test { user_handle: vec![0x00], other_ui: None, cred_random: None, + cred_protect_policy: None, }; assert_eq!(found_credential, Some(expected_credential)); } + #[test] + fn test_find_with_cred_protect() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + assert_eq!(persistent_store.count_credentials(), 0); + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let credential = PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id: rng.gen_uniform_u8x32().to_vec(), + private_key, + rp_id: String::from("example.com"), + user_handle: vec![0x00], + other_ui: None, + cred_random: None, + cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), + }; + assert!(persistent_store.store_credential(credential).is_ok()); + + let no_credential = persistent_store.find_credential("example.com", &vec![0x00], true); + assert_eq!(no_credential, None); + } + #[test] fn test_master_keys() { let mut rng = ThreadRng256 {};