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:
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.