diff --git a/README.md b/README.md index 41f5378..d6c47f5 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,9 @@ a few things you can personalize: length. 1. Increase the `MAX_CRED_BLOB_LENGTH` in `ctap/mod.rs`, if you expect blobs bigger than the default value. +1. Implement enterprise attestation. This can be as easy as setting + ENTERPRISE_ATTESTATION_MODE in `ctap/mod.rs`. If you want to use a different + attestation type than batch attestation, you have to implement it first. 1. If a certification (additional to FIDO's) requires that all requests are protected with user verification, set `ENFORCE_ALWAYS_UV` in `ctap/config_mod.rs` to `true`. diff --git a/src/ctap/command.rs b/src/ctap/command.rs index a76254a..eb16a1f 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -161,7 +161,7 @@ pub struct AuthenticatorMakeCredentialParameters { pub options: MakeCredentialOptions, pub pin_uv_auth_param: Option>, pub pin_uv_auth_protocol: Option, - pub enterprise_attestation: Option, + pub enterprise_attestation: Option, } impl TryFrom for AuthenticatorMakeCredentialParameters { @@ -219,7 +219,7 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { 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()?; - let enterprise_attestation = enterprise_attestation.map(extract_bool).transpose()?; + let enterprise_attestation = enterprise_attestation.map(extract_unsigned).transpose()?; Ok(AuthenticatorMakeCredentialParameters { client_data_hash, @@ -601,7 +601,7 @@ mod test { 0x05 => cbor_array![], 0x08 => vec![0x12, 0x34], 0x09 => 1, - 0x0A => true, + 0x0A => 2, }; let returned_make_credential_parameters = AuthenticatorMakeCredentialParameters::try_from(cbor_value).unwrap(); @@ -635,7 +635,7 @@ mod test { options, pin_uv_auth_param: Some(vec![0x12, 0x34]), pin_uv_auth_protocol: Some(1), - enterprise_attestation: Some(true), + enterprise_attestation: Some(2), }; assert_eq!( diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 0d55717..b4c7cc0 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -18,9 +18,21 @@ use super::pin_protocol_v1::PinProtocolV1; use super::response::ResponseData; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; -use super::{check_pin_uv_auth_protocol, ENFORCE_ALWAYS_UV}; +use super::{check_pin_uv_auth_protocol, ENFORCE_ALWAYS_UV, ENTERPRISE_ATTESTATION_MODE}; use alloc::vec; +/// Processes the subcommand enableEnterpriseAttestation for AuthenticatorConfig. +fn process_enable_enterprise_attestation( + persistent_store: &mut PersistentStore, +) -> Result { + if ENTERPRISE_ATTESTATION_MODE.is_some() { + persistent_store.enable_enterprise_attestation()?; + Ok(ResponseData::AuthenticatorConfig) + } else { + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + } +} + /// Processes the subcommand toggleAlwaysUv for AuthenticatorConfig. fn process_toggle_always_uv( persistent_store: &mut PersistentStore, @@ -100,6 +112,9 @@ pub fn process_config( } match sub_command { + ConfigSubCommand::EnableEnterpriseAttestation => { + process_enable_enterprise_attestation(persistent_store) + } ConfigSubCommand::ToggleAlwaysUv => process_toggle_always_uv(persistent_store), ConfigSubCommand::SetMinPinLength => { if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params { @@ -117,6 +132,34 @@ mod test { use super::*; use crypto::rng256::ThreadRng256; + #[test] + fn test_process_enable_enterprise_attestation() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::EnableEnterpriseAttestation, + sub_command_params: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + + if ENTERPRISE_ATTESTATION_MODE.is_some() { + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.enterprise_attestation(), Ok(true)); + } else { + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + } + #[test] fn test_process_toggle_always_uv() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index f0721ac..5a2cc4b 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -105,6 +105,22 @@ fn enumerate_credentials_response( }) } +/// Check if the token permissions have the correct associated RP ID. +/// +/// Either no RP ID is associated, or the RP ID matches the stored credential. +fn check_rp_id_permissions( + persistent_store: &mut PersistentStore, + pin_protocol_v1: &mut PinProtocolV1, + credential_id: &[u8], +) -> Result<(), Ctap2StatusCode> { + // Pre-check a sufficient condition before calling the store. + if pin_protocol_v1.has_no_rp_id_permission().is_ok() { + return Ok(()); + } + let (_, credential) = persistent_store.find_credential_item(credential_id)?; + pin_protocol_v1.has_no_or_rp_id_permission(&credential.rp_id) +} + /// Processes the subcommand getCredsMetadata for CredentialManagement. fn process_get_creds_metadata( persistent_store: &PersistentStore, @@ -157,12 +173,14 @@ fn process_enumerate_rps_get_next_rp( fn process_enumerate_credentials_begin( persistent_store: &PersistentStore, stateful_command_permission: &mut StatefulPermission, + pin_protocol_v1: &mut PinProtocolV1, sub_command_params: CredentialManagementSubCommandParameters, now: ClockValue, ) -> Result { let rp_id_hash = sub_command_params .rp_id_hash .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + pin_protocol_v1.has_no_or_rp_id_hash_permission(&rp_id_hash[..])?; let mut iter_result = Ok(()); let iter = persistent_store.iter_credentials(&mut iter_result)?; let mut rp_credentials: Vec = iter @@ -201,18 +219,21 @@ fn process_enumerate_credentials_get_next_credential( /// Processes the subcommand deleteCredential for CredentialManagement. fn process_delete_credential( persistent_store: &mut PersistentStore, + pin_protocol_v1: &mut PinProtocolV1, sub_command_params: CredentialManagementSubCommandParameters, ) -> Result<(), Ctap2StatusCode> { let credential_id = sub_command_params .credential_id .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)? .key_id; + check_rp_id_permissions(persistent_store, pin_protocol_v1, &credential_id)?; persistent_store.delete_credential(&credential_id) } /// Processes the subcommand updateUserInformation for CredentialManagement. fn process_update_user_information( persistent_store: &mut PersistentStore, + pin_protocol_v1: &mut PinProtocolV1, sub_command_params: CredentialManagementSubCommandParameters, ) -> Result<(), Ctap2StatusCode> { let credential_id = sub_command_params @@ -222,6 +243,7 @@ fn process_update_user_information( let user = sub_command_params .user .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + check_rp_id_permissions(persistent_store, pin_protocol_v1, &credential_id)?; persistent_store.update_credential(&credential_id, user) } @@ -257,13 +279,10 @@ pub fn process_credential_management( match sub_command { CredentialManagementSubCommand::GetCredsMetadata | CredentialManagementSubCommand::EnumerateRpsBegin - | CredentialManagementSubCommand::DeleteCredential | CredentialManagementSubCommand::EnumerateCredentialsBegin + | CredentialManagementSubCommand::DeleteCredential | CredentialManagementSubCommand::UpdateUserInformation => { check_pin_uv_auth_protocol(pin_protocol)?; - persistent_store - .pin_hash()? - .ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; let pin_auth = pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; let mut management_data = vec![sub_command as u8]; if let Some(sub_command_params) = sub_command_params.clone() { @@ -274,9 +293,8 @@ pub fn process_credential_management( if !pin_protocol_v1.verify_pin_auth_token(&management_data, &pin_auth) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } + // The RP ID permission is handled differently per subcommand below. pin_protocol_v1.has_permission(PinPermission::CredentialManagement)?; - pin_protocol_v1.has_no_permission_rp_id()?; - // TODO(kaczmarczyck) sometimes allow a RP ID } CredentialManagementSubCommand::EnumerateRpsGetNextRp | CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {} @@ -284,13 +302,17 @@ pub fn process_credential_management( let response = match sub_command { CredentialManagementSubCommand::GetCredsMetadata => { + pin_protocol_v1.has_no_rp_id_permission()?; Some(process_get_creds_metadata(persistent_store)?) } - CredentialManagementSubCommand::EnumerateRpsBegin => Some(process_enumerate_rps_begin( - persistent_store, - stateful_command_permission, - now, - )?), + CredentialManagementSubCommand::EnumerateRpsBegin => { + pin_protocol_v1.has_no_rp_id_permission()?; + Some(process_enumerate_rps_begin( + persistent_store, + stateful_command_permission, + now, + )?) + } CredentialManagementSubCommand::EnumerateRpsGetNextRp => Some( process_enumerate_rps_get_next_rp(persistent_store, stateful_command_permission)?, ), @@ -298,6 +320,7 @@ pub fn process_credential_management( Some(process_enumerate_credentials_begin( persistent_store, stateful_command_permission, + pin_protocol_v1, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, now, )?) @@ -311,6 +334,7 @@ pub fn process_credential_management( CredentialManagementSubCommand::DeleteCredential => { process_delete_credential( persistent_store, + pin_protocol_v1, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, )?; None @@ -318,6 +342,7 @@ pub fn process_credential_management( CredentialManagementSubCommand::UpdateUserInformation => { process_update_user_information( persistent_store, + pin_protocol_v1, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, )?; None diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 1992469..04e9a36 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -939,6 +939,24 @@ impl From for cbor::Value { } } +#[derive(Debug, PartialEq)] +pub enum EnterpriseAttestationMode { + VendorFacilitated = 0x01, + PlatformManaged = 0x02, +} + +impl TryFrom for EnterpriseAttestationMode { + type Error = Ctap2StatusCode; + + fn try_from(value: u64) -> Result { + match value { + 1 => Ok(EnterpriseAttestationMode::VendorFacilitated), + 2 => Ok(EnterpriseAttestationMode::PlatformManaged), + _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION), + } + } +} + #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum CredentialManagementSubCommand { @@ -1795,6 +1813,26 @@ mod test { assert_eq!(cbor::Value::from(config_sub_command_params), cbor_params); } + #[test] + fn test_from_enterprise_attestation_mode() { + assert_eq!( + EnterpriseAttestationMode::try_from(0), + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION), + ); + assert_eq!( + EnterpriseAttestationMode::try_from(1), + Ok(EnterpriseAttestationMode::VendorFacilitated), + ); + assert_eq!( + EnterpriseAttestationMode::try_from(2), + Ok(EnterpriseAttestationMode::PlatformManaged), + ); + assert_eq!( + EnterpriseAttestationMode::try_from(3), + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION), + ); + } + #[test] fn test_from_into_cred_management_sub_command() { let cbor_sub_command: cbor::Value = cbor_int!(0x01); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 2180eb7..85deafe 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -36,10 +36,10 @@ use self::command::{ use self::config_command::process_config; use self::credential_management::process_credential_management; use self::data_formats::{ - AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionExtensions, - PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, - PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity, - SignatureAlgorithm, + AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, EnterpriseAttestationMode, + GetAssertionExtensions, PackedAttestationStatement, PublicKeyCredentialDescriptor, + PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType, + PublicKeyCredentialUserEntity, SignatureAlgorithm, }; use self::hid::ChannelID; use self::large_blobs::{LargeBlobs, MAX_MSG_SIZE}; @@ -61,6 +61,7 @@ use alloc::vec::Vec; use arrayref::array_ref; use byteorder::{BigEndian, ByteOrder}; use cbor::cbor_map_options; +use core::convert::TryFrom; #[cfg(feature = "debug_ctap")] use core::fmt::Write; use crypto::cbc::{cbc_decrypt, cbc_encrypt}; @@ -74,9 +75,11 @@ use libtock_drivers::crp; use libtock_drivers::timer::{ClockValue, Duration}; // This flag enables or disables basic attestation for FIDO2. U2F is unaffected by -// this setting. The basic attestation uses the signing key from key_material.rs -// as a batch key. Turn it on if you want attestation. In this case, be aware that -// it is your responsibility to generate your own key material and keep it secret. +// this setting. The basic attestation uses the signing key configured with a +// vendor command as a batch key. If you turn batch attestation on, be aware that +// it is your responsibility to safely generate and store the key material. Also, +// the batches must have size of at least 100k authenticators before using new +// key material. const USE_BATCH_ATTESTATION: bool = false; // The signature counter is currently implemented as a global counter, if you set // this flag to true. The spec strongly suggests to have per-credential-counters, @@ -86,6 +89,21 @@ const USE_BATCH_ATTESTATION: bool = false; // solution is a compromise to be compatible with U2F and not wasting storage. const USE_SIGNATURE_COUNTER: bool = true; pub const INITIAL_SIGNATURE_COUNTER: u32 = 1; +// This flag allows usage of enterprise attestation. For privacy reasons, it is +// disabled by default. You can choose between +// - EnterpriseAttestationMode::VendorFacilitated, +// - EnterpriseAttestationMode::PlatformManaged. +// For VendorFacilitated, choose an appriopriate ENTERPRISE_RP_ID_LIST. +// To enable the feature, send the subcommand enableEnterpriseAttestation in +// AuthenticatorConfig. An enterprise might want to customize the type of +// attestation that is used. OpenSK defaults to batch attestation. Configuring +// individual certificates then makes authenticators identifiable. Do NOT set +// USE_BATCH_ATTESTATION to true at the same time in this case! The code asserts +// that you don't use the same key material for batch and enterprise attestation. +// If you implement your own enterprise attestation mechanism, and you want batch +// attestation at the same time, proceed carefully and remove the assertion. +pub const ENTERPRISE_ATTESTATION_MODE: Option = None; +const ENTERPRISE_RP_ID_LIST: &[&str] = &[]; // Our credential ID consists of // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, @@ -311,6 +329,11 @@ where check_user_presence: CheckUserPresence, now: ClockValue, ) -> CtapState<'a, R, CheckUserPresence> { + #[allow(clippy::assertions_on_constants)] + { + assert!(!USE_BATCH_ATTESTATION || ENTERPRISE_ATTESTATION_MODE.is_none()); + } + let persistent_store = PersistentStore::new(rng); let pin_protocol_v1 = PinProtocolV1::new(rng); CtapState { @@ -573,7 +596,7 @@ where options, pin_uv_auth_param, pin_uv_auth_protocol, - enterprise_attestation: _, + enterprise_attestation, } = make_credential_params; self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?; @@ -583,6 +606,26 @@ where } let rp_id = rp.rp_id; + let ep_att = if let Some(enterprise_attestation) = enterprise_attestation { + let authenticator_mode = + ENTERPRISE_ATTESTATION_MODE.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?; + if !self.persistent_store.enterprise_attestation()? { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + match ( + EnterpriseAttestationMode::try_from(enterprise_attestation)?, + authenticator_mode, + ) { + ( + EnterpriseAttestationMode::PlatformManaged, + EnterpriseAttestationMode::PlatformManaged, + ) => ENTERPRISE_RP_ID_LIST.contains(&rp_id.as_str()), + _ => true, + } + } else { + false + }; + let mut cred_protect_policy = extensions.cred_protect; if cred_protect_policy.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) < DEFAULT_CRED_PROTECT.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) @@ -649,7 +692,7 @@ where } self.pin_protocol_v1 .has_permission(PinPermission::MakeCredential)?; - self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; + self.pin_protocol_v1.ensure_rp_id_permission(&rp_id)?; UP_FLAG | UV_FLAG | AT_FLAG | ed_flag } None => { @@ -738,7 +781,7 @@ where let mut signature_data = auth_data.clone(); signature_data.extend(client_data_hash); - let (signature, x5c) = if USE_BATCH_ATTESTATION { + let (signature, x5c) = if USE_BATCH_ATTESTATION || ep_att { let attestation_private_key = self .persistent_store .attestation_private_key()? @@ -765,11 +808,13 @@ where x5c, ecdaa_key_id: None, }; + let ep_att = if ep_att { Some(true) } else { None }; Ok(ResponseData::AuthenticatorMakeCredential( AuthenticatorMakeCredentialResponse { fmt: String::from("packed"), auth_data, att_stmt: attestation_statement, + ep_att, large_blob_key, }, )) @@ -938,7 +983,7 @@ where } self.pin_protocol_v1 .has_permission(PinPermission::GetAssertion)?; - self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; + self.pin_protocol_v1.ensure_rp_id_permission(&rp_id)?; UV_FLAG } None => { @@ -1055,6 +1100,12 @@ where options_map.insert(String::from("up"), true); options_map.insert(String::from("pinUvAuthToken"), true); options_map.insert(String::from("largeBlobs"), true); + if ENTERPRISE_ATTESTATION_MODE.is_some() { + options_map.insert( + String::from("ep"), + self.persistent_store.enterprise_attestation()?, + ); + } options_map.insert(String::from("authnrCfg"), true); options_map.insert(String::from("credMgmt"), true); options_map.insert(String::from("setMinPINLength"), true); @@ -1252,6 +1303,7 @@ mod test { fmt, auth_data, att_stmt, + ep_att, large_blob_key, } = make_credential_response; // The expected response is split to only assert the non-random parts. @@ -1272,6 +1324,7 @@ mod test { &auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], expected_extension_cbor ); + assert!(ep_att.is_none()); assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); assert_eq!(large_blob_key, None); } @@ -1301,12 +1354,13 @@ mod test { String::from("largeBlobKey"), ]], 0x03 => ctap_state.persistent_store.aaguid().unwrap(), - 0x04 => cbor_map! { + 0x04 => cbor_map_options! { "rk" => true, "clientPin" => false, "up" => true, "pinUvAuthToken" => true, "largeBlobs" => true, + "ep" => ENTERPRISE_ATTESTATION_MODE.map(|_| false), "authnrCfg" => true, "credMgmt" => true, "setMinPINLength" => true, diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 6ec5644..3b00a35 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -501,6 +501,7 @@ impl PinProtocolV1 { encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cred_random) } + /// Check if the required command's token permission is granted. pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { // Relies on the fact that all permissions are represented by powers of two. if permission as u8 & self.permissions != 0 { @@ -510,22 +511,47 @@ impl PinProtocolV1 { } } - pub fn has_no_permission_rp_id(&self) -> Result<(), Ctap2StatusCode> { + /// Check if no RP ID is associated with the token permission. + pub fn has_no_rp_id_permission(&self) -> Result<(), Ctap2StatusCode> { if self.permissions_rp_id.is_some() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } Ok(()) } - pub fn has_permission_for_rp_id(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { - if let Some(permissions_rp_id) = &self.permissions_rp_id { - if rp_id != permissions_rp_id { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - } else { - self.permissions_rp_id = Some(String::from(rp_id)); + /// Check if no or the passed RP ID is associated with the token permission. + pub fn has_no_or_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { + match &self.permissions_rp_id { + Some(p) if rp_id != p => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + _ => Ok(()), + } + } + + /// Check if no RP ID is associated with the token permission, or it matches the hash. + pub fn has_no_or_rp_id_hash_permission( + &self, + rp_id_hash: &[u8], + ) -> Result<(), Ctap2StatusCode> { + match &self.permissions_rp_id { + Some(p) if rp_id_hash != Sha256::hash(p.as_bytes()) => { + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + } + _ => Ok(()), + } + } + + /// Check if the passed RP ID is associated with the token permission. + /// + /// If no RP ID is associated, associate the passed RP ID as a side effect. + pub fn ensure_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { + match &self.permissions_rp_id { + Some(p) if rp_id != p => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + None => { + self.permissions_rp_id = Some(String::from(rp_id)); + Ok(()) + } + _ => Ok(()), } - Ok(()) } #[cfg(test)] @@ -1150,24 +1176,65 @@ mod test { } #[test] - fn test_has_no_permission_rp_id() { + fn test_has_no_rp_id_permission() { let mut rng = ThreadRng256 {}; let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - assert_eq!(pin_protocol_v1.has_no_permission_rp_id(), Ok(())); - assert_eq!(pin_protocol_v1.permissions_rp_id, None,); + assert_eq!(pin_protocol_v1.has_no_rp_id_permission(), Ok(())); + assert_eq!(pin_protocol_v1.permissions_rp_id, None); pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); assert_eq!( - pin_protocol_v1.has_no_permission_rp_id(), + pin_protocol_v1.has_no_rp_id_permission(), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } #[test] - fn test_has_permission_for_rp_id() { + fn test_has_no_or_rp_id_permission() { let mut rng = ThreadRng256 {}; let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); assert_eq!( - pin_protocol_v1.has_permission_for_rp_id("example.com"), + pin_protocol_v1.has_no_or_rp_id_permission("example.com"), + Ok(()) + ); + assert_eq!(pin_protocol_v1.permissions_rp_id, None); + pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); + assert_eq!( + pin_protocol_v1.has_no_or_rp_id_permission("example.com"), + Ok(()) + ); + assert_eq!( + pin_protocol_v1.has_no_or_rp_id_permission("another.example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_has_no_or_rp_id_hash_permission() { + let mut rng = ThreadRng256 {}; + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let rp_id_hash = Sha256::hash(b"example.com"); + assert_eq!( + pin_protocol_v1.has_no_or_rp_id_hash_permission(&rp_id_hash), + Ok(()) + ); + assert_eq!(pin_protocol_v1.permissions_rp_id, None); + pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); + assert_eq!( + pin_protocol_v1.has_no_or_rp_id_hash_permission(&rp_id_hash), + Ok(()) + ); + assert_eq!( + pin_protocol_v1.has_no_or_rp_id_hash_permission(&[0x4A; 32]), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_ensure_rp_id_permission() { + let mut rng = ThreadRng256 {}; + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + assert_eq!( + pin_protocol_v1.ensure_rp_id_permission("example.com"), Ok(()) ); assert_eq!( @@ -1175,11 +1242,11 @@ mod test { Some(String::from("example.com")) ); assert_eq!( - pin_protocol_v1.has_permission_for_rp_id("example.com"), + pin_protocol_v1.ensure_rp_id_permission("example.com"), Ok(()) ); assert_eq!( - pin_protocol_v1.has_permission_for_rp_id("counter-example.com"), + pin_protocol_v1.ensure_rp_id_permission("counter-example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 093d4c9..b6b3d25 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -61,6 +61,7 @@ pub struct AuthenticatorMakeCredentialResponse { pub fmt: String, pub auth_data: Vec, pub att_stmt: PackedAttestationStatement, + pub ep_att: Option, pub large_blob_key: Option>, } @@ -70,6 +71,7 @@ impl From for cbor::Value { fmt, auth_data, att_stmt, + ep_att, large_blob_key, } = make_credential_response; @@ -77,6 +79,7 @@ impl From for cbor::Value { 0x01 => fmt, 0x02 => auth_data, 0x03 => att_stmt, + 0x04 => ep_att, 0x05 => large_blob_key, } } @@ -320,6 +323,7 @@ mod test { fmt: "packed".to_string(), auth_data: vec![0xAD], att_stmt, + ep_att: Some(true), large_blob_key: Some(vec![0x1B]), }; let response_cbor: Option = @@ -328,6 +332,7 @@ mod test { 0x01 => "packed", 0x02 => vec![0xAD], 0x03 => cbor_packed_attestation_statement, + 0x04 => true, 0x05 => vec![0x1B], }; assert_eq!(response_cbor, Some(expected_cbor)); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 3b0fb9e..a8be6f1 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -56,7 +56,7 @@ const MAX_SUPPORTED_RESIDENT_KEYS: usize = 150; const MAX_PIN_RETRIES: u8 = 8; const DEFAULT_MIN_PIN_LENGTH: u8 = 4; -const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); +const DEFAULT_MIN_PIN_LENGTH_RP_IDS: &[&str] = &[]; // 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. pub const MAX_RP_IDS_LENGTH: usize = 8; @@ -151,7 +151,7 @@ impl PersistentStore { /// # Errors /// /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. - fn find_credential_item( + pub fn find_credential_item( &self, credential_id: &[u8], ) -> Result<(usize, PublicKeyCredentialSource), Ctap2StatusCode> { @@ -439,12 +439,17 @@ impl PersistentStore { /// Returns the list of RP IDs that are used to check if reading the minimum PIN length is /// allowed. pub fn min_pin_length_rp_ids(&self) -> Result, Ctap2StatusCode> { - let rp_ids = self - .store - .find(key::MIN_PIN_LENGTH_RP_IDS)? - .map_or(Some(DEFAULT_MIN_PIN_LENGTH_RP_IDS), |value| { - deserialize_min_pin_length_rp_ids(&value) - }); + let rp_ids = self.store.find(key::MIN_PIN_LENGTH_RP_IDS)?.map_or_else( + || { + Some( + DEFAULT_MIN_PIN_LENGTH_RP_IDS + .iter() + .map(|&s| String::from(s)) + .collect(), + ) + }, + |value| deserialize_min_pin_length_rp_ids(&value), + ); debug_assert!(rp_ids.is_some()); Ok(rp_ids.unwrap_or_default()) } @@ -455,7 +460,8 @@ impl PersistentStore { min_pin_length_rp_ids: Vec, ) -> Result<(), Ctap2StatusCode> { let mut min_pin_length_rp_ids = min_pin_length_rp_ids; - for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS { + for &rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS.iter() { + let rp_id = String::from(rp_id); if !min_pin_length_rp_ids.contains(&rp_id) { min_pin_length_rp_ids.push(rp_id); } @@ -611,6 +617,23 @@ impl PersistentStore { Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[])?) } + /// Returns whether enterprise attestation is enabled. + pub fn enterprise_attestation(&self) -> Result { + match self.store.find(key::ENTERPRISE_ATTESTATION)? { + None => Ok(false), + Some(value) if value.is_empty() => Ok(true), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Marks enterprise attestation as enabled. + pub fn enable_enterprise_attestation(&mut self) -> Result<(), Ctap2StatusCode> { + if !self.enterprise_attestation()? { + self.store.insert(key::ENTERPRISE_ATTESTATION, &[])?; + } + Ok(()) + } + /// Returns whether alwaysUv is enabled. pub fn has_always_uv(&self) -> Result { if ENFORCE_ALWAYS_UV { @@ -1210,7 +1233,8 @@ mod test { persistent_store.set_min_pin_length_rp_ids(rp_ids.clone()), Ok(()) ); - for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS { + for &rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS.iter() { + let rp_id = String::from(rp_id); if !rp_ids.contains(&rp_id) { rp_ids.push(rp_id); } @@ -1332,6 +1356,18 @@ mod test { assert!(!persistent_store.has_force_pin_change().unwrap()); } + #[test] + fn test_enterprise_attestation() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + assert!(!persistent_store.enterprise_attestation().unwrap()); + assert_eq!(persistent_store.enable_enterprise_attestation(), Ok(())); + assert!(persistent_store.enterprise_attestation().unwrap()); + persistent_store.reset(&mut rng).unwrap(); + assert!(!persistent_store.enterprise_attestation().unwrap()); + } + #[test] fn test_always_uv() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 9387931..7c6bea5 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -96,7 +96,10 @@ make_partition! { /// If this entry exists and is empty, alwaysUv is enabled. ALWAYS_UV = 2038; - /// If this entry exists and equals 1, the PIN needs to be changed. + /// If this entry exists and is empty, enterprise attestation is enabled. + ENTERPRISE_ATTESTATION = 2039; + + /// If this entry exists and is empty, the PIN needs to be changed. FORCE_PIN_CHANGE = 2040; /// The secret of the CredRandom feature.