webauthn_rp

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

commit 2acae1c69f2fddd68a09e6bdf5b7a9b349f09c39
parent 18f2066416892be8936c6677fb0f8dcd35213b1f
Author: Zack Newman <zack@philomathiclife.com>
Date:   Sat, 29 Mar 2025 22:15:21 -0600

generalize authentication further based on user

Diffstat:
MREADME.md | 5++---
Msrc/lib.rs | 13+++++++++----
Msrc/request.rs | 10++++++----
Msrc/request/auth.rs | 23++++++++++++-----------
Msrc/request/register.rs | 99++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/request/register/ser.rs | 4++--
Msrc/response.rs | 11+++++------
Msrc/response/auth.rs | 106++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/response/auth/ser.rs | 154++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/response/auth/ser_relaxed.rs | 314+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/response/register.rs | 7+++----
Msrc/response/register/ser.rs | 6+++---
Msrc/response/register/ser_relaxed.rs | 59++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/response/ser.rs | 49+++++++++++++++++++------------------------------
14 files changed, 518 insertions(+), 342 deletions(-)

diff --git a/README.md b/README.md @@ -40,9 +40,8 @@ cannot be constructed when [`bin`](#bin) or [`serde`](#serde) is not enabled. ### `serde` -For many [`serde_relaxed`](#serde_relaxed) should be used instead. This feature _strictly_ adheres to the -JSON-motivated definitions. You _will_ encounter clients that send data that cannot be deserialized using -this feature. +This feature _strictly_ adheres to the JSON-motivated definitions. You _will_ encounter clients that send data that +cannot be deserialized using this feature. For many [`serde_relaxed`](#serde_relaxed) should be used instead. Enables (de)serialization of data sent to/from the client via [`serde`](https://docs.rs/serde/latest/serde/) based on the JSON-motivated definitions (e.g., diff --git a/src/lib.rs b/src/lib.rs @@ -40,9 +40,9 @@ //! //! ### `serde` //! -//! For many [`serde_relaxed`](#serde_relaxed) should be used instead. This feature _strictly_ adheres to the -//! JSON-motivated definitions. You _will_ encounter clients that send data that cannot be deserialized using -//! this feature. +//! This feature _strictly_ adheres to the JSON-motivated definitions. You _will_ encounter clients that send data +//! that cannot be deserialized using this feature. For many [`serde_relaxed`](#serde_relaxed) should be used +//! instead. //! //! Enables (de)serialization of data sent to/from the client via [`serde`](https://docs.rs/serde/latest/serde/) //! based on the JSON-motivated definitions (e.g., @@ -371,7 +371,12 @@ pub use crate::{ PublicKeyCredentialCreationOptions, RegistrationClientState, RegistrationServerState, }, }, - response::{auth::Authentication, register::Registration}, + response::{ + auth::{ + Authentication, PasskeyAuthentication, PasskeyAuthentication16, PasskeyAuthentication64, + }, + register::Registration, + }, }; /// Error returned in [`RegCeremonyErr::Credential`] and [`AuthCeremonyErr::Credential`] as well as /// from [`AuthenticatedCredential::new`]. diff --git a/src/request.rs b/src/request.rs @@ -197,7 +197,7 @@ impl Challenge { /// The number of bytes a `Challenge` takes to encode in base64url. #[expect(clippy::unwrap_used, reason = "we want to crash when there is a bug")] pub(super) const BASE64_LEN: usize = super::base64url_nopad_len(16).unwrap(); - /// Returns a new `Challenge` based on a randomly-generated `u128`. + /// Generates a random `Challenge`. /// /// # Examples /// @@ -1634,16 +1634,18 @@ impl BuildHasher for BuildIdentityHasher { IdentityHasher(0) } } -/// Prevent users from implementing [`ServerState`]. +/// Prevent users from implementing [`ServerState`] and [`super::register::User`]. mod private { - /// Marker trait used as a supertrait of `ServerState`. + /// Marker trait used as a supertrait of `ServerState` and `User`. pub trait Sealed {} impl Sealed for super::AuthenticationServerState {} impl Sealed for super::RegistrationServerState {} + impl<T> Sealed for super::register::UserHandle<T> {} + impl<T> Sealed for Option<super::register::UserHandle<T>> {} } /// Subset of data shared by both [`RegistrationServerState`] and [`AuthenticationServerState`]. /// -/// This trait is sealed and cannot be implemented for types outside of `webauthn_rp`. +/// This `trait` is sealed and cannot be implemented for types outside of `webauthn_rp`. pub trait ServerState: private::Sealed { /// Returns the `Instant` the ceremony expires. /// diff --git a/src/request/auth.rs b/src/request/auth.rs @@ -11,6 +11,7 @@ use super::{ use super::{ super::{ AuthenticatedCredential, + request::register::User, response::{ AuthenticatorAttachment, auth::{ @@ -873,7 +874,7 @@ impl<O, T> Default for AuthenticationVerificationOptions<'_, '_, O, T> { /// [`Self::backup_requirement`] is `None`, [`Self::error_on_unsolicited_extensions`] is `true`, /// [`Self::auth_attachment_enforcement`] is [`AuthenticatorAttachmentEnforcement::default`], /// [`Self::update_uv`] is `false`, [`Self::sig_counter_enforcement`] is - /// [`SignatureCounterEnforcement::Fail`], and [`Self::client_data_json_relaxed`] is `true`. + /// [`SignatureCounterEnforcement::default`], and [`Self::client_data_json_relaxed`] is `true`. #[inline] fn default() -> Self { Self { @@ -956,11 +957,11 @@ impl AuthenticationServerState { P256Key: AsRef<[u8]>, P384Key: AsRef<[u8]>, RsaKey: AsRef<[u8]>, - User: AsRef<[u8]>, + U: User, >( self, rp_id: &RpId, - response: &'a Authentication<User>, + response: &'a Authentication<U>, cred: &mut AuthenticatedCredential< 'a, 'user, @@ -1002,10 +1003,10 @@ impl AuthenticationServerState { response .response .user_handle() - .as_ref() + .is_same(cred.user_id) .ok_or(AuthCeremonyErr::MissingUserHandle) - .and_then(|user| { - if cred.user_id() == user { + .and_then(|same| { + if same { if cred.id == response.raw_id { Ok(None) } else { @@ -1098,17 +1099,17 @@ impl AuthenticationServerState { /// Errors iff [`AuthenticatedCredential::user_handle`] does not match [`Authentication::user_handle`] or /// [`PublicKeyCredentialRequestOptions::allow_credentials`] does not have a [`CredInfo`] such that /// [`CredInfo::id`] matches [`Authentication::raw_id`]. - fn verify_nondiscoverable<'a, PublicKey, User: AsRef<[u8]>>( + fn verify_nondiscoverable<'a, PublicKey, U: User>( &self, - response: &'a Authentication<User>, + response: &'a Authentication<U>, cred: &AuthenticatedCredential<'a, '_, PublicKey>, ) -> Result<Option<ServerCredSpecificExtensionInfo>, AuthCeremonyErr> { response .response .user_handle() - .as_ref() - .map_or(Ok(()), |user| { - if user == cred.user_id() { + .is_same(cred.user_id()) + .map_or(Ok(()), |same| { + if same { Ok(()) } else { Err(AuthCeremonyErr::UserHandleMismatch) diff --git a/src/request/register.rs b/src/request/register.rs @@ -19,7 +19,10 @@ use super::{ }; #[cfg(doc)] use crate::{ - request::{AsciiDomain, DomainOrigin, Url, auth::PublicKeyCredentialRequestOptions}, + request::{ + AsciiDomain, DomainOrigin, Url, auth::AuthenticationServerState, + auth::PublicKeyCredentialRequestOptions, + }, response::{AuthTransports, AuthenticatorTransport, Backup, CollectedClientData}, }; use alloc::borrow::Cow; @@ -228,29 +231,30 @@ impl<'a: 'b, 'b> From<Nickname<'a>> for Cow<'b, str> { value.0 } } -impl<'a: 'b, 'b> TryFrom<&'a str> for Nickname<'b> { +impl<'a: 'b, 'b> TryFrom<Cow<'a, str>> for Nickname<'b> { type Error = NicknameErr; /// # Examples /// /// ``` + /// # use std::borrow::Cow; /// # use webauthn_rp::request::register::{error::NicknameErr, Nickname}; /// assert_eq!( - /// Nickname::try_from("Srinivasa Ramanujan")?.as_ref(), + /// Nickname::try_from(Cow::Borrowed("Srinivasa Ramanujan"))?.as_ref(), /// "Srinivasa Ramanujan" /// ); /// assert_eq!( - /// Nickname::try_from("श्रीनिवास रामानुजन्")?.as_ref(), + /// Nickname::try_from(Cow::Borrowed("श्रीनिवास रामानुजन्"))?.as_ref(), /// "श्रीनिवास रामानुजन्" /// ); /// // Empty strings are not valid. - /// assert!(Nickname::try_from("").map_or_else( + /// assert!(Nickname::try_from(Cow::Borrowed("")).map_or_else( /// |e| matches!(e, NicknameErr::Rfc8266), /// |_| false /// )); /// # Ok::<_, webauthn_rp::AggErr>(()) /// ``` #[inline] - fn try_from(value: &'a str) -> Result<Self, Self::Error> { + fn try_from(value: Cow<'a, str>) -> Result<Self, Self::Error> { precis_profiles::Nickname::new() .enforce(value) .map_err(|_e| NicknameErr::Rfc8266) @@ -263,6 +267,22 @@ impl<'a: 'b, 'b> TryFrom<&'a str> for Nickname<'b> { }) } } +impl<'a: 'b, 'b> TryFrom<&'a str> for Nickname<'b> { + type Error = NicknameErr; + /// Same as [`Nickname::try_from`] except the input is a `str`. + #[inline] + fn try_from(value: &'a str) -> Result<Self, Self::Error> { + Self::try_from(Cow::Borrowed(value)) + } +} +impl TryFrom<String> for Nickname<'_> { + type Error = NicknameErr; + /// Same as [`Nickname::try_from`] except the input is a `String`. + #[inline] + fn try_from(value: String) -> Result<Self, Self::Error> { + Self::try_from(Cow::Owned(value)) + } +} /// String returned from the /// [UsernameCasePreserved Enforcement rule](https://www.rfc-editor.org/rfc/rfc8265#section-3.4.3) as defined in /// RFC 8265. @@ -294,25 +314,26 @@ impl Borrow<str> for Username<'_> { self.0.as_ref() } } -impl<'a: 'b, 'b> TryFrom<&'a str> for Username<'b> { +impl<'a: 'b, 'b> TryFrom<Cow<'a, str>> for Username<'b> { type Error = UsernameErr; /// # Examples /// /// ``` + /// # use std::borrow::Cow; /// # use webauthn_rp::request::register::{error::UsernameErr, Username}; /// assert_eq!( - /// Username::try_from("leonhard.euler")?.as_ref(), + /// Username::try_from(Cow::Borrowed("leonhard.euler"))?.as_ref(), /// "leonhard.euler" /// ); /// // Empty strings are not valid. - /// assert!(Username::try_from("").map_or_else( + /// assert!(Username::try_from(Cow::Borrowed("")).map_or_else( /// |e| matches!(e, UsernameErr::Rfc8265), /// |_| false /// )); /// # Ok::<_, webauthn_rp::AggErr>(()) /// ``` #[inline] - fn try_from(value: &'a str) -> Result<Self, Self::Error> { + fn try_from(value: Cow<'a, str>) -> Result<Self, Self::Error> { UsernameCasePreserved::default() .enforce(value) .map_err(|_e| UsernameErr::Rfc8265) @@ -325,6 +346,22 @@ impl<'a: 'b, 'b> TryFrom<&'a str> for Username<'b> { }) } } +impl<'a: 'b, 'b> TryFrom<&'a str> for Username<'b> { + type Error = UsernameErr; + /// Same as [`Username::try_from`] except the input is a `str`. + #[inline] + fn try_from(value: &'a str) -> Result<Self, Self::Error> { + Self::try_from(Cow::Borrowed(value)) + } +} +impl TryFrom<String> for Username<'_> { + type Error = UsernameErr; + /// Same as [`Username::try_from`] except the input is a `String`. + #[inline] + fn try_from(value: String) -> Result<Self, Self::Error> { + Self::try_from(Cow::Owned(value)) + } +} impl<'a: 'b, 'b> From<Username<'a>> for Cow<'b, str> { #[inline] fn from(value: Username<'a>) -> Self { @@ -783,7 +820,8 @@ impl UserHandle<Vec<u8>> { #[inline] #[must_use] pub fn new() -> Self { - Self::rand(64).unwrap_or_else(|_e| unreachable!("there is a bug in UserHandle::rand")) + Self::rand(USER_HANDLE_MAX_LEN) + .unwrap_or_else(|_e| unreachable!("there is a bug in UserHandle::rand")) } } /// Implements [`Default`] for [`UserHandle`] of array of length of the passed `usize` literal. @@ -923,6 +961,45 @@ impl<T: Hash> Hash for UserHandle<T> { self.0.hash(state); } } +/// `UserHandle` that is based on the [spec recommendation](https://www.w3.org/TR/webauthn-3/#user-handle). +pub type UserHandle64 = UserHandle<[u8; USER_HANDLE_MAX_LEN]>; +/// `UserHandle` that is based on 16 bytes. +/// +/// While not the recommended size like [`UserHandle64`], 16 bytes is common for many deployments since +/// it's the same size as [Universally Unique IDentifiers (UUIDs)](https://www.rfc-editor.org/rfc/rfc9562). +pub type UserHandle16 = UserHandle<[u8; 16]>; +/// Unifies `Option<UserHandle<T>>` and [`UserHandle`] such that both can be used for +/// [`AuthenticationServerState::verify`]. +/// +/// This `trait` is sealed and cannot be implemented for types outside of `webauthn_rp`. +pub trait User: super::private::Sealed { + /// Returns `true` iff [`UserHandle`] must exist. + fn must_exist() -> bool; + /// Returns `None` iff `self` cannot be compared to `other`. + /// Returns `Some(true)` iff `self` is equivalent to `other`. + /// Returns `Some(false)` iff `self` is not equivalent to `other`. + fn is_same(&self, other: UserHandle<&[u8]>) -> Option<bool>; +} +impl<T: AsRef<[u8]>> User for UserHandle<T> { + #[inline] + fn must_exist() -> bool { + true + } + #[inline] + fn is_same(&self, other: UserHandle<&[u8]>) -> Option<bool> { + Some(self.as_slice() == other) + } +} +impl<T: AsRef<[u8]>> User for Option<UserHandle<T>> { + #[inline] + fn must_exist() -> bool { + false + } + #[inline] + fn is_same(&self, other: UserHandle<&[u8]>) -> Option<bool> { + self.as_ref().map(|val| val.as_slice() == other) + } +} /// [The `PublicKeyCredentialUserEntity`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentity) /// sent to the client. #[derive(Clone, Debug)] diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs @@ -961,7 +961,7 @@ where D: Deserializer<'de>, { /// `Visitor` for `PublicKeyCredentialUserEntity`. - #[expect(clippy::type_complexity, reason = "type alias doesn't fit well with lifetimes")] + #[expect(clippy::type_complexity, reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable")] struct PublicKeyCredentialUserEntityVisitor<'a, 'b, U>(PhantomData<fn() -> (&'a (), &'b (), U)>); impl<'d: 'a + 'b, 'a, 'b, U> Visitor<'d> for PublicKeyCredentialUserEntityVisitor<'a, 'b, U> where @@ -994,7 +994,7 @@ where impl Visitor<'_> for FieldVisitor { type Value = Field; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - write!(formatter, "'{NAME}', '{ID}', '{DISPLAY_NAME}'") + write!(formatter, "'{NAME}', '{ID}', or '{DISPLAY_NAME}'") } fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> where diff --git a/src/response.rs b/src/response.rs @@ -35,7 +35,7 @@ use ser_relaxed::SerdeJsonErr; /// # use webauthn_rp::{ /// # request::{auth::{error::RequestOptionsErr, AuthenticationClientState, PublicKeyCredentialRequestOptions, AuthenticationVerificationOptions}, error::AsciiDomainErr, register::{UserHandle, USER_HANDLE_MAX_LEN}, AsciiDomain, BackupReq, RpId}, /// # response::{auth::{error::AuthCeremonyErr, Authentication}, error::CollectedClientDataErr, register::{AuthenticatorExtensionOutputStaticState, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, CompressedPubKey, StaticState}, AuthenticatorAttachment, Backup, CollectedClientData, CredentialId}, -/// # AuthenticatedCredential, CredentialErr +/// # AuthenticatedCredential, CredentialErr, PasskeyAuthentication64 /// # }; /// # #[derive(Debug)] /// # enum E { @@ -89,14 +89,13 @@ use ser_relaxed::SerdeJsonErr; /// InsertResult::Success /// )); /// # #[cfg(feature = "serde")] -/// let authentication = serde_json::from_str::<Authentication<[u8; USER_HANDLE_MAX_LEN]>>(get_authentication_json(client).as_str())?; -/// // `UserHandle` must exist since we sent an empty `AllowedCredentials`. +/// let authentication = serde_json::from_str::<PasskeyAuthentication64>(get_authentication_json(client).as_str())?; /// # #[cfg(feature = "serde")] -/// let user_handle = authentication.response().user_handle().ok_or(E::MissingUserHandle)?; +/// let user_handle = authentication.response().user_handle(); /// # #[cfg(feature = "serde")] -/// let (static_state, dynamic_state) = get_credential(authentication.raw_id(), user_handle).ok_or(E::UnknownCredential)?; +/// let (static_state, dynamic_state) = get_credential(authentication.raw_id(), user_handle.into()).ok_or(E::UnknownCredential)?; /// # #[cfg(all(feature = "custom", feature = "serde"))] -/// let mut cred = AuthenticatedCredential::new(authentication.raw_id(), user_handle, static_state, dynamic_state)?; +/// let mut cred = AuthenticatedCredential::new(authentication.raw_id(), user_handle.into(), static_state, dynamic_state)?; /// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom", feature = "serde"))] /// if ceremonies.take(&authentication.challenge()?).ok_or(E::MissingCeremony)?.verify(&rp_id, &authentication, &mut cred, &AuthenticationVerificationOptions::<&str, &str>::default())? { /// update_cred(authentication.raw_id(), cred.dynamic_state()); diff --git a/src/response/auth.rs b/src/response/auth.rs @@ -1,6 +1,9 @@ #[cfg(feature = "serde_relaxed")] use self::{ - super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr}, + super::{ + super::request::register::User, + ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr}, + }, ser_relaxed::{AuthenticationRelaxed, CustomAuthentication}, }; #[cfg(all(doc, feature = "serde_relaxed"))] @@ -14,7 +17,10 @@ use super::super::{ }, }; use super::{ - super::UserHandle, + super::{ + UserHandle, + request::register::{UserHandle16, UserHandle64}, + }, AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthenticatorAttachment, CborSuccess, ClientDataJsonParser as _, CollectedClientData, CredentialId, Flag, FromCbor, LimitedVerificationParser, ParsedAuthData, Response, SentChallenge, @@ -278,7 +284,7 @@ impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AuthenticatorData<'b> { } /// [`AuthenticatorAssertionResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse). #[derive(Debug)] -pub struct AuthenticatorAssertion<User> { +pub struct AuthenticatorAssertion<U> { /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson). client_data_json: Vec<u8>, /// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata) @@ -287,9 +293,9 @@ pub struct AuthenticatorAssertion<User> { /// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature). signature: Vec<u8>, /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle). - user_handle: Option<UserHandle<User>>, + user_handle: U, } -impl<User> AuthenticatorAssertion<User> { +impl<U> AuthenticatorAssertion<U> { /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson). #[inline] #[must_use] @@ -317,6 +323,12 @@ impl<User> AuthenticatorAssertion<User> { pub fn signature(&self) -> &[u8] { self.signature.as_slice() } + /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle). + #[inline] + #[must_use] + pub const fn user_handle(&self) -> &U { + &self.user_handle + } /// Constructs an instance of `Self` with the contained data. /// /// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes @@ -325,7 +337,7 @@ impl<User> AuthenticatorAssertion<User> { client_data_json: Vec<u8>, mut authenticator_data: Vec<u8>, signature: Vec<u8>, - user_handle: Option<UserHandle<User>>, + user_handle: U, ) -> Self { authenticator_data .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice()); @@ -337,39 +349,41 @@ impl<User> AuthenticatorAssertion<User> { } } } -impl<User: AsRef<[u8]>> AuthenticatorAssertion<User> { - /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle). - #[inline] - #[must_use] - pub fn user_handle(&self) -> Option<UserHandle<&[u8]>> { - self.user_handle.as_ref().map(UserHandle::as_slice) - } -} -impl AuthenticatorAssertion<Vec<u8>> { +impl<T> AuthenticatorAssertion<Option<UserHandle<T>>> { /// Constructs an instance of `Self` with the contained data. /// /// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes /// of available capacity; if not, a reallocation will occur. #[inline] #[must_use] - pub fn new_user_vec( + pub fn with_optional_user( client_data_json: Vec<u8>, authenticator_data: Vec<u8>, signature: Vec<u8>, - user_handle: Option<UserHandle<Vec<u8>>>, + user_handle: Option<UserHandle<T>>, ) -> Self { Self::new_inner(client_data_json, authenticator_data, signature, user_handle) } - /// Same as [`Self::new_user_vec`] except `user_handle` is required. + /// Same as [`Self::with_optional_user`] with `None` used for `user_handle`. + #[inline] + #[must_use] + pub fn without_user( + client_data_json: Vec<u8>, + authenticator_data: Vec<u8>, + signature: Vec<u8>, + ) -> Self { + Self::with_optional_user(client_data_json, authenticator_data, signature, None) + } + /// Same as [`Self::with_optional_user`] with `Some(user_handle)` used for `user_handle`. #[inline] #[must_use] - pub fn with_user_vec( + pub fn with_user( client_data_json: Vec<u8>, authenticator_data: Vec<u8>, signature: Vec<u8>, - user_handle: UserHandle<Vec<u8>>, + user_handle: UserHandle<T>, ) -> Self { - Self::new_inner( + Self::with_optional_user( client_data_json, authenticator_data, signature, @@ -377,7 +391,7 @@ impl AuthenticatorAssertion<Vec<u8>> { ) } } -impl<const USER_LEN: usize> AuthenticatorAssertion<[u8; USER_LEN]> { +impl<T> AuthenticatorAssertion<UserHandle<T>> { /// Constructs an instance of `Self` with the contained data. /// /// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes @@ -388,28 +402,12 @@ impl<const USER_LEN: usize> AuthenticatorAssertion<[u8; USER_LEN]> { client_data_json: Vec<u8>, authenticator_data: Vec<u8>, signature: Vec<u8>, - user_handle: Option<UserHandle<[u8; USER_LEN]>>, + user_handle: UserHandle<T>, ) -> Self { Self::new_inner(client_data_json, authenticator_data, signature, user_handle) } - /// Same as [`Self::new`] except `user_handle` is required. - #[inline] - #[must_use] - pub fn with_user( - client_data_json: Vec<u8>, - authenticator_data: Vec<u8>, - signature: Vec<u8>, - user_handle: UserHandle<[u8; USER_LEN]>, - ) -> Self { - Self::new_inner( - client_data_json, - authenticator_data, - signature, - Some(user_handle), - ) - } } -impl<User> AuthResponse for AuthenticatorAssertion<User> { +impl<U> AuthResponse for AuthenticatorAssertion<U> { type Auth<'a> = AuthenticatorData<'a> where @@ -449,7 +447,7 @@ impl<User> AuthResponse for AuthenticatorAssertion<User> { .map_err(AuthRespErr::CollectedClientDataRelaxed) } if relaxed { - get_client_collected_data::<'_, User>(self.client_data_json.as_slice()) + get_client_collected_data::<'_, U>(self.client_data_json.as_slice()) } else { CollectedClientData::from_client_data_json::<false>(self.client_data_json.as_slice()) .map_err(AuthRespErr::CollectedClientData) @@ -517,6 +515,8 @@ impl<User> AuthResponse for AuthenticatorAssertion<User> { }) } } +/// `AuthenticatorAssertion` with a required `UserHandle`. +pub type PasskeyAuthenticatorAssertion<T> = AuthenticatorAssertion<UserHandle<T>>; /// [`PublicKeyCredential`](https://www.w3.org/TR/webauthn-3/#iface-pkcredential) for authentication ceremonies. #[expect( clippy::field_scoped_visibility_modifiers, @@ -531,7 +531,7 @@ pub struct Authentication<User> { /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment). pub(crate) authenticator_attachment: AuthenticatorAttachment, } -impl<User> Authentication<User> { +impl<U> Authentication<U> { /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid). #[inline] #[must_use] @@ -541,7 +541,7 @@ impl<User> Authentication<User> { /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response). #[inline] #[must_use] - pub const fn response(&self) -> &AuthenticatorAssertion<User> { + pub const fn response(&self) -> &AuthenticatorAssertion<U> { &self.response } /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment). @@ -557,7 +557,7 @@ impl<User> Authentication<User> { #[must_use] pub const fn new( raw_id: CredentialId<Vec<u8>>, - response: AuthenticatorAssertion<User>, + response: AuthenticatorAssertion<U>, authenticator_attachment: AuthenticatorAttachment, ) -> Self { Self { @@ -618,9 +618,9 @@ impl<User> Authentication<User> { #[inline] pub fn from_json_relaxed<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr> where - UserHandle<User>: Deserialize<'a>, + U: Deserialize<'a> + User + Default, { - serde_json::from_slice::<AuthenticationRelaxed<User>>(json).map(|val| val.0) + serde_json::from_slice::<AuthenticationRelaxed<U>>(json).map(|val| val.0) } /// Convenience function for [`CustomAuthentication::deserialize`]. /// @@ -632,14 +632,20 @@ impl<User> Authentication<User> { #[inline] pub fn from_json_custom<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr> where - UserHandle<User>: Deserialize<'a>, + U: Deserialize<'a> + User + Default, { - serde_json::from_slice::<CustomAuthentication<User>>(json).map(|val| val.0) + serde_json::from_slice::<CustomAuthentication<U>>(json).map(|val| val.0) } } -impl<User> Response for Authentication<User> { - type Auth = AuthenticatorAssertion<User>; +impl<U> Response for Authentication<U> { + type Auth = AuthenticatorAssertion<U>; fn auth(&self) -> &Self::Auth { &self.response } } +/// `Authentication` with a required `UserHandle`. +pub type PasskeyAuthentication<T> = Authentication<UserHandle<T>>; +/// `Authentication` with a required `UserHandle64`. +pub type PasskeyAuthentication64 = Authentication<UserHandle64>; +/// `Authentication` with a required `UserHandle16`. +pub type PasskeyAuthentication16 = Authentication<UserHandle16>; diff --git a/src/response/auth/ser.rs b/src/response/auth/ser.rs @@ -4,17 +4,20 @@ )] use super::{ super::{ - super::response::ser::{Base64DecodedVal, PublicKeyCredential}, + super::{ + request::register::User, + response::ser::{Base64DecodedVal, PublicKeyCredential}, + }, ser::{ AuthenticationExtensionsPrfOutputsHelper, AuthenticationExtensionsPrfValues, ClientExtensions, }, }, - Authentication, AuthenticatorAssertion, UserHandle, + Authentication, AuthenticatorAssertion, error::UnknownCredentialOptions, }; #[cfg(doc)] -use super::{AuthenticatorAttachment, CredentialId}; +use super::{AuthenticatorAttachment, CredentialId, UserHandle}; use core::{ fmt::{self, Formatter}, marker::PhantomData, @@ -82,23 +85,22 @@ impl<'e> Deserialize<'e> for AuthData { /// /// Unknown fields are ignored and only `clientDataJSON`, `authenticatorData`, and `signature` are required iff /// `RELAXED`. -pub(super) struct AuthenticatorAssertionVisitor<const RELAXED: bool, User>( - PhantomData<fn() -> User>, -); +pub(super) struct AuthenticatorAssertionVisitor<const RELAXED: bool, U>(PhantomData<fn() -> U>); impl<const RELAXED: bool, USER> AuthenticatorAssertionVisitor<RELAXED, USER> { /// Returns `Self`. pub fn new() -> Self { Self(PhantomData) } } -impl<'d, const R: bool, User> Visitor<'d> for AuthenticatorAssertionVisitor<R, User> +impl<'d, const R: bool, U> Visitor<'d> for AuthenticatorAssertionVisitor<R, U> where - UserHandle<User>: Deserialize<'d>, + U: Deserialize<'d> + User + Default, { - type Value = AuthenticatorAssertion<User>; + type Value = AuthenticatorAssertion<U>; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { formatter.write_str("AuthenticatorAssertion") } + #[expect(clippy::too_many_lines, reason = "107 lines is fine")] fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> where A: MapAccess<'d>, @@ -200,13 +202,20 @@ where auth.ok_or_else(|| Error::missing_field(AUTHENTICATOR_DATA)) .and_then(|authenticator_data| { sig.ok_or_else(|| Error::missing_field(SIGNATURE)) - .map(|signature| { - AuthenticatorAssertion::new_inner( - client_data_json, - authenticator_data, - signature, - user_handle.flatten(), - ) + .and_then(|signature| { + if U::must_exist() { + user_handle.ok_or_else(|| Error::missing_field(USER_HANDLE)) + } else { + user_handle.map_or_else(|| Ok(U::default()), Ok) + } + .map(|user| { + AuthenticatorAssertion::new_inner( + client_data_json, + authenticator_data, + signature, + user, + ) + }) }) }) }) @@ -223,9 +232,9 @@ const USER_HANDLE: &str = "userHandle"; /// Fields in `AuthenticatorAssertionResponseJSON`. pub(super) const AUTH_ASSERT_FIELDS: &[&str; 4] = &[CLIENT_DATA_JSON, AUTHENTICATOR_DATA, SIGNATURE, USER_HANDLE]; -impl<'de, User> Deserialize<'de> for AuthenticatorAssertion<User> +impl<'de, U> Deserialize<'de> for AuthenticatorAssertion<U> where - UserHandle<User>: Deserialize<'de>, + U: Deserialize<'de> + User + Default, { /// Deserializes a `struct` based on /// [`AuthenticatorAssertionResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorassertionresponsejson). @@ -237,8 +246,9 @@ where /// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponsejson-signature) are /// base64url-decoded; /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponsejson-userhandle) is - /// `null` or deserialized via [`UserHandle::deserialize`]; and all `required` fields in the - /// `AuthenticatorAssertionResponseJSON` Web IDL `dictionary` exist (and are not `null`). + /// based on `U`. The key is allowed to not exist or exist with the value `null` iff `U` is + /// `Option<UserHandle<_>>`; otherwise it is deserialized via [`UserHandle::deserialize`]. All `required` + /// fields in the `AuthenticatorAssertionResponseJSON` Web IDL `dictionary` exist (and are not `null`). #[inline] fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where @@ -247,7 +257,7 @@ where deserializer.deserialize_struct( "AuthenticatorAssertion", AUTH_ASSERT_FIELDS, - AuthenticatorAssertionVisitor::<false, User>::new(), + AuthenticatorAssertionVisitor::<false, U>::new(), ) } } @@ -361,9 +371,9 @@ impl<'de> Deserialize<'de> for ClientExtensionsOutputs { ) } } -impl<'de, User> Deserialize<'de> for Authentication<User> +impl<'de, U> Deserialize<'de> for Authentication<U> where - UserHandle<User>: Deserialize<'de>, + U: Deserialize<'de> + User + Default, { /// Deserializes a `struct` based on /// [`AuthenticationResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationresponsejson). @@ -400,7 +410,7 @@ where where D: Deserializer<'de>, { - PublicKeyCredential::<false, false, AuthenticatorAssertion<User>, ClientExtensionsOutputs>::deserialize( + PublicKeyCredential::<false, false, AuthenticatorAssertion<U>, ClientExtensionsOutputs>::deserialize( deserializer, ) .map(|cred| Self { @@ -453,7 +463,7 @@ impl Serialize for UnknownCredentialOptions<'_, '_> { mod tests { use super::super::{ super::super::request::register::USER_HANDLE_MIN_LEN, Authentication, - AuthenticatorAttachment, + AuthenticatorAttachment, PasskeyAuthentication, UserHandle, }; use data_encoding::BASE64URL_NOPAD; use rsa::sha2::{Digest as _, Sha256}; @@ -510,7 +520,7 @@ mod tests { let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice()); // Base case is valid. assert!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -550,7 +560,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "ABABABABABABABABABABAA", @@ -575,7 +585,7 @@ mod tests { // missing `id`. err = Error::missing_field("id").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { @@ -597,11 +607,11 @@ mod tests { err ); // `null` `id`. - err = Error::invalid_type(Unexpected::Other("null"), &"id") + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": null, "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -625,7 +635,7 @@ mod tests { // missing `rawId`. err = Error::missing_field("rawId").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "response": { @@ -646,11 +656,11 @@ mod tests { err ); // `null` `rawId`. - err = Error::invalid_type(Unexpected::Other("null"), &"rawId") + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": null, @@ -676,7 +686,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -701,7 +711,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -725,7 +735,7 @@ mod tests { // Missing `signature`. err = Error::missing_field("signature").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -750,7 +760,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -773,7 +783,7 @@ mod tests { ); // Missing `userHandle`. assert!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<Authentication<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -792,7 +802,7 @@ mod tests { ); // `null` `userHandle`. assert!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<Authentication<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -812,7 +822,7 @@ mod tests { ); // `null` `authenticatorAttachment`. assert!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -842,7 +852,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -869,7 +879,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -894,7 +904,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -918,7 +928,7 @@ mod tests { // Missing `response`. err = Error::missing_field("response").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -938,7 +948,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -959,7 +969,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -980,7 +990,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1008,7 +1018,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1032,7 +1042,7 @@ mod tests { // Missing `type`. err = Error::missing_field("type").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1053,11 +1063,11 @@ mod tests { err ); // `null` `type`. - err = Error::invalid_type(Unexpected::Other("null"), &"type to be 'public-key'") + err = Error::invalid_type(Unexpected::Other("null"), &"public-key") .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1083,7 +1093,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1109,7 +1119,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!(null).to_string().as_str() ) .unwrap_err() @@ -1120,7 +1130,7 @@ mod tests { // Empty. err = Error::missing_field("response").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({}).to_string().as_str() ) .unwrap_err() @@ -1142,7 +1152,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1169,7 +1179,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -1209,7 +1219,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1234,7 +1244,7 @@ mod tests { // Duplicate field in `PublicKeyCredential`. err = Error::duplicate_field("id").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -1310,7 +1320,7 @@ mod tests { let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice()); // Base case is valid. assert!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1339,7 +1349,7 @@ mod tests { ); // `null` `prf`. assert!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1364,7 +1374,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1390,7 +1400,7 @@ mod tests { // Duplicate field. err = Error::duplicate_field("prf").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -1417,7 +1427,7 @@ mod tests { ); // `null` `results`. assert!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1442,7 +1452,7 @@ mod tests { // Duplicate field in `prf`. err = Error::duplicate_field("results").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -1472,7 +1482,7 @@ mod tests { // Missing `first`. err = Error::missing_field("first").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1499,7 +1509,7 @@ mod tests { ); // `null` `first`. assert!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1525,7 +1535,7 @@ mod tests { ); // `null` `second`. assert!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1555,7 +1565,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1587,7 +1597,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1620,7 +1630,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1651,7 +1661,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1682,7 +1692,7 @@ mod tests { // Duplicate field in `results`. err = Error::duplicate_field("first").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", diff --git a/src/response/auth/ser_relaxed.rs b/src/response/auth/ser_relaxed.rs @@ -6,6 +6,7 @@ use super::super::{Challenge, CredentialId}; use super::{ super::{ + super::request::register::{User, UserHandle16, UserHandle64}, auth::ser::{ AUTH_ASSERT_FIELDS, AuthData, AuthenticatorAssertionVisitor, ClientExtensionsOutputs, ClientExtensionsOutputsVisitor, EXT_FIELDS, @@ -59,10 +60,10 @@ impl<'de> Deserialize<'de> for ClientExtensionsOutputsRelaxed { } /// `newtype` around `AuthenticatorAssertion` with a "relaxed" [`Self::deserialize`] implementation. #[derive(Debug)] -pub struct AuthenticatorAssertionRelaxed<User>(pub AuthenticatorAssertion<User>); -impl<'de, User> Deserialize<'de> for AuthenticatorAssertionRelaxed<User> +pub struct AuthenticatorAssertionRelaxed<U>(pub AuthenticatorAssertion<U>); +impl<'de, U> Deserialize<'de> for AuthenticatorAssertionRelaxed<U> where - UserHandle<User>: Deserialize<'de>, + U: Deserialize<'de> + User + Default, { /// Same as [`AuthenticatorAssertion::deserialize`] except unknown keys are ignored. /// @@ -76,17 +77,17 @@ where .deserialize_struct( "AuthenticatorAssertionRelaxed", AUTH_ASSERT_FIELDS, - AuthenticatorAssertionVisitor::<true, User>::new(), + AuthenticatorAssertionVisitor::<true, U>::new(), ) .map(Self) } } /// `newtype` around `Authentication` with a "relaxed" [`Self::deserialize`] implementation. #[derive(Debug)] -pub struct AuthenticationRelaxed<User>(pub Authentication<User>); -impl<'de, User> Deserialize<'de> for AuthenticationRelaxed<User> +pub struct AuthenticationRelaxed<U>(pub Authentication<U>); +impl<'de, U> Deserialize<'de> for AuthenticationRelaxed<U> where - UserHandle<User>: Deserialize<'de>, + U: Deserialize<'de> + User + Default, { /// Same as [`Authentication::deserialize`] except unknown keys are ignored; /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-response) is deserialized @@ -106,8 +107,8 @@ where /// if it exists it must be `null`, and /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) can exist but /// must be `null` if so; and only - /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-id) and `response` are required. For - /// the other fields, they are allowed to not exist or be `null`. + /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-id) and `response` are required. + /// `rawId` and `type` and allowed to not exist. For the other fields, they are allowed to not exist or be `null`. /// /// Note that duplicate keys are still forbidden, and data matching still applies when applicable. #[expect(clippy::unreachable, reason = "when there is a bug, we want to crash")] @@ -119,7 +120,7 @@ where PublicKeyCredential::< true, false, - AuthenticatorAssertionRelaxed<User>, + AuthenticatorAssertionRelaxed<U>, ClientExtensionsOutputsRelaxed, >::deserialize(deserializer) .map(|cred| { @@ -133,18 +134,24 @@ where }) } } +/// `AuthenticationRelaxed` with a required `UserHandle`. +pub type PasskeyAuthenticationRelaxed<T> = AuthenticationRelaxed<UserHandle<T>>; +/// `AuthenticationRelaxed` with a required `UserHandle64`. +pub type PasskeyAuthenticationRelaxed64 = AuthenticationRelaxed<UserHandle64>; +/// `AuthenticationRelaxed` with a required `UserHandle16`. +pub type PasskeyAuthenticationRelaxed16 = AuthenticationRelaxed<UserHandle16>; /// `newtype` around `Authentication` with a custom [`Self::deserialize`] implementation. #[derive(Debug)] -pub struct CustomAuthentication<User>(pub Authentication<User>); -impl<'de, User> Deserialize<'de> for CustomAuthentication<User> +pub struct CustomAuthentication<U>(pub Authentication<U>); +impl<'de, U> Deserialize<'de> for CustomAuthentication<U> where - UserHandle<User>: Deserialize<'de>, + U: Deserialize<'de> + User + Default, { /// Despite the spec having a /// [pre-defined format](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationresponsejson) that clients /// can follow, the downside is the superfluous data it contains. /// - /// There simply is no reason to send the [`CredentialId`] _two_ times. This redundant data puts RPs in + /// There simply is no reason to send the [`CredentialId`] twice. This redundant data puts RPs in /// a position where they either ignore the data or parse the data to ensure no contradictions exist /// (e.g., [FIDO conformance requires one to verify `id` and `rawId` exist and match](https://github.com/w3c/webauthn/issues/2119#issuecomment-2287875401)). /// @@ -154,29 +161,43 @@ where /// /// ```json /// { - /// "authenticatorAttachment":null|"platform"|"cross-platform", - /// "authenticatorData":<base64url string>, - /// "clientDataJSON":<base64url string>, - /// "clientExtensionResults":{}|{"prf":null}|{"prf":{}}|{"prf":{"results":null}}|{"prf":{"results":{"first":null}}}|{"prf":{"results":{"first":null,"second":null}}}, - /// "id":<see CredentialId::deserialize>, - /// "signature":<base64url string>, - /// "type":null|"public-key", - /// "userHandle":null|<see UserHandle::deserialize> + /// "authenticatorAttachment": null | "platform" | "cross-platform", + /// "authenticatorData": <base64url string>, + /// "clientDataJSON": <base64url string>, + /// "clientExtensionResults": { + /// "prf": null | PRFJSON + /// }, + /// "id": <see CredentialId::deserialize>, + /// "signature": <base64url string>, + /// "type": "public-key", + /// "userHandle": null | <see UserHandle::deserialize> + /// } + /// // PRFJSON: + /// { + /// "results": null | PRFOutputsJSON + /// } + /// // PRFOutputsJSON: + /// { + /// "first": null, + /// "second": null /// } /// ``` /// - /// All of the above keys are required with the exceptions of `"authenticatorAttachment"`, `"type"`, and - /// `"userHandle"`. + /// `"userHandle"` is required to exist and not be `null` iff `U` is [`UserHandle`]. When it does exist and + /// is not `null`, then it is deserialized via [`UserHandle::deserialize`]. All of the remaining keys are + /// required with the exceptions of `"authenticatorAttachment"` and `"type"`. `"prf"` is not required in the + /// `clientExtensionResults` object, `"results"` is required in the `PRFJSON` object, and `"first"` + /// (but not `"second"`) is required in `PRFOutputsJSON`. /// /// # Examples /// /// ``` - /// # use webauthn_rp::{request::register::USER_HANDLE_MIN_LEN, response::auth::ser_relaxed::CustomAuthentication}; + /// # use webauthn_rp::{request::register::{UserHandle, USER_HANDLE_MIN_LEN}, response::auth::ser_relaxed::CustomAuthentication}; /// assert!( /// // The below payload is technically valid, but `AuthenticationServerState::verify` will fail /// // since the authenticatorData is not valid. This is true for `Authentication::deserialize` /// // as well since authenticatorData parsing is always deferred. - /// serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + /// serde_json::from_str::<CustomAuthentication<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>( /// r#"{ /// "authenticatorData": "AA", /// "authenticatorAttachment": "cross-platform", @@ -199,12 +220,12 @@ where D: Deserializer<'de>, { /// `Visitor` for `CustomAuthentication`. - struct CustomAuthenticationVisitor<U>(PhantomData<fn() -> U>); - impl<'d, U> Visitor<'d> for CustomAuthenticationVisitor<U> + struct CustomAuthenticationVisitor<UHand>(PhantomData<fn() -> UHand>); + impl<'d, UHand> Visitor<'d> for CustomAuthenticationVisitor<UHand> where - UserHandle<U>: Deserialize<'d>, + UHand: Deserialize<'d> + User + Default, { - type Value = CustomAuthentication<U>; + type Value = CustomAuthentication<UHand>; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { formatter.write_str("CustomAuthentication") } @@ -325,13 +346,13 @@ where if typ { return Err(Error::duplicate_field(TYPE)); } - typ = map.next_value::<Option<Type>>().map(|_| true)?; + typ = map.next_value::<Type>().map(|_| true)?; } Field::UserHandle => { if user_handle.is_some() { return Err(Error::duplicate_field(USER_HANDLE)); } - user_handle = map.next_value::<Option<_>>().map(Some)?; + user_handle = map.next_value().map(Some)?; } } } @@ -347,24 +368,30 @@ where .ok_or_else(|| Error::missing_field(SIGNATURE)) .and_then(|sig| { if ext { - Ok(CustomAuthentication(Authentication { - response: AuthenticatorAssertion::new_inner( - c_data, - auth_data, - sig, - user_handle.unwrap_or_default(), - ), - authenticator_attachment: - authenticator_attachment.map_or( - AuthenticatorAttachment::None, - |auth_attach| { - auth_attach.unwrap_or( - AuthenticatorAttachment::None, - ) - }, + if UHand::must_exist() { + user_handle.ok_or_else(|| Error::missing_field(USER_HANDLE)) + } else { + user_handle.map_or_else(|| Ok(UHand::default()), Ok) + }.map(|user| { + CustomAuthentication(Authentication { + response: AuthenticatorAssertion::new_inner( + c_data, + auth_data, + sig, + user, ), - raw_id, - })) + authenticator_attachment: + authenticator_attachment.map_or( + AuthenticatorAttachment::None, + |auth_attach| { + auth_attach.unwrap_or( + AuthenticatorAttachment::None, + ) + }, + ), + raw_id, + }) + }) } else { Err(Error::missing_field( CLIENT_EXTENSION_RESULTS, @@ -410,11 +437,21 @@ where ) } } +/// `CustomAuthentication` with a required `UserHandle`. +pub type PasskeyCustomAuthentication<T> = CustomAuthentication<UserHandle<T>>; +/// `CustomAuthentication` with a required `UserHandle64`. +pub type PasskeyCustomAuthentication64 = CustomAuthentication<UserHandle64>; +/// `CustomAuthentication` with a required `UserHandle16`. +pub type PasskeyCustomAuthentication16 = CustomAuthentication<UserHandle16>; #[cfg(test)] mod tests { use super::{ - super::{super::super::request::register::USER_HANDLE_MIN_LEN, AuthenticatorAttachment}, - AuthenticationRelaxed, CustomAuthentication, + super::{ + super::super::request::register::USER_HANDLE_MIN_LEN, AuthenticatorAttachment, + UserHandle, + }, + AuthenticationRelaxed, CustomAuthentication, PasskeyAuthenticationRelaxed, + PasskeyCustomAuthentication, }; use data_encoding::BASE64URL_NOPAD; use rsa::sha2::{Digest as _, Sha256}; @@ -471,7 +508,7 @@ mod tests { let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice()); // Base case is valid. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -511,7 +548,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "ABABABABABABABABABABAA", @@ -536,7 +573,7 @@ mod tests { // missing `id`. err = Error::missing_field("id").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { @@ -558,11 +595,11 @@ mod tests { err ); // `null` `id`. - err = Error::invalid_type(Unexpected::Other("null"), &"id") + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": null, "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -585,7 +622,7 @@ mod tests { ); // missing `rawId`. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "response": { @@ -603,8 +640,11 @@ mod tests { .is_ok() ); // `null` `rawId`. - assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": null, @@ -620,14 +660,17 @@ mod tests { .to_string() .as_str() ) - .is_ok() + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err ); // Missing `authenticatorData`. err = Error::missing_field("authenticatorData") .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -652,7 +695,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -676,7 +719,7 @@ mod tests { // Missing `signature`. err = Error::missing_field("signature").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -701,7 +744,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -724,7 +767,9 @@ mod tests { ); // Missing `userHandle`. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::< + AuthenticationRelaxed<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>, + >( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -743,7 +788,9 @@ mod tests { ); // `null` `userHandle`. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::< + AuthenticationRelaxed<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>, + >( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -763,7 +810,7 @@ mod tests { ); // `null` `authenticatorAttachment`. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -793,7 +840,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -820,7 +867,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -845,7 +892,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -869,7 +916,7 @@ mod tests { // Missing `response`. err = Error::missing_field("response").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -889,7 +936,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -910,7 +957,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -928,7 +975,7 @@ mod tests { ); // Missing `clientExtensionResults`. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -947,7 +994,7 @@ mod tests { ); // `null` `clientExtensionResults`. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -967,7 +1014,7 @@ mod tests { ); // Missing `type`. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -985,8 +1032,11 @@ mod tests { .is_ok() ); // `null` `type`. - assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + err = Error::invalid_type(Unexpected::Other("null"), &"public-key") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1002,14 +1052,17 @@ mod tests { .to_string() .as_str() ) - .is_ok() + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err ); // Not exactly `public-type` `type`. err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1035,7 +1088,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!(null).to_string().as_str() ) .unwrap_err() @@ -1046,7 +1099,7 @@ mod tests { // Empty. err = Error::missing_field("response").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({}).to_string().as_str() ) .unwrap_err() @@ -1056,7 +1109,7 @@ mod tests { ); // Unknown field in `response`. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1080,7 +1133,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -1106,7 +1159,7 @@ mod tests { ); // Unknown field in `PublicKeyCredential`. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1128,7 +1181,7 @@ mod tests { // Duplicate field in `PublicKeyCredential`. err = Error::duplicate_field("id").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -1154,7 +1207,7 @@ mod tests { ); // Base case is valid. assert!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, @@ -1181,7 +1234,7 @@ mod tests { // missing `id`. err = Error::missing_field("id").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "clientDataJSON": b64_cdata, "authenticatorData": b64_adata, @@ -1204,7 +1257,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": null, "clientDataJSON": b64_cdata, @@ -1227,7 +1280,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, @@ -1249,7 +1302,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, @@ -1270,7 +1323,7 @@ mod tests { // Missing `signature`. err = Error::missing_field("signature").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, @@ -1292,7 +1345,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, @@ -1312,7 +1365,9 @@ mod tests { ); // Missing `userHandle`. assert!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::< + CustomAuthentication<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>, + >( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, @@ -1328,7 +1383,9 @@ mod tests { ); // `null` `userHandle`. assert!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::< + CustomAuthentication<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>, + >( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, @@ -1345,7 +1402,7 @@ mod tests { ); // `null` `authenticatorAttachment`. assert!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, @@ -1372,7 +1429,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, @@ -1396,7 +1453,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "authenticatorData": b64_adata, @@ -1418,7 +1475,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": null, @@ -1441,7 +1498,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({}).to_string().as_str() ) .unwrap_err() @@ -1454,7 +1511,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, @@ -1476,7 +1533,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, @@ -1495,7 +1552,7 @@ mod tests { err ); assert!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, @@ -1509,8 +1566,12 @@ mod tests { ) .is_ok() ); - assert!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + // `null` `type`. + err = Error::invalid_type(Unexpected::Other("null"), &"public-key") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, @@ -1523,14 +1584,17 @@ mod tests { .to_string() .as_str() ) - .is_ok() + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err ); // Not exactly `public-type` `type`. err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, @@ -1553,7 +1617,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!(null).to_string().as_str() ) .unwrap_err() @@ -1579,7 +1643,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, @@ -1603,7 +1667,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -1676,7 +1740,7 @@ mod tests { let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice()); // Base case is valid. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1705,7 +1769,7 @@ mod tests { ); // `null` `prf`. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1727,7 +1791,7 @@ mod tests { ); // Unknown `clientExtensionResults`. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1750,7 +1814,7 @@ mod tests { // Duplicate field. let mut err = Error::duplicate_field("prf").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -1777,7 +1841,7 @@ mod tests { ); // `null` `results`. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1802,7 +1866,7 @@ mod tests { // Duplicate field in `prf`. err = Error::duplicate_field("results").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -1831,7 +1895,7 @@ mod tests { ); // Missing `first`. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1855,7 +1919,7 @@ mod tests { ); // `null` `first`. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1881,7 +1945,7 @@ mod tests { ); // `null` `second`. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1911,7 +1975,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1943,7 +2007,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1976,7 +2040,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -2004,7 +2068,7 @@ mod tests { ); // Unknown `prf` field. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -2029,7 +2093,7 @@ mod tests { ); // Unknown `results` field. assert!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -2057,7 +2121,7 @@ mod tests { // Duplicate field in `results`. err = Error::duplicate_field("first").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", diff --git a/src/response/register.rs b/src/response/register.rs @@ -3091,6 +3091,7 @@ mod tests { AggErr, request::{AsciiDomain, RpId}, }, + UserHandle, auth::{AuthenticatorAssertion, AuthenticatorData}, }, AttestationFormat, AttestationObject, AuthDataContainer as _, AuthTransports, @@ -3154,11 +3155,10 @@ mod tests { .unwrap(); let client_data_json_2 = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d".as_slice()).unwrap(); let signature = HEXLOWER.decode(b"3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87".as_slice()).unwrap(); - let auth_assertion = AuthenticatorAssertion::new_user_vec( + let auth_assertion = AuthenticatorAssertion::<Option<UserHandle<Vec<u8>>>>::without_user( client_data_json_2, authenticator_data, signature, - None, ); let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?; assert_eq!( @@ -3236,11 +3236,10 @@ mod tests { .unwrap(); let client_data_json_2 = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a225248696843784e534e493352594d45314f7731476d3132786e726b634a5f6666707637546e2d4a71386773222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a206754623533727a36456853576f6d58477a696d4331513d3d227d".as_slice()).unwrap(); let signature = HEXLOWER.decode(b"3044022076691be76a8618976d9803c4cdc9b97d34a7af37e3bdc894a2bf54f040ffae850220448033a015296ffb09a762efd0d719a55346941e17e91ebf64c60d439d0b9744".as_slice()).unwrap(); - let auth_assertion = AuthenticatorAssertion::new_user_vec( + let auth_assertion = AuthenticatorAssertion::<Option<UserHandle<Vec<u8>>>>::without_user( client_data_json_2, authenticator_data, signature, - None, ); let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?; assert_eq!( diff --git a/src/response/register/ser.rs b/src/response/register/ser.rs @@ -1792,7 +1792,7 @@ mod tests { err ); // `null` `id`. - err = Error::invalid_type(Unexpected::Other("null"), &"id") + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") .to_string() .into_bytes(); assert_eq!( @@ -1845,7 +1845,7 @@ mod tests { err ); // `null` `rawId`. - err = Error::invalid_type(Unexpected::Other("null"), &"rawId") + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") .to_string() .into_bytes(); assert_eq!( @@ -2585,7 +2585,7 @@ mod tests { err ); // `null` `type`. - err = Error::invalid_type(Unexpected::Other("null"), &"type to be 'public-key'") + err = Error::invalid_type(Unexpected::Other("null"), &"public-key") .to_string() .into_bytes(); assert_eq!( diff --git a/src/response/register/ser_relaxed.rs b/src/response/register/ser_relaxed.rs @@ -175,7 +175,8 @@ impl<'de> Deserialize<'de> for RegistrationRelaxed { /// via [`AuthenticatorAttestationRelaxed::deserialize`], /// [`clientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-clientextensionresults) /// is `null` or deserialized via [`ClientExtensionsOutputsRelaxed::deserialize`], and only `response` is required. - /// For the other fields, they are allowed to not exist or be `null`. + /// `id`, `rawId`, and `type` are allowed to not exist. For the other fields, they are allowed to not exist or + /// be `null`. /// /// Note that duplicate keys are still forbidden, and data matching still applies when applicable. #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")] @@ -229,12 +230,12 @@ impl<'de> Deserialize<'de> for CustomRegistration { /// /// ```json /// { - /// "attestationObject":<base64url string>, - /// "authenticatorAttachment":null|"platform"|"cross-platform", - /// "clientDataJSON":<base64url string>, - /// "clientExtensionResults":<see ClientExtensionsOutputs::deserialize>, - /// "transports":<see AuthTransports::deserialize>, - /// "type":null|"public-key" + /// "attestationObject": <base64url string>, + /// "authenticatorAttachment": null | "platform" | "cross-platform", + /// "clientDataJSON": <base64url string>, + /// "clientExtensionResults": <see ClientExtensionsOutputs::deserialize>, + /// "transports": <see AuthTransports::deserialize>, + /// "type": "public-key" /// } /// ``` /// @@ -376,7 +377,7 @@ impl<'de> Deserialize<'de> for CustomRegistration { if typ { return Err(Error::duplicate_field(TYPE)); } - typ = map.next_value::<Option<Type>>().map(|_| true)?; + typ = map.next_value::<Type>().map(|_| true)?; } } } @@ -719,13 +720,16 @@ mod tests { .is_ok() ); // `null` `id`. - assert!( + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") + .to_string() + .into_bytes(); + assert_eq!( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": null, "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": null, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -738,7 +742,10 @@ mod tests { .to_string() .as_str() ) - .is_ok() + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err ); // Missing `rawId`. assert!( @@ -762,7 +769,10 @@ mod tests { .is_ok() ); // `null` `rawId`. - assert!( + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") + .to_string() + .into_bytes(); + assert_eq!( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", @@ -781,7 +791,10 @@ mod tests { .to_string() .as_str() ) - .is_ok() + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err ); // `id` and the credential id in authenticator data mismatch. err = Error::invalid_value( @@ -1433,7 +1446,10 @@ mod tests { .is_ok() ); // `null` `type`. - assert!( + err = Error::invalid_type(Unexpected::Other("null"), &"public-key") + .to_string() + .into_bytes(); + assert_eq!( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1452,7 +1468,10 @@ mod tests { .to_string() .as_str() ) - .is_ok() + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err ); // Not exactly `public-type` `type`. err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") @@ -1902,7 +1921,10 @@ mod tests { .map_or(false, |_| true) ); // `null` `type`. - assert!( + err = Error::invalid_type(Unexpected::Other("null"), &"public-key") + .to_string() + .into_bytes(); + assert_eq!( serde_json::from_str::<CustomRegistration>( serde_json::json!({ "attestationObject": b64_aobj, @@ -1914,7 +1936,10 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |_| true) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err ); // Not exactly `public-type` `type`. err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") diff --git a/src/response/ser.rs b/src/response/ser.rs @@ -647,7 +647,7 @@ where } } let mut opt_id = None; - let mut typ = None; + let mut typ = false; let mut raw = None; let mut resp = None; let mut attach = None; @@ -658,19 +658,19 @@ where if opt_id.is_some() { return Err(Error::duplicate_field(ID)); } - opt_id = map.next_value::<Option<_>>().map(Some)?; + opt_id = map.next_value::<CredentialId<_>>().map(Some)?; } Field::Type => { - if typ.is_some() { + if typ { return Err(Error::duplicate_field(TYPE)); } - typ = map.next_value::<Option<Type>>().map(Some)?; + typ = map.next_value::<Type>().map(|_| true)?; } Field::RawId => { if raw.is_some() { return Err(Error::duplicate_field(RAW_ID)); } - raw = map.next_value::<Option<CredentialId<_>>>().map(Some)?; + raw = map.next_value::<CredentialId<_>>().map(Some)?; } Field::Response => { if resp.is_some() { @@ -695,12 +695,10 @@ where } resp.ok_or_else(|| Error::missing_field(RESPONSE)) .and_then(|response| { - opt_id.ok_or(false).and_then(|id| id.ok_or(true)).map_or_else( - |flag| { + opt_id.map_or_else( + || { if REL && REGI { Ok(None) - } else if flag { - Err(Error::invalid_type(Unexpected::Other("null"), &format!("{ID} to be a base64url-encoded CredentialId").as_str())) } else { Err(Error::missing_field(ID)) } @@ -708,12 +706,10 @@ where |id| Ok(Some(id)), ) .and_then(|id| { - raw.ok_or(false).and_then(|opt_raw_id| opt_raw_id.ok_or(true)).map_or_else( - |flag| { + raw.map_or_else( + || { if REL { Ok(()) - } else if flag { - Err(Error::invalid_type(Unexpected::Other("null"), &format!("{RAW_ID} to be a base64url-encoded CredentialId").as_str())) } else { Err(Error::missing_field(RAW_ID)) } @@ -748,23 +744,16 @@ where Ok ) .and_then(|client_extension_results| { - typ.ok_or(false).and_then(|opt_typ| opt_typ.ok_or(true)).map_or_else( - |flag| { - if REL { - Ok(()) - } else if flag { - Err(Error::invalid_type(Unexpected::Other("null"), &format!("{TYPE} to be '{PUBLIC_KEY}'").as_str())) - } else { - Err(Error::missing_field(TYPE)) - } - }, - |_| Ok(()), - ).map(|()| PublicKeyCredential { - id, - response, - authenticator_attachment: attach.flatten().unwrap_or(AuthenticatorAttachment::None), - client_extension_results, - }) + if typ || REL { + Ok(PublicKeyCredential { + id, + response, + authenticator_attachment: attach.flatten().unwrap_or(AuthenticatorAttachment::None), + client_extension_results, + }) + } else { + Err(Error::missing_field(TYPE)) + } }) }) })