From eaba377f659da6915ff7d7d4bf7bbceece365dc9 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Fri, 21 Aug 2026 10:16:14 +1000 Subject: [PATCH 1/3] User verification info changes: * Update `AuthenticatorOptions::user_verification` docs to use the CTAP 2.3 description, and note some caveats. * Add `AuthenticatorOptions::supports_uv()` helper. --- src/ctap2/commands/get_info.rs | 74 ++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/src/ctap2/commands/get_info.rs b/src/ctap2/commands/get_info.rs index a0eef804..19420c6b 100644 --- a/src/ctap2/commands/get_info.rs +++ b/src/ctap2/commands/get_info.rs @@ -90,23 +90,47 @@ pub struct AuthenticatorOptions { #[serde(rename = "up", default = "true_val")] pub user_presence: bool, - /// Indicates that the device is capable of verifying the user within - /// itself. For example, devices with UI, biometrics fall into this - /// category. - /// If present and set to true, it indicates that the device is capable of - /// user verification within itself and has been configured. - /// If present and set to false, it indicates that the device is capable of - /// user verification within itself and has not been yet configured. For - /// example, a biometric device that has not yet been configured will - /// return this parameter set to false. - /// If absent, it indicates that the device is not capable of user - /// verification within itself. - /// A device that can only do Client PIN will not return the "uv" parameter. - /// If a device is capable of verifying the user within itself as well as - /// able to do Client PIN, it will return both "uv" and the Client PIN - /// option. - // TODO(MS): My Token (key-ID FIDO2) does return Some(false) here, even though - // it has no built-in verification method. Not to be trusted... + /// In CTAP 2.1+, indicates that the authenticator supports + /// [a built-in user verification method][0]. + /// + /// For example, devices with UI, biometrics fall into this category. + /// + /// * If `Some(true)`, it indicates that the device is capable of built-in user verification and + /// its user verification feature is presently configured. + /// + /// * If `Some(false)`, it indicates that the authenticator is capable of built-in user + /// verification and its user verification feature is not presently configured. + /// + /// For example, an authenticator featuring a built-in biometric user verification feature + /// that is not presently configured will return this option set to `Some(false)`. + /// + /// * If `None`, it indicates that the authenticator does not have a built-in user verification + /// capability. + /// + /// A device that can only do Client PIN will return `None`. + /// + /// If a device is capable of both built-in user verification and Client PIN, the authenticator + /// will return both the "uv" and [the "clientPin"][Self::client_pin] option ids. + /// + /// ### Caveats + /// + /// [CTAP 2.0][1] gives the `uv` option a completely different meaning to CTAP 2.1+: + /// + /// > Indicates that the device is capable of verifying the user as part of the + /// > `authenticatorGetAssertion` request. Default: `false` + /// + /// Some CTAP 2.1-PRE authenticators that _only_ support client PIN erroneously return + /// `Some(false)` (eg: Key-ID FIDO2). + /// + /// ### References + /// + /// * [CTAP 2.0][1] (different to CTAP 2.1 and later) + /// * [CTAP 2.1](https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#getinfo-uv) + /// * [CTAP 2.2](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#getinfo-uv) + /// * [CTAP 2.3](https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html#getinfo-uv) + /// + /// [0]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#built-in-user-verification-method + /// [1]: https://fidoalliance.org/specs/fido-v2.0-ps-20170927/fido-client-to-authenticator-protocol-v2.0-ps-20170927.html#authenticatorgetinfo-0x04 #[serde(rename = "uv")] pub user_verification: Option, @@ -369,9 +393,25 @@ impl AuthenticatorInfo { AuthenticatorVersion::U2F_V2 } + /// `true` if the device has been configured with [some form of user verification][0] + /// (ie: a [client PIN][1] is set and/or [built-in user verification][2] is configured). + /// + /// [0]: https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html#some-form-of-user-verification + /// [1]: AuthenticatorOptions::client_pin + /// [2]: AuthenticatorOptions::user_verification pub fn device_is_protected(&self) -> bool { self.options.client_pin == Some(true) || self.options.user_verification == Some(true) } + + /// `true` if the device supports [some form of user verification][0]. + /// + /// This is a mandatory feature on CTAP 2.1+ authenticators that support [resident keys][1]. It + /// + /// [0]: https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html#some-form-of-user-verification + /// [1]: AuthenticatorOptions::resident_key + pub fn supports_uv(&self) -> bool { + self.options.client_pin.is_some() || self.options.user_verification.is_some() + } } impl CtapResponse for AuthenticatorInfo {} From db87010840c8a5263587a539049622adb6698723 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Fri, 21 Aug 2026 10:19:39 +1000 Subject: [PATCH 2/3] GetAssertion: don't skip UV when required, don't skip UV if not discouraged and 'possible'. --- src/ctap2/commands/get_assertion.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 9b1e542d..e62f8fb1 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -489,13 +489,19 @@ impl PinUvAuthCommand for GetAssertion { info: &AuthenticatorInfo, uv_req: UserVerificationRequirement, ) -> bool { - let supports_uv = info.options.user_verification == Some(true); - let pin_configured = info.options.client_pin == Some(true); - let device_protected = supports_uv || pin_configured; + if uv_req == UserVerificationRequirement::Required + || info.options.always_uv.unwrap_or(false) + { + // The RP requires UV, or the authenticator always requires UV (CTAP 2.1 §7.2.2). + return false; + } + let uv_discouraged = uv_req == UserVerificationRequirement::Discouraged; - let always_uv = info.options.always_uv == Some(true); - !always_uv && (!device_protected || uv_discouraged) + // The RP "prefers enforcing UV" (CTAP 2.1 §6.2.1 step 1.1) or + // "prefers UV ... if possible" (WebAuthn-3 §5.8.6). UV is "possible" on + // authenticators that support it, but it might not be configured. + uv_discouraged || !info.supports_uv() } fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> { From eaaf4982b028188a3513b48c57ce02bd6a12e730 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Fri, 21 Aug 2026 11:13:22 +1000 Subject: [PATCH 3/3] MakeCredentials: Fix UV-skipping logic: * Don't skip UV when `uv = required` * Don't skip UV when `uv != discouraged` and UV is "possible" but unconfigured This is rewritten to be closer to the CTAP 2.3 spec's steps. --- src/ctap2/commands/make_credentials.rs | 47 +++++++++++++++----------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index cce1d570..600e2750 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -498,27 +498,36 @@ impl PinUvAuthCommand for MakeCredentials { info: &AuthenticatorInfo, uv_req: UserVerificationRequirement, ) -> bool { - // TODO(MS): Handle here the case where we NEED a UV, the device supports PINs, but hasn't set a PIN. - // For this, the user has to be prompted to set a PIN first (see https://github.com/mozilla/authenticator-rs/issues/223) - - let supports_uv = info.options.user_verification == Some(true); - let pin_configured = info.options.client_pin == Some(true); - - // CTAP 2.0 authenticators require user verification if the device is protected - let device_protected = supports_uv || pin_configured; - - // CTAP 2.1 authenticators may allow the creation of non-discoverable credentials without - // user verification. This is only relevant if the relying party has not requested user - // verification. - let make_cred_uv_not_required = info.options.make_cred_uv_not_rqd == Some(true) - && self.options.resident_key != Some(true) - && uv_req == UserVerificationRequirement::Discouraged; + // TODO(MS): Handle setting up a PIN where needed + // (see https://github.com/mozilla/authenticator-rs/issues/223) + if uv_req == UserVerificationRequirement::Required + || info.options.always_uv.unwrap_or(false) + { + // The RP requires UV, or the authenticator always requires UV (CTAP 2.1 §7.2.2). + return false; + } - // Alternatively, CTAP 2.1 authenticators may require user verification regardless of the - // RP's requirement. - let always_uv = info.options.always_uv == Some(true); + // `true` if we'd plan to not use UV (ie: uv option is false and pinUvAuthParam unset) + let uv_discouraged = uv_req == UserVerificationRequirement::Discouraged; + let make_cred_uv_not_required = info.options.make_cred_uv_not_rqd == Some(true); + let rk = self.options.resident_key == Some(true); + + if info.device_is_protected() && (!make_cred_uv_not_required || rk) { + // CTAP 2.3 §6.1.2 Step 7 only requires UV for RKs if the device is protected and + // supports make_cred_uv_not_rqd. + // https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html#ref-for-getinfo-makecreduvnotrqd%E2%91%A1 + // + // CTAP 2.3 §6.1.2 Step 8 and CTAP 2.0 §5.1 Step 5 require UV if the device is protected + // and does not support make_cred_uv_not_rqd. + // https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html#ref-for-getinfo-makecreduvnotrqd%E2%91%A2 + // https://fidoalliance.org/specs/fido-v2.0-ps-20170927/fido-client-to-authenticator-protocol-v2.0-ps-20170927.html#authenticatorMakeCredential:~:text=If%20pinAuth%20parameter%20is%20not%20present%20and%20clientPin%20been%20set%20on%20the%20authenticator%2C + return false; + } - !always_uv && (!device_protected || make_cred_uv_not_required) + // The RP "prefers enforcing UV" (CTAP 2.1 §6.1.1 step 1.1) or + // "prefers UV ... if possible" (WebAuthn-3 §5.8.6). UV is "possible" on + // authenticators that support it, but it might not be configured. + uv_discouraged || !info.supports_uv() } fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> {