diff --git a/libwebauthn/src/ops/webauthn/make_credential.rs b/libwebauthn/src/ops/webauthn/make_credential.rs index 03d2b6d..237d65c 100644 --- a/libwebauthn/src/ops/webauthn/make_credential.rs +++ b/libwebauthn/src/ops/webauthn/make_credential.rs @@ -599,6 +599,32 @@ impl DowngradableRequest for MakeCredentialRequest { return false; } + // Enforced credProtect with a non-default policy cannot be honoured by U2F. + if let Some(cred_protect) = self + .extensions + .as_ref() + .and_then(|e| e.cred_protect.as_ref()) + { + if cred_protect.enforce_policy + && cred_protect.policy != CredentialProtectionPolicy::UserVerificationOptional + { + debug!("Not downgradable: request enforces a non-default credProtect policy"); + return false; + } + } + + // U2F has no large-blob storage. + if matches!( + self.extensions + .as_ref() + .and_then(|e| e.large_blob.as_ref()) + .map(|lb| lb.support), + Some(MakeCredentialLargeBlobExtension::Required) + ) { + debug!("Not downgradable: request requires the largeBlob extension"); + return false; + } + true } diff --git a/libwebauthn/src/ops/webauthn/mod.rs b/libwebauthn/src/ops/webauthn/mod.rs index 2a57e67..36ab56b 100644 --- a/libwebauthn/src/ops/webauthn/mod.rs +++ b/libwebauthn/src/ops/webauthn/mod.rs @@ -25,10 +25,11 @@ pub use idl::{ }; pub use make_credential::{ CredentialPropsExtension, CredentialProtectionExtension, CredentialProtectionPolicy, - MakeCredentialLargeBlobExtension, MakeCredentialLargeBlobExtensionOutput, - MakeCredentialPrfInput, MakeCredentialPrfOutput, MakeCredentialRequest, MakeCredentialResponse, - MakeCredentialsRequestExtensions, MakeCredentialsResponseExtensions, - MakeCredentialsResponseUnsignedExtensions, ResidentKeyRequirement, + MakeCredentialLargeBlobExtension, MakeCredentialLargeBlobExtensionInput, + MakeCredentialLargeBlobExtensionOutput, MakeCredentialPrfInput, MakeCredentialPrfOutput, + MakeCredentialRequest, MakeCredentialResponse, MakeCredentialsRequestExtensions, + MakeCredentialsResponseExtensions, MakeCredentialsResponseUnsignedExtensions, + ResidentKeyRequirement, }; pub use psl::{DatFileLoadError, DatFilePublicSuffixList, PublicSuffixList, SYSTEM_PSL_PATH}; use serde::Deserialize; @@ -82,7 +83,11 @@ pub trait DowngradableRequest { #[cfg(test)] mod tests { - use crate::ops::webauthn::make_credential::ResidentKeyRequirement; + use crate::ops::webauthn::make_credential::{ + CredentialProtectionExtension, CredentialProtectionPolicy, + MakeCredentialLargeBlobExtension, MakeCredentialLargeBlobExtensionInput, + MakeCredentialsRequestExtensions, ResidentKeyRequirement, + }; use crate::ops::webauthn::{ DowngradableRequest, MakeCredentialRequest, UserVerificationRequirement, }; @@ -123,4 +128,86 @@ mod tests { )]; assert!(!request.is_downgradable()); } + + #[test] + fn ctap2_make_credential_downgradable_enforced_cred_protect_required() { + let mut request = MakeCredentialRequest::dummy(); + request.algorithms = vec![Ctap2CredentialType::default()]; + request.extensions = Some(MakeCredentialsRequestExtensions { + cred_protect: Some(CredentialProtectionExtension { + policy: CredentialProtectionPolicy::UserVerificationRequired, + enforce_policy: true, + }), + ..MakeCredentialsRequestExtensions::default() + }); + assert!(!request.is_downgradable()); + } + + #[test] + fn ctap2_make_credential_downgradable_enforced_cred_protect_optional_with_list() { + let mut request = MakeCredentialRequest::dummy(); + request.algorithms = vec![Ctap2CredentialType::default()]; + request.extensions = Some(MakeCredentialsRequestExtensions { + cred_protect: Some(CredentialProtectionExtension { + policy: CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList, + enforce_policy: true, + }), + ..MakeCredentialsRequestExtensions::default() + }); + assert!(!request.is_downgradable()); + } + + #[test] + fn ctap2_make_credential_downgradable_enforced_cred_protect_optional() { + let mut request = MakeCredentialRequest::dummy(); + request.algorithms = vec![Ctap2CredentialType::default()]; + request.extensions = Some(MakeCredentialsRequestExtensions { + cred_protect: Some(CredentialProtectionExtension { + policy: CredentialProtectionPolicy::UserVerificationOptional, + enforce_policy: true, + }), + ..MakeCredentialsRequestExtensions::default() + }); + assert!(request.is_downgradable()); + } + + #[test] + fn ctap2_make_credential_downgradable_non_enforced_cred_protect() { + let mut request = MakeCredentialRequest::dummy(); + request.algorithms = vec![Ctap2CredentialType::default()]; + request.extensions = Some(MakeCredentialsRequestExtensions { + cred_protect: Some(CredentialProtectionExtension { + policy: CredentialProtectionPolicy::UserVerificationRequired, + enforce_policy: false, + }), + ..MakeCredentialsRequestExtensions::default() + }); + assert!(request.is_downgradable()); + } + + #[test] + fn ctap2_make_credential_downgradable_large_blob_required() { + let mut request = MakeCredentialRequest::dummy(); + request.algorithms = vec![Ctap2CredentialType::default()]; + request.extensions = Some(MakeCredentialsRequestExtensions { + large_blob: Some(MakeCredentialLargeBlobExtensionInput { + support: MakeCredentialLargeBlobExtension::Required, + }), + ..MakeCredentialsRequestExtensions::default() + }); + assert!(!request.is_downgradable()); + } + + #[test] + fn ctap2_make_credential_downgradable_large_blob_preferred() { + let mut request = MakeCredentialRequest::dummy(); + request.algorithms = vec![Ctap2CredentialType::default()]; + request.extensions = Some(MakeCredentialsRequestExtensions { + large_blob: Some(MakeCredentialLargeBlobExtensionInput { + support: MakeCredentialLargeBlobExtension::Preferred, + }), + ..MakeCredentialsRequestExtensions::default() + }); + assert!(request.is_downgradable()); + } } diff --git a/libwebauthn/src/proto/ctap2/model/make_credential.rs b/libwebauthn/src/proto/ctap2/model/make_credential.rs index db798d8..75714d2 100644 --- a/libwebauthn/src/proto/ctap2/model/make_credential.rs +++ b/libwebauthn/src/proto/ctap2/model/make_credential.rs @@ -231,12 +231,10 @@ impl Ctap2MakeCredentialsRequestExtensions { .map(|info| info.support) { Some(MakeCredentialLargeBlobExtension::Required) => { - // "required": The credential will be created with an authenticator to store blobs. The create() call will fail if this is impossible. + // Required + unsupported must fail rather than silently degrade. if !info.option_enabled("largeBlobs") { - warn!("This request will potentially fail. Large blob extension required, but device does not support it."); + return Err(Error::Ctap(CtapError::UnsupportedExtension)); } - // We still send the request to the device and let it sort it out. - // We only add a warning for easier debugging. Some(true) } Some(MakeCredentialLargeBlobExtension::Preferred) => { @@ -386,3 +384,89 @@ pub struct Ctap2MakeCredentialsResponseExtensions { #[serde(default, skip_serializing_if = "Option::is_none")] pub min_pin_length: Option, } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ops::webauthn::MakeCredentialLargeBlobExtensionInput; + use std::collections::HashMap; + + fn info_with_options(options: &[(&str, bool)]) -> Ctap2GetInfoResponse { + let mut info = Ctap2GetInfoResponse::default(); + let mut map = HashMap::new(); + for (k, v) in options { + map.insert((*k).to_string(), *v); + } + info.options = Some(map); + info + } + + #[test] + fn ctap2_extensions_large_blob_required_unsupported_returns_unsupported_extension() { + let info = info_with_options(&[("largeBlobs", false)]); + let requested = MakeCredentialsRequestExtensions { + large_blob: Some(MakeCredentialLargeBlobExtensionInput { + support: MakeCredentialLargeBlobExtension::Required, + }), + ..MakeCredentialsRequestExtensions::default() + }; + + let result = + Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info); + assert!(matches!( + result, + Err(Error::Ctap(CtapError::UnsupportedExtension)) + )); + } + + #[test] + fn ctap2_extensions_large_blob_required_option_absent_returns_unsupported_extension() { + // No options at all (largeBlobs neither present nor enabled). + let info = Ctap2GetInfoResponse::default(); + let requested = MakeCredentialsRequestExtensions { + large_blob: Some(MakeCredentialLargeBlobExtensionInput { + support: MakeCredentialLargeBlobExtension::Required, + }), + ..MakeCredentialsRequestExtensions::default() + }; + + let result = + Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info); + assert!(matches!( + result, + Err(Error::Ctap(CtapError::UnsupportedExtension)) + )); + } + + #[test] + fn ctap2_extensions_large_blob_required_supported_returns_some_true() { + let info = info_with_options(&[("largeBlobs", true)]); + let requested = MakeCredentialsRequestExtensions { + large_blob: Some(MakeCredentialLargeBlobExtensionInput { + support: MakeCredentialLargeBlobExtension::Required, + }), + ..MakeCredentialsRequestExtensions::default() + }; + + let extensions = + Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info) + .unwrap(); + assert_eq!(extensions.large_blob_key, Some(true)); + } + + #[test] + fn ctap2_extensions_large_blob_preferred_unsupported_omits_request() { + let info = info_with_options(&[("largeBlobs", false)]); + let requested = MakeCredentialsRequestExtensions { + large_blob: Some(MakeCredentialLargeBlobExtensionInput { + support: MakeCredentialLargeBlobExtension::Preferred, + }), + ..MakeCredentialsRequestExtensions::default() + }; + + let extensions = + Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info) + .unwrap(); + assert_eq!(extensions.large_blob_key, None); + } +}