diff --git a/src/api/customization.rs b/src/api/customization.rs index 01de7b8..09b0bc4 100644 --- a/src/api/customization.rs +++ b/src/api/customization.rs @@ -118,12 +118,11 @@ pub trait Customization { /// /// - If the mode is VendorFacilitated, enterprise_attestation_mode() must be non-empty. /// - /// This list is only considered if the enterprise attestation mode is - /// VendorFacilitated. + /// This list is only considered if enterprise attestation is used. #[cfg(feature = "std")] fn enterprise_rp_id_list(&self) -> Vec; - // Returns whether the rp_id is contained in enterprise_rp_id_list(). + /// Returns whether the rp_id is contained in enterprise_rp_id_list(). fn is_enterprise_rp_id(&self, rp_id: &str) -> bool; /// Maximum message size send for CTAP commands. @@ -303,11 +302,18 @@ pub fn is_valid(customization: &impl Customization) -> bool { return false; } - // enterprise_rp_id_list() should be non-empty in vendor facilitated mode, and empty otherwise. + // enterprise_rp_id_list() should be non-empty in vendor facilitated mode. if matches!( customization.enterprise_attestation_mode(), Some(EnterpriseAttestationMode::VendorFacilitated) - ) == customization.enterprise_rp_id_list().is_empty() + ) && customization.enterprise_rp_id_list().is_empty() + { + return false; + } + + // enterprise_rp_id_list() should be empty without an enterprise attestation mode. + if customization.enterprise_attestation_mode().is_none() + && !customization.enterprise_rp_id_list().is_empty() { return false; } diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 69b8fe6..e83a14d 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -230,6 +230,7 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { let pin_uv_auth_protocol = pin_uv_auth_protocol .map(PinUvAuthProtocol::try_from) .transpose()?; + // We don't convert into EnterpriseAttestationMode to maintain the correct order of errors. let enterprise_attestation = enterprise_attestation.map(extract_unsigned).transpose()?; Ok(AuthenticatorMakeCredentialParameters { diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 69649b8..2a73544 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -697,8 +697,8 @@ impl CtapState { ( EnterpriseAttestationMode::PlatformManaged, EnterpriseAttestationMode::PlatformManaged, - ) => env.customization().is_enterprise_rp_id(&rp_id), - _ => true, + ) => true, + _ => env.customization().is_enterprise_rp_id(&rp_id), } } else { false @@ -2048,6 +2048,138 @@ mod test { ); } + fn check_ep(make_credential_response: Result, has_ep: bool) { + let ep_att = if has_ep { Some(true) } else { None }; + match make_credential_response.unwrap() { + ResponseData::AuthenticatorMakeCredential(make_credential_response) => { + assert_eq!(make_credential_response.ep_att, ep_att); + } + _ => panic!("Invalid response type"), + } + } + + #[test] + fn test_process_make_credential_with_enterprise_attestation_vendor_facilitated() { + let mut env = TestEnv::new(); + env.customization_mut().enterprise_attestation_mode = + Some(EnterpriseAttestationMode::VendorFacilitated); + env.customization_mut().enterprise_rp_id_list = vec!["example.com".to_string()]; + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + + let mut key_bytes = [0; 32]; + let private_key = crypto::ecdsa::SecKey::gensk(env.rng()); + private_key.to_bytes(array_mut_ref!(key_bytes, 0, 32)); + storage::set_attestation_certificate(&mut env, &[0xCC]).unwrap(); + storage::set_attestation_private_key(&mut env, &key_bytes).unwrap(); + storage::enable_enterprise_attestation(&mut env).unwrap(); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.enterprise_attestation = Some(1); + make_credential_params.rp = PublicKeyCredentialRpEntity { + rp_id: "counter-example.com".to_string(), + rp_name: None, + rp_icon: None, + }; + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + check_ep(make_credential_response, false); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.enterprise_attestation = Some(2); + make_credential_params.rp = PublicKeyCredentialRpEntity { + rp_id: "counter-example.com".to_string(), + rp_name: None, + rp_icon: None, + }; + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + check_ep(make_credential_response, false); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.enterprise_attestation = Some(1); + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + check_ep(make_credential_response, true); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.enterprise_attestation = Some(2); + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + check_ep(make_credential_response, true); + } + + #[test] + fn test_process_make_credential_with_enterprise_attestation_platform_managed() { + let mut env = TestEnv::new(); + env.customization_mut().enterprise_attestation_mode = + Some(EnterpriseAttestationMode::PlatformManaged); + env.customization_mut().enterprise_rp_id_list = vec!["example.com".to_string()]; + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + + let mut key_bytes = [0; 32]; + let private_key = crypto::ecdsa::SecKey::gensk(env.rng()); + private_key.to_bytes(array_mut_ref!(key_bytes, 0, 32)); + storage::set_attestation_certificate(&mut env, &[0xCC]).unwrap(); + storage::set_attestation_private_key(&mut env, &key_bytes).unwrap(); + storage::enable_enterprise_attestation(&mut env).unwrap(); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.enterprise_attestation = Some(1); + make_credential_params.rp = PublicKeyCredentialRpEntity { + rp_id: "counter-example.com".to_string(), + rp_name: None, + rp_icon: None, + }; + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + check_ep(make_credential_response, false); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.enterprise_attestation = Some(1); + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + check_ep(make_credential_response, true); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.enterprise_attestation = Some(2); + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + check_ep(make_credential_response, true); + } + + #[test] + fn test_process_make_credential_with_enterprise_attestation_invalid() { + let mut env = TestEnv::new(); + env.customization_mut().enterprise_attestation_mode = + Some(EnterpriseAttestationMode::PlatformManaged); + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.enterprise_attestation = Some(2); + let make_credential_response = + ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + let mut key_bytes = [0; 32]; + let private_key = crypto::ecdsa::SecKey::gensk(env.rng()); + private_key.to_bytes(array_mut_ref!(key_bytes, 0, 32)); + storage::set_attestation_certificate(&mut env, &[0xCC]).unwrap(); + storage::set_attestation_private_key(&mut env, &key_bytes).unwrap(); + storage::enable_enterprise_attestation(&mut env).unwrap(); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.enterprise_attestation = Some(3); + 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_INVALID_OPTION) + ); + } + #[test] fn test_process_make_credential_cancelled() { let mut env = TestEnv::new(); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 9700cb0..2130a52 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -577,6 +577,9 @@ pub fn enterprise_attestation(env: &mut impl Env) -> Result Result<(), Ctap2StatusCode> { + if attestation_private_key(env)?.is_none() || attestation_certificate(env)?.is_none() { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } if !enterprise_attestation(env)? { env.store().insert(key::ENTERPRISE_ATTESTATION, &[])?; } @@ -1079,8 +1082,8 @@ mod test { init(&mut env).unwrap(); // Make sure the attestation are absent. There is no batch attestation in tests. - assert!(attestation_private_key(&mut env,).unwrap().is_none()); - assert!(attestation_certificate(&mut env,).unwrap().is_none()); + assert!(attestation_private_key(&mut env).unwrap().is_none()); + assert!(attestation_certificate(&mut env).unwrap().is_none()); // Make sure the persistent keys are initialized to dummy values. let dummy_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; @@ -1233,6 +1236,18 @@ mod test { fn test_enterprise_attestation() { let mut env = TestEnv::new(); + assert!(!enterprise_attestation(&mut env).unwrap()); + assert_eq!( + enable_enterprise_attestation(&mut env), + Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) + ); + assert!(!enterprise_attestation(&mut env).unwrap()); + + let dummy_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + let dummy_cert = [0xddu8; 20]; + set_attestation_private_key(&mut env, &dummy_key).unwrap(); + set_attestation_certificate(&mut env, &dummy_cert).unwrap(); + assert!(!enterprise_attestation(&mut env).unwrap()); assert_eq!(enable_enterprise_attestation(&mut env), Ok(())); assert!(enterprise_attestation(&mut env).unwrap());