Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions libwebauthn/src/ops/webauthn/make_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,32 @@ impl DowngradableRequest<RegisterRequest> 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
}

Expand Down
97 changes: 92 additions & 5 deletions libwebauthn/src/ops/webauthn/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -82,7 +83,11 @@ pub trait DowngradableRequest<T> {

#[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,
};
Expand Down Expand Up @@ -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());
}
}
92 changes: 88 additions & 4 deletions libwebauthn/src/proto/ctap2/model/make_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -386,3 +384,89 @@ pub struct Ctap2MakeCredentialsResponseExtensions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_pin_length: Option<u32>,
}

#[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);
}
}
Loading