webauthn_rp

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

auth.rs (29044B)


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