webauthn_rp

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

auth.rs (33112B)


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