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 5e4daf3..351ac1e 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -12,15 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::check_pin_uv_auth_protocol; use super::command::AuthenticatorConfigParameters; use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams}; 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, 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 setMinPINLength for AuthenticatorConfig. fn process_set_min_pin_length( persistent_store: &mut PersistentStore, @@ -85,6 +97,9 @@ pub fn process_config( } match sub_command { + ConfigSubCommand::EnableEnterpriseAttestation => { + process_enable_enterprise_attestation(persistent_store) + } ConfigSubCommand::SetMinPinLength => { if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params { process_set_min_pin_length(persistent_store, params) @@ -101,6 +116,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) + ); + } + } + fn create_min_pin_config_params( min_pin_length: u8, min_pin_length_rp_ids: Option>, diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 1992469..9f4b68c 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,22 @@ 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(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 ab66177..eeb43bc 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}; @@ -86,6 +87,18 @@ 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! +pub const ENTERPRISE_ATTESTATION_MODE: Option = None; +const ENTERPRISE_RP_ID_LIST: Vec = Vec::new(); // Our credential ID consists of // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, @@ -562,7 +575,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)?; @@ -572,6 +585,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), + _ => 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) @@ -723,7 +756,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()? @@ -750,11 +783,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, }, )) @@ -1026,6 +1061,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); @@ -1227,6 +1268,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. @@ -1247,6 +1289,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); } @@ -1276,12 +1319,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/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 b982922..c38146a 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -610,6 +610,23 @@ impl PersistentStore { pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { 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(()) + } } impl From for Ctap2StatusCode { @@ -1308,6 +1325,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_serialize_deserialize_credential() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 2093685..dd9f67e 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -93,7 +93,10 @@ make_partition! { /// The stored large blob can be too big for one key, so it has to be sharded. LARGE_BLOB_SHARDS = 2000..2004; - /// 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.