Skip to content

Commit 23216b6

Browse files
fix(ctap2): feature-detect credProtect via getInfo extensions when enforcing policy
The enforcement gate keyed off is_uv_protected, which reports whether a PIN or built-in UV is configured rather than whether the authenticator implements credProtect. Detect the extension from the getInfo extensions array instead. This refuses authenticators that would silently drop an enforced policy and stops rejecting credProtect-capable authenticators that have no PIN or UV set yet.
1 parent d593d6b commit 23216b6

1 file changed

Lines changed: 76 additions & 2 deletions

File tree

libwebauthn/src/proto/ctap2/model/make_credential.rs

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ impl Ctap2MakeCredentialsRequestExtensions {
237237
if let Some(cred_protection) = requested_extensions.cred_protect.as_ref() {
238238
if cred_protection.enforce_policy
239239
&& cred_protection.policy != CredentialProtectionPolicy::UserVerificationOptional
240-
&& !info.is_uv_protected()
240+
&& !info.supports_extension("credProtect")
241241
{
242242
return Err(Error::Ctap(CtapError::UnsupportedExtension));
243243
}
@@ -496,7 +496,9 @@ pub struct Ctap2MakeCredentialsResponseExtensions {
496496
mod tests {
497497
use super::*;
498498
use crate::ops::webauthn::MakeCredentialLargeBlobExtensionInput;
499-
use crate::ops::webauthn::{MakeCredentialPrfInput, MakeCredentialRequest};
499+
use crate::ops::webauthn::{
500+
CredentialProtectionExtension, MakeCredentialPrfInput, MakeCredentialRequest,
501+
};
500502
use std::collections::HashMap;
501503
use std::time::Duration;
502504

@@ -579,6 +581,78 @@ mod tests {
579581
assert_eq!(extensions.large_blob_key, None);
580582
}
581583

584+
fn requested_with_cred_protect(
585+
policy: CredentialProtectionPolicy,
586+
enforce_policy: bool,
587+
) -> MakeCredentialsRequestExtensions {
588+
MakeCredentialsRequestExtensions {
589+
cred_protect: Some(CredentialProtectionExtension {
590+
policy,
591+
enforce_policy,
592+
}),
593+
..MakeCredentialsRequestExtensions::default()
594+
}
595+
}
596+
597+
#[test]
598+
fn cred_protect_enforced_above_optional_without_support_returns_unsupported_extension() {
599+
// UV-protected but credProtect not advertised: must fail, not silently drop the policy.
600+
let info = info_with_options(&[("clientPin", true)]);
601+
let requested =
602+
requested_with_cred_protect(CredentialProtectionPolicy::UserVerificationRequired, true);
603+
let result =
604+
Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info);
605+
assert!(matches!(
606+
result,
607+
Err(Error::Ctap(CtapError::UnsupportedExtension))
608+
));
609+
}
610+
611+
#[test]
612+
fn cred_protect_enforced_above_optional_with_support_carries_policy() {
613+
// credProtect advertised but no PIN/UV set yet: still honour the policy.
614+
let info = info_with_extensions(&["credProtect"]);
615+
let requested =
616+
requested_with_cred_protect(CredentialProtectionPolicy::UserVerificationRequired, true);
617+
let extensions =
618+
Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info)
619+
.unwrap();
620+
assert_eq!(
621+
extensions.cred_protect,
622+
Some(Ctap2CredentialProtectionPolicy::Required)
623+
);
624+
}
625+
626+
#[test]
627+
fn cred_protect_enforced_optional_is_never_rejected() {
628+
let info = Ctap2GetInfoResponse::default();
629+
let requested =
630+
requested_with_cred_protect(CredentialProtectionPolicy::UserVerificationOptional, true);
631+
let extensions =
632+
Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info)
633+
.unwrap();
634+
assert_eq!(
635+
extensions.cred_protect,
636+
Some(Ctap2CredentialProtectionPolicy::Optional)
637+
);
638+
}
639+
640+
#[test]
641+
fn cred_protect_not_enforced_above_optional_is_never_rejected() {
642+
let info = Ctap2GetInfoResponse::default();
643+
let requested = requested_with_cred_protect(
644+
CredentialProtectionPolicy::UserVerificationRequired,
645+
false,
646+
);
647+
let extensions =
648+
Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info)
649+
.unwrap();
650+
assert_eq!(
651+
extensions.cred_protect,
652+
Some(Ctap2CredentialProtectionPolicy::Required)
653+
);
654+
}
655+
582656
fn info_with_extensions(exts: &[&str]) -> Ctap2GetInfoResponse {
583657
Ctap2GetInfoResponse {
584658
extensions: Some(exts.iter().map(|s| s.to_string()).collect()),

0 commit comments

Comments
 (0)