Skip to content

Commit 958894a

Browse files
fix(ctap2): feature-detect credProtect from getInfo (#273)
When a relying party requires a credential protection policy, the client must refuse an authenticator that cannot honor it. The check looked at user verification state, which says nothing about credProtect support. This feature-detects credProtect from the authenticator getInfo and enforces the policy against that. Closes #253.
1 parent 6749f91 commit 958894a

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)