webauthn_rp

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

commit 06862a895a528d5842d3ec5ca4f10823755fb0fe
parent 2d457f2d60480a8c170dcdb903bdf33058e0e1f3
Author: Zack Newman <zack@philomathiclife.com>
Date:   Tue, 31 Mar 2026 16:08:19 -0600

change how hints are done in preparation for to be added

Diffstat:
Msrc/request.rs | 376++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/request/auth.rs | 8++++----
Msrc/request/auth/ser.rs | 36++++++++++++++++++++----------------
Msrc/request/register.rs | 236+++++++++++++++++++++++++++++--------------------------------------------------
Msrc/request/register/error.rs | 4++++
Msrc/request/register/ser.rs | 414+++++++++++++++++++++++--------------------------------------------------------
Msrc/request/register/ser_server_state.rs | 86++++++-------------------------------------------------------------------------
Msrc/request/ser.rs | 405+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/request/ser_server_state.rs | 81+------------------------------------------------------------------------------
Msrc/response.rs | 3++-
Msrc/response/register/error.rs | 8++++----
11 files changed, 782 insertions(+), 875 deletions(-)

diff --git a/src/request.rs b/src/request.rs @@ -10,7 +10,7 @@ use super::{ }, register::{CredentialCreationOptions, RegistrationServerState}, }, - response::register::ClientExtensionsOutputs, + response::{AuthenticatorAttachment, register::ClientExtensionsOutputs}, }; use crate::{ request::{ @@ -1148,42 +1148,214 @@ pub enum UserVerificationRequirement { /// [`preferred`](https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-preferred). Preferred, } -/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint). -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub enum Hint { - /// No hints. - #[default] - None, +/// [`PublicKeyCredentialHint`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PublicKeyCredentialHint { /// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-security-key). SecurityKey, /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-client-device). ClientDevice, /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-hybrid). Hybrid, - /// [`Self::SecurityKey`] and [`Self::ClientDevice`]. - SecurityKeyClientDevice, - /// [`Self::ClientDevice`] and [`Self::SecurityKey`]. - ClientDeviceSecurityKey, - /// [`Self::SecurityKey`] and [`Self::Hybrid`]. - SecurityKeyHybrid, - /// [`Self::Hybrid`] and [`Self::SecurityKey`]. - HybridSecurityKey, - /// [`Self::ClientDevice`] and [`Self::Hybrid`]. - ClientDeviceHybrid, - /// [`Self::Hybrid`] and [`Self::ClientDevice`]. - HybridClientDevice, - /// [`Self::SecurityKeyClientDevice`] and [`Self::Hybrid`]. - SecurityKeyClientDeviceHybrid, - /// [`Self::SecurityKeyHybrid`] and [`Self::ClientDevice`]. - SecurityKeyHybridClientDevice, - /// [`Self::ClientDeviceSecurityKey`] and [`Self::Hybrid`]. - ClientDeviceSecurityKeyHybrid, - /// [`Self::ClientDeviceHybrid`] and [`Self::SecurityKey`]. - ClientDeviceHybridSecurityKey, - /// [`Self::HybridSecurityKey`] and [`Self::ClientDevice`]. - HybridSecurityKeyClientDevice, - /// [`Self::HybridClientDevice`] and [`Self::SecurityKey`]. - HybridClientDeviceSecurityKey, +} +impl PublicKeyCredentialHint { + /// Returns `true` iff `self` is the same as `other`. + const fn is_eq(self, other: Self) -> bool { + match self { + Self::SecurityKey => matches!(other, Self::SecurityKey), + Self::ClientDevice => matches!(other, Self::ClientDevice), + Self::Hybrid => matches!(other, Self::Hybrid), + } + } +} +/// Unique sequence of [`PublicKeyCredentialHint`]. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct Hints([Option<PublicKeyCredentialHint>; 3]); +impl Hints { + /// Empty sequence of [`PublicKeyCredentialHint`]s. + pub const EMPTY: Self = Self([None; 3]); + /// Adds `hint` to `self` iff `self` doesn't already contain `hint`. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::{Hints, PublicKeyCredentialHint}; + /// assert_eq!( + /// Hints::EMPTY + /// .add(PublicKeyCredentialHint::SecurityKey) + /// .first(), + /// Some(PublicKeyCredentialHint::SecurityKey) + /// ); + /// ``` + #[inline] + #[must_use] + pub const fn add(mut self, hint: PublicKeyCredentialHint) -> Self { + let mut vals = self.0.as_mut_slice(); + while let [ref mut first, ref mut rem @ ..] = *vals { + match *first { + None => { + *first = Some(hint); + return self; + } + Some(h) => { + if h.is_eq(hint) { + return self; + } + } + } + vals = rem; + } + self + } + /// Returns the first `PublicKeyCredentialHint`. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::Hints; + /// assert!(Hints::EMPTY.first().is_none()); + /// ``` + #[inline] + #[must_use] + pub const fn first(self) -> Option<PublicKeyCredentialHint> { + self.0[0] + } + /// Returns the second `PublicKeyCredentialHint`. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::Hints; + /// assert!(Hints::EMPTY.second().is_none()); + /// ``` + #[inline] + #[must_use] + pub const fn second(self) -> Option<PublicKeyCredentialHint> { + self.0[1] + } + /// Returns the third `PublicKeyCredentialHint`. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::Hints; + /// assert!(Hints::EMPTY.third().is_none()); + /// ``` + #[inline] + #[must_use] + pub const fn third(self) -> Option<PublicKeyCredentialHint> { + self.0[2] + } + /// Returns the number of [`PublicKeyCredentialHint`]s in `self`. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::Hints; + /// assert_eq!(Hints::EMPTY.count(), 0); + /// ``` + #[expect( + clippy::arithmetic_side_effects, + clippy::as_conversions, + reason = "comment justifies correctness" + )] + #[inline] + #[must_use] + pub const fn count(self) -> u8 { + // `bool as u8` is well-defined. This maxes at 3, so overflow isn't possible. + self.first().is_some() as u8 + self.second().is_some() as u8 + self.third().is_some() as u8 + } + /// Returns `true` iff `self` is empty. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::Hints; + /// assert!(Hints::EMPTY.is_empty()); + /// ``` + #[inline] + #[must_use] + pub const fn is_empty(self) -> bool { + self.count() == 0 + } + /// Returns `true` iff `self` contains `hint`. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::{Hints, PublicKeyCredentialHint}; + /// assert!(!Hints::EMPTY.contains(PublicKeyCredentialHint::Hybrid)); + /// ``` + #[inline] + #[must_use] + pub const fn contains(self, hint: PublicKeyCredentialHint) -> bool { + let mut vals = self.0.as_slice(); + while let [ref first, ref rem @ ..] = *vals { + match *first { + None => return false, + Some(h) => { + if h.is_eq(hint) { + return true; + } + } + } + vals = rem; + } + false + } + /// Returns `true` iff `self` contains a `hint` that maps to [`AuthenticatorAttachment::CrossPlatform`]. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::Hints; + /// assert!(!Hints::EMPTY.contains_platform_hints()); + /// ``` + #[inline] + #[must_use] + pub const fn contains_cross_platform_hints(self) -> bool { + let mut vals = self.0.as_slice(); + while let [ref first, ref rem @ ..] = *vals { + match *first { + None => return false, + Some(h) => { + if matches!( + h, + PublicKeyCredentialHint::SecurityKey | PublicKeyCredentialHint::Hybrid + ) { + return true; + } + } + } + vals = rem; + } + false + } + /// Returns `true` iff `self` contains a `hint` that maps to [`AuthenticatorAttachment::Platform`]. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::Hints; + /// assert!(!Hints::EMPTY.contains_platform_hints()); + /// ``` + #[inline] + #[must_use] + pub const fn contains_platform_hints(self) -> bool { + let mut vals = self.0.as_slice(); + while let [ref first, ref rem @ ..] = *vals { + match *first { + None => return false, + Some(h) => { + if h.is_eq(PublicKeyCredentialHint::ClientDevice) { + return true; + } + } + } + vals = rem; + } + false + } } /// Controls if the response to a requested extension is required to be sent back. /// @@ -1702,7 +1874,6 @@ impl<'first, 'second> PrfInput<'first, 'second> { pub const FIVE_MINUTES: NonZeroU32 = NonZeroU32::new(300_000).unwrap(); #[cfg(test)] mod tests { - use super::AsciiDomainStatic; #[cfg(feature = "custom")] use super::{ super::{ @@ -1736,6 +1907,7 @@ mod tests { UserHandle, }, }; + use super::{AsciiDomainStatic, Hints, PublicKeyCredentialHint}; #[cfg(feature = "custom")] use ed25519_dalek::{Signer as _, SigningKey}; #[cfg(feature = "custom")] @@ -10539,4 +10711,144 @@ mod tests { )?); Ok(()) } + #[expect( + clippy::cognitive_complexity, + clippy::too_many_lines, + reason = "a lot to test" + )] + #[test] + fn hints() { + assert_eq!(Hints::EMPTY.0, [None; 3]); + assert!( + Hints::EMPTY.first().is_none() + && Hints::EMPTY.second().is_none() + && Hints::EMPTY.third().is_none() + ); + assert_eq!(Hints::EMPTY.count(), 0); + assert!(Hints::EMPTY.is_empty()); + assert!( + !(Hints::EMPTY.contains_cross_platform_hints() + || Hints::EMPTY.contains_platform_hints()) + ); + assert!(!Hints::EMPTY.contains(PublicKeyCredentialHint::SecurityKey)); + assert!(!Hints::EMPTY.contains(PublicKeyCredentialHint::ClientDevice)); + assert!(!Hints::EMPTY.contains(PublicKeyCredentialHint::Hybrid)); + let mut hints = Hints::EMPTY.add(PublicKeyCredentialHint::SecurityKey); + assert_eq!( + hints.0, + [Some(PublicKeyCredentialHint::SecurityKey), None, None] + ); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::SecurityKey).0); + assert!( + hints.first() == Some(PublicKeyCredentialHint::SecurityKey) + && hints.second().is_none() + && hints.third().is_none() + ); + assert_eq!(hints.count(), 1); + assert!(!hints.is_empty()); + assert!(hints.contains_cross_platform_hints() && !hints.contains_platform_hints()); + assert!(hints.contains(PublicKeyCredentialHint::SecurityKey)); + assert!(!hints.contains(PublicKeyCredentialHint::ClientDevice)); + assert!(!hints.contains(PublicKeyCredentialHint::Hybrid)); + hints = Hints::EMPTY.add(PublicKeyCredentialHint::Hybrid); + assert_eq!(hints.0, [Some(PublicKeyCredentialHint::Hybrid), None, None]); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::Hybrid).0); + assert!( + hints.first() == Some(PublicKeyCredentialHint::Hybrid) + && hints.second().is_none() + && hints.third().is_none() + ); + assert_eq!(hints.count(), 1); + assert!(!hints.is_empty()); + assert!(hints.contains_cross_platform_hints() && !hints.contains_platform_hints()); + assert!(hints.contains(PublicKeyCredentialHint::Hybrid)); + assert!(!hints.contains(PublicKeyCredentialHint::SecurityKey)); + assert!(!hints.contains(PublicKeyCredentialHint::ClientDevice)); + hints = Hints::EMPTY.add(PublicKeyCredentialHint::ClientDevice); + assert_eq!( + hints.0, + [Some(PublicKeyCredentialHint::ClientDevice), None, None] + ); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::ClientDevice).0); + assert!( + hints.first() == Some(PublicKeyCredentialHint::ClientDevice) + && hints.second().is_none() + && hints.third().is_none() + ); + assert_eq!(hints.count(), 1); + assert!(!hints.is_empty()); + assert!(!hints.contains_cross_platform_hints() && hints.contains_platform_hints()); + assert!(hints.contains(PublicKeyCredentialHint::ClientDevice)); + assert!(!hints.contains(PublicKeyCredentialHint::SecurityKey)); + assert!(!hints.contains(PublicKeyCredentialHint::Hybrid)); + hints = hints.add(PublicKeyCredentialHint::Hybrid); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::ClientDevice).0); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::Hybrid).0); + assert_eq!( + hints.0, + [ + Some(PublicKeyCredentialHint::ClientDevice), + Some(PublicKeyCredentialHint::Hybrid), + None + ] + ); + assert!( + hints.first() == Some(PublicKeyCredentialHint::ClientDevice) + && hints.second() == Some(PublicKeyCredentialHint::Hybrid) + && hints.third().is_none() + ); + assert_eq!(hints.count(), 2); + assert!(!hints.is_empty()); + assert!(hints.contains_cross_platform_hints() && hints.contains_platform_hints()); + assert!(hints.contains(PublicKeyCredentialHint::ClientDevice)); + assert!(!hints.contains(PublicKeyCredentialHint::SecurityKey)); + assert!(hints.contains(PublicKeyCredentialHint::Hybrid)); + hints = hints.add(PublicKeyCredentialHint::SecurityKey); + assert_eq!( + hints.0, + [ + Some(PublicKeyCredentialHint::ClientDevice), + Some(PublicKeyCredentialHint::Hybrid), + Some(PublicKeyCredentialHint::SecurityKey), + ] + ); + assert!( + hints.first() == Some(PublicKeyCredentialHint::ClientDevice) + && hints.second() == Some(PublicKeyCredentialHint::Hybrid) + && hints.third() == Some(PublicKeyCredentialHint::SecurityKey) + ); + assert_eq!(hints.count(), 3); + assert!(!hints.is_empty()); + assert!(hints.contains_cross_platform_hints() && hints.contains_platform_hints()); + assert!(hints.contains(PublicKeyCredentialHint::ClientDevice)); + assert!(hints.contains(PublicKeyCredentialHint::SecurityKey)); + assert!(hints.contains(PublicKeyCredentialHint::Hybrid)); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::SecurityKey).0); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::Hybrid).0); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::ClientDevice).0); + hints = Hints::EMPTY + .add(PublicKeyCredentialHint::Hybrid) + .add(PublicKeyCredentialHint::SecurityKey); + assert_eq!( + hints.0, + [ + Some(PublicKeyCredentialHint::Hybrid), + Some(PublicKeyCredentialHint::SecurityKey), + None, + ] + ); + assert!( + hints.first() == Some(PublicKeyCredentialHint::Hybrid) + && hints.second() == Some(PublicKeyCredentialHint::SecurityKey) + && hints.third().is_none() + ); + assert_eq!(hints.count(), 2); + assert!(!hints.is_empty()); + assert!(hints.contains_cross_platform_hints() && !hints.contains_platform_hints()); + assert!(!hints.contains(PublicKeyCredentialHint::ClientDevice)); + assert!(hints.contains(PublicKeyCredentialHint::SecurityKey)); + assert!(hints.contains(PublicKeyCredentialHint::Hybrid)); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::SecurityKey).0); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::Hybrid).0); + } } diff --git a/src/request/auth.rs b/src/request/auth.rs @@ -25,8 +25,8 @@ use super::{ }, }, BackupReq, Ceremony, CeremonyOptions, Challenge, CredentialId, CredentialMediationRequirement, - Credentials, ExtensionReq, FIVE_MINUTES, Hint, Origin, PrfInput, PublicKeyCredentialDescriptor, - RpId, SentChallenge, TimedCeremony, UserVerificationRequirement, + Credentials, ExtensionReq, FIVE_MINUTES, Hints, Origin, PrfInput, + PublicKeyCredentialDescriptor, RpId, SentChallenge, TimedCeremony, UserVerificationRequirement, auth::error::{InvalidTimeout, NonDiscoverableCredentialRequestOptionsErr}, }; use core::{ @@ -559,7 +559,7 @@ pub struct PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> { /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification). pub user_verification: UserVerificationRequirement, /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-hints). - pub hints: Hint, + pub hints: Hints, /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-extensions). pub extensions: Extension<'prf_first, 'prf_second>, } @@ -589,7 +589,7 @@ impl<'rp_id> PublicKeyCredentialRequestOptions<'rp_id, '_, '_> { timeout: FIVE_MINUTES, rp_id, user_verification: UserVerificationRequirement::Required, - hints: Hint::None, + hints: Hints::EMPTY, extensions: Extension::default(), } } diff --git a/src/request/auth/ser.rs b/src/request/auth/ser.rs @@ -2,7 +2,7 @@ use super::{ super::{super::response::ser::Null, ser::PrfHelper}, AllowedCredential, AllowedCredentials, Challenge, CredentialMediationRequirement, Credentials as _, DiscoverableAuthenticationClientState, DiscoverableCredentialRequestOptions, - Extension, ExtensionReq, FIVE_MINUTES, Hint, NonDiscoverableAuthenticationClientState, + Extension, ExtensionReq, FIVE_MINUTES, Hints, NonDiscoverableAuthenticationClientState, NonDiscoverableCredentialRequestOptions, PrfInput, PrfInputOwned, PublicKeyCredentialRequestOptions, RpId, UserVerificationRequirement, }; @@ -348,13 +348,13 @@ impl Serialize for DiscoverableAuthenticationClientState<'_, '_, '_> { /// # AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Extension, /// # PrfInputOwned, DiscoverableCredentialRequestOptions /// # }, - /// # AsciiDomain, ExtensionReq, Hint, PrfInput, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement, + /// # AsciiDomain, ExtensionReq, Hints, PublicKeyCredentialHint, PrfInput, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement, /// # }, /// # response::{AuthTransports, CredentialId}, /// # }; /// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); /// let mut options = DiscoverableCredentialRequestOptions::passkey(&rp_id); - /// options.public_key.hints = Hint::SecurityKey; + /// options.public_key.hints = Hints::EMPTY.add(PublicKeyCredentialHint::SecurityKey); /// options.public_key.extensions = Extension { /// prf: Some((PrfInput { /// first: [0; 4].as_slice(), @@ -412,7 +412,7 @@ impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_, '_> { /// # AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Extension, /// # PrfInputOwned, NonDiscoverableCredentialRequestOptions /// # }, - /// # AsciiDomain, ExtensionReq, Hint, PrfInput, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement, + /// # AsciiDomain, ExtensionReq, Hints, PublicKeyCredentialHint, PrfInput, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement, /// # }, /// # response::{AuthTransports, CredentialId}, /// # }; @@ -449,7 +449,7 @@ impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_, '_> { /// let opts = &mut options.options; /// # #[cfg(not(all(feature = "bin", feature = "custom")))] /// # let mut opts = webauthn_rp::DiscoverableCredentialRequestOptions::passkey(&rp_id).public_key; - /// opts.hints = Hint::SecurityKey; + /// opts.hints = Hints::EMPTY.add(PublicKeyCredentialHint::SecurityKey); /// // This is actually useless since `CredentialSpecificExtension` takes priority /// // when the client receives the payload. We set it for illustration purposes only. /// // If `creds` contained an `AllowedCredential` that didn't set @@ -681,7 +681,7 @@ pub struct PublicKeyCredentialRequestOptionsOwned { /// See [`PublicKeyCredentialRequestOptions::user_verification`]. pub user_verification: UserVerificationRequirement, /// See [`PublicKeyCredentialRequestOptions::hints`]. - pub hints: Hint, + pub hints: Hints, /// See [`PublicKeyCredentialRequestOptions::extensions`]. pub extensions: ExtensionOwned, } @@ -783,7 +783,7 @@ impl Default for PublicKeyCredentialRequestOptionsOwned { rp_id: None, timeout: FIVE_MINUTES, user_verification: UserVerificationRequirement::Preferred, - hints: Hint::default(), + hints: Hints::default(), extensions: ExtensionOwned::default(), } } @@ -1076,8 +1076,9 @@ impl<'de> Deserialize<'de> for ClientCredentialRequestOptions { #[cfg(test)] mod test { use super::{ - super::ExtensionReq, ClientCredentialRequestOptions, CredentialMediationRequirement, - ExtensionOwned, FIVE_MINUTES, Hint, NonZeroU32, PublicKeyCredentialRequestOptionsOwned, + super::{super::PublicKeyCredentialHint, ExtensionReq}, + ClientCredentialRequestOptions, CredentialMediationRequirement, ExtensionOwned, + FIVE_MINUTES, Hints, NonZeroU32, PublicKeyCredentialRequestOptionsOwned, UserVerificationRequirement, }; use serde_json::Error; @@ -1114,7 +1115,7 @@ mod test { options.public_key.user_verification, UserVerificationRequirement::Preferred )); - assert!(matches!(options.public_key.hints, Hint::None)); + assert_eq!(options.public_key.hints, Hints::EMPTY); assert!(options.public_key.extensions.prf.is_none()); options = serde_json::from_str::<ClientCredentialRequestOptions>( r#"{"mediation":null,"publicKey":null}"#, @@ -1129,7 +1130,7 @@ mod test { options.public_key.user_verification, UserVerificationRequirement::Preferred )); - assert!(matches!(options.public_key.hints, Hint::None)); + assert_eq!(options.public_key.hints, Hints::EMPTY); assert!(options.public_key.extensions.prf.is_none()); options = serde_json::from_str::<ClientCredentialRequestOptions>(r#"{"publicKey":{}}"#)?; assert!(options.public_key.rp_id.is_none()); @@ -1138,7 +1139,7 @@ mod test { options.public_key.user_verification, UserVerificationRequirement::Preferred )); - assert!(matches!(options.public_key.hints, Hint::None)); + assert_eq!(options.public_key.hints, Hints::EMPTY); assert!(options.public_key.extensions.prf.is_none()); options = serde_json::from_str::<ClientCredentialRequestOptions>( r#"{"mediation":"conditional","publicKey":{"rpId":"example.com","timeout":300000,"allowCredentials":[],"userVerification":"required","extensions":{"prf":{"eval":{"first":"","second":""}}},"hints":["security-key"],"challenge":null}}"#, @@ -1226,7 +1227,7 @@ mod test { UserVerificationRequirement::Preferred )); assert!(key.extensions.prf.is_none()); - assert!(matches!(key.hints, Hint::None)); + assert_eq!(key.hints, Hints::EMPTY); key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( r#"{"rpId":null,"timeout":null,"allowCredentials":null,"userVerification":null,"extensions":null,"hints":null,"challenge":null}"#, )?; @@ -1237,7 +1238,7 @@ mod test { UserVerificationRequirement::Preferred )); assert!(key.extensions.prf.is_none()); - assert!(matches!(key.hints, Hint::None)); + assert_eq!(key.hints, Hints::EMPTY); key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( r#"{"allowCredentials":[],"extensions":{},"hints":[]}"#, )?; @@ -1245,7 +1246,7 @@ mod test { key.user_verification, UserVerificationRequirement::Preferred )); - assert!(matches!(key.hints, Hint::None)); + assert_eq!(key.hints, Hints::EMPTY); assert!(key.extensions.prf.is_none()); key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( r#"{"extensions":{"prf":null}}"#, @@ -1260,7 +1261,10 @@ mod test { key.user_verification, UserVerificationRequirement::Required )); - assert!(matches!(key.hints, Hint::SecurityKey)); + assert_eq!( + key.hints, + Hints::EMPTY.add(PublicKeyCredentialHint::SecurityKey) + ); assert!(key.extensions.prf.is_some_and(|prf| prf.first.is_empty() && prf.second.is_some_and(|p| p.is_empty()) && matches!(prf.ext_req, ExtensionReq::Allow))); diff --git a/src/request/register.rs b/src/request/register.rs @@ -13,7 +13,7 @@ use super::{ }, }, BackupReq, Ceremony, Challenge, CredentialMediationRequirement, ExtensionInfo, ExtensionReq, - FIVE_MINUTES, Hint, Origin, PrfInput, PublicKeyCredentialDescriptor, RpId, SentChallenge, + FIVE_MINUTES, Hints, Origin, PrfInput, PublicKeyCredentialDescriptor, RpId, SentChallenge, TimedCeremony, UserVerificationRequirement, register::error::{CreationOptionsErr, NicknameErr, UsernameErr}, }; @@ -1314,112 +1314,38 @@ pub enum ResidentKeyRequirement { /// [`preferred`](https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-preferred). Preferred, } -/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint) -/// for [`AuthenticatorAttachment::CrossPlatform`] authenticators. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub enum CrossPlatformHint { - /// No hints. - #[default] - None, - /// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-security-key). - SecurityKey, - /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-hybrid). - Hybrid, - /// [`Self::SecurityKey`] and [`Self::Hybrid`]. - SecurityKeyHybrid, - /// [`Self::Hybrid`] and [`Self::SecurityKey`]. - HybridSecurityKey, -} -impl From<CrossPlatformHint> for Hint { - #[inline] - fn from(value: CrossPlatformHint) -> Self { - match value { - CrossPlatformHint::None => Self::None, - CrossPlatformHint::SecurityKey => Self::SecurityKey, - CrossPlatformHint::Hybrid => Self::Hybrid, - CrossPlatformHint::SecurityKeyHybrid => Self::SecurityKeyHybrid, - CrossPlatformHint::HybridSecurityKey => Self::HybridSecurityKey, - } - } -} -/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint) -/// for [`AuthenticatorAttachment::Platform`] authenticators. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub enum PlatformHint { - /// No hints. - #[default] - None, - /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-client-device). - ClientDevice, -} -impl From<PlatformHint> for Hint { - #[inline] - fn from(value: PlatformHint) -> Self { - match value { - PlatformHint::None => Self::None, - PlatformHint::ClientDevice => Self::ClientDevice, - } - } -} -/// [`AuthenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment) -/// requirement with associated hints for further refinement. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum AuthenticatorAttachmentReq { - /// No attachment information (i.e., any [`AuthenticatorAttachment`]). - None(Hint), - /// [`platform`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattachment-platform) is required - /// to be used. - Platform(PlatformHint), - /// [`cross-platform`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattachment-cross-platform) is - /// required to be used. - CrossPlatform(CrossPlatformHint), -} -impl Default for AuthenticatorAttachmentReq { - #[inline] - fn default() -> Self { - Self::None(Hint::default()) - } -} -impl AuthenticatorAttachmentReq { - /// Validates `self` against `other` ignoring [`Self::immutable_attachment`]. - const fn validate( - self, - require_response: bool, - other: AuthenticatorAttachment, - ) -> Result<(), RegCeremonyErr> { +impl AuthenticatorAttachment { + /// Validates `self` against `other` based on `require_response`. + const fn validate(self, require_response: bool, other: Self) -> Result<(), RegCeremonyErr> { match self { - Self::None(_) => { - if require_response && matches!(other, AuthenticatorAttachment::None) { + Self::None => { + if require_response && matches!(other, Self::None) { Err(RegCeremonyErr::MissingAuthenticatorAttachment) } else { Ok(()) } } - Self::Platform(_) => match other { - AuthenticatorAttachment::None => { + Self::Platform => match other { + Self::None => { if require_response { Err(RegCeremonyErr::MissingAuthenticatorAttachment) } else { Ok(()) } } - AuthenticatorAttachment::Platform => Ok(()), - AuthenticatorAttachment::CrossPlatform => { - Err(RegCeremonyErr::AuthenticatorAttachmentMismatch) - } + Self::Platform => Ok(()), + Self::CrossPlatform => Err(RegCeremonyErr::AuthenticatorAttachmentMismatch), }, - Self::CrossPlatform(_) => match other { - AuthenticatorAttachment::None => { + Self::CrossPlatform => match other { + Self::None => { if require_response { Err(RegCeremonyErr::MissingAuthenticatorAttachment) } else { Ok(()) } } - AuthenticatorAttachment::CrossPlatform => Ok(()), - AuthenticatorAttachment::Platform => { - Err(RegCeremonyErr::AuthenticatorAttachmentMismatch) - } + Self::CrossPlatform => Ok(()), + Self::Platform => Err(RegCeremonyErr::AuthenticatorAttachmentMismatch), }, } } @@ -1428,7 +1354,7 @@ impl AuthenticatorAttachmentReq { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct AuthenticatorSelectionCriteria { /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-authenticatorattachment). - pub authenticator_attachment: AuthenticatorAttachmentReq, + pub authenticator_attachment: AuthenticatorAttachment, /// [`residentKey`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-residentkey). pub resident_key: ResidentKeyRequirement, /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification). @@ -1442,30 +1368,22 @@ impl AuthenticatorSelectionCriteria { /// # Examples /// /// ``` - /// # use webauthn_rp::request::{ + /// # use webauthn_rp::{request::{ /// # register::{ - /// # AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, ResidentKeyRequirement, + /// # AuthenticatorSelectionCriteria, ResidentKeyRequirement, /// # }, - /// # Hint, UserVerificationRequirement, - /// # }; + /// # UserVerificationRequirement, + /// # }, response::AuthenticatorAttachment}; /// let crit = AuthenticatorSelectionCriteria::passkey(); - /// assert!( - /// matches!(crit.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) - /// ); - /// assert!(matches!( - /// crit.resident_key, - /// ResidentKeyRequirement::Required - /// )); - /// assert!(matches!( - /// crit.user_verification, - /// UserVerificationRequirement::Required - /// )); + /// assert_eq!(crit.authenticator_attachment, AuthenticatorAttachment::None); + /// assert_eq!(crit.resident_key, ResidentKeyRequirement::Required); + /// assert_eq!(crit.user_verification, UserVerificationRequirement::Required); /// ``` #[inline] #[must_use] pub fn passkey() -> Self { Self { - authenticator_attachment: AuthenticatorAttachmentReq::default(), + authenticator_attachment: AuthenticatorAttachment::default(), resident_key: ResidentKeyRequirement::Required, user_verification: UserVerificationRequirement::Required, } @@ -1488,30 +1406,22 @@ impl AuthenticatorSelectionCriteria { /// # Examples /// /// ``` - /// # use webauthn_rp::request::{ + /// # use webauthn_rp::{request::{ /// # register::{ - /// # AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, ResidentKeyRequirement, + /// # AuthenticatorSelectionCriteria, ResidentKeyRequirement, /// # }, - /// # Hint, UserVerificationRequirement, - /// # }; + /// # UserVerificationRequirement, + /// # }, response::AuthenticatorAttachment}; /// let crit = AuthenticatorSelectionCriteria::second_factor(); - /// assert!( - /// matches!(crit.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) - /// ); - /// assert!(matches!( - /// crit.resident_key, - /// ResidentKeyRequirement::Discouraged - /// )); - /// assert!(matches!( - /// crit.user_verification, - /// UserVerificationRequirement::Discouraged - /// )); + /// assert_eq!(crit.authenticator_attachment, AuthenticatorAttachment::None); + /// assert_eq!(crit.resident_key, ResidentKeyRequirement::Discouraged); + /// assert_eq!(crit.user_verification, UserVerificationRequirement::Discouraged); /// ``` #[inline] #[must_use] pub fn second_factor() -> Self { Self { - authenticator_attachment: AuthenticatorAttachmentReq::default(), + authenticator_attachment: AuthenticatorAttachment::default(), resident_key: ResidentKeyRequirement::Discouraged, user_verification: UserVerificationRequirement::Discouraged, } @@ -1696,34 +1606,57 @@ impl< let extensions = self.public_key.extensions.into(); validate_options_helper(self.public_key.authenticator_selection, extensions).and_then( |()| { - #[cfg(not(feature = "serializable_server_state"))] - let now = Instant::now(); - #[cfg(feature = "serializable_server_state")] - let now = SystemTime::now(); - now.checked_add(Duration::from_millis( - NonZeroU64::from(self.public_key.timeout).get(), - )) - .ok_or(CreationOptionsErr::InvalidTimeout) - .map(|expiration| { - // We remove duplicates. The order has no significance, so this is OK. - self.public_key - .exclude_credentials - .sort_unstable_by(|a, b| a.id.as_ref().cmp(b.id.as_ref())); - self.public_key - .exclude_credentials - .dedup_by(|a, b| a.id.as_ref() == b.id.as_ref()); - ( - RegistrationServerState { - mediation: self.mediation, - challenge: SentChallenge(self.public_key.challenge.0), - pub_key_cred_params: self.public_key.pub_key_cred_params, - authenticator_selection: self.public_key.authenticator_selection, - extensions, - expiration, - user_id: *self.public_key.user.id, - }, - RegistrationClientState(self), - ) + match self + .public_key + .authenticator_selection + .authenticator_attachment + { + AuthenticatorAttachment::None => Ok(()), + AuthenticatorAttachment::Platform => { + if self.public_key.hints.contains_cross_platform_hints() { + Err(CreationOptionsErr::HintsIncompatibleWithAuthAttachment) + } else { + Ok(()) + } + } + AuthenticatorAttachment::CrossPlatform => { + if self.public_key.hints.contains_platform_hints() { + Err(CreationOptionsErr::HintsIncompatibleWithAuthAttachment) + } else { + Ok(()) + } + } + } + .and_then(|()| { + #[cfg(not(feature = "serializable_server_state"))] + let now = Instant::now(); + #[cfg(feature = "serializable_server_state")] + let now = SystemTime::now(); + now.checked_add(Duration::from_millis( + NonZeroU64::from(self.public_key.timeout).get(), + )) + .ok_or(CreationOptionsErr::InvalidTimeout) + .map(|expiration| { + // We remove duplicates. The order has no significance, so this is OK. + self.public_key + .exclude_credentials + .sort_unstable_by(|a, b| a.id.as_ref().cmp(b.id.as_ref())); + self.public_key + .exclude_credentials + .dedup_by(|a, b| a.id.as_ref() == b.id.as_ref()); + ( + RegistrationServerState { + mediation: self.mediation, + challenge: SentChallenge(self.public_key.challenge.0), + pub_key_cred_params: self.public_key.pub_key_cred_params, + authenticator_selection: self.public_key.authenticator_selection, + extensions, + expiration, + user_id: *self.public_key.user.id, + }, + RegistrationClientState(self), + ) + }) }) }, ) @@ -1793,6 +1726,8 @@ pub struct PublicKeyCredentialCreationOptions< pub exclude_credentials: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>, /// [`authenticatorSelection`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-authenticatorselection). pub authenticator_selection: AuthenticatorSelectionCriteria, + /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-hints). + pub hints: Hints, /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions). pub extensions: Extension<'prf_first, 'prf_second>, } @@ -1857,6 +1792,7 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize> timeout: FIVE_MINUTES, exclude_credentials, authenticator_selection: AuthenticatorSelectionCriteria::passkey(), + hints: Hints::EMPTY, extensions: Extension { cred_props: None, cred_protect: CredProtect::UserVerificationRequired( @@ -2767,7 +2703,7 @@ mod tests { .encode() .map_err(AggErr::EncodeRegistrationServerState)?; assert_eq!(enc_data.capacity(), enc_data.len()); - assert_eq!(enc_data.len(), 1 + 16 + 1 + 4 + (1 + 3 + 3 + 2) + 12 + 1); + assert_eq!(enc_data.len(), 1 + 16 + 1 + 3 + (1 + 3 + 3 + 2) + 12 + 1); assert!(server.is_eq(&RegistrationServerState::decode(enc_data.as_slice())?)); Ok(()) } diff --git a/src/request/register/error.rs b/src/request/register/error.rs @@ -56,6 +56,9 @@ pub enum CreationOptionsErr { /// Error when [`Extension::cred_protect`] is [`CredProtect::UserVerificationRequired`] but [`AuthenticatorSelectionCriteria::user_verification`] is not /// [`UserVerificationRequirement::Required`]. CredProtectRequiredWithoutUserVerification, + /// Error when [`PublicKeyCredentialCreationOptions::hints`] is not compatible with + /// [`AuthenticatorSelectionCriteria::authenticator_attachment`]. + HintsIncompatibleWithAuthAttachment, /// [`PublicKeyCredentialCreationOptions::timeout`] could not be added to [`Instant::now`] or [`SystemTime::now`]. InvalidTimeout, } @@ -64,6 +67,7 @@ impl Display for CreationOptionsErr { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(match *self { Self::CredProtectRequiredWithoutUserVerification => "credProtect extension with a value of user verification required was requested without requiring user verification", + Self::HintsIncompatibleWithAuthAttachment => "hints are not compatible with the requested authenticator attachment modality", Self::InvalidTimeout => "the timeout could not be added to the current Instant", }) } diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs @@ -5,16 +5,13 @@ use super::{ auth::PrfInputOwned, ser::PrfHelper, }, - AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, Challenge, CoseAlgorithmIdentifier, + AuthenticatorAttachment, AuthenticatorSelectionCriteria, Challenge, CoseAlgorithmIdentifier, CoseAlgorithmIdentifiers, CredProtect, CredentialCreationOptions, - CredentialMediationRequirement, CrossPlatformHint, DisplayName, Extension, ExtensionInfo, - ExtensionReq, FIVE_MINUTES, FourToSixtyThree, Hint, Nickname, PlatformHint, PrfInput, - PublicKeyCredentialCreationOptions, PublicKeyCredentialDescriptor, - PublicKeyCredentialUserEntity, RegistrationClientState, ResidentKeyRequirement, RpId, - UserHandle, UserVerificationRequirement, Username, + CredentialMediationRequirement, DisplayName, Extension, ExtensionInfo, ExtensionReq, + FIVE_MINUTES, FourToSixtyThree, Hints, Nickname, PrfInput, PublicKeyCredentialCreationOptions, + PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, RegistrationClientState, + ResidentKeyRequirement, RpId, UserHandle, UserVerificationRequirement, Username, }; -#[cfg(doc)] -use crate::response::AuthenticatorAttachment; use alloc::borrow::Cow; #[cfg(doc)] use core::str::FromStr; @@ -403,70 +400,6 @@ impl Serialize for ResidentKeyRequirement { }) } } -impl Serialize for CrossPlatformHint { - /// Serializes `self` to conform with - /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-hints). - /// - /// # Examples - /// - /// ``` - /// # use webauthn_rp::request::register::CrossPlatformHint; - /// assert_eq!( - /// serde_json::to_string(&CrossPlatformHint::None)?, - /// r#"[]"# - /// ); - /// assert_eq!( - /// serde_json::to_string(&CrossPlatformHint::SecurityKey)?, - /// r#"["security-key"]"# - /// ); - /// assert_eq!( - /// serde_json::to_string(&CrossPlatformHint::Hybrid)?, - /// r#"["hybrid"]"# - /// ); - /// assert_eq!( - /// serde_json::to_string(&CrossPlatformHint::SecurityKeyHybrid)?, - /// r#"["security-key","hybrid"]"# - /// ); - /// assert_eq!( - /// serde_json::to_string(&CrossPlatformHint::HybridSecurityKey)?, - /// r#"["hybrid","security-key"]"# - /// ); - /// # Ok::<_, serde_json::Error>(()) - /// ``` - #[inline] - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - Hint::from(*self).serialize(serializer) - } -} -impl Serialize for PlatformHint { - /// Serializes `self` to conform with - /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-hints). - /// - /// # Examples - /// - /// ``` - /// # use webauthn_rp::request::register::PlatformHint; - /// assert_eq!( - /// serde_json::to_string(&PlatformHint::None)?, - /// r#"[]"# - /// ); - /// assert_eq!( - /// serde_json::to_string(&PlatformHint::ClientDevice)?, - /// r#"["client-device"]"# - /// ); - /// # Ok::<_, serde_json::Error>(()) - /// ``` - #[inline] - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - Hint::from(*self).serialize(serializer) - } -} /// `"platform"`. const PLATFORM: &str = "platform"; /// `"cross-platform"`. @@ -486,7 +419,7 @@ impl Serialize for AuthenticatorSelectionCriteria { /// # Examples /// /// ``` - /// # use webauthn_rp::request::register::{AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, CrossPlatformHint}; + /// # use webauthn_rp::{request::register::AuthenticatorSelectionCriteria, response::AuthenticatorAttachment}; /// assert_eq!( /// serde_json::to_string(&AuthenticatorSelectionCriteria::passkey())?, /// r#"{"residentKey":"required","requireResidentKey":true,"userVerification":"required"}"# @@ -496,9 +429,7 @@ impl Serialize for AuthenticatorSelectionCriteria { /// r#"{"residentKey":"discouraged","requireResidentKey":false,"userVerification":"discouraged"}"# /// ); /// let mut crit = AuthenticatorSelectionCriteria::passkey(); - /// crit.authenticator_attachment = AuthenticatorAttachmentReq::CrossPlatform( - /// CrossPlatformHint::SecurityKey, - /// ); + /// crit.authenticator_attachment = AuthenticatorAttachment::CrossPlatform; /// assert_eq!( /// serde_json::to_string(&crit)?, /// r#"{"authenticatorAttachment":"cross-platform","residentKey":"required","requireResidentKey":true,"userVerification":"required"}"# @@ -510,10 +441,7 @@ impl Serialize for AuthenticatorSelectionCriteria { where S: Serializer, { - let count = if matches!( - self.authenticator_attachment, - AuthenticatorAttachmentReq::None(_) - ) { + let count = if matches!(self.authenticator_attachment, AuthenticatorAttachment::None) { 3 } else { 4 @@ -528,7 +456,7 @@ impl Serialize for AuthenticatorSelectionCriteria { AUTHENTICATOR_ATTACHMENT, if matches!( self.authenticator_attachment, - AuthenticatorAttachmentReq::Platform(_) + AuthenticatorAttachment::Platform ) { PLATFORM } else { @@ -739,33 +667,42 @@ where &self.pub_key_cred_params, ) .and_then(|()| { - ser.serialize_field(TIMEOUT, &self.timeout).and_then( - |()| { - ser.serialize_field( - EXCLUDE_CREDENTIALS, - self.exclude_credentials.as_slice(), - ) - .and_then(|()| { + ser.serialize_field(TIMEOUT, &self.timeout).and_then(|()| { + ser.serialize_field( + EXCLUDE_CREDENTIALS, + self.exclude_credentials.as_slice(), + ) + .and_then( + |()| { ser.serialize_field( AUTHENTICATOR_SELECTION, &self.authenticator_selection, ) .and_then(|()| { - ser.serialize_field(HINTS, &match self.authenticator_selection.authenticator_attachment { - AuthenticatorAttachmentReq::None(hint) => hint, - AuthenticatorAttachmentReq::Platform(hint) => hint.into(), - AuthenticatorAttachmentReq::CrossPlatform(hint) => hint.into(), - }).and_then(|()| { - ser.serialize_field(ATTESTATION, NONE).and_then(|()| { - ser.serialize_field(ATTESTATION_FORMATS, [NONE].as_slice()).and_then(|()| { - ser.serialize_field(EXTENSIONS, &self.extensions).and_then(|()| ser.end()) + ser.serialize_field(HINTS, &self.hints) + .and_then(|()| { + ser.serialize_field( + ATTESTATION, + NONE, + ) + .and_then(|()| { + ser.serialize_field( + ATTESTATION_FORMATS, + [NONE].as_slice(), + ) + .and_then(|()| { + ser.serialize_field( + EXTENSIONS, + &self.extensions, + ) + .and_then(|()| ser.end()) + }) }) }) - }) }) - }) - }, - ) + }, + ) + }) }) }) }) @@ -867,11 +804,11 @@ where /// # use webauthn_rp::{ /// # request::{ /// # register::{ - /// # FourToSixtyThree, UserHandle64, AuthenticatorAttachmentReq, CredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle + /// # FourToSixtyThree, UserHandle64, CredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle /// # }, - /// # AsciiDomain, ExtensionInfo, Hint, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement, + /// # AsciiDomain, ExtensionInfo, Hints, PublicKeyCredentialHint, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement, /// # }, - /// # response::{AuthTransports, CredentialId}, + /// # response::{AuthTransports, AuthenticatorAttachment, CredentialId}, /// # }; /// /// Retrieves the `AuthTransports` associated with the unique `cred_id` /// /// from the database. @@ -893,7 +830,8 @@ where /// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); /// let user_handle = UserHandle64::new(); /// let mut options = CredentialCreationOptions::passkey(&rp_id, PublicKeyCredentialUserEntity { name: "pierre.de.fermat".try_into()?, id: &user_handle, display_name: "Pierre de Fermat".try_into()?, }, creds); - /// options.public_key.authenticator_selection.authenticator_attachment = AuthenticatorAttachmentReq::None(Hint::SecurityKey); + /// options.public_key.authenticator_selection.authenticator_attachment = AuthenticatorAttachment::None; + /// options.public_key.hints = Hints::EMPTY.add(PublicKeyCredentialHint::SecurityKey); /// options.public_key.extensions.min_pin_length = Some((FourToSixtyThree::Sixteen, ExtensionInfo::RequireEnforceValue)); /// # #[cfg(all(feature = "bin", feature = "custom"))] /// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState::serialize")); @@ -1959,53 +1897,6 @@ impl<'de> Deserialize<'de> for UserVerificationRequirement { Requirement::deserialize(deserializer).map(Self::from) } } -impl<'de> Deserialize<'de> for AuthenticatorAttachmentReq { - /// Deserializes a [`prim@str`] according to - /// [`AuthenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment). - /// - /// Note the contained hint will be none (e.g., [`PlatformHint::None`]). - /// - /// # Examples - /// - /// ``` - /// # use webauthn_rp::request::register::{AuthenticatorAttachmentReq, PlatformHint}; - /// assert!(matches!( - /// serde_json::from_str(r#""platform""#)?, - /// AuthenticatorAttachmentReq::Platform(hint) if matches!(hint, PlatformHint::None) - /// )); - /// # Ok::<_, serde_json::Error>(()) - /// ``` - #[inline] - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - /// `Visitor` for `AuthenticatorAttachmentReq`. - struct AuthenticatorAttachmentReqVisitor; - impl Visitor<'_> for AuthenticatorAttachmentReqVisitor { - type Value = AuthenticatorAttachmentReq; - fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - write!(formatter, "'{PLATFORM}' or '{CROSS_PLATFORM}'") - } - fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> - where - E: Error, - { - match v { - PLATFORM => Ok(AuthenticatorAttachmentReq::Platform(PlatformHint::None)), - CROSS_PLATFORM => Ok(AuthenticatorAttachmentReq::CrossPlatform( - CrossPlatformHint::None, - )), - _ => Err(E::invalid_value( - Unexpected::Str(v), - &format!("'{PLATFORM}' or '{CROSS_PLATFORM}'").as_str(), - )), - } - } - } - deserializer.deserialize_str(AuthenticatorAttachmentReqVisitor) - } -} impl<'de> Deserialize<'de> for AuthenticatorSelectionCriteria { /// Deserializes a `struct` based on /// [`AuthenticatorSelectionCriteria`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorselectioncriteria). @@ -2021,12 +1912,10 @@ impl<'de> Deserialize<'de> for AuthenticatorSelectionCriteria { /// # Examples /// /// ``` - /// # use webauthn_rp::request::{Hint, register::{AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria}}; - /// assert!( - /// matches!( - /// serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{"authenticatorAttachment":null,"residentKey":"required","requireResidentKey":true,"userVerification":"required"}"#)?.authenticator_attachment, - /// AuthenticatorAttachmentReq::None(hints) if matches!(hints, Hint::None) - /// ) + /// # use webauthn_rp::{request::register::AuthenticatorSelectionCriteria, response::AuthenticatorAttachment}; + /// assert_eq!( + /// serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{"authenticatorAttachment":null,"residentKey":"required","requireResidentKey":true,"userVerification":"required"}"#)?.authenticator_attachment, + /// AuthenticatorAttachment::None, /// ); /// # Ok::<_, serde_json::Error>(()) /// ``` @@ -2599,6 +2488,8 @@ pub struct PublicKeyCredentialCreationOptionsOwned< pub timeout: NonZeroU32, /// See [`PublicKeyCredentialCreationOptions::authenticator_selection`]. pub authenticator_selection: AuthenticatorSelectionCriteria, + /// See [`PublicKeyCredentialCreationOptions::hints`]. + pub hints: Hints, /// See [`PublicKeyCredentialCreationOptions::extensions`]. pub extensions: ExtensionOwned, } @@ -2657,6 +2548,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER timeout: self.timeout, exclude_credentials, authenticator_selection: self.authenticator_selection, + hints: self.hints, extensions: self.extensions.as_extension(), }) }) @@ -2688,6 +2580,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER timeout: self.timeout, exclude_credentials, authenticator_selection: self.authenticator_selection, + hints: self.hints, extensions: self.extensions.as_extension(), }) } @@ -2726,6 +2619,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER timeout: self.timeout, exclude_credentials, authenticator_selection: self.authenticator_selection, + hints: self.hints, extensions: self.extensions.as_extension(), }) } @@ -2760,6 +2654,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER timeout: self.timeout, exclude_credentials, authenticator_selection: self.authenticator_selection, + hints: self.hints, extensions, }) }) @@ -2792,6 +2687,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER timeout: self.timeout, exclude_credentials, authenticator_selection: self.authenticator_selection, + hints: self.hints, extensions: self.extensions.as_extension(), } } @@ -2825,6 +2721,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER timeout: self.timeout, exclude_credentials, authenticator_selection: self.authenticator_selection, + hints: self.hints, extensions, }) } @@ -2872,6 +2769,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER timeout: self.timeout, exclude_credentials, authenticator_selection: self.authenticator_selection, + hints: self.hints, extensions, }) } @@ -2912,6 +2810,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER timeout: self.timeout, exclude_credentials, authenticator_selection: self.authenticator_selection, + hints: self.hints, extensions, } } @@ -2925,10 +2824,11 @@ impl<const USER_LEN: usize> Default for PublicKeyCredentialCreationOptionsOwned< pub_key_cred_params: CoseAlgorithmIdentifiers::default(), timeout: FIVE_MINUTES, authenticator_selection: AuthenticatorSelectionCriteria { - authenticator_attachment: AuthenticatorAttachmentReq::None(Hint::None), + authenticator_attachment: AuthenticatorAttachment::default(), resident_key: ResidentKeyRequirement::Discouraged, user_verification: UserVerificationRequirement::Preferred, }, + hints: Hints::EMPTY, extensions: ExtensionOwned::default(), } } @@ -3058,7 +2958,7 @@ where let mut time = None; let mut exclude = None; let mut auth = None; - let mut hint: Option<Hint> = None; + let mut hint = None; let mut ext = None; let mut attest = None; let mut formats = None; @@ -3107,98 +3007,13 @@ where if auth.is_some() { return Err(Error::duplicate_field(AUTHENTICATOR_SELECTION)); } - auth = map.next_value::<Option<AuthenticatorSelectionCriteria>>().and_then(|opt| { - opt.map_or(Ok(Some(AuthenticatorSelectionCriteria { authenticator_attachment: AuthenticatorAttachmentReq::default(), resident_key: ResidentKeyRequirement::Discouraged, user_verification: UserVerificationRequirement::Preferred, })), |mut crit| { - let h = hint.unwrap_or_default(); - match crit.authenticator_attachment { - AuthenticatorAttachmentReq::None(ref mut hi) => { - *hi = h; - Ok(Some(crit)) - } - AuthenticatorAttachmentReq::Platform(ref mut hi) => { - match h { - Hint::None => Ok(Some(crit)), - Hint::ClientDevice => { - *hi = PlatformHint::ClientDevice; - Ok(Some(crit)) - } - Hint::SecurityKey | Hint::Hybrid | Hint::SecurityKeyClientDevice | Hint::ClientDeviceSecurityKey | Hint::SecurityKeyHybrid | Hint::HybridSecurityKey | Hint::ClientDeviceHybrid | Hint::HybridClientDevice | Hint::SecurityKeyClientDeviceHybrid | Hint::SecurityKeyHybridClientDevice | Hint::ClientDeviceSecurityKeyHybrid | Hint::ClientDeviceHybridSecurityKey | Hint::HybridSecurityKeyClientDevice | Hint::HybridClientDeviceSecurityKey => Err(Error::custom("'platform' authenticator attachment modality must coincide with no hints or 'client-device' hint")), - } - } - AuthenticatorAttachmentReq::CrossPlatform(ref mut hi) => { - match h { - Hint::None => Ok(Some(crit)), - Hint::SecurityKey => { - *hi = CrossPlatformHint::SecurityKey; - Ok(Some(crit)) - } - Hint::Hybrid => { - *hi = CrossPlatformHint::Hybrid; - Ok(Some(crit)) - } - Hint::SecurityKeyHybrid => { - *hi = CrossPlatformHint::SecurityKeyHybrid; - Ok(Some(crit)) - } - Hint::HybridSecurityKey => { - *hi = CrossPlatformHint::HybridSecurityKey; - Ok(Some(crit)) - } - Hint::ClientDevice | Hint::SecurityKeyClientDevice | Hint::ClientDeviceSecurityKey | Hint::ClientDeviceHybrid | Hint::HybridClientDevice | Hint::SecurityKeyClientDeviceHybrid | Hint::SecurityKeyHybridClientDevice | Hint::ClientDeviceSecurityKeyHybrid | Hint::ClientDeviceHybridSecurityKey | Hint::HybridSecurityKeyClientDevice | Hint::HybridClientDeviceSecurityKey => Err(Error::custom("'cross-platform' authenticator attachment modality must coincide with no hints or hints that lack 'client-device'")), - } - } - } - }) - })?; + auth = map.next_value::<Option<_>>().map(Some)?; } Field::Hints => { if hint.is_some() { return Err(Error::duplicate_field(HINTS)); } - hint = map.next_value::<Option<Hint>>().and_then(|opt| { - opt.map_or(Ok(Some(Hint::None)), |h| { - auth.as_mut().map_or(Ok(Some(h)), |crit| { - match crit.authenticator_attachment { - AuthenticatorAttachmentReq::None(ref mut hi) => { - *hi = h; - Ok(Some(h)) - } - AuthenticatorAttachmentReq::Platform(ref mut hi) => { - match h{ - Hint::None => Ok(Some(h)), - Hint::ClientDevice => { - *hi = PlatformHint::ClientDevice; - Ok(Some(h)) - } - Hint::SecurityKey | Hint::Hybrid | Hint::SecurityKeyClientDevice | Hint::ClientDeviceSecurityKey | Hint::SecurityKeyHybrid | Hint::HybridSecurityKey | Hint::ClientDeviceHybrid | Hint::HybridClientDevice | Hint::SecurityKeyClientDeviceHybrid | Hint::SecurityKeyHybridClientDevice | Hint::ClientDeviceSecurityKeyHybrid | Hint::ClientDeviceHybridSecurityKey | Hint::HybridSecurityKeyClientDevice | Hint::HybridClientDeviceSecurityKey => Err(Error::custom("'platform' authenticator attachment modality must coincide with no hints or 'client-device' hint")), - } - } - AuthenticatorAttachmentReq::CrossPlatform(ref mut hi) => { - match h { - Hint::None => Ok(Some(h)), - Hint::SecurityKey => { - *hi = CrossPlatformHint::SecurityKey; - Ok(Some(h)) - } - Hint::Hybrid => { - *hi = CrossPlatformHint::Hybrid; - Ok(Some(h)) - } - Hint::SecurityKeyHybrid => { - *hi = CrossPlatformHint::SecurityKeyHybrid; - Ok(Some(h)) - } - Hint::HybridSecurityKey => { - *hi = CrossPlatformHint::HybridSecurityKey; - Ok(Some(h)) - } - Hint::ClientDevice | Hint::SecurityKeyClientDevice | Hint::ClientDeviceSecurityKey | Hint::ClientDeviceHybrid | Hint::HybridClientDevice | Hint::SecurityKeyClientDeviceHybrid | Hint::SecurityKeyHybridClientDevice | Hint::ClientDeviceSecurityKeyHybrid | Hint::ClientDeviceHybridSecurityKey | Hint::HybridSecurityKeyClientDevice | Hint::HybridClientDeviceSecurityKey => Err(Error::custom("'cross-platform' authenticator attachment modality must coincide with no hints or hints that lack 'client-device'")), - } - } - } - }) - }) - })?; + hint = map.next_value::<Option<_>>().map(Some)?; } Field::Extensions => { if ext.is_some() { @@ -3225,11 +3040,14 @@ where user: user_info.flatten().unwrap_or_default(), pub_key_cred_params: params.flatten().unwrap_or_default(), timeout: time.flatten().unwrap_or(FIVE_MINUTES), - authenticator_selection: auth.unwrap_or(AuthenticatorSelectionCriteria { - authenticator_attachment: AuthenticatorAttachmentReq::None(Hint::None), - resident_key: ResidentKeyRequirement::Discouraged, - user_verification: UserVerificationRequirement::Preferred, - }), + authenticator_selection: auth.flatten().unwrap_or( + AuthenticatorSelectionCriteria { + authenticator_attachment: AuthenticatorAttachment::None, + resident_key: ResidentKeyRequirement::Discouraged, + user_verification: UserVerificationRequirement::Preferred, + }, + ), + hints: hint.flatten().unwrap_or_default(), extensions: ext.flatten().unwrap_or_default(), }) } @@ -3385,12 +3203,11 @@ where #[cfg(test)] mod test { use super::{ - AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, - ClientCredentialCreationOptions, CoseAlgorithmIdentifier, CoseAlgorithmIdentifiers, - CredProtect, CredentialMediationRequirement, CrossPlatformHint, ExtensionInfo, - ExtensionOwned, ExtensionReq, FIVE_MINUTES, FourToSixtyThree, Hint, NonZeroU32, - PlatformHint, PublicKeyCredentialCreationOptionsOwned, PublicKeyCredentialUserEntityOwned, - ResidentKeyRequirement, UserVerificationRequirement, + AuthenticatorAttachment, AuthenticatorSelectionCriteria, ClientCredentialCreationOptions, + CoseAlgorithmIdentifier, CoseAlgorithmIdentifiers, CredProtect, + CredentialMediationRequirement, ExtensionInfo, ExtensionOwned, ExtensionReq, FIVE_MINUTES, + FourToSixtyThree, NonZeroU32, PublicKeyCredentialCreationOptionsOwned, + PublicKeyCredentialUserEntityOwned, ResidentKeyRequirement, UserVerificationRequirement, }; use serde_json::Error; #[expect( @@ -3434,8 +3251,12 @@ mod test { CoseAlgorithmIdentifiers::ALL.0 ); assert_eq!(options.public_key.timeout, FIVE_MINUTES); - assert!( - matches!(options.public_key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) + assert_eq!( + options + .public_key + .authenticator_selection + .authenticator_attachment, + AuthenticatorAttachment::None, ); assert!(matches!( options.public_key.authenticator_selection.resident_key, @@ -3468,8 +3289,12 @@ mod test { CoseAlgorithmIdentifiers::ALL.0 ); assert_eq!(options.public_key.timeout, FIVE_MINUTES); - assert!( - matches!(options.public_key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) + assert_eq!( + options + .public_key + .authenticator_selection + .authenticator_attachment, + AuthenticatorAttachment::None, ); assert!(matches!( options.public_key.authenticator_selection.resident_key, @@ -3498,8 +3323,12 @@ mod test { CoseAlgorithmIdentifiers::ALL.0 ); assert_eq!(options.public_key.timeout, FIVE_MINUTES); - assert!( - matches!(options.public_key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) + assert_eq!( + options + .public_key + .authenticator_selection + .authenticator_attachment, + AuthenticatorAttachment::None, ); assert!(matches!( options.public_key.authenticator_selection.resident_key, @@ -3562,8 +3391,12 @@ mod test { .0 ); assert_eq!(options.public_key.timeout, FIVE_MINUTES); - assert!( - matches!(options.public_key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::CrossPlatform(hint) if matches!(hint, CrossPlatformHint::SecurityKey)) + assert_eq!( + options + .public_key + .authenticator_selection + .authenticator_attachment, + AuthenticatorAttachment::CrossPlatform, ); assert!(matches!( options.public_key.authenticator_selection.resident_key, @@ -3633,15 +3466,6 @@ mod test { Some("duplicate field `attestation`") ); err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>( - r#"{"authenticatorSelection":{"authenticatorAttachment":"platform"},"hints":["client-device", "security-key"]}"#, - ).unwrap_err(); - assert_eq!( - err.to_string().get(..96), - Some( - "'platform' authenticator attachment modality must coincide with no hints or 'client-device' hint" - ) - ); - err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>( r#"{"challenge":"AAAAAAAAAAAAAAAAAAAAAA"}"#, ) .unwrap_err(); @@ -3704,8 +3528,9 @@ mod test { assert!(key.user.display_name.is_none()); assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0); assert_eq!(key.timeout, FIVE_MINUTES); - assert!( - matches!(key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) + assert_eq!( + key.authenticator_selection.authenticator_attachment, + AuthenticatorAttachment::None, ); assert!(matches!( key.authenticator_selection.resident_key, @@ -3728,8 +3553,9 @@ mod test { assert!(key.user.display_name.is_none()); assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0); assert_eq!(key.timeout, FIVE_MINUTES); - assert!( - matches!(key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) + assert_eq!( + key.authenticator_selection.authenticator_attachment, + AuthenticatorAttachment::None, ); assert!(matches!( key.authenticator_selection.resident_key, @@ -3751,8 +3577,9 @@ mod test { assert!(key.user.id.is_none()); assert!(key.user.display_name.is_none()); assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0); - assert!( - matches!(key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) + assert_eq!( + key.authenticator_selection.authenticator_attachment, + AuthenticatorAttachment::None, ); assert!(matches!( key.authenticator_selection.resident_key, @@ -3774,8 +3601,9 @@ mod test { assert!(key.user.id.is_none()); assert!(key.user.display_name.is_none()); assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0); - assert!( - matches!(key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) + assert_eq!( + key.authenticator_selection.authenticator_attachment, + AuthenticatorAttachment::None, ); assert!(matches!( key.authenticator_selection.resident_key, @@ -3812,8 +3640,9 @@ mod test { .0 ); assert_eq!(key.timeout, FIVE_MINUTES); - assert!( - matches!(key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::CrossPlatform(hint) if matches!(hint, CrossPlatformHint::SecurityKey)) + assert_eq!( + key.authenticator_selection.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform, ); assert!(matches!( key.authenticator_selection.resident_key, @@ -4034,8 +3863,9 @@ mod test { let mut crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( r#"{"authenticatorAttachment":"platform","residentKey":"required","requireResidentKey":true,"userVerification":"required"}"#, )?; - assert!( - matches!(crit.authenticator_attachment, AuthenticatorAttachmentReq::Platform(hint) if matches!(hint, PlatformHint::None)) + assert_eq!( + crit.authenticator_attachment, + AuthenticatorAttachment::Platform, ); assert!(matches!( crit.resident_key, @@ -4048,9 +3878,7 @@ mod test { crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( r#"{"authenticatorAttachment":null,"residentKey":null,"requireResidentKey":null,"userVerification":null}"#, )?; - assert!( - matches!(crit.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) - ); + assert_eq!(crit.authenticator_attachment, AuthenticatorAttachment::None,); assert!(matches!( crit.resident_key, ResidentKeyRequirement::Discouraged @@ -4060,9 +3888,7 @@ mod test { UserVerificationRequirement::Preferred )); crit = serde_json::from_str::<AuthenticatorSelectionCriteria>("{}")?; - assert!( - matches!(crit.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) - ); + assert_eq!(crit.authenticator_attachment, AuthenticatorAttachment::None,); assert!(matches!( crit.resident_key, ResidentKeyRequirement::Discouraged @@ -4074,9 +3900,7 @@ mod test { crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( r#"{"residentKey":"preferred","requireResidentKey":false}"#, )?; - assert!( - matches!(crit.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) - ); + assert_eq!(crit.authenticator_attachment, AuthenticatorAttachment::None,); assert!(matches!( crit.resident_key, ResidentKeyRequirement::Preferred diff --git a/src/request/register/ser_server_state.rs b/src/request/register/ser_server_state.rs @@ -2,10 +2,9 @@ use super::{ super::super::bin::{ Decode, DecodeBuffer, EncDecErr, Encode, EncodeBuffer, EncodeBufferFallible as _, }, - AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, CoseAlgorithmIdentifiers, - CredProtect, CredentialMediationRequirement, CrossPlatformHint, ExtensionInfo, Hint, - PlatformHint, RegistrationServerState, ResidentKeyRequirement, SentChallenge, - ServerExtensionInfo, ServerPrfInfo, UserHandle, UserVerificationRequirement, + AuthenticatorAttachment, AuthenticatorSelectionCriteria, CoseAlgorithmIdentifiers, CredProtect, + CredentialMediationRequirement, ExtensionInfo, RegistrationServerState, ResidentKeyRequirement, + SentChallenge, ServerExtensionInfo, ServerPrfInfo, UserHandle, UserVerificationRequirement, }; use core::{ error::Error, @@ -29,79 +28,6 @@ impl<'a> DecodeBuffer<'a> for CoseAlgorithmIdentifiers { }) } } -impl EncodeBuffer for PlatformHint { - fn encode_into_buffer(&self, buffer: &mut Vec<u8>) { - match *self { - Self::None => 0u8, - Self::ClientDevice => 1, - } - .encode_into_buffer(buffer); - } -} -impl<'a> DecodeBuffer<'a> for PlatformHint { - type Err = EncDecErr; - fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> { - u8::decode_from_buffer(data).and_then(|val| match val { - 0 => Ok(Self::None), - 1 => Ok(Self::ClientDevice), - _ => Err(EncDecErr), - }) - } -} -impl EncodeBuffer for CrossPlatformHint { - fn encode_into_buffer(&self, buffer: &mut Vec<u8>) { - match *self { - Self::None => 0u8, - Self::SecurityKey => 1, - Self::Hybrid => 2, - Self::SecurityKeyHybrid => 3, - Self::HybridSecurityKey => 4, - } - .encode_into_buffer(buffer); - } -} -impl<'a> DecodeBuffer<'a> for CrossPlatformHint { - type Err = EncDecErr; - fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> { - u8::decode_from_buffer(data).and_then(|val| match val { - 0 => Ok(Self::None), - 1 => Ok(Self::SecurityKey), - 2 => Ok(Self::Hybrid), - 3 => Ok(Self::SecurityKeyHybrid), - 4 => Ok(Self::HybridSecurityKey), - _ => Err(EncDecErr), - }) - } -} -impl EncodeBuffer for AuthenticatorAttachmentReq { - fn encode_into_buffer(&self, buffer: &mut Vec<u8>) { - match *self { - Self::None(hint) => { - 0u8.encode_into_buffer(buffer); - hint.encode_into_buffer(buffer); - } - Self::Platform(hint) => { - 1u8.encode_into_buffer(buffer); - hint.encode_into_buffer(buffer); - } - Self::CrossPlatform(hint) => { - 2u8.encode_into_buffer(buffer); - hint.encode_into_buffer(buffer); - } - } - } -} -impl<'a> DecodeBuffer<'a> for AuthenticatorAttachmentReq { - type Err = EncDecErr; - fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> { - u8::decode_from_buffer(data).and_then(|val| match val { - 0 => Hint::decode_from_buffer(data).map(Self::None), - 1 => PlatformHint::decode_from_buffer(data).map(Self::Platform), - 2 => CrossPlatformHint::decode_from_buffer(data).map(Self::CrossPlatform), - _ => Err(EncDecErr), - }) - } -} impl EncodeBuffer for AuthenticatorSelectionCriteria { fn encode_into_buffer(&self, buffer: &mut Vec<u8>) { self.authenticator_attachment.encode_into_buffer(buffer); @@ -112,7 +38,7 @@ impl EncodeBuffer for AuthenticatorSelectionCriteria { impl<'a> DecodeBuffer<'a> for AuthenticatorSelectionCriteria { type Err = EncDecErr; fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> { - AuthenticatorAttachmentReq::decode_from_buffer(data).and_then(|authenticator_attachment| { + AuthenticatorAttachment::decode_from_buffer(data).and_then(|authenticator_attachment| { ResidentKeyRequirement::decode_from_buffer(data).and_then(|resident_key| { UserVerificationRequirement::decode_from_buffer(data).map(|user_verification| { Self { @@ -256,7 +182,7 @@ impl<const USER_LEN: usize> Encode for RegistrationServerState<USER_LEN> { // * 1 for `CredentialMediationRequirement` // * 16 for `SentChallenge` // * 1 for `CoseAlgorithmIdentifiers` - // * 4 for `AuthenticatorSelectionCriteria` + // * 3 for `AuthenticatorSelectionCriteria` // * 4–10 for `Extension` // * 12 for `SystemTime` // * 1–64 for `UserHandle<USER_LEN>` @@ -264,7 +190,7 @@ impl<const USER_LEN: usize> Encode for RegistrationServerState<USER_LEN> { let mut buffer = Vec::with_capacity( 1 + 16 + 1 - + 4 + + 3 + (1 + usize::from(self.extensions.cred_props.is_some()) + if matches!(self.extensions.cred_protect, CredProtect::None) { 1 diff --git a/src/request/ser.rs b/src/request/ser.rs @@ -1,7 +1,8 @@ use super::{ super::response::ser::Base64DecodedVal, AsciiDomain, AsciiDomainStatic, Challenge, - CredentialId, CredentialMediationRequirement, ExtensionReq, Hint, PrfInput, - PublicKeyCredentialDescriptor, RpId, Url, UserVerificationRequirement, auth::PrfInputOwned, + CredentialId, CredentialMediationRequirement, ExtensionReq, Hints, PrfInput, + PublicKeyCredentialDescriptor, PublicKeyCredentialHint, RpId, Url, UserVerificationRequirement, + auth::PrfInputOwned, }; use core::{ fmt::{self, Formatter}, @@ -249,76 +250,164 @@ const SECURITY_KEY: &str = "security-key"; const CLIENT_DEVICE: &str = "client-device"; /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-hybrid). const HYBRID: &str = "hybrid"; -impl Serialize for Hint { +impl Serialize for PublicKeyCredentialHint { + /// Serializes `self` as a [`prim@str`] conforming with + /// [`PublicKeyCredentialHint`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint). + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::PublicKeyCredentialHint; + /// assert_eq!( + /// serde_json::to_string(&PublicKeyCredentialHint::SecurityKey)?, + /// r#""security-key""# + /// ); + /// assert_eq!( + /// serde_json::to_string(&PublicKeyCredentialHint::ClientDevice)?, + /// r#""client-device""# + /// ); + /// assert_eq!( + /// serde_json::to_string(&PublicKeyCredentialHint::Hybrid)?, + /// r#""hybrid""# + /// ); + /// # Ok::<_, serde_json::Error>(()) + /// ``` + #[inline] + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_str(match *self { + Self::SecurityKey => SECURITY_KEY, + Self::ClientDevice => CLIENT_DEVICE, + Self::Hybrid => HYBRID, + }) + } +} +impl Serialize for Hints { /// Serializes `self` to conform with /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-hints). /// /// # Examples /// /// ``` - /// # use webauthn_rp::request::Hint; + /// # use webauthn_rp::request::{Hints, PublicKeyCredentialHint}; /// assert_eq!( - /// serde_json::to_string(&Hint::None)?, + /// serde_json::to_string(&Hints::EMPTY)?, /// r#"[]"# /// ); /// assert_eq!( - /// serde_json::to_string(&Hint::SecurityKey)?, + /// serde_json::to_string(&Hints::EMPTY.add(PublicKeyCredentialHint::SecurityKey))?, /// r#"["security-key"]"# /// ); /// assert_eq!( - /// serde_json::to_string(&Hint::ClientDevice)?, + /// serde_json::to_string(&Hints::EMPTY.add(PublicKeyCredentialHint::ClientDevice))?, /// r#"["client-device"]"# /// ); /// assert_eq!( - /// serde_json::to_string(&Hint::Hybrid)?, + /// serde_json::to_string(&Hints::EMPTY.add(PublicKeyCredentialHint::Hybrid))?, /// r#"["hybrid"]"# /// ); /// assert_eq!( - /// serde_json::to_string(&Hint::SecurityKeyClientDevice)?, + /// serde_json::to_string( + /// &Hints::EMPTY + /// .add(PublicKeyCredentialHint::SecurityKey) + /// .add(PublicKeyCredentialHint::ClientDevice) + /// )?, /// r#"["security-key","client-device"]"# /// ); /// assert_eq!( - /// serde_json::to_string(&Hint::ClientDeviceSecurityKey)?, + /// serde_json::to_string( + /// &Hints::EMPTY + /// .add(PublicKeyCredentialHint::ClientDevice) + /// .add(PublicKeyCredentialHint::SecurityKey) + /// )?, /// r#"["client-device","security-key"]"# /// ); /// assert_eq!( - /// serde_json::to_string(&Hint::SecurityKeyHybrid)?, + /// serde_json::to_string( + /// &Hints::EMPTY + /// .add(PublicKeyCredentialHint::SecurityKey) + /// .add(PublicKeyCredentialHint::Hybrid) + /// )?, /// r#"["security-key","hybrid"]"# /// ); /// assert_eq!( - /// serde_json::to_string(&Hint::HybridSecurityKey)?, + /// serde_json::to_string( + /// &Hints::EMPTY + /// .add(PublicKeyCredentialHint::Hybrid) + /// .add(PublicKeyCredentialHint::SecurityKey) + /// )?, /// r#"["hybrid","security-key"]"# /// ); /// assert_eq!( - /// serde_json::to_string(&Hint::ClientDeviceHybrid)?, + /// serde_json::to_string( + /// &Hints::EMPTY + /// .add(PublicKeyCredentialHint::ClientDevice) + /// .add(PublicKeyCredentialHint::Hybrid) + /// )?, /// r#"["client-device","hybrid"]"# /// ); /// assert_eq!( - /// serde_json::to_string(&Hint::HybridClientDevice)?, + /// serde_json::to_string( + /// &Hints::EMPTY + /// .add(PublicKeyCredentialHint::Hybrid) + /// .add(PublicKeyCredentialHint::ClientDevice) + /// )?, /// r#"["hybrid","client-device"]"# /// ); /// assert_eq!( - /// serde_json::to_string(&Hint::SecurityKeyClientDeviceHybrid)?, + /// serde_json::to_string( + /// &Hints::EMPTY + /// .add(PublicKeyCredentialHint::SecurityKey) + /// .add(PublicKeyCredentialHint::ClientDevice) + /// .add(PublicKeyCredentialHint::Hybrid) + /// )?, /// r#"["security-key","client-device","hybrid"]"# /// ); /// assert_eq!( - /// serde_json::to_string(&Hint::SecurityKeyHybridClientDevice)?, + /// serde_json::to_string( + /// &Hints::EMPTY + /// .add(PublicKeyCredentialHint::SecurityKey) + /// .add(PublicKeyCredentialHint::Hybrid) + /// .add(PublicKeyCredentialHint::ClientDevice) + /// )?, /// r#"["security-key","hybrid","client-device"]"# /// ); /// assert_eq!( - /// serde_json::to_string(&Hint::ClientDeviceSecurityKeyHybrid)?, + /// serde_json::to_string( + /// &Hints::EMPTY + /// .add(PublicKeyCredentialHint::ClientDevice) + /// .add(PublicKeyCredentialHint::SecurityKey) + /// .add(PublicKeyCredentialHint::Hybrid) + /// )?, /// r#"["client-device","security-key","hybrid"]"# /// ); /// assert_eq!( - /// serde_json::to_string(&Hint::ClientDeviceHybridSecurityKey)?, + /// serde_json::to_string( + /// &Hints::EMPTY + /// .add(PublicKeyCredentialHint::ClientDevice) + /// .add(PublicKeyCredentialHint::Hybrid) + /// .add(PublicKeyCredentialHint::SecurityKey) + /// )?, /// r#"["client-device","hybrid","security-key"]"# /// ); /// assert_eq!( - /// serde_json::to_string(&Hint::HybridSecurityKeyClientDevice)?, + /// serde_json::to_string( + /// &Hints::EMPTY + /// .add(PublicKeyCredentialHint::Hybrid) + /// .add(PublicKeyCredentialHint::SecurityKey) + /// .add(PublicKeyCredentialHint::ClientDevice) + /// )?, /// r#"["hybrid","security-key","client-device"]"# /// ); /// assert_eq!( - /// serde_json::to_string(&Hint::HybridClientDeviceSecurityKey)?, + /// serde_json::to_string( + /// &Hints::EMPTY + /// .add(PublicKeyCredentialHint::Hybrid) + /// .add(PublicKeyCredentialHint::ClientDevice) + /// .add(PublicKeyCredentialHint::SecurityKey) + /// )?, /// r#"["hybrid","client-device","security-key"]"# /// ); /// # Ok::<_, serde_json::Error>(()) @@ -328,85 +417,18 @@ impl Serialize for Hint { where S: Serializer, { - let count = match *self { - Self::None => 0, - Self::SecurityKey | Self::ClientDevice | Self::Hybrid => 1, - Self::SecurityKeyClientDevice - | Self::ClientDeviceSecurityKey - | Self::SecurityKeyHybrid - | Self::HybridSecurityKey - | Self::ClientDeviceHybrid - | Self::HybridClientDevice => 2, - Self::SecurityKeyClientDeviceHybrid - | Self::SecurityKeyHybridClientDevice - | Self::ClientDeviceSecurityKeyHybrid - | Self::ClientDeviceHybridSecurityKey - | Self::HybridSecurityKeyClientDevice - | Self::HybridClientDeviceSecurityKey => 3, - }; - serializer.serialize_seq(Some(count)).and_then(|mut ser| { - match *self { - Self::None => Ok(()), - Self::SecurityKey => ser.serialize_element(SECURITY_KEY), - Self::ClientDevice => ser.serialize_element(CLIENT_DEVICE), - Self::Hybrid => ser.serialize_element(HYBRID), - Self::SecurityKeyClientDevice => ser - .serialize_element(SECURITY_KEY) - .and_then(|()| ser.serialize_element(CLIENT_DEVICE)), - Self::ClientDeviceSecurityKey => ser - .serialize_element(CLIENT_DEVICE) - .and_then(|()| ser.serialize_element(SECURITY_KEY)), - Self::SecurityKeyHybrid => ser - .serialize_element(SECURITY_KEY) - .and_then(|()| ser.serialize_element(HYBRID)), - Self::HybridSecurityKey => ser - .serialize_element(HYBRID) - .and_then(|()| ser.serialize_element(SECURITY_KEY)), - Self::ClientDeviceHybrid => ser - .serialize_element(CLIENT_DEVICE) - .and_then(|()| ser.serialize_element(HYBRID)), - Self::HybridClientDevice => ser - .serialize_element(HYBRID) - .and_then(|()| ser.serialize_element(CLIENT_DEVICE)), - Self::SecurityKeyClientDeviceHybrid => { - ser.serialize_element(SECURITY_KEY).and_then(|()| { - ser.serialize_element(CLIENT_DEVICE) - .and_then(|()| ser.serialize_element(HYBRID)) - }) - } - Self::SecurityKeyHybridClientDevice => { - ser.serialize_element(SECURITY_KEY).and_then(|()| { - ser.serialize_element(HYBRID) - .and_then(|()| ser.serialize_element(CLIENT_DEVICE)) - }) - } - Self::ClientDeviceSecurityKeyHybrid => { - ser.serialize_element(CLIENT_DEVICE).and_then(|()| { - ser.serialize_element(SECURITY_KEY) - .and_then(|()| ser.serialize_element(HYBRID)) - }) - } - Self::ClientDeviceHybridSecurityKey => { - ser.serialize_element(CLIENT_DEVICE).and_then(|()| { - ser.serialize_element(HYBRID) - .and_then(|()| ser.serialize_element(SECURITY_KEY)) - }) - } - Self::HybridSecurityKeyClientDevice => { - ser.serialize_element(HYBRID).and_then(|()| { - ser.serialize_element(SECURITY_KEY) - .and_then(|()| ser.serialize_element(CLIENT_DEVICE)) - }) - } - Self::HybridClientDeviceSecurityKey => { - ser.serialize_element(HYBRID).and_then(|()| { - ser.serialize_element(CLIENT_DEVICE) - .and_then(|()| ser.serialize_element(SECURITY_KEY)) + serializer + .serialize_seq(Some(usize::from(self.count()))) + .and_then(|mut ser| { + self.0 + .iter() + .try_fold((), |(), opt| { + opt.ok_or_else(|| None) + .and_then(|hint| ser.serialize_element(&hint).map_err(Some)) }) - } - } - .and_then(|()| ser.end()) - }) + .or_else(|e| e.map_or(Ok(()), Err)) + .and_then(|()| ser.end()) + }) } } /// `"first"`. @@ -656,24 +678,42 @@ impl<'de> Deserialize<'de> for RpId { String::deserialize(deserializer).and_then(|dom| Self::try_from(dom).map_err(Error::custom)) } } -/// Helper for `Hint::deserialize`. -enum HintHelper { - /// `"security-key"` - SecurityKey, - /// `"client-device"` - ClientDevice, - /// `"hybrid"` - Hybrid, -} -impl<'de> Deserialize<'de> for HintHelper { +impl<'de> Deserialize<'de> for PublicKeyCredentialHint { + /// Deserializes a [`prim@str`] based on + /// [`PublicKeyCredentialHint`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint). + /// + /// Note unknown values will lead to an error. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::PublicKeyCredentialHint; + /// assert_eq!( + /// serde_json::from_str::<PublicKeyCredentialHint>(r#""security-key""#)?, + /// PublicKeyCredentialHint::SecurityKey, + /// ); + /// assert_eq!( + /// serde_json::from_str::<PublicKeyCredentialHint>(r#""client-device""#)?, + /// PublicKeyCredentialHint::ClientDevice, + /// ); + /// assert_eq!( + /// serde_json::from_str::<PublicKeyCredentialHint>(r#""hybrid""#)?, + /// PublicKeyCredentialHint::Hybrid, + /// ); + /// assert!( + /// serde_json::from_str::<PublicKeyCredentialHint>(r#""foo""#).is_err() + /// ); + /// # Ok::<_, serde_json::Error>(()) + /// ``` + #[inline] fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { - /// `Visitor` for `HintHelper`. - struct HintHelperVisitor; - impl Visitor<'_> for HintHelperVisitor { - type Value = HintHelper; + /// `Visitor` for `PublicKeyCredentialHint`. + struct PublicKeyCredentialHintVisitor; + impl Visitor<'_> for PublicKeyCredentialHintVisitor { + type Value = PublicKeyCredentialHint; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { write!( formatter, @@ -685,9 +725,9 @@ impl<'de> Deserialize<'de> for HintHelper { E: Error, { match v { - SECURITY_KEY => Ok(HintHelper::SecurityKey), - CLIENT_DEVICE => Ok(HintHelper::ClientDevice), - HYBRID => Ok(HintHelper::Hybrid), + SECURITY_KEY => Ok(PublicKeyCredentialHint::SecurityKey), + CLIENT_DEVICE => Ok(PublicKeyCredentialHint::ClientDevice), + HYBRID => Ok(PublicKeyCredentialHint::Hybrid), _ => Err(E::invalid_value( Unexpected::Str(v), &format!("'{SECURITY_KEY}', '{CLIENT_DEVICE}', or '{HYBRID}'").as_str(), @@ -695,39 +735,43 @@ impl<'de> Deserialize<'de> for HintHelper { } } } - deserializer.deserialize_str(HintHelperVisitor) + deserializer.deserialize_str(PublicKeyCredentialHintVisitor) } } -impl<'de> Deserialize<'de> for Hint { +impl<'de> Deserialize<'de> for Hints { /// Deserializes a sequence based on /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-hints). /// - /// Note duplicates and unknown values will lead to an error. + /// Note unknown values will lead to an error. /// /// # Examples /// /// ``` - /// # use webauthn_rp::request::Hint; - /// assert!( - /// matches!( - /// serde_json::from_str(r#"["security-key", "hybrid", "client-device"]"#)?, - /// Hint::SecurityKeyHybridClientDevice - /// ) + /// # use webauthn_rp::request::{Hints, PublicKeyCredentialHint}; + /// assert_eq!( + /// serde_json::from_str::<Hints>(r#"["security-key", "hybrid", "client-device"]"#)?, + /// Hints::EMPTY + /// .add(PublicKeyCredentialHint::SecurityKey) + /// .add(PublicKeyCredentialHint::Hybrid) + /// .add(PublicKeyCredentialHint::ClientDevice), /// ); - /// assert!( - /// matches!( - /// serde_json::from_str(r#"["hybrid", "security-key", "client-device"]"#)?, - /// Hint::HybridSecurityKeyClientDevice - /// ) + /// assert_eq!( + /// serde_json::from_str::<Hints>(r#"["hybrid", "security-key", "client-device"]"#)?, + /// Hints::EMPTY + /// .add(PublicKeyCredentialHint::Hybrid) + /// .add(PublicKeyCredentialHint::SecurityKey) + /// .add(PublicKeyCredentialHint::ClientDevice), /// ); - /// assert!( - /// matches!( - /// serde_json::from_str(r#"[]"#)?, - /// Hint::None - /// ) + /// assert_eq!( + /// serde_json::from_str::<Hints>(r#"[]"#)?, + /// Hints::EMPTY /// ); - /// assert!( - /// serde_json::from_str::<Hint>(r#"["security-key", "hybrid", "hybrid"]"#).is_err() + /// // Only the first instances are retained while duplicates are ignored. + /// assert_eq!( + /// serde_json::from_str::<Hints>(r#"["hybrid", "client-device", "hybrid"]"#)?, + /// Hints::EMPTY + /// .add(PublicKeyCredentialHint::Hybrid) + /// .add(PublicKeyCredentialHint::ClientDevice) /// ); /// # Ok::<_, serde_json::Error>(()) /// ``` @@ -736,92 +780,27 @@ impl<'de> Deserialize<'de> for Hint { where D: Deserializer<'de>, { - /// `Visitor` for `Hint`. - struct HintVisitor; - impl<'d> Visitor<'d> for HintVisitor { - type Value = Hint; + /// `Visitor` for `Hints`. + struct HintsVisitor; + impl<'d> Visitor<'d> for HintsVisitor { + type Value = Hints; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - formatter.write_str("unique sequence of hints") + formatter.write_str("sequence of hints") } fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> where A: SeqAccess<'d>, { - let mut hint = Hint::None; - while let Some(elem) = seq.next_element::<HintHelper>()? { - hint = match elem { - HintHelper::SecurityKey => match hint { - Hint::None => Hint::SecurityKey, - Hint::SecurityKey - | Hint::SecurityKeyClientDevice - | Hint::ClientDeviceSecurityKey - | Hint::SecurityKeyHybrid - | Hint::HybridSecurityKey - | Hint::SecurityKeyClientDeviceHybrid - | Hint::SecurityKeyHybridClientDevice - | Hint::ClientDeviceSecurityKeyHybrid - | Hint::ClientDeviceHybridSecurityKey - | Hint::HybridSecurityKeyClientDevice - | Hint::HybridClientDeviceSecurityKey => { - return Err(Error::custom(format!( - "'{SECURITY_KEY}' hint appeared more than once" - ))); - } - Hint::ClientDevice => Hint::ClientDeviceSecurityKey, - Hint::Hybrid => Hint::HybridSecurityKey, - Hint::ClientDeviceHybrid => Hint::ClientDeviceHybridSecurityKey, - Hint::HybridClientDevice => Hint::HybridClientDeviceSecurityKey, - }, - HintHelper::ClientDevice => match hint { - Hint::None => Hint::ClientDevice, - Hint::SecurityKey => Hint::SecurityKeyClientDevice, - Hint::ClientDevice - | Hint::ClientDeviceSecurityKey - | Hint::SecurityKeyClientDevice - | Hint::ClientDeviceHybrid - | Hint::HybridClientDevice - | Hint::ClientDeviceSecurityKeyHybrid - | Hint::ClientDeviceHybridSecurityKey - | Hint::SecurityKeyClientDeviceHybrid - | Hint::SecurityKeyHybridClientDevice - | Hint::HybridClientDeviceSecurityKey - | Hint::HybridSecurityKeyClientDevice => { - return Err(Error::custom(format!( - "'{CLIENT_DEVICE}' hint appeared more than once" - ))); - } - Hint::Hybrid => Hint::HybridClientDevice, - Hint::SecurityKeyHybrid => Hint::SecurityKeyHybridClientDevice, - Hint::HybridSecurityKey => Hint::HybridSecurityKeyClientDevice, - }, - HintHelper::Hybrid => match hint { - Hint::None => Hint::Hybrid, - Hint::Hybrid - | Hint::HybridClientDevice - | Hint::ClientDeviceHybrid - | Hint::HybridSecurityKey - | Hint::SecurityKeyHybrid - | Hint::HybridClientDeviceSecurityKey - | Hint::HybridSecurityKeyClientDevice - | Hint::ClientDeviceHybridSecurityKey - | Hint::ClientDeviceSecurityKeyHybrid - | Hint::SecurityKeyHybridClientDevice - | Hint::SecurityKeyClientDeviceHybrid => { - return Err(Error::custom(format!( - "'{HYBRID}' hint appeared more than once" - ))); - } - Hint::ClientDevice => Hint::ClientDeviceHybrid, - Hint::SecurityKey => Hint::SecurityKeyHybrid, - Hint::ClientDeviceSecurityKey => Hint::ClientDeviceSecurityKeyHybrid, - Hint::SecurityKeyClientDevice => Hint::SecurityKeyClientDeviceHybrid, - }, - }; + let mut hints = Hints::EMPTY; + while let Some(elem) = seq.next_element()? + && hints.count() < 3 + { + hints = hints.add(elem); } - Ok(hint) + Ok(hints) } } - deserializer.deserialize_seq(HintVisitor) + deserializer.deserialize_seq(HintsVisitor) } } impl<'de> Deserialize<'de> for CredentialMediationRequirement { diff --git a/src/request/ser_server_state.rs b/src/request/ser_server_state.rs @@ -1,6 +1,6 @@ use super::{ super::bin::{DecodeBuffer, EncDecErr, Encode, EncodeBuffer, EncodeBufferFallible}, - CredentialMediationRequirement, ExtensionInfo, ExtensionReq, Hint, SentChallenge, + CredentialMediationRequirement, ExtensionInfo, ExtensionReq, SentChallenge, UserVerificationRequirement, }; use core::{convert::Infallible, num::NonZeroU32, time::Duration}; @@ -59,85 +59,6 @@ impl<'a> DecodeBuffer<'a> for ExtensionReq { }) } } -/// [`Hint::None`] tag. -const HINT_NONE: u8 = 0; -/// [`Hint::SecurityKey`] tag. -const HINT_SEC_KEY: u8 = 1; -/// [`Hint::ClientDevice`] tag. -const HINT_CLIENT_DEV: u8 = 2; -/// [`Hint::Hybrid`] tag. -const HINT_HYBRID: u8 = 3; -/// [`Hint::SecurityKeyClientDevice`] tag. -const HINT_SEC_KEY_CLIENT_DEV: u8 = 4; -/// [`Hint::ClientDeviceSecurityKey`] tag. -const HINT_CLIENT_DEV_SEC_KEY: u8 = 5; -/// [`Hint::SecurityKeyHybrid`] tag. -const HINT_SEC_KEY_HYBRID: u8 = 6; -/// [`Hint::HybridSecurityKey`] tag. -const HINT_HYBRID_SEC_KEY: u8 = 7; -/// [`Hint::ClientDeviceHybrid`] tag. -const HINT_CLIENT_DEV_HYBRID: u8 = 8; -/// [`Hint::HybridClientDevice`] tag. -const HINT_HYBRID_CLIENT_DEV: u8 = 9; -/// [`Hint::SecurityKeyClientDeviceHybrid`] tag. -const HINT_SEC_KEY_CLIENT_DEV_HYBRID: u8 = 10; -/// [`Hint::SecurityKeyHybridClientDevice`] tag. -const HINT_SEC_KEY_HYBRID_CLIENT_DEV: u8 = 11; -/// [`Hint::ClientDeviceSecurityKeyHybrid`] tag. -const HINT_CLIENT_DEV_SEC_KEY_HYBRID: u8 = 12; -/// [`Hint::ClientDeviceHybridSecurityKey`] tag. -const HINT_CLIENT_DEV_HYBRID_SEC_KEY: u8 = 13; -/// [`Hint::HybridSecurityKeyClientDevice`] tag. -const HINT_HYBRID_SEC_KEY_CLIENT_DEV: u8 = 14; -/// [`Hint::HybridClientDeviceSecurityKey`] tag. -const HINT_HYBRID_CLIENT_DEV_SEC_KEY: u8 = 15; -impl EncodeBuffer for Hint { - fn encode_into_buffer(&self, buffer: &mut Vec<u8>) { - match *self { - Self::None => HINT_NONE, - Self::SecurityKey => HINT_SEC_KEY, - Self::ClientDevice => HINT_CLIENT_DEV, - Self::Hybrid => HINT_HYBRID, - Self::SecurityKeyClientDevice => HINT_SEC_KEY_CLIENT_DEV, - Self::ClientDeviceSecurityKey => HINT_CLIENT_DEV_SEC_KEY, - Self::SecurityKeyHybrid => HINT_SEC_KEY_HYBRID, - Self::HybridSecurityKey => HINT_HYBRID_SEC_KEY, - Self::ClientDeviceHybrid => HINT_CLIENT_DEV_HYBRID, - Self::HybridClientDevice => HINT_HYBRID_CLIENT_DEV, - Self::SecurityKeyClientDeviceHybrid => HINT_SEC_KEY_CLIENT_DEV_HYBRID, - Self::SecurityKeyHybridClientDevice => HINT_SEC_KEY_HYBRID_CLIENT_DEV, - Self::ClientDeviceSecurityKeyHybrid => HINT_CLIENT_DEV_SEC_KEY_HYBRID, - Self::ClientDeviceHybridSecurityKey => HINT_CLIENT_DEV_HYBRID_SEC_KEY, - Self::HybridSecurityKeyClientDevice => HINT_HYBRID_SEC_KEY_CLIENT_DEV, - Self::HybridClientDeviceSecurityKey => HINT_HYBRID_CLIENT_DEV_SEC_KEY, - } - .encode_into_buffer(buffer); - } -} -impl<'a> DecodeBuffer<'a> for Hint { - type Err = EncDecErr; - fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> { - u8::decode_from_buffer(data).and_then(|val| match val { - HINT_NONE => Ok(Self::None), - HINT_SEC_KEY => Ok(Self::SecurityKey), - HINT_CLIENT_DEV => Ok(Self::ClientDevice), - HINT_HYBRID => Ok(Self::Hybrid), - HINT_SEC_KEY_CLIENT_DEV => Ok(Self::SecurityKeyClientDevice), - HINT_CLIENT_DEV_SEC_KEY => Ok(Self::ClientDeviceSecurityKey), - HINT_SEC_KEY_HYBRID => Ok(Self::SecurityKeyHybrid), - HINT_HYBRID_SEC_KEY => Ok(Self::HybridSecurityKey), - HINT_CLIENT_DEV_HYBRID => Ok(Self::ClientDeviceHybrid), - HINT_HYBRID_CLIENT_DEV => Ok(Self::HybridClientDevice), - HINT_SEC_KEY_CLIENT_DEV_HYBRID => Ok(Self::SecurityKeyClientDeviceHybrid), - HINT_SEC_KEY_HYBRID_CLIENT_DEV => Ok(Self::SecurityKeyHybridClientDevice), - HINT_CLIENT_DEV_SEC_KEY_HYBRID => Ok(Self::ClientDeviceSecurityKeyHybrid), - HINT_CLIENT_DEV_HYBRID_SEC_KEY => Ok(Self::ClientDeviceHybridSecurityKey), - HINT_HYBRID_SEC_KEY_CLIENT_DEV => Ok(Self::HybridSecurityKeyClientDevice), - HINT_HYBRID_CLIENT_DEV_SEC_KEY => Ok(Self::HybridClientDeviceSecurityKey), - _ => Err(EncDecErr), - }) - } -} impl EncodeBuffer for SentChallenge { fn encode_into_buffer(&self, buffer: &mut Vec<u8>) { self.0.encode_into_buffer(buffer); diff --git a/src/response.rs b/src/response.rs @@ -434,9 +434,10 @@ impl AuthTransports { } } /// [`AuthenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment). -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum AuthenticatorAttachment { /// No attachment information. + #[default] None, /// [`platform`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattachment-platform). Platform, diff --git a/src/response/register/error.rs b/src/response/register/error.rs @@ -7,9 +7,9 @@ use super::{ request::{ BackupReq, CredentialMediationRequirement, UserVerificationRequirement, register::{ - AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, - CredentialCreationOptions, Extension, PublicKeyCredentialCreationOptions, - RegistrationServerState, RegistrationVerificationOptions, + AuthenticatorSelectionCriteria, CredentialCreationOptions, Extension, + PublicKeyCredentialCreationOptions, RegistrationServerState, + RegistrationVerificationOptions, }, }, }, @@ -660,7 +660,7 @@ pub enum RegCeremonyErr { BackupDoesNotExist, /// [`AuthenticatorAttachment`] was not sent back despite being required. MissingAuthenticatorAttachment, - /// [`AuthenticatorAttachmentReq::Platform`] or [`AuthenticatorAttachmentReq::CrossPlatform`] was sent + /// [`AuthenticatorAttachment::Platform`] or [`AuthenticatorAttachment::CrossPlatform`] was sent /// but [`AuthenticatorAttachment`] was not the same. AuthenticatorAttachmentMismatch, /// Variant returned when there is an issue with [`Extension`]s.