From 14189a398addfc273e179b84d43ecce4f3666258 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 20 Jan 2021 18:46:38 +0100 Subject: [PATCH] implements the credBlob extensions --- README.md | 2 + src/ctap/command.rs | 42 ++-- src/ctap/credential_management.rs | 2 + src/ctap/data_formats.rs | 47 +++- src/ctap/mod.rs | 359 +++++++++++++++++++++++------- src/ctap/storage.rs | 6 +- 6 files changed, 356 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index 0691fb8..da46d7f 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,8 @@ a few things you can personalize: allows some relying parties to read the minimum PIN length by default. The latter allows storing more relying parties that may check the minimum PIN length. +1. Increase the `MAX_CRED_BLOB_LENGTH` in `ctap/mod.rs`, if you expect blobs + bigger than the default value. ### 3D printed enclosure diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 620a06f..128c4b4 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -147,8 +147,9 @@ pub struct AuthenticatorMakeCredentialParameters { pub user: PublicKeyCredentialUserEntity, pub pub_key_cred_params: Vec, pub exclude_list: Option>, - pub extensions: Option, - // Even though options are optional, we can use the default if not present. + // Extensions are optional, but we can use defaults for all missing fields. + pub extensions: MakeCredentialExtensions, + // Same for options, use defaults when not present. pub options: MakeCredentialOptions, pub pin_uv_auth_param: Option>, pub pin_uv_auth_protocol: Option, @@ -198,15 +199,13 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { let extensions = extensions .map(MakeCredentialExtensions::try_from) - .transpose()?; + .transpose()? + .unwrap_or_default(); - let options = match options { - Some(entry) => MakeCredentialOptions::try_from(entry)?, - None => MakeCredentialOptions { - rk: false, - uv: false, - }, - }; + let options = options + .map(MakeCredentialOptions::try_from) + .transpose()? + .unwrap_or_default(); let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; @@ -230,8 +229,9 @@ pub struct AuthenticatorGetAssertionParameters { pub rp_id: String, pub client_data_hash: Vec, pub allow_list: Option>, - pub extensions: Option, - // Even though options are optional, we can use the default if not present. + // Extensions are optional, but we can use defaults for all missing fields. + pub extensions: GetAssertionExtensions, + // Same for options, use defaults when not present. pub options: GetAssertionOptions, pub pin_uv_auth_param: Option>, pub pin_uv_auth_protocol: Option, @@ -272,15 +272,13 @@ impl TryFrom for AuthenticatorGetAssertionParameters { let extensions = extensions .map(GetAssertionExtensions::try_from) - .transpose()?; + .transpose()? + .unwrap_or_default(); - let options = match options { - Some(entry) => GetAssertionOptions::try_from(entry)?, - None => GetAssertionOptions { - up: true, - uv: false, - }, - }; + let options = options + .map(GetAssertionOptions::try_from) + .transpose()? + .unwrap_or_default(); let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; @@ -545,7 +543,7 @@ mod test { user, pub_key_cred_params: vec![ES256_CRED_PARAM], exclude_list: Some(vec![]), - extensions: None, + extensions: MakeCredentialExtensions::default(), options, pin_uv_auth_param: Some(vec![0x12, 0x34]), pin_uv_auth_protocol: Some(1), @@ -591,7 +589,7 @@ mod test { rp_id, client_data_hash, allow_list: Some(vec![pub_key_cred_descriptor]), - extensions: None, + extensions: GetAssertionExtensions::default(), options, pin_uv_auth_param: Some(vec![0x12, 0x34]), pin_uv_auth_protocol: Some(1), diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 25fe3b9..7681fa1 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -80,6 +80,7 @@ fn enumerate_credentials_response( creation_order: _, user_name, user_icon, + cred_blob: _, } = credential; let user = PublicKeyCredentialUserEntity { user_id: user_handle, @@ -346,6 +347,7 @@ mod test { creation_order: 0, user_name: Some("name".to_string()), user_icon: Some("icon".to_string()), + cred_blob: None, } } diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 9d4d10f..da992f8 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -275,11 +275,13 @@ impl From for cbor::Value { } } +#[derive(Default)] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] pub struct MakeCredentialExtensions { pub hmac_secret: bool, pub cred_protect: Option, pub min_pin_length: bool, + pub cred_blob: Option>, } impl TryFrom for MakeCredentialExtensions { @@ -288,6 +290,7 @@ impl TryFrom for MakeCredentialExtensions { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { + "credBlob" => cred_blob, "credProtect" => cred_protect, "hmac-secret" => hmac_secret, "minPinLength" => min_pin_length, @@ -299,17 +302,21 @@ impl TryFrom for MakeCredentialExtensions { .map(CredentialProtectionPolicy::try_from) .transpose()?; let min_pin_length = min_pin_length.map_or(Ok(false), extract_bool)?; + let cred_blob = cred_blob.map(extract_byte_string).transpose()?; Ok(Self { hmac_secret, cred_protect, min_pin_length, + cred_blob, }) } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Default)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct GetAssertionExtensions { pub hmac_secret: Option, + pub cred_blob: bool, } impl TryFrom for GetAssertionExtensions { @@ -318,6 +325,7 @@ impl TryFrom for GetAssertionExtensions { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { + "credBlob" => cred_blob, "hmac-secret" => hmac_secret, } = extract_map(cbor_value)?; } @@ -325,7 +333,11 @@ impl TryFrom for GetAssertionExtensions { let hmac_secret = hmac_secret .map(GetAssertionHmacSecretInput::try_from) .transpose()?; - Ok(Self { hmac_secret }) + let cred_blob = cred_blob.map_or(Ok(false), extract_bool)?; + Ok(Self { + hmac_secret, + cred_blob, + }) } } @@ -361,6 +373,7 @@ impl TryFrom for GetAssertionHmacSecretInput { } // Even though options are optional, we can use the default if not present. +#[derive(Default)] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct MakeCredentialOptions { pub rk: bool, @@ -400,6 +413,15 @@ pub struct GetAssertionOptions { pub uv: bool, } +impl Default for GetAssertionOptions { + fn default() -> Self { + GetAssertionOptions { + up: true, + uv: false, + } + } +} + impl TryFrom for GetAssertionOptions { type Error = Ctap2StatusCode; @@ -523,6 +545,7 @@ pub struct PublicKeyCredentialSource { pub creation_order: u64, pub user_name: Option, pub user_icon: Option, + pub cred_blob: Option>, } // We serialize credentials for the persistent storage using CBOR maps. Each field of a credential @@ -537,6 +560,7 @@ enum PublicKeyCredentialSourceField { CreationOrder = 7, UserName = 8, UserIcon = 9, + CredBlob = 10, // When a field is removed, its tag should be reserved and not used for new fields. We document // those reserved tags below. // Reserved tags: @@ -563,6 +587,7 @@ impl From for cbor::Value { PublicKeyCredentialSourceField::CreationOrder => credential.creation_order, PublicKeyCredentialSourceField::UserName => credential.user_name, PublicKeyCredentialSourceField::UserIcon => credential.user_icon, + PublicKeyCredentialSourceField::CredBlob => credential.cred_blob, } } } @@ -582,6 +607,7 @@ impl TryFrom for PublicKeyCredentialSource { PublicKeyCredentialSourceField::CreationOrder => creation_order, PublicKeyCredentialSourceField::UserName => user_name, PublicKeyCredentialSourceField::UserIcon => user_icon, + PublicKeyCredentialSourceField::CredBlob => cred_blob, } = extract_map(cbor_value)?; } @@ -601,6 +627,7 @@ impl TryFrom for PublicKeyCredentialSource { let creation_order = creation_order.map(extract_unsigned).unwrap_or(Ok(0))?; let user_name = user_name.map(extract_text_string).transpose()?; let user_icon = user_icon.map(extract_text_string).transpose()?; + let cred_blob = cred_blob.map(extract_byte_string).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: @@ -622,6 +649,7 @@ impl TryFrom for PublicKeyCredentialSource { creation_order, user_name, user_icon, + cred_blob, }) } } @@ -1493,12 +1521,14 @@ mod test { "hmac-secret" => true, "credProtect" => CredentialProtectionPolicy::UserVerificationRequired, "minPinLength" => true, + "credBlob" => vec![0xCB], }; let extensions = MakeCredentialExtensions::try_from(cbor_extensions); let expected_extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), min_pin_length: true, + cred_blob: Some(vec![0xCB]), }; assert_eq!(extensions, Ok(expected_extensions)); } @@ -1515,6 +1545,7 @@ mod test { 2 => vec![0x02; 32], 3 => vec![0x03; 16], }, + "credBlob" => true, }; let extensions = GetAssertionExtensions::try_from(cbor_extensions); let expected_input = GetAssertionHmacSecretInput { @@ -1524,6 +1555,7 @@ mod test { }; let expected_extensions = GetAssertionExtensions { hmac_secret: Some(expected_input), + cred_blob: true, }; assert_eq!(extensions, Ok(expected_extensions)); } @@ -1816,6 +1848,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert_eq!( @@ -1858,6 +1891,16 @@ mod test { ..credential }; + assert_eq!( + PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), + Ok(credential.clone()) + ); + + let credential = PublicKeyCredentialSource { + cred_blob: Some(vec![0xCB]), + ..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 8ac324c..6ffd108 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -35,7 +35,7 @@ use self::command::{ use self::config_command::process_config; use self::credential_management::process_credential_management; use self::data_formats::{ - AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput, + AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionExtensions, PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, @@ -47,17 +47,18 @@ use self::response::{ AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, }; use self::status_code::Ctap2StatusCode; -use self::storage::PersistentStore; +use self::storage::{PersistentStore, MAX_RP_IDS_LENGTH}; use self::timed_permission::TimedPermission; #[cfg(feature = "with_ctap1")] use self::timed_permission::U2fUserPresenceState; +use alloc::boxed::Box; use alloc::collections::BTreeMap; use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; use arrayref::array_ref; use byteorder::{BigEndian, ByteOrder}; -use cbor::{cbor_map, cbor_map_options}; +use cbor::cbor_map_options; #[cfg(feature = "debug_ctap")] use core::fmt::Write; use crypto::cbc::{cbc_decrypt, cbc_encrypt}; @@ -124,6 +125,8 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa // - Some(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList) // - Some(CredentialProtectionPolicy::UserVerificationRequired) const DEFAULT_CRED_PROTECT: Option = None; +// Maximum size stored with the credBlob extension. Must be at least 32. +const MAX_CRED_BLOB_LENGTH: usize = 32; // Checks the PIN protocol parameter against all supported versions. pub fn check_pin_uv_auth_protocol( @@ -154,7 +157,7 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str { pub struct AssertionInput { client_data_hash: Vec, auth_data: Vec, - hmac_secret_input: Option, + extensions: GetAssertionExtensions, has_uv: bool, } @@ -168,7 +171,7 @@ pub struct AssertionState { /// Stores which command currently holds state for subsequent calls. pub enum StatefulCommand { Reset, - GetAssertion(AssertionState), + GetAssertion(Box), EnumerateRps(usize), EnumerateCredentials(Vec), } @@ -419,6 +422,7 @@ where creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, })) } @@ -558,27 +562,31 @@ where } let rp_id = rp.rp_id; - let (use_hmac_extension, cred_protect_policy, min_pin_length) = - if let Some(extensions) = extensions { - let mut cred_protect = extensions.cred_protect; - if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) - < DEFAULT_CRED_PROTECT - .unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) - { - cred_protect = DEFAULT_CRED_PROTECT; - } - let min_pin_length = extensions.min_pin_length - && self - .persistent_store - .min_pin_length_rp_ids()? - .contains(&rp_id); - (extensions.hmac_secret, cred_protect, min_pin_length) - } else { - (false, DEFAULT_CRED_PROTECT, false) - }; - - let has_extension_output = - use_hmac_extension || cred_protect_policy.is_some() || min_pin_length; + let mut cred_protect_policy = extensions.cred_protect; + if cred_protect_policy.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + < DEFAULT_CRED_PROTECT.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + { + cred_protect_policy = DEFAULT_CRED_PROTECT; + } + let min_pin_length = extensions.min_pin_length + && self + .persistent_store + .min_pin_length_rp_ids()? + .contains(&rp_id); + // None for no input, false for invalid input, true for valid input. + let has_cred_blob_output = extensions.cred_blob.is_some(); + let cred_blob = extensions + .cred_blob + .filter(|c| options.rk && c.len() <= MAX_CRED_BLOB_LENGTH); + let cred_blob_output = if has_cred_blob_output { + Some(cred_blob.is_some()) + } else { + None + }; + let has_extension_output = extensions.hmac_secret + || cred_protect_policy.is_some() + || min_pin_length + || has_cred_blob_output; let rp_id_hash = Sha256::hash(rp_id.as_bytes()); if let Some(exclude_list) = exclude_list { @@ -656,6 +664,7 @@ where user_icon: user .user_icon .map(|s| truncate_to_char_boundary(&s, 64).to_string()), + cred_blob, }; self.persistent_store.store_credential(credential_source)?; random_id @@ -675,7 +684,11 @@ where return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } if has_extension_output { - let hmac_secret_output = if use_hmac_extension { Some(true) } else { None }; + let hmac_secret_output = if extensions.hmac_secret { + Some(true) + } else { + None + }; let min_pin_length_output = if min_pin_length { Some(self.persistent_store.min_pin_length()? as u64) } else { @@ -685,6 +698,7 @@ where "hmac-secret" => hmac_secret_output, "credProtect" => cred_protect_policy, "minPinLength" => min_pin_length_output, + "credBlob" => cred_blob_output, }; if !cbor::write(extensions_output, &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); @@ -754,18 +768,30 @@ where let AssertionInput { client_data_hash, mut auth_data, - hmac_secret_input, + extensions, has_uv, } = assertion_input; // Process extensions. - if let Some(hmac_secret_input) = hmac_secret_input { - let cred_random = self.generate_cred_random(&credential.private_key, has_uv)?; - let encrypted_output = self - .pin_protocol_v1 - .process_hmac_secret(hmac_secret_input, &cred_random)?; - let extensions_output = cbor_map! { + if extensions.hmac_secret.is_some() || extensions.cred_blob { + let encrypted_output = if let Some(hmac_secret_input) = extensions.hmac_secret { + let cred_random = self.generate_cred_random(&credential.private_key, has_uv)?; + Some( + self.pin_protocol_v1 + .process_hmac_secret(hmac_secret_input, &cred_random)?, + ) + } else { + None + }; + // This could be written more nicely with `then_some` when stable. + let cred_blob = if extensions.cred_blob { + Some(credential.cred_blob.unwrap_or_default()) + } else { + None + }; + let extensions_output = cbor_map_options! { "hmac-secret" => encrypted_output, + "credBlob" => cred_blob, }; if !cbor::write(extensions_output, &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); @@ -854,8 +880,7 @@ where self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?; - let hmac_secret_input = extensions.map(|e| e.hmac_secret).flatten(); - if hmac_secret_input.is_some() && !options.up { + if extensions.hmac_secret.is_some() && !options.up { // The extension is actually supported, but we need user presence. return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION); } @@ -891,7 +916,7 @@ where if options.up { flags |= UP_FLAG; } - if hmac_secret_input.is_some() { + if extensions.hmac_secret.is_some() || extensions.cred_blob { flags |= ED_FLAG; } @@ -939,17 +964,17 @@ where let assertion_input = AssertionInput { client_data_hash, auth_data: self.generate_auth_data(&rp_id_hash, flags)?, - hmac_secret_input, + extensions, has_uv, }; let number_of_credentials = if next_credential_keys.is_empty() { None } else { let number_of_credentials = Some(next_credential_keys.len() + 1); - let assertion_state = StatefulCommand::GetAssertion(AssertionState { + let assertion_state = StatefulCommand::GetAssertion(Box::new(AssertionState { assertion_input: assertion_input.clone(), next_credential_keys, - }); + })); self.stateful_command_permission .set_command(now, assertion_state); number_of_credentials @@ -993,22 +1018,21 @@ where String::from("hmac-secret"), String::from("credProtect"), String::from("minPinLength"), + String::from("credBlob"), ]), aaguid: self.persistent_store.aaguid()?, options: Some(options_map), max_msg_size: Some(1024), pin_protocols: Some(vec![PIN_PROTOCOL_VERSION]), max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), - // TODO(#106) update with version 2.1 of HMAC-secret max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), transports: Some(vec![AuthenticatorTransport::Usb]), algorithms: Some(vec![ES256_CRED_PARAM]), default_cred_protect: DEFAULT_CRED_PROTECT, min_pin_length: self.persistent_store.min_pin_length()?, firmware_version: None, - max_cred_blob_length: None, - // TODO(kaczmarczyck) update when extension is implemented - max_rp_ids_for_set_min_pin_length: None, + max_cred_blob_length: Some(MAX_CRED_BLOB_LENGTH as u64), + max_rp_ids_for_set_min_pin_length: Some(MAX_RP_IDS_LENGTH as u64), remaining_discoverable_credentials: Some( self.persistent_store.remaining_credentials()? as u64, ), @@ -1149,11 +1173,11 @@ where mod test { use super::command::AuthenticatorAttestationMaterial; use super::data_formats::{ - CoseKey, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, + CoseKey, GetAssertionHmacSecretInput, GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::*; - use cbor::cbor_array; + use cbor::{cbor_array, cbor_map}; use crypto::rng256::ThreadRng256; const CLOCK_FREQUENCY_HZ: usize = 32768; @@ -1209,7 +1233,7 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); - let mut expected_response = vec![0x00, 0xAB, 0x01]; + let mut expected_response = vec![0x00, 0xAD, 0x01]; // The version array differs with CTAP1, always including 2.0 and 2.1. #[cfg(not(feature = "with_ctap1"))] let version_count = 2; @@ -1221,10 +1245,11 @@ mod test { expected_response.extend( [ 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, 0x6C, 0x46, 0x49, 0x44, 0x4F, - 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x83, 0x6B, 0x68, 0x6D, 0x61, + 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x84, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x6B, 0x63, 0x72, 0x65, 0x64, 0x50, 0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, 0x6C, 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, - 0x65, 0x6E, 0x67, 0x74, 0x68, 0x03, 0x50, + 0x65, 0x6E, 0x67, 0x74, 0x68, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, + 0x03, 0x50, ] .iter(), ); @@ -1237,7 +1262,7 @@ mod test { 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x08, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, - 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x14, 0x18, 0x96, + 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x0F, 0x18, 0x20, 0x10, 0x08, 0x14, 0x18, 0x96, ] .iter(), ); @@ -1269,7 +1294,7 @@ mod test { user, pub_key_cred_params, exclude_list: None, - extensions: None, + extensions: MakeCredentialExtensions::default(), options, pin_uv_auth_param: None, pin_uv_auth_protocol: None, @@ -1293,11 +1318,12 @@ mod test { fn create_make_credential_parameters_with_cred_protect_policy( policy: CredentialProtectionPolicy, ) -> AuthenticatorMakeCredentialParameters { - let extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: false, cred_protect: Some(policy), min_pin_length: false, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; make_credential_params @@ -1380,6 +1406,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert!(ctap_state .persistent_store @@ -1458,11 +1485,12 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - let extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: None, min_pin_length: false, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; make_credential_params.extensions = extensions; @@ -1487,11 +1515,12 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - let extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: None, min_pin_length: false, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; let make_credential_response = @@ -1516,11 +1545,12 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // First part: The extension is ignored, since the RP ID is not on the list. - let extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: false, cred_protect: None, min_pin_length: true, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; let make_credential_response = @@ -1541,11 +1571,12 @@ mod test { Ok(()) ); - let extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: false, cred_protect: None, min_pin_length: true, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; let make_credential_response = @@ -1563,6 +1594,82 @@ mod test { ); } + #[test] + fn test_process_make_credential_cred_blob_ok() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let extensions = MakeCredentialExtensions { + hmac_secret: false, + cred_protect: None, + min_pin_length: false, + cred_blob: Some(vec![0xCB]), + }; + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let expected_extension_cbor = [ + 0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0xF5, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &expected_extension_cbor, + ); + + let mut iter_result = Ok(()); + let iter = ctap_state + .persistent_store + .iter_credentials(&mut iter_result) + .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); + assert_eq!(stored_credential.cred_blob, Some(vec![0xCB])); + } + + #[test] + fn test_process_make_credential_cred_blob_too_big() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let extensions = MakeCredentialExtensions { + hmac_secret: false, + cred_protect: None, + min_pin_length: false, + cred_blob: Some(vec![0xCB; MAX_CRED_BLOB_LENGTH + 1]), + }; + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let expected_extension_cbor = [ + 0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0xF4, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &expected_extension_cbor, + ); + + let mut iter_result = Ok(()); + let iter = ctap_state + .persistent_store + .iter_credentials(&mut iter_result) + .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); + assert_eq!(stored_credential.cred_blob, None); + } + #[test] fn test_process_make_credential_cancelled() { let mut rng = ThreadRng256 {}; @@ -1586,6 +1693,7 @@ mod test { flags: u8, signature_counter: u32, expected_number_of_credentials: Option, + expected_extension_cbor: &[u8], ) { match response.unwrap() { ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { @@ -1605,6 +1713,7 @@ mod test { &mut expected_auth_data[signature_counter_position..], signature_counter, ); + expected_auth_data.extend(expected_extension_cbor); assert_eq!(auth_data, expected_auth_data); assert_eq!(user, Some(expected_user)); assert_eq!(number_of_credentials, expected_number_of_credentials); @@ -1613,6 +1722,29 @@ mod test { } } + fn check_assertion_response_with_extension( + response: Result, + expected_user_id: Vec, + signature_counter: u32, + expected_number_of_credentials: Option, + expected_extension_cbor: &[u8], + ) { + let expected_user = PublicKeyCredentialUserEntity { + user_id: expected_user_id, + user_name: None, + user_display_name: None, + user_icon: None, + }; + check_assertion_response_with_user( + response, + expected_user, + 0x80, + signature_counter, + expected_number_of_credentials, + expected_extension_cbor, + ); + } + fn check_assertion_response( response: Result, expected_user_id: Vec, @@ -1631,6 +1763,7 @@ mod test { 0x00, signature_counter, expected_number_of_credentials, + &[], ); } @@ -1649,7 +1782,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1676,11 +1809,12 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - let make_extensions = Some(MakeCredentialExtensions { + let make_extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: None, min_pin_length: false, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; make_credential_params.extensions = make_extensions; @@ -1704,9 +1838,10 @@ mod test { salt_enc: vec![0x02; 32], salt_auth: vec![0x03; 16], }; - let get_extensions = Some(GetAssertionExtensions { + let get_extensions = GetAssertionExtensions { hmac_secret: Some(hmac_secret_input), - }); + cred_blob: false, + }; let cred_desc = PublicKeyCredentialDescriptor { key_type: PublicKeyCredentialType::PublicKey, @@ -1744,11 +1879,12 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - let make_extensions = Some(MakeCredentialExtensions { + let make_extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: None, min_pin_length: false, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = make_extensions; assert!(ctap_state @@ -1761,9 +1897,10 @@ mod test { salt_enc: vec![0x02; 32], salt_auth: vec![0x03; 16], }; - let get_extensions = Some(GetAssertionExtensions { + let get_extensions = GetAssertionExtensions { hmac_secret: Some(hmac_secret_input), - }); + cred_blob: false, + }; let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), @@ -1800,7 +1937,7 @@ mod test { let cred_desc = PublicKeyCredentialDescriptor { key_type: PublicKeyCredentialType::PublicKey, key_id: credential_id.clone(), - transports: None, // You can set USB as a hint here. + transports: None, }; let credential = PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, @@ -1815,6 +1952,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert!(ctap_state .persistent_store @@ -1825,7 +1963,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1847,7 +1985,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: Some(vec![cred_desc.clone()]), - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1877,6 +2015,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert!(ctap_state .persistent_store @@ -1887,7 +2026,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: Some(vec![cred_desc]), - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1906,6 +2045,69 @@ mod test { ); } + #[test] + fn test_process_get_assertion_with_cred_blob() { + 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, DUMMY_CLOCK_VALUE); + + 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: Some(vec![0xCB]), + }; + assert!(ctap_state + .persistent_store + .store_credential(credential) + .is_ok()); + + let extensions = GetAssertionExtensions { + hmac_secret: None, + cred_blob: true, + }; + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + let signature_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + let expected_extension_cbor = [ + 0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0x41, 0xCB, + ]; + check_assertion_response_with_extension( + get_assertion_response, + vec![0x1D], + signature_counter, + None, + &expected_extension_cbor, + ); + } + #[test] fn test_process_get_next_assertion_two_credentials_with_uv() { let mut rng = ThreadRng256 {}; @@ -1951,7 +2153,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: true, @@ -1974,6 +2176,7 @@ mod test { 0x04, signature_counter, Some(2), + &[], ); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); @@ -1983,6 +2186,7 @@ mod test { 0x04, signature_counter, None, + &[], ); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); @@ -2027,7 +2231,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -2091,7 +2295,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -2147,6 +2351,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert!(ctap_state .persistent_store diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 18dd199..ffc5dd6 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -57,7 +57,7 @@ const DEFAULT_MIN_PIN_LENGTH: u8 = 4; const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); // This constant is an attempt to limit storage requirements. If you don't set it to 0, // the stored strings can still be unbounded, but that is true for all RP IDs. -const MAX_RP_IDS_LENGTH: usize = 8; +pub const MAX_RP_IDS_LENGTH: usize = 8; /// Wrapper for master keys. pub struct MasterKeys { @@ -690,6 +690,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, } } @@ -906,6 +907,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert_eq!(found_credential, Some(expected_credential)); } @@ -927,6 +929,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert!(persistent_store.store_credential(credential).is_ok()); @@ -1160,6 +1163,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; let serialized = serialize_credential(credential.clone()).unwrap(); let reconstructed = deserialize_credential(&serialized).unwrap();