webauthn_rp

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

auth.rs (30390B)


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