From 365aeb997c902924f98cd5d5566f5e73079e01c5 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Fri, 14 Aug 2026 11:56:12 +1000 Subject: [PATCH] example/ctap2: allow changing `user_verification_req` --- examples/ctap2.rs | 68 +++++++++++++++++++++++++++++++++++++++------ src/ctap2/server.rs | 15 ++++++++++ 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/examples/ctap2.rs b/examples/ctap2.rs index 78701bdc..d096ecc5 100644 --- a/examples/ctap2.rs +++ b/examples/ctap2.rs @@ -5,11 +5,14 @@ use authenticator::{ authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs}, crypto::COSEAlgorithm, - ctap2::server::{ - AuthenticationExtensionsClientInputs, CredentialProtectionPolicy, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, - PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement, Transport, - UserVerificationRequirement, + ctap2::{ + attestation::AuthenticatorDataFlags, + server::{ + AuthenticationExtensionsClientInputs, CredentialProtectionPolicy, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, + PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement, Transport, + UserVerificationRequirement, + }, }, statecallback::StateCallback, Pin, StatusPinUv, StatusUpdate, @@ -48,6 +51,13 @@ fn main() { opts.optflag("s", "hmac_secret", "With hmac-secret"); opts.optflag("h", "help", "print this help menu"); opts.optflag("f", "fallback", "Use CTAP1 fallback implementation"); + opts.optopt( + "u", + "uv", + "User verification requirement (required, preferred, discouraged). Default is \"preferred\".", + "UV", + ); + let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(f) => panic!("{}", f.to_string()), @@ -77,6 +87,21 @@ fn main() { } }; + let user_verification_req = match matches.opt_get_default::( + "uv", + UserVerificationRequirement::Preferred, + ) { + Ok(uv) => { + println!("User verification requirement: {uv:?}"); + uv + } + Err(e) => { + println!("Unknown user verification mode: {e}"); + print_usage(&program, opts); + return; + } + }; + println!("Asking a security key to register now..."); let mut chall_bytes = [0u8; 32]; thread_rng().fill_bytes(&mut chall_bytes); @@ -181,7 +206,7 @@ fn main() { ], transports: vec![Transport::USB, Transport::NFC], }], - user_verification_req: UserVerificationRequirement::Preferred, + user_verification_req, resident_key_req: ResidentKeyRequirement::Discouraged, extensions: AuthenticationExtensionsClientInputs { cred_props: Some(true), @@ -212,6 +237,20 @@ fn main() { .expect("Problem receiving, unable to continue"); match register_result { Ok(a) => { + println!("Register result: {a:?}"); + + let uv = a + .att_obj + .auth_data + .flags + .contains(AuthenticatorDataFlags::USER_VERIFIED); + if user_verification_req == UserVerificationRequirement::Required && !uv { + panic!("User verification is required, but the authenticator did not set the UV flag (WebAuthn-3 §7.1, step 16)"); + } + if uv { + println!("User verified!"); + } + println!("Ok!"); attestation_object = a; break; @@ -220,8 +259,6 @@ fn main() { }; } - println!("Register result: {:?}", &attestation_object); - println!(); println!("*********************************************************************"); println!("Asking a security key to sign now, with the data from the register..."); @@ -242,7 +279,7 @@ fn main() { origin: format!("https://{rp_id}"), relying_party_id: rp_id, allow_list, - user_verification_req: UserVerificationRequirement::Preferred, + user_verification_req, user_presence_req: true, extensions: AuthenticationExtensionsClientInputs { app_id: using_app_id.then(|| app_id.clone()), @@ -270,6 +307,19 @@ fn main() { match sign_result { Ok(assertion_object) => { println!("Assertion Object: {assertion_object:?}"); + + let uv = assertion_object + .assertion + .auth_data + .flags + .contains(AuthenticatorDataFlags::USER_VERIFIED); + if user_verification_req == UserVerificationRequirement::Required && !uv { + panic!("User verification is required, but the authenticator did not set the UV flag (WebAuthn-3 §7.2, step 17)"); + } + if uv { + println!("User verified!"); + } + if using_app_id { println!( "Used AppID: {}", diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index 9b1de8d2..6027dac5 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -12,6 +12,7 @@ use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::convert::{Into, TryFrom}; use std::fmt; +use std::str::FromStr; #[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] pub struct RpIdHash(pub [u8; 32]); @@ -302,6 +303,20 @@ pub enum UserVerificationRequirement { Required, } +impl FromStr for UserVerificationRequirement { + type Err = String; + + fn from_str(s: &str) -> Result { + // https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement + match s { + "required" => Ok(Self::Required), + "preferred" => Ok(Self::Preferred), + "discouraged" => Ok(Self::Discouraged), + s => Err(s.to_string()), + } + } +} + #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum CredentialProtectionPolicy { UserVerificationOptional = 1,