webauthn_rp

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

auth.rs (91170B)


      1 #[cfg(doc)]
      2 use super::{
      3     super::response::{
      4         Backup, CollectedClientData, Flag,
      5         auth::AuthenticatorData,
      6         register::{
      7             AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputsStaticState,
      8             DynamicState, StaticState,
      9         },
     10     },
     11     AsciiDomain, AsciiDomainStatic, DomainOrigin, Url,
     12     register::{self, PublicKeyCredentialCreationOptions},
     13 };
     14 use super::{
     15     super::{
     16         AuthenticatedCredential,
     17         response::{
     18             AuthenticatorAttachment,
     19             auth::{
     20                 Authentication, AuthenticatorExtensionOutput, DiscoverableAuthentication,
     21                 HmacSecret, NonDiscoverableAuthentication,
     22                 error::{AuthCeremonyErr, ExtensionErr, OneOrTwo},
     23             },
     24             register::{CompressedPubKey, CredentialProtectionPolicy},
     25         },
     26     },
     27     BackupReq, Ceremony, CeremonyOptions, Challenge, CredentialId, CredentialMediationRequirement,
     28     Credentials, ExtensionReq, FIVE_MINUTES, Hint, Origin, PrfInput, PublicKeyCredentialDescriptor,
     29     RpId, SentChallenge, TimedCeremony, UserVerificationRequirement,
     30     auth::error::{InvalidTimeout, NonDiscoverableCredentialRequestOptionsErr},
     31 };
     32 use core::{
     33     borrow::Borrow,
     34     cmp::Ordering,
     35     hash::{Hash, Hasher},
     36     num::{NonZeroU32, NonZeroU64},
     37     time::Duration,
     38 };
     39 #[cfg(any(doc, not(feature = "serializable_server_state")))]
     40 use std::time::Instant;
     41 #[cfg(any(doc, feature = "serializable_server_state"))]
     42 use std::time::SystemTime;
     43 /// Contains error types.
     44 pub mod error;
     45 /// Contains functionality to serialize data to a client.
     46 #[cfg(feature = "serde")]
     47 pub mod ser;
     48 /// Contains functionality to (de)serialize [`DiscoverableAuthenticationServerState`] and
     49 /// [`NonDiscoverableAuthenticationServerState`] to a data store.
     50 #[cfg(feature = "serializable_server_state")]
     51 pub mod ser_server_state;
     52 /// Controls how [signature counter](https://www.w3.org/TR/webauthn-3/#signature-counter) is enforced.
     53 ///
     54 /// Note that if the previous signature counter is positive and the new counter is not strictly greater, then the
     55 /// authenticator is likely a clone (i.e., there are at least two copies of the private key).
     56 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
     57 pub enum SignatureCounterEnforcement {
     58     /// Fail the authentication ceremony if the counter is less than or equal to the previous value when the
     59     /// previous value is positive.
     60     #[default]
     61     Fail,
     62     /// When the counter is less than the previous value, don't fail and update the value.
     63     ///
     64     /// Note in the special case that the new signature counter is 0, [`DynamicState::sign_count`] _won't_
     65     /// be updated since that would allow an attacker to permanently disable the counter.
     66     Update,
     67     /// When the counter is less than the previous value, don't fail but don't update the value.
     68     Ignore,
     69 }
     70 impl SignatureCounterEnforcement {
     71     /// Validates the signature counter based on `self`.
     72     const fn validate(self, prev: u32, cur: u32) -> Result<u32, AuthCeremonyErr> {
     73         if prev == 0 || cur > prev {
     74             Ok(cur)
     75         } else {
     76             match self {
     77                 Self::Fail => Err(AuthCeremonyErr::SignatureCounter),
     78                 // When the new counter is `0`, we use the previous counter to avoid an attacker from
     79                 // being able to permanently disable it.
     80                 Self::Update => Ok(if cur == 0 { prev } else { cur }),
     81                 Self::Ignore => Ok(prev),
     82             }
     83         }
     84     }
     85 }
     86 /// Owned version of [`PrfInput`].
     87 ///
     88 /// When relying on [`NonDiscoverableCredentialRequestOptions`], it's recommended to use credential-specific PRF
     89 /// inputs that are continuously rolled over. One uses this type for such a thing.
     90 #[derive(Clone, Debug, Eq, PartialEq)]
     91 pub struct PrfInputOwned {
     92     /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first).
     93     pub first: Vec<u8>,
     94     /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second).
     95     pub second: Option<Vec<u8>>,
     96     /// Note this is only applicable for authenticators that implement the
     97     /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) extension on top of the
     98     /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-extension)
     99     /// extension since the data is encrypted and is part of the [`AuthenticatorData`].
    100     pub ext_req: ExtensionReq,
    101 }
    102 /// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client.
    103 #[derive(Clone, Copy, Debug)]
    104 pub struct Extension<'prf_first, 'prf_second> {
    105     /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension).
    106     ///
    107     /// If both [`CredentialSpecificExtension::prf`] and this are [`Some`], then `CredentialSpecificExtension::prf`
    108     /// takes priority.
    109     ///
    110     /// Note `ExtensionReq` is only applicable for authenticators that implement the
    111     /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) extension on top of the
    112     /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-extension)
    113     /// extension since the data is encrypted and is part of the [`AuthenticatorData`].
    114     pub prf: Option<(PrfInput<'prf_first, 'prf_second>, ExtensionReq)>,
    115 }
    116 impl<'prf_first, 'prf_second> Extension<'prf_first, 'prf_second> {
    117     /// Returns an `Extension` with [`Self::prf`] set to `None`.
    118     #[inline]
    119     #[must_use]
    120     pub const fn none() -> Self {
    121         Self { prf: None }
    122     }
    123     /// Returns an `Extension` with [`Self::prf`] set to `None`.
    124     #[expect(single_use_lifetimes, reason = "false positive")]
    125     #[inline]
    126     #[must_use]
    127     pub const fn with_prf<'a: 'prf_first, 'b: 'prf_second>(
    128         input: PrfInput<'a, 'b>,
    129         req: ExtensionReq,
    130     ) -> Self {
    131         Self {
    132             prf: Some((input, req)),
    133         }
    134     }
    135 }
    136 impl Default for Extension<'_, '_> {
    137     /// Same as [`Self::none`].
    138     #[inline]
    139     fn default() -> Self {
    140         Self::none()
    141     }
    142 }
    143 /// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client that
    144 /// are credential-specific which among other things implies a non-discoverable request.
    145 #[derive(Clone, Debug, Default)]
    146 pub struct CredentialSpecificExtension {
    147     /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension).
    148     ///
    149     /// If both [`Extension::prf`] and this are [`Some`], then this take priority.
    150     pub prf: Option<PrfInputOwned>,
    151 }
    152 /// Registered credential used in
    153 /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials).
    154 #[derive(Clone, Debug)]
    155 pub struct AllowedCredential {
    156     /// The registered credential.
    157     pub credential: PublicKeyCredentialDescriptor<Box<[u8]>>,
    158     /// Credential-specific extensions.
    159     pub extension: CredentialSpecificExtension,
    160 }
    161 impl From<PublicKeyCredentialDescriptor<Box<[u8]>>> for AllowedCredential {
    162     #[inline]
    163     fn from(credential: PublicKeyCredentialDescriptor<Box<[u8]>>) -> Self {
    164         Self {
    165             credential,
    166             extension: CredentialSpecificExtension::default(),
    167         }
    168     }
    169 }
    170 impl From<AllowedCredential> for PublicKeyCredentialDescriptor<Box<[u8]>> {
    171     #[inline]
    172     fn from(credential: AllowedCredential) -> Self {
    173         credential.credential
    174     }
    175 }
    176 /// Queue of unique [`AllowedCredential`]s.
    177 #[derive(Clone, Debug, Default)]
    178 pub struct AllowedCredentials {
    179     /// Allowed credentials.
    180     creds: Vec<AllowedCredential>,
    181     /// Number of `AllowedCredential`s that have PRF inputs.
    182     ///
    183     /// Useful to help serialization.
    184     prf_count: usize,
    185 }
    186 impl Credentials for AllowedCredentials {
    187     type Credential = AllowedCredential;
    188     /// # Examples
    189     ///
    190     /// ```
    191     /// # use webauthn_rp::request::{auth::AllowedCredentials, Credentials};
    192     /// assert!(AllowedCredentials::with_capacity(1).as_ref().is_empty());
    193     /// ```
    194     #[inline]
    195     fn with_capacity(capacity: usize) -> Self {
    196         Self {
    197             creds: Vec::with_capacity(capacity),
    198             prf_count: 0,
    199         }
    200     }
    201     /// # Examples
    202     ///
    203     /// ```
    204     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    205     /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr};
    206     /// # use webauthn_rp::{
    207     /// #     request::{auth::AllowedCredentials, PublicKeyCredentialDescriptor, Credentials},
    208     /// #     response::{AuthTransports, CredentialId},
    209     /// # };
    210     /// /// Retrieves the `AuthTransports` associated with the unique `cred_id`
    211     /// /// from the database.
    212     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    213     /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> {
    214     ///     // ⋮
    215     /// #     AuthTransports::decode(32)
    216     /// }
    217     /// let mut creds = AllowedCredentials::with_capacity(1);
    218     /// assert!(creds.as_ref().is_empty());
    219     /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is
    220     /// // likely never needed since the `CredentialId` was originally sent from the client and is likely
    221     /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
    222     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    223     /// let id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?;
    224     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    225     /// let transports = get_transports((&id).into())?;
    226     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    227     /// assert!(creds.push(PublicKeyCredentialDescriptor { id, transports }.into()));
    228     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    229     /// let id_copy = CredentialId::try_from(vec![0; 16].into_boxed_slice())?;
    230     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    231     /// let transports_2 = AuthTransports::NONE;
    232     /// // Duplicate `CredentialId`s don't get added.
    233     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    234     /// assert!(!creds.push(
    235     ///     PublicKeyCredentialDescriptor {
    236     ///         id: id_copy,
    237     ///         transports: transports_2
    238     ///     }
    239     ///     .into()
    240     /// ));
    241     /// # Ok::<_, webauthn_rp::AggErr>(())
    242     /// ```
    243     #[expect(
    244         clippy::arithmetic_side_effects,
    245         reason = "comment explains how overflow is not possible"
    246     )]
    247     #[inline]
    248     fn push(&mut self, cred: Self::Credential) -> bool {
    249         self.creds
    250             .iter()
    251             .try_fold((), |(), c| {
    252                 if c.credential.id == cred.credential.id {
    253                     Err(())
    254                 } else {
    255                     Ok(())
    256                 }
    257             })
    258             .is_ok_and(|()| {
    259                 // This can't overflow since `self.creds.push` would `panic` since
    260                 // `self.prf_count <= self.creds.len()`.
    261                 self.prf_count += usize::from(cred.extension.prf.is_some());
    262                 self.creds.push(cred);
    263                 true
    264             })
    265     }
    266     #[inline]
    267     fn len(&self) -> usize {
    268         self.creds.len()
    269     }
    270 }
    271 impl AsRef<[AllowedCredential]> for AllowedCredentials {
    272     #[inline]
    273     fn as_ref(&self) -> &[AllowedCredential] {
    274         self.creds.as_slice()
    275     }
    276 }
    277 impl From<&AllowedCredentials> for Box<[CredInfo]> {
    278     #[inline]
    279     fn from(value: &AllowedCredentials) -> Self {
    280         let len = value.creds.len();
    281         value
    282             .creds
    283             .iter()
    284             .fold(Vec::with_capacity(len), |mut creds, cred| {
    285                 creds.push(CredInfo {
    286                     id: cred.credential.id.clone(),
    287                     ext: (&cred.extension).into(),
    288                 });
    289                 creds
    290             })
    291             .into_boxed_slice()
    292     }
    293 }
    294 impl From<AllowedCredentials> for Vec<PublicKeyCredentialDescriptor<Box<[u8]>>> {
    295     #[inline]
    296     fn from(value: AllowedCredentials) -> Self {
    297         let mut creds = Self::with_capacity(value.creds.len());
    298         value.creds.into_iter().fold((), |(), cred| {
    299             creds.push(cred.credential);
    300         });
    301         creds
    302     }
    303 }
    304 impl From<Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>> for AllowedCredentials {
    305     #[inline]
    306     fn from(value: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>) -> Self {
    307         let mut creds = Self::with_capacity(value.len());
    308         value.into_iter().fold((), |(), credential| {
    309             _ = creds.push(AllowedCredential {
    310                 credential,
    311                 extension: CredentialSpecificExtension { prf: None },
    312             });
    313         });
    314         creds
    315     }
    316 }
    317 /// The [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions)
    318 /// to send to the client when authenticating a discoverable credential.
    319 ///
    320 /// Upon saving the [`DiscoverableAuthenticationServerState`] returned from [`Self::start_ceremony`], one MUST send
    321 /// [`DiscoverableAuthenticationClientState`] to the client ASAP. After receiving the newly created
    322 /// [`DiscoverableAuthentication`], it is validated using [`DiscoverableAuthenticationServerState::verify`].
    323 #[derive(Debug)]
    324 pub struct DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> {
    325     /// [`mediation`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement).
    326     ///
    327     /// Note if this is [`CredentialMediationRequirement::Conditional`], user agents are instructed to not
    328     /// enforce any timeout; as result, one may want to set [`PublicKeyCredentialRequestOptions::timeout`] to
    329     /// [`NonZeroU32::MAX`].
    330     pub mediation: CredentialMediationRequirement,
    331     /// `public-key` [credential type](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry).
    332     pub public_key: PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>,
    333 }
    334 impl<'rp_id, 'prf_first, 'prf_second>
    335     DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>
    336 {
    337     /// Creates a `DiscoverableCredentialRequestOptions` containing [`CredentialMediationRequirement::default`] and
    338     /// [`PublicKeyCredentialRequestOptions::passkey`].
    339     ///
    340     /// # Examples
    341     ///
    342     /// ```
    343     /// # use webauthn_rp::request::{auth::DiscoverableCredentialRequestOptions, AsciiDomain, RpId, UserVerificationRequirement};
    344     /// assert!(matches!(
    345     ///     DiscoverableCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).public_key.user_verification,
    346     ///     UserVerificationRequirement::Required
    347     /// ));
    348     /// # Ok::<_, webauthn_rp::AggErr>(())
    349     /// ```
    350     #[expect(single_use_lifetimes, reason = "false positive")]
    351     #[inline]
    352     #[must_use]
    353     pub fn passkey<'a: 'rp_id>(rp_id: &'a RpId) -> Self {
    354         Self {
    355             mediation: CredentialMediationRequirement::default(),
    356             public_key: PublicKeyCredentialRequestOptions::passkey(rp_id),
    357         }
    358     }
    359     /// Begins the [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony) consuming
    360     /// `self`. Note that the expiration [`Instant`]/[`SystemTime`] is saved, so
    361     /// `DiscoverableAuthenticationClientState` MUST be sent ASAP. In order to complete authentication, the returned
    362     /// `DiscoverableAuthenticationServerState` MUST be saved so that it can later be used to verify the credential
    363     /// assertion with [`DiscoverableAuthenticationServerState::verify`].
    364     ///
    365     /// # Errors
    366     ///
    367     /// Errors iff `self` contains incompatible configuration.
    368     #[inline]
    369     pub fn start_ceremony(
    370         self,
    371     ) -> Result<
    372         (
    373             DiscoverableAuthenticationServerState,
    374             DiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>,
    375         ),
    376         InvalidTimeout,
    377     > {
    378         #[cfg(not(feature = "serializable_server_state"))]
    379         let res = Instant::now();
    380         #[cfg(feature = "serializable_server_state")]
    381         let res = SystemTime::now();
    382         res.checked_add(Duration::from_millis(
    383             NonZeroU64::from(self.public_key.timeout).get(),
    384         ))
    385         .ok_or(InvalidTimeout)
    386         .map(|expiration| {
    387             (
    388                 DiscoverableAuthenticationServerState(AuthenticationServerState {
    389                     challenge: SentChallenge(self.public_key.challenge.0),
    390                     user_verification: self.public_key.user_verification,
    391                     extensions: self.public_key.extensions.into(),
    392                     expiration,
    393                 }),
    394                 DiscoverableAuthenticationClientState(self),
    395             )
    396         })
    397     }
    398     /// Same as [`Self::start_ceremony`] except the raw challenge is returned instead of
    399     /// [`DiscoverableAuthenticationClientState`].
    400     ///
    401     /// Note this is useful when one configures the authentication ceremony client-side and only needs the
    402     /// server-generated challenge. It's of course essential that `self` is configured exactly the same as
    403     /// how it is configured client-side. See
    404     /// [`challengeURL`](https://github.com/w3c/webauthn/wiki/Explainer:-WebAuthn-challengeURL) for more
    405     /// information.
    406     ///
    407     /// # Errors
    408     ///
    409     /// Read [`Self::start_ceremony`].
    410     #[inline]
    411     pub fn start_ceremony_challenge_only(
    412         self,
    413     ) -> Result<(DiscoverableAuthenticationServerState, [u8; 16]), InvalidTimeout> {
    414         self.start_ceremony()
    415             .map(|(server, client)| (server, client.0.public_key.challenge.into_array()))
    416     }
    417 }
    418 /// The [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions)
    419 /// to send to the client when authenticating non-discoverable credentials.
    420 ///
    421 /// Upon saving the [`NonDiscoverableAuthenticationServerState`] returned from [`Self::start_ceremony`], one MUST send
    422 /// [`NonDiscoverableAuthenticationClientState`] to the client ASAP. After receiving the newly created
    423 /// [`NonDiscoverableAuthentication`], it is validated using [`NonDiscoverableAuthenticationServerState::verify`].
    424 #[derive(Debug)]
    425 pub struct NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> {
    426     /// [`mediation`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement).
    427     pub mediation: CredentialMediationRequirement,
    428     /// [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions).
    429     pub options: PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>,
    430     /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials).
    431     pub allow_credentials: AllowedCredentials,
    432 }
    433 impl<'rp_id, 'prf_first, 'prf_second>
    434     NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>
    435 {
    436     /// Creates a `NonDiscoverableCredentialRequestOptions` containing
    437     /// [`CredentialMediationRequirement::default`],
    438     /// [`PublicKeyCredentialRequestOptions::second_factor`], and the passed [`AllowedCredentials`].
    439     ///
    440     /// # Examples
    441     ///
    442     /// ```
    443     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    444     /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr};
    445     /// # use webauthn_rp::{
    446     /// #     request::{
    447     /// #         auth::{AllowedCredentials, NonDiscoverableCredentialRequestOptions},
    448     /// #         AsciiDomain, RpId, PublicKeyCredentialDescriptor, Credentials
    449     /// #     },
    450     /// #     response::{AuthTransports, CredentialId},
    451     /// # };
    452     /// /// Retrieves the `AuthTransports` associated with the unique `cred_id`
    453     /// /// from the database.
    454     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    455     /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> {
    456     ///     // ⋮
    457     /// #     AuthTransports::decode(32)
    458     /// }
    459     /// let mut creds = AllowedCredentials::with_capacity(1);
    460     /// assert!(creds.as_ref().is_empty());
    461     /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is
    462     /// // likely never needed since the `CredentialId` was originally sent from the client and is likely
    463     /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
    464     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    465     /// let id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?;
    466     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    467     /// let transports = get_transports((&id).into())?;
    468     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    469     /// assert!(creds.push(PublicKeyCredentialDescriptor { id, transports }.into()));
    470     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    471     /// assert_eq!(
    472     ///     NonDiscoverableCredentialRequestOptions::second_factor(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?), creds)
    473     ///         .allow_credentials
    474     ///         .len(),
    475     ///     1
    476     /// );
    477     /// # Ok::<_, webauthn_rp::AggErr>(())
    478     /// ```
    479     #[expect(single_use_lifetimes, reason = "false positive")]
    480     #[inline]
    481     #[must_use]
    482     pub fn second_factor<'a: 'rp_id>(
    483         rp_id: &'a RpId,
    484         allow_credentials: AllowedCredentials,
    485     ) -> Self {
    486         Self {
    487             mediation: CredentialMediationRequirement::default(),
    488             options: PublicKeyCredentialRequestOptions::second_factor(rp_id),
    489             allow_credentials,
    490         }
    491     }
    492     /// Begins the [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony) consuming
    493     /// `self`. Note that the expiration [`Instant`]/[`SystemTime`] is saved, so `NonDiscoverableAuthenticationClientState`
    494     /// MUST be sent ASAP. In order to complete authentication, the returned `NonDiscoverableAuthenticationServerState`
    495     /// MUST be saved so that it can later be used to verify the credential assertion with
    496     /// [`NonDiscoverableAuthenticationServerState::verify`].
    497     ///
    498     /// # Errors
    499     ///
    500     /// Errors iff `self` contains incompatible configuration.
    501     #[inline]
    502     pub fn start_ceremony(
    503         self,
    504     ) -> Result<
    505         (
    506             NonDiscoverableAuthenticationServerState,
    507             NonDiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>,
    508         ),
    509         NonDiscoverableCredentialRequestOptionsErr,
    510     > {
    511         if self.allow_credentials.is_empty() {
    512             Err(NonDiscoverableCredentialRequestOptionsErr::EmptyAllowedCredentials)
    513         } else if matches!(self.mediation, CredentialMediationRequirement::Conditional) {
    514             Err(NonDiscoverableCredentialRequestOptionsErr::ConditionalMediationRequested)
    515         } else {
    516             #[cfg(not(feature = "serializable_server_state"))]
    517             let res = Instant::now();
    518             #[cfg(feature = "serializable_server_state")]
    519             let res = SystemTime::now();
    520             res.checked_add(Duration::from_millis(
    521                 NonZeroU64::from(self.options.timeout).get(),
    522             ))
    523             .ok_or(NonDiscoverableCredentialRequestOptionsErr::InvalidTimeout)
    524             .map(|expiration| {
    525                 (
    526                     NonDiscoverableAuthenticationServerState {
    527                         state: AuthenticationServerState {
    528                             challenge: SentChallenge(self.options.challenge.0),
    529                             user_verification: self.options.user_verification,
    530                             extensions: self.options.extensions.into(),
    531                             expiration,
    532                         },
    533                         allow_credentials: Box::from(&self.allow_credentials),
    534                     },
    535                     NonDiscoverableAuthenticationClientState(self),
    536                 )
    537             })
    538         }
    539     }
    540 }
    541 /// The [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions)
    542 /// to send to the client when authenticating a credential.
    543 ///
    544 /// This does _not_ contain [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials).
    545 #[derive(Debug)]
    546 pub struct PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> {
    547     /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge).
    548     pub challenge: Challenge,
    549     /// [`timeout`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-timeout).
    550     ///
    551     /// Note we require a positive value despite the spec allowing an optional nonnegative value. This jives
    552     /// with the fact that in-memory storage is required when `serializable_server_state` is not enabled
    553     /// when authenticating credentials as no timeout would make out-of-memory (OOM) conditions more likely.
    554     pub timeout: NonZeroU32,
    555     /// [`rpId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-rpid).
    556     ///
    557     /// This MUST be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] used when the credential was registered.
    558     pub rp_id: &'rp_id RpId,
    559     /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification).
    560     pub user_verification: UserVerificationRequirement,
    561     /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-hints).
    562     pub hints: Hint,
    563     /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-extensions).
    564     pub extensions: Extension<'prf_first, 'prf_second>,
    565 }
    566 impl<'rp_id> PublicKeyCredentialRequestOptions<'rp_id, '_, '_> {
    567     /// Creates a `PublicKeyCredentialRequestOptions` with [`Self::user_verification`] set to
    568     /// [`UserVerificationRequirement::Required`] and [`Self::timeout`] set to [`FIVE_MINUTES`].
    569     ///
    570     /// Note `rp_id` _must_ be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] when the
    571     /// credential was registered.
    572     ///
    573     /// # Examples
    574     ///
    575     /// ```
    576     /// # use webauthn_rp::request::{auth::PublicKeyCredentialRequestOptions, AsciiDomain, RpId, UserVerificationRequirement};
    577     /// assert!(matches!(
    578     ///     PublicKeyCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).user_verification,
    579     ///     UserVerificationRequirement::Required
    580     /// ));
    581     /// # Ok::<_, webauthn_rp::AggErr>(())
    582     /// ```
    583     #[expect(single_use_lifetimes, reason = "false positive")]
    584     #[inline]
    585     #[must_use]
    586     pub fn passkey<'a: 'rp_id>(rp_id: &'a RpId) -> Self {
    587         Self {
    588             challenge: Challenge::new(),
    589             timeout: FIVE_MINUTES,
    590             rp_id,
    591             user_verification: UserVerificationRequirement::Required,
    592             hints: Hint::None,
    593             extensions: Extension::default(),
    594         }
    595     }
    596     /// Creates a `PublicKeyCredentialRequestOptions` with [`Self::user_verification`] set to
    597     /// [`UserVerificationRequirement::Discouraged`] and [`Self::timeout`] set to [`FIVE_MINUTES`].
    598     ///
    599     /// Note `rp_id` _must_ be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] when the
    600     /// credentials were registered.
    601     ///
    602     /// # Examples
    603     ///
    604     /// ```
    605     /// # use webauthn_rp::request::{auth::PublicKeyCredentialRequestOptions, AsciiDomain, RpId, UserVerificationRequirement};
    606     /// assert!(matches!(
    607     ///     PublicKeyCredentialRequestOptions::second_factor(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).user_verification,
    608     ///     UserVerificationRequirement::Discouraged
    609     /// ));
    610     /// # Ok::<_, webauthn_rp::AggErr>(())
    611     /// ```
    612     #[expect(single_use_lifetimes, reason = "false positive")]
    613     #[inline]
    614     #[must_use]
    615     pub fn second_factor<'a: 'rp_id>(rp_id: &'a RpId) -> Self {
    616         let mut opts = Self::passkey(rp_id);
    617         opts.user_verification = UserVerificationRequirement::Discouraged;
    618         opts
    619     }
    620 }
    621 /// Container of a [`DiscoverableCredentialRequestOptions`] that has been used to start the authentication ceremony.
    622 /// This gets sent to the client ASAP.
    623 #[derive(Debug)]
    624 pub struct DiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>(
    625     DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>,
    626 );
    627 impl<'rp_id, 'prf_first, 'prf_second>
    628     DiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>
    629 {
    630     /// Returns the `DiscoverableCredentialRequestOptions` that was used to start an authentication ceremony.
    631     #[inline]
    632     #[must_use]
    633     pub const fn options(
    634         &self,
    635     ) -> &DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> {
    636         &self.0
    637     }
    638 }
    639 /// Container of a [`NonDiscoverableCredentialRequestOptions`] that has been used to start the authentication
    640 /// ceremony. This gets sent to the client ASAP.
    641 #[derive(Debug)]
    642 pub struct NonDiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>(
    643     NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>,
    644 );
    645 impl<'rp_id, 'prf_first, 'prf_second>
    646     NonDiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>
    647 {
    648     /// Returns the `NonDiscoverableCredentialRequestOptions` that was used to start an authentication ceremony.
    649     #[inline]
    650     #[must_use]
    651     pub const fn options(
    652         &self,
    653     ) -> &NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> {
    654         &self.0
    655     }
    656 }
    657 /// The possible combinations of an [`AuthenticatedCredential`]'s [`StaticState`]'s
    658 /// `extensions.hmac_secret` and `client_extension_results.prf`.
    659 ///
    660 /// Note we ensure in `crate::verify_static_and_dynamic_state` that `hmac_secret` does not exist when
    661 /// `prf` does not exist, `hmac_secret` does not exist or is `false` when `prf` is `false`, or
    662 /// `hmac_secret` does not exist or is `true` when `prf` is `true`.
    663 #[derive(Clone, Copy)]
    664 enum CredPrf {
    665     /// No `prf` or `hmac_secret`.
    666     None,
    667     /// `prf.enabled` is `false` but there is no `hmac_secret`.
    668     FalseNoHmac,
    669     /// `prf.enabled` and `hmac_secret` are `false`.
    670     FalseFalseHmac,
    671     /// `prf.enabled` is `true` but there is no `hmac_secret`.
    672     TrueNoHmac,
    673     /// `prf.enabled` and `hmac_secret` are `true`.
    674     TrueTrueHmac,
    675 }
    676 /// `PrfInput` and `PrfInputOwned` without the actual data sent to reduce memory usage when storing
    677 /// [`DiscoverableAuthenticationServerState`] in an in-memory collection.
    678 #[derive(Clone, Copy, Debug)]
    679 enum ServerPrfInfo {
    680     /// No `PrfInput`.
    681     None,
    682     /// `PrfInput::second` was `None`.
    683     One(ExtensionReq),
    684     /// `PrfInput::second` was `Some`.
    685     Two(ExtensionReq),
    686 }
    687 impl ServerPrfInfo {
    688     /// Validates `val` based on the passed arguments.
    689     ///
    690     /// It's not possible to request the PRF extension without sending `UserVerificationRequirement::Required`;
    691     /// thus `user_verified` will always be `true` when sending PRF; otherwise ceremony validation will error.
    692     /// However when we _don't_ send the PRF extension _and_ we don't error on an unsolicited response, it's
    693     /// possible to receive an `HmacSecret` without the user having been verified; thus we only ensure
    694     /// `user_verified` is true when we don't error on unsolicted responses _and_ we didn't send the PRF extension.
    695     const fn validate(
    696         self,
    697         user_verified: bool,
    698         cred_prf: CredPrf,
    699         hmac: HmacSecret,
    700         err_unsolicited: bool,
    701     ) -> Result<(), ExtensionErr> {
    702         match hmac {
    703             HmacSecret::None => match self {
    704                 Self::None => Ok(()),
    705                 Self::One(req) | Self::Two(req) => {
    706                     if matches!(req, ExtensionReq::Allow)
    707                         || !matches!(cred_prf, CredPrf::TrueTrueHmac)
    708                     {
    709                         Ok(())
    710                     } else {
    711                         Err(ExtensionErr::MissingHmacSecret)
    712                     }
    713                 }
    714             },
    715             HmacSecret::One => match self {
    716                 Self::None => {
    717                     if err_unsolicited {
    718                         Err(ExtensionErr::ForbiddenHmacSecret)
    719                     } else if matches!(cred_prf, CredPrf::None | CredPrf::TrueTrueHmac) {
    720                         if user_verified {
    721                             Ok(())
    722                         } else {
    723                             Err(ExtensionErr::UserNotVerifiedHmacSecret)
    724                         }
    725                     } else {
    726                         Err(ExtensionErr::HmacSecretForNonHmacSecretCredential)
    727                     }
    728                 }
    729                 Self::One(_) => {
    730                     if matches!(cred_prf, CredPrf::None | CredPrf::TrueTrueHmac) {
    731                         Ok(())
    732                     } else {
    733                         Err(ExtensionErr::HmacSecretForNonHmacSecretCredential)
    734                     }
    735                 }
    736                 Self::Two(_) => Err(ExtensionErr::InvalidHmacSecretValue(
    737                     OneOrTwo::Two,
    738                     OneOrTwo::One,
    739                 )),
    740             },
    741             HmacSecret::Two => match self {
    742                 Self::None => {
    743                     if err_unsolicited {
    744                         Err(ExtensionErr::ForbiddenHmacSecret)
    745                     } else if matches!(cred_prf, CredPrf::None | CredPrf::TrueTrueHmac) {
    746                         if user_verified {
    747                             Ok(())
    748                         } else {
    749                             Err(ExtensionErr::UserNotVerifiedHmacSecret)
    750                         }
    751                     } else {
    752                         Err(ExtensionErr::HmacSecretForNonHmacSecretCredential)
    753                     }
    754                 }
    755                 Self::One(_) => Err(ExtensionErr::InvalidHmacSecretValue(
    756                     OneOrTwo::One,
    757                     OneOrTwo::Two,
    758                 )),
    759                 Self::Two(_) => {
    760                     if matches!(cred_prf, CredPrf::None | CredPrf::TrueTrueHmac) {
    761                         Ok(())
    762                     } else {
    763                         Err(ExtensionErr::HmacSecretForNonHmacSecretCredential)
    764                     }
    765                 }
    766             },
    767         }
    768     }
    769 }
    770 #[cfg(test)]
    771 impl PartialEq for ServerPrfInfo {
    772     fn eq(&self, other: &Self) -> bool {
    773         match *self {
    774             Self::None => matches!(*other, Self::None),
    775             Self::One(req) => matches!(*other, Self::One(req2) if req == req2),
    776             Self::Two(req) => matches!(*other, Self::Two(req2) if req == req2),
    777         }
    778     }
    779 }
    780 impl From<Option<(PrfInput<'_, '_>, ExtensionReq)>> for ServerPrfInfo {
    781     fn from(value: Option<(PrfInput<'_, '_>, ExtensionReq)>) -> Self {
    782         value.map_or(Self::None, |val| {
    783             val.0
    784                 .second
    785                 .map_or_else(|| Self::One(val.1), |_| Self::Two(val.1))
    786         })
    787     }
    788 }
    789 impl From<&PrfInputOwned> for ServerPrfInfo {
    790     fn from(value: &PrfInputOwned) -> Self {
    791         value
    792             .second
    793             .as_ref()
    794             .map_or_else(|| Self::One(value.ext_req), |_| Self::Two(value.ext_req))
    795     }
    796 }
    797 /// `Extension` without the actual data sent to reduce memory usage when storing [`AuthenticationServerState`]
    798 /// in an in-memory collection.
    799 #[derive(Clone, Copy, Debug)]
    800 struct ServerExtensionInfo {
    801     /// `Extension::prf`.
    802     prf: ServerPrfInfo,
    803 }
    804 impl From<Extension<'_, '_>> for ServerExtensionInfo {
    805     fn from(value: Extension<'_, '_>) -> Self {
    806         Self {
    807             prf: value.prf.into(),
    808         }
    809     }
    810 }
    811 #[cfg(test)]
    812 impl PartialEq for ServerExtensionInfo {
    813     fn eq(&self, other: &Self) -> bool {
    814         self.prf == other.prf
    815     }
    816 }
    817 /// `CredentialSpecificExtension` without the actual data sent to reduce memory usage when storing [`AuthenticationServerState`]
    818 /// in an in-memory collection.
    819 #[derive(Clone, Copy, Debug)]
    820 struct ServerCredSpecificExtensionInfo {
    821     /// `CredentialSpecificExtension::prf`.
    822     prf: ServerPrfInfo,
    823 }
    824 #[cfg(test)]
    825 impl PartialEq for ServerCredSpecificExtensionInfo {
    826     fn eq(&self, other: &Self) -> bool {
    827         self.prf == other.prf
    828     }
    829 }
    830 impl From<&CredentialSpecificExtension> for ServerCredSpecificExtensionInfo {
    831     fn from(value: &CredentialSpecificExtension) -> Self {
    832         Self {
    833             prf: value
    834                 .prf
    835                 .as_ref()
    836                 .map_or(ServerPrfInfo::None, ServerPrfInfo::from),
    837         }
    838     }
    839 }
    840 impl ServerExtensionInfo {
    841     /// Validates the extensions.
    842     ///
    843     /// Note that this MUST only be called internally by `auth::validate_extensions`.
    844     const fn validate_extensions(
    845         self,
    846         user_verified: bool,
    847         auth_ext: AuthenticatorExtensionOutput,
    848         error_unsolicited: bool,
    849         cred_prf: CredPrf,
    850     ) -> Result<(), ExtensionErr> {
    851         ServerPrfInfo::validate(
    852             self.prf,
    853             user_verified,
    854             cred_prf,
    855             auth_ext.hmac_secret,
    856             error_unsolicited,
    857         )
    858     }
    859 }
    860 /// Validates the extensions.
    861 fn validate_extensions(
    862     ext: ServerExtensionInfo,
    863     user_verified: bool,
    864     cred_ext: Option<ServerCredSpecificExtensionInfo>,
    865     auth_ext: AuthenticatorExtensionOutput,
    866     error_unsolicited: bool,
    867     cred_prf: CredPrf,
    868 ) -> Result<(), ExtensionErr> {
    869     cred_ext.map_or_else(
    870         || {
    871             // No credental-specific extensions, so we can simply focus on `ext`.
    872             ext.validate_extensions(user_verified, auth_ext, error_unsolicited, cred_prf)
    873         },
    874         |c_ext| {
    875             // Must carefully process each extension based on overlap and which gets priority over the other.
    876             if matches!(c_ext.prf, ServerPrfInfo::None) {
    877                 ext.prf.validate(
    878                     user_verified,
    879                     cred_prf,
    880                     auth_ext.hmac_secret,
    881                     error_unsolicited,
    882                 )
    883             } else {
    884                 c_ext.prf.validate(
    885                     user_verified,
    886                     cred_prf,
    887                     auth_ext.hmac_secret,
    888                     error_unsolicited,
    889                 )
    890             }
    891         },
    892     )
    893 }
    894 /// [`AllowedCredential`] with less data to reduce memory usage when storing [`AuthenticationServerState`]
    895 /// in an in-memory collection.
    896 #[derive(Debug)]
    897 struct CredInfo {
    898     /// The Credential ID.
    899     id: CredentialId<Box<[u8]>>,
    900     /// Any credential-specific extensions.
    901     ext: ServerCredSpecificExtensionInfo,
    902 }
    903 #[cfg(test)]
    904 impl PartialEq for CredInfo {
    905     fn eq(&self, other: &Self) -> bool {
    906         self.id == other.id && self.ext == other.ext
    907     }
    908 }
    909 /// Controls how to handle a change in [`DynamicState::authenticator_attachment`].
    910 ///
    911 /// Note when `DynamicState::authenticator_attachment` is [`AuthenticatorAttachment::None`], then it will
    912 /// be updated regardless. Similarly when [`Authentication::authenticator_attachment`] is
    913 /// `AuthenticatorAttachment::None`, it will never update `DynamicState::authenticator_attachment`.
    914 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    915 pub enum AuthenticatorAttachmentEnforcement {
    916     /// Fail the authentication ceremony if [`AuthenticatorAttachment`] is not the same.
    917     ///
    918     /// The contained `bool` represents if `AuthenticatorAttachment` must be sent.
    919     Fail(bool),
    920     /// Update [`DynamicState::authenticator_attachment`] to the sent [`AuthenticatorAttachment`].
    921     ///
    922     /// The contained `bool` represents if `AuthenticatorAttachment` must be sent.
    923     Update(bool),
    924     /// Do not update [`DynamicState::authenticator_attachment`] to the sent [`AuthenticatorAttachment`].
    925     ///
    926     /// The contained `bool` represents if `AuthenticatorAttachment` must be sent.
    927     Ignore(bool),
    928 }
    929 impl AuthenticatorAttachmentEnforcement {
    930     /// Validates `cur` based on `self` and `prev`.
    931     const fn validate(
    932         self,
    933         prev: AuthenticatorAttachment,
    934         cur: AuthenticatorAttachment,
    935     ) -> Result<AuthenticatorAttachment, AuthCeremonyErr> {
    936         match cur {
    937             AuthenticatorAttachment::None => match self {
    938                 Self::Fail(require) | Self::Update(require) | Self::Ignore(require) => {
    939                     if require {
    940                         Err(AuthCeremonyErr::MissingAuthenticatorAttachment)
    941                     } else {
    942                         // We don't overwrite the previous one with [`AuthenticatorAttachment::None`].
    943                         Ok(prev)
    944                     }
    945                 }
    946             },
    947             AuthenticatorAttachment::Platform => match self {
    948                 Self::Fail(_) => {
    949                     if matches!(prev, AuthenticatorAttachment::CrossPlatform) {
    950                         Err(AuthCeremonyErr::AuthenticatorAttachmentMismatch)
    951                     } else {
    952                         // We don't fail when we previously had [`AuthenticatorAttachment::None`].
    953                         Ok(cur)
    954                     }
    955                 }
    956                 Self::Update(_) => Ok(cur),
    957                 Self::Ignore(_) => {
    958                     if matches!(prev, AuthenticatorAttachment::None) {
    959                         // We overwrite the previous one when it is [`AuthenticatorAttachment::None`].
    960                         Ok(cur)
    961                     } else {
    962                         Ok(prev)
    963                     }
    964                 }
    965             },
    966             AuthenticatorAttachment::CrossPlatform => match self {
    967                 Self::Fail(_) => {
    968                     if matches!(prev, AuthenticatorAttachment::Platform) {
    969                         Err(AuthCeremonyErr::AuthenticatorAttachmentMismatch)
    970                     } else {
    971                         // We don't fail when we previously had [`AuthenticatorAttachment::None`].
    972                         Ok(cur)
    973                     }
    974                 }
    975                 Self::Update(_) => Ok(cur),
    976                 Self::Ignore(_) => {
    977                     if matches!(prev, AuthenticatorAttachment::None) {
    978                         // We overwrite the previous one when it is [`AuthenticatorAttachment::None`].
    979                         Ok(cur)
    980                     } else {
    981                         Ok(prev)
    982                     }
    983                 }
    984             },
    985         }
    986     }
    987 }
    988 impl Default for AuthenticatorAttachmentEnforcement {
    989     /// Returns [`Self::Ignore`] containing `false`.
    990     #[inline]
    991     fn default() -> Self {
    992         Self::Ignore(false)
    993     }
    994 }
    995 /// Additional verification options to perform in [`DiscoverableAuthenticationServerState::verify`] and
    996 /// [`NonDiscoverableAuthenticationServerState::verify`].
    997 #[derive(Clone, Copy, Debug)]
    998 pub struct AuthenticationVerificationOptions<'origins, 'top_origins, O, T> {
    999     /// Origins to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin).
   1000     ///
   1001     /// When this is empty, the origin that will be used will be based on the [`RpId`] passed to
   1002     /// [`DiscoverableAuthenticationServerState::verify`] or [`NonDiscoverableAuthenticationServerState::verify`].
   1003     /// If [`RpId::Domain`] or [`RpId::StaticDomain`], then the [`DomainOrigin`] returned from passing
   1004     /// [`AsciiDomain::as_ref`] and [`AsciiDomainStatic::as_str`] to [`DomainOrigin::new`] respectively will be
   1005     /// used; otherwise the [`Url`] in [`RpId::Url`] will be used.
   1006     pub allowed_origins: &'origins [O],
   1007     /// [Top-level origins](https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-top-level-origin)
   1008     /// to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin).
   1009     ///
   1010     /// When this is `Some`, [`CollectedClientData::cross_origin`] is allowed to be `true`. When the contained
   1011     /// `slice` is empty, [`CollectedClientData::top_origin`] must be `None`. When this is `None`,
   1012     /// `CollectedClientData::cross_origin` must be `false` and `CollectedClientData::top_origin` must be `None`.
   1013     pub allowed_top_origins: Option<&'top_origins [T]>,
   1014     /// The required [`Backup`] state of the credential.
   1015     ///
   1016     /// Note that `None` is _not_ the same as `Some(BackupReq::None)` as the latter indicates that any [`Backup`]
   1017     /// is allowed. This is rarely what you want; instead, `None` indicates that [`BackupReq::from`] applied to
   1018     /// [`DynamicState::backup`] will be used in [`DiscoverableAuthenticationServerState::verify`] and
   1019     /// [`NonDiscoverableAuthenticationServerState::verify`] and
   1020     pub backup_requirement: Option<BackupReq>,
   1021     /// Error when unsolicited extensions are sent back iff `true`.
   1022     pub error_on_unsolicited_extensions: bool,
   1023     /// Dictates what happens when [`Authentication::authenticator_attachment`] is not the same as
   1024     /// [`DynamicState::authenticator_attachment`].
   1025     pub auth_attachment_enforcement: AuthenticatorAttachmentEnforcement,
   1026     /// [`DynamicState::user_verified`] will be set to `true` iff [`Flag::user_verified`] when `true`.
   1027     pub update_uv: bool,
   1028     /// Dictates what happens when [`AuthenticatorData::sign_count`] is not updated to a strictly greater value.
   1029     pub sig_counter_enforcement: SignatureCounterEnforcement,
   1030     /// [`CollectedClientData::from_client_data_json_relaxed`] is used to extract [`CollectedClientData`] iff `true`.
   1031     #[cfg(feature = "serde_relaxed")]
   1032     pub client_data_json_relaxed: bool,
   1033 }
   1034 impl<O, T> AuthenticationVerificationOptions<'_, '_, O, T> {
   1035     /// Returns `Self` such that [`Self::allowed_origins`] is empty, [`Self::allowed_top_origins`] is `None`,
   1036     /// [`Self::backup_requirement`] is `None`, [`Self::error_on_unsolicited_extensions`] is `true`,
   1037     /// [`Self::auth_attachment_enforcement`] is [`AuthenticatorAttachmentEnforcement::default`],
   1038     /// [`Self::update_uv`] is `false`, [`Self::sig_counter_enforcement`] is
   1039     /// [`SignatureCounterEnforcement::default`], and [`Self::client_data_json_relaxed`] is `true`.
   1040     ///
   1041     /// Note `O` and `T` should implement `PartialEq<Origin<'_>>` (e.g., `&str`).
   1042     #[inline]
   1043     #[must_use]
   1044     pub const fn new() -> Self {
   1045         Self {
   1046             allowed_origins: [].as_slice(),
   1047             allowed_top_origins: None,
   1048             backup_requirement: None,
   1049             error_on_unsolicited_extensions: true,
   1050             auth_attachment_enforcement: AuthenticatorAttachmentEnforcement::Ignore(false),
   1051             update_uv: false,
   1052             sig_counter_enforcement: SignatureCounterEnforcement::Fail,
   1053             #[cfg(feature = "serde_relaxed")]
   1054             client_data_json_relaxed: true,
   1055         }
   1056     }
   1057 }
   1058 impl<O, T> Default for AuthenticationVerificationOptions<'_, '_, O, T> {
   1059     /// Same as [`Self::new`].
   1060     #[inline]
   1061     fn default() -> Self {
   1062         Self::new()
   1063     }
   1064 }
   1065 // This is essentially the `DiscoverableCredentialRequestOptions` used to create it; however to reduce
   1066 // memory usage, we remove all unnecessary data making an instance of this 48 bytes in size
   1067 // `x86_64-unknown-linux-gnu` platforms.
   1068 //
   1069 /// State needed to be saved when beginning the authentication ceremony.
   1070 ///
   1071 /// Saves the necessary information associated with the [`DiscoverableCredentialRequestOptions`] used to create it
   1072 /// via [`DiscoverableCredentialRequestOptions::start_ceremony`] so that authentication of a credential can be
   1073 /// performed with [`Self::verify`].
   1074 ///
   1075 /// `DiscoverableAuthenticationServerState` implements [`Borrow`] of [`SentChallenge`]; thus to obtain the correct
   1076 /// `DiscoverableAuthenticationServerState` associated with a [`DiscoverableAuthentication`], one should use its
   1077 /// corresponding [`DiscoverableAuthentication::challenge`].
   1078 #[derive(Debug)]
   1079 pub struct DiscoverableAuthenticationServerState(AuthenticationServerState);
   1080 impl DiscoverableAuthenticationServerState {
   1081     /// Verifies `response` is valid based on `self` consuming `self` and updating `cred`. Returns `true`
   1082     /// iff `cred` was mutated.
   1083     ///
   1084     /// `rp_id` MUST be the same as the [`PublicKeyCredentialRequestOptions::rp_id`] used when starting the
   1085     /// ceremony.
   1086     ///
   1087     /// It is _essential_ to save [`AuthenticatedCredential::dynamic_state`] overwriting the original value iff `Ok(true)`
   1088     /// is returned.
   1089     ///
   1090     /// # Errors
   1091     ///
   1092     /// Errors iff `response` is not valid according to the
   1093     /// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) or violates any
   1094     /// of the settings in `options`.
   1095     #[inline]
   1096     pub fn verify<
   1097         'a,
   1098         const USER_LEN: usize,
   1099         O: PartialEq<Origin<'a>>,
   1100         T: PartialEq<Origin<'a>>,
   1101         MlDsa87Key: AsRef<[u8]>,
   1102         MlDsa65Key: AsRef<[u8]>,
   1103         MlDsa44Key: AsRef<[u8]>,
   1104         EdKey: AsRef<[u8]>,
   1105         P256Key: AsRef<[u8]>,
   1106         P384Key: AsRef<[u8]>,
   1107         RsaKey: AsRef<[u8]>,
   1108     >(
   1109         self,
   1110         rp_id: &RpId,
   1111         response: &'a DiscoverableAuthentication<USER_LEN>,
   1112         cred: &mut AuthenticatedCredential<
   1113             '_,
   1114             '_,
   1115             USER_LEN,
   1116             CompressedPubKey<MlDsa87Key, MlDsa65Key, MlDsa44Key, EdKey, P256Key, P384Key, RsaKey>,
   1117         >,
   1118         options: &AuthenticationVerificationOptions<'_, '_, O, T>,
   1119     ) -> Result<bool, AuthCeremonyErr> {
   1120         // Step 6 item 2.
   1121         if cred.user_id == response.response.user_handle() {
   1122             // Step 6 item 2.
   1123             if cred.id.as_ref() == response.raw_id.as_ref() {
   1124                 self.0.verify(rp_id, response, cred, options, None)
   1125             } else {
   1126                 Err(AuthCeremonyErr::CredentialIdMismatch)
   1127             }
   1128         } else {
   1129             Err(AuthCeremonyErr::UserHandleMismatch)
   1130         }
   1131     }
   1132     #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))]
   1133     fn is_eq(&self, other: &Self) -> bool {
   1134         self.0.is_eq(&other.0)
   1135     }
   1136 }
   1137 // This is essentially the `NonDiscoverableCredentialRequestOptions` used to create it; however to reduce
   1138 // memory usage, we remove all unnecessary data making an instance of this as small as 64 bytes in size on
   1139 // `x86_64-unknown-linux-gnu` platforms. This does not include the size of each `CredInfo` which should exist
   1140 // elsewhere on the heap but obviously contributes memory overall.
   1141 /// State needed to be saved when beginning the authentication ceremony.
   1142 ///
   1143 /// Saves the necessary information associated with the [`NonDiscoverableCredentialRequestOptions`] used to create
   1144 /// it via [`NonDiscoverableCredentialRequestOptions::start_ceremony`] so that authentication of a credential can be
   1145 /// performed with [`Self::verify`].
   1146 ///
   1147 /// `NonDiscoverableAuthenticationServerState` implements [`Borrow`] of [`SentChallenge`]; thus to obtain the
   1148 /// correct `NonDiscoverableAuthenticationServerState` associated with a [`NonDiscoverableAuthentication`], one
   1149 /// should use its corresponding [`NonDiscoverableAuthentication::challenge`].
   1150 #[derive(Debug)]
   1151 pub struct NonDiscoverableAuthenticationServerState {
   1152     /// Most server state.
   1153     state: AuthenticationServerState,
   1154     /// The set of credentials that are allowed.
   1155     allow_credentials: Box<[CredInfo]>,
   1156 }
   1157 impl NonDiscoverableAuthenticationServerState {
   1158     /// Verifies `response` is valid based on `self` consuming `self` and updating `cred`. Returns `true`
   1159     /// iff `cred` was mutated.
   1160     ///
   1161     /// `rp_id` MUST be the same as the [`PublicKeyCredentialRequestOptions::rp_id`] used when starting the
   1162     /// ceremony.
   1163     ///
   1164     /// It is _essential_ to save [`AuthenticatedCredential::dynamic_state`] overwriting the original value iff `Ok(true)`
   1165     /// is returned.
   1166     ///
   1167     /// # Errors
   1168     ///
   1169     /// Errors iff `response` is not valid according to the
   1170     /// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) or violates any
   1171     /// of the settings in `options`.
   1172     #[inline]
   1173     pub fn verify<
   1174         'a,
   1175         const USER_LEN: usize,
   1176         O: PartialEq<Origin<'a>>,
   1177         T: PartialEq<Origin<'a>>,
   1178         MlDsa87Key: AsRef<[u8]>,
   1179         MlDsa65Key: AsRef<[u8]>,
   1180         MlDsa44Key: AsRef<[u8]>,
   1181         EdKey: AsRef<[u8]>,
   1182         P256Key: AsRef<[u8]>,
   1183         P384Key: AsRef<[u8]>,
   1184         RsaKey: AsRef<[u8]>,
   1185     >(
   1186         self,
   1187         rp_id: &RpId,
   1188         response: &'a NonDiscoverableAuthentication<USER_LEN>,
   1189         cred: &mut AuthenticatedCredential<
   1190             '_,
   1191             '_,
   1192             USER_LEN,
   1193             CompressedPubKey<MlDsa87Key, MlDsa65Key, MlDsa44Key, EdKey, P256Key, P384Key, RsaKey>,
   1194         >,
   1195         options: &AuthenticationVerificationOptions<'_, '_, O, T>,
   1196     ) -> Result<bool, AuthCeremonyErr> {
   1197         response
   1198             .response
   1199             .user_handle()
   1200             .as_ref()
   1201             .map_or(Ok(()), |user| {
   1202                 // Step 6 item 1.
   1203                 if *user == cred.user_id() {
   1204                     Ok(())
   1205                 } else {
   1206                     Err(AuthCeremonyErr::UserHandleMismatch)
   1207                 }
   1208             })
   1209             .and_then(|()| {
   1210                 self.allow_credentials
   1211                     .iter()
   1212                     // Step 5.
   1213                     .find(|c| c.id == response.raw_id)
   1214                     .ok_or(AuthCeremonyErr::NoMatchingAllowedCredential)
   1215                     .and_then(|c| {
   1216                         // Step 6 item 1.
   1217                         if c.id.as_ref() == cred.id.as_ref() {
   1218                             self.state
   1219                                 .verify(rp_id, response, cred, options, Some(c.ext))
   1220                         } else {
   1221                             Err(AuthCeremonyErr::CredentialIdMismatch)
   1222                         }
   1223                     })
   1224             })
   1225     }
   1226     #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))]
   1227     fn is_eq(&self, other: &Self) -> bool {
   1228         self.state.is_eq(&other.state) && self.allow_credentials == other.allow_credentials
   1229     }
   1230 }
   1231 // This is essentially the `PublicKeyCredentialRequestOptions` used to create it; however to reduce
   1232 // memory usage, we remove all unnecessary data making an instance of this 48 bytes in size on
   1233 // `x86_64-unknown-linux-gnu` platforms.
   1234 /// Shared state used by [`DiscoverableAuthenticationServerState`] and [`NonDiscoverableAuthenticationServerState`].
   1235 #[derive(Debug)]
   1236 struct AuthenticationServerState {
   1237     // This is a `SentChallenge` since we need `AuthenticationServerState` to be fetchable after receiving the
   1238     // response from the client. This response must obviously be constructable; thus its challenge is a
   1239     // `SentChallenge`.
   1240     //
   1241     // This must never be mutated since we want to ensure it is actually a `Challenge` (which
   1242     // can only be constructed via `Challenge::new`). This is guaranteed to be true iff
   1243     // `serializable_server_state` is not enabled.
   1244     /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge).
   1245     challenge: SentChallenge,
   1246     /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification).
   1247     user_verification: UserVerificationRequirement,
   1248     /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-extensions).
   1249     extensions: ServerExtensionInfo,
   1250     /// `Instant` the ceremony expires.
   1251     #[cfg(not(feature = "serializable_server_state"))]
   1252     expiration: Instant,
   1253     /// `SystemTime` the ceremony expires.
   1254     #[cfg(feature = "serializable_server_state")]
   1255     expiration: SystemTime,
   1256 }
   1257 impl AuthenticationServerState {
   1258     /// Verifies `response` is valid based on `self` consuming `self` and updating `cred`. Returns `true`
   1259     /// iff `cred` was mutated.
   1260     ///
   1261     /// `rp_id` MUST be the same as the [`PublicKeyCredentialRequestOptions::rp_id`] used when starting the
   1262     /// ceremony.
   1263     ///
   1264     /// It is _essential_ to save [`AuthenticatedCredential::dynamic_state`] overwriting the original value iff `Ok(true)`
   1265     /// is returned.
   1266     ///
   1267     /// # Errors
   1268     ///
   1269     /// Errors iff `response` is not valid according to the
   1270     /// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) or violates any
   1271     /// of the settings in `options`.
   1272     fn verify<
   1273         'a,
   1274         const USER_LEN: usize,
   1275         const DISCOVERABLE: bool,
   1276         O: PartialEq<Origin<'a>>,
   1277         T: PartialEq<Origin<'a>>,
   1278         MlDsa87Key: AsRef<[u8]>,
   1279         MlDsa65Key: AsRef<[u8]>,
   1280         MlDsa44Key: AsRef<[u8]>,
   1281         EdKey: AsRef<[u8]>,
   1282         P256Key: AsRef<[u8]>,
   1283         P384Key: AsRef<[u8]>,
   1284         RsaKey: AsRef<[u8]>,
   1285     >(
   1286         self,
   1287         rp_id: &RpId,
   1288         response: &'a Authentication<USER_LEN, DISCOVERABLE>,
   1289         cred: &mut AuthenticatedCredential<
   1290             '_,
   1291             '_,
   1292             USER_LEN,
   1293             CompressedPubKey<MlDsa87Key, MlDsa65Key, MlDsa44Key, EdKey, P256Key, P384Key, RsaKey>,
   1294         >,
   1295         options: &AuthenticationVerificationOptions<'_, '_, O, T>,
   1296         cred_ext: Option<ServerCredSpecificExtensionInfo>,
   1297     ) -> Result<bool, AuthCeremonyErr> {
   1298         // [Authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion)
   1299         // is handled by:
   1300         //
   1301         // 1. Calling code.
   1302         // 2. Client code and the construction of `resp` (hopefully via [`Authentication::deserialize`]).
   1303         // 3. Client code and the construction of `resp` (hopefully via [`AuthenticatorAssertion::deserialize`]).
   1304         // 4. Client code and the construction of `resp` (hopefully via [`ClientExtensionsOutputs::deserialize`]).
   1305         // 5. [`NonDiscoverableAuthenticationServerState::verify`].
   1306         // 6. [`DiscoverableAuthenticationServerState::verify`] and [`NonDiscoverableAuthenticationServerState::verify`].
   1307         // 7. Informative only in that it defines variables.
   1308         // 8. [`Self::partial_validate`].
   1309         // 9. [`Self::partial_validate`].
   1310         // 10. [`Self::partial_validate`].
   1311         // 11. [`Self::partial_validate`].
   1312         // 12. [`Self::partial_validate`].
   1313         // 13. [`Self::partial_validate`].
   1314         // 14. [`Self::partial_validate`].
   1315         // 15. [`Self::partial_validate`].
   1316         // 16. [`Self::partial_validate`].
   1317         // 17. [`Self::partial_validate`].
   1318         // 18. [`Self::partial_validate`].
   1319         // 19. [`Self::partial_validate`].
   1320         // 20. [`Self::partial_validate`].
   1321         // 21. [`Self::partial_validate`].
   1322         // 22. Below.
   1323         // 23. Below.
   1324         // 24. Below.
   1325         // 25. Below.
   1326 
   1327         // Steps 8–21.
   1328         self.partial_validate(
   1329             rp_id,
   1330             response,
   1331             (&cred.static_state.credential_public_key).into(),
   1332             &CeremonyOptions {
   1333                 allowed_origins: options.allowed_origins,
   1334                 allowed_top_origins: options.allowed_top_origins,
   1335                 backup_requirement: options
   1336                     .backup_requirement
   1337                     .unwrap_or_else(|| BackupReq::from(cred.dynamic_state.backup)),
   1338                 #[cfg(feature = "serde_relaxed")]
   1339                 client_data_json_relaxed: options.client_data_json_relaxed,
   1340             },
   1341         )
   1342         .map_err(AuthCeremonyErr::from)
   1343         .and_then(|auth_data| {
   1344             options
   1345                 .auth_attachment_enforcement
   1346                 .validate(
   1347                     cred.dynamic_state.authenticator_attachment,
   1348                     response.authenticator_attachment,
   1349                 )
   1350                 .and_then(|auth_attachment| {
   1351                     let flags = auth_data.flags();
   1352                     // Step 23.
   1353                     validate_extensions(
   1354                         self.extensions,
   1355                         flags.user_verified,
   1356                         cred_ext,
   1357                         auth_data.extensions(),
   1358                         options.error_on_unsolicited_extensions,
   1359                         cred.static_state.client_extension_results.prf.map_or(
   1360                             CredPrf::None,
   1361                             |prf| {
   1362                                 if prf.enabled {
   1363                                     cred.static_state
   1364                                         .extensions
   1365                                         .hmac_secret
   1366                                         .map_or(CredPrf::TrueNoHmac, |_| CredPrf::TrueTrueHmac)
   1367                                 } else {
   1368                                     cred.static_state
   1369                                         .extensions
   1370                                         .hmac_secret
   1371                                         .map_or(CredPrf::FalseNoHmac, |_| CredPrf::FalseFalseHmac)
   1372                                 }
   1373                             },
   1374                         ),
   1375                     )
   1376                     .map_err(AuthCeremonyErr::Extension)
   1377                     .and_then(|()| {
   1378                         // Step 22.
   1379                         options
   1380                             .sig_counter_enforcement
   1381                             .validate(cred.dynamic_state.sign_count, auth_data.sign_count())
   1382                             .and_then(|sig_counter| {
   1383                                 let prev_dyn_state = cred.dynamic_state;
   1384                                 // Step 24 item 2.
   1385                                 cred.dynamic_state.backup = flags.backup;
   1386                                 if options.update_uv && flags.user_verified {
   1387                                     // Step 24 item 3.
   1388                                     cred.dynamic_state.user_verified = true;
   1389                                 }
   1390                                 // Step 24 item 1.
   1391                                 cred.dynamic_state.sign_count = sig_counter;
   1392                                 cred.dynamic_state.authenticator_attachment = auth_attachment;
   1393                                 // Step 25.
   1394                                 if flags.user_verified {
   1395                                     Ok(())
   1396                                 } else {
   1397                                     match cred.static_state.extensions.cred_protect {
   1398                                         CredentialProtectionPolicy::None | CredentialProtectionPolicy::UserVerificationOptional => Ok(()),
   1399                                         CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList => {
   1400                                             if DISCOVERABLE {
   1401                                                 Err(AuthCeremonyErr::DiscoverableCredProtectCredentialIdList)
   1402                                             } else {
   1403                                                 Ok(())
   1404                                             }
   1405                                         }
   1406                                         CredentialProtectionPolicy::UserVerificationRequired => {
   1407                                             Err(AuthCeremonyErr::UserNotVerifiedCredProtectRequired)
   1408                                         }
   1409                                     }
   1410                                 }.inspect_err(|_| {
   1411                                     cred.dynamic_state = prev_dyn_state;
   1412                                 }).map(|()| {
   1413                                     prev_dyn_state != cred.dynamic_state
   1414                                 })
   1415                             })
   1416                     })
   1417                 })
   1418         })
   1419     }
   1420     #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))]
   1421     fn is_eq(&self, other: &Self) -> bool {
   1422         self.challenge == other.challenge
   1423             && self.user_verification == other.user_verification
   1424             && self.extensions == other.extensions
   1425             && self.expiration == other.expiration
   1426     }
   1427 }
   1428 impl TimedCeremony for AuthenticationServerState {
   1429     #[cfg(any(doc, not(feature = "serializable_server_state")))]
   1430     #[inline]
   1431     fn expiration(&self) -> Instant {
   1432         self.expiration
   1433     }
   1434     #[cfg(all(not(doc), feature = "serializable_server_state"))]
   1435     #[inline]
   1436     fn expiration(&self) -> SystemTime {
   1437         self.expiration
   1438     }
   1439 }
   1440 impl TimedCeremony for DiscoverableAuthenticationServerState {
   1441     #[cfg(any(doc, not(feature = "serializable_server_state")))]
   1442     #[inline]
   1443     fn expiration(&self) -> Instant {
   1444         self.0.expiration()
   1445     }
   1446     #[cfg(all(not(doc), feature = "serializable_server_state"))]
   1447     #[inline]
   1448     fn expiration(&self) -> SystemTime {
   1449         self.0.expiration()
   1450     }
   1451 }
   1452 impl TimedCeremony for NonDiscoverableAuthenticationServerState {
   1453     #[cfg(any(doc, not(feature = "serializable_server_state")))]
   1454     #[inline]
   1455     fn expiration(&self) -> Instant {
   1456         self.state.expiration()
   1457     }
   1458     #[cfg(all(not(doc), feature = "serializable_server_state"))]
   1459     #[inline]
   1460     fn expiration(&self) -> SystemTime {
   1461         self.state.expiration()
   1462     }
   1463 }
   1464 impl<const USER_LEN: usize, const DISCOVERABLE: bool> Ceremony<USER_LEN, DISCOVERABLE>
   1465     for AuthenticationServerState
   1466 {
   1467     type R = Authentication<USER_LEN, DISCOVERABLE>;
   1468     fn rand_challenge(&self) -> SentChallenge {
   1469         self.challenge
   1470     }
   1471     #[cfg(not(feature = "serializable_server_state"))]
   1472     fn expiry(&self) -> Instant {
   1473         self.expiration
   1474     }
   1475     #[cfg(feature = "serializable_server_state")]
   1476     fn expiry(&self) -> SystemTime {
   1477         self.expiration
   1478     }
   1479     fn user_verification(&self) -> UserVerificationRequirement {
   1480         self.user_verification
   1481     }
   1482 }
   1483 impl Borrow<SentChallenge> for AuthenticationServerState {
   1484     #[inline]
   1485     fn borrow(&self) -> &SentChallenge {
   1486         &self.challenge
   1487     }
   1488 }
   1489 impl PartialEq for AuthenticationServerState {
   1490     #[inline]
   1491     fn eq(&self, other: &Self) -> bool {
   1492         self.challenge == other.challenge
   1493     }
   1494 }
   1495 impl PartialEq<&Self> for AuthenticationServerState {
   1496     #[inline]
   1497     fn eq(&self, other: &&Self) -> bool {
   1498         *self == **other
   1499     }
   1500 }
   1501 impl PartialEq<AuthenticationServerState> for &AuthenticationServerState {
   1502     #[inline]
   1503     fn eq(&self, other: &AuthenticationServerState) -> bool {
   1504         **self == *other
   1505     }
   1506 }
   1507 impl Eq for AuthenticationServerState {}
   1508 impl Hash for AuthenticationServerState {
   1509     #[inline]
   1510     fn hash<H: Hasher>(&self, state: &mut H) {
   1511         self.challenge.hash(state);
   1512     }
   1513 }
   1514 impl PartialOrd for AuthenticationServerState {
   1515     #[inline]
   1516     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
   1517         Some(self.cmp(other))
   1518     }
   1519 }
   1520 impl Ord for AuthenticationServerState {
   1521     #[inline]
   1522     fn cmp(&self, other: &Self) -> Ordering {
   1523         self.challenge.cmp(&other.challenge)
   1524     }
   1525 }
   1526 impl Borrow<SentChallenge> for DiscoverableAuthenticationServerState {
   1527     #[inline]
   1528     fn borrow(&self) -> &SentChallenge {
   1529         self.0.borrow()
   1530     }
   1531 }
   1532 impl PartialEq for DiscoverableAuthenticationServerState {
   1533     #[inline]
   1534     fn eq(&self, other: &Self) -> bool {
   1535         self.0 == other.0
   1536     }
   1537 }
   1538 impl PartialEq<&Self> for DiscoverableAuthenticationServerState {
   1539     #[inline]
   1540     fn eq(&self, other: &&Self) -> bool {
   1541         self.0 == other.0
   1542     }
   1543 }
   1544 impl PartialEq<DiscoverableAuthenticationServerState> for &DiscoverableAuthenticationServerState {
   1545     #[inline]
   1546     fn eq(&self, other: &DiscoverableAuthenticationServerState) -> bool {
   1547         self.0 == other.0
   1548     }
   1549 }
   1550 impl Eq for DiscoverableAuthenticationServerState {}
   1551 impl Hash for DiscoverableAuthenticationServerState {
   1552     #[inline]
   1553     fn hash<H: Hasher>(&self, state: &mut H) {
   1554         self.0.hash(state);
   1555     }
   1556 }
   1557 impl PartialOrd for DiscoverableAuthenticationServerState {
   1558     #[inline]
   1559     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
   1560         Some(self.cmp(other))
   1561     }
   1562 }
   1563 impl Ord for DiscoverableAuthenticationServerState {
   1564     #[inline]
   1565     fn cmp(&self, other: &Self) -> Ordering {
   1566         self.0.cmp(&other.0)
   1567     }
   1568 }
   1569 impl Borrow<SentChallenge> for NonDiscoverableAuthenticationServerState {
   1570     #[inline]
   1571     fn borrow(&self) -> &SentChallenge {
   1572         self.state.borrow()
   1573     }
   1574 }
   1575 impl PartialEq for NonDiscoverableAuthenticationServerState {
   1576     #[inline]
   1577     fn eq(&self, other: &Self) -> bool {
   1578         self.state == other.state
   1579     }
   1580 }
   1581 impl PartialEq<&Self> for NonDiscoverableAuthenticationServerState {
   1582     #[inline]
   1583     fn eq(&self, other: &&Self) -> bool {
   1584         self.state == other.state
   1585     }
   1586 }
   1587 impl PartialEq<NonDiscoverableAuthenticationServerState>
   1588     for &NonDiscoverableAuthenticationServerState
   1589 {
   1590     #[inline]
   1591     fn eq(&self, other: &NonDiscoverableAuthenticationServerState) -> bool {
   1592         self.state == other.state
   1593     }
   1594 }
   1595 impl Eq for NonDiscoverableAuthenticationServerState {}
   1596 impl Hash for NonDiscoverableAuthenticationServerState {
   1597     #[inline]
   1598     fn hash<H: Hasher>(&self, state: &mut H) {
   1599         self.state.hash(state);
   1600     }
   1601 }
   1602 impl PartialOrd for NonDiscoverableAuthenticationServerState {
   1603     #[inline]
   1604     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
   1605         Some(self.cmp(other))
   1606     }
   1607 }
   1608 impl Ord for NonDiscoverableAuthenticationServerState {
   1609     #[inline]
   1610     fn cmp(&self, other: &Self) -> Ordering {
   1611         self.state.cmp(&other.state)
   1612     }
   1613 }
   1614 #[cfg(test)]
   1615 mod tests {
   1616     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1617     use super::{
   1618         super::super::{
   1619             AuthenticatedCredential, DynamicState, StaticState, UserHandle,
   1620             response::{
   1621                 Backup,
   1622                 auth::{DiscoverableAuthenticatorAssertion, HmacSecret},
   1623                 register::{
   1624                     AuthenticationExtensionsPrfOutputs, AuthenticatorExtensionOutputStaticState,
   1625                     ClientExtensionsOutputsStaticState, CompressedPubKeyOwned,
   1626                     CredentialProtectionPolicy, Ed25519PubKey,
   1627                 },
   1628             },
   1629         },
   1630         AuthCeremonyErr, AuthenticationVerificationOptions, AuthenticatorAttachment,
   1631         AuthenticatorAttachmentEnforcement, DiscoverableAuthentication, ExtensionErr, OneOrTwo,
   1632         PrfInput, SignatureCounterEnforcement,
   1633     };
   1634     #[cfg(all(
   1635         feature = "custom",
   1636         any(
   1637             feature = "serializable_server_state",
   1638             not(any(feature = "bin", feature = "serde"))
   1639         )
   1640     ))]
   1641     use super::{
   1642         super::{super::AggErr, Challenge, CredentialId, RpId, UserVerificationRequirement},
   1643         DiscoverableCredentialRequestOptions, ExtensionReq,
   1644     };
   1645     #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
   1646     use super::{
   1647         super::{
   1648             super::bin::{Decode as _, Encode as _},
   1649             AsciiDomain, AuthTransports,
   1650         },
   1651         AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Credentials as _,
   1652         DiscoverableAuthenticationServerState, Extension, NonDiscoverableAuthenticationServerState,
   1653         NonDiscoverableCredentialRequestOptions, PrfInputOwned, PublicKeyCredentialDescriptor,
   1654     };
   1655     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1656     use ed25519_dalek::{Signer as _, SigningKey};
   1657     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1658     use rsa::sha2::{Digest as _, Sha256};
   1659     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1660     const CBOR_BYTES: u8 = 0b010_00000;
   1661     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1662     const CBOR_TEXT: u8 = 0b011_00000;
   1663     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1664     const CBOR_MAP: u8 = 0b101_00000;
   1665     #[expect(clippy::panic_in_result_fn, reason = "OK in tests")]
   1666     #[test]
   1667     #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
   1668     fn eddsa_auth_ser() -> Result<(), AggErr> {
   1669         let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
   1670         let mut creds = AllowedCredentials::with_capacity(1);
   1671         _ = creds.push(AllowedCredential {
   1672             credential: PublicKeyCredentialDescriptor {
   1673                 id: CredentialId::try_from(vec![0; 16].into_boxed_slice())?,
   1674                 transports: AuthTransports::NONE,
   1675             },
   1676             extension: CredentialSpecificExtension {
   1677                 prf: Some(PrfInputOwned {
   1678                     first: Vec::new(),
   1679                     second: Some(Vec::new()),
   1680                     ext_req: ExtensionReq::Require,
   1681                 }),
   1682             },
   1683         });
   1684         let mut opts = NonDiscoverableCredentialRequestOptions::second_factor(&rp_id, creds);
   1685         opts.options.user_verification = UserVerificationRequirement::Required;
   1686         opts.options.challenge = Challenge(0);
   1687         opts.options.extensions = Extension { prf: None };
   1688         let server = opts.start_ceremony()?.0;
   1689         let enc_data = server.encode()?;
   1690         assert_eq!(enc_data.capacity(), 16 + 2 + 1 + 1 + 12 + 2 + 128 + 1);
   1691         assert_eq!(enc_data.len(), 16 + 2 + 1 + 1 + 12 + 2 + 16 + 2);
   1692         assert!(
   1693             server.is_eq(&NonDiscoverableAuthenticationServerState::decode(
   1694                 enc_data.as_slice()
   1695             )?)
   1696         );
   1697         let mut opts_2 = DiscoverableCredentialRequestOptions::passkey(&rp_id);
   1698         opts_2.public_key.challenge = Challenge(0);
   1699         opts_2.public_key.extensions = Extension { prf: None };
   1700         let server_2 = opts_2.start_ceremony()?.0;
   1701         let enc_data_2 = server_2
   1702             .encode()
   1703             .map_err(AggErr::EncodeDiscoverableAuthenticationServerState)?;
   1704         assert_eq!(enc_data_2.capacity(), enc_data_2.len());
   1705         assert_eq!(enc_data_2.len(), 16 + 1 + 1 + 12);
   1706         assert!(
   1707             server_2.is_eq(&DiscoverableAuthenticationServerState::decode(
   1708                 enc_data_2.as_slice()
   1709             )?)
   1710         );
   1711         Ok(())
   1712     }
   1713     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1714     #[derive(Clone, Copy)]
   1715     struct TestResponseOptions {
   1716         user_verified: bool,
   1717         hmac: HmacSecret,
   1718     }
   1719     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1720     #[derive(Clone, Copy)]
   1721     enum PrfCredOptions {
   1722         None,
   1723         FalseNoHmac,
   1724         FalseHmacFalse,
   1725         TrueNoHmac,
   1726         TrueHmacTrue,
   1727     }
   1728     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1729     #[derive(Clone, Copy)]
   1730     struct TestCredOptions {
   1731         cred_protect: CredentialProtectionPolicy,
   1732         prf: PrfCredOptions,
   1733     }
   1734     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1735     #[derive(Clone, Copy)]
   1736     enum PrfUvOptions {
   1737         /// `true` iff `UserVerificationRequirement::Required` should be used; otherwise
   1738         /// `UserVerificationRequirement::Preferred` is used.
   1739         None(bool),
   1740         Prf((PrfInput<'static, 'static>, ExtensionReq)),
   1741     }
   1742     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1743     #[derive(Clone, Copy)]
   1744     struct TestRequestOptions {
   1745         error_unsolicited: bool,
   1746         prf_uv: PrfUvOptions,
   1747     }
   1748     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1749     #[derive(Clone, Copy)]
   1750     struct TestOptions {
   1751         request: TestRequestOptions,
   1752         response: TestResponseOptions,
   1753         cred: TestCredOptions,
   1754     }
   1755     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1756     fn generate_client_data_json() -> Vec<u8> {
   1757         let mut json = Vec::with_capacity(256);
   1758         json.extend_from_slice(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice());
   1759         json
   1760     }
   1761     #[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
   1762     #[expect(clippy::too_many_lines, reason = "a lot to test")]
   1763     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1764     fn generate_authenticator_data_public_key_sig(
   1765         opts: TestResponseOptions,
   1766     ) -> (Vec<u8>, CompressedPubKeyOwned, Vec<u8>) {
   1767         let mut authenticator_data = Vec::with_capacity(256);
   1768         authenticator_data.extend_from_slice(
   1769             [
   1770                 // RP ID HASH.
   1771                 // This will be overwritten later.
   1772                 0,
   1773                 0,
   1774                 0,
   1775                 0,
   1776                 0,
   1777                 0,
   1778                 0,
   1779                 0,
   1780                 0,
   1781                 0,
   1782                 0,
   1783                 0,
   1784                 0,
   1785                 0,
   1786                 0,
   1787                 0,
   1788                 0,
   1789                 0,
   1790                 0,
   1791                 0,
   1792                 0,
   1793                 0,
   1794                 0,
   1795                 0,
   1796                 0,
   1797                 0,
   1798                 0,
   1799                 0,
   1800                 0,
   1801                 0,
   1802                 0,
   1803                 0,
   1804                 // FLAGS.
   1805                 // UP, UV, AT, and ED (right-to-left).
   1806                 0b0000_0001
   1807                     | if opts.user_verified {
   1808                         0b0000_0100
   1809                     } else {
   1810                         0b0000_0000
   1811                     }
   1812                     | if matches!(opts.hmac, HmacSecret::None) {
   1813                         0
   1814                     } else {
   1815                         0b1000_0000
   1816                     },
   1817                 // COUNTER.
   1818                 // 0 as 32-bit big endian.
   1819                 0,
   1820                 0,
   1821                 0,
   1822                 0,
   1823             ]
   1824             .as_slice(),
   1825         );
   1826         authenticator_data[..32].copy_from_slice(&Sha256::digest(b"example.com"));
   1827         match opts.hmac {
   1828             HmacSecret::None => {}
   1829             HmacSecret::One => {
   1830                 authenticator_data.extend_from_slice(
   1831                     [
   1832                         CBOR_MAP | 1,
   1833                         // CBOR text of length 11.
   1834                         CBOR_TEXT | 11,
   1835                         b'h',
   1836                         b'm',
   1837                         b'a',
   1838                         b'c',
   1839                         b'-',
   1840                         b's',
   1841                         b'e',
   1842                         b'c',
   1843                         b'r',
   1844                         b'e',
   1845                         b't',
   1846                         CBOR_BYTES | 24,
   1847                         48,
   1848                     ]
   1849                     .as_slice(),
   1850                 );
   1851                 authenticator_data.extend_from_slice([0; 48].as_slice());
   1852             }
   1853             HmacSecret::Two => {
   1854                 authenticator_data.extend_from_slice(
   1855                     [
   1856                         CBOR_MAP | 1,
   1857                         // CBOR text of length 11.
   1858                         CBOR_TEXT | 11,
   1859                         b'h',
   1860                         b'm',
   1861                         b'a',
   1862                         b'c',
   1863                         b'-',
   1864                         b's',
   1865                         b'e',
   1866                         b'c',
   1867                         b'r',
   1868                         b'e',
   1869                         b't',
   1870                         CBOR_BYTES | 24,
   1871                         80,
   1872                     ]
   1873                     .as_slice(),
   1874                 );
   1875                 authenticator_data.extend_from_slice([0; 80].as_slice());
   1876             }
   1877         }
   1878         let len = authenticator_data.len();
   1879         authenticator_data
   1880             .extend_from_slice(&Sha256::digest(generate_client_data_json().as_slice()));
   1881         let sig_key = SigningKey::from_bytes(&[0; 32]);
   1882         let sig = sig_key.sign(authenticator_data.as_slice()).to_vec();
   1883         authenticator_data.truncate(len);
   1884         (
   1885             authenticator_data,
   1886             CompressedPubKeyOwned::Ed25519(Ed25519PubKey::from(sig_key.verifying_key().to_bytes())),
   1887             sig,
   1888         )
   1889     }
   1890     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1891     fn validate(options: TestOptions) -> Result<(), AggErr> {
   1892         let rp_id = RpId::Domain("example.com".to_owned().try_into()?);
   1893         let user = UserHandle::from([0; 1]);
   1894         let (authenticator_data, credential_public_key, signature) =
   1895             generate_authenticator_data_public_key_sig(options.response);
   1896         let credential_id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?;
   1897         let authentication = DiscoverableAuthentication::new(
   1898             credential_id.clone(),
   1899             DiscoverableAuthenticatorAssertion::new(
   1900                 generate_client_data_json(),
   1901                 authenticator_data,
   1902                 signature,
   1903                 user,
   1904             ),
   1905             AuthenticatorAttachment::None,
   1906         );
   1907         let auth_opts = AuthenticationVerificationOptions::<'static, 'static, &str, &str> {
   1908             allowed_origins: [].as_slice(),
   1909             allowed_top_origins: None,
   1910             auth_attachment_enforcement: AuthenticatorAttachmentEnforcement::Update(false),
   1911             backup_requirement: None,
   1912             error_on_unsolicited_extensions: options.request.error_unsolicited,
   1913             sig_counter_enforcement: SignatureCounterEnforcement::Fail,
   1914             update_uv: false,
   1915             #[cfg(feature = "serde_relaxed")]
   1916             client_data_json_relaxed: false,
   1917         };
   1918         let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
   1919         opts.public_key.challenge = Challenge(0);
   1920         opts.public_key.user_verification = UserVerificationRequirement::Preferred;
   1921         match options.request.prf_uv {
   1922             PrfUvOptions::None(required) => {
   1923                 if required {
   1924                     opts.public_key.user_verification = UserVerificationRequirement::Required;
   1925                 }
   1926             }
   1927             PrfUvOptions::Prf(input) => {
   1928                 opts.public_key.user_verification = UserVerificationRequirement::Required;
   1929                 opts.public_key.extensions.prf = Some(input);
   1930             }
   1931         }
   1932         let mut cred = AuthenticatedCredential::new(
   1933             (&credential_id).into(),
   1934             &user,
   1935             StaticState {
   1936                 credential_public_key,
   1937                 extensions: AuthenticatorExtensionOutputStaticState {
   1938                     cred_protect: options.cred.cred_protect,
   1939                     hmac_secret: match options.cred.prf {
   1940                         PrfCredOptions::None
   1941                         | PrfCredOptions::FalseNoHmac
   1942                         | PrfCredOptions::TrueNoHmac => None,
   1943                         PrfCredOptions::FalseHmacFalse => Some(false),
   1944                         PrfCredOptions::TrueHmacTrue => Some(true),
   1945                     },
   1946                 },
   1947                 client_extension_results: ClientExtensionsOutputsStaticState {
   1948                     prf: match options.cred.prf {
   1949                         PrfCredOptions::None => None,
   1950                         PrfCredOptions::FalseNoHmac | PrfCredOptions::FalseHmacFalse => {
   1951                             Some(AuthenticationExtensionsPrfOutputs { enabled: false })
   1952                         }
   1953                         PrfCredOptions::TrueNoHmac | PrfCredOptions::TrueHmacTrue => {
   1954                             Some(AuthenticationExtensionsPrfOutputs { enabled: true })
   1955                         }
   1956                     },
   1957                 },
   1958             },
   1959             DynamicState {
   1960                 user_verified: true,
   1961                 backup: Backup::NotEligible,
   1962                 sign_count: 0,
   1963                 authenticator_attachment: AuthenticatorAttachment::None,
   1964             },
   1965         )?;
   1966         opts.start_ceremony()?
   1967             .0
   1968             .verify(&rp_id, &authentication, &mut cred, &auth_opts)
   1969             .map_err(AggErr::AuthCeremony)
   1970             .map(|_| ())
   1971     }
   1972     /// Test all, and only, possible `UserNotVerified` errors.
   1973     /// 4 * 5 * 3 * 2 * 5 = 600 tests.
   1974     /// We ignore this due to how long it takes (around 4 seconds or so).
   1975     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   1976     #[ignore = "slow"]
   1977     #[test]
   1978     fn uv_required_err() {
   1979         const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [
   1980             CredentialProtectionPolicy::None,
   1981             CredentialProtectionPolicy::UserVerificationOptional,
   1982             CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList,
   1983             CredentialProtectionPolicy::UserVerificationRequired,
   1984         ];
   1985         const ALL_PRF_CRED_OPTIONS: [PrfCredOptions; 5] = [
   1986             PrfCredOptions::None,
   1987             PrfCredOptions::FalseNoHmac,
   1988             PrfCredOptions::FalseHmacFalse,
   1989             PrfCredOptions::TrueNoHmac,
   1990             PrfCredOptions::TrueHmacTrue,
   1991         ];
   1992         const ALL_HMAC_OPTIONS: [HmacSecret; 3] =
   1993             [HmacSecret::None, HmacSecret::One, HmacSecret::Two];
   1994         const ALL_UNSOLICIT_OPTIONS: [bool; 2] = [false, true];
   1995         const ALL_NOT_FALSE_PRF_UV_OPTIONS: [PrfUvOptions; 5] = [
   1996             PrfUvOptions::None(true),
   1997             PrfUvOptions::Prf((
   1998                 PrfInput {
   1999                     first: [].as_slice(),
   2000                     second: None,
   2001                 },
   2002                 ExtensionReq::Require,
   2003             )),
   2004             PrfUvOptions::Prf((
   2005                 PrfInput {
   2006                     first: [].as_slice(),
   2007                     second: None,
   2008                 },
   2009                 ExtensionReq::Allow,
   2010             )),
   2011             PrfUvOptions::Prf((
   2012                 PrfInput {
   2013                     first: [].as_slice(),
   2014                     second: Some([].as_slice()),
   2015                 },
   2016                 ExtensionReq::Require,
   2017             )),
   2018             PrfUvOptions::Prf((
   2019                 PrfInput {
   2020                     first: [].as_slice(),
   2021                     second: Some([].as_slice()),
   2022                 },
   2023                 ExtensionReq::Allow,
   2024             )),
   2025         ];
   2026         for cred_protect in ALL_CRED_PROTECT_OPTIONS {
   2027             for prf in ALL_PRF_CRED_OPTIONS {
   2028                 for hmac in ALL_HMAC_OPTIONS {
   2029                     for error_unsolicited in ALL_UNSOLICIT_OPTIONS {
   2030                         for prf_uv in ALL_NOT_FALSE_PRF_UV_OPTIONS {
   2031                             assert!(validate(TestOptions {
   2032                                 request: TestRequestOptions {
   2033                                     error_unsolicited,
   2034                                     prf_uv,
   2035                                 },
   2036                                 response: TestResponseOptions {
   2037                                     user_verified: false,
   2038                                     hmac,
   2039                                 },
   2040                                 cred: TestCredOptions { cred_protect, prf, },
   2041                             }).is_err_and(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::UserNotVerified))));
   2042                         }
   2043                     }
   2044                 }
   2045             }
   2046         }
   2047     }
   2048     /// Test all, and only, possible `UserNotVerified` errors.
   2049     /// 4 * 5 * 2 * 2 = 80 tests.
   2050     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   2051     #[test]
   2052     fn forbidden_hmac() {
   2053         const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [
   2054             CredentialProtectionPolicy::None,
   2055             CredentialProtectionPolicy::UserVerificationOptional,
   2056             CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList,
   2057             CredentialProtectionPolicy::UserVerificationRequired,
   2058         ];
   2059         const ALL_PRF_CRED_OPTIONS: [PrfCredOptions; 5] = [
   2060             PrfCredOptions::None,
   2061             PrfCredOptions::FalseNoHmac,
   2062             PrfCredOptions::FalseHmacFalse,
   2063             PrfCredOptions::TrueNoHmac,
   2064             PrfCredOptions::TrueHmacTrue,
   2065         ];
   2066         const ALL_HMAC_OPTIONS: [HmacSecret; 2] = [HmacSecret::One, HmacSecret::Two];
   2067         const ALL_UV_OPTIONS: [bool; 2] = [false, true];
   2068         for cred_protect in ALL_CRED_PROTECT_OPTIONS {
   2069             for prf in ALL_PRF_CRED_OPTIONS {
   2070                 for hmac in ALL_HMAC_OPTIONS {
   2071                     for user_verified in ALL_UV_OPTIONS {
   2072                         assert!(validate(TestOptions {
   2073                             request: TestRequestOptions {
   2074                                 error_unsolicited: true,
   2075                                 prf_uv: PrfUvOptions::None(false),
   2076                             },
   2077                             response: TestResponseOptions {
   2078                                 user_verified,
   2079                                 hmac,
   2080                             },
   2081                             cred: TestCredOptions { cred_protect, prf, },
   2082                         }).is_err_and(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::ForbiddenHmacSecret)))));
   2083                     }
   2084                 }
   2085             }
   2086         }
   2087     }
   2088     #[expect(clippy::panic_in_result_fn, reason = "OK in tests")]
   2089     #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
   2090     #[test]
   2091     fn prf() -> Result<(), AggErr> {
   2092         let mut opts = TestOptions {
   2093             request: TestRequestOptions {
   2094                 error_unsolicited: false,
   2095                 prf_uv: PrfUvOptions::Prf((
   2096                     PrfInput {
   2097                         first: [].as_slice(),
   2098                         second: None,
   2099                     },
   2100                     ExtensionReq::Allow,
   2101                 )),
   2102             },
   2103             response: TestResponseOptions {
   2104                 user_verified: true,
   2105                 hmac: HmacSecret::None,
   2106             },
   2107             cred: TestCredOptions {
   2108                 cred_protect: CredentialProtectionPolicy::None,
   2109                 prf: PrfCredOptions::None,
   2110             },
   2111         };
   2112         validate(opts)?;
   2113         opts.request.prf_uv = PrfUvOptions::Prf((
   2114             PrfInput {
   2115                 first: [].as_slice(),
   2116                 second: None,
   2117             },
   2118             ExtensionReq::Require,
   2119         ));
   2120         opts.cred.prf = PrfCredOptions::TrueHmacTrue;
   2121         assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::MissingHmacSecret)))));
   2122         opts.response.hmac = HmacSecret::One;
   2123         opts.request.prf_uv = PrfUvOptions::Prf((
   2124             PrfInput {
   2125                 first: [].as_slice(),
   2126                 second: None,
   2127             },
   2128             ExtensionReq::Allow,
   2129         ));
   2130         opts.cred.prf = PrfCredOptions::TrueNoHmac;
   2131         assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::HmacSecretForNonHmacSecretCredential)))));
   2132         opts.response.hmac = HmacSecret::Two;
   2133         assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidHmacSecretValue(OneOrTwo::One, OneOrTwo::Two))))));
   2134         opts.response.hmac = HmacSecret::One;
   2135         opts.cred.prf = PrfCredOptions::FalseNoHmac;
   2136         assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::HmacSecretForNonHmacSecretCredential)))));
   2137         opts.response.user_verified = false;
   2138         opts.request.prf_uv = PrfUvOptions::None(false);
   2139         opts.cred.prf = PrfCredOptions::TrueHmacTrue;
   2140         assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::UserNotVerifiedHmacSecret)))));
   2141         Ok(())
   2142     }
   2143 }