diff --git a/README.md b/README.md index 28093fc3..6e68ba1d 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/binaries.sha256sum b/reproducible/binaries.sha256sum new file mode 100644 index 00000000..6858153d --- /dev/null +++ b/reproducible/binaries.sha256sum @@ -0,0 +1,9 @@ +c182bb4902fff51b2f56810fc2a27df3646cd66ba21359162354d53445623ab8 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin +2c2879a0263ebaa6e841db4b352346cc5b4cef5084ce85525cf49669d3b0b41d target/nrf52840dk_merged.hex +0a9929ba8fa57e8a502a49fc7c53177397202e1b11f4c7c3cb6ed68b2b99dd46 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin +652824a90674dc3199070f2f46a791ab4951e367982ecae6f65fc41338a5a856 target/nrf52840_dongle_merged.hex +cca9086c9149c607589b23ffa599a5e4c26db7c20bd3700b79528bd3a5df991d third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin +a030505f5576129954a3977f97957b8b4e023b2b51a29d45d4511566458666ac target/nrf52840_dongle_dfu_merged.hex +8857488ba6a69e366f0da229bbfc012a2ad291d3a88d9494247d600c10bb19b7 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin +82ac4290967ae67a78c986444fd6c2a2aa5668ac254a2af642c98be4a064f913 target/nrf52840_mdk_dfu_merged.hex +c3e901a80fd779b15a6f266e48dcef5140b318f5f62b23a96547498572ac1666 target/tab/ctap2.tab diff --git a/reproducible/reference_binaries_macos-10.15.sha256sum b/reproducible/reference_binaries_macos-10.15.sha256sum index 98b20af2..bb62ef98 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 -84d97929d7592d89c7f321ffccafa4148263607e28918e53d9286be8ca55c209 target/nrf52840dk_merged.hex +63dda5b708add5ac72db0c0757de451f95173b0fc7d71fc85c3b25460850e23c target/nrf52840dk_merged.hex 88f00a5e1dae6ab3f7571c254ac75f5f3e29ebea7f3ca46c16cfdc3708e804fc third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin -02009688b6ef8583f78f9b94ba8af65dfa3749b20516972cdb0d8ea7ac409268 target/nrf52840_dongle_merged.hex +2fca47df0053c6750d9f8fe61e8e6a0e9eee2457c908aa1c2225e2aab2690cb4 target/nrf52840_dongle_merged.hex 1bc69b48a2c48da55db8b322902e1fe3f2e095c0dd8517db28837d86e0addc85 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin -a24e7459b93eea1fc7557ecd9e4271a2ed729425990d6198be6791f364b1384b target/nrf52840_dongle_dfu_merged.hex +33b5a96bb427de653440cd8bfa31e518e1a2c269368a6cd103602d2fd6f3e127 target/nrf52840_dongle_dfu_merged.hex f38ee31d3a09e7e11848e78b5318f95517b6dcd076afcb37e6e3d3e5e9995cc7 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin -5315b77b997952de10e239289e54b44c24105646f2411074332bb46f4b686ae6 target/nrf52840_mdk_dfu_merged.hex -9012744b93f8bac122fa18916cf8c22d1b8f729a284366802ed222bbc985e3f0 target/tab/ctap2.tab +dce8f1f02e2f7e93634b1edd02343547e139445d23c2d17ce6ccdc4895a67c8c target/nrf52840_mdk_dfu_merged.hex +1e216599d58e6c66845845a2ec709f72842e21d48a2185022223d1ffe006de07 target/tab/ctap2.tab diff --git a/reproducible/reference_binaries_ubuntu-18.04.sha256sum b/reproducible/reference_binaries_ubuntu-18.04.sha256sum index 3171ce7d..d57d20c5 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 -805ca30b898b41a091cc136ab9b78b4e566e10c5469902d18c326c132ed4193e target/nrf52840dk_merged.hex +d978bbb7d56e54f1d8dc43a8b73e01ede50f404f8640ed8969112acac9efa36e target/nrf52840dk_merged.hex 0a9929ba8fa57e8a502a49fc7c53177397202e1b11f4c7c3cb6ed68b2b99dd46 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin -960dce1eb78f34a3c4cfdb543314da8ce211dced41f34da053669c8773926e1d target/nrf52840_dongle_merged.hex +53f3f390fce878d4d6108d34a0f4f305375ca125a02a4160839eeee3acd55e88 target/nrf52840_dongle_merged.hex cca9086c9149c607589b23ffa599a5e4c26db7c20bd3700b79528bd3a5df991d third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin -1755746cb3a28162a0bbd0b994332fa9ffaedca684803dfd9ef584040cea73ca target/nrf52840_dongle_dfu_merged.hex +68ef4c5d2e5b53761c31e185fa6237eaaa13736f8e0657c68d235f27497efe85 target/nrf52840_dongle_dfu_merged.hex 8857488ba6a69e366f0da229bbfc012a2ad291d3a88d9494247d600c10bb19b7 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin -04b94cd65bf83fa12030c4deaa831e0251f5f8b9684ec972d03a46e8f32e98b4 target/nrf52840_mdk_dfu_merged.hex -69dd51b947013b77e3577784384c935ed76930d1fb3ba46e9a5b6b5d71941057 target/tab/ctap2.tab +898842aeaf9a7b03f3f5828871fc8a41fff8fc550683c18f9cfd718f8da26ba4 target/nrf52840_mdk_dfu_merged.hex +d7d8e13bd8a183a6868a463512a999e1c3843d9d5b13f1d35909bfa80d24619e target/tab/ctap2.tab diff --git a/reproducible/reproduced.tar b/reproducible/reproduced.tar new file mode 100644 index 00000000..96616b53 Binary files /dev/null and b/reproducible/reproduced.tar differ diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index d55121e3..d226fca3 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -155,13 +155,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, @@ -197,7 +197,7 @@ 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, @@ -292,6 +292,14 @@ impl Extensions { .get("hmac-secret") .map(GetAssertionHmacSecretInput::try_from) } + + pub fn make_credential_cred_protect_policy( + &self, + ) -> Option> { + self.0 + .get("credProtect") + .map(CredentialProtectionPolicy::try_from) + } } #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] @@ -421,6 +429,12 @@ pub enum SignatureAlgorithm { Unknown = 0, } +impl From for cbor::Value { + fn from(alg: SignatureAlgorithm) -> Self { + (alg as i64).into() + } +} + impl TryFrom<&cbor::Value> for SignatureAlgorithm { type Error = Ctap2StatusCode; @@ -432,6 +446,46 @@ impl TryFrom<&cbor::Value> for SignatureAlgorithm { } } +#[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), + } + } +} + +impl TryFrom<&cbor::Value> for CredentialProtectionPolicy { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: &cbor::Value) -> Result { + match read_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 +502,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 +514,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 +537,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 +566,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,10 +588,20 @@ 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. @@ -667,6 +738,20 @@ pub(super) fn read_integer(cbor_value: &cbor::Value) -> Result Result { + match cbor_value { + cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => { + 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), + _ => 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()), @@ -1064,7 +1149,7 @@ mod test { let signature_algorithm = SignatureAlgorithm::try_from(&cbor_signature_algorithm); 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::from(signature_algorithm.unwrap()); assert_eq!(created_cbor, cbor_signature_algorithm); let cbor_unknown_algorithm = cbor_int!(-1); @@ -1073,6 +1158,37 @@ mod test { 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::from(CredentialProtectionPolicy::UserVerificationOptional); + let policy = CredentialProtectionPolicy::try_from(&cbor_policy); + let expected_policy = CredentialProtectionPolicy::UserVerificationOptional; + assert_eq!(policy, Ok(expected_policy)); + let created_cbor = cbor::Value::from(policy.unwrap()); + assert_eq!(created_cbor, cbor_policy); + + let cbor_policy_error = 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"); @@ -1183,6 +1299,18 @@ mod test { assert_eq!(get_assertion_input, Some(Ok(expected_input))); } + #[test] + fn test_cred_protect_extension() { + let cbor_extensions = cbor_map! { + "credProtect" => CredentialProtectionPolicy::UserVerificationRequired, + }; + let extensions = Extensions::try_from(&cbor_extensions).unwrap(); + assert_eq!( + extensions.make_credential_cred_protect_policy(), + Some(Ok(CredentialProtectionPolicy::UserVerificationRequired)) + ); + } + #[test] fn test_from_make_credential_options() { let cbor_make_options = cbor_map! { @@ -1261,6 +1389,7 @@ mod test { user_handle: b"foo".to_vec(), other_ui: None, cred_random: None, + cred_protect_policy: None, }; assert_eq!( @@ -1283,6 +1412,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 a39a983f..5514fea2 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::key_material::{AAGUID, ATTESTATION_CERTIFICATE, ATTESTATION_PRIVATE_KEY}; @@ -109,6 +110,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 { @@ -335,6 +340,7 @@ where user_handle: vec![], other_ui: None, cred_random: None, + cred_protect_policy: None, }) } @@ -428,30 +434,44 @@ 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 + .make_credential_cred_protect_policy() + .transpose()?; + if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + < DEFAULT_CRED_PROTECT + .unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + { + cred_protect = DEFAULT_CRED_PROTECT; + } + (extensions.has_make_credential_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); } } @@ -459,6 +479,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() { @@ -501,6 +522,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 @@ -521,9 +543,11 @@ 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 = cbor_map_options! { + "hmac-secret" => hmac_secret_output, + "credProtect" => cred_protect_policy, }; if !cbor::write(extensions, &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); @@ -624,8 +648,9 @@ where 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() { @@ -657,11 +682,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 @@ -673,7 +701,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() { @@ -785,6 +813,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, }, @@ -1173,6 +1202,31 @@ 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 mut extension_map = BTreeMap::new(); + extension_map.insert("credProtect".to_string(), cbor::Value::from(policy)); + let extensions = Some(Extensions::new(extension_map)); + 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 {}; @@ -1271,35 +1325,81 @@ 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] @@ -1460,6 +1560,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 {}; @@ -1476,6 +1676,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 389b82d9..2a33a6d9 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 6fc398ee..4e33ba17 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::PIN_AUTH_LENGTH; use alloc::string::String; @@ -203,6 +203,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()), @@ -213,7 +214,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( @@ -245,7 +255,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()), @@ -258,6 +272,7 @@ impl PersistentStore { debug_assert!(credential.is_some()); credential }) + .filter(|cred| !check_cred_protect || cred.is_discoverable()) .collect() } @@ -440,6 +455,7 @@ mod test { user_handle, other_ui: None, cred_random: None, + cred_protect_policy: None, } } @@ -524,7 +540,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] ); @@ -572,7 +588,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 @@ -582,6 +598,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 {}; @@ -598,9 +638,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, @@ -609,10 +649,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 {};