webauthn_rp

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

commit 18f2066416892be8936c6677fb0f8dcd35213b1f
parent ac55944f66d2533d5ab9542f2b54c1dc9d72ea9d
Author: Zack Newman <zack@philomathiclife.com>
Date:   Mon, 24 Mar 2025 18:25:09 -0600

more userhandle impls, more decode impls, better relaxed deserialization integration

Diffstat:
MCargo.toml | 2+-
MREADME.md | 4++++
Msrc/bin.rs | 24++++++++++++++++--------
Msrc/lib.rs | 4++++
Msrc/request.rs | 114++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/request/auth.rs | 80++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/request/register.rs | 177+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/request/register/bin.rs | 28++++++++++++++++++----------
Msrc/request/register/custom.rs | 17++++++-----------
Msrc/request/register/ser.rs | 22++++++++++++++--------
Msrc/response.rs | 14+++++++-------
Msrc/response/auth.rs | 175++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/response/auth/ser.rs | 649++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/response/auth/ser_relaxed.rs | 1722++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/response/bin.rs | 11+++++++++++
Msrc/response/custom.rs | 158++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/response/register.rs | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/response/register/ser.rs | 1166+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/response/register/ser_relaxed.rs | 3581+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/response/ser.rs | 84++++++++++++++++++++++++++++++++++++++++++-------------------------------------
20 files changed, 5190 insertions(+), 2989 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -10,7 +10,7 @@ name = "webauthn_rp" readme = "README.md" repository = "https://git.philomathiclife.com/repos/webauthn_rp/" rust-version = "1.85.0" -version = "0.2.7" +version = "0.3.0" [package.metadata.docs.rs] all-features = true diff --git a/README.md b/README.md @@ -40,6 +40,10 @@ 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. + 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., [`RegistrationResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-registrationresponsejson)). Since diff --git a/src/bin.rs b/src/bin.rs @@ -207,11 +207,13 @@ impl<'a> DecodeBuffer<'a> for u16 { reason = "we must standardize the endianness to remove ambiguity" )] fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> { - data.split_at_checked(2) + /// Number of bytes `u16` is made of. + const SIZE: usize = 2; + data.split_at_checked(SIZE) .ok_or(EncDecErr) .map(|(le_bytes, rem)| { *data = rem; - let mut val = [0; 2]; + let mut val = [0; SIZE]; val.copy_from_slice(le_bytes); Self::from_le_bytes(val) }) @@ -224,11 +226,13 @@ impl<'a> DecodeBuffer<'a> for u32 { reason = "we must standardize the endianness to remove ambiguity" )] fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> { - data.split_at_checked(4) + /// Number of bytes `u32` is made of. + const SIZE: usize = 4; + data.split_at_checked(SIZE) .ok_or(EncDecErr) .map(|(le_bytes, rem)| { *data = rem; - let mut val = [0; 4]; + let mut val = [0; SIZE]; val.copy_from_slice(le_bytes); Self::from_le_bytes(val) }) @@ -241,11 +245,13 @@ impl<'a> DecodeBuffer<'a> for u64 { reason = "we must standardize the endianness to remove ambiguity" )] fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> { - data.split_at_checked(8) + /// Number of bytes `u64` is made of. + const SIZE: usize = 8; + data.split_at_checked(SIZE) .ok_or(EncDecErr) .map(|(le_bytes, rem)| { *data = rem; - let mut val = [0; 8]; + let mut val = [0; SIZE]; val.copy_from_slice(le_bytes); Self::from_le_bytes(val) }) @@ -258,11 +264,13 @@ impl<'a> DecodeBuffer<'a> for u128 { reason = "we must standardize the endianness to remove ambiguity" )] fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> { - data.split_at_checked(16) + /// Number of bytes `u128` is made of. + const SIZE: usize = 16; + data.split_at_checked(SIZE) .ok_or(EncDecErr) .map(|(le_bytes, rem)| { *data = rem; - let mut val = [0; 16]; + let mut val = [0; SIZE]; val.copy_from_slice(le_bytes); Self::from_le_bytes(val) }) diff --git a/src/lib.rs b/src/lib.rs @@ -40,6 +40,10 @@ //! //! ### `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. +//! //! 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., //! [`RegistrationResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-registrationresponsejson)). Since diff --git a/src/request.rs b/src/request.rs @@ -45,7 +45,7 @@ use url::Url as Uri; /// # use webauthn_rp::{ /// # request::{ /// # auth::{AllowedCredentials, PublicKeyCredentialRequestOptions}, -/// # register::UserHandle, +/// # register::{UserHandle, USER_HANDLE_MAX_LEN}, /// # AsciiDomain, Credentials, PublicKeyCredentialDescriptor, RpId, /// # }, /// # response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN}, @@ -76,9 +76,9 @@ use url::Url as Uri; /// # #[cfg(all(feature = "custom", feature = "serde"))] /// assert!(serde_json::to_string(&client_2).is_ok()); /// /// Extract `UserHandle` from session cookie. -/// fn get_user_handle() -> UserHandle<Vec<u8>> { +/// fn get_user_handle() -> UserHandle<[u8; USER_HANDLE_MAX_LEN]> { /// // ⋮ -/// # UserHandle::new() +/// # UserHandle::new_rand() /// } /// # #[cfg(feature = "custom")] /// /// Fetch the `AllowedCredentials` associated with `user`. @@ -110,7 +110,7 @@ pub mod error; /// # use webauthn_rp::{ /// # request::{ /// # register::{ -/// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle, +/// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MAX_LEN, /// # }, /// # AsciiDomain, PublicKeyCredentialDescriptor, RpId /// # }, @@ -144,9 +144,9 @@ pub mod error; /// # #[cfg(feature = "serde")] /// assert!(serde_json::to_string(&client_2).is_ok()); /// /// Extract `UserHandle` from session cookie if this is not the first credential registered. -/// fn get_user_handle() -> UserHandle<Vec<u8>> { +/// fn get_user_handle() -> UserHandle<[u8; USER_HANDLE_MAX_LEN]> { /// // ⋮ -/// # UserHandle::new() +/// # UserHandle::new_rand() /// } /// /// Fetch `PublicKeyCredentialUserEntity` info associated with `user`. /// /// @@ -926,17 +926,27 @@ pub enum UserVerificationRequirement { /// [`preferred`](https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-preferred). Preferred, } -/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhints). +#[cfg(test)] +impl PartialEq for UserVerificationRequirement { + fn eq(&self, other: &Self) -> bool { + match *self { + Self::Required => matches!(other, Self::Required), + Self::Discouraged => matches!(other, Self::Discouraged), + Self::Preferred => matches!(other, Self::Preferred), + } + } +} +/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint). #[derive(Clone, Copy, Debug, Default)] pub enum Hint { /// No hints. #[default] None, - /// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-security-key). + /// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-security-key). SecurityKey, - /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-client-device). + /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-client-device). ClientDevice, - /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-hybrid). + /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-hybrid). Hybrid, /// [`Self::SecurityKey`] and [`Self::ClientDevice`]. SecurityKeyClientDevice, @@ -963,6 +973,41 @@ pub enum Hint { /// [`Self::HybridClientDevice`] and [`Self::SecurityKey`]. HybridClientDeviceSecurityKey, } +#[cfg(test)] +impl PartialEq for Hint { + fn eq(&self, other: &Self) -> bool { + match *self { + Self::None => matches!(other, Self::None), + Self::SecurityKey => matches!(other, Self::SecurityKey), + Self::ClientDevice => matches!(other, Self::ClientDevice), + Self::Hybrid => matches!(other, Self::Hybrid), + Self::SecurityKeyClientDevice => matches!(other, Self::SecurityKeyClientDevice), + Self::ClientDeviceSecurityKey => matches!(other, Self::ClientDeviceSecurityKey), + Self::SecurityKeyHybrid => matches!(other, Self::SecurityKeyHybrid), + Self::HybridSecurityKey => matches!(other, Self::HybridSecurityKey), + Self::ClientDeviceHybrid => matches!(other, Self::ClientDeviceHybrid), + Self::HybridClientDevice => matches!(other, Self::HybridClientDevice), + Self::SecurityKeyClientDeviceHybrid => { + matches!(other, Self::SecurityKeyClientDeviceHybrid) + } + Self::SecurityKeyHybridClientDevice => { + matches!(other, Self::SecurityKeyHybridClientDevice) + } + Self::ClientDeviceSecurityKeyHybrid => { + matches!(other, Self::ClientDeviceSecurityKeyHybrid) + } + Self::ClientDeviceHybridSecurityKey => { + matches!(other, Self::ClientDeviceHybridSecurityKey) + } + Self::HybridSecurityKeyClientDevice => { + matches!(other, Self::HybridSecurityKeyClientDevice) + } + Self::HybridClientDeviceSecurityKey => { + matches!(other, Self::HybridClientDeviceSecurityKey) + } + } + } +} /// Controls if the response to a requested extension is required to be sent back. /// /// Note when requiring an extension, the extension must not only be sent back but also @@ -978,6 +1023,15 @@ pub enum ExtensionReq { /// The response to a requested extension is allowed, but not required, to be sent back. Allow, } +#[cfg(test)] +impl PartialEq for ExtensionReq { + fn eq(&self, other: &Self) -> bool { + match *self { + Self::Require => matches!(other, Self::Require), + Self::Allow => matches!(other, Self::Allow), + } + } +} /// Dictates how an extension should be processed. /// /// If one wants to only control if the extension should be returned, use [`ExtensionReq`]. @@ -992,6 +1046,17 @@ pub enum ExtensionInfo { /// Allow the associated extension to exist but don't enforce its value. AllowDontEnforceValue, } +#[cfg(test)] +impl PartialEq for ExtensionInfo { + fn eq(&self, other: &Self) -> bool { + match *self { + Self::RequireEnforceValue => matches!(other, Self::RequireEnforceValue), + Self::RequireDontEnforceValue => matches!(other, Self::RequireDontEnforceValue), + Self::AllowEnforceValue => matches!(other, Self::AllowEnforceValue), + Self::AllowDontEnforceValue => matches!(other, Self::AllowDontEnforceValue), + } + } +} impl Display for ExtensionInfo { #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { @@ -1019,6 +1084,17 @@ pub enum CredentialMediationRequirement { /// [`required`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-required) Required, } +#[cfg(test)] +impl PartialEq for CredentialMediationRequirement { + fn eq(&self, other: &Self) -> bool { + match *self { + Self::Silent => matches!(other, Self::Silent), + Self::Optional => matches!(other, Self::Optional), + Self::Conditional => matches!(other, Self::Conditional), + Self::Required => matches!(other, Self::Required), + } + } +} /// Backup requirements for the credential. #[derive(Clone, Copy, Debug, Default)] pub enum BackupReq { @@ -1194,7 +1270,7 @@ impl<'o, 't, O, T> From<&RegistrationVerificationOptions<'o, 't, O, T>> /// Functionality common to both registration and authentication ceremonies. /// /// Designed to be implemented on the _request_ side. -trait Ceremony { +trait Ceremony<U> { /// The type of response that is associated with the ceremony. type R: Response; /// Challenge. @@ -2408,11 +2484,11 @@ mod tests { &rp_id, &Authentication { raw_id: CredentialId::try_from(vec![0; 16])?, - response: AuthenticatorAssertion::new( + response: AuthenticatorAssertion::with_user( client_data_json, authenticator_data, sig, - Some(UserHandle::try_from(vec![0])?), + UserHandle::from([0]), ), authenticator_attachment: AuthenticatorAttachment::None, }, @@ -2774,11 +2850,11 @@ mod tests { &rp_id, &Authentication { raw_id: CredentialId::try_from(vec![0; 16])?, - response: AuthenticatorAssertion::new( + response: AuthenticatorAssertion::with_user( client_data_json, authenticator_data, der_sig.as_bytes().into(), - Some(UserHandle::try_from(vec![0])?), + UserHandle::from([0]), ), authenticator_attachment: AuthenticatorAttachment::None, }, @@ -3179,11 +3255,11 @@ mod tests { &rp_id, &Authentication { raw_id: CredentialId::try_from(vec![0; 16])?, - response: AuthenticatorAssertion::new( + response: AuthenticatorAssertion::with_user( client_data_json, authenticator_data, der_sig.as_bytes().into(), - Some(UserHandle::try_from(vec![0])?), + UserHandle::from([0]), ), authenticator_attachment: AuthenticatorAttachment::None, }, @@ -3846,11 +3922,11 @@ mod tests { &rp_id, &Authentication { raw_id: CredentialId::try_from(vec![0; 16])?, - response: AuthenticatorAssertion::new( + response: AuthenticatorAssertion::with_user( client_data_json, authenticator_data, sig, - Some(UserHandle::try_from(vec![0])?), + UserHandle::from([0]), ), authenticator_attachment: AuthenticatorAttachment::None, }, diff --git a/src/request/auth.rs b/src/request/auth.rs @@ -612,6 +612,15 @@ impl ServerPrfInfo { } } } +#[cfg(test)] +impl PartialEq for ServerPrfInfo { + fn eq(&self, other: &Self) -> bool { + match *self { + Self::One(req) => matches!(*other, Self::One(req2) if req == req2), + Self::Two(req) => matches!(*other, Self::Two(req2) if req == req2), + } + } +} impl From<PrfInput<'_>> for ServerPrfInfo { fn from(value: PrfInput<'_>) -> Self { value @@ -641,6 +650,12 @@ impl From<Extension<'_>> for ServerExtensionInfo { } } } +#[cfg(test)] +impl PartialEq for ServerExtensionInfo { + fn eq(&self, other: &Self) -> bool { + self.prf == other.prf + } +} /// `CredentialSpecificExtension` without the actual data sent to reduce memory usage when storing [`AuthenticationServerState`] /// in an in-memory collection. #[derive(Clone, Copy, Debug)] @@ -648,6 +663,12 @@ struct ServerCredSpecificExtensionInfo { /// `CredentialSpecificExtension::prf`. prf: Option<ServerPrfInfo>, } +#[cfg(test)] +impl PartialEq for ServerCredSpecificExtensionInfo { + fn eq(&self, other: &Self) -> bool { + self.prf == other.prf + } +} impl From<&CredentialSpecificExtension> for ServerCredSpecificExtensionInfo { fn from(value: &CredentialSpecificExtension) -> Self { Self { @@ -718,6 +739,12 @@ struct CredInfo { /// Any credential-specific extensions. ext: ServerCredSpecificExtensionInfo, } +#[cfg(test)] +impl PartialEq for CredInfo { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.ext == other.ext + } +} /// Controls how to handle a change in [`DynamicState::authenticator_attachment`]. /// /// Note when `DynamicState::authenticator_attachment` is [`AuthenticatorAttachment::None`], then it will @@ -929,10 +956,11 @@ impl AuthenticationServerState { P256Key: AsRef<[u8]>, P384Key: AsRef<[u8]>, RsaKey: AsRef<[u8]>, + User: AsRef<[u8]>, >( self, rp_id: &RpId, - response: &'a Authentication, + response: &'a Authentication<User>, cred: &mut AuthenticatedCredential< 'a, 'user, @@ -1070,9 +1098,9 @@ 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>( + fn verify_nondiscoverable<'a, PublicKey, User: AsRef<[u8]>>( &self, - response: &'a Authentication, + response: &'a Authentication<User>, cred: &AuthenticatedCredential<'a, '_, PublicKey>, ) -> Result<Option<ServerCredSpecificExtensionInfo>, AuthCeremonyErr> { response @@ -1094,6 +1122,14 @@ impl AuthenticationServerState { .map(|c| Some(c.ext)) }) } + #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))] + fn is_eq(&self, other: &Self) -> bool { + self.challenge == other.challenge + && self.allow_credentials == other.allow_credentials + && self.user_verification == other.user_verification + && self.extensions == other.extensions + && self.expiration == other.expiration + } } impl ServerState for AuthenticationServerState { #[cfg(any(doc, not(feature = "serializable_server_state")))] @@ -1111,8 +1147,8 @@ impl ServerState for AuthenticationServerState { self.challenge } } -impl Ceremony for AuthenticationServerState { - type R = Authentication; +impl<User> Ceremony<User> for AuthenticationServerState { + type R = Authentication<User>; fn rand_challenge(&self) -> SentChallenge { self.challenge } @@ -1184,7 +1220,7 @@ mod tests { }, AllowedCredential, AllowedCredentials, AuthenticationServerState, Challenge, CredentialId, CredentialSpecificExtension, Credentials as _, Extension, ExtensionReq, PrfInputOwned, - PublicKeyCredentialDescriptor, PublicKeyCredentialRequestOptions, RpId, ServerPrfInfo, + PublicKeyCredentialDescriptor, PublicKeyCredentialRequestOptions, RpId, UserVerificationRequirement, }; #[cfg(all(feature = "custom", feature = "serializable_server_state"))] @@ -1371,35 +1407,9 @@ mod tests { .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice()); authenticator_data.truncate(132); let server = opts.start_ceremony()?.0; - let server_2 = AuthenticationServerState::decode(server.encode()?.as_slice())?; - assert_eq!(server.challenge.0, server_2.challenge.0); - assert_eq!(server.allow_credentials.len(), 1); - assert_eq!( - server.allow_credentials.len(), - server_2.allow_credentials.len() - ); - assert_eq!( - server.allow_credentials[0].id, - server_2.allow_credentials[0].id - ); - assert!(matches!( - server.allow_credentials[0].ext.prf.unwrap(), - ServerPrfInfo::Two(req) if matches!(req, ExtensionReq::Require) - )); - assert!(server_2.allow_credentials[0].ext.prf.map_or(false, |prf| { - matches!(prf, ServerPrfInfo::Two(req) if matches!(req, ExtensionReq::Require)) - })); - assert!( - matches!( - server.user_verification, - UserVerificationRequirement::Required - ) && matches!( - server_2.user_verification, - UserVerificationRequirement::Required - ) - ); - assert!(server.extensions.prf.is_none() && server_2.extensions.prf.is_none()); - assert_eq!(server.expiration, server_2.expiration); + assert!(server.is_eq(&AuthenticationServerState::decode( + server.encode()?.as_slice() + )?)); Ok(()) } } diff --git a/src/request/register.rs b/src/request/register.rs @@ -157,6 +157,23 @@ impl CredProtect { } } } +#[cfg(test)] +impl PartialEq for CredProtect { + fn eq(&self, other: &Self) -> bool { + match *self { + Self::None => matches!(other, Self::None), + Self::UserVerificationOptional(info) => { + matches!(*other, Self::UserVerificationOptional(info2) if info == info2) + } + Self::UserVerificationOptionalWithCredentialIdList(info) => { + matches!(*other, Self::UserVerificationOptionalWithCredentialIdList(info2) if info == info2) + } + Self::UserVerificationRequired(info) => { + matches!(*other, Self::UserVerificationRequired(info2) if info == info2) + } + } + } +} impl Display for CredProtect { #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { @@ -415,6 +432,12 @@ impl Default for CoseAlgorithmIdentifiers { Self::ALL } } +#[cfg(test)] +impl PartialEq for CoseAlgorithmIdentifiers { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} /// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client. #[derive(Clone, Copy, Debug, Default)] pub struct Extension { @@ -658,6 +681,15 @@ impl Extension { }) } } +#[cfg(test)] +impl PartialEq for Extension { + fn eq(&self, other: &Self) -> bool { + self.cred_props == other.cred_props + && self.cred_protect == other.cred_protect + && self.min_pin_length == other.min_pin_length + && self.prf == other.prf + } +} /// The maximum number of bytes a [`UserHandle`] can be made of per /// [WebAuthn](https://www.w3.org/TR/webauthn-3/#user-handle). pub const USER_HANDLE_MAX_LEN: usize = 64; @@ -680,6 +712,12 @@ impl<T> UserHandle<T> { &self.0 } } +impl<T: AsRef<[u8]>> UserHandle<T> { + /// Returns a `UserHandle` containing a `slice`. + pub(crate) fn as_slice(&self) -> UserHandle<&[u8]> { + UserHandle(self.0.as_ref()) + } +} #[cfg(any(feature = "bin", feature = "custom"))] impl<'a> UserHandle<&'a [u8]> { /// Creates a `UserHandle` from a `slice`. @@ -936,16 +974,26 @@ pub enum ResidentKeyRequirement { /// [`preferred`](https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-preferred). Preferred, } -/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhints) +#[cfg(test)] +impl PartialEq for ResidentKeyRequirement { + fn eq(&self, other: &Self) -> bool { + match *self { + Self::Required => matches!(other, Self::Required), + Self::Discouraged => matches!(other, Self::Discouraged), + Self::Preferred => matches!(other, Self::Preferred), + } + } +} +/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint) /// for [`AuthenticatorAttachment::CrossPlatform`] authenticators. #[derive(Clone, Copy, Debug, Default)] pub enum CrossPlatformHint { /// No hints. #[default] None, - /// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-security-key). + /// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-security-key). SecurityKey, - /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-hybrid). + /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-hybrid). Hybrid, /// [`Self::SecurityKey`] and [`Self::Hybrid`]. SecurityKeyHybrid, @@ -964,14 +1012,26 @@ impl From<CrossPlatformHint> for Hint { } } } -/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhints) +#[cfg(test)] +impl PartialEq for CrossPlatformHint { + fn eq(&self, other: &Self) -> bool { + match *self { + Self::None => matches!(other, Self::None), + Self::SecurityKey => matches!(other, Self::SecurityKey), + Self::Hybrid => matches!(other, Self::Hybrid), + Self::SecurityKeyHybrid => matches!(other, Self::SecurityKeyHybrid), + Self::HybridSecurityKey => matches!(other, Self::HybridSecurityKey), + } + } +} +/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint) /// for [`AuthenticatorAttachment::Platform`] authenticators. #[derive(Clone, Copy, Debug, Default)] pub enum PlatformHint { /// No hints. #[default] None, - /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-client-device). + /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-client-device). ClientDevice, } impl From<PlatformHint> for Hint { @@ -983,6 +1043,15 @@ impl From<PlatformHint> for Hint { } } } +#[cfg(test)] +impl PartialEq for PlatformHint { + fn eq(&self, other: &Self) -> bool { + match *self { + Self::None => matches!(other, Self::None), + Self::ClientDevice => matches!(other, Self::ClientDevice), + } + } +} /// [`AuthenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment) /// requirement with associated hints for further refinement. #[derive(Clone, Copy, Debug)] @@ -1046,6 +1115,18 @@ impl AuthenticatorAttachmentReq { } } } +#[cfg(test)] +impl PartialEq for AuthenticatorAttachmentReq { + fn eq(&self, other: &Self) -> bool { + match *self { + Self::None(info) => matches!(*other, Self::None(info2) if info == info2), + Self::Platform(info) => matches!(*other, Self::Platform(info2) if info == info2), + Self::CrossPlatform(info) => { + matches!(*other, Self::CrossPlatform(info2) if info == info2) + } + } + } +} /// [`AuthenticatorSelectionCriteria`](https://www.w3.org/TR/webauthn-3/#dictionary-authenticatorSelection). #[derive(Clone, Copy, Debug)] pub struct AuthenticatorSelectionCriteria { @@ -1149,6 +1230,14 @@ impl AuthenticatorSelectionCriteria { .validate(require_auth_attachment, auth_attachment) } } +#[cfg(test)] +impl PartialEq for AuthenticatorSelectionCriteria { + fn eq(&self, other: &Self) -> bool { + self.authenticator_attachment == other.authenticator_attachment + && self.resident_key == other.resident_key + && self.user_verification == other.user_verification + } +} /// Helper that verifies the overlap of [`PublicKeyCredentialCreationOptions::start_ceremony`] and /// [`RegistrationServerState::decode`]. const fn validate_options_helper( @@ -1667,6 +1756,15 @@ impl RegistrationServerState { } }) } + #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))] + fn is_eq(&self, other: &Self) -> bool { + self.mediation == other.mediation + && self.challenge == other.challenge + && self.pub_key_cred_params == other.pub_key_cred_params + && self.authenticator_selection == other.authenticator_selection + && self.extensions == other.extensions + && self.expiration == other.expiration + } } impl ServerState for RegistrationServerState { #[cfg(any(doc, not(feature = "serializable_server_state")))] @@ -1684,7 +1782,7 @@ impl ServerState for RegistrationServerState { self.challenge } } -impl Ceremony for RegistrationServerState { +impl Ceremony<()> for RegistrationServerState { type R = Registration; fn rand_challenge(&self) -> SentChallenge { self.challenge @@ -1755,10 +1853,8 @@ mod tests { }, AsciiDomain, }, - AuthenticatorAttachmentReq, Challenge, CredProtect, CredentialMediationRequirement, - Extension, ExtensionInfo, Hint, PublicKeyCredentialCreationOptions, - PublicKeyCredentialUserEntity, RegistrationServerState, ResidentKeyRequirement, RpId, - UserHandle, UserVerificationRequirement, + Challenge, CredProtect, Extension, ExtensionInfo, PublicKeyCredentialCreationOptions, + PublicKeyCredentialUserEntity, RegistrationServerState, RpId, UserHandle, }; #[cfg(all(feature = "custom", feature = "serializable_server_state"))] use ed25519_dalek::{Signer as _, SigningKey}; @@ -2104,64 +2200,9 @@ mod tests { attestation_object[32..96].copy_from_slice(sig.to_bytes().as_slice()); attestation_object.truncate(261); let server = opts.start_ceremony()?.0; - let server_2 = RegistrationServerState::decode(server.encode()?.as_slice())?; - assert!( - matches!(server.mediation, CredentialMediationRequirement::Optional) - && matches!(server_2.mediation, CredentialMediationRequirement::Optional) - ); - assert_eq!(server.challenge.0, server_2.challenge.0); - assert_eq!(server.pub_key_cred_params.0, server_2.pub_key_cred_params.0); - assert!( - matches!(server.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) - && matches!(server_2.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) - ); - assert!( - matches!( - server.authenticator_selection.resident_key, - ResidentKeyRequirement::Required - ) && matches!( - server_2.authenticator_selection.resident_key, - ResidentKeyRequirement::Required - ) - ); - assert!( - matches!( - server.authenticator_selection.user_verification, - UserVerificationRequirement::Required - ) && matches!( - server_2.authenticator_selection.user_verification, - UserVerificationRequirement::Required - ) - ); - assert!(server.extensions.cred_props.is_none() && server_2.extensions.cred_props.is_none()); - assert!( - matches!( - server.extensions.cred_protect, - CredProtect::UserVerificationRequired(info) if matches!(info, ExtensionInfo::RequireEnforceValue) - ) && matches!( - server_2.extensions.cred_protect, - CredProtect::UserVerificationRequired(info) if matches!(info, ExtensionInfo::RequireEnforceValue) - ) - ); - let (pin, info) = server.extensions.min_pin_length.unwrap(); - assert!( - server_2 - .extensions - .min_pin_length - .map_or(false, |(pin2, info2)| pin == pin2 - && matches!(info, ExtensionInfo::RequireEnforceValue) - && matches!(info2, ExtensionInfo::RequireEnforceValue)) - ); - assert!( - matches!( - server.extensions.prf.unwrap(), - ExtensionInfo::RequireEnforceValue - ) && server_2.extensions.prf.map_or(false, |info| matches!( - info, - ExtensionInfo::RequireEnforceValue - )) - ); - assert_eq!(server.expiration, server_2.expiration); + assert!(server.is_eq(&RegistrationServerState::decode( + server.encode()?.as_slice() + )?)); Ok(()) } } diff --git a/src/request/register/bin.rs b/src/request/register/bin.rs @@ -31,6 +31,14 @@ impl Decode for UserHandle<Vec<u8>> { } } } +impl<'b> Decode for UserHandle<&'b [u8]> { + type Input<'a> = &'b [u8]; + type Err = UserHandleErr; + #[inline] + fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> { + UserHandle::<&[u8]>::from_slice(input) + } +} impl<const LEN: usize> Decode for UserHandle<[u8; LEN]> where Self: Default, @@ -73,16 +81,16 @@ impl Display for DecodeNicknameErr { } } impl Error for DecodeNicknameErr {} -impl Decode for Nickname<'_> { - type Input<'a> = String; +impl<'b> Decode for Nickname<'b> { + type Input<'a> = &'b str; type Err = DecodeNicknameErr; #[inline] fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> { - match Nickname::try_from(input.as_str()).map_err(DecodeNicknameErr::Nickname) { + match Nickname::try_from(input).map_err(DecodeNicknameErr::Nickname) { Ok(v) => match v.0 { Cow::Borrowed(name) => { - if name == input.as_str() { - Ok(Self(Cow::Owned(input))) + if name == input { + Ok(Self(Cow::Borrowed(input))) } else { Err(DecodeNicknameErr::NotCanonical) } @@ -124,16 +132,16 @@ impl Display for DecodeUsernameErr { } } impl Error for DecodeUsernameErr {} -impl Decode for Username<'_> { - type Input<'a> = String; +impl<'b> Decode for Username<'b> { + type Input<'a> = &'b str; type Err = DecodeUsernameErr; #[inline] fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> { - match Username::try_from(input.as_str()).map_err(DecodeUsernameErr::Username) { + match Username::try_from(input).map_err(DecodeUsernameErr::Username) { Ok(v) => match v.0 { Cow::Borrowed(name) => { - if name == input.as_str() { - Ok(Self(Cow::Owned(input))) + if name == input { + Ok(Self(Cow::Borrowed(input))) } else { Err(DecodeUsernameErr::NotCanonical) } diff --git a/src/request/register/custom.rs b/src/request/register/custom.rs @@ -1,16 +1,11 @@ -use super::{USER_HANDLE_MAX_LEN, USER_HANDLE_MIN_LEN, UserHandle, UserHandleErr}; -#[expect(clippy::fallible_impl_from, reason = "backward compatible fix")] -impl<const LEN: usize> From<[u8; LEN]> for UserHandle<[u8; LEN]> { - #[expect(clippy::panic, reason = "backward compatible fix")] +use super::{UserHandle, UserHandleErr}; +impl<const LEN: usize> From<[u8; LEN]> for UserHandle<[u8; LEN]> +where + Self: Default, +{ #[inline] fn from(value: [u8; LEN]) -> Self { - if (USER_HANDLE_MIN_LEN..=USER_HANDLE_MAX_LEN).contains(&value.len()) { - Self(value) - } else { - panic!( - "UserHandle::from must only be passed an array of length 1 to 64 inclusively. Update webauthn_rp to 0.3.0 or greater to avoid this `panic` possibility" - ); - } + Self(value) } } impl<'a: 'b, 'b> TryFrom<&'a [u8]> for UserHandle<&'b [u8]> { diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs @@ -912,8 +912,10 @@ where deserializer.deserialize_str(UserHandleVisitor) } } -impl<'de: 'name + 'display_name, 'name, 'display_name> Deserialize<'de> - for PublicKeyCredentialUserEntity<'name, 'display_name, Vec<u8>> +impl<'de: 'name + 'display_name, 'name, 'display_name, T> Deserialize<'de> + for PublicKeyCredentialUserEntity<'name, 'display_name, T> +where + UserHandle<T>: Deserialize<'de>, { /// Deserializes a `struct` based on /// [`PublicKeyCredentialUserEntityJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentityjson). @@ -921,9 +923,9 @@ impl<'de: 'name + 'display_name, 'name, 'display_name> Deserialize<'de> /// # Examples /// /// ``` - /// # use webauthn_rp::request::register::{Nickname, PublicKeyCredentialUserEntity}; + /// # use webauthn_rp::request::register::{Nickname, PublicKeyCredentialUserEntity, USER_HANDLE_MIN_LEN}; /// assert_eq!( - /// serde_json::from_str::<PublicKeyCredentialUserEntity<Vec<u8>>>( + /// serde_json::from_str::<PublicKeyCredentialUserEntity<[u8; USER_HANDLE_MIN_LEN]>>( /// serde_json::json!({ /// "name": "pythagoras.of.samos", /// "id": "AA", @@ -959,9 +961,13 @@ impl<'de: 'name + 'display_name, 'name, 'display_name> Deserialize<'de> D: Deserializer<'de>, { /// `Visitor` for `PublicKeyCredentialUserEntity`. - struct PublicKeyCredentialUserEntityVisitor<'a, 'b>(PhantomData<fn() -> (&'a (), &'b ())>); - impl<'d: 'a + 'b, 'a, 'b> Visitor<'d> for PublicKeyCredentialUserEntityVisitor<'a, 'b> { - type Value = PublicKeyCredentialUserEntity<'a, 'b, Vec<u8>>; + #[expect(clippy::type_complexity, reason = "type alias doesn't fit well with lifetimes")] + 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 + UserHandle<U>: Deserialize<'d>, + { + type Value = PublicKeyCredentialUserEntity<'a, 'b, U>; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { formatter.write_str("PublicKeyCredentialUserEntity") } @@ -988,7 +994,7 @@ impl<'de: 'name + 'display_name, 'name, 'display_name> Deserialize<'de> impl Visitor<'_> for FieldVisitor { type Value = Field; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - formatter.write_str("'name', 'id', or 'displayName'") + write!(formatter, "'{NAME}', '{ID}', '{DISPLAY_NAME}'") } fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> where diff --git a/src/response.rs b/src/response.rs @@ -33,7 +33,7 @@ use ser_relaxed::SerdeJsonErr; /// # #[cfg(not(feature = "serializable_server_state"))] /// # use webauthn_rp::request::{FixedCapHashSet, InsertResult}; /// # use webauthn_rp::{ -/// # request::{auth::{error::RequestOptionsErr, AuthenticationClientState, PublicKeyCredentialRequestOptions, AuthenticationVerificationOptions}, error::AsciiDomainErr, register::UserHandle, AsciiDomain, BackupReq, RpId}, +/// # 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 /// # }; @@ -89,7 +89,7 @@ use ser_relaxed::SerdeJsonErr; /// InsertResult::Success /// )); /// # #[cfg(feature = "serde")] -/// let authentication = serde_json::from_str::<Authentication>(get_authentication_json(client).as_str())?; +/// 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`. /// # #[cfg(feature = "serde")] /// let user_handle = authentication.response().user_handle().ok_or(E::MissingUserHandle)?; @@ -147,7 +147,7 @@ mod cbor; /// Contains functionality that needs to be accessible when `bin` or `serde` are not enabled. #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] #[cfg(feature = "custom")] -mod custom; +pub mod custom; /// Contains error types. pub mod error; /// Contains functionality for completing the @@ -160,7 +160,7 @@ pub mod error; /// # #[cfg(not(feature = "serializable_server_state"))] /// # use webauthn_rp::request::{FixedCapHashSet, InsertResult}; /// # use webauthn_rp::{ -/// # request::{register::{error::CreationOptionsErr, PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, RegistrationVerificationOptions}, error::AsciiDomainErr, AsciiDomain, PublicKeyCredentialDescriptor, RpId}, +/// # request::{register::{error::CreationOptionsErr, PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, USER_HANDLE_MAX_LEN, RegistrationVerificationOptions}, error::AsciiDomainErr, AsciiDomain, PublicKeyCredentialDescriptor, RpId}, /// # response::{register::{error::RegCeremonyErr, Registration}, error::CollectedClientDataErr, CollectedClientData}, /// # RegisteredCredential /// # }; @@ -217,9 +217,9 @@ pub mod error; /// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom", feature = "serde_relaxed"))] /// insert_cred(ceremonies.take(&registration.challenge()?).ok_or(E::MissingCeremony)?.verify(&rp_id, handle, &registration, &ver_opts)?); /// /// Extract `UserHandle` from session cookie if this is not the first credential registered. -/// fn get_user_handle() -> UserHandle<Vec<u8>> { +/// fn get_user_handle() -> UserHandle<[u8; USER_HANDLE_MAX_LEN]> { /// // ⋮ -/// # UserHandle::new() +/// # UserHandle::new_rand() /// } /// /// Fetch `PublicKeyCredentialUserEntity` info associated with `user`. /// /// @@ -483,7 +483,7 @@ impl PartialEq<AuthenticatorAttachment> for &AuthenticatorAttachment { } } /// The maximum number of bytes that can make up a Credential ID -/// [per WebAuthn](https://www.w3.org/TR/webauthn-3/#credential-id) +/// [per WebAuthn](https://www.w3.org/TR/webauthn-3/#credential-id). pub const CRED_ID_MAX_LEN: usize = 1023; /// The minimum number of bytes that can make up a Credential ID /// [per WebAuthn](https://www.w3.org/TR/webauthn-3/#credential-id). diff --git a/src/response/auth.rs b/src/response/auth.rs @@ -1,24 +1,27 @@ +#[cfg(feature = "serde_relaxed")] +use self::{ + super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr}, + ser_relaxed::{AuthenticationRelaxed, CustomAuthentication}, +}; #[cfg(all(doc, feature = "serde_relaxed"))] use super::super::request::FixedCapHashSet; #[cfg(doc)] use super::super::{ + AuthenticatedCredential, RegisteredCredential, StaticState, request::{ - auth::{AuthenticationServerState, PublicKeyCredentialRequestOptions}, Challenge, + auth::{AuthenticationServerState, PublicKeyCredentialRequestOptions}, }, - AuthenticatedCredential, RegisteredCredential, StaticState, }; -#[cfg(feature = "serde_relaxed")] -use super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr}; use super::{ super::UserHandle, + AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthenticatorAttachment, + CborSuccess, ClientDataJsonParser as _, CollectedClientData, CredentialId, Flag, FromCbor, + LimitedVerificationParser, ParsedAuthData, Response, SentChallenge, auth::error::{AuthenticatorDataErr, AuthenticatorExtensionOutputErr}, cbor, error::CollectedClientDataErr, register::CompressedPubKey, - AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthenticatorAttachment, - CborSuccess, ClientDataJsonParser as _, CollectedClientData, CredentialId, Flag, FromCbor, - LimitedVerificationParser, ParsedAuthData, Response, SentChallenge, }; use core::convert::Infallible; use ed25519_dalek::{Signature, Verifier as _}; @@ -26,8 +29,10 @@ use p256::ecdsa::DerSignature as P256DerSig; use p384::ecdsa::DerSignature as P384DerSig; use rsa::{ pkcs1v15, - sha2::{digest::Digest as _, Sha256}, + sha2::{Sha256, digest::Digest as _}, }; +#[cfg(feature = "serde_relaxed")] +use serde::Deserialize; /// Contains error types. pub mod error; /// Contains functionality to deserialize data from a client. @@ -273,7 +278,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 { +pub struct AuthenticatorAssertion<User> { /// [`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) @@ -282,9 +287,9 @@ pub struct AuthenticatorAssertion { /// [`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<Vec<u8>>>, + user_handle: Option<UserHandle<User>>, } -impl AuthenticatorAssertion { +impl<User> AuthenticatorAssertion<User> { /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson). #[inline] #[must_use] @@ -312,35 +317,99 @@ impl AuthenticatorAssertion { pub fn signature(&self) -> &[u8] { self.signature.as_slice() } + /// 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. + fn new_inner( + client_data_json: Vec<u8>, + mut authenticator_data: Vec<u8>, + signature: Vec<u8>, + user_handle: Option<UserHandle<User>>, + ) -> Self { + authenticator_data + .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice()); + Self { + client_data_json, + authenticator_data_and_c_data_hash: authenticator_data, + signature, + user_handle, + } + } +} +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::from) + self.user_handle.as_ref().map(UserHandle::as_slice) } +} +impl AuthenticatorAssertion<Vec<u8>> { /// 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( + pub fn new_user_vec( client_data_json: Vec<u8>, - mut authenticator_data: Vec<u8>, + authenticator_data: Vec<u8>, signature: Vec<u8>, user_handle: Option<UserHandle<Vec<u8>>>, ) -> Self { - authenticator_data - .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice()); - Self { + Self::new_inner(client_data_json, authenticator_data, signature, user_handle) + } + /// Same as [`Self::new_user_vec`] except `user_handle` is required. + #[inline] + #[must_use] + pub fn with_user_vec( + client_data_json: Vec<u8>, + authenticator_data: Vec<u8>, + signature: Vec<u8>, + user_handle: UserHandle<Vec<u8>>, + ) -> Self { + Self::new_inner( client_data_json, - authenticator_data_and_c_data_hash: authenticator_data, + authenticator_data, signature, - user_handle, - } + Some(user_handle), + ) } } -impl AuthResponse for AuthenticatorAssertion { +impl<const USER_LEN: usize> AuthenticatorAssertion<[u8; USER_LEN]> { + /// 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( + client_data_json: Vec<u8>, + authenticator_data: Vec<u8>, + signature: Vec<u8>, + user_handle: Option<UserHandle<[u8; USER_LEN]>>, + ) -> 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> { type Auth<'a> = AuthenticatorData<'a> where @@ -356,25 +425,31 @@ impl AuthResponse for AuthenticatorAssertion { > { /// Always `panic`s. #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")] + #[expect( + clippy::extra_unused_type_parameters, + reason = "same function signature as when serde_relaxed is enabled" + )] #[cfg(not(feature = "serde_relaxed"))] - fn get_client_collected_data(_: &[u8]) -> ! { - unreachable!("AuthenticatorAssertion::parse_data_and_verify_sig must be passed false when serde_relaxed is not enabled"); + fn get_client_collected_data<'b, U: 'b>(_: &'b [u8]) -> ! { + unreachable!( + "AuthenticatorAssertion::parse_data_and_verify_sig must be passed false when serde_relaxed is not enabled" + ); } /// Parses `data` using `CollectedClientData::from_client_data_json_relaxed::<false>`. #[cfg(feature = "serde_relaxed")] - fn get_client_collected_data( - data: &[u8], + fn get_client_collected_data<'b, U: 'b>( + data: &'b [u8], ) -> Result< - CollectedClientData<'_>, + CollectedClientData<'b>, AuthRespErr< - <<AuthenticatorAssertion as AuthResponse>::Auth<'_> as AuthDataContainer<'_>>::Err, + <<AuthenticatorAssertion<U> as AuthResponse>::Auth<'b> as AuthDataContainer<'b>>::Err, >, - > { + >{ CollectedClientData::from_client_data_json_relaxed::<false>(data) .map_err(AuthRespErr::CollectedClientDataRelaxed) } if relaxed { - get_client_collected_data(self.client_data_json.as_slice()) + get_client_collected_data::<'_, User>(self.client_data_json.as_slice()) } else { CollectedClientData::from_client_data_json::<false>(self.client_data_json.as_slice()) .map_err(AuthRespErr::CollectedClientData) @@ -448,15 +523,15 @@ impl AuthResponse for AuthenticatorAssertion { reason = "no invariants to uphold" )] #[derive(Debug)] -pub struct Authentication { +pub struct Authentication<User> { /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid). pub(crate) raw_id: CredentialId<Vec<u8>>, /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response) - pub(crate) response: AuthenticatorAssertion, + pub(crate) response: AuthenticatorAssertion<User>, /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment). pub(crate) authenticator_attachment: AuthenticatorAttachment, } -impl Authentication { +impl<User> Authentication<User> { /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid). #[inline] #[must_use] @@ -466,7 +541,7 @@ impl Authentication { /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response). #[inline] #[must_use] - pub const fn response(&self) -> &AuthenticatorAssertion { + pub const fn response(&self) -> &AuthenticatorAssertion<User> { &self.response } /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment). @@ -482,7 +557,7 @@ impl Authentication { #[must_use] pub const fn new( raw_id: CredentialId<Vec<u8>>, - response: AuthenticatorAssertion, + response: AuthenticatorAssertion<User>, authenticator_attachment: AuthenticatorAttachment, ) -> Self { Self { @@ -533,9 +608,37 @@ impl Authentication { self.response.client_data_json.as_slice(), ) } + /// Convenience function for [`AuthenticationRelaxed::deserialize`]. + /// + /// # Errors + /// + /// Errors iff [`AuthenticationRelaxed::deserialize`] does. + #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] + #[cfg(feature = "serde_relaxed")] + #[inline] + pub fn from_json_relaxed<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr> + where + UserHandle<User>: Deserialize<'a>, + { + serde_json::from_slice::<AuthenticationRelaxed<User>>(json).map(|val| val.0) + } + /// Convenience function for [`CustomAuthentication::deserialize`]. + /// + /// # Errors + /// + /// Errors iff [`CustomAuthentication::deserialize`] does. + #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] + #[cfg(feature = "serde_relaxed")] + #[inline] + pub fn from_json_custom<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr> + where + UserHandle<User>: Deserialize<'a>, + { + serde_json::from_slice::<CustomAuthentication<User>>(json).map(|val| val.0) + } } -impl Response for Authentication { - type Auth = AuthenticatorAssertion; +impl<User> Response for Authentication<User> { + type Auth = AuthenticatorAssertion<User>; fn auth(&self) -> &Self::Auth { &self.response } diff --git a/src/response/auth/ser.rs b/src/response/auth/ser.rs @@ -10,36 +10,95 @@ use super::{ ClientExtensions, }, }, + Authentication, AuthenticatorAssertion, UserHandle, error::UnknownCredentialOptions, - Authentication, AuthenticatorAssertion, }; #[cfg(doc)] -use super::{AuthenticatorAttachment, CredentialId, UserHandle}; +use super::{AuthenticatorAttachment, CredentialId}; use core::{ fmt::{self, Formatter}, marker::PhantomData, str, }; use data_encoding::BASE64URL_NOPAD; -use rsa::sha2::{digest::OutputSizeUser as _, Sha256}; +use rsa::sha2::{Sha256, digest::OutputSizeUser as _}; use serde::{ de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, Unexpected, Visitor}, ser::{Serialize, SerializeStruct as _, Serializer}, }; +/// Authenticator data. +pub(super) struct AuthData(pub Vec<u8>); +impl<'e> Deserialize<'e> for AuthData { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'e>, + { + /// `Visitor` for `AuthData`. + struct AuthDataVisitor; + impl Visitor<'_> for AuthDataVisitor { + type Value = AuthData; + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_str("AuthenticatorData") + } + #[expect( + clippy::panic_in_result_fn, + reason = "we want to crash when there is a bug" + )] + #[expect( + clippy::arithmetic_side_effects, + reason = "comment justifies its correctness" + )] + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: Error, + { + crate::base64url_nopad_decode_len(v.len()) + .ok_or_else(|| E::invalid_value(Unexpected::Str(v), &"base64url-encoded value")) + .and_then(|len| { + // The decoded length is 3/4 of the encoded length, so overflow could only occur + // if usize::MAX / 4 < 32 => usize::MAX < 128 < u8::MAX; thus overflow is not + // possible. + // We add 32 since the SHA-256 hash of `clientDataJSON` will be added to the + // raw authenticator data by `AuthenticatorDataAssertion::new`. + let mut auth_data = vec![0; len + Sha256::output_size()]; + auth_data.truncate(len); + BASE64URL_NOPAD + .decode_mut(v.as_bytes(), auth_data.as_mut_slice()) + .map_err(|e| E::custom(e.error)) + .map(|dec_len| { + assert_eq!( + len, dec_len, + "there is a bug in BASE64URL_NOPAD::decode_mut" + ); + AuthData(auth_data) + }) + }) + } + } + deserializer.deserialize_str(AuthDataVisitor) + } +} /// `Visitor` for `AuthenticatorAssertion`. /// /// Unknown fields are ignored and only `clientDataJSON`, `authenticatorData`, and `signature` are required iff /// `RELAXED`. -pub(super) struct AuthenticatorAssertionVisitor<const RELAXED: bool>; -impl<'d, const R: bool> Visitor<'d> for AuthenticatorAssertionVisitor<R> { - type Value = AuthenticatorAssertion; +pub(super) struct AuthenticatorAssertionVisitor<const RELAXED: bool, User>( + PhantomData<fn() -> User>, +); +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> +where + UserHandle<User>: Deserialize<'d>, +{ + type Value = AuthenticatorAssertion<User>; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { formatter.write_str("AuthenticatorAssertion") } - #[expect( - clippy::too_many_lines, - reason = "don't want to move code to an outer scope" - )] fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> where A: MapAccess<'d>, @@ -67,7 +126,10 @@ impl<'d, const R: bool> Visitor<'d> for AuthenticatorAssertionVisitor<R> { impl<const IG: bool> Visitor<'_> for FieldVisitor<IG> { type Value = Field<IG>; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - write!(formatter, "'{CLIENT_DATA_JSON}', '{AUTHENTICATOR_DATA}', '{SIGNATURE}', or '{USER_HANDLE}'") + write!( + formatter, + "'{CLIENT_DATA_JSON}', '{AUTHENTICATOR_DATA}', '{SIGNATURE}', or '{USER_HANDLE}'" + ) } fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> where @@ -91,60 +153,6 @@ impl<'d, const R: bool> Visitor<'d> for AuthenticatorAssertionVisitor<R> { deserializer.deserialize_identifier(FieldVisitor::<I>) } } - /// Authenticator data. - struct AuthData(Vec<u8>); - impl<'e> Deserialize<'e> for AuthData { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'e>, - { - /// `Visitor` for `AuthData`. - struct AuthDataVisitor; - impl Visitor<'_> for AuthDataVisitor { - type Value = AuthData; - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("AuthenticatorData") - } - #[expect( - clippy::panic_in_result_fn, - reason = "we want to crash when there is a bug" - )] - #[expect( - clippy::arithmetic_side_effects, - reason = "comment justifies its correctness" - )] - fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> - where - E: Error, - { - crate::base64url_nopad_decode_len(v.len()) - .ok_or_else(|| { - E::invalid_value(Unexpected::Str(v), &"base64url-encoded value") - }) - .and_then(|len| { - // The decoded length is 3/4 of the encoded length, so overflow could only occur - // if usize::MAX / 4 < 32 => usize::MAX < 128 < u8::MAX; thus overflow is not - // possible. - // We add 32 since the SHA-256 hash of `clientDataJSON` will be added to the - // raw authenticator data by `AuthenticatorDataAssertion::new`. - let mut auth_data = vec![0; len + Sha256::output_size()]; - auth_data.truncate(len); - BASE64URL_NOPAD - .decode_mut(v.as_bytes(), auth_data.as_mut_slice()) - .map_err(|e| E::custom(e.error)) - .map(|dec_len| { - assert_eq!( - len, dec_len, - "there is a bug in BASE64URL_NOPAD::decode_mut" - ); - AuthData(auth_data) - }) - }) - } - } - deserializer.deserialize_str(AuthDataVisitor) - } - } let mut client_data = None; let mut auth = None; let mut sig = None; @@ -193,7 +201,7 @@ impl<'d, const R: bool> Visitor<'d> for AuthenticatorAssertionVisitor<R> { .and_then(|authenticator_data| { sig.ok_or_else(|| Error::missing_field(SIGNATURE)) .map(|signature| { - AuthenticatorAssertion::new( + AuthenticatorAssertion::new_inner( client_data_json, authenticator_data, signature, @@ -215,7 +223,10 @@ 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> Deserialize<'de> for AuthenticatorAssertion { +impl<'de, User> Deserialize<'de> for AuthenticatorAssertion<User> +where + UserHandle<User>: Deserialize<'de>, +{ /// Deserializes a `struct` based on /// [`AuthenticatorAssertionResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorassertionresponsejson). /// @@ -236,7 +247,7 @@ impl<'de> Deserialize<'de> for AuthenticatorAssertion { deserializer.deserialize_struct( "AuthenticatorAssertion", AUTH_ASSERT_FIELDS, - AuthenticatorAssertionVisitor::<false>, + AuthenticatorAssertionVisitor::<false, User>::new(), ) } } @@ -350,7 +361,10 @@ impl<'de> Deserialize<'de> for ClientExtensionsOutputs { ) } } -impl<'de> Deserialize<'de> for Authentication { +impl<'de, User> Deserialize<'de> for Authentication<User> +where + UserHandle<User>: Deserialize<'de>, +{ /// Deserializes a `struct` based on /// [`AuthenticationResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationresponsejson). /// @@ -386,7 +400,7 @@ impl<'de> Deserialize<'de> for Authentication { where D: Deserializer<'de>, { - PublicKeyCredential::<false, false, AuthenticatorAssertion, ClientExtensionsOutputs>::deserialize( + PublicKeyCredential::<false, false, AuthenticatorAssertion<User>, ClientExtensionsOutputs>::deserialize( deserializer, ) .map(|cred| Self { @@ -437,7 +451,10 @@ impl Serialize for UnknownCredentialOptions<'_, '_> { } #[cfg(test)] mod tests { - use super::super::{Authentication, AuthenticatorAttachment}; + use super::super::{ + super::super::request::register::USER_HANDLE_MIN_LEN, Authentication, + AuthenticatorAttachment, + }; use data_encoding::BASE64URL_NOPAD; use rsa::sha2::{Digest as _, Sha256}; use serde::de::{Error as _, Unexpected}; @@ -492,32 +509,34 @@ mod tests { let b64_sig = BASE64URL_NOPAD.encode([].as_slice()); let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice()); // Base case is valid. - assert!(serde_json::from_str::<Authentication>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |auth| auth.response.client_data_json - == c_data_json.as_bytes() - && auth.response.authenticator_data_and_c_data_hash[..37] == auth_data - && auth.response.authenticator_data_and_c_data_hash[37..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && matches!( - auth.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform - ))); + assert!( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |auth| auth.response.client_data_json + == c_data_json.as_bytes() + && auth.response.authenticator_data_and_c_data_hash[..37] == auth_data + && auth.response.authenticator_data_and_c_data_hash[37..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && matches!( + auth.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + )) + ); // `id` and `rawId` mismatch. let mut err = Error::invalid_value( Unexpected::Bytes( @@ -531,7 +550,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "ABABABABABABABABABABAA", @@ -556,7 +575,7 @@ mod tests { // missing `id`. err = Error::missing_field("id").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { @@ -582,7 +601,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": null, "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -606,7 +625,7 @@ mod tests { // missing `rawId`. err = Error::missing_field("rawId").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "response": { @@ -631,7 +650,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": null, @@ -657,7 +676,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -682,7 +701,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -706,7 +725,7 @@ mod tests { // Missing `signature`. err = Error::missing_field("signature").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -731,7 +750,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -753,62 +772,68 @@ mod tests { err ); // Missing `userHandle`. - assert!(serde_json::from_str::<Authentication>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `userHandle`. - assert!(serde_json::from_str::<Authentication>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": null, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": null, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `authenticatorAttachment`. - assert!(serde_json::from_str::<Authentication>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": null, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |auth| matches!( - auth.authenticator_attachment, - AuthenticatorAttachment::None - ))); + assert!( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |auth| matches!( + auth.authenticator_attachment, + AuthenticatorAttachment::None + )) + ); // Unknown `authenticatorAttachment`. err = Error::invalid_value( Unexpected::Str("Platform"), @@ -817,7 +842,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -844,7 +869,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -869,7 +894,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -893,7 +918,7 @@ mod tests { // Missing `response`. err = Error::missing_field("response").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -913,7 +938,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -934,7 +959,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -955,7 +980,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -983,7 +1008,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1007,7 +1032,7 @@ mod tests { // Missing `type`. err = Error::missing_field("type").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1032,7 +1057,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1058,7 +1083,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1084,19 +1109,23 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>(serde_json::json!(null).to_string().as_str()) - .unwrap_err() - .to_string() - .into_bytes()[..err.len()], + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!(null).to_string().as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], err ); // Empty. err = Error::missing_field("response").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>(serde_json::json!({}).to_string().as_str()) - .unwrap_err() - .to_string() - .into_bytes()[..err.len()], + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({}).to_string().as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], err ); // Unknown field in `response`. @@ -1113,7 +1142,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1140,7 +1169,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -1180,7 +1209,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1205,7 +1234,7 @@ mod tests { // Duplicate field in `PublicKeyCredential`. err = Error::duplicate_field("id").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -1280,58 +1309,62 @@ mod tests { let b64_sig = BASE64URL_NOPAD.encode([].as_slice()); let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice()); // Base case is valid. - assert!(serde_json::from_str::<Authentication>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |auth| auth.response.client_data_json - == c_data_json.as_bytes() - && auth.response.authenticator_data_and_c_data_hash[..37] == auth_data - && auth.response.authenticator_data_and_c_data_hash[37..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && matches!( - auth.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform - ))); + assert!( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |auth| auth.response.client_data_json + == c_data_json.as_bytes() + && auth.response.authenticator_data_and_c_data_hash[..37] == auth_data + && auth.response.authenticator_data_and_c_data_hash[37..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && matches!( + auth.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + )) + ); // `null` `prf`. - assert!(serde_json::from_str::<Authentication>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": null - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": null + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Unknown `clientExtensionResults`. let mut err = Error::unknown_field("Prf", ["prf"].as_slice()) .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1357,7 +1390,7 @@ mod tests { // Duplicate field. err = Error::duplicate_field("prf").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -1383,31 +1416,33 @@ mod tests { err ); // `null` `results`. - assert!(serde_json::from_str::<Authentication>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": null, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": null, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Duplicate field in `prf`. err = Error::duplicate_field("results").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -1437,7 +1472,7 @@ mod tests { // Missing `first`. err = Error::missing_field("first").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1463,60 +1498,64 @@ mod tests { err ); // `null` `first`. - assert!(serde_json::from_str::<Authentication>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": { - "first": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": { + "first": null + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `second`. - assert!(serde_json::from_str::<Authentication>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": { - "first": null, - "second": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": { + "first": null, + "second": null + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Non-`null` `first`. err = Error::invalid_type(Unexpected::Option, &"null") .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1548,7 +1587,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1581,7 +1620,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1612,7 +1651,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1643,7 +1682,7 @@ mod tests { // Duplicate field in `results`. err = Error::duplicate_field("first").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<Authentication>( + serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", diff --git a/src/response/auth/ser_relaxed.rs b/src/response/auth/ser_relaxed.rs @@ -1,20 +1,30 @@ +#![expect( + clippy::question_mark_used, + reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs" +)] #[cfg(doc)] -use super::super::Challenge; +use super::super::{Challenge, CredentialId}; use super::{ super::{ auth::ser::{ - AuthenticatorAssertionVisitor, ClientExtensionsOutputs, ClientExtensionsOutputsVisitor, - AUTH_ASSERT_FIELDS, EXT_FIELDS, + AUTH_ASSERT_FIELDS, AuthData, AuthenticatorAssertionVisitor, ClientExtensionsOutputs, + ClientExtensionsOutputsVisitor, EXT_FIELDS, + }, + ser::{ + AuthenticationExtensionsPrfOutputsHelper, Base64DecodedVal, ClientExtensions, + PublicKeyCredential, Type, }, - ser::{AuthenticationExtensionsPrfOutputsHelper, ClientExtensions, PublicKeyCredential}, ser_relaxed::AuthenticationExtensionsPrfValuesRelaxed, }, - Authentication, AuthenticatorAssertion, + Authentication, AuthenticatorAssertion, AuthenticatorAttachment, UserHandle, +}; +use core::{ + fmt::{self, Formatter}, + marker::PhantomData, }; -use core::marker::PhantomData; #[cfg(doc)] use data_encoding::BASE64URL_NOPAD; -use serde::de::{Deserialize, Deserializer}; +use serde::de::{Deserialize, Deserializer, Error, MapAccess, Visitor}; /// `newtype` around `ClientExtensionsOutputs` with a "relaxed" [`Self::deserialize`] implementation. struct ClientExtensionsOutputsRelaxed(pub ClientExtensionsOutputs); impl ClientExtensions for ClientExtensionsOutputsRelaxed { @@ -49,8 +59,11 @@ impl<'de> Deserialize<'de> for ClientExtensionsOutputsRelaxed { } /// `newtype` around `AuthenticatorAssertion` with a "relaxed" [`Self::deserialize`] implementation. #[derive(Debug)] -pub struct AuthenticatorAssertionRelaxed(pub AuthenticatorAssertion); -impl<'de> Deserialize<'de> for AuthenticatorAssertionRelaxed { +pub struct AuthenticatorAssertionRelaxed<User>(pub AuthenticatorAssertion<User>); +impl<'de, User> Deserialize<'de> for AuthenticatorAssertionRelaxed<User> +where + UserHandle<User>: Deserialize<'de>, +{ /// Same as [`AuthenticatorAssertion::deserialize`] except unknown keys are ignored. /// /// Note that duplicate keys are still forbidden. @@ -63,15 +76,18 @@ impl<'de> Deserialize<'de> for AuthenticatorAssertionRelaxed { .deserialize_struct( "AuthenticatorAssertionRelaxed", AUTH_ASSERT_FIELDS, - AuthenticatorAssertionVisitor::<true>, + AuthenticatorAssertionVisitor::<true, User>::new(), ) .map(Self) } } /// `newtype` around `Authentication` with a "relaxed" [`Self::deserialize`] implementation. #[derive(Debug)] -pub struct AuthenticationRelaxed(pub Authentication); -impl<'de> Deserialize<'de> for AuthenticationRelaxed { +pub struct AuthenticationRelaxed<User>(pub Authentication<User>); +impl<'de, User> Deserialize<'de> for AuthenticationRelaxed<User> +where + UserHandle<User>: Deserialize<'de>, +{ /// Same as [`Authentication::deserialize`] except unknown keys are ignored; /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-response) is deserialized /// via [`AuthenticatorAssertionRelaxed::deserialize`]; @@ -103,7 +119,7 @@ impl<'de> Deserialize<'de> for AuthenticationRelaxed { PublicKeyCredential::< true, false, - AuthenticatorAssertionRelaxed, + AuthenticatorAssertionRelaxed<User>, ClientExtensionsOutputsRelaxed, >::deserialize(deserializer) .map(|cred| { @@ -117,9 +133,289 @@ impl<'de> Deserialize<'de> for AuthenticationRelaxed { }) } } +/// `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> +where + UserHandle<User>: Deserialize<'de>, +{ + /// 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 + /// 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)). + /// + /// While [`Authentication::deserialize`] _strictly_ adheres to the JSON definition, this implementation + /// strictly disallows superfluous data. Specifically the following JSON is required to be sent where duplicate + /// and unknown keys are disallowed: + /// + /// ```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> + /// } + /// ``` + /// + /// All of the above keys are required with the exceptions of `"authenticatorAttachment"`, `"type"`, and + /// `"userHandle"`. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::{request::register::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]>>( + /// r#"{ + /// "authenticatorData": "AA", + /// "authenticatorAttachment": "cross-platform", + /// "clientExtensionResults": {}, + /// "clientDataJSON": "AA", + /// "id": "AAAAAAAAAAAAAAAAAAAAAA", + /// "signature": "AA", + /// "type": "public-key", + /// "userHandle": "AA" + /// }"# + /// ).is_ok()); + /// ``` + #[expect( + clippy::too_many_lines, + reason = "want to hide; thus don't put in outer scope" + )] + #[inline] + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + /// `Visitor` for `CustomAuthentication`. + struct CustomAuthenticationVisitor<U>(PhantomData<fn() -> U>); + impl<'d, U> Visitor<'d> for CustomAuthenticationVisitor<U> + where + UserHandle<U>: Deserialize<'d>, + { + type Value = CustomAuthentication<U>; + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_str("CustomAuthentication") + } + #[expect( + clippy::too_many_lines, + reason = "want to hide; thus don't put in outer scope" + )] + fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> + where + A: MapAccess<'d>, + { + /// Fields in the JSON. + enum Field { + /// `authenticatorAttachment` key. + AuthenticatorAttachment, + /// `authenticatorData` key. + AuthenticatorData, + /// `clientDataJSON` key. + ClientDataJson, + /// `clientExtensionResults` key. + ClientExtensionResults, + /// `id` key. + Id, + /// `signature` key. + Signature, + /// `type` key. + Type, + /// `userHandle` key. + UserHandle, + } + impl<'e> Deserialize<'e> for Field { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'e>, + { + /// `Visitor` for `Field`. + struct FieldVisitor; + impl Visitor<'_> for FieldVisitor { + type Value = Field; + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + write!( + formatter, + "'{AUTHENTICATOR_ATTACHMENT}', '{AUTHENTICATOR_DATA}', '{CLIENT_DATA_JSON}', '{CLIENT_EXTENSION_RESULTS}', '{ID}', '{SIGNATURE}', '{TYPE}', or '{USER_HANDLE}'" + ) + } + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: Error, + { + match v { + AUTHENTICATOR_ATTACHMENT => Ok(Field::AuthenticatorAttachment), + AUTHENTICATOR_DATA => Ok(Field::AuthenticatorData), + CLIENT_DATA_JSON => Ok(Field::ClientDataJson), + CLIENT_EXTENSION_RESULTS => Ok(Field::ClientExtensionResults), + ID => Ok(Field::Id), + SIGNATURE => Ok(Field::Signature), + TYPE => Ok(Field::Type), + USER_HANDLE => Ok(Field::UserHandle), + _ => Err(E::unknown_field(v, FIELDS)), + } + } + } + deserializer.deserialize_identifier(FieldVisitor) + } + } + let mut authenticator_attachment = None; + let mut authenticator_data = None; + let mut client_data_json = None; + let mut ext = false; + let mut id = None; + let mut signature = None; + let mut typ = false; + let mut user_handle = None; + while let Some(key) = map.next_key()? { + match key { + Field::AuthenticatorAttachment => { + if authenticator_attachment.is_some() { + return Err(Error::duplicate_field(AUTHENTICATOR_ATTACHMENT)); + } + authenticator_attachment = map.next_value::<Option<_>>().map(Some)?; + } + Field::AuthenticatorData => { + if authenticator_data.is_some() { + return Err(Error::duplicate_field(AUTHENTICATOR_DATA)); + } + authenticator_data = + map.next_value::<AuthData>().map(|val| Some(val.0))?; + } + Field::ClientDataJson => { + if client_data_json.is_some() { + return Err(Error::duplicate_field(CLIENT_DATA_JSON)); + } + client_data_json = map + .next_value::<Base64DecodedVal>() + .map(|val| Some(val.0))?; + } + Field::ClientExtensionResults => { + if ext { + return Err(Error::duplicate_field(CLIENT_EXTENSION_RESULTS)); + } + ext = map.next_value::<ClientExtensionsOutputs>().map(|_| true)?; + } + Field::Id => { + if id.is_some() { + return Err(Error::duplicate_field(ID)); + } + id = map.next_value().map(Some)?; + } + Field::Signature => { + if signature.is_some() { + return Err(Error::duplicate_field(SIGNATURE)); + } + signature = map + .next_value::<Base64DecodedVal>() + .map(|val| Some(val.0))?; + } + Field::Type => { + if typ { + return Err(Error::duplicate_field(TYPE)); + } + typ = map.next_value::<Option<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)?; + } + } + } + authenticator_data + .ok_or_else(|| Error::missing_field(AUTHENTICATOR_DATA)) + .and_then(|auth_data| { + client_data_json + .ok_or_else(|| Error::missing_field(CLIENT_DATA_JSON)) + .and_then(|c_data| { + id.ok_or_else(|| Error::missing_field(ID)) + .and_then(|raw_id| { + signature + .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, + ) + }, + ), + raw_id, + })) + } else { + Err(Error::missing_field( + CLIENT_EXTENSION_RESULTS, + )) + } + }) + }) + }) + }) + } + } + /// `authenticatorAttachment` key. + const AUTHENTICATOR_ATTACHMENT: &str = "authenticatorAttachment"; + /// `authenticatorData` key. + const AUTHENTICATOR_DATA: &str = "authenticatorData"; + /// `clientDataJSON` key. + const CLIENT_DATA_JSON: &str = "clientDataJSON"; + /// `clientExtensionResults` key. + const CLIENT_EXTENSION_RESULTS: &str = "clientExtensionResults"; + /// `id` key. + const ID: &str = "id"; + /// `signature` key. + const SIGNATURE: &str = "signature"; + /// `type` key. + const TYPE: &str = "type"; + /// `userHandle` key. + const USER_HANDLE: &str = "userHandle"; + /// Fields. + const FIELDS: &[&str; 8] = &[ + AUTHENTICATOR_ATTACHMENT, + AUTHENTICATOR_DATA, + CLIENT_DATA_JSON, + CLIENT_EXTENSION_RESULTS, + ID, + SIGNATURE, + TYPE, + USER_HANDLE, + ]; + deserializer.deserialize_struct( + "CustomAuthentication", + FIELDS, + CustomAuthenticationVisitor(PhantomData), + ) + } +} #[cfg(test)] mod tests { - use super::{super::AuthenticatorAttachment, AuthenticationRelaxed}; + use super::{ + super::{super::super::request::register::USER_HANDLE_MIN_LEN, AuthenticatorAttachment}, + AuthenticationRelaxed, CustomAuthentication, + }; use data_encoding::BASE64URL_NOPAD; use rsa::sha2::{Digest as _, Sha256}; use serde::de::{Error as _, Unexpected}; @@ -174,32 +470,34 @@ mod tests { let b64_sig = BASE64URL_NOPAD.encode([].as_slice()); let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice()); // Base case is valid. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |auth| auth.0.response.client_data_json - == c_data_json.as_bytes() - && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data - && auth.0.response.authenticator_data_and_c_data_hash[37..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && matches!( - auth.0.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform - ))); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |auth| auth.0.response.client_data_json + == c_data_json.as_bytes() + && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data + && auth.0.response.authenticator_data_and_c_data_hash[37..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && matches!( + auth.0.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + )) + ); // `id` and `rawId` mismatch. let mut err = Error::invalid_value( Unexpected::Bytes( @@ -213,7 +511,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "ABABABABABABABABABABAA", @@ -238,7 +536,7 @@ mod tests { // missing `id`. err = Error::missing_field("id").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { @@ -264,7 +562,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": null, "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -286,46 +584,50 @@ mod tests { err ); // missing `rawId`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `rawId`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": null, - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": null, + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Missing `authenticatorData`. err = Error::missing_field("authenticatorData") .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -350,7 +652,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -374,7 +676,7 @@ mod tests { // Missing `signature`. err = Error::missing_field("signature").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -399,7 +701,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -421,62 +723,68 @@ mod tests { err ); // Missing `userHandle`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `userHandle`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": null, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": null, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `authenticatorAttachment`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": null, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |auth| matches!( - auth.0.authenticator_attachment, - AuthenticatorAttachment::None - ))); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |auth| matches!( + auth.0.authenticator_attachment, + AuthenticatorAttachment::None + )) + ); // Unknown `authenticatorAttachment`. err = Error::invalid_value( Unexpected::Str("Platform"), @@ -485,7 +793,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -512,7 +820,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -537,7 +845,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -561,7 +869,7 @@ mod tests { // Missing `response`. err = Error::missing_field("response").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -581,7 +889,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -602,7 +910,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -619,81 +927,89 @@ mod tests { err ); // Missing `clientExtensionResults`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `clientExtensionResults`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": null, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": null, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Missing `type`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `type`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": null - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": null + }) + .to_string() + .as_str() + ) + .is_ok() + ); // 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>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -719,7 +1035,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!(null).to_string().as_str() ) .unwrap_err() @@ -730,7 +1046,7 @@ mod tests { // Empty. err = Error::missing_field("response").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({}).to_string().as_str() ) .unwrap_err() @@ -739,30 +1055,32 @@ mod tests { err ); // Unknown field in `response`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "foo": true, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "foo": true, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Duplicate field in `response`. err = Error::duplicate_field("userHandle") .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -787,39 +1105,513 @@ mod tests { err ); // Unknown field in `PublicKeyCredential`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key", + "foo": true, + }) + .to_string() + .as_str() + ) + .is_ok() + ); + // 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]>>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata}\", + \"authenticatorData\": \"{b64_adata}\", + \"signature\": \"{b64_sig}\", + \"userHandle\": \"{b64_user}\" + }}, + \"clientExtensionResults\": {{}}, + \"type\": \"public-key\" + + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Base case is valid. + assert!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", "clientDataJSON": b64_cdata, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key", - "foo": true, - }) + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |auth| auth.0.response.client_data_json + == c_data_json.as_bytes() + && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data + && auth.0.response.authenticator_data_and_c_data_hash[37..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && matches!( + auth.0.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + )) + ); + // 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::json!({ + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // `null` `id`. + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": null, + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .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::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // `null` `authenticatorData`. + err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorData") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata, + "authenticatorData": null, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // 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::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // `null` `signature`. + err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": null, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Missing `userHandle`. + assert!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); + // `null` `userHandle`. + assert!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); + // `null` `authenticatorAttachment`. + assert!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "authenticatorAttachment": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |auth| matches!( + auth.0.authenticator_attachment, + AuthenticatorAttachment::None + )) + ); + // Unknown `authenticatorAttachment`. + err = Error::invalid_value( + Unexpected::Str("Platform"), + &"'platform' or 'cross-platform'", + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "authenticatorAttachment": "Platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Missing `clientDataJSON`. + err = Error::missing_field("clientDataJSON") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // `null` `clientDataJSON`. + err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": null, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Empty. + err = Error::missing_field("authenticatorData") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({}).to_string().as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Missing `clientExtensionResults`. + err = Error::missing_field("clientExtensionResults") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // `null` `clientExtensionResults`. + err = Error::invalid_type(Unexpected::Other("null"), &"ClientExtensionsOutputs") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": null, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + assert!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + }) + .to_string() + .as_str() + ) + .is_ok() + ); + assert!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": null + }) + .to_string() + .as_str() + ) + .is_ok() + ); + // 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::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "Public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // `null`. + err = Error::invalid_type(Unexpected::Other("null"), &"CustomAuthentication") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!(null).to_string().as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Unknown field. + err = Error::unknown_field( + "foo", + [ + "authenticatorAttachment", + "authenticatorData", + "clientDataJSON", + "clientExtensionResults", + "id", + "signature", + "type", + "userHandle", + ] + .as_slice(), + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "foo": true, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Duplicate field. + err = Error::duplicate_field("userHandle") .to_string() - .as_str() - ) - .is_ok()); - // Duplicate field in `PublicKeyCredential`. - err = Error::duplicate_field("id").to_string().into_bytes(); + .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", - \"authenticatorData\": \"{b64_adata}\", - \"signature\": \"{b64_sig}\", - \"userHandle\": \"{b64_user}\" - }}, + \"clientDataJSON\": \"{b64_cdata}\", + \"authenticatorData\": \"{b64_adata}\", + \"signature\": \"{b64_sig}\", + \"userHandle\": \"{b64_user}\", + \"userHandle\": \"{b64_user}\" \"clientExtensionResults\": {{}}, \"type\": \"public-key\" @@ -883,76 +1675,82 @@ mod tests { let b64_sig = BASE64URL_NOPAD.encode([].as_slice()); let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice()); // Base case is valid. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |auth| auth.0.response.client_data_json - == c_data_json.as_bytes() - && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data - && auth.0.response.authenticator_data_and_c_data_hash[37..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && matches!( - auth.0.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform - ))); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |auth| auth.0.response.client_data_json + == c_data_json.as_bytes() + && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data + && auth.0.response.authenticator_data_and_c_data_hash[37..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && matches!( + auth.0.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + )) + ); // `null` `prf`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": null - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": null + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Unknown `clientExtensionResults`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "Prf": null - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "Prf": null + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Duplicate field. let mut err = Error::duplicate_field("prf").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -978,31 +1776,33 @@ mod tests { err ); // `null` `results`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": null, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": null, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Duplicate field in `prf`. err = Error::duplicate_field("results").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", @@ -1030,82 +1830,88 @@ mod tests { err ); // Missing `first`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": {}, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": {}, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `first`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": { - "first": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": { + "first": null + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `second`. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": { - "first": null, - "second": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": { + "first": null, + "second": null + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Non-`null` `first`. err = Error::invalid_type(Unexpected::Option, &"null") .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1137,7 +1943,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1170,7 +1976,7 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", @@ -1197,57 +2003,61 @@ mod tests { err ); // Unknown `prf` field. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "foo": true, - "results": null - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "foo": true, + "results": null + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Unknown `results` field. - assert!(serde_json::from_str::<AuthenticationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": { - "first": null, - "Second": null + assert!( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": { + "first": null, + "Second": null + } } - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Duplicate field in `results`. err = Error::duplicate_field("first").to_string().into_bytes(); assert_eq!( - serde_json::from_str::<AuthenticationRelaxed>( + serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>( format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", diff --git a/src/response/bin.rs b/src/response/bin.rs @@ -102,6 +102,17 @@ impl Decode for CredentialId<Vec<u8>> { } } } +impl<'b> Decode for CredentialId<&'b [u8]> { + type Input<'a> = &'b [u8]; + type Err = CredentialIdErr; + #[inline] + fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> { + match CredentialId::from_slice(input) { + Ok(_) => Ok(Self(input)), + Err(e) => Err(e), + } + } +} impl EncodeBuffer for CredentialId<&[u8]> { #[expect(clippy::unreachable, reason = "when there is a bug, we want to crash")] fn encode_into_buffer(&self, buffer: &mut Vec<u8>) { diff --git a/src/response/custom.rs b/src/response/custom.rs @@ -1,4 +1,5 @@ -use super::{CredentialId, CredentialIdErr}; +use super::{AuthTransports, AuthenticatorTransport, CredentialId, CredentialIdErr}; +use core::iter::FusedIterator; impl<'a: 'b, 'b> TryFrom<&'a [u8]> for CredentialId<&'b [u8]> { type Error = CredentialIdErr; #[inline] @@ -16,3 +17,158 @@ impl TryFrom<Vec<u8>> for CredentialId<Vec<u8>> { } } } +/// [`Iterator`] of [`AuthenticatorTransport`]s returned from +/// [`AuthTransports::into_iter`]. +#[derive(Debug)] +pub struct AuthTransportIter(AuthTransports); +impl Iterator for AuthTransportIter { + type Item = AuthenticatorTransport; + #[inline] + fn next(&mut self) -> Option<Self::Item> { + let mut nxt = self.0.remove(AuthenticatorTransport::Ble); + if self.0.0 != nxt.0 { + self.0 = nxt; + return Some(AuthenticatorTransport::Ble); + } + nxt = self.0.remove(AuthenticatorTransport::Hybrid); + if self.0.0 != nxt.0 { + self.0 = nxt; + return Some(AuthenticatorTransport::Hybrid); + } + nxt = self.0.remove(AuthenticatorTransport::Internal); + if self.0.0 != nxt.0 { + self.0 = nxt; + return Some(AuthenticatorTransport::Internal); + } + nxt = self.0.remove(AuthenticatorTransport::Nfc); + if self.0.0 != nxt.0 { + self.0 = nxt; + return Some(AuthenticatorTransport::Nfc); + } + nxt = self.0.remove(AuthenticatorTransport::SmartCard); + if self.0.0 != nxt.0 { + self.0 = nxt; + return Some(AuthenticatorTransport::SmartCard); + } + nxt = self.0.remove(AuthenticatorTransport::Usb); + if self.0.0 == nxt.0 { + None + } else { + self.0 = nxt; + Some(AuthenticatorTransport::Usb) + } + } + #[inline] + fn size_hint(&self) -> (usize, Option<usize>) { + let count = self.len(); + (count, Some(count)) + } + #[inline] + fn count(self) -> usize + where + Self: Sized, + { + self.len() + } + #[inline] + fn last(mut self) -> Option<Self::Item> + where + Self: Sized, + { + self.next_back() + } +} +impl ExactSizeIterator for AuthTransportIter { + #[expect(clippy::as_conversions, reason = "comment justifies correctness")] + #[inline] + fn len(&self) -> usize { + // Maximum count is 6, so this is fine. + self.0.count() as usize + } +} +impl DoubleEndedIterator for AuthTransportIter { + #[inline] + fn next_back(&mut self) -> Option<Self::Item> { + let mut nxt = self.0.remove(AuthenticatorTransport::Usb); + if self.0.0 != nxt.0 { + self.0 = nxt; + return Some(AuthenticatorTransport::Usb); + } + nxt = self.0.remove(AuthenticatorTransport::SmartCard); + if self.0.0 != nxt.0 { + self.0 = nxt; + return Some(AuthenticatorTransport::SmartCard); + } + nxt = self.0.remove(AuthenticatorTransport::Nfc); + if self.0.0 != nxt.0 { + self.0 = nxt; + return Some(AuthenticatorTransport::Nfc); + } + nxt = self.0.remove(AuthenticatorTransport::Internal); + if self.0.0 != nxt.0 { + self.0 = nxt; + return Some(AuthenticatorTransport::Internal); + } + nxt = self.0.remove(AuthenticatorTransport::Hybrid); + if self.0.0 != nxt.0 { + self.0 = nxt; + return Some(AuthenticatorTransport::Hybrid); + } + nxt = self.0.remove(AuthenticatorTransport::Ble); + if self.0.0 == nxt.0 { + None + } else { + self.0 = nxt; + Some(AuthenticatorTransport::Ble) + } + } +} +impl FusedIterator for AuthTransportIter {} +impl IntoIterator for AuthTransports { + type Item = AuthenticatorTransport; + type IntoIter = AuthTransportIter; + #[inline] + fn into_iter(self) -> Self::IntoIter { + AuthTransportIter(self) + } +} +#[cfg(test)] +mod tests { + use super::{AuthTransports, AuthenticatorTransport}; + #[test] + fn iter_all() { + let mut iter = AuthTransports::ALL.into_iter(); + assert_eq!(iter.len(), 6); + assert!( + iter.next() + .map_or(false, |tran| matches!(tran, AuthenticatorTransport::Ble)) + ); + assert_eq!(iter.len(), 5); + assert!( + iter.next() + .map_or(false, |tran| matches!(tran, AuthenticatorTransport::Hybrid)) + ); + assert_eq!(iter.len(), 4); + assert!( + iter.next_back() + .map_or(false, |tran| matches!(tran, AuthenticatorTransport::Usb)) + ); + assert_eq!(iter.len(), 3); + assert!(iter.next().map_or(false, |tran| matches!( + tran, + AuthenticatorTransport::Internal + ))); + assert_eq!(iter.len(), 2); + assert!(iter.next_back().map_or(false, |tran| matches!( + tran, + AuthenticatorTransport::SmartCard + ))); + assert_eq!(iter.len(), 1); + assert!( + iter.next() + .map_or(false, |tran| matches!(tran, AuthenticatorTransport::Nfc)) + ); + assert_eq!(iter.len(), 0); + assert!(iter.next().is_none()); + } +} diff --git a/src/response/register.rs b/src/response/register.rs @@ -1,7 +1,10 @@ +#[cfg(feature = "serde_relaxed")] +use self::{ + super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr}, + ser_relaxed::{CustomRegistration, RegistrationRelaxed}, +}; #[cfg(all(doc, feature = "serde_relaxed"))] use super::super::request::FixedCapHashSet; -#[cfg(feature = "serde_relaxed")] -use super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr}; #[cfg(all(doc, feature = "bin"))] use super::{ super::bin::{Decode, Encode}, @@ -9,7 +12,10 @@ use super::{ }; use super::{ super::request::register::ResidentKeyRequirement, - cbor, + AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthTransports, + AuthenticatorAttachment, Backup, CborSuccess, ClientDataJsonParser as _, CollectedClientData, + CredentialId, Flag, FromCbor, LimitedVerificationParser, ParsedAuthData, Response, + SentChallenge, cbor, error::CollectedClientDataErr, register::error::{ AaguidErr, AttestationErr, AttestationObjectErr, AttestedCredentialDataErr, @@ -17,20 +23,16 @@ use super::{ CompressedP384PubKeyErr, CoseKeyErr, Ed25519PubKeyErr, Ed25519SignatureErr, PubKeyErr, RsaPubKeyErr, UncompressedP256PubKeyErr, UncompressedP384PubKeyErr, }, - AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthTransports, - AuthenticatorAttachment, Backup, CborSuccess, ClientDataJsonParser as _, CollectedClientData, - CredentialId, Flag, FromCbor, LimitedVerificationParser, ParsedAuthData, Response, - SentChallenge, }; #[cfg(doc)] use super::{ super::{ + AuthenticatedCredential, RegisteredCredential, request::{ + BackupReq, Challenge, auth::{AuthenticationVerificationOptions, PublicKeyCredentialRequestOptions}, register::{Extension, RegistrationServerState}, - BackupReq, Challenge, }, - AuthenticatedCredential, RegisteredCredential, }, AuthenticatorTransport, }; @@ -41,19 +43,21 @@ use core::{ }; use ed25519_dalek::{Signature, Verifier as _, VerifyingKey}; use p256::{ - ecdsa::{DerSignature as P256Sig, VerifyingKey as P256VerKey}, - elliptic_curve::{generic_array::typenum::ToInt as _, point::DecompressPoint as _, Curve}, AffinePoint as P256Affine, EncodedPoint as P256Pt, NistP256, + ecdsa::{DerSignature as P256Sig, VerifyingKey as P256VerKey}, + elliptic_curve::{Curve, generic_array::typenum::ToInt as _, point::DecompressPoint as _}, }; use p384::{ - ecdsa::{DerSignature as P384Sig, VerifyingKey as P384VerKey}, AffinePoint as P384Affine, EncodedPoint as P384Pt, NistP384, + ecdsa::{DerSignature as P384Sig, VerifyingKey as P384VerKey}, }; use rsa::{ - pkcs1v15::{self, VerifyingKey as RsaVerKey}, - sha2::{digest::Digest as _, Sha256}, BigUint, RsaPublicKey, + pkcs1v15::{self, VerifyingKey as RsaVerKey}, + sha2::{Sha256, digest::Digest as _}, }; +#[cfg(all(doc, feature = "serde_relaxed"))] +use serde::Deserialize; /// Contains functionality to (de)serialize data to a data store. #[cfg_attr(docsrs, doc(cfg(feature = "bin")))] #[cfg(feature = "bin")] @@ -635,6 +639,14 @@ impl<'a> UncompressedP256PubKey<'a> { )) .map_err(|_e| PubKeyErr::P256) } + /// Returns `true` iff [`Self::y`] is odd. + #[expect(clippy::indexing_slicing, reason = "comment justifies correctness")] + #[inline] + #[must_use] + pub const fn y_is_odd(self) -> bool { + // `self.1.len() == 32`, so this won't `panic`. + self.1[31] & 1 == 1 + } } impl<'a: 'b, 'b> TryFrom<(&'a [u8], &'a [u8])> for UncompressedP256PubKey<'b> { type Error = UncompressedP256PubKeyErr; @@ -874,6 +886,14 @@ impl<'a> UncompressedP384PubKey<'a> { )) .map_err(|_e| PubKeyErr::P384) } + /// Returns `true` iff [`Self::y`] is odd. + #[expect(clippy::indexing_slicing, reason = "comment justifies correctness")] + #[inline] + #[must_use] + pub const fn y_is_odd(self) -> bool { + // `self.1.len() == 48`, so this won't `panic`. + self.1[47] & 1 == 1 + } } impl<'a: 'b, 'b> TryFrom<(&'a [u8], &'a [u8])> for UncompressedP384PubKey<'b> { type Error = UncompressedP384PubKeyErr; @@ -939,6 +959,7 @@ impl<T> CompressedP384PubKey<T> { } /// `true` iff the y-coordinate is odd. #[inline] + #[must_use] pub const fn y_is_odd(&self) -> bool { self.y_is_odd } @@ -1691,15 +1712,15 @@ impl<'a: 'b, 'b, T: AsRef<[u8]>, T2: AsRef<[u8]>, T3: AsRef<[u8]>, T4: AsRef<[u8 } } impl< - T: PartialEq<T5>, - T5: PartialEq<T>, - T2: PartialEq<T6>, - T6: PartialEq<T2>, - T3: PartialEq<T7>, - T7: PartialEq<T3>, - T4: PartialEq<T8>, - T8: PartialEq<T4>, - > PartialEq<CompressedPubKey<T, T2, T3, T4>> for CompressedPubKey<T5, T6, T7, T8> + T: PartialEq<T5>, + T5: PartialEq<T>, + T2: PartialEq<T6>, + T6: PartialEq<T2>, + T3: PartialEq<T7>, + T7: PartialEq<T3>, + T4: PartialEq<T8>, + T8: PartialEq<T4>, +> PartialEq<CompressedPubKey<T, T2, T3, T4>> for CompressedPubKey<T5, T6, T7, T8> { #[inline] fn eq(&self, other: &CompressedPubKey<T, T2, T3, T4>) -> bool { @@ -1718,15 +1739,15 @@ impl< } } impl< - T: PartialEq<T5>, - T5: PartialEq<T>, - T2: PartialEq<T6>, - T6: PartialEq<T2>, - T3: PartialEq<T7>, - T7: PartialEq<T3>, - T4: PartialEq<T8>, - T8: PartialEq<T4>, - > PartialEq<CompressedPubKey<T, T2, T3, T4>> for &CompressedPubKey<T5, T6, T7, T8> + T: PartialEq<T5>, + T5: PartialEq<T>, + T2: PartialEq<T6>, + T6: PartialEq<T2>, + T3: PartialEq<T7>, + T7: PartialEq<T3>, + T4: PartialEq<T8>, + T8: PartialEq<T4>, +> PartialEq<CompressedPubKey<T, T2, T3, T4>> for &CompressedPubKey<T5, T6, T7, T8> { #[inline] fn eq(&self, other: &CompressedPubKey<T, T2, T3, T4>) -> bool { @@ -1734,15 +1755,15 @@ impl< } } impl< - T: PartialEq<T5>, - T5: PartialEq<T>, - T2: PartialEq<T6>, - T6: PartialEq<T2>, - T3: PartialEq<T7>, - T7: PartialEq<T3>, - T4: PartialEq<T8>, - T8: PartialEq<T4>, - > PartialEq<&CompressedPubKey<T, T2, T3, T4>> for CompressedPubKey<T5, T6, T7, T8> + T: PartialEq<T5>, + T5: PartialEq<T>, + T2: PartialEq<T6>, + T6: PartialEq<T2>, + T3: PartialEq<T7>, + T7: PartialEq<T3>, + T4: PartialEq<T8>, + T8: PartialEq<T4>, +> PartialEq<&CompressedPubKey<T, T2, T3, T4>> for CompressedPubKey<T5, T6, T7, T8> { #[inline] fn eq(&self, other: &&CompressedPubKey<T, T2, T3, T4>) -> bool { @@ -2539,7 +2560,7 @@ impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AttestationObject<'b> { /// [`AuthenticatorAttestationResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorattestationresponse). #[derive(Debug)] pub struct AuthenticatorAttestation { - /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-clientdatajson). + /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson). client_data_json: Vec<u8>, /// [attestation object](https://www.w3.org/TR/webauthn-3/#attestation-object) followed by the SHA-256 hash /// of [`Self::client_data_json`]. @@ -2548,7 +2569,7 @@ pub struct AuthenticatorAttestation { transports: AuthTransports, } impl AuthenticatorAttestation { - /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-clientdatajson). + /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson). #[inline] #[must_use] pub fn client_data_json(&self) -> &[u8] { @@ -2786,6 +2807,28 @@ impl Registration { self.response.client_data_json.as_slice(), ) } + /// Convenience function for [`RegistrationRelaxed::deserialize`]. + /// + /// # Errors + /// + /// Errors iff [`RegistrationRelaxed::deserialize`] does. + #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] + #[cfg(feature = "serde_relaxed")] + #[inline] + pub fn from_json_relaxed(json: &[u8]) -> Result<Self, SerdeJsonErr> { + serde_json::from_slice::<RegistrationRelaxed>(json).map(|val| val.0) + } + /// Convenience function for [`CustomRegistration::deserialize`]. + /// + /// # Errors + /// + /// Errors iff [`CustomRegistration::deserialize`] does. + #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] + #[cfg(feature = "serde_relaxed")] + #[inline] + pub fn from_json_custom(json: &[u8]) -> Result<Self, SerdeJsonErr> { + serde_json::from_slice::<CustomRegistration>(json).map(|val| val.0) + } } impl Response for Registration { type Auth = AuthenticatorAttestation; @@ -3045,8 +3088,8 @@ mod tests { use super::{ super::{ super::{ - request::{AsciiDomain, RpId}, AggErr, + request::{AsciiDomain, RpId}, }, auth::{AuthenticatorAssertion, AuthenticatorData}, }, @@ -3111,8 +3154,12 @@ 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(client_data_json_2, authenticator_data, signature, None); + let auth_assertion = AuthenticatorAssertion::new_user_vec( + client_data_json_2, + authenticator_data, + signature, + None, + ); let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?; assert_eq!( auth_data.rp_id_hash(), @@ -3189,8 +3236,12 @@ 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(client_data_json_2, authenticator_data, signature, None); + let auth_assertion = AuthenticatorAssertion::new_user_vec( + client_data_json_2, + authenticator_data, + signature, + None, + ); let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?; assert_eq!( auth_data.rp_id_hash(), diff --git a/src/response/register/ser.rs b/src/response/register/ser.rs @@ -22,7 +22,7 @@ use core::{ str, }; use data_encoding::BASE64URL_NOPAD; -use rsa::sha2::{digest::OutputSizeUser as _, Sha256}; +use rsa::sha2::{Sha256, digest::OutputSizeUser as _}; use serde::de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, Unexpected, Visitor}; /// Functionality for deserializing DER-encoded `SubjectPublicKeyInfo` _without_ making copies of data or /// verifying the key is valid. This exists purely to ensure that the public key we receive in JSON is the same as @@ -34,8 +34,8 @@ mod spki { }; use core::fmt::{self, Display, Formatter}; use p256::{ - elliptic_curve::{generic_array::typenum::type_operators::ToInt as _, Curve}, NistP256, + elliptic_curve::{Curve, generic_array::typenum::type_operators::ToInt as _}, }; use p384::NistP384; /// Value assigned to the integer type under the universal tag class per @@ -703,7 +703,10 @@ impl<'e, const I: bool> Deserialize<'e> for AttestField<I> { impl<const IG: bool> Visitor<'_> for AttestFieldVisitor<IG> { type Value = AttestField<IG>; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - write!(formatter, "'{CLIENT_DATA_JSON}', '{ATTESTATION_OBJECT}', '{AUTHENTICATOR_DATA}', '{TRANSPORTS}', '{PUBLIC_KEY}', or '{PUBLIC_KEY_ALGORITHM}'") + write!( + formatter, + "'{CLIENT_DATA_JSON}', '{ATTESTATION_OBJECT}', '{AUTHENTICATOR_DATA}', '{TRANSPORTS}', '{PUBLIC_KEY}', or '{PUBLIC_KEY_ALGORITHM}'" + ) } fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> where @@ -735,7 +738,7 @@ impl<'e, const I: bool> Deserialize<'e> for AttestField<I> { /// a `Vec` that contains the attestation object and hash for signature verification. Calling code /// can avoid any reallocation that would occur when the capacity is not large enough by ensuring the /// passed `Vec` has at least 32 bytes of available capacity. -struct AttObj(Vec<u8>); +pub(super) struct AttObj(pub Vec<u8>); impl<'e> Deserialize<'e> for AttObj { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where @@ -1382,37 +1385,38 @@ impl<'de> Deserialize<'de> for Registration { mod tests { use super::{ super::{ - cbor, AuthenticatorAttachment, Ed25519PubKey, Registration, RsaPubKey, - UncompressedP256PubKey, UncompressedP384PubKey, ALG, EC2, EDDSA, ES256, ES384, KTY, - OKP, RSA, + ALG, AuthenticatorAttachment, EC2, EDDSA, ES256, ES384, Ed25519PubKey, KTY, OKP, RSA, + Registration, RsaPubKey, UncompressedP256PubKey, UncompressedP384PubKey, cbor, }, - spki::SubjectPublicKeyInfo, CoseAlgorithmIdentifier, + spki::SubjectPublicKeyInfo, }; use data_encoding::BASE64URL_NOPAD; - use ed25519_dalek::{pkcs8::EncodePublicKey, VerifyingKey}; + use ed25519_dalek::{VerifyingKey, pkcs8::EncodePublicKey}; use p256::{ - elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _}, EncodedPoint as P256Pt, PublicKey as P256PubKey, SecretKey as P256Key, + elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _}, }; use p384::{EncodedPoint as P384Pt, PublicKey as P384PubKey, SecretKey as P384Key}; use rsa::{ + BigUint, RsaPrivateKey, sha2::{Digest as _, Sha256}, traits::PublicKeyParts, - BigUint, RsaPrivateKey, }; use serde::de::{Error as _, Unexpected}; use serde_json::Error; #[test] fn ed25519_spki() { - assert!(Ed25519PubKey::from_der( - VerifyingKey::from_bytes(&[1; 32]) - .unwrap() - .to_public_key_der() - .unwrap() - .as_bytes() - ) - .map_or(false, |k| k.0 == [1; 32])); + assert!( + Ed25519PubKey::from_der( + VerifyingKey::from_bytes(&[1; 32]) + .unwrap() + .to_public_key_der() + .unwrap() + .as_bytes() + ) + .map_or(false, |k| k.0 == [1; 32]) + ); } #[test] fn p256_spki() { @@ -1692,38 +1696,40 @@ mod tests { let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg.response.client_data_json - == c_data_json.as_bytes() - && reg.response.attestation_object_and_c_data_hash[..att_obj.len()] - == att_obj - && reg.response.attestation_object_and_c_data_hash[att_obj.len()..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && reg.response.transports.count() == 6 - && matches!( - reg.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() ) - && reg.client_extension_results.cred_props.is_none() - && reg.client_extension_results.prf.is_none())); + .map_or(false, |reg| reg.response.client_data_json + == c_data_json.as_bytes() + && reg.response.attestation_object_and_c_data_hash[..att_obj.len()] + == att_obj + && reg.response.attestation_object_and_c_data_hash[att_obj.len()..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && reg.response.transports.count() == 6 + && matches!( + reg.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + ) + && reg.client_extension_results.cred_props.is_none() + && reg.client_extension_results.prf.is_none()) + ); // `id` and `rawId` mismatch. let mut err = Error::invalid_value( Unexpected::Bytes( @@ -2184,25 +2190,27 @@ mod tests { err ); // Duplicate `transports` are allowed. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": ["usb", "usb"], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg.response.transports.count() == 1)); + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": ["usb", "usb"], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg.response.transports.count() == 1) + ); // `null` `transports`. err = Error::invalid_type(Unexpected::Other("null"), &"transports") .to_string() @@ -2263,29 +2271,31 @@ mod tests { err ); // `null` `authenticatorAttachment`. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "authenticatorAttachment": null, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| matches!( - reg.authenticator_attachment, - AuthenticatorAttachment::None - ))); + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "authenticatorAttachment": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| matches!( + reg.authenticator_attachment, + AuthenticatorAttachment::None + )) + ); // Unknown `authenticatorAttachment`. err = Error::invalid_value( Unexpected::Str("Platform"), @@ -2959,86 +2969,92 @@ mod tests { let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg.response.client_data_json - == c_data_json.as_bytes() - && reg.response.attestation_object_and_c_data_hash[..att_obj.len()] - == att_obj - && reg.response.attestation_object_and_c_data_hash[att_obj.len()..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && reg.response.transports.is_empty() - && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None) - && reg.client_extension_results.cred_props.is_none() - && reg.client_extension_results.prf.is_none())); + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg.response.client_data_json + == c_data_json.as_bytes() + && reg.response.attestation_object_and_c_data_hash[..att_obj.len()] + == att_obj + && reg.response.attestation_object_and_c_data_hash[att_obj.len()..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && reg.response.transports.is_empty() + && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None) + && reg.client_extension_results.cred_props.is_none() + && reg.client_extension_results.prf.is_none()) + ); // `null` `credProps`. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": null - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .is_none() - && reg.client_extension_results.prf.is_none())); + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": null + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg + .client_extension_results + .cred_props + .is_none() + && reg.client_extension_results.prf.is_none()) + ); // `null` `prf`. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": null - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .is_none() - && reg.client_extension_results.prf.is_none())); + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": null + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg + .client_extension_results + .cred_props + .is_none() + && reg.client_extension_results.prf.is_none()) + ); // Unknown `clientExtensionResults`. let mut err = Error::unknown_field("CredProps", ["credProps", "prf"].as_slice()) .to_string() @@ -3102,115 +3118,123 @@ mod tests { err ); // `null` `rk`. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": { - "rk": null - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .map_or(false, |props| props.rk.is_none()) - && reg.client_extension_results.prf.is_none())); - // Missing `rk`. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": {} - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .map_or(false, |props| props.rk.is_none()) - && reg.client_extension_results.prf.is_none())); + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": { + "rk": null + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg + .client_extension_results + .cred_props + .map_or(false, |props| props.rk.is_none()) + && reg.client_extension_results.prf.is_none()) + ); + // Missing `rk`. + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": {} + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg + .client_extension_results + .cred_props + .map_or(false, |props| props.rk.is_none()) + && reg.client_extension_results.prf.is_none()) + ); // `true` rk`. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": { - "rk": true - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .map_or(false, |props| props.rk.unwrap_or_default()) - && reg.client_extension_results.prf.is_none())); + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": { + "rk": true + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg + .client_extension_results + .cred_props + .map_or(false, |props| props.rk.unwrap_or_default()) + && reg.client_extension_results.prf.is_none()) + ); // `false` rk`. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": { - "rk": false - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .map_or(false, |props| props.rk.map_or(false, |rk| !rk)) - && reg.client_extension_results.prf.is_none())); + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": { + "rk": false + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg + .client_extension_results + .cred_props + .map_or(false, |props| props.rk.map_or(false, |rk| !rk)) + && reg.client_extension_results.prf.is_none()) + ); // Invalid `rk`. err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean") .to_string() @@ -3368,67 +3392,71 @@ mod tests { err ); // `true` `enabled`. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .is_none() - && reg + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg .client_extension_results - .prf - .map_or(false, |prf| prf.enabled))); + .cred_props + .is_none() + && reg + .client_extension_results + .prf + .map_or(false, |prf| prf.enabled)) + ); // `false` `enabled`. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": false, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .is_none() - && reg + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": false, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg .client_extension_results - .prf - .map_or(false, |prf| !prf.enabled))); + .cred_props + .is_none() + && reg + .client_extension_results + .prf + .map_or(false, |prf| !prf.enabled)) + ); // Invalid `enabled`. err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean") .to_string() @@ -3462,37 +3490,39 @@ mod tests { err ); // `null` `results` with `enabled` `true`. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": null, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .is_none() - && reg + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": null, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg .client_extension_results - .prf - .map_or(false, |prf| prf.enabled))); + .cred_props + .is_none() + && reg + .client_extension_results + .prf + .map_or(false, |prf| prf.enabled)) + ); // `null` `results` with `enabled` `false`. err = Error::custom( "prf must not have 'results', including a null 'results', if 'enabled' is false", @@ -3560,9 +3590,39 @@ mod tests { .into_bytes()[..err.len()], err ); - // Missing `first`. - err = Error::missing_field("first").to_string().into_bytes(); - assert_eq!( + // Missing `first`. + err = Error::missing_field("first").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": {}, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // `null` `first`. + assert!( serde_json::from_str::<Registration>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", @@ -3578,7 +3638,9 @@ mod tests { "clientExtensionResults": { "prf": { "enabled": true, - "results": {}, + "results": { + "first": null + }, } }, "type": "public-key" @@ -3586,80 +3648,52 @@ mod tests { .to_string() .as_str() ) - .unwrap_err() - .to_string() - .into_bytes()[..err.len()], - err - ); - // `null` `first`. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": { - "first": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .is_none() - && reg + .map_or(false, |reg| reg .client_extension_results - .prf - .map_or(false, |prf| prf.enabled))); + .cred_props + .is_none() + && reg + .client_extension_results + .prf + .map_or(false, |prf| prf.enabled)) + ); // `null` `second`. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": { - "first": null, - "second": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .is_none() - && reg + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": { + "first": null, + "second": null + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg .client_extension_results - .prf - .map_or(false, |prf| prf.enabled))); + .cred_props + .is_none() + && reg + .client_extension_results + .prf + .map_or(false, |prf| prf.enabled)) + ); // Non-`null` `first`. err = Error::invalid_type(Unexpected::Option, &"null") .to_string() @@ -4052,34 +4086,36 @@ mod tests { let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -7, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg.response.client_data_json - == c_data_json.as_bytes() - && reg.response.attestation_object_and_c_data_hash[..att_obj.len()] - == att_obj - && reg.response.attestation_object_and_c_data_hash[att_obj.len()..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && reg.response.transports.is_empty() - && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None) - && reg.client_extension_results.cred_props.is_none() - && reg.client_extension_results.prf.is_none())); + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -7, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg.response.client_data_json + == c_data_json.as_bytes() + && reg.response.attestation_object_and_c_data_hash[..att_obj.len()] + == att_obj + && reg.response.attestation_object_and_c_data_hash[att_obj.len()..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && reg.response.transports.is_empty() + && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None) + && reg.client_extension_results.cred_props.is_none() + && reg.client_extension_results.prf.is_none()) + ); // `publicKeyAlgorithm` mismatch. let mut err = Error::invalid_value( Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()), @@ -4515,34 +4551,36 @@ mod tests { let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -35, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg.response.client_data_json - == c_data_json.as_bytes() - && reg.response.attestation_object_and_c_data_hash[..att_obj.len()] - == att_obj - && reg.response.attestation_object_and_c_data_hash[att_obj.len()..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && reg.response.transports.is_empty() - && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None) - && reg.client_extension_results.cred_props.is_none() - && reg.client_extension_results.prf.is_none())); + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -35, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg.response.client_data_json + == c_data_json.as_bytes() + && reg.response.attestation_object_and_c_data_hash[..att_obj.len()] + == att_obj + && reg.response.attestation_object_and_c_data_hash[att_obj.len()..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && reg.response.transports.is_empty() + && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None) + && reg.client_extension_results.cred_props.is_none() + && reg.client_extension_results.prf.is_none()) + ); // `publicKeyAlgorithm` mismatch. let mut err = Error::invalid_value( Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()), @@ -4677,24 +4715,26 @@ mod tests { err ); // Missing `publicKey` is allowed when not using EdDSA, ES256, or RS256. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKeyAlgorithm": -35, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKeyAlgorithm": -35, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `publicKeyAlgorithm` mismatch when `publicKey` does not exist. err = Error::invalid_value( Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()), @@ -4725,25 +4765,27 @@ mod tests { err ); // `null` `publicKey` is allowed when not using EdDSA, ES256, or RS256. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": null, - "publicKeyAlgorithm": -35, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": null, + "publicKeyAlgorithm": -35, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `publicKeyAlgorithm` mismatch when `publicKey` is null. err = Error::invalid_value( Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()), @@ -5236,34 +5278,36 @@ mod tests { let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. - assert!(serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -257, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg.response.client_data_json - == c_data_json.as_bytes() - && reg.response.attestation_object_and_c_data_hash[..att_obj.len()] - == att_obj - && reg.response.attestation_object_and_c_data_hash[att_obj.len()..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && reg.response.transports.is_empty() - && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None) - && reg.client_extension_results.cred_props.is_none() - && reg.client_extension_results.prf.is_none())); + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -257, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg.response.client_data_json + == c_data_json.as_bytes() + && reg.response.attestation_object_and_c_data_hash[..att_obj.len()] + == att_obj + && reg.response.attestation_object_and_c_data_hash[att_obj.len()..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && reg.response.transports.is_empty() + && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None) + && reg.client_extension_results.cred_props.is_none() + && reg.client_extension_results.prf.is_none()) + ); // `publicKeyAlgorithm` mismatch. let mut err = Error::invalid_value( Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()), diff --git a/src/response/register/ser_relaxed.rs b/src/response/register/ser_relaxed.rs @@ -1,22 +1,30 @@ +#![expect( + clippy::question_mark_used, + reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs" +)] #[cfg(doc)] -use super::super::Challenge; +use super::super::{super::request::register::CoseAlgorithmIdentifier, Challenge, CredentialId}; use super::{ super::{ register::ser::{ - AuthenticatorAttestationVisitor, ClientExtensionsOutputsVisitor, AUTH_ATTEST_FIELDS, - EXT_FIELDS, + AUTH_ATTEST_FIELDS, AttObj, AuthenticatorAttestationVisitor, + ClientExtensionsOutputsVisitor, EXT_FIELDS, + }, + ser::{ + AuthenticationExtensionsPrfOutputsHelper, Base64DecodedVal, ClientExtensions, + PublicKeyCredential, Type, }, - ser::{AuthenticationExtensionsPrfOutputsHelper, ClientExtensions, PublicKeyCredential}, ser_relaxed::AuthenticationExtensionsPrfValuesRelaxed, }, + AttestationObject, AuthenticationExtensionsPrfOutputs, AuthenticatorAttachment, + AuthenticatorAttestation, ClientExtensionsOutputs, CredentialPropertiesOutput, Registration, ser::{AuthAttest, CredentialPropertiesOutputVisitor, PROPS_FIELDS}, - AttestationObject, AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation, - ClientExtensionsOutputs, CredentialPropertiesOutput, Registration, }; -use core::marker::PhantomData; -#[cfg(doc)] -use data_encoding::BASE64URL_NOPAD; -use serde::de::{Deserialize, Deserializer, Error, Unexpected}; +use core::{ + fmt::{self, Formatter}, + marker::PhantomData, +}; +use serde::de::{Deserialize, Deserializer, Error, MapAccess, Unexpected, Visitor}; /// `newtype` around `CredentialPropertiesOutput` with a "relaxed" [`Self::deserialize`] implementation. #[derive(Debug)] pub struct CredentialPropertiesOutputRelaxed(pub CredentialPropertiesOutput); @@ -201,26 +209,253 @@ impl<'de> Deserialize<'de> for RegistrationRelaxed { }) } } +/// `newtype` around `Registration` with a custom [`Self::deserialize`] implementation. +#[derive(Debug)] +pub struct CustomRegistration(pub Registration); +impl<'de> Deserialize<'de> for CustomRegistration { + /// Despite the spec having a + /// [pre-defined format](https://www.w3.org/TR/webauthn-3/#dictdef-registrationresponsejson) that clients + /// can follow, the downside is the superfluous data it contains. + /// + /// There simply is no reason to send the [`CredentialId`] _four_ times. 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)). + /// + /// While [`Registration::deserialize`] _strictly_ adheres to the JSON definition (e.g., it requires `publicKey` + /// to exist and match with what is in both `authenticatorData` and `attestationObject` when the underlying + /// algorithm is not [`CoseAlgorithmIdentifier::Es384`]), this implementation + /// strictly disallows superfluous data. Specifically the following JSON is required to be sent where duplicate + /// and unknown keys are disallowed: + /// + /// ```json + /// { + /// "attestationObject":<base64url string>, + /// "authenticatorAttachment":null|"platform"|"cross-platform", + /// "clientDataJSON":<base64url string>, + /// "clientExtensionResults":<see ClientExtensionsOutputs::deserialize>, + /// "transports":<see AuthTransports::deserialize>, + /// "type":null|"public-key" + /// } + /// ``` + /// + /// All of the above keys are required with the exceptions of `"authenticatorAttachment"` and `"type"`. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::response::register::ser_relaxed::CustomRegistration; + /// assert!( + /// // The below payload is technically valid, but `RegistrationServerState::verify` will fail + /// // since the attestationObject is not valid. This is true for `Registration::deserialize` + /// // as well since attestationObject parsing is always deferred. + /// serde_json::from_str::<CustomRegistration>( + /// r#"{ + /// "transports": ["usb"], + /// "attestationObject": "AA", + /// "authenticatorAttachment": "cross-platform", + /// "clientExtensionResults": {}, + /// "clientDataJSON": "AA", + /// "type": "public-key" + /// }"# + /// ).is_ok()); + /// ``` + #[expect( + clippy::too_many_lines, + reason = "want to hide; thus don't want to put in an outer scope" + )] + #[inline] + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + /// `Visitor` for `CustomRegistration`. + struct CustomRegistrationVisitor; + impl<'d> Visitor<'d> for CustomRegistrationVisitor { + type Value = CustomRegistration; + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_str("CustomRegistration") + } + #[expect( + clippy::too_many_lines, + reason = "want to hide; thus don't want to put in an outer scope" + )] + fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> + where + A: MapAccess<'d>, + { + /// Fields in the JSON. + enum Field { + /// `attestationObject` key. + AttestationObject, + /// `authenticatorAttachment` key. + AuthenticatorAttachment, + /// `clientDataJSON` key. + ClientDataJson, + /// `clientExtensionResults` key. + ClientExtensionResults, + /// `transports` key. + Transports, + /// `type` key. + Type, + } + impl<'e> Deserialize<'e> for Field { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'e>, + { + /// `Visitor` for `Field`. + struct FieldVisitor; + impl Visitor<'_> for FieldVisitor { + type Value = Field; + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + write!( + formatter, + "'{ATTESTATION_OBJECT}', '{AUTHENTICATOR_ATTACHMENT}', '{CLIENT_DATA_JSON}', '{CLIENT_EXTENSION_RESULTS}', '{TRANSPORTS}', or '{TYPE}'" + ) + } + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: Error, + { + match v { + ATTESTATION_OBJECT => Ok(Field::AttestationObject), + AUTHENTICATOR_ATTACHMENT => Ok(Field::AuthenticatorAttachment), + CLIENT_DATA_JSON => Ok(Field::ClientDataJson), + CLIENT_EXTENSION_RESULTS => Ok(Field::ClientExtensionResults), + TRANSPORTS => Ok(Field::Transports), + TYPE => Ok(Field::Type), + _ => Err(E::unknown_field(v, FIELDS)), + } + } + } + deserializer.deserialize_identifier(FieldVisitor) + } + } + let mut attestation_object = None; + let mut authenticator_attachment = None; + let mut client_data_json = None; + let mut ext = None; + let mut transports = None; + let mut typ = false; + while let Some(key) = map.next_key()? { + match key { + Field::AttestationObject => { + if attestation_object.is_some() { + return Err(Error::duplicate_field(ATTESTATION_OBJECT)); + } + attestation_object = + map.next_value::<AttObj>().map(|val| Some(val.0))?; + } + Field::AuthenticatorAttachment => { + if authenticator_attachment.is_some() { + return Err(Error::duplicate_field(AUTHENTICATOR_ATTACHMENT)); + } + authenticator_attachment = map.next_value::<Option<_>>().map(Some)?; + } + Field::ClientDataJson => { + if client_data_json.is_some() { + return Err(Error::duplicate_field(CLIENT_DATA_JSON)); + } + client_data_json = map + .next_value::<Base64DecodedVal>() + .map(|val| Some(val.0))?; + } + Field::ClientExtensionResults => { + if ext.is_some() { + return Err(Error::duplicate_field(CLIENT_EXTENSION_RESULTS)); + } + ext = map.next_value().map(Some)?; + } + Field::Transports => { + if transports.is_some() { + return Err(Error::duplicate_field(TRANSPORTS)); + } + transports = map.next_value().map(Some)?; + } + Field::Type => { + if typ { + return Err(Error::duplicate_field(TYPE)); + } + typ = map.next_value::<Option<Type>>().map(|_| true)?; + } + } + } + attestation_object + .ok_or_else(|| Error::missing_field(ATTESTATION_OBJECT)) + .and_then(|att_obj| { + client_data_json + .ok_or_else(|| Error::missing_field(CLIENT_DATA_JSON)) + .and_then(|c_data| { + ext.ok_or_else(|| Error::missing_field(CLIENT_EXTENSION_RESULTS)) + .and_then(|client_extension_results| { + transports + .ok_or_else(|| Error::missing_field(TRANSPORTS)) + .map(|trans| { + CustomRegistration(Registration { + response: AuthenticatorAttestation::new( + c_data, att_obj, trans, + ), + authenticator_attachment: + authenticator_attachment.map_or( + AuthenticatorAttachment::None, + |auth_attach| { + auth_attach.unwrap_or( + AuthenticatorAttachment::None, + ) + }, + ), + client_extension_results, + }) + }) + }) + }) + }) + } + } + /// `attestationObject` key. + const ATTESTATION_OBJECT: &str = "attestationObject"; + /// `authenticatorAttachment` key. + const AUTHENTICATOR_ATTACHMENT: &str = "authenticatorAttachment"; + /// `clientDataJSON` key. + const CLIENT_DATA_JSON: &str = "clientDataJSON"; + /// `clientExtensionResults` key. + const CLIENT_EXTENSION_RESULTS: &str = "clientExtensionResults"; + /// `transports` key. + const TRANSPORTS: &str = "transports"; + /// `type` key. + const TYPE: &str = "type"; + /// Fields. + const FIELDS: &[&str; 6] = &[ + ATTESTATION_OBJECT, + AUTHENTICATOR_ATTACHMENT, + CLIENT_DATA_JSON, + CLIENT_EXTENSION_RESULTS, + TRANSPORTS, + TYPE, + ]; + deserializer.deserialize_struct("CustomRegistration", FIELDS, CustomRegistrationVisitor) + } +} #[cfg(test)] mod tests { use super::{ super::{ - super::super::request::register::CoseAlgorithmIdentifier, cbor, - AuthenticatorAttachment, ALG, EC2, EDDSA, ES256, ES384, KTY, OKP, RSA, + super::super::request::register::CoseAlgorithmIdentifier, ALG, AuthenticatorAttachment, + EC2, EDDSA, ES256, ES384, KTY, OKP, RSA, cbor, }, - RegistrationRelaxed, + CustomRegistration, RegistrationRelaxed, }; use data_encoding::BASE64URL_NOPAD; - use ed25519_dalek::{pkcs8::EncodePublicKey, VerifyingKey}; + use ed25519_dalek::{VerifyingKey, pkcs8::EncodePublicKey}; use p256::{ - elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _}, EncodedPoint as P256Pt, PublicKey as P256PubKey, SecretKey as P256Key, + elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _}, }; use p384::{EncodedPoint as P384Pt, PublicKey as P384PubKey, SecretKey as P384Key}; use rsa::{ + BigUint, RsaPrivateKey, sha2::{Digest as _, Sha256}, traits::PublicKeyParts, - BigUint, RsaPrivateKey, }; use serde::de::{Error as _, Unexpected}; use serde_json::Error; @@ -392,38 +627,40 @@ mod tests { let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg.0.response.client_data_json - == c_data_json.as_bytes() - && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()] - == att_obj - && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && reg.0.response.transports.count() == 6 - && matches!( - reg.0.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() ) - && reg.0.client_extension_results.cred_props.is_none() - && reg.0.client_extension_results.prf.is_none())); + .map_or(false, |reg| reg.0.response.client_data_json + == c_data_json.as_bytes() + && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()] + == att_obj + && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && reg.0.response.transports.count() == 6 + && matches!( + reg.0.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + ) + && reg.0.client_extension_results.cred_props.is_none() + && reg.0.client_extension_results.prf.is_none()) + ); // `id` and `rawId` mismatch. let mut err = Error::invalid_value( Unexpected::Bytes( @@ -461,83 +698,91 @@ mod tests { err ); // missing `id`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `id`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": null, - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": null, + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Missing `rawId`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `rawId`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": null, - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": null, + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `id` and the credential id in authenticator data mismatch. err = Error::invalid_value( Unexpected::Bytes( @@ -607,61 +852,16 @@ mod tests { err ); // Missing `authenticatorData`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); - // `null `authenticatorData`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "transports": [], - "authenticatorData": null, - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); - // `publicKeyAlgorithm` mismatch. - err = Error::invalid_value( - Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()), - &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Eddsa).as_str() - ) - .to_string().into_bytes(); - assert_eq!( + assert!( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -7, + "publicKeyAlgorithm": -8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -670,50 +870,103 @@ mod tests { .to_string() .as_str() ) - .unwrap_err() - .to_string() - .into_bytes()[..err.len()], - err + .is_ok() ); - // Missing `publicKeyAlgorithm`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() + // `null `authenticatorData`. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "transports": [], + "authenticatorData": null, + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); + // `publicKeyAlgorithm` mismatch. + err = Error::invalid_value( + Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()), + &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Eddsa).as_str() ) - .is_ok()); - // `null` `publicKeyAlgorithm`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": null, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) + .to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -7, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() .to_string() - .as_str() - ) - .is_ok()); + .into_bytes()[..err.len()], + err + ); + // Missing `publicKeyAlgorithm`. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); + // `null` `publicKeyAlgorithm`. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": null, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `publicKey` mismatch. err = Error::invalid_value( Unexpected::Bytes([0; 32].as_slice()), @@ -746,111 +999,71 @@ mod tests { err ); // Missing `publicKey`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `publicKey`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": null, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": null, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Missing `transports`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Duplicate `transports` are allowed. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": ["usb", "usb"], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg.0.response.transports.count() == 1)); - // `null` `transports`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": null, - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); - // Unknown `transports`. - err = Error::invalid_value( - Unexpected::Str("Usb"), - &"'ble', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'", - ) - .to_string() - .into_bytes(); - assert_eq!( + assert!( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", @@ -858,7 +1071,7 @@ mod tests { "response": { "clientDataJSON": b64_cdata, "authenticatorData": b64_adata, - "transports": ["Usb"], + "transports": ["usb", "usb"], "publicKey": b64_key, "publicKeyAlgorithm": -8, "attestationObject": b64_aobj, @@ -869,35 +1082,662 @@ mod tests { .to_string() .as_str() ) - .unwrap_err() - .to_string() - .into_bytes()[..err.len()], - err + .map_or(false, |reg| reg.0.response.transports.count() == 1) ); - // `null` `authenticatorAttachment`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, + // `null` `transports`. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": null, + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); + // Unknown `transports`. + err = Error::invalid_value( + Unexpected::Str("Usb"), + &"'ble', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'", + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": ["Usb"], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // `null` `authenticatorAttachment`. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "authenticatorAttachment": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| matches!( + reg.0.authenticator_attachment, + AuthenticatorAttachment::None + )) + ); + // Unknown `authenticatorAttachment`. + err = Error::invalid_value( + Unexpected::Str("Platform"), + &"'platform' or 'cross-platform'", + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "authenticatorAttachment": "Platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Missing `clientDataJSON`. + err = Error::missing_field("clientDataJSON") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // `null` `clientDataJSON`. + err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": null, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Missing `attestationObject`. + err = Error::missing_field("attestationObject") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // `null` `attestationObject`. + err = Error::invalid_type( + Unexpected::Other("null"), + &"base64url-encoded attestation object", + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": null, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Missing `response`. + err = Error::missing_field("response").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // `null` `response`. + err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAttestation") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Empty `response`. + err = Error::missing_field("clientDataJSON") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": {}, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Missing `clientExtensionResults`. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); + // `null` `clientExtensionResults`. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": null, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); + // Missing `type`. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + }) + .to_string() + .as_str() + ) + .is_ok() + ); + // `null` `type`. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": null + }) + .to_string() + .as_str() + ) + .is_ok() + ); + // 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::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "Public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // `null`. + err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!(null).to_string().as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Empty. + err = Error::missing_field("response").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>(serde_json::json!({}).to_string().as_str()) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Unknown field in `response`. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + "foo": true, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); + // Duplicate field in `response`. + err = Error::duplicate_field("transports") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata}\", + \"authenticatorData\": \"{b64_adata}\", + \"transports\": [], + \"publicKey\": \"{b64_key}\", + \"publicKeyAlgorithm\": -8, + \"attestationObject\": \"{b64_aobj}\", + \"transports\": [] + }}, + \"clientExtensionResults\": {{}}, + \"type\": \"public-key\" + + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Unknown field in `PublicKeyCredential`. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj + }, + "clientExtensionResults": {}, + "type": "public-key", + "foo": true, + }) + .to_string() + .as_str() + ) + .is_ok() + ); + // Duplicate field in `PublicKeyCredential`. + err = Error::duplicate_field("id").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata}\", + \"authenticatorData\": \"{b64_adata}\", + \"transports\": [], + \"publicKey\": \"{b64_key}\", + \"publicKeyAlgorithm\": -8, + \"attestationObject\": \"{b64_aobj}\" + }}, + \"clientExtensionResults\": {{}}, + \"type\": \"public-key\" + + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Base case is correct. + assert!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ "attestationObject": b64_aobj, - }, - "authenticatorAttachment": null, - "clientExtensionResults": {}, - "type": "public-key" - }) + "authenticatorAttachment": "cross-platform", + "clientDataJSON": b64_cdata, + "clientExtensionResults": {}, + "transports": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"], + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg.0.response.client_data_json + == c_data_json.as_bytes() + && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()] + == att_obj + && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && reg.0.response.transports.count() == 6 + && matches!( + reg.0.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + ) + && reg.0.client_extension_results.cred_props.is_none() + && reg.0.client_extension_results.prf.is_none()) + ); + // Missing `transports`. + err = Error::missing_field("transports").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "attestationObject": b64_aobj, + "authenticatorAttachment": "cross-platform", + "clientDataJSON": b64_cdata, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() .to_string() - .as_str() + .into_bytes()[..err.len()], + err + ); + // Duplicate `transports` are allowed. + assert!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "attestationObject": b64_aobj, + "authenticatorAttachment": "cross-platform", + "clientDataJSON": b64_cdata, + "clientExtensionResults": {}, + "transports": ["usb", "usb"], + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg.0.response.transports.count() == 1) + ); + // `null` `transports`. + err = Error::invalid_type(Unexpected::Other("null"), &"AuthTransports") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "clientDataJSON": b64_cdata, + "transports": null, + "attestationObject": b64_aobj, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // Unknown `transports`. + err = Error::invalid_value( + Unexpected::Str("Usb"), + &"'ble', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'", ) - .map_or(false, |reg| matches!( - reg.0.authenticator_attachment, - AuthenticatorAttachment::None - ))); + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "attestationObject": b64_aobj, + "authenticatorAttachment": "cross-platform", + "clientDataJSON": b64_cdata, + "clientExtensionResults": {}, + "transports": ["Usb"], + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // `null` `authenticatorAttachment`. + assert!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "attestationObject": b64_aobj, + "authenticatorAttachment": null, + "clientDataJSON": b64_cdata, + "clientExtensionResults": {}, + "transports": [], + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| matches!( + reg.0.authenticator_attachment, + AuthenticatorAttachment::None + )) + ); // Unknown `authenticatorAttachment`. err = Error::invalid_value( Unexpected::Str("Platform"), @@ -906,20 +1746,13 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( + serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, + "attestationObject": b64_aobj, "authenticatorAttachment": "Platform", + "clientDataJSON": b64_cdata, "clientExtensionResults": {}, + "transports": [], "type": "public-key" }) .to_string() @@ -935,17 +1768,10 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( + serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, + "transports": [], + "attestationObject": b64_aobj, "clientExtensionResults": {}, "type": "public-key" }) @@ -962,20 +1788,11 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( + serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": null, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" + "clientDataJSON": null, + "transports": [], + "attestationObject": b64_aobj, }) .to_string() .as_str() @@ -990,17 +1807,10 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( + serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - }, + "clientDataJSON": b64_cdata, + "transports": [], "clientExtensionResults": {}, "type": "public-key" }) @@ -1020,18 +1830,11 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( + serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": null, - }, + "clientDataJSON": b64_cdata, + "transports": [], + "attestationObject": null, "clientExtensionResults": {}, "type": "public-key" }) @@ -1043,14 +1846,16 @@ mod tests { .into_bytes()[..err.len()], err ); - // Missing `response`. - err = Error::missing_field("response").to_string().into_bytes(); + // Missing `clientExtensionResults`. + err = Error::missing_field("clientExtensionResults") + .to_string() + .into_bytes(); assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( + serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "clientExtensionResults": {}, + "clientDataJSON": b64_cdata, + "transports": [], + "attestationObject": b64_aobj, "type": "public-key" }) .to_string() @@ -1061,17 +1866,17 @@ mod tests { .into_bytes()[..err.len()], err ); - // `null` `response`. - err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAttestation") + // `null` `clientExtensionResults`. + err = Error::invalid_type(Unexpected::Other("null"), &"ClientExtensionsOutputs") .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( + serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": null, - "clientExtensionResults": {}, + "clientDataJSON": b64_cdata, + "transports": [], + "attestationObject": b64_aobj, + "clientExtensionResults": null, "type": "public-key" }) .to_string() @@ -1082,122 +1887,45 @@ mod tests { .into_bytes()[..err.len()], err ); - // Empty `response`. - err = Error::missing_field("clientDataJSON") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( + // Missing `type`. + assert!( + serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": {}, + "attestationObject": b64_aobj, + "clientDataJSON": b64_cdata, "clientExtensionResults": {}, - "type": "public-key" + "transports": [] }) .to_string() .as_str() ) - .unwrap_err() - .to_string() - .into_bytes()[..err.len()], - err + .map_or(false, |_| true) ); - // Missing `clientExtensionResults`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); - // `null` `clientExtensionResults`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": null, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); - // Missing `type`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - }) - .to_string() - .as_str() - ) - .is_ok()); // `null` `type`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { + assert!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "attestationObject": b64_aobj, "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, + "clientExtensionResults": {}, "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": null - }) - .to_string() - .as_str() - ) - .is_ok()); + "type": null + }) + .to_string() + .as_str() + ) + .map_or(false, |_| true) + ); // 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::<RegistrationRelaxed>( + serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, + "clientDataJSON": b64_cdata, + "transports": [], + "attestationObject": b64_aobj, "clientExtensionResults": {}, "type": "Public-key" }) @@ -1210,11 +1938,11 @@ mod tests { err ); // `null`. - err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential") + err = Error::invalid_type(Unexpected::Other("null"), &"CustomRegistration") .to_string() .into_bytes(); assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( + serde_json::from_str::<CustomRegistration>( serde_json::json!(null).to_string().as_str() ) .unwrap_err() @@ -1223,59 +1951,42 @@ mod tests { err ); // Empty. - err = Error::missing_field("response").to_string().into_bytes(); + err = Error::missing_field("attestationObject") + .to_string() + .into_bytes(); assert_eq!( - serde_json::from_str::<RegistrationRelaxed>(serde_json::json!({}).to_string().as_str()) + serde_json::from_str::<CustomRegistration>(serde_json::json!({}).to_string().as_str()) .unwrap_err() .to_string() .into_bytes()[..err.len()], err ); - // Unknown field in `response`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - "foo": true, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() + // Unknown field. + err = Error::unknown_field( + "foo", + [ + "attestationObject", + "authenticatorAttachment", + "clientDataJSON", + "clientExtensionResults", + "transports", + "type", + ] + .as_slice(), ) - .is_ok()); - // Duplicate field in `response`. - err = Error::duplicate_field("transports") - .to_string() - .into_bytes(); + .to_string() + .into_bytes(); assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", - \"authenticatorData\": \"{b64_adata}\", - \"transports\": [], - \"publicKey\": \"{b64_key}\", - \"publicKeyAlgorithm\": -8, - \"attestationObject\": \"{b64_aobj}\", - \"transports\": [] - }}, - \"clientExtensionResults\": {{}}, - \"type\": \"public-key\" - - }}" - ) + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "clientDataJSON": b64_cdata, + "transports": [], + "attestationObject": b64_aobj, + "foo": true, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() .as_str() ) .unwrap_err() @@ -1283,47 +1994,20 @@ mod tests { .into_bytes()[..err.len()], err ); - // Unknown field in `PublicKeyCredential`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj - }, - "clientExtensionResults": {}, - "type": "public-key", - "foo": true, - }) + // Duplicate field. + err = Error::duplicate_field("transports") .to_string() - .as_str() - ) - .is_ok()); - // Duplicate field in `PublicKeyCredential`. - err = Error::duplicate_field("id").to_string().into_bytes(); + .into_bytes(); assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( + serde_json::from_str::<CustomRegistration>( format!( "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", - \"authenticatorData\": \"{b64_adata}\", - \"transports\": [], - \"publicKey\": \"{b64_key}\", - \"publicKeyAlgorithm\": -8, - \"attestationObject\": \"{b64_aobj}\" - }}, + \"clientDataJSON\": \"{b64_cdata}\", + \"transports\": [], + \"attestationObject\": \"{b64_aobj}\", + \"transports\": [] \"clientExtensionResults\": {{}}, \"type\": \"public-key\" - }}" ) .as_str() @@ -1502,115 +2186,123 @@ mod tests { let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg.0.response.client_data_json - == c_data_json.as_bytes() - && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()] - == att_obj - && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && reg.0.response.transports.is_empty() - && matches!( - reg.0.authenticator_attachment, - AuthenticatorAttachment::None + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() ) - && reg.0.client_extension_results.cred_props.is_none() - && reg.0.client_extension_results.prf.is_none())); + .map_or(false, |reg| reg.0.response.client_data_json + == c_data_json.as_bytes() + && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()] + == att_obj + && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && reg.0.response.transports.is_empty() + && matches!( + reg.0.authenticator_attachment, + AuthenticatorAttachment::None + ) + && reg.0.client_extension_results.cred_props.is_none() + && reg.0.client_extension_results.prf.is_none()) + ); // `null` `credProps`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": null - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .is_none() - && reg.0.client_extension_results.prf.is_none())); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": null + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg + .0 + .client_extension_results + .cred_props + .is_none() + && reg.0.client_extension_results.prf.is_none()) + ); // `null` `prf`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": null - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .is_none() - && reg.0.client_extension_results.prf.is_none())); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": null + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg + .0 + .client_extension_results + .cred_props + .is_none() + && reg.0.client_extension_results.prf.is_none()) + ); // Unknown `clientExtensionResults`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "CredProps": { - "rk": true - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "CredProps": { + "rk": true + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Duplicate field. let mut err = Error::duplicate_field("credProps").to_string().into_bytes(); assert_eq!( @@ -1636,125 +2328,133 @@ mod tests { ) .as_str() ) - .unwrap_err() - .to_string() - .into_bytes()[..err.len()], - err + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // `null` `rk`. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": { + "rk": null + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg + .0 + .client_extension_results + .cred_props + .map_or(false, |props| props.rk.is_none()) + && reg.0.client_extension_results.prf.is_none()) + ); + // Missing `rk`. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": {} + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg + .0 + .client_extension_results + .cred_props + .map_or(false, |props| props.rk.is_none()) + && reg.0.client_extension_results.prf.is_none()) + ); + // `true` rk`. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": { + "rk": true + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg + .0 + .client_extension_results + .cred_props + .map_or(false, |props| props.rk.unwrap_or_default()) + && reg.0.client_extension_results.prf.is_none()) ); - // `null` `rk`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": { - "rk": null - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .map_or(false, |props| props.rk.is_none()) - && reg.0.client_extension_results.prf.is_none())); - // Missing `rk`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": {} - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .map_or(false, |props| props.rk.is_none()) - && reg.0.client_extension_results.prf.is_none())); - // `true` rk`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": { - "rk": true - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .map_or(false, |props| props.rk.unwrap_or_default()) - && reg.0.client_extension_results.prf.is_none())); // `false` rk`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": { - "rk": false - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .map_or(false, |props| props.rk.map_or(false, |rk| !rk)) - && reg.0.client_extension_results.prf.is_none())); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": { + "rk": false + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg + .0 + .client_extension_results + .cred_props + .map_or(false, |props| props.rk.map_or(false, |rk| !rk)) + && reg.0.client_extension_results.prf.is_none()) + ); // Invalid `rk`. err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean") .to_string() @@ -1788,29 +2488,31 @@ mod tests { err ); // Unknown `credProps` field. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": { - "Rk": true, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": { + "Rk": true, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Duplicate field in `credProps`. err = Error::duplicate_field("rk").to_string().into_bytes(); assert_eq!( @@ -1904,71 +2606,75 @@ mod tests { err ); // `true` `enabled`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .is_none() - && reg + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg .0 .client_extension_results - .prf - .map_or(false, |prf| prf.enabled))); + .cred_props + .is_none() + && reg + .0 + .client_extension_results + .prf + .map_or(false, |prf| prf.enabled)) + ); // `false` `enabled`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": false, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .is_none() - && reg + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": false, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg .0 .client_extension_results - .prf - .map_or(false, |prf| !prf.enabled))); + .cred_props + .is_none() + && reg + .0 + .client_extension_results + .prf + .map_or(false, |prf| !prf.enabled)) + ); // Invalid `enabled`. err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean") .to_string() @@ -1996,45 +2702,47 @@ mod tests { .to_string() .as_str() ) - .unwrap_err() - .to_string() - .into_bytes()[..err.len()], - err - ); - // `null` `results` with `enabled` `true`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": null, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .is_none() - && reg + .unwrap_err() + .to_string() + .into_bytes()[..err.len()], + err + ); + // `null` `results` with `enabled` `true`. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": null, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg .0 .client_extension_results - .prf - .map_or(false, |prf| prf.enabled))); + .cred_props + .is_none() + && reg + .0 + .client_extension_results + .prf + .map_or(false, |prf| prf.enabled)) + ); // `null` `results` with `enabled` `false`. err = Error::custom( "prf must not have 'results', including a null 'results', if 'enabled' is false", @@ -2103,103 +2811,109 @@ mod tests { err ); // Missing `first`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": {}, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": {}, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `first`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": { - "first": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .is_none() - && reg + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": { + "first": null + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg .0 .client_extension_results - .prf - .map_or(false, |prf| prf.enabled))); + .cred_props + .is_none() + && reg + .0 + .client_extension_results + .prf + .map_or(false, |prf| prf.enabled)) + ); // `null` `second`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": { - "first": null, - "second": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .is_none() - && reg + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": { + "first": null, + "second": null + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg .0 .client_extension_results - .prf - .map_or(false, |prf| prf.enabled))); + .cred_props + .is_none() + && reg + .0 + .client_extension_results + .prf + .map_or(false, |prf| prf.enabled)) + ); // Non-`null` `first`. err = Error::invalid_type(Unexpected::Option, &"null") .to_string() @@ -2272,58 +2986,62 @@ mod tests { err ); // Unknown `prf` field. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "Results": null - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "Results": null + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Unknown `results` field. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": { - "first": null, - "Second": null + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": { + "first": null, + "Second": null + } } - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // Duplicate field in `results`. err = Error::duplicate_field("first").to_string().into_bytes(); assert_eq!( @@ -2576,37 +3294,39 @@ mod tests { let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -7, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg.0.response.client_data_json - == c_data_json.as_bytes() - && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()] - == att_obj - && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && reg.0.response.transports.is_empty() - && matches!( - reg.0.authenticator_attachment, - AuthenticatorAttachment::None + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -7, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() ) - && reg.0.client_extension_results.cred_props.is_none() - && reg.0.client_extension_results.prf.is_none())); + .map_or(false, |reg| reg.0.response.client_data_json + == c_data_json.as_bytes() + && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()] + == att_obj + && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && reg.0.response.transports.is_empty() + && matches!( + reg.0.authenticator_attachment, + AuthenticatorAttachment::None + ) + && reg.0.client_extension_results.cred_props.is_none() + && reg.0.client_extension_results.prf.is_none()) + ); // `publicKeyAlgorithm` mismatch. let mut err = Error::invalid_value( Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()), @@ -2638,44 +3358,48 @@ mod tests { err ); // Missing `publicKeyAlgorithm`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `publicKeyAlgorithm`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": null, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": null, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `publicKey` mismatch. let bad_pub_key = P256PubKey::from_encoded_point(&P256Pt::from_affine_coordinates( &[ @@ -2723,44 +3447,75 @@ mod tests { err ); // Missing `publicKey`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKeyAlgorithm": -7, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKeyAlgorithm": -7, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `publicKey`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": null, + "publicKeyAlgorithm": -7, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); + // Base case is valid. + assert!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, "transports": [], - "publicKey": null, - "publicKeyAlgorithm": -7, "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg.0.response.client_data_json + == c_data_json.as_bytes() + && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()] + == att_obj + && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && reg.0.response.transports.is_empty() + && matches!( + reg.0.authenticator_attachment, + AuthenticatorAttachment::None + ) + && reg.0.client_extension_results.cred_props.is_none() + && reg.0.client_extension_results.prf.is_none()) + ); } #[test] fn es384_registration_deserialize_data_mismatch() { @@ -3012,37 +3767,39 @@ mod tests { let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -35, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg.0.response.client_data_json - == c_data_json.as_bytes() - && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()] - == att_obj - && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && reg.0.response.transports.is_empty() - && matches!( - reg.0.authenticator_attachment, - AuthenticatorAttachment::None + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -35, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() ) - && reg.0.client_extension_results.cred_props.is_none() - && reg.0.client_extension_results.prf.is_none())); + .map_or(false, |reg| reg.0.response.client_data_json + == c_data_json.as_bytes() + && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()] + == att_obj + && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && reg.0.response.transports.is_empty() + && matches!( + reg.0.authenticator_attachment, + AuthenticatorAttachment::None + ) + && reg.0.client_extension_results.cred_props.is_none() + && reg.0.client_extension_results.prf.is_none()) + ); // `publicKeyAlgorithm` mismatch. let mut err = Error::invalid_value( Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()), @@ -3074,44 +3831,48 @@ mod tests { err ); // Missing `publicKeyAlgorithm`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `publicKeyAlgorithm`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": null, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": null, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `publicKey` mismatch. let bad_pub_key = P384PubKey::from_encoded_point(&P384Pt::from_affine_coordinates( &[ @@ -3161,24 +3922,26 @@ mod tests { err ); // Missing `publicKey`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKeyAlgorithm": -35, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKeyAlgorithm": -35, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `publicKeyAlgorithm` mismatch when `publicKey` does not exist. err = Error::invalid_value( Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()), @@ -3209,25 +3972,27 @@ mod tests { err ); // `null` `publicKey`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": null, - "publicKeyAlgorithm": -35, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": null, + "publicKeyAlgorithm": -35, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `publicKeyAlgorithm` mismatch when `publicKey` is null. err = Error::invalid_value( Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()), @@ -3258,6 +4023,33 @@ mod tests { .into_bytes()[..err.len()], err ); + // Base case is valid. + assert!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "clientDataJSON": b64_cdata, + "transports": [], + "attestationObject": b64_aobj, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg.0.response.client_data_json + == c_data_json.as_bytes() + && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()] + == att_obj + && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && reg.0.response.transports.is_empty() + && matches!( + reg.0.authenticator_attachment, + AuthenticatorAttachment::None + ) + && reg.0.client_extension_results.cred_props.is_none() + && reg.0.client_extension_results.prf.is_none()) + ); } #[test] fn rs256_registration_deserialize_data_mismatch() { @@ -3720,37 +4512,39 @@ mod tests { let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -257, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .map_or(false, |reg| reg.0.response.client_data_json - == c_data_json.as_bytes() - && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()] - == att_obj - && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && reg.0.response.transports.is_empty() - && matches!( - reg.0.authenticator_attachment, - AuthenticatorAttachment::None + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -257, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() ) - && reg.0.client_extension_results.cred_props.is_none() - && reg.0.client_extension_results.prf.is_none())); + .map_or(false, |reg| reg.0.response.client_data_json + == c_data_json.as_bytes() + && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()] + == att_obj + && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && reg.0.response.transports.is_empty() + && matches!( + reg.0.authenticator_attachment, + AuthenticatorAttachment::None + ) + && reg.0.client_extension_results.cred_props.is_none() + && reg.0.client_extension_results.prf.is_none()) + ); // `publicKeyAlgorithm` mismatch. let mut err = Error::invalid_value( Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()), @@ -3782,44 +4576,48 @@ mod tests { err ); // Missing `publicKeyAlgorithm`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `publicKeyAlgorithm`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": null, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": null, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `publicKey` mismatch. let bad_pub_key = RsaPrivateKey::from_components( BigUint::from_slice( @@ -3913,43 +4711,74 @@ mod tests { err ); // Missing `publicKey`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, - "transports": [], - "publicKeyAlgorithm": -257, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKeyAlgorithm": -257, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); // `null` `publicKey`. - assert!(serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": null, + "publicKeyAlgorithm": -257, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok() + ); + // Base case is valid. + assert!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ "clientDataJSON": b64_cdata, - "authenticatorData": b64_adata, "transports": [], - "publicKey": null, - "publicKeyAlgorithm": -257, "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok()); + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .map_or(false, |reg| reg.0.response.client_data_json + == c_data_json.as_bytes() + && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()] + == att_obj + && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..] + == *Sha256::digest(c_data_json.as_bytes()).as_slice() + && reg.0.response.transports.is_empty() + && matches!( + reg.0.authenticator_attachment, + AuthenticatorAttachment::None + ) + && reg.0.client_extension_results.cred_props.is_none() + && reg.0.client_extension_results.prf.is_none()) + ); } } diff --git a/src/response/ser.rs b/src/response/ser.rs @@ -99,7 +99,10 @@ impl<'de> Deserialize<'de> for AuthenticatorTransport { USB => Ok(AuthenticatorTransport::Usb), _ => Err(E::invalid_value( Unexpected::Str(v), - &format!("'{BLE}', '{HYBRID}', '{INTERNAL}', '{NFC}', '{SMART_CARD}', or '{USB}'").as_str(), + &format!( + "'{BLE}', '{HYBRID}', '{INTERNAL}', '{NFC}', '{SMART_CARD}', or '{USB}'" + ) + .as_str(), )), } } @@ -532,6 +535,34 @@ pub(super) struct PublicKeyCredential<const RELAXED: bool, const REG: bool, Auth /// [`getClientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-getclientextensionresults). pub client_extension_results: Ext, } +/// Deserializes the value for type. +pub(super) struct Type; +impl<'e> Deserialize<'e> for Type { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'e>, + { + /// `Visitor` for `Type`. + struct TypeVisitor; + impl Visitor<'_> for TypeVisitor { + type Value = Type; + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_str(PUBLIC_KEY) + } + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: Error, + { + if v == PUBLIC_KEY { + Ok(Type) + } else { + Err(E::invalid_value(Unexpected::Str(v), &PUBLIC_KEY)) + } + } + } + deserializer.deserialize_str(TypeVisitor) + } +} /// `Visitor` for `PublicKeyCredential`. /// /// When `!RELAXED`, `REG` is ignored and all fields must exist and unknown fields are not allowed. @@ -544,7 +575,7 @@ struct PublicKeyCredentialVisitor<const RELAXED: bool, const REG: bool, R, E>( impl<'d, const REL: bool, const REGI: bool, R, E> Visitor<'d> for PublicKeyCredentialVisitor<REL, REGI, R, E> where - R: for<'a> Deserialize<'a>, + R: Deserialize<'d>, E: for<'a> Deserialize<'a> + ClientExtensions, { type Value = PublicKeyCredential<REL, REGI, R, E>; @@ -586,7 +617,10 @@ where impl<const IGN: bool> Visitor<'_> for FieldVisitor<IGN> { type Value = Field<IGN>; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - write!(formatter, "'{ID}', '{TYPE}', '{RAW_ID}', '{RESPONSE}', '{AUTHENTICATOR_ATTACHMENT}', or '{CLIENT_EXTENSION_RESULTS}'") + write!( + formatter, + "'{ID}', '{TYPE}', '{RAW_ID}', '{RESPONSE}', '{AUTHENTICATOR_ATTACHMENT}', or '{CLIENT_EXTENSION_RESULTS}'" + ) } fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> where @@ -612,34 +646,6 @@ where deserializer.deserialize_identifier(FieldVisitor::<IGNORE>) } } - /// Deserializes the value for type. - struct Type; - impl<'e> Deserialize<'e> for Type { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'e>, - { - /// `Visitor` for `Type`. - struct TypeVisitor; - impl Visitor<'_> for TypeVisitor { - type Value = Type; - fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - formatter.write_str(PUBLIC_KEY) - } - fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> - where - E: Error, - { - if v == PUBLIC_KEY { - Ok(Type) - } else { - Err(E::invalid_value(Unexpected::Str(v), &PUBLIC_KEY)) - } - } - } - deserializer.deserialize_str(TypeVisitor) - } - } let mut opt_id = None; let mut typ = None; let mut raw = None; @@ -791,7 +797,7 @@ const REG_FIELDS: &[&str; 6] = &[ impl<'de, const REL: bool, const REGI: bool, R, E> Deserialize<'de> for PublicKeyCredential<REL, REGI, R, E> where - R: for<'a> Deserialize<'a>, + R: Deserialize<'de>, E: for<'a> Deserialize<'a> + ClientExtensions, { /// Deserializes a `struct` based on @@ -821,7 +827,7 @@ impl Serialize for AllAcceptedCredentialsOptions<'_, '_> { /// # #[cfg(feature = "bin")] /// # use webauthn_rp::bin::Decode; /// # use webauthn_rp::{ - /// # request::{register::UserHandle, AsciiDomain, RpId}, + /// # request::{register::{UserHandle, USER_HANDLE_MIN_LEN}, AsciiDomain, RpId}, /// # response::{error::CredentialIdErr, AllAcceptedCredentialsOptions, CredentialId}, /// # }; /// /// Retrieves the `CredentialId`s associated with `user_id` from the database. @@ -832,9 +838,9 @@ impl Serialize for AllAcceptedCredentialsOptions<'_, '_> { /// } /// /// Retrieves the `UserHandle` from a session cookie. /// # #[cfg(feature = "custom")] - /// fn get_user_handle() -> UserHandle<Vec<u8>> { + /// fn get_user_handle() -> UserHandle<[u8; USER_HANDLE_MIN_LEN]> { /// // ⋮ - /// # UserHandle::try_from(vec![0]).unwrap() + /// # [0].into() /// } /// # #[cfg(feature = "custom")] /// let user_id = get_user_handle(); @@ -881,7 +887,7 @@ impl Serialize for CurrentUserDetailsOptions<'_, '_, '_, '_> { /// # #[cfg(feature = "bin")] /// # use webauthn_rp::bin::Decode; /// # use webauthn_rp::{ - /// # request::{register::{Nickname, PublicKeyCredentialUserEntity, UserHandle, Username}, AsciiDomain, RpId}, + /// # request::{register::{Nickname, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MIN_LEN, Username}, AsciiDomain, RpId}, /// # response::CurrentUserDetailsOptions, /// # AggErr, /// # }; @@ -889,13 +895,13 @@ impl Serialize for CurrentUserDetailsOptions<'_, '_, '_, '_> { /// # #[cfg(feature = "bin")] /// fn get_user_info(user_id: UserHandle<&[u8]>) -> Result<(Username, Option<Nickname>), AggErr> { /// // ⋮ - /// # Ok((Username::decode("foo".to_owned()).unwrap(), Some(Nickname::decode("foo".to_owned()).unwrap()))) + /// # Ok((Username::decode("foo").unwrap(), Some(Nickname::decode("foo").unwrap()))) /// } /// /// Retrieves the `UserHandle` from a session cookie. /// # #[cfg(feature = "custom")] - /// fn get_user_handle() -> UserHandle<Vec<u8>> { + /// fn get_user_handle() -> UserHandle<[u8; USER_HANDLE_MIN_LEN]> { /// // ⋮ - /// # UserHandle::try_from(vec![0]).unwrap() + /// # [0].into() /// } /// # #[cfg(feature = "custom")] /// let user_handle = get_user_handle();