webauthn_rp

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

auth.rs (23202B)


      1 #[cfg(all(doc, feature = "serde_relaxed"))]
      2 use super::super::request::FixedCapHashSet;
      3 #[cfg(doc)]
      4 use super::super::{
      5     request::{
      6         auth::{AuthenticationServerState, PublicKeyCredentialRequestOptions},
      7         Challenge,
      8     },
      9     AuthenticatedCredential, RegisteredCredential, StaticState,
     10 };
     11 #[cfg(feature = "serde_relaxed")]
     12 use super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr};
     13 use super::{
     14     super::UserHandle,
     15     auth::error::{AuthenticatorDataErr, AuthenticatorExtensionOutputErr},
     16     cbor,
     17     error::CollectedClientDataErr,
     18     register::CompressedPubKey,
     19     AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthenticatorAttachment,
     20     CborSuccess, ClientDataJsonParser as _, CollectedClientData, CredentialId, Flag, FromCbor,
     21     LimitedVerificationParser, ParsedAuthData, Response, SentChallenge,
     22 };
     23 use core::convert::Infallible;
     24 use ed25519_dalek::{Signature, Verifier as _};
     25 use p256::ecdsa::DerSignature as P256DerSig;
     26 use p384::ecdsa::DerSignature as P384DerSig;
     27 use rsa::{
     28     pkcs1v15,
     29     sha2::{digest::Digest as _, Sha256},
     30 };
     31 /// Contains error types.
     32 pub mod error;
     33 /// Contains functionality to deserialize data from a client.
     34 #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
     35 #[cfg(feature = "serde")]
     36 pub(super) mod ser;
     37 /// Contains functionality to deserialize data from a client in a "relaxed" way.
     38 #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
     39 #[cfg(feature = "serde_relaxed")]
     40 pub mod ser_relaxed;
     41 /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension).
     42 #[derive(Clone, Copy, Debug)]
     43 pub enum HmacSecret {
     44     /// No `hmac-secret` response.
     45     None,
     46     /// One encrypted `hmac-secret`.
     47     One,
     48     /// Two encrypted `hmac-secret`s.
     49     Two,
     50 }
     51 impl FromCbor<'_> for HmacSecret {
     52     type Err = AuthenticatorExtensionOutputErr;
     53     fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
     54         /// AES block size.
     55         const AES_BLOCK_SIZE: usize = 16;
     56         /// HMAC-SHA-256 output length.
     57         const HMAC_SHA_256_LEN: usize = 32;
     58         /// Length of two HMAC-SHA-256 outputs concatenated together.
     59         const TWO_HMAC_SHA_256_LEN: usize = HMAC_SHA_256_LEN << 1;
     60         // We need the smallest multiple of `AES_BLOCK_SIZE` that
     61         // is strictly greater than `HMAC_SHA_256_LEN`.
     62         /// AES-256 output length on a 32-byte input.
     63         #[expect(
     64             clippy::integer_division_remainder_used,
     65             reason = "doesn't need to be constant time"
     66         )]
     67         const ONE_SECRET_LEN: usize =
     68             HMAC_SHA_256_LEN + (AES_BLOCK_SIZE - (HMAC_SHA_256_LEN % AES_BLOCK_SIZE));
     69         // We need the smallest multiple of `AES_BLOCK_SIZE` that
     70         // is strictly greater than `TWO_HMAC_SHA_256_LEN`.
     71         /// AES-256 output length on a 64-byte input.
     72         #[expect(
     73             clippy::integer_division_remainder_used,
     74             reason = "doesn't need to be constant time"
     75         )]
     76         const TWO_SECRET_LEN: usize =
     77             TWO_HMAC_SHA_256_LEN + (AES_BLOCK_SIZE - (TWO_HMAC_SHA_256_LEN % AES_BLOCK_SIZE));
     78         cbor.split_at_checked(cbor::HMAC_SECRET.len())
     79             .ok_or(AuthenticatorExtensionOutputErr::Len)
     80             .and_then(|(key, key_rem)| {
     81                 if key == cbor::HMAC_SECRET {
     82                     key_rem
     83                         .split_first()
     84                         .ok_or(AuthenticatorExtensionOutputErr::Len)
     85                         .and_then(|(bytes, bytes_rem)| {
     86                             if *bytes == cbor::BYTES_INFO_24 {
     87                                 bytes_rem
     88                                     .split_first()
     89                                     .ok_or(AuthenticatorExtensionOutputErr::Len)
     90                                     .and_then(|(&len, len_rem)| {
     91                                         len_rem.split_at_checked(usize::from(len)).ok_or(AuthenticatorExtensionOutputErr::Len).and_then(|(_, remaining)| {
     92                                             match usize::from(len) {
     93                                                 ONE_SECRET_LEN => {
     94                                                     Ok(CborSuccess {
     95                                                         value: Self::One,
     96                                                         remaining,
     97                                                     })
     98                                                 }
     99                                                 TWO_SECRET_LEN => {
    100                                                     Ok(CborSuccess {
    101                                                         value: Self::Two,
    102                                                         remaining,
    103                                                     })
    104                                                 }
    105                                                 _ => Err(AuthenticatorExtensionOutputErr::HmacSecretValue),
    106                                             }
    107                                         })
    108                                     })
    109                             } else {
    110                                 Err(AuthenticatorExtensionOutputErr::HmacSecretType)
    111                             }
    112                         })
    113                 } else {
    114                     Err(AuthenticatorExtensionOutputErr::Unsupported)
    115                 }
    116             })
    117     }
    118 }
    119 /// [Authenticator extension output](https://www.w3.org/TR/webauthn-3/#authenticator-extension-output).
    120 #[derive(Clone, Copy, Debug)]
    121 pub struct AuthenticatorExtensionOutput {
    122     /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension).
    123     pub hmac_secret: HmacSecret,
    124 }
    125 impl AuthExtOutput for AuthenticatorExtensionOutput {
    126     fn missing(self) -> bool {
    127         matches!(self.hmac_secret, HmacSecret::None)
    128     }
    129 }
    130 impl FromCbor<'_> for AuthenticatorExtensionOutput {
    131     type Err = AuthenticatorExtensionOutputErr;
    132     fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
    133         // We don't allow unsupported extensions; thus the only possibilities is any ordered element of
    134         // the power set of {"hmac-secret":<HmacSecret>}.
    135         cbor.split_first().map_or_else(
    136             || {
    137                 Ok(CborSuccess {
    138                     value: Self {
    139                         hmac_secret: HmacSecret::None,
    140                     },
    141                     remaining: cbor,
    142                 })
    143             },
    144             |(map, map_rem)| {
    145                 if *map == cbor::MAP_1 {
    146                     HmacSecret::from_cbor(map_rem).map(|success| CborSuccess {
    147                         value: Self {
    148                             hmac_secret: success.value,
    149                         },
    150                         remaining: success.remaining,
    151                     })
    152                 } else {
    153                     Err(AuthenticatorExtensionOutputErr::CborHeader)
    154                 }
    155             },
    156         )
    157     }
    158 }
    159 /// Unit type for `AuthData::CredData`.
    160 pub(crate) struct NoCred;
    161 impl FromCbor<'_> for NoCred {
    162     type Err = Infallible;
    163     fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
    164         Ok(CborSuccess {
    165             value: Self,
    166             remaining: cbor,
    167         })
    168     }
    169 }
    170 /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data).
    171 #[derive(Clone, Copy, Debug)]
    172 pub struct AuthenticatorData<'a> {
    173     /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash).
    174     rp_id_hash: &'a [u8],
    175     /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags).
    176     flags: Flag,
    177     /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount).
    178     sign_count: u32,
    179     /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions).
    180     extensions: AuthenticatorExtensionOutput,
    181 }
    182 impl<'a> AuthenticatorData<'a> {
    183     /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash).
    184     #[inline]
    185     #[must_use]
    186     pub const fn rp_id_hash(&self) -> &'a [u8] {
    187         self.rp_id_hash
    188     }
    189     /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags).
    190     #[inline]
    191     #[must_use]
    192     pub const fn flags(&self) -> Flag {
    193         self.flags
    194     }
    195     /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount).
    196     #[inline]
    197     #[must_use]
    198     pub const fn sign_count(&self) -> u32 {
    199         self.sign_count
    200     }
    201     /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions).
    202     #[inline]
    203     #[must_use]
    204     pub const fn extensions(&self) -> AuthenticatorExtensionOutput {
    205         self.extensions
    206     }
    207 }
    208 impl<'a> AuthData<'a> for AuthenticatorData<'a> {
    209     type UpBitErr = ();
    210     type CredData = NoCred;
    211     type Ext = AuthenticatorExtensionOutput;
    212     fn contains_at_bit() -> bool {
    213         false
    214     }
    215     fn user_is_not_present() -> Result<(), Self::UpBitErr> {
    216         Err(())
    217     }
    218     fn new(
    219         rp_id_hash: &'a [u8],
    220         flags: Flag,
    221         sign_count: u32,
    222         _: Self::CredData,
    223         extensions: Self::Ext,
    224     ) -> Self {
    225         Self {
    226             rp_id_hash,
    227             flags,
    228             sign_count,
    229             extensions,
    230         }
    231     }
    232     fn rp_hash(&self) -> &'a [u8] {
    233         self.rp_id_hash
    234     }
    235     fn flag(&self) -> Flag {
    236         self.flags
    237     }
    238 }
    239 impl<'a> AuthDataContainer<'a> for AuthenticatorData<'a> {
    240     type Auth = Self;
    241     type Err = AuthenticatorDataErr;
    242     #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
    243     #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")]
    244     fn from_data(data: &'a [u8]) -> Result<ParsedAuthData<'a, Self>, Self::Err> {
    245         // `data.len().checked_sub(Sha256::output_size()).unwrap()` is less than `data.len()`,
    246         // so indexing is fine.
    247         Self::try_from(&data[..data.len().checked_sub(Sha256::output_size()).unwrap_or_else(|| unreachable!("AuthenticatorData::from_data must be passed a slice with 32 bytes of trailing data"))]).map(|auth_data| ParsedAuthData { data: auth_data, auth_data_and_32_trailing_bytes: data, })
    248     }
    249     fn authenticator_data(&self) -> &Self::Auth {
    250         self
    251     }
    252 }
    253 impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AuthenticatorData<'b> {
    254     type Error = AuthenticatorDataErr;
    255     /// Deserializes `value` based on the
    256     /// [authenticator data structure](https://www.w3.org/TR/webauthn-3/#table-authData).
    257     #[expect(
    258         clippy::panic_in_result_fn,
    259         reason = "we want to crash when there is a bug"
    260     )]
    261     #[inline]
    262     fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
    263         Self::from_cbor(value)
    264             .map_err(AuthenticatorDataErr::from)
    265             .map(|auth_data| {
    266                 assert!(
    267                     auth_data.remaining.is_empty(),
    268                     "there is a bug in AuthenticatorData::from_cbor"
    269                 );
    270                 auth_data.value
    271             })
    272     }
    273 }
    274 /// [`AuthenticatorAssertionResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse).
    275 #[derive(Debug)]
    276 pub struct AuthenticatorAssertion {
    277     /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson).
    278     client_data_json: Vec<u8>,
    279     /// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata)
    280     /// followed by the SHA-256 hash of [`Self::client_data_json`].
    281     authenticator_data_and_c_data_hash: Vec<u8>,
    282     /// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature).
    283     signature: Vec<u8>,
    284     /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle).
    285     user_handle: Option<UserHandle<Vec<u8>>>,
    286 }
    287 impl AuthenticatorAssertion {
    288     /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson).
    289     #[inline]
    290     #[must_use]
    291     pub fn client_data_json(&self) -> &[u8] {
    292         self.client_data_json.as_slice()
    293     }
    294     /// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata).
    295     #[expect(
    296         clippy::arithmetic_side_effects,
    297         clippy::indexing_slicing,
    298         reason = "comment justifies their correctness"
    299     )]
    300     #[inline]
    301     #[must_use]
    302     pub fn authenticator_data(&self) -> &[u8] {
    303         // We only allow creation via [`Self::new`] which creates [`Self::authenticator_data_and_c_data_hash`]
    304         // by appending the SHA-256 hash of [`Self::client_data_json`] to the authenticator data that was passed;
    305         // thus indexing is fine and subtraction won't cause underflow.
    306         &self.authenticator_data_and_c_data_hash
    307             [..self.authenticator_data_and_c_data_hash.len() - Sha256::output_size()]
    308     }
    309     /// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature).
    310     #[inline]
    311     #[must_use]
    312     pub fn signature(&self) -> &[u8] {
    313         self.signature.as_slice()
    314     }
    315     /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle).
    316     #[inline]
    317     #[must_use]
    318     pub fn user_handle(&self) -> Option<UserHandle<&[u8]>> {
    319         self.user_handle.as_ref().map(UserHandle::from)
    320     }
    321     /// Constructs an instance of `Self` with the contained data.
    322     ///
    323     /// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes
    324     /// of available capacity; if not, a reallocation will occur.
    325     #[inline]
    326     #[must_use]
    327     pub fn new(
    328         client_data_json: Vec<u8>,
    329         mut authenticator_data: Vec<u8>,
    330         signature: Vec<u8>,
    331         user_handle: Option<UserHandle<Vec<u8>>>,
    332     ) -> Self {
    333         authenticator_data
    334             .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
    335         Self {
    336             client_data_json,
    337             authenticator_data_and_c_data_hash: authenticator_data,
    338             signature,
    339             user_handle,
    340         }
    341     }
    342 }
    343 impl AuthResponse for AuthenticatorAssertion {
    344     type Auth<'a>
    345         = AuthenticatorData<'a>
    346     where
    347         Self: 'a;
    348     type CredKey<'a> = CompressedPubKey<&'a [u8], &'a [u8], &'a [u8], &'a [u8]>;
    349     fn parse_data_and_verify_sig(
    350         &self,
    351         key: Self::CredKey<'_>,
    352         relaxed: bool,
    353     ) -> Result<
    354         (CollectedClientData<'_>, Self::Auth<'_>),
    355         AuthRespErr<<Self::Auth<'_> as AuthDataContainer<'_>>::Err>,
    356     > {
    357         /// Always `panic`s.
    358         #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
    359         #[cfg(not(feature = "serde_relaxed"))]
    360         fn get_client_collected_data(_: &[u8]) -> ! {
    361             unreachable!("AuthenticatorAssertion::parse_data_and_verify_sig must be passed false when serde_relaxed is not enabled");
    362         }
    363         /// Parses `data` using `CollectedClientData::from_client_data_json_relaxed::<false>`.
    364         #[cfg(feature = "serde_relaxed")]
    365         fn get_client_collected_data(
    366             data: &[u8],
    367         ) -> Result<
    368             CollectedClientData<'_>,
    369             AuthRespErr<
    370                 <<AuthenticatorAssertion as AuthResponse>::Auth<'_> as AuthDataContainer<'_>>::Err,
    371             >,
    372         > {
    373             CollectedClientData::from_client_data_json_relaxed::<false>(data)
    374                 .map_err(AuthRespErr::CollectedClientDataRelaxed)
    375         }
    376         if relaxed {
    377             get_client_collected_data(self.client_data_json.as_slice())
    378         } else {
    379             CollectedClientData::from_client_data_json::<false>(self.client_data_json.as_slice())
    380                 .map_err(AuthRespErr::CollectedClientData)
    381         }
    382         .and_then(|client_data_json| {
    383             Self::Auth::from_data(self.authenticator_data_and_c_data_hash.as_slice())
    384                 .map_err(AuthRespErr::Auth)
    385                 .and_then(|val| {
    386                     match key {
    387                         CompressedPubKey::Ed25519(k) => k
    388                             .into_ver_key()
    389                             .map_err(AuthRespErr::PubKey)
    390                             .and_then(|ver_key| {
    391                                 Signature::from_slice(self.signature.as_slice())
    392                                     .and_then(|sig| {
    393                                         ver_key.verify(
    394                                             self.authenticator_data_and_c_data_hash.as_slice(),
    395                                             &sig,
    396                                         )
    397                                     })
    398                                     .map_err(|_e| AuthRespErr::Signature)
    399                             }),
    400                         CompressedPubKey::P256(k) => k
    401                             .into_ver_key()
    402                             .map_err(AuthRespErr::PubKey)
    403                             .and_then(|ver_key| {
    404                                 P256DerSig::from_bytes(self.signature.as_slice())
    405                                     .and_then(|sig| {
    406                                         ver_key.verify(
    407                                             self.authenticator_data_and_c_data_hash.as_slice(),
    408                                             &sig,
    409                                         )
    410                                     })
    411                                     .map_err(|_e| AuthRespErr::Signature)
    412                             }),
    413                         CompressedPubKey::P384(k) => k
    414                             .into_ver_key()
    415                             .map_err(AuthRespErr::PubKey)
    416                             .and_then(|ver_key| {
    417                                 P384DerSig::from_bytes(self.signature.as_slice())
    418                                     .and_then(|sig| {
    419                                         ver_key.verify(
    420                                             self.authenticator_data_and_c_data_hash.as_slice(),
    421                                             &sig,
    422                                         )
    423                                     })
    424                                     .map_err(|_e| AuthRespErr::Signature)
    425                             }),
    426                         CompressedPubKey::Rsa(k) => k
    427                             .as_ver_key()
    428                             .map_err(AuthRespErr::PubKey)
    429                             .and_then(|ver_key| {
    430                                 pkcs1v15::Signature::try_from(self.signature.as_slice())
    431                                     .and_then(|sig| {
    432                                         ver_key.verify(
    433                                             self.authenticator_data_and_c_data_hash.as_slice(),
    434                                             &sig,
    435                                         )
    436                                     })
    437                                     .map_err(|_e| AuthRespErr::Signature)
    438                             }),
    439                     }
    440                     .map(|()| (client_data_json, val.data))
    441                 })
    442         })
    443     }
    444 }
    445 /// [`PublicKeyCredential`](https://www.w3.org/TR/webauthn-3/#iface-pkcredential) for authentication ceremonies.
    446 #[expect(
    447     clippy::field_scoped_visibility_modifiers,
    448     reason = "no invariants to uphold"
    449 )]
    450 #[derive(Debug)]
    451 pub struct Authentication {
    452     /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid).
    453     pub(crate) raw_id: CredentialId<Vec<u8>>,
    454     /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response)
    455     pub(crate) response: AuthenticatorAssertion,
    456     /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
    457     pub(crate) authenticator_attachment: AuthenticatorAttachment,
    458 }
    459 impl Authentication {
    460     /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid).
    461     #[inline]
    462     #[must_use]
    463     pub fn raw_id(&self) -> CredentialId<&[u8]> {
    464         (&self.raw_id).into()
    465     }
    466     /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response).
    467     #[inline]
    468     #[must_use]
    469     pub const fn response(&self) -> &AuthenticatorAssertion {
    470         &self.response
    471     }
    472     /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
    473     #[inline]
    474     #[must_use]
    475     pub const fn authenticator_attachment(&self) -> AuthenticatorAttachment {
    476         self.authenticator_attachment
    477     }
    478     /// Constructs an `Authentication`.
    479     #[cfg_attr(docsrs, doc(cfg(feature = "custom")))]
    480     #[cfg(feature = "custom")]
    481     #[inline]
    482     #[must_use]
    483     pub const fn new(
    484         raw_id: CredentialId<Vec<u8>>,
    485         response: AuthenticatorAssertion,
    486         authenticator_attachment: AuthenticatorAttachment,
    487     ) -> Self {
    488         Self {
    489             raw_id,
    490             response,
    491             authenticator_attachment,
    492         }
    493     }
    494     /// Returns the associated `SentChallenge`.
    495     ///
    496     /// This is useful when wanting to extract the corresponding [`AuthenticationServerState`] from
    497     /// an in-memory collection (e.g., [`FixedCapHashSet`]) or storage.
    498     ///
    499     /// Note if [`CollectedClientData::from_client_data_json`] returns `Ok`, then this will return `Ok`
    500     /// containing the same value as [`CollectedClientData::challenge`]; however the converse is _not_ true.
    501     /// This is because this function parses the minimal amount of data possible.
    502     ///
    503     /// # Errors
    504     ///
    505     /// Errors iff [`AuthenticatorAssertion::client_data_json`] does not contain a base64url-encoded
    506     /// [`Challenge`] in the required position.
    507     #[inline]
    508     pub fn challenge(&self) -> Result<SentChallenge, CollectedClientDataErr> {
    509         LimitedVerificationParser::<false>::get_sent_challenge(
    510             self.response.client_data_json.as_slice(),
    511         )
    512     }
    513     /// Returns the associated `SentChallenge`.
    514     ///
    515     /// This is useful when wanting to extract the corresponding [`AuthenticationServerState`] from
    516     /// an in-memory collection (e.g., [`FixedCapHashSet`]) or storage.
    517     ///
    518     /// Note if [`CollectedClientData::from_client_data_json_relaxed`] returns `Ok`, then this will return
    519     /// `Ok` containing the same value as [`CollectedClientData::challenge`]; however the converse
    520     /// is _not_ true. This is because this function attempts to reduce the amount of data parsed.
    521     ///
    522     /// # Errors
    523     ///
    524     /// Errors iff [`AuthenticatorAssertion::client_data_json`] is invalid JSON _after_ ignoring
    525     /// a leading U+FEFF and replacing any sequences of invalid UTF-8 code units with U+FFFD or
    526     /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge) does not exist
    527     /// or is not a base64url-encoded [`Challenge`].
    528     #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
    529     #[cfg(feature = "serde_relaxed")]
    530     #[inline]
    531     pub fn challenge_relaxed(&self) -> Result<SentChallenge, SerdeJsonErr> {
    532         RelaxedClientDataJsonParser::<false>::get_sent_challenge(
    533             self.response.client_data_json.as_slice(),
    534         )
    535     }
    536 }
    537 impl Response for Authentication {
    538     type Auth = AuthenticatorAssertion;
    539     fn auth(&self) -> &Self::Auth {
    540         &self.response
    541     }
    542 }