From 81330e5d521604aed9f07855535ff7069efe4d98 Mon Sep 17 00:00:00 2001 From: hcyang <100930165+hcyang-google@users.noreply.github.com> Date: Wed, 2 Nov 2022 18:08:25 +0800 Subject: [PATCH] Persist/parse slot_id in/from credential (#569) * Persist/parse slot_id in/from credential Persist slot_id into credential_id or the resident credential record during MakeCredential, and parse it during GetAssertion. Add related unittests. * Fix styles * Move enable_pin_uv back to ctap/mod.rs --- src/ctap/credential_id.rs | 59 ++- src/ctap/credential_management.rs | 123 ++++- src/ctap/ctap1.rs | 26 +- src/ctap/data_formats.rs | 24 + src/ctap/mod.rs | 729 ++++++++++++++++++++++++++++-- src/ctap/storage.rs | 9 +- 6 files changed, 902 insertions(+), 68 deletions(-) diff --git a/src/ctap/credential_id.rs b/src/ctap/credential_id.rs index 20d9b73..7fd1b39 100644 --- a/src/ctap/credential_id.rs +++ b/src/ctap/credential_id.rs @@ -19,7 +19,7 @@ use super::data_formats::{ use super::status_code::Ctap2StatusCode; use super::{cbor_read, cbor_write}; use crate::api::key_store::KeyStore; -use crate::ctap::data_formats::{extract_byte_string, extract_map}; +use crate::ctap::data_formats::{extract_byte_string, extract_map, extract_unsigned}; use crate::env::Env; use alloc::string::String; use alloc::vec::Vec; @@ -48,6 +48,7 @@ struct CredentialSource { rp_id_hash: [u8; 32], cred_protect_policy: Option, cred_blob: Option>, + slot_id: Option, } // The data fields contained in the credential ID are serialized using CBOR maps. @@ -57,6 +58,7 @@ enum CredentialSourceField { RpIdHash = 1, CredProtectPolicy = 2, CredBlob = 3, + SlotId = 4, } impl From for sk_cbor::Value { @@ -84,6 +86,7 @@ fn decrypt_legacy_credential_id( rp_id_hash: plaintext[32..64].try_into().unwrap(), cred_protect_policy: None, cred_blob: None, + slot_id: None, })) } @@ -102,6 +105,7 @@ fn decrypt_cbor_credential_id( CredentialSourceField::RpIdHash=> rp_id_hash, CredentialSourceField::CredProtectPolicy => cred_protect_policy, CredentialSourceField::CredBlob => cred_blob, + CredentialSourceField::SlotId => slot_id, } = extract_map(cbor_credential_source)?; } Ok(match (private_key, rp_id_hash) { @@ -115,11 +119,19 @@ fn decrypt_cbor_credential_id( .map(CredentialProtectionPolicy::try_from) .transpose()?; let cred_blob = cred_blob.map(extract_byte_string).transpose()?; + let slot_id = match slot_id.map(extract_unsigned).transpose()? { + Some(x) => Some( + usize::try_from(x) + .map_err(|_| Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?, + ), + None => None, + }; Some(CredentialSource { private_key, rp_id_hash: rp_id_hash.try_into().unwrap(), cred_protect_policy, cred_blob, + slot_id, }) } _ => None, @@ -167,6 +179,7 @@ pub fn encrypt_to_credential_id( rp_id_hash: &[u8; 32], cred_protect_policy: Option, cred_blob: Option>, + slot_id: usize, ) -> Result, Ctap2StatusCode> { let mut payload = Vec::new(); let cbor = cbor_map_options! { @@ -174,6 +187,7 @@ pub fn encrypt_to_credential_id( CredentialSourceField::RpIdHash=> rp_id_hash, CredentialSourceField::CredProtectPolicy => cred_protect_policy, CredentialSourceField::CredBlob => cred_blob, + CredentialSourceField::SlotId => slot_id as u64, }; cbor_write(cbor, &mut payload)?; add_padding(&mut payload)?; @@ -262,6 +276,7 @@ pub fn decrypt_credential_id( user_icon: None, cred_blob: credential_source.cred_blob, large_blob_key: None, + slot_id: credential_source.slot_id, })) } @@ -282,7 +297,7 @@ mod test { let rp_id_hash = [0x55; 32]; let encrypted_id = - encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap(); + encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None, 0).unwrap(); let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash) .unwrap() .unwrap(); @@ -308,7 +323,7 @@ mod test { let rp_id_hash = [0x55; 32]; let mut encrypted_id = - encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap(); + encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None, 0).unwrap(); encrypted_id[0] = UNSUPPORTED_CREDENTIAL_ID_VERSION; // Override the HMAC to pass the check. encrypted_id.truncate(&encrypted_id.len() - 32); @@ -328,7 +343,7 @@ mod test { let rp_id_hash = [0x55; 32]; let encrypted_id = - encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap(); + encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None, 0).unwrap(); for i in 0..encrypted_id.len() { let mut modified_id = encrypted_id.clone(); modified_id[i] ^= 0x01; @@ -356,7 +371,7 @@ mod test { let rp_id_hash = [0x55; 32]; let encrypted_id = - encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap(); + encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None, 0).unwrap(); for length in (1..CBOR_CREDENTIAL_ID_SIZE).step_by(16) { assert_eq!( @@ -423,7 +438,7 @@ mod test { let rp_id_hash = [0x55; 32]; let encrypted_id = - encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap(); + encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None, 0).unwrap(); assert_eq!(encrypted_id.len(), CBOR_CREDENTIAL_ID_SIZE); } @@ -444,6 +459,7 @@ mod test { &rp_id_hash, cred_protect_policy, cred_blob, + 0, ); assert!(encrypted_id.is_ok()); @@ -461,6 +477,7 @@ mod test { &rp_id_hash, Some(CredentialProtectionPolicy::UserVerificationRequired), None, + 0, ) .unwrap(); @@ -481,9 +498,15 @@ mod test { let rp_id_hash = [0x55; 32]; let cred_blob = Some(vec![0x55; env.customization().max_cred_blob_length()]); - let encrypted_id = - encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, cred_blob.clone()) - .unwrap(); + let encrypted_id = encrypt_to_credential_id( + &mut env, + &private_key, + &rp_id_hash, + None, + cred_blob.clone(), + 0, + ) + .unwrap(); let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash) .unwrap() @@ -491,4 +514,22 @@ mod test { assert_eq!(decrypted_source.private_key, private_key); assert_eq!(decrypted_source.cred_blob, cred_blob); } + + #[test] + fn test_slot_id_persisted() { + let mut env = TestEnv::new(); + let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); + + let rp_id_hash = [0x55; 32]; + let slot_id = 1; + let encrypted_id = + encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None, slot_id) + .unwrap(); + + let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash) + .unwrap() + .unwrap(); + assert_eq!(decrypted_source.private_key, private_key); + assert_eq!(decrypted_source.slot_id, Some(slot_id)); + } } diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index e6894b6..7ae1376 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -81,6 +81,7 @@ fn enumerate_credentials_response( user_icon, cred_blob: _, large_blob_key, + slot_id: _, } = credential; let user = PublicKeyCredentialUserEntity { user_id: user_handle, @@ -183,12 +184,17 @@ fn process_enumerate_credentials_begin( .rp_id_hash .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash[..])?; + // enumerateCredentials needs UV, so slot_id must not be None. + let slot_id = client_pin + .get_slot_id_in_use_or_default(env)? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; let mut iter_result = Ok(()); let iter = storage::iter_credentials(env, &mut iter_result)?; let mut rp_credentials: Vec = iter .filter_map(|(key, credential)| { let cred_rp_id_hash = Sha256::hash(credential.rp_id.as_bytes()); - if cred_rp_id_hash == rp_id_hash.as_slice() { + let slot_id_matches = credential.slot_id.unwrap_or(0) == slot_id; + if cred_rp_id_hash == rp_id_hash.as_slice() && slot_id_matches { Some(key) } else { None @@ -385,6 +391,7 @@ mod test { user_icon: Some("icon".to_string()), cred_blob: None, large_blob_key: None, + slot_id: None, } } @@ -766,6 +773,120 @@ mod test { ); } + #[test] + fn test_process_enumerate_credentials_multi_pin() { + let mut env = TestEnv::new(); + storage::_enable_multi_pin_for_test(&mut env).unwrap(); + let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); + let pin_uv_auth_token = [0x55; 32]; + let client_pin = ClientPin::new_test( + &mut env, + 1, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); + + // credential_source1 has no slot_id, so should be treated as slot 0. Only credential_source 2 and 4 + // should be discovered. + let credential_source1 = create_credential_source(&mut env); + let mut credential_source2 = create_credential_source(&mut env); + credential_source2.user_handle = vec![0x02]; + credential_source2.slot_id = Some(1); + let mut credential_source3 = create_credential_source(&mut env); + credential_source3.user_handle = vec![0x03]; + credential_source3.slot_id = Some(2); + let mut credential_source4 = create_credential_source(&mut env); + credential_source4.user_handle = vec![0x04]; + credential_source4.slot_id = Some(1); + + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + ctap_state.client_pin = client_pin; + + storage::store_credential(&mut env, credential_source1).unwrap(); + storage::store_credential(&mut env, credential_source2).unwrap(); + storage::store_credential(&mut env, credential_source3).unwrap(); + storage::store_credential(&mut env, credential_source4).unwrap(); + + storage::set_pin(&mut env, 1, &[0u8; 16], 4).unwrap(); + let pin_uv_auth_param = Some(vec![ + 0xF8, 0xB0, 0x3C, 0xC1, 0xD5, 0x58, 0x9C, 0xB7, 0x4D, 0x42, 0xA1, 0x64, 0x14, 0x28, + 0x2B, 0x68, + ]); + + let sub_command_params = CredentialManagementSubCommandParameters { + rp_id_hash: Some(Sha256::hash(b"example.com").to_vec()), + credential_id: None, + user: None, + }; + // RP ID hash: + // A379A6F6EEAFB9A55E378C118034E2751E682FAB9F2D30AB13D2125586CE1947 + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, + sub_command_params: Some(sub_command_params), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, + }; + let cred_management_response = process_credential_management( + &mut env, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.client_pin, + cred_management_params, + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert!(response.user.is_some()); + assert!(response.public_key.is_some()); + assert_eq!(response.total_credentials, Some(2)); + } + _ => panic!("Invalid response type"), + }; + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, + sub_command_params: None, + pin_uv_auth_protocol: None, + pin_uv_auth_param: None, + }; + let cred_management_response = process_credential_management( + &mut env, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.client_pin, + cred_management_params, + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert!(response.user.is_some()); + assert!(response.public_key.is_some()); + assert_eq!(response.total_credentials, None); + } + _ => panic!("Invalid response type"), + }; + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, + sub_command_params: None, + pin_uv_auth_protocol: None, + pin_uv_auth_param: None, + }; + let cred_management_response = process_credential_management( + &mut env, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.client_pin, + cred_management_params, + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + #[test] fn test_process_delete_credential() { let mut env = TestEnv::new(); diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index fa0f69a..5cfe04e 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -249,7 +249,7 @@ impl Ctap1Command { .ecdsa_key(env) .map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?; let pk = sk.genpk(); - let key_handle = encrypt_to_credential_id(env, &private_key, &application, None, None) + let key_handle = encrypt_to_credential_id(env, &private_key, &application, None, None, 0) .map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?; if key_handle.len() > 0xFF { // This is just being defensive with unreachable code. @@ -499,7 +499,8 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); + let key_handle = + encrypt_to_credential_id(&mut env, &sk, &application, None, None, 0).unwrap(); let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); let response = @@ -517,7 +518,8 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); + let key_handle = + encrypt_to_credential_id(&mut env, &sk, &application, None, None, 0).unwrap(); let application = [0x55; 32]; let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); @@ -536,7 +538,8 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); + let key_handle = + encrypt_to_credential_id(&mut env, &sk, &application, None, None, 0).unwrap(); let mut message = create_authenticate_message( &application, Ctap1Flags::DontEnforceUpAndSign, @@ -574,7 +577,8 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); + let key_handle = + encrypt_to_credential_id(&mut env, &sk, &application, None, None, 0).unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[0] = 0xEE; @@ -594,7 +598,8 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); + let key_handle = + encrypt_to_credential_id(&mut env, &sk, &application, None, None, 0).unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[1] = 0xEE; @@ -614,7 +619,8 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); + let key_handle = + encrypt_to_credential_id(&mut env, &sk, &application, None, None, 0).unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[2] = 0xEE; @@ -642,7 +648,8 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); + let key_handle = + encrypt_to_credential_id(&mut env, &sk, &application, None, None, 0).unwrap(); let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); @@ -670,7 +677,8 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); + let key_handle = + encrypt_to_credential_id(&mut env, &sk, &application, None, None, 0).unwrap(); let message = create_authenticate_message( &application, Ctap1Flags::DontEnforceUpAndSign, diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 23f589d..662b5ce 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -595,6 +595,7 @@ pub struct PublicKeyCredentialSource { pub user_icon: Option, pub cred_blob: Option>, pub large_blob_key: Option>, + pub slot_id: Option, } // We serialize credentials for the persistent storage using CBOR maps. Each field of a credential @@ -613,6 +614,7 @@ enum PublicKeyCredentialSourceField { CredBlob = 10, LargeBlobKey = 11, PrivateKey = 12, + SlotId = 13, // When a field is removed, its tag should be reserved and not used for new fields. We document // those reserved tags below. // Reserved tags: @@ -639,6 +641,7 @@ impl From for cbor::Value { PublicKeyCredentialSourceField::CredBlob => credential.cred_blob, PublicKeyCredentialSourceField::LargeBlobKey => credential.large_blob_key, PublicKeyCredentialSourceField::PrivateKey => credential.private_key, + PublicKeyCredentialSourceField::SlotId => credential.slot_id.map(|x| x as u64), } } } @@ -661,6 +664,7 @@ impl TryFrom for PublicKeyCredentialSource { PublicKeyCredentialSourceField::CredBlob => cred_blob, PublicKeyCredentialSourceField::LargeBlobKey => large_blob_key, PublicKeyCredentialSourceField::PrivateKey => private_key, + PublicKeyCredentialSourceField::SlotId => slot_id, } = extract_map(cbor_value)?; } @@ -687,6 +691,12 @@ impl TryFrom for PublicKeyCredentialSource { .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?, (None, Some(k)) => k, }; + let slot_id = match slot_id.map(extract_unsigned).transpose()? { + Some(x) => Some( + usize::try_from(x).map_err(|_| Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?, + ), + None => None, + }; // 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 @@ -711,6 +721,7 @@ impl TryFrom for PublicKeyCredentialSource { user_icon, cred_blob, large_blob_key, + slot_id, }) } } @@ -2229,6 +2240,7 @@ mod test { user_icon: None, cred_blob: None, large_blob_key: None, + slot_id: None, }; assert_eq!( @@ -2291,6 +2303,16 @@ mod test { ..credential }; + assert_eq!( + PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), + Ok(credential.clone()) + ); + + let credential = PublicKeyCredentialSource { + slot_id: Some(1), + ..credential + }; + assert_eq!( PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), Ok(credential) @@ -2315,6 +2337,7 @@ mod test { user_icon: None, cred_blob: None, large_blob_key: None, + slot_id: None, }; let source_cbor = cbor_map! { @@ -2347,6 +2370,7 @@ mod test { user_icon: None, cred_blob: None, large_blob_key: None, + slot_id: None, }; let source_cbor = cbor_map! { diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index d224dd7..001786e 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -743,7 +743,7 @@ impl CtapState { Ok(()) } - fn check_cred_protect_for_listed_credential( + fn check_cred_protect( &mut self, credential: &Option, has_uv: bool, @@ -759,6 +759,29 @@ impl CtapState { } } + // This checks that either the credential contains no slot id and the expected slot id + // is 0, or the credential contains the expected slot id. + fn check_slot_id_matches( + &mut self, + credential: &PublicKeyCredentialSource, + slot_id: usize, + ) -> bool { + credential.slot_id.unwrap_or(0) == slot_id + } + + // Perform cred protect and slot id checks on a credential to determine whether it is + // "visible". + fn check_listed_credential_is_visible( + &mut self, + credential: &Option, + has_uv: bool, + slot_id: usize, + ) -> bool { + credential.is_some() + && self.check_cred_protect(credential, has_uv) + && self.check_slot_id_matches(credential.as_ref().unwrap(), slot_id) + } + fn process_make_credential( &mut self, env: &mut impl Env, @@ -865,12 +888,14 @@ impl CtapState { let rp_id_hash = Sha256::hash(rp_id.as_bytes()); if let Some(exclude_list) = exclude_list { for cred_desc in exclude_list { - if self.check_cred_protect_for_listed_credential( + if self.check_listed_credential_is_visible( &storage::find_credential(env, &rp_id, &cred_desc.key_id)?, has_uv, - ) || self.check_cred_protect_for_listed_credential( + slot_id, + ) || self.check_listed_credential_is_visible( &decrypt_credential_id(env, cred_desc.key_id, &rp_id_hash)?, has_uv, + slot_id, ) { // Perform this check, so bad actors can't brute force exclude_list // without user interaction. @@ -917,7 +942,6 @@ impl CtapState { // We decide on the algorithm early, but delay key creation since it takes time. // We rather do that later so all intermediate checks may return faster. let private_key = PrivateKey::new(env, algorithm); - // TODO: persist slot_id in the credential. let credential_id = if options.rk { let random_id = env.rng().gen_uniform_u8x32().to_vec(); let credential_source = PublicKeyCredentialSource { @@ -941,6 +965,7 @@ impl CtapState { .map(|s| truncate_to_char_boundary(&s, 64).to_string()), cred_blob, large_blob_key: large_blob_key.clone(), + slot_id: Some(slot_id), }; storage::store_credential(env, credential_source)?; random_id @@ -951,6 +976,7 @@ impl CtapState { &rp_id_hash, cred_protect_policy, cred_blob, + slot_id, )? }; @@ -1143,14 +1169,15 @@ impl CtapState { rp_id: &str, rp_id_hash: &[u8], has_uv: bool, + slot_id: usize, ) -> Result, Ctap2StatusCode> { for allowed_credential in allow_list { let credential = storage::find_credential(env, rp_id, &allowed_credential.key_id)?; - if self.check_cred_protect_for_listed_credential(&credential, has_uv) { + if self.check_listed_credential_is_visible(&credential, has_uv, slot_id) { return Ok(credential); } let credential = decrypt_credential_id(env, allowed_credential.key_id, rp_id_hash)?; - if self.check_cred_protect_for_listed_credential(&credential, has_uv) { + if self.check_listed_credential_is_visible(&credential, has_uv, slot_id) { return Ok(credential); } } @@ -1233,7 +1260,6 @@ impl CtapState { let slot_id = slot_id.ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; let rp_id_hash = Sha256::hash(rp_id.as_bytes()); - // TODO: check slot_id in the credential matches. let (credential, next_credential_keys) = if let Some(allow_list) = allow_list { ( self.get_any_credential_from_allow_list( @@ -1242,6 +1268,7 @@ impl CtapState { &rp_id, &rp_id_hash, has_uv, + slot_id, )?, vec![], ) @@ -1250,7 +1277,10 @@ impl CtapState { let iter = storage::iter_credentials(env, &mut iter_result)?; let mut stored_credentials: Vec<(usize, u64)> = iter .filter_map(|(key, credential)| { - if credential.rp_id == rp_id && (has_uv || credential.is_discoverable()) { + if credential.rp_id == rp_id + && (has_uv || credential.is_discoverable()) + && self.check_slot_id_matches(&credential, slot_id) + { Some((key, credential.creation_order)) } else { None @@ -1599,6 +1629,34 @@ mod test { #[cfg(feature = "vendor_hid")] const VENDOR_CHANNEL: Channel = Channel::VendorHid([0x12, 0x34, 0x56, 0x78]); + const DUMMY_CLIENT_DATA_HASH: [u8; 1] = [0xCD]; + + fn enable_pin_uv( + state: &mut CtapState, + env: &mut impl Env, + pin_uv_auth_protocol: PinUvAuthProtocol, + slot_id: usize, + client_data_hash: &[u8], + ) -> Result, Ctap2StatusCode> { + let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let client_pin = ClientPin::new_test( + env, + slot_id, + key_agreement_key, + pin_uv_auth_token, + pin_uv_auth_protocol, + ); + state.client_pin = client_pin; + storage::set_pin(env, slot_id, &[0x88; 16], 4)?; + + Ok(authenticate_pin_uv_auth_token( + &pin_uv_auth_token, + client_data_hash, + pin_uv_auth_protocol, + )) + } + fn check_make_response( make_credential_response: &Result, flags: u8, @@ -1696,7 +1754,7 @@ mod test { } fn create_minimal_make_credential_parameters() -> AuthenticatorMakeCredentialParameters { - let client_data_hash = vec![0xCD]; + let client_data_hash = DUMMY_CLIENT_DATA_HASH.to_vec(); let rp = PublicKeyCredentialRpEntity { rp_id: String::from("example.com"), rp_name: None, @@ -1727,6 +1785,16 @@ mod test { } } + fn add_pin_uv_to_make_credential_parameters( + params: &mut AuthenticatorMakeCredentialParameters, + pin_uv_auth_protocol: PinUvAuthProtocol, + pin_uv_auth_param: Vec, + ) { + params.options.uv = true; + params.pin_uv_auth_param = Some(pin_uv_auth_param); + params.pin_uv_auth_protocol = Some(pin_uv_auth_protocol); + } + fn create_make_credential_parameters_with_exclude_list( excluded_credential_id: &[u8], ) -> AuthenticatorMakeCredentialParameters { @@ -1844,6 +1912,7 @@ mod test { user_icon: None, cred_blob: None, large_blob_key: None, + slot_id: None, }; assert!(storage::store_credential(&mut env, excluded_credential_source).is_ok()); @@ -1855,6 +1924,116 @@ mod test { ); } + #[test] + fn test_process_make_credential_credential_excluded_multi_pin_slot_matched() { + let mut env = TestEnv::new(); + storage::_enable_multi_pin_for_test(&mut env).unwrap(); + + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + let excluded_private_key = PrivateKey::new_ecdsa(&mut env); + let excluded_credential_id = vec![0x01, 0x23, 0x45, 0x67]; + let slot_id = 1; + + let pin_uv_auth_protocol = PinUvAuthProtocol::V1; + let pin_uv_auth_param = enable_pin_uv( + &mut ctap_state, + &mut env, + pin_uv_auth_protocol, + slot_id, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(); + + let mut make_credential_params = + create_make_credential_parameters_with_exclude_list(&excluded_credential_id); + add_pin_uv_to_make_credential_parameters( + &mut make_credential_params, + pin_uv_auth_protocol, + pin_uv_auth_param, + ); + + let excluded_credential_source = PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id: excluded_credential_id, + private_key: excluded_private_key, + rp_id: String::from("example.com"), + user_handle: vec![], + user_display_name: None, + cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: None, + slot_id: Some(slot_id), + }; + assert!(storage::store_credential(&mut env, excluded_credential_source).is_ok()); + + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED) + ); + } + + #[test] + fn test_process_make_credential_credential_excluded_multi_pin_slot_mismatched() { + let mut env = TestEnv::new(); + storage::_enable_multi_pin_for_test(&mut env).unwrap(); + + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + let excluded_private_key = PrivateKey::new_ecdsa(&mut env); + let excluded_credential_id = vec![0x01, 0x23, 0x45, 0x67]; + let slot_id = 1; + + let pin_uv_auth_protocol = PinUvAuthProtocol::V1; + let pin_uv_auth_param = enable_pin_uv( + &mut ctap_state, + &mut env, + pin_uv_auth_protocol, + slot_id, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(); + + let mut make_credential_params = + create_make_credential_parameters_with_exclude_list(&excluded_credential_id); + add_pin_uv_to_make_credential_parameters( + &mut make_credential_params, + pin_uv_auth_protocol, + pin_uv_auth_param, + ); + + let excluded_credential_source = PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id: excluded_credential_id, + private_key: excluded_private_key, + rp_id: String::from("example.com"), + user_handle: vec![], + user_display_name: None, + cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: None, + // Doesn't match the current slot_id. + slot_id: Some(2), + }; + assert!(storage::store_credential(&mut env, excluded_credential_source).is_ok()); + + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + check_make_response( + &make_credential_response, + 0x45, + &storage::aaguid(&mut env).unwrap(), + 0x20, + &[], + ); + } + #[test] fn test_process_make_credential_credential_with_cred_protect() { let mut env = TestEnv::new(); @@ -1949,6 +2128,162 @@ mod test { assert!(make_credential_response.is_ok()); } + #[test] + fn test_process_make_credential_multi_pin() { + let mut env = TestEnv::new(); + storage::_enable_multi_pin_for_test(&mut env).unwrap(); + + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + let slot_id_1 = 1; + let pin_uv_auth_protocol = PinUvAuthProtocol::V1; + let mut pin_uv_auth_param = enable_pin_uv( + &mut ctap_state, + &mut env, + pin_uv_auth_protocol, + slot_id_1, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + add_pin_uv_to_make_credential_parameters( + &mut make_credential_params, + pin_uv_auth_protocol, + pin_uv_auth_param.clone(), + ); + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + assert!(make_credential_response.is_ok()); + + let mut iter_result = Ok(()); + let iter = storage::iter_credentials(&mut env, &mut iter_result).unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); + let credential_id = stored_credential.credential_id; + assert_eq!(stored_credential.slot_id, Some(slot_id_1)); + + make_credential_params = + create_make_credential_parameters_with_exclude_list(&credential_id); + pin_uv_auth_param = enable_pin_uv( + &mut ctap_state, + &mut env, + pin_uv_auth_protocol, + slot_id_1, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(); + add_pin_uv_to_make_credential_parameters( + &mut make_credential_params, + pin_uv_auth_protocol, + pin_uv_auth_param, + ); + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED) + ); + + make_credential_params = + create_make_credential_parameters_with_exclude_list(&credential_id); + let slot_id_2 = 2; + pin_uv_auth_param = enable_pin_uv( + &mut ctap_state, + &mut env, + pin_uv_auth_protocol, + slot_id_2, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(); + add_pin_uv_to_make_credential_parameters( + &mut make_credential_params, + pin_uv_auth_protocol, + pin_uv_auth_param, + ); + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + assert!(make_credential_response.is_ok()); + } + + #[test] + fn test_non_resident_process_make_credential_multi_pin() { + let mut env = TestEnv::new(); + storage::_enable_multi_pin_for_test(&mut env).unwrap(); + + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + let slot_id_1 = 1; + let pin_uv_auth_protocol = PinUvAuthProtocol::V1; + let mut pin_uv_auth_param = enable_pin_uv( + &mut ctap_state, + &mut env, + pin_uv_auth_protocol, + slot_id_1, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + add_pin_uv_to_make_credential_parameters( + &mut make_credential_params, + pin_uv_auth_protocol, + pin_uv_auth_param.clone(), + ); + make_credential_params.options.rk = false; + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + assert!(make_credential_response.is_ok()); + + let credential_id = parse_credential_id_from_non_resident_make_credential_response( + &mut env, + make_credential_response.unwrap(), + ); + + make_credential_params = + create_make_credential_parameters_with_exclude_list(&credential_id); + pin_uv_auth_param = enable_pin_uv( + &mut ctap_state, + &mut env, + pin_uv_auth_protocol, + slot_id_1, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(); + add_pin_uv_to_make_credential_parameters( + &mut make_credential_params, + pin_uv_auth_protocol, + pin_uv_auth_param, + ); + make_credential_params.options.rk = false; + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED) + ); + + make_credential_params = + create_make_credential_parameters_with_exclude_list(&credential_id); + let slot_id_2 = 2; + pin_uv_auth_param = enable_pin_uv( + &mut ctap_state, + &mut env, + pin_uv_auth_protocol, + slot_id_2, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(); + add_pin_uv_to_make_credential_parameters( + &mut make_credential_params, + pin_uv_auth_protocol, + pin_uv_auth_param, + ); + make_credential_params.options.rk = false; + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + assert!(make_credential_response.is_ok()); + } + #[test] fn test_process_make_credential_hmac_secret() { let mut env = TestEnv::new(); @@ -2148,30 +2483,22 @@ mod test { pin_uv_auth_protocol: PinUvAuthProtocol, ) { let mut env = TestEnv::new(); - let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); - let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; - let client_pin = ClientPin::new_test( - &mut env, - 0, - key_agreement_key, - pin_uv_auth_token, - pin_uv_auth_protocol, - ); - let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); - ctap_state.client_pin = client_pin; - storage::set_pin(&mut env, 0, &[0x88; 16], 4).unwrap(); - let client_data_hash = [0xCD]; - let pin_uv_auth_param = authenticate_pin_uv_auth_token( - &pin_uv_auth_token, - &client_data_hash, + let pin_uv_auth_param = enable_pin_uv( + &mut ctap_state, + &mut env, pin_uv_auth_protocol, - ); + 0, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(); let mut make_credential_params = create_minimal_make_credential_parameters(); - make_credential_params.options.uv = true; - make_credential_params.pin_uv_auth_param = Some(pin_uv_auth_param); - make_credential_params.pin_uv_auth_protocol = Some(pin_uv_auth_protocol); + add_pin_uv_to_make_credential_parameters( + &mut make_credential_params, + pin_uv_auth_protocol, + pin_uv_auth_param, + ); let make_credential_response = ctap_state.process_make_credential( &mut env, make_credential_params.clone(), @@ -2460,9 +2787,10 @@ mod test { ); } - fn check_assertion_response( + fn check_assertion_response_with_flags( response: Result, expected_user_id: Vec, + flags: u8, signature_counter: u32, expected_number_of_credentials: Option, ) { @@ -2475,13 +2803,28 @@ mod test { check_assertion_response_with_user( response, Some(expected_user), - 0x00, + flags, signature_counter, expected_number_of_credentials, &[], ); } + fn check_assertion_response( + response: Result, + expected_user_id: Vec, + signature_counter: u32, + expected_number_of_credentials: Option, + ) { + check_assertion_response_with_flags( + response, + expected_user_id, + 0x00, + signature_counter, + expected_number_of_credentials, + ) + } + #[test] fn test_resident_process_get_assertion() { let mut env = TestEnv::new(); @@ -2494,7 +2837,7 @@ mod test { let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), allow_list: None, extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { @@ -2555,7 +2898,7 @@ mod test { let allow_list = credential_descriptor.map(|c| vec![c]); AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), allow_list, extensions: get_extensions, options: GetAssertionOptions { @@ -2709,12 +3052,13 @@ mod test { user_icon: None, cred_blob: None, large_blob_key: None, + slot_id: None, }; assert!(storage::store_credential(&mut env, credential).is_ok()); let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), allow_list: None, extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { @@ -2737,7 +3081,7 @@ mod test { let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), allow_list: Some(vec![cred_desc.clone()]), extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { @@ -2769,12 +3113,13 @@ mod test { user_icon: None, cred_blob: None, large_blob_key: None, + slot_id: None, }; assert!(storage::store_credential(&mut env, credential).is_ok()); let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), allow_list: Some(vec![cred_desc]), extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { @@ -2819,7 +3164,7 @@ mod test { }; let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), allow_list: Some(vec![cred_desc]), extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { @@ -2855,7 +3200,7 @@ mod test { }; let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), allow_list: Some(vec![cred_desc]), extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { @@ -2877,6 +3222,294 @@ mod test { ); } + #[test] + fn test_resident_process_get_assertion_multi_pin() { + let mut env = TestEnv::new(); + storage::_enable_multi_pin_for_test(&mut env).unwrap(); + + let private_key = PrivateKey::new_ecdsa(&mut env); + let credential_id = env.rng().gen_uniform_u8x32().to_vec(); + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + let slot_id_1 = 1; + let slot_id_2 = 2; + + let cred_desc = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: credential_id.clone(), + transports: None, + }; + 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![0x1D], + user_display_name: None, + cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: None, + slot_id: Some(slot_id_1), + }; + assert!(storage::store_credential(&mut env, credential).is_ok()); + + let pin_uv_auth_protocol = PinUvAuthProtocol::V1; + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), + allow_list: Some(vec![cred_desc.clone()]), + extensions: GetAssertionExtensions::default(), + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: Some( + enable_pin_uv( + &mut ctap_state, + &mut env, + pin_uv_auth_protocol, + slot_id_2, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(), + ), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), + }; + let get_assertion_response = ctap_state.process_get_assertion( + &mut env, + get_assertion_params, + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + 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: DUMMY_CLIENT_DATA_HASH.to_vec(), + allow_list: Some(vec![cred_desc.clone()]), + extensions: GetAssertionExtensions::default(), + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: Some( + enable_pin_uv( + &mut ctap_state, + &mut env, + pin_uv_auth_protocol, + slot_id_1, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(), + ), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), + }; + let get_assertion_response = ctap_state.process_get_assertion( + &mut env, + get_assertion_params, + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + let signature_counter = storage::global_signature_counter(&mut env, slot_id_1).unwrap(); + check_assertion_response_with_flags( + get_assertion_response, + vec![0x1D], + UV_FLAG, + signature_counter, + None, + ); + + // No slot_id should be treated as a credential of slot 0, so the credential shouldn't be found + // if we're using slot 1's UV. + let credential = PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id, + private_key, + rp_id: String::from("example.com"), + user_handle: vec![0x1D], + user_display_name: None, + cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: None, + slot_id: None, + }; + assert!(storage::store_credential(&mut env, credential).is_ok()); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), + allow_list: Some(vec![cred_desc.clone()]), + extensions: GetAssertionExtensions::default(), + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: Some( + enable_pin_uv( + &mut ctap_state, + &mut env, + pin_uv_auth_protocol, + slot_id_1, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(), + ), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), + }; + let get_assertion_response = ctap_state.process_get_assertion( + &mut env, + get_assertion_params, + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + 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: DUMMY_CLIENT_DATA_HASH.to_vec(), + allow_list: Some(vec![cred_desc]), + extensions: GetAssertionExtensions::default(), + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: Some( + enable_pin_uv( + &mut ctap_state, + &mut env, + pin_uv_auth_protocol, + 0, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(), + ), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), + }; + let get_assertion_response = ctap_state.process_get_assertion( + &mut env, + get_assertion_params, + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + let signature_counter = storage::global_signature_counter(&mut env, 0).unwrap(); + check_assertion_response_with_flags( + get_assertion_response, + vec![0x1D], + UV_FLAG, + signature_counter, + None, + ); + } + + #[test] + fn test_non_resident_process_get_assertion_multi_pin() { + let mut env = TestEnv::new(); + storage::_enable_multi_pin_for_test(&mut env).unwrap(); + + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + let pin_uv_auth_protocol = PinUvAuthProtocol::V1; + let slot_id_1 = 1; + let slot_id_2 = 2; + + let mut make_credential_params = create_minimal_make_credential_parameters(); + add_pin_uv_to_make_credential_parameters( + &mut make_credential_params, + pin_uv_auth_protocol, + enable_pin_uv( + &mut ctap_state, + &mut env, + pin_uv_auth_protocol, + slot_id_1, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(), + ); + make_credential_params.options.rk = false; + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + assert!(make_credential_response.is_ok()); + let credential_id = parse_credential_id_from_non_resident_make_credential_response( + &mut env, + make_credential_response.unwrap(), + ); + let cred_desc = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: credential_id, + transports: None, + }; + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), + allow_list: Some(vec![cred_desc.clone()]), + extensions: GetAssertionExtensions::default(), + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: Some( + enable_pin_uv( + &mut ctap_state, + &mut env, + pin_uv_auth_protocol, + slot_id_1, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(), + ), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), + }; + let get_assertion_response = ctap_state.process_get_assertion( + &mut env, + get_assertion_params, + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + assert!(get_assertion_response.is_ok()); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), + allow_list: Some(vec![cred_desc]), + extensions: GetAssertionExtensions::default(), + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: Some( + enable_pin_uv( + &mut ctap_state, + &mut env, + pin_uv_auth_protocol, + slot_id_2, + &DUMMY_CLIENT_DATA_HASH, + ) + .unwrap(), + ), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), + }; + let get_assertion_response = ctap_state.process_get_assertion( + &mut env, + get_assertion_params, + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS), + ); + } + #[test] fn test_process_get_assertion_with_cred_blob() { let mut env = TestEnv::new(); @@ -2897,6 +3530,7 @@ mod test { user_icon: None, cred_blob: Some(vec![0xCB]), large_blob_key: None, + slot_id: None, }; assert!(storage::store_credential(&mut env, credential).is_ok()); @@ -2906,7 +3540,7 @@ mod test { }; let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), allow_list: None, extensions, options: GetAssertionOptions { @@ -2975,7 +3609,7 @@ mod test { }; let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), allow_list: Some(vec![cred_desc]), extensions, options: GetAssertionOptions { @@ -3024,6 +3658,7 @@ mod test { user_icon: None, cred_blob: None, large_blob_key: Some(vec![0x1C; 32]), + slot_id: None, }; assert!(storage::store_credential(&mut env, credential).is_ok()); @@ -3033,7 +3668,7 @@ mod test { }; let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), allow_list: None, extensions, options: GetAssertionOptions { @@ -3100,7 +3735,7 @@ mod test { ctap_state.client_pin = client_pin; // The PIN length is outside of the test scope and most likely incorrect. storage::set_pin(&mut env, 0, &[0u8; 16], 4).unwrap(); - let client_data_hash = vec![0xCD]; + let client_data_hash = DUMMY_CLIENT_DATA_HASH.to_vec(); let pin_uv_auth_param = authenticate_pin_uv_auth_token( &pin_uv_auth_token, &client_data_hash, @@ -3194,7 +3829,7 @@ mod test { let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), allow_list: None, extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { @@ -3255,7 +3890,7 @@ mod test { let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), allow_list: None, extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { @@ -3315,6 +3950,7 @@ mod test { user_icon: None, cred_blob: None, large_blob_key: None, + slot_id: None, }; assert!(storage::store_credential(&mut env, credential_source).is_ok()); assert!(storage::count_credentials(&mut env).unwrap() > 0); @@ -3743,7 +4379,7 @@ mod test { let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), allow_list: None, extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { @@ -3820,6 +4456,7 @@ mod test { user_icon: Some("icon".to_string()), cred_blob: None, large_blob_key: None, + slot_id: None, }; let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); @@ -3927,7 +4564,7 @@ mod test { let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash: DUMMY_CLIENT_DATA_HASH.to_vec(), allow_list: None, extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 14a943e..a842a80 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -623,9 +623,9 @@ pub fn has_multi_pin(env: &mut impl Env) -> Result { } // TODO: Call this in config_commands after the whole multi-PIN feature is ready. -// Before that, this function should stay private, only for testing purpose. +// Before that, this function only be used for testing purpose. /// Enables multi-PIN, when disabled. -fn _enable_multi_pin(env: &mut impl Env) -> Result<(), Ctap2StatusCode> { +pub fn _enable_multi_pin_for_test(env: &mut impl Env) -> Result<(), Ctap2StatusCode> { if !has_multi_pin(env)? { env.store().insert(key::MULTI_PIN, &[])?; } @@ -777,6 +777,7 @@ mod test { user_icon: None, cred_blob: None, large_blob_key: None, + slot_id: None, } } @@ -973,6 +974,7 @@ mod test { user_icon: None, cred_blob: None, large_blob_key: None, + slot_id: None, }; assert_eq!(found_credential, Some(expected_credential)); } @@ -1515,7 +1517,7 @@ mod test { let mut env = TestEnv::new(); assert!(!has_multi_pin(&mut env).unwrap()); - assert_eq!(_enable_multi_pin(&mut env), Ok(())); + assert_eq!(_enable_multi_pin_for_test(&mut env), Ok(())); assert!(has_multi_pin(&mut env).unwrap()); } @@ -1536,6 +1538,7 @@ mod test { user_icon: Some(String::from("icon")), cred_blob: Some(vec![0xCB]), large_blob_key: Some(vec![0x1B]), + slot_id: Some(1), }; let serialized = serialize_credential(credential.clone()).unwrap(); let reconstructed = deserialize_credential(&serialized).unwrap();