webauthn_rp

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

auth.rs (30033B)


      1 #[cfg(feature = "serde_relaxed")]
      2 use self::{
      3     super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr},
      4     ser_relaxed::{AuthenticationRelaxed, CustomAuthentication},
      5 };
      6 #[cfg(doc)]
      7 use super::super::{
      8     AuthenticatedCredential, RegisteredCredential, StaticState,
      9     hash::hash_set::MaxLenHashSet,
     10     request::{
     11         Challenge,
     12         auth::{
     13             CredentialSpecificExtension, DiscoverableAuthenticationServerState, Extension,
     14             NonDiscoverableAuthenticationServerState, PublicKeyCredentialRequestOptions,
     15         },
     16         register::{self, UserHandle16, UserHandle64},
     17     },
     18 };
     19 use super::{
     20     super::{UserHandle, request::register::USER_HANDLE_MAX_LEN},
     21     AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthenticatorAttachment,
     22     CborSuccess, ClientDataJsonParser as _, CollectedClientData, CredentialId, Flag, FromCbor,
     23     HmacSecretGet, HmacSecretGetErr, LimitedVerificationParser, ParsedAuthData, Response,
     24     SentChallenge,
     25     auth::error::{AuthenticatorDataErr, AuthenticatorExtensionOutputErr, MissingUserHandleErr},
     26     cbor,
     27     error::CollectedClientDataErr,
     28     register::CompressedPubKey,
     29 };
     30 use core::convert::Infallible;
     31 use ed25519_dalek::{Signature, Verifier as _};
     32 use p256::ecdsa::DerSignature as P256DerSig;
     33 use p384::ecdsa::DerSignature as P384DerSig;
     34 use rsa::{
     35     pkcs1v15,
     36     sha2::{Sha256, digest::Digest as _},
     37 };
     38 #[cfg(feature = "serde_relaxed")]
     39 use serde::Deserialize;
     40 /// Contains error types.
     41 pub mod error;
     42 /// Contains functionality to deserialize data from a client.
     43 #[cfg(feature = "serde")]
     44 pub(super) mod ser;
     45 /// Contains functionality to deserialize data from a client in a "relaxed" way.
     46 #[cfg(feature = "serde_relaxed")]
     47 pub mod ser_relaxed;
     48 /// [`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).
     49 ///
     50 /// This is only relevant when [`Extension::prf`] or [`CredentialSpecificExtension::prf`] is `Some` and for
     51 /// authenticators that implement [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) on top of
     52 /// `hmac-secret`.
     53 ///
     54 /// Note while many authenticators that implement `prf` don't require `prf` to have been sent during registration
     55 /// (i.e., [`register::Extension::prf`]), it is recommended to do so for those authenticators that do require it.
     56 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     57 pub enum HmacSecret {
     58     /// No `hmac-secret` response.
     59     ///
     60     /// Either [`Extension::prf`] was not sent, the credential is not PRF-capable, or the authenticator does not use
     61     /// the
     62     /// [`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)
     63     /// extension.
     64     None,
     65     /// One encrypted `hmac-secret`.
     66     ///
     67     /// [`Extension::prf`] was sent with one PRF input for a PRF-capable credential whose authenticator implements
     68     /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) on top of the
     69     /// [`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)
     70     /// extension.
     71     One,
     72     /// Two encrypted `hmac-secret`s.
     73     ///
     74     /// [`Extension::prf`] was sent with two PRF inputs for a PRF-capable credential whose authenticator implements
     75     /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) on top of the
     76     /// [`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)
     77     /// extension.
     78     Two,
     79 }
     80 /// [Authenticator extension output](https://www.w3.org/TR/webauthn-3/#authenticator-extension-output).
     81 #[derive(Clone, Copy, Debug)]
     82 pub struct AuthenticatorExtensionOutput {
     83     /// [`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).
     84     pub hmac_secret: HmacSecret,
     85 }
     86 impl AuthExtOutput for AuthenticatorExtensionOutput {
     87     fn missing(self) -> bool {
     88         matches!(self.hmac_secret, HmacSecret::None)
     89     }
     90 }
     91 impl From<HmacSecretGetErr> for AuthenticatorExtensionOutputErr {
     92     #[inline]
     93     fn from(value: HmacSecretGetErr) -> Self {
     94         match value {
     95             HmacSecretGetErr::Len => Self::Len,
     96             HmacSecretGetErr::Type => Self::HmacSecretType,
     97             HmacSecretGetErr::Value => Self::HmacSecretValue,
     98         }
     99     }
    100 }
    101 impl From<HmacSecretGet<false>> for HmacSecret {
    102     #[inline]
    103     fn from(value: HmacSecretGet<false>) -> Self {
    104         match value {
    105             HmacSecretGet::None => Self::None,
    106             HmacSecretGet::One => Self::One,
    107             HmacSecretGet::Two => Self::Two,
    108         }
    109     }
    110 }
    111 impl FromCbor<'_> for AuthenticatorExtensionOutput {
    112     type Err = AuthenticatorExtensionOutputErr;
    113     fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
    114         // We don't allow unsupported extensions; thus the only possibilities is any ordered element of
    115         // the power set of {"hmac-secret":<HmacSecret>}.
    116         cbor.split_first().map_or_else(
    117             || {
    118                 Ok(CborSuccess {
    119                     value: Self {
    120                         hmac_secret: HmacSecret::None,
    121                     },
    122                     remaining: cbor,
    123                 })
    124             },
    125             |(map, map_rem)| {
    126                 if *map == cbor::MAP_1 {
    127                     HmacSecretGet::from_cbor(map_rem)
    128                         .map_err(AuthenticatorExtensionOutputErr::from)
    129                         .map(|success| CborSuccess {
    130                             value: Self {
    131                                 hmac_secret: success.value.into(),
    132                             },
    133                             remaining: success.remaining,
    134                         })
    135                 } else {
    136                     Err(AuthenticatorExtensionOutputErr::CborHeader)
    137                 }
    138             },
    139         )
    140     }
    141 }
    142 /// Unit type for `AuthData::CredData`.
    143 pub(crate) struct NoCred;
    144 impl FromCbor<'_> for NoCred {
    145     type Err = Infallible;
    146     fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
    147         Ok(CborSuccess {
    148             value: Self,
    149             remaining: cbor,
    150         })
    151     }
    152 }
    153 /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data).
    154 #[derive(Clone, Copy, Debug)]
    155 pub struct AuthenticatorData<'a> {
    156     /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash).
    157     rp_id_hash: &'a [u8],
    158     /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags).
    159     flags: Flag,
    160     /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount).
    161     sign_count: u32,
    162     /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions).
    163     extensions: AuthenticatorExtensionOutput,
    164 }
    165 impl<'a> AuthenticatorData<'a> {
    166     /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash).
    167     #[inline]
    168     #[must_use]
    169     pub const fn rp_id_hash(&self) -> &'a [u8] {
    170         self.rp_id_hash
    171     }
    172     /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags).
    173     #[inline]
    174     #[must_use]
    175     pub const fn flags(&self) -> Flag {
    176         self.flags
    177     }
    178     /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount).
    179     #[inline]
    180     #[must_use]
    181     pub const fn sign_count(&self) -> u32 {
    182         self.sign_count
    183     }
    184     /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions).
    185     #[inline]
    186     #[must_use]
    187     pub const fn extensions(&self) -> AuthenticatorExtensionOutput {
    188         self.extensions
    189     }
    190 }
    191 impl<'a> AuthData<'a> for AuthenticatorData<'a> {
    192     type UpBitErr = ();
    193     type CredData = NoCred;
    194     type Ext = AuthenticatorExtensionOutput;
    195     fn contains_at_bit() -> bool {
    196         false
    197     }
    198     fn user_is_not_present() -> Result<(), Self::UpBitErr> {
    199         Err(())
    200     }
    201     fn new(
    202         rp_id_hash: &'a [u8],
    203         flags: Flag,
    204         sign_count: u32,
    205         _: Self::CredData,
    206         extensions: Self::Ext,
    207     ) -> Self {
    208         Self {
    209             rp_id_hash,
    210             flags,
    211             sign_count,
    212             extensions,
    213         }
    214     }
    215     fn rp_hash(&self) -> &'a [u8] {
    216         self.rp_id_hash
    217     }
    218     fn flag(&self) -> Flag {
    219         self.flags
    220     }
    221 }
    222 impl<'a> AuthDataContainer<'a> for AuthenticatorData<'a> {
    223     type Auth = Self;
    224     type Err = AuthenticatorDataErr;
    225     #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
    226     #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")]
    227     fn from_data(data: &'a [u8]) -> Result<ParsedAuthData<'a, Self>, Self::Err> {
    228         // `data.len().checked_sub(Sha256::output_size()).unwrap()` is less than `data.len()`,
    229         // so indexing is fine.
    230         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, })
    231     }
    232     fn authenticator_data(&self) -> &Self::Auth {
    233         self
    234     }
    235 }
    236 impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AuthenticatorData<'b> {
    237     type Error = AuthenticatorDataErr;
    238     /// Deserializes `value` based on the
    239     /// [authenticator data structure](https://www.w3.org/TR/webauthn-3/#table-authData).
    240     #[expect(
    241         clippy::panic_in_result_fn,
    242         reason = "we want to crash when there is a bug"
    243     )]
    244     #[inline]
    245     fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
    246         Self::from_cbor(value)
    247             .map_err(AuthenticatorDataErr::from)
    248             .map(|auth_data| {
    249                 assert!(
    250                     auth_data.remaining.is_empty(),
    251                     "there is a bug in AuthenticatorData::from_cbor"
    252                 );
    253                 auth_data.value
    254             })
    255     }
    256 }
    257 /// [`AuthenticatorAssertionResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse).
    258 #[derive(Debug)]
    259 pub struct AuthenticatorAssertion<const USER_LEN: usize, const DISCOVERABLE: bool> {
    260     /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson).
    261     client_data_json: Vec<u8>,
    262     /// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata)
    263     /// followed by the SHA-256 hash of [`Self::client_data_json`].
    264     authenticator_data_and_c_data_hash: Vec<u8>,
    265     /// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature).
    266     signature: Vec<u8>,
    267     /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle).
    268     user_handle: Option<UserHandle<USER_LEN>>,
    269 }
    270 impl<const USER_LEN: usize, const DISCOVERABLE: bool>
    271     AuthenticatorAssertion<USER_LEN, DISCOVERABLE>
    272 {
    273     /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson).
    274     #[inline]
    275     #[must_use]
    276     pub const fn client_data_json(&self) -> &[u8] {
    277         self.client_data_json.as_slice()
    278     }
    279     /// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata).
    280     #[expect(
    281         clippy::arithmetic_side_effects,
    282         clippy::indexing_slicing,
    283         reason = "comment justifies their correctness"
    284     )]
    285     #[inline]
    286     #[must_use]
    287     pub fn authenticator_data(&self) -> &[u8] {
    288         // We only allow creation via [`Self::new`] which creates [`Self::authenticator_data_and_c_data_hash`]
    289         // by appending the SHA-256 hash of [`Self::client_data_json`] to the authenticator data that was passed;
    290         // thus indexing is fine and subtraction won't cause underflow.
    291         &self.authenticator_data_and_c_data_hash
    292             [..self.authenticator_data_and_c_data_hash.len() - Sha256::output_size()]
    293     }
    294     /// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature).
    295     #[inline]
    296     #[must_use]
    297     pub const fn signature(&self) -> &[u8] {
    298         self.signature.as_slice()
    299     }
    300     /// Constructs an instance of `Self` with the contained data.
    301     ///
    302     /// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes
    303     /// of available capacity; if not, a reallocation will occur.
    304     fn new_inner(
    305         client_data_json: Vec<u8>,
    306         mut authenticator_data: Vec<u8>,
    307         signature: Vec<u8>,
    308         user_handle: Option<UserHandle<USER_LEN>>,
    309     ) -> Self {
    310         authenticator_data.extend_from_slice(&Sha256::digest(client_data_json.as_slice()));
    311         Self {
    312             client_data_json,
    313             authenticator_data_and_c_data_hash: authenticator_data,
    314             signature,
    315             user_handle,
    316         }
    317     }
    318 }
    319 impl<const USER_LEN: usize> AuthenticatorAssertion<USER_LEN, false> {
    320     /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle).
    321     #[inline]
    322     #[must_use]
    323     pub const fn user_handle(&self) -> Option<&UserHandle<USER_LEN>> {
    324         self.user_handle.as_ref()
    325     }
    326     /// Constructs an instance of `Self` with the contained data.
    327     ///
    328     /// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes
    329     /// of available capacity; if not, a reallocation will occur.
    330     #[inline]
    331     #[must_use]
    332     pub fn with_optional_user(
    333         client_data_json: Vec<u8>,
    334         authenticator_data: Vec<u8>,
    335         signature: Vec<u8>,
    336         user_handle: Option<UserHandle<USER_LEN>>,
    337     ) -> Self {
    338         Self::new_inner(client_data_json, authenticator_data, signature, user_handle)
    339     }
    340     /// Same as [`Self::with_optional_user`] with `None` used for `user_handle`.
    341     #[inline]
    342     #[must_use]
    343     pub fn without_user(
    344         client_data_json: Vec<u8>,
    345         authenticator_data: Vec<u8>,
    346         signature: Vec<u8>,
    347     ) -> Self {
    348         Self::with_optional_user(client_data_json, authenticator_data, signature, None)
    349     }
    350     /// Same as [`Self::with_optional_user`] with `Some(user_handle)` used for `user_handle`.
    351     #[inline]
    352     #[must_use]
    353     pub fn with_user(
    354         client_data_json: Vec<u8>,
    355         authenticator_data: Vec<u8>,
    356         signature: Vec<u8>,
    357         user_handle: UserHandle<USER_LEN>,
    358     ) -> Self {
    359         Self::with_optional_user(
    360             client_data_json,
    361             authenticator_data,
    362             signature,
    363             Some(user_handle),
    364         )
    365     }
    366 }
    367 impl<const USER_LEN: usize> AuthenticatorAssertion<USER_LEN, true> {
    368     /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle).
    369     #[expect(clippy::unreachable, reason = "want to crash when there is a bug")]
    370     #[inline]
    371     #[must_use]
    372     pub fn user_handle(&self) -> &UserHandle<USER_LEN> {
    373         self.user_handle
    374             .as_ref()
    375             .unwrap_or_else(|| unreachable!("bug in AuthenticatorAssertion<USER_LEN, true>"))
    376     }
    377     /// Constructs an instance of `Self` with the contained data.
    378     ///
    379     /// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes
    380     /// of available capacity; if not, a reallocation will occur.
    381     #[inline]
    382     #[must_use]
    383     pub fn new(
    384         client_data_json: Vec<u8>,
    385         authenticator_data: Vec<u8>,
    386         signature: Vec<u8>,
    387         user_handle: UserHandle<USER_LEN>,
    388     ) -> Self {
    389         Self::new_inner(
    390             client_data_json,
    391             authenticator_data,
    392             signature,
    393             Some(user_handle),
    394         )
    395     }
    396 }
    397 impl<const USER_LEN: usize, const DISCOVERABLE: bool> AuthResponse
    398     for AuthenticatorAssertion<USER_LEN, DISCOVERABLE>
    399 {
    400     type Auth<'a>
    401         = AuthenticatorData<'a>
    402     where
    403         Self: 'a;
    404     type CredKey<'a> = CompressedPubKey<&'a [u8], &'a [u8], &'a [u8], &'a [u8]>;
    405     fn parse_data_and_verify_sig(
    406         &self,
    407         key: Self::CredKey<'_>,
    408         relaxed: bool,
    409     ) -> Result<
    410         (CollectedClientData<'_>, Self::Auth<'_>),
    411         AuthRespErr<<Self::Auth<'_> as AuthDataContainer<'_>>::Err>,
    412     > {
    413         /// Always `panic`s.
    414         #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
    415         #[cfg(not(feature = "serde_relaxed"))]
    416         fn get_client_collected_data<const LEN: usize, const DISC: bool>(_: &[u8]) -> ! {
    417             unreachable!(
    418                 "AuthenticatorAssertion::parse_data_and_verify_sig must be passed false when serde_relaxed is not enabled"
    419             );
    420         }
    421         /// Parses `data` using `CollectedClientData::from_client_data_json_relaxed::<false>`.
    422         #[cfg(feature = "serde_relaxed")]
    423         fn get_client_collected_data<const LEN: usize, const DISC: bool>(
    424             data: &[u8],
    425         ) -> Result<
    426             CollectedClientData<'_>,
    427             AuthRespErr<
    428                 <<AuthenticatorAssertion<LEN, DISC> as AuthResponse>::Auth<'_> as AuthDataContainer<'_>>::Err,
    429             >,
    430         >{
    431             CollectedClientData::from_client_data_json_relaxed::<false>(data)
    432                 .map_err(AuthRespErr::CollectedClientDataRelaxed)
    433         }
    434         if relaxed {
    435             get_client_collected_data::<USER_LEN, DISCOVERABLE>(self.client_data_json.as_slice())
    436         } else {
    437             CollectedClientData::from_client_data_json::<false>(self.client_data_json.as_slice())
    438                 .map_err(AuthRespErr::CollectedClientData)
    439         }
    440         .and_then(|client_data_json| {
    441             Self::Auth::from_data(self.authenticator_data_and_c_data_hash.as_slice())
    442                 .map_err(AuthRespErr::Auth)
    443                 .and_then(|val| {
    444                     match key {
    445                         CompressedPubKey::Ed25519(k) => k
    446                             .into_ver_key()
    447                             .map_err(AuthRespErr::PubKey)
    448                             .and_then(|ver_key| {
    449                                 Signature::from_slice(self.signature.as_slice())
    450                                     .and_then(|sig| {
    451                                         // We don't need to use `VerifyingKey::verify_strict` since
    452                                         // `Ed25519PubKey::into_ver_key` verifies the public key is not
    453                                         // in the small-order subgroup. `VerifyingKey::verify_strict` additionally
    454                                         // ensures _R_ of the signature is not in the small-order subgroup, but this
    455                                         // doesn't provide additional benefits and is still not enough to comply
    456                                         // with standards like RFC 8032 or NIST SP 800-186.
    457                                         ver_key.verify(
    458                                             self.authenticator_data_and_c_data_hash.as_slice(),
    459                                             &sig,
    460                                         )
    461                                     })
    462                                     .map_err(|_e| AuthRespErr::Signature)
    463                             }),
    464                         CompressedPubKey::P256(k) => k
    465                             .into_ver_key()
    466                             .map_err(AuthRespErr::PubKey)
    467                             .and_then(|ver_key| {
    468                                 P256DerSig::from_bytes(self.signature.as_slice())
    469                                     .and_then(|sig| {
    470                                         ver_key.verify(
    471                                             self.authenticator_data_and_c_data_hash.as_slice(),
    472                                             &sig,
    473                                         )
    474                                     })
    475                                     .map_err(|_e| AuthRespErr::Signature)
    476                             }),
    477                         CompressedPubKey::P384(k) => k
    478                             .into_ver_key()
    479                             .map_err(AuthRespErr::PubKey)
    480                             .and_then(|ver_key| {
    481                                 P384DerSig::from_bytes(self.signature.as_slice())
    482                                     .and_then(|sig| {
    483                                         ver_key.verify(
    484                                             self.authenticator_data_and_c_data_hash.as_slice(),
    485                                             &sig,
    486                                         )
    487                                     })
    488                                     .map_err(|_e| AuthRespErr::Signature)
    489                             }),
    490                         CompressedPubKey::Rsa(k) => {
    491                             pkcs1v15::Signature::try_from(self.signature.as_slice())
    492                                 .and_then(|sig| {
    493                                     k.as_ver_key().verify(
    494                                         self.authenticator_data_and_c_data_hash.as_slice(),
    495                                         &sig,
    496                                     )
    497                                 })
    498                                 .map_err(|_e| AuthRespErr::Signature)
    499                         }
    500                     }
    501                     .map(|()| (client_data_json, val.data))
    502                 })
    503         })
    504     }
    505 }
    506 /// `AuthenticatorAssertion` with a required `UserHandle`.
    507 pub type DiscoverableAuthenticatorAssertion<const USER_LEN: usize> =
    508     AuthenticatorAssertion<USER_LEN, true>;
    509 /// `AuthenticatorAssertion` with an optional `UserHandle`.
    510 pub type NonDiscoverableAuthenticatorAssertion<const USER_LEN: usize> =
    511     AuthenticatorAssertion<USER_LEN, false>;
    512 impl<const USER_LEN: usize> From<DiscoverableAuthenticatorAssertion<USER_LEN>>
    513     for NonDiscoverableAuthenticatorAssertion<USER_LEN>
    514 {
    515     #[inline]
    516     fn from(value: DiscoverableAuthenticatorAssertion<USER_LEN>) -> Self {
    517         Self {
    518             client_data_json: value.client_data_json,
    519             authenticator_data_and_c_data_hash: value.authenticator_data_and_c_data_hash,
    520             signature: value.signature,
    521             user_handle: value.user_handle,
    522         }
    523     }
    524 }
    525 impl<const USER_LEN: usize> TryFrom<NonDiscoverableAuthenticatorAssertion<USER_LEN>>
    526     for DiscoverableAuthenticatorAssertion<USER_LEN>
    527 {
    528     type Error = MissingUserHandleErr;
    529     #[inline]
    530     fn try_from(
    531         value: NonDiscoverableAuthenticatorAssertion<USER_LEN>,
    532     ) -> Result<Self, MissingUserHandleErr> {
    533         if value.user_handle.is_some() {
    534             Ok(Self {
    535                 client_data_json: value.client_data_json,
    536                 authenticator_data_and_c_data_hash: value.authenticator_data_and_c_data_hash,
    537                 signature: value.signature,
    538                 user_handle: value.user_handle,
    539             })
    540         } else {
    541             Err(MissingUserHandleErr)
    542         }
    543     }
    544 }
    545 /// [`PublicKeyCredential`](https://www.w3.org/TR/webauthn-3/#iface-pkcredential) for authentication ceremonies.
    546 #[expect(
    547     clippy::field_scoped_visibility_modifiers,
    548     reason = "no invariants to uphold"
    549 )]
    550 #[derive(Debug)]
    551 pub struct Authentication<const USER_LEN: usize, const DISCOVERABLE: bool> {
    552     /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid).
    553     pub(crate) raw_id: CredentialId<Vec<u8>>,
    554     /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response)
    555     pub(crate) response: AuthenticatorAssertion<USER_LEN, DISCOVERABLE>,
    556     /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
    557     pub(crate) authenticator_attachment: AuthenticatorAttachment,
    558 }
    559 impl<const USER_LEN: usize, const DISCOVERABLE: bool> Authentication<USER_LEN, DISCOVERABLE> {
    560     /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid).
    561     #[inline]
    562     #[must_use]
    563     pub fn raw_id(&self) -> CredentialId<&[u8]> {
    564         (&self.raw_id).into()
    565     }
    566     /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response).
    567     #[inline]
    568     #[must_use]
    569     pub const fn response(&self) -> &AuthenticatorAssertion<USER_LEN, DISCOVERABLE> {
    570         &self.response
    571     }
    572     /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
    573     #[inline]
    574     #[must_use]
    575     pub const fn authenticator_attachment(&self) -> AuthenticatorAttachment {
    576         self.authenticator_attachment
    577     }
    578     /// Constructs an `Authentication`.
    579     #[cfg(feature = "custom")]
    580     #[inline]
    581     #[must_use]
    582     pub const fn new(
    583         raw_id: CredentialId<Vec<u8>>,
    584         response: AuthenticatorAssertion<USER_LEN, DISCOVERABLE>,
    585         authenticator_attachment: AuthenticatorAttachment,
    586     ) -> Self {
    587         Self {
    588             raw_id,
    589             response,
    590             authenticator_attachment,
    591         }
    592     }
    593     /// Returns the associated `SentChallenge`.
    594     ///
    595     /// This is useful when wanting to extract the corresponding [`DiscoverableAuthenticationServerState`]
    596     /// or [`NonDiscoverableAuthenticationServerState`] from an in-memory collection (e.g., [`MaxLenHashSet`]) or
    597     /// storage.
    598     ///
    599     /// Note if [`CollectedClientData::from_client_data_json`] returns `Ok`, then this will return `Ok`
    600     /// containing the same value as [`CollectedClientData::challenge`]; however the converse is _not_ true.
    601     /// This is because this function parses the minimal amount of data possible.
    602     ///
    603     /// # Errors
    604     ///
    605     /// Errors iff [`AuthenticatorAssertion::client_data_json`] does not contain a base64url-encoded
    606     /// [`Challenge`] in the required position.
    607     #[inline]
    608     pub fn challenge(&self) -> Result<SentChallenge, CollectedClientDataErr> {
    609         LimitedVerificationParser::<false>::get_sent_challenge(
    610             self.response.client_data_json.as_slice(),
    611         )
    612     }
    613     /// Returns the associated `SentChallenge`.
    614     ///
    615     /// This is useful when wanting to extract the corresponding [`DiscoverableAuthenticationServerState`]
    616     /// or [`NonDiscoverableAuthenticationServerState`] from an in-memory collection (e.g.,
    617     /// [`MaxLenHashSet`]) or storage.
    618     ///
    619     /// Note if [`CollectedClientData::from_client_data_json_relaxed`] returns `Ok`, then this will return
    620     /// `Ok` containing the same value as [`CollectedClientData::challenge`]; however the converse
    621     /// is _not_ true. This is because this function attempts to reduce the amount of data parsed.
    622     ///
    623     /// # Errors
    624     ///
    625     /// Errors iff [`AuthenticatorAssertion::client_data_json`] is invalid JSON _after_ ignoring
    626     /// a leading U+FEFF and replacing any sequences of invalid UTF-8 code units with U+FFFD or
    627     /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge) does not exist
    628     /// or is not a base64url-encoded [`Challenge`].
    629     #[cfg(feature = "serde_relaxed")]
    630     #[inline]
    631     pub fn challenge_relaxed(&self) -> Result<SentChallenge, SerdeJsonErr> {
    632         RelaxedClientDataJsonParser::<false>::get_sent_challenge(
    633             self.response.client_data_json.as_slice(),
    634         )
    635     }
    636     /// Convenience function for [`AuthenticationRelaxed::deserialize`].
    637     ///
    638     /// # Errors
    639     ///
    640     /// Errors iff [`AuthenticationRelaxed::deserialize`] does.
    641     #[cfg(feature = "serde_relaxed")]
    642     #[inline]
    643     pub fn from_json_relaxed<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr>
    644     where
    645         UserHandle<USER_LEN>: Deserialize<'a>,
    646     {
    647         serde_json::from_slice::<AuthenticationRelaxed<USER_LEN, DISCOVERABLE>>(json)
    648             .map(|val| val.0)
    649     }
    650     /// Convenience function for [`CustomAuthentication::deserialize`].
    651     ///
    652     /// # Errors
    653     ///
    654     /// Errors iff [`CustomAuthentication::deserialize`] does.
    655     #[cfg(feature = "serde_relaxed")]
    656     #[inline]
    657     pub fn from_json_custom<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr>
    658     where
    659         UserHandle<USER_LEN>: Deserialize<'a>,
    660     {
    661         serde_json::from_slice::<CustomAuthentication<USER_LEN, DISCOVERABLE>>(json)
    662             .map(|val| val.0)
    663     }
    664 }
    665 impl<const USER_LEN: usize, const DISCOVERABLE: bool> Response
    666     for Authentication<USER_LEN, DISCOVERABLE>
    667 {
    668     type Auth = AuthenticatorAssertion<USER_LEN, DISCOVERABLE>;
    669     fn auth(&self) -> &Self::Auth {
    670         &self.response
    671     }
    672 }
    673 /// `Authentication` with a required [`UserHandle`].
    674 pub type DiscoverableAuthentication<const USER_LEN: usize> = Authentication<USER_LEN, true>;
    675 /// `Authentication` with a required [`UserHandle64`].
    676 pub type DiscoverableAuthentication64 = Authentication<USER_HANDLE_MAX_LEN, true>;
    677 /// `Authentication` with a required [`UserHandle16`].
    678 pub type DiscoverableAuthentication16 = Authentication<16, true>;
    679 /// `Authentication` with an optional [`UserHandle`].
    680 pub type NonDiscoverableAuthentication<const USER_LEN: usize> = Authentication<USER_LEN, false>;
    681 impl<const USER_LEN: usize> From<DiscoverableAuthentication<USER_LEN>>
    682     for NonDiscoverableAuthentication<USER_LEN>
    683 {
    684     #[inline]
    685     fn from(value: DiscoverableAuthentication<USER_LEN>) -> Self {
    686         Self {
    687             raw_id: value.raw_id,
    688             response: value.response.into(),
    689             authenticator_attachment: value.authenticator_attachment,
    690         }
    691     }
    692 }
    693 impl<const USER_LEN: usize> TryFrom<NonDiscoverableAuthentication<USER_LEN>>
    694     for DiscoverableAuthentication<USER_LEN>
    695 {
    696     type Error = MissingUserHandleErr;
    697     #[inline]
    698     fn try_from(
    699         value: NonDiscoverableAuthentication<USER_LEN>,
    700     ) -> Result<Self, MissingUserHandleErr> {
    701         value.response.try_into().map(|response| Self {
    702             raw_id: value.raw_id,
    703             response,
    704             authenticator_attachment: value.authenticator_attachment,
    705         })
    706     }
    707 }
    708 /// `Authentication` with an optional [`UserHandle64`].
    709 pub type NonDiscoverableAuthentication64 = Authentication<USER_HANDLE_MAX_LEN, false>;
    710 /// `Authentication` with an optional [`UserHandle16`].
    711 pub type NonDiscoverableAuthentication16 = Authentication<16, false>;