webauthn_rp

WebAuthn Level 3 RP library.
git clone https://git.philomathiclife.com/repos/webauthn_rp
Log | Files | Refs | README

commit e417cff57c7d18af13f0b34d3024cd12a83e4b58
parent fcd94da3dcb4e3353a974c9973cad59a8b083dab
Author: Zack Newman <zack@philomathiclife.com>
Date:   Thu, 12 Jun 2025 14:00:54 -0600

dont require uv for prf

Diffstat:
MCargo.toml | 1-
Msrc/lib.rs | 33+++++++++++++++++++--------------
Msrc/request/auth.rs | 136++++++++++++++++++++++++++-----------------------------------------------------
Msrc/request/auth/error.rs | 25++++++++-----------------
Msrc/request/auth/ser_server_state.rs | 39++++++++++++++-------------------------
Msrc/request/register.rs | 7+++----
Msrc/request/register/error.rs | 4----
Msrc/response.rs | 10+++++-----
8 files changed, 93 insertions(+), 162 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -47,7 +47,6 @@ implicit_return = "allow" min_ident_chars = "allow" missing_trait_methods = "allow" module_name_repetitions = "allow" -multiple_crate_versions = "allow" pub_with_shorthand = "allow" pub_use = "allow" question_mark_used = "allow" diff --git a/src/lib.rs b/src/lib.rs @@ -510,6 +510,10 @@ //! //! [^note]: `panic`s related to memory allocations or stack overflow are possible since such issues are not //! formally guarded against. +#![expect( + clippy::multiple_crate_versions, + reason = "RustCrypto hasn't updated rand yet" +)] #![cfg_attr(docsrs, feature(doc_cfg))] #[cfg(not(any(feature = "custom", all(feature = "bin", feature = "serde"))))] compile_error!("'custom' must be enabled or both 'bin' and 'serde' must be enabled"); @@ -558,7 +562,7 @@ use crate::{ }; use crate::{ request::{ - auth::error::{RequestOptionsErr, SecondFactorErr}, + auth::error::{InvalidTimeout, SecondFactorErr}, error::{AsciiDomainErr, DomainOriginParseErr, PortParseErr, SchemeParseErr, UrlErr}, register::{ ResidentKeyRequirement, USER_HANDLE_MAX_LEN, UserHandle, @@ -695,10 +699,9 @@ pub enum CredentialErr { /// Variant when [`CredentialProtectionPolicy::UserVerificationRequired`], but /// [`DynamicState::user_verified`] is `false`. CredProtectUserVerificationRequiredWithoutUserVerified, - /// Variant when [`ClientExtensionsOutputs::prf`] is - /// `Some(AuthenticationExtensionsPRFOutputs { enabled: true })` and + /// Variant when [`AuthenticatorExtensionOutput::hmac_secret`] is `Some(true)` and /// [`DynamicState::user_verified`] is `false`. - PrfWithoutUserVerified, + HmacSecretWithoutUserVerified, /// Variant when [`AuthenticatorExtensionOutput::hmac_secret`] is `Some(true)`, but /// [`ClientExtensionsOutputs::prf`] is `Some(AuthenticationExtensionsPRFOutputs { enabled: false })` /// or `AuthenticatorExtensionOutput::hmac_secret` is `Some`, but @@ -719,7 +722,9 @@ impl Display for CredentialErr { Self::CredProtectUserVerificationRequiredWithoutUserVerified => { "credProtect requires user verification, but the user is not verified" } - Self::PrfWithoutUserVerified => "prf is enabled, but the user is not verified", + Self::HmacSecretWithoutUserVerified => { + "hmac-secret was enabled, but the user is not verified" + } Self::HmacSecretWithoutPrf => "hmac-secret was enabled but prf was not", Self::PrfWithoutHmacSecret => "prf was enabled, but hmac-secret was not", Self::ResidentKeyRequiredServerCredentialCreated => { @@ -746,11 +751,11 @@ fn verify_static_and_dynamic_state<T>( ) { Err(CredentialErr::CredProtectUserVerificationRequiredWithoutUserVerified) } else if static_state - .client_extension_results - .prf - .is_some_and(|prf| prf.enabled) + .extensions + .hmac_secret + .is_some_and(convert::identity) { - Err(CredentialErr::PrfWithoutUserVerified) + Err(CredentialErr::HmacSecretWithoutUserVerified) } else { Ok(()) } @@ -1127,7 +1132,7 @@ pub enum AggErr { /// Variant when [`DiscoverableCredentialRequestOptions::start_ceremony`] or /// [`NonDiscoverableCredentialRequestOptions::start_ceremony`] /// error. - RequestOptions(RequestOptionsErr), + InvalidTimeout(InvalidTimeout), /// Variant when [`NonDiscoverableCredentialRequestOptions::second_factor`] errors. SecondFactor(SecondFactorErr), /// Variant when [`CredentialCreationOptions::start_ceremony`] errors. @@ -1243,10 +1248,10 @@ impl From<PortParseErr> for AggErr { Self::Port(value) } } -impl From<RequestOptionsErr> for AggErr { +impl From<InvalidTimeout> for AggErr { #[inline] - fn from(value: RequestOptionsErr) -> Self { - Self::RequestOptions(value) + fn from(value: InvalidTimeout) -> Self { + Self::InvalidTimeout(value) } } impl From<SecondFactorErr> for AggErr { @@ -1420,7 +1425,7 @@ impl Display for AggErr { Self::Scheme(err) => err.fmt(f), Self::DomainOrigin(ref err) => err.fmt(f), Self::Port(ref err) => err.fmt(f), - Self::RequestOptions(err) => err.fmt(f), + Self::InvalidTimeout(err) => err.fmt(f), Self::SecondFactor(err) => err.fmt(f), Self::CreationOptions(err) => err.fmt(f), Self::Nickname(err) => err.fmt(f), diff --git a/src/request/auth.rs b/src/request/auth.rs @@ -27,7 +27,7 @@ use super::{ BackupReq, Ceremony, CeremonyOptions, Challenge, CredentialId, CredentialMediationRequirement, Credentials, ExtensionReq, Hint, Origin, PrfInput, PublicKeyCredentialDescriptor, RpId, SentChallenge, THREE_HUNDRED_THOUSAND, TimedCeremony, UserVerificationRequirement, - auth::error::{RequestOptionsErr, SecondFactorErr}, + auth::error::{InvalidTimeout, SecondFactorErr}, }; use core::{ borrow::Borrow, @@ -288,38 +288,6 @@ impl From<Vec<PublicKeyCredentialDescriptor<Vec<u8>>>> for AllowedCredentials { creds } } -/// Helper that verifies the overlap of [`DiscoverableCredentialRequestOptions::start_ceremony`] and -/// [`DiscoverableAuthenticationServerState::decode`]. -const fn validate_discoverable_options_helper( - ext: ServerExtensionInfo, - uv: UserVerificationRequirement, -) -> Result<(), RequestOptionsErr> { - // If PRF is set, the user has to verify themselves. - if matches!(ext.prf, ServerPrfInfo::One(_) | ServerPrfInfo::Two(_)) - && !matches!(uv, UserVerificationRequirement::Required) - { - Err(RequestOptionsErr::PrfWithoutUserVerification) - } else { - Ok(()) - } -} -/// Helper that verifies the overlap of [`NonDiscoverableCredentialRequestOptions::start_ceremony`] and -/// [`NonDiscoverableAuthenticationServerState::decode`]. -fn validate_non_discoverable_options_helper( - uv: UserVerificationRequirement, - creds: &[CredInfo], -) -> Result<(), RequestOptionsErr> { - creds.iter().try_fold((), |(), cred| { - // If PRF is set, the user has to verify themselves. - if matches!(cred.ext.prf, ServerPrfInfo::One(_) | ServerPrfInfo::Two(_)) - && !matches!(uv, UserVerificationRequirement::Required) - { - Err(RequestOptionsErr::PrfWithoutUserVerification) - } else { - Ok(()) - } - }) -} /// The [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions) /// to send to the client when authenticating a discoverable credentential. /// @@ -374,31 +342,27 @@ impl<'rp_id, 'prf_first, 'prf_second> DiscoverableAuthenticationServerState, DiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>, ), - RequestOptionsErr, + InvalidTimeout, > { - let extensions = self.public_key.extensions.into(); - validate_discoverable_options_helper(extensions, self.public_key.user_verification) - .and_then(|()| { - #[cfg(not(feature = "serializable_server_state"))] - let res = Instant::now(); - #[cfg(feature = "serializable_server_state")] - let res = SystemTime::now(); - res.checked_add(Duration::from_millis( - NonZeroU64::from(self.public_key.timeout).get(), - )) - .ok_or(RequestOptionsErr::InvalidTimeout) - .map(|expiration| { - ( - DiscoverableAuthenticationServerState(AuthenticationServerState { - challenge: SentChallenge(self.public_key.challenge.0), - user_verification: self.public_key.user_verification, - extensions, - expiration, - }), - DiscoverableAuthenticationClientState(self), - ) - }) - }) + #[cfg(not(feature = "serializable_server_state"))] + let res = Instant::now(); + #[cfg(feature = "serializable_server_state")] + let res = SystemTime::now(); + res.checked_add(Duration::from_millis( + NonZeroU64::from(self.public_key.timeout).get(), + )) + .ok_or(InvalidTimeout) + .map(|expiration| { + ( + DiscoverableAuthenticationServerState(AuthenticationServerState { + challenge: SentChallenge(self.public_key.challenge.0), + user_verification: self.public_key.user_verification, + extensions: self.public_key.extensions.into(), + expiration, + }), + DiscoverableAuthenticationClientState(self), + ) + }) } } /// The [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions) @@ -516,42 +480,30 @@ impl<'rp_id, 'prf_first, 'prf_second> NonDiscoverableAuthenticationServerState, NonDiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>, ), - RequestOptionsErr, + InvalidTimeout, > { - let extensions = self.options.extensions.into(); - validate_discoverable_options_helper(extensions, self.options.user_verification).and_then( - |()| { - let allow_credentials = Vec::from(&self.allow_credentials); - validate_non_discoverable_options_helper( - self.options.user_verification, - allow_credentials.as_slice(), - ) - .and_then(|()| { - #[cfg(not(feature = "serializable_server_state"))] - let res = Instant::now(); - #[cfg(feature = "serializable_server_state")] - let res = SystemTime::now(); - res.checked_add(Duration::from_millis( - NonZeroU64::from(self.options.timeout).get(), - )) - .ok_or(RequestOptionsErr::InvalidTimeout) - .map(|expiration| { - ( - NonDiscoverableAuthenticationServerState { - state: AuthenticationServerState { - challenge: SentChallenge(self.options.challenge.0), - user_verification: self.options.user_verification, - extensions, - expiration, - }, - allow_credentials, - }, - NonDiscoverableAuthenticationClientState(self), - ) - }) - }) - }, - ) + #[cfg(not(feature = "serializable_server_state"))] + let res = Instant::now(); + #[cfg(feature = "serializable_server_state")] + let res = SystemTime::now(); + res.checked_add(Duration::from_millis( + NonZeroU64::from(self.options.timeout).get(), + )) + .ok_or(InvalidTimeout) + .map(|expiration| { + ( + NonDiscoverableAuthenticationServerState { + state: AuthenticationServerState { + challenge: SentChallenge(self.options.challenge.0), + user_verification: self.options.user_verification, + extensions: self.options.extensions.into(), + expiration, + }, + allow_credentials: Vec::from(&self.allow_credentials), + }, + NonDiscoverableAuthenticationClientState(self), + ) + }) } } /// The [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions) diff --git a/src/request/auth/error.rs b/src/request/auth/error.rs @@ -22,25 +22,16 @@ impl Display for SecondFactorErr { } impl Error for SecondFactorErr {} /// Error returned by [`DiscoverableCredentialRequestOptions::start_ceremony`] -/// and [`NonDiscoverableCredentialRequestOptions::start_ceremony`] +/// and [`NonDiscoverableCredentialRequestOptions::start_ceremony`]. +/// +/// This happens when [`PublicKeyCredentialRequestOptions::timeout`] could not be added to [`Instant::now`] or +/// [`SystemTime::now`]. #[derive(Clone, Copy, Debug)] -pub enum RequestOptionsErr { - /// Error when [`Extension::prf`] or [`CredentialSpecificExtension::prf`] is [`Some`] but - /// [`PublicKeyCredentialRequestOptions::user_verification`] is not - /// [`UserVerificationRequirement::Required`]. - PrfWithoutUserVerification, - /// [`PublicKeyCredentialRequestOptions::timeout`] could not be added to [`Instant::now`] or [`SystemTime::now`]. - InvalidTimeout, -} -impl Display for RequestOptionsErr { +pub struct InvalidTimeout; +impl Display for InvalidTimeout { #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_str(match *self { - Self::PrfWithoutUserVerification => { - "prf extension was requested without requiring user verification" - } - Self::InvalidTimeout => "the timeout could not be added to the current Instant", - }) + f.write_str("the timeout could not be added to the current Instant") } } -impl Error for RequestOptionsErr {} +impl Error for InvalidTimeout {} diff --git a/src/request/auth/ser_server_state.rs b/src/request/auth/ser_server_state.rs @@ -213,16 +213,12 @@ impl<'a> DecodeBuffer<'a> for AuthenticationServerState { SentChallenge::decode_from_buffer(data).and_then(|challenge| { UserVerificationRequirement::decode_from_buffer(data).and_then(|user_verification| { ServerExtensionInfo::decode_from_buffer(data).and_then(|extensions| { - super::validate_discoverable_options_helper(extensions, user_verification) - .map_err(|_e| EncDecErr) - .and_then(|()| { - SystemTime::decode_from_buffer(data).map(|expiration| Self { - challenge, - user_verification, - extensions, - expiration, - }) - }) + SystemTime::decode_from_buffer(data).map(|expiration| Self { + challenge, + user_verification, + extensions, + expiration, + }) }) }) }) @@ -295,21 +291,14 @@ impl Decode for NonDiscoverableAuthenticationServerState { Vec::decode_from_buffer(&mut input) .map_err(|_e| DecodeNonDiscoverableAuthenticationServerStateErr::Other) .and_then(|allow_credentials| { - super::validate_non_discoverable_options_helper( - state.user_verification, - allow_credentials.as_slice(), - ) - .map_err(|_e| DecodeNonDiscoverableAuthenticationServerStateErr::Other) - .and({ - if input.is_empty() { - Ok(Self { - state, - allow_credentials, - }) - } else { - Err(DecodeNonDiscoverableAuthenticationServerStateErr::TrailingData) - } - }) + if input.is_empty() { + Ok(Self { + state, + allow_credentials, + }) + } else { + Err(DecodeNonDiscoverableAuthenticationServerStateErr::TrailingData) + } }) }) } diff --git a/src/request/register.rs b/src/request/register.rs @@ -1282,8 +1282,6 @@ const fn validate_options_helper( UserVerificationRequirement::Required ) { Ok(()) - } else if !matches!(extensions.prf, ServerPrfInfo::None) { - Err(CreationOptionsErr::PrfWithoutUserVerification) } else if matches!( extensions.cred_protect, CredProtect::UserVerificationRequired(_, _) @@ -3656,10 +3654,11 @@ mod tests { validate(opts)?; opts.request.prf_uv = PrfUvOptions::None(false); opts.response.user_verified = false; - opts.response.hmac = HmacSecret::None; + opts.response.hmac = HmacSecret::Enabled; opts.response.prf = Some(true); - assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::PrfWithoutUserVerified))), |_| false)); + assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::HmacSecretWithoutUserVerified))), |_| false)); opts.response.prf = None; + opts.response.hmac = HmacSecret::None; validate(opts)?; Ok(()) } diff --git a/src/request/register/error.rs b/src/request/register/error.rs @@ -53,9 +53,6 @@ impl Error for UsernameErr {} /// Error returned by [`CredentialCreationOptions::start_ceremony`]. #[derive(Clone, Copy, Debug)] pub enum CreationOptionsErr { - /// Error when [`Extension::prf`] is [`Some`] but [`AuthenticatorSelectionCriteria::user_verification`] is not - /// [`UserVerificationRequirement::Required`]. - PrfWithoutUserVerification, /// Error when [`Extension::cred_protect`] is [`CredProtect::UserVerificationRequired`] but [`AuthenticatorSelectionCriteria::user_verification`] is not /// [`UserVerificationRequirement::Required`]. CredProtectRequiredWithoutUserVerification, @@ -66,7 +63,6 @@ impl Display for CreationOptionsErr { #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(match *self { - Self::PrfWithoutUserVerification => "prf extension was requested without requiring user verification", Self::CredProtectRequiredWithoutUserVerification => "credProtect extension with a value of user verification required was requested without requiring user verification", Self::InvalidTimeout => "the timeout could not be added to the current Instant", }) diff --git a/src/response.rs b/src/response.rs @@ -33,7 +33,7 @@ use ser_relaxed::SerdeJsonErr; /// # use data_encoding::BASE64URL_NOPAD; /// # use webauthn_rp::{ /// # hash::hash_set::FixedCapHashSet, -/// # request::{auth::{error::RequestOptionsErr, DiscoverableAuthenticationClientState, DiscoverableCredentialRequestOptions, AuthenticationVerificationOptions}, error::AsciiDomainErr, register::{UserHandle, USER_HANDLE_MAX_LEN, UserHandle64}, AsciiDomain, BackupReq, RpId}, +/// # request::{auth::{error::InvalidTimeout, DiscoverableAuthenticationClientState, DiscoverableCredentialRequestOptions, AuthenticationVerificationOptions}, error::AsciiDomainErr, register::{UserHandle, USER_HANDLE_MAX_LEN, UserHandle64}, AsciiDomain, BackupReq, RpId}, /// # response::{auth::{error::AuthCeremonyErr, DiscoverableAuthentication64}, error::CollectedClientDataErr, register::{AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputsStaticState, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, CompressedPubKey, StaticState}, AuthenticatorAttachment, Backup, CollectedClientData, CredentialId}, /// # AuthenticatedCredential, CredentialErr /// # }; @@ -41,7 +41,7 @@ use ser_relaxed::SerdeJsonErr; /// # enum E { /// # CollectedClientData(CollectedClientDataErr), /// # RpId(AsciiDomainErr), -/// # RequestOptions(RequestOptionsErr), +/// # InvalidTimeout(InvalidTimeout), /// # SerdeJson(serde_json::Error), /// # MissingUserHandle, /// # MissingCeremony, @@ -59,9 +59,9 @@ use ser_relaxed::SerdeJsonErr; /// # Self::CollectedClientData(value) /// # } /// # } -/// # impl From<RequestOptionsErr> for E { -/// # fn from(value: RequestOptionsErr) -> Self { -/// # Self::RequestOptions(value) +/// # impl From<InvalidTimeout> for E { +/// # fn from(value: InvalidTimeout) -> Self { +/// # Self::InvalidTimeout(value) /// # } /// # } /// # impl From<serde_json::Error> for E {