webauthn_rp

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

auth.rs (33014B)


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