improved testing, addresses comments and a default level fix

This commit is contained in:
Fabian Kaczmarczyck
2020-05-25 19:56:29 +02:00
parent 8d737b3c80
commit a95ef72a93
4 changed files with 117 additions and 67 deletions

View File

@@ -99,9 +99,10 @@ a few things you can personalize:
4. Depending on your available flash storage, choose an appropriate maximum 4. Depending on your available flash storage, choose an appropriate maximum
number of supported residential keys and number of pages in number of supported residential keys and number of pages in
`ctap/storage.rs`. `ctap/storage.rs`.
5. Change the default level for the credProtect extension. Resident credentials 5. Change the default level for the credProtect extension in `ctap/mod.rs`.
become undiscoverable without user verification. This helps privacy, but When changing the default, resident credentials become undiscoverable without
can make usage less comfortable for less important credentials. user verification. This helps privacy, but can make usage less comfortable
for credentials that need less protection.
### 3D printed enclosure ### 3D printed enclosure

View File

@@ -440,7 +440,7 @@ impl TryFrom<&cbor::Value> for SignatureAlgorithm {
} }
} }
#[derive(Clone, PartialEq, PartialOrd)] #[derive(Clone, Copy, PartialEq, PartialOrd)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
pub enum CredentialProtectionPolicy { pub enum CredentialProtectionPolicy {
UserVerificationOptional = 0x01, UserVerificationOptional = 0x01,
@@ -1079,6 +1079,22 @@ mod test {
assert_eq!(unknown_algorithm, Ok(expected_unknown_algorithm)); 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] #[test]
fn test_from_into_cred_protection_policy() { fn test_from_into_cred_protection_policy() {
let cbor_policy = cbor_int!(CredentialProtectionPolicy::UserVerificationOptional as i64); let cbor_policy = cbor_int!(CredentialProtectionPolicy::UserVerificationOptional as i64);

View File

@@ -438,12 +438,9 @@ where
let mut cred_protect = extensions let mut cred_protect = extensions
.make_credential_cred_protect_policy() .make_credential_cred_protect_policy()
.transpose()?; .transpose()?;
if cred_protect if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional)
.as_ref() < DEFAULT_CRED_PROTECT
.unwrap_or(&CredentialProtectionPolicy::UserVerificationOptional) .unwrap_or(CredentialProtectionPolicy::UserVerificationOptional)
> DEFAULT_CRED_PROTECT
.as_ref()
.unwrap_or(&CredentialProtectionPolicy::UserVerificationOptional)
{ {
cred_protect = DEFAULT_CRED_PROTECT; cred_protect = DEFAULT_CRED_PROTECT;
} }
@@ -704,15 +701,7 @@ where
found_credentials found_credentials
} else { } else {
// TODO(kaczmarczyck) use GetNextAssertion // TODO(kaczmarczyck) use GetNextAssertion
let found_credentials = self.persistent_store.filter_credential(&rp_id); self.persistent_store.filter_credential(&rp_id, !has_uv)
if has_uv {
found_credentials
} else {
found_credentials
.into_iter()
.filter(|cred| cred.is_discoverable())
.collect()
}
}; };
let credential = if let Some(credential) = credentials.first() { let credential = if let Some(credential) = credentials.first() {
@@ -1213,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_int!(policy as i64));
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] #[test]
fn test_residential_process_make_credential() { fn test_residential_process_make_credential() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -1311,9 +1325,11 @@ mod test {
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let excluded_credential_id = vec![0x01, 0x23, 0x45, 0x67]; 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 { let excluded_credential_source = PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey, key_type: PublicKeyCredentialType::PublicKey,
credential_id: excluded_credential_id.clone(), credential_id: excluded_credential_id,
private_key: excluded_private_key, private_key: excluded_private_key,
rp_id: String::from("example.com"), rp_id: String::from("example.com"),
user_handle: vec![], user_handle: vec![],
@@ -1326,17 +1342,8 @@ mod test {
.store_credential(excluded_credential_source) .store_credential(excluded_credential_source)
.is_ok()); .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 = let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
assert_eq!( assert_eq!(
make_credential_response, make_credential_response,
Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED) Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED)
@@ -1346,53 +1353,50 @@ mod test {
#[test] #[test]
fn test_process_make_credential_credential_with_cred_protect() { fn test_process_make_credential_credential_with_cred_protect() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
let excluded_private_key = crypto::ecdsa::SecKey::gensk(&mut rng);
let user_immediately_present = |_| Ok(()); let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let excluded_credential_id = vec![0x01, 0x23, 0x45, 0x67]; let test_policy = CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList;
let mut excluded_credential_source = PublicKeyCredentialSource { let make_credential_params =
key_type: PublicKeyCredentialType::PublicKey, create_make_credential_parameters_with_cred_protect_policy(test_policy);
credential_id: excluded_credential_id.clone(),
private_key: excluded_private_key,
rp_id: String::from("example.com"),
user_handle: vec![],
other_ui: None,
cred_random: None,
cred_protect_policy: Some(
CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList,
),
};
assert!(ctap_state
.persistent_store
.store_credential(excluded_credential_source.clone())
.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.clone()]);
let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.exclude_list = exclude_list;
let make_credential_response = let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); 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!( assert_eq!(
make_credential_response, make_credential_response,
Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED) Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED)
); );
excluded_credential_source.cred_protect_policy = let test_policy = CredentialProtectionPolicy::UserVerificationRequired;
Some(CredentialProtectionPolicy::UserVerificationRequired); let make_credential_params =
assert!(ctap_state 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 .persistent_store
.store_credential(excluded_credential_source) .filter_credential("example.com", false)
.is_ok()); .pop()
let exclude_list = Some(vec![excluded_credential_descriptor]); .unwrap();
let mut make_credential_params = create_minimal_make_credential_parameters(); let credential_id = stored_credential.credential_id;
make_credential_params.exclude_list = exclude_list; 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 = let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
assert!(make_credential_response.is_ok()); assert!(make_credential_response.is_ok());

View File

@@ -269,7 +269,11 @@ impl PersistentStore {
Ok(()) Ok(())
} }
pub fn filter_credential(&self, rp_id: &str) -> Vec<PublicKeyCredentialSource> { pub fn filter_credential(
&self,
rp_id: &str,
check_cred_protect: bool,
) -> Vec<PublicKeyCredentialSource> {
self.store self.store
.find_all(&Key::Credential { .find_all(&Key::Credential {
rp_id: Some(rp_id.into()), rp_id: Some(rp_id.into()),
@@ -282,6 +286,7 @@ impl PersistentStore {
debug_assert!(credential.is_some()); debug_assert!(credential.is_some());
credential credential
}) })
.filter(|cred| !check_cred_protect || cred.is_discoverable())
.collect() .collect()
} }
@@ -549,7 +554,7 @@ mod test {
.is_ok()); .is_ok());
assert_eq!(persistent_store.count_credentials(), 1); assert_eq!(persistent_store.count_credentials(), 1);
assert_eq!( assert_eq!(
&persistent_store.filter_credential("example.com"), &persistent_store.filter_credential("example.com", false),
&[expected_credential] &[expected_credential]
); );
@@ -597,7 +602,7 @@ mod test {
.store_credential(credential_source2) .store_credential(credential_source2)
.is_ok()); .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_eq!(filtered_credentials.len(), 2);
assert!( assert!(
(filtered_credentials[0].credential_id == id0 (filtered_credentials[0].credential_id == id0
@@ -607,6 +612,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] #[test]
fn test_find() { fn test_find() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};