Skip to content

Commit 06793c6

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 6749f91 commit 06793c6

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
}
@@ -498,7 +498,9 @@ pub struct Ctap2MakeCredentialsResponseExtensions {
498498
mod tests {
499499
use super::*;
500500
use crate::ops::webauthn::MakeCredentialLargeBlobExtensionInput;
501-
use crate::ops::webauthn::{MakeCredentialPrfInput, MakeCredentialRequest};
501+
use crate::ops::webauthn::{
502+
CredentialProtectionExtension, MakeCredentialPrfInput, MakeCredentialRequest,
503+
};
502504
use std::collections::HashMap;
503505
use std::time::Duration;
504506

@@ -581,6 +583,78 @@ mod tests {
581583
assert_eq!(extensions.large_blob_key, None);
582584
}
583585

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

0 commit comments

Comments
 (0)