webauthn_rp

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

request.rs (73243B)


      1 #[cfg(test)]
      2 mod tests;
      3 #[cfg(doc)]
      4 use super::{
      5     hash::hash_set::MaxLenHashSet,
      6     request::{
      7         auth::{
      8             AllowedCredential, AllowedCredentials, CredentialSpecificExtension,
      9             DiscoverableAuthenticationServerState, DiscoverableCredentialRequestOptions,
     10             NonDiscoverableAuthenticationServerState, NonDiscoverableCredentialRequestOptions,
     11             PublicKeyCredentialRequestOptions,
     12         },
     13         register::{CredentialCreationOptions, RegistrationServerState},
     14     },
     15     response::{AuthenticatorAttachment, register::ClientExtensionsOutputs},
     16 };
     17 use crate::{
     18     request::{
     19         error::{
     20             AsciiDomainErr, DomainOriginParseErr, PortParseErr, RpIdErr, SchemeParseErr, UrlErr,
     21         },
     22         register::{BackupReq, RegistrationVerificationOptions},
     23     },
     24     response::{
     25         AuthData as _, AuthDataContainer, AuthResponse, AuthTransports, Backup, CeremonyErr,
     26         CredentialId, Origin, Response, SentChallenge,
     27     },
     28 };
     29 use core::{
     30     borrow::Borrow,
     31     cell::Cell,
     32     fmt::{self, Display, Formatter},
     33     num::NonZeroU32,
     34     str::FromStr,
     35 };
     36 use rsa::sha2::{Digest as _, Sha256};
     37 #[cfg(any(doc, not(feature = "serializable_server_state")))]
     38 use std::time::Instant;
     39 #[cfg(feature = "serializable_server_state")]
     40 use std::time::SystemTime;
     41 use url::Url as Uri;
     42 /// Contains functionality for beginning the
     43 /// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony).
     44 ///
     45 /// # Examples
     46 ///
     47 /// ```
     48 /// # use core::convert;
     49 /// # use webauthn_rp::{
     50 /// #     hash::hash_set::{InsertRemoveExpired, MaxLenHashSet},
     51 /// #     request::{
     52 /// #         auth::{AllowedCredentials, DiscoverableCredentialRequestOptions, NonDiscoverableCredentialRequestOptions},
     53 /// #         register::UserHandle64,
     54 /// #         Credentials, PublicKeyCredentialDescriptor, RpId,
     55 /// #     },
     56 /// #     response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN},
     57 /// #     AggErr,
     58 /// # };
     59 /// const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap();
     60 /// let mut ceremonies = MaxLenHashSet::new(128);
     61 /// let (server, client) = DiscoverableCredentialRequestOptions::passkey(RP_ID).start_ceremony()?;
     62 /// assert_eq!(ceremonies.insert_remove_all_expired(server), InsertRemoveExpired::Success);
     63 /// # #[cfg(feature = "custom")]
     64 /// let mut ceremonies_2 = MaxLenHashSet::new(128);
     65 /// # #[cfg(feature = "serde")]
     66 /// assert!(serde_json::to_string(&client).is_ok());
     67 /// let user_handle = get_user_handle();
     68 /// # #[cfg(feature = "custom")]
     69 /// let creds = get_registered_credentials(&user_handle)?;
     70 /// # #[cfg(feature = "custom")]
     71 /// let (server_2, client_2) =
     72 ///     NonDiscoverableCredentialRequestOptions::second_factor(RP_ID, creds).start_ceremony()?;
     73 /// # #[cfg(feature = "custom")]
     74 /// assert_eq!(ceremonies_2.insert_remove_all_expired(server_2), InsertRemoveExpired::Success);
     75 /// # #[cfg(all(feature = "custom", feature = "serde"))]
     76 /// assert!(serde_json::to_string(&client_2).is_ok());
     77 /// /// Extract `UserHandle` from session cookie.
     78 /// fn get_user_handle() -> UserHandle64 {
     79 ///     // ⋮
     80 /// #     UserHandle64::new()
     81 /// }
     82 /// # #[cfg(feature = "custom")]
     83 /// /// Fetch the `AllowedCredentials` associated with `user`.
     84 /// fn get_registered_credentials(user: &UserHandle64) -> Result<AllowedCredentials, AggErr> {
     85 ///     // ⋮
     86 /// #     let mut creds = AllowedCredentials::new();
     87 /// #     creds.push(
     88 /// #         PublicKeyCredentialDescriptor {
     89 /// #             id: CredentialId::try_from(vec![0; CRED_ID_MIN_LEN].into_boxed_slice())?,
     90 /// #             transports: AuthTransports::NONE,
     91 /// #         }
     92 /// #         .into(),
     93 /// #     );
     94 /// #     Ok(creds)
     95 /// }
     96 /// # Ok::<_, AggErr>(())
     97 /// ```
     98 pub mod auth;
     99 /// Contains error types.
    100 pub mod error;
    101 /// Contains functionality for beginning the
    102 /// [registration ceremony](https://www.w3.org/TR/webauthn-3/#registration-ceremony).
    103 ///
    104 /// # Examples
    105 ///
    106 /// ```
    107 /// # use core::convert;
    108 /// # use webauthn_rp::{
    109 /// #     hash::hash_set::{InsertRemoveExpired, MaxLenHashSet},
    110 /// #     request::{
    111 /// #         register::{
    112 /// #             CredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MAX_LEN, UserHandle64,
    113 /// #         },
    114 /// #         PublicKeyCredentialDescriptor, RpId
    115 /// #     },
    116 /// #     response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN},
    117 /// #     AggErr,
    118 /// # };
    119 /// const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap();
    120 /// # #[cfg(feature = "custom")]
    121 /// let mut ceremonies = MaxLenHashSet::new(128);
    122 /// # #[cfg(feature = "custom")]
    123 /// let user_handle = get_user_handle();
    124 /// # #[cfg(feature = "custom")]
    125 /// let user = get_user_entity(&user_handle)?;
    126 /// # #[cfg(feature = "custom")]
    127 /// let creds = get_registered_credentials(&user_handle)?;
    128 /// # #[cfg(feature = "custom")]
    129 /// let (server, client) = CredentialCreationOptions::passkey(RP_ID, user.clone(), creds)
    130 ///     .start_ceremony()?;
    131 /// # #[cfg(feature = "custom")]
    132 /// assert_eq!(ceremonies.insert_remove_all_expired(server), InsertRemoveExpired::Success);
    133 /// # #[cfg(all(feature = "serde", feature = "custom"))]
    134 /// assert!(serde_json::to_string(&client).is_ok());
    135 /// # #[cfg(feature = "custom")]
    136 /// let creds_2 = get_registered_credentials(&user_handle)?;
    137 /// # #[cfg(feature = "custom")]
    138 /// let (server_2, client_2) =
    139 ///     CredentialCreationOptions::second_factor(RP_ID, user, creds_2).start_ceremony()?;
    140 /// # #[cfg(feature = "custom")]
    141 /// assert_eq!(ceremonies.insert_remove_all_expired(server_2), InsertRemoveExpired::Success);
    142 /// # #[cfg(all(feature = "serde", feature = "custom"))]
    143 /// assert!(serde_json::to_string(&client_2).is_ok());
    144 /// /// Extract `UserHandle` from session cookie or storage if this is not the first credential registered.
    145 /// # #[cfg(feature = "custom")]
    146 /// fn get_user_handle() -> UserHandle64 {
    147 ///     // ⋮
    148 /// #     [0; USER_HANDLE_MAX_LEN].into()
    149 /// }
    150 /// /// Fetch `PublicKeyCredentialUserEntity` info associated with `user`.
    151 /// ///
    152 /// /// If this is the first time a credential is being registered, then `PublicKeyCredentialUserEntity`
    153 /// /// will need to be constructed with `name` and `display_name` passed from the client and `UserHandle::new`
    154 /// /// used for `id`. Once created, this info can be stored such that the entity information
    155 /// /// does not need to be requested for subsequent registrations.
    156 /// # #[cfg(feature = "custom")]
    157 /// fn get_user_entity(user: &UserHandle64) -> Result<PublicKeyCredentialUserEntity<'_, '_, '_, USER_HANDLE_MAX_LEN>, AggErr> {
    158 ///     // ⋮
    159 /// #     Ok(PublicKeyCredentialUserEntity {
    160 /// #         name: "foo",
    161 /// #         id: user,
    162 /// #         display_name: "",
    163 /// #     })
    164 /// }
    165 /// /// Fetch the `PublicKeyCredentialDescriptor`s associated with `user`.
    166 /// ///
    167 /// /// This doesn't need to be called when this is the first credential registered for `user`; instead
    168 /// /// an empty `Vec` should be passed.
    169 /// fn get_registered_credentials(
    170 ///     user: &UserHandle64,
    171 /// ) -> Result<Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>, AggErr> {
    172 ///     // ⋮
    173 /// #     Ok(Vec::new())
    174 /// }
    175 /// # Ok::<_, AggErr>(())
    176 /// ```
    177 pub mod register;
    178 /// Contains functionality to serialize data to a client.
    179 #[cfg(feature = "serde")]
    180 mod ser;
    181 /// Contains functionality to (de)serialize data needed for [`RegistrationServerState`],
    182 /// [`DiscoverableAuthenticationServerState`], and [`NonDiscoverableAuthenticationServerState`] to a data store.
    183 #[cfg(feature = "serializable_server_state")]
    184 pub(super) mod ser_server_state;
    185 // `Challenge` must _never_ be constructable directly or indirectly; thus its tuple field must always be private,
    186 // and it must never implement `trait`s (e.g., `Clone`) that would allow indirect creation. It must only ever
    187 // be constructed via `Self::new` or `Self::default`. In contrast downstream code must be able to construct
    188 // `SentChallenge` since it is used during ceremony validation; thus we must keep `Challenge` and `SentChallenge`
    189 // as separate types.
    190 /// [Cryptographic challenge](https://www.w3.org/TR/webauthn-3/#sctn-cryptographic-challenges).
    191 #[expect(
    192     missing_copy_implementations,
    193     reason = "want to enforce randomly-generated challenges"
    194 )]
    195 #[derive(Debug)]
    196 pub struct Challenge(u128);
    197 impl Challenge {
    198     /// The number of bytes a `Challenge` takes to encode in base64url.
    199     pub(super) const BASE64_LEN: usize = base64url_nopad::encode_len(16);
    200     /// Generates a random `Challenge`.
    201     ///
    202     /// # Examples
    203     ///
    204     /// ```
    205     /// # use webauthn_rp::request::Challenge;
    206     /// // The probability of a `Challenge` being 0 (assuming a good entropy
    207     /// // source) is 2^-128 ≈ 2.9 x 10^-39.
    208     /// assert_ne!(Challenge::new().into_data(), 0);
    209     /// ```
    210     #[inline]
    211     #[must_use]
    212     pub fn new() -> Self {
    213         Self(rand::random())
    214     }
    215     /// Returns the contained `u128` consuming `self`.
    216     #[inline]
    217     #[must_use]
    218     pub const fn into_data(self) -> u128 {
    219         self.0
    220     }
    221     /// Returns the contained `u128`.
    222     #[inline]
    223     #[must_use]
    224     pub const fn as_data(&self) -> u128 {
    225         self.0
    226     }
    227     /// Returns the contained `u128` as a little-endian `array` consuming `self`.
    228     #[inline]
    229     #[must_use]
    230     pub const fn into_array(self) -> [u8; 16] {
    231         self.as_array()
    232     }
    233     /// Returns the contained `u128` as a little-endian `array`.
    234     #[expect(
    235         clippy::little_endian_bytes,
    236         reason = "Challenge and SentChallenge need to be compatible, and we need to ensure the data is sent and received in the same order"
    237     )]
    238     #[inline]
    239     #[must_use]
    240     pub const fn as_array(&self) -> [u8; 16] {
    241         self.0.to_le_bytes()
    242     }
    243 }
    244 impl Default for Challenge {
    245     /// Same as [`Self::new`].
    246     #[inline]
    247     fn default() -> Self {
    248         Self::new()
    249     }
    250 }
    251 impl From<Challenge> for u128 {
    252     #[inline]
    253     fn from(value: Challenge) -> Self {
    254         value.0
    255     }
    256 }
    257 impl From<&Challenge> for u128 {
    258     #[inline]
    259     fn from(value: &Challenge) -> Self {
    260         value.0
    261     }
    262 }
    263 impl From<Challenge> for [u8; 16] {
    264     #[inline]
    265     fn from(value: Challenge) -> Self {
    266         value.into_array()
    267     }
    268 }
    269 impl From<&Challenge> for [u8; 16] {
    270     #[inline]
    271     fn from(value: &Challenge) -> Self {
    272         value.as_array()
    273     }
    274 }
    275 /// A [domain](https://url.spec.whatwg.org/#concept-domain) in representation format consisting of only and any
    276 /// ASCII.
    277 ///
    278 /// The only ASCII character disallowed in a label is `'.'` since it is used exclusively as a separator. Every
    279 /// label must have length inclusively between 1 and 63, and the total length of the domain must be at most 253
    280 /// when a trailing `'.'` does not exist; otherwise the max length is 254. The root domain (i.e., `'.'`) is not
    281 /// allowed.
    282 ///
    283 /// Note if the domain is a `&'static str`, then use [`AsciiDomainStatic`] instead.
    284 #[derive(Clone, Debug, Eq, PartialEq)]
    285 pub struct AsciiDomain(String);
    286 impl AsciiDomain {
    287     /// Removes a trailing `'.'` if it exists.
    288     ///
    289     /// # Examples
    290     ///
    291     /// ```
    292     /// # use webauthn_rp::request::{AsciiDomain, error::AsciiDomainErr};
    293     /// let mut dom = AsciiDomain::try_from("example.com.".to_owned())?;
    294     /// assert_eq!(dom.as_ref(), "example.com.");
    295     /// dom.remove_trailing_dot();
    296     /// assert_eq!(dom.as_ref(), "example.com");
    297     /// dom.remove_trailing_dot();
    298     /// assert_eq!(dom.as_ref(), "example.com");
    299     /// # Ok::<_, AsciiDomainErr>(())
    300     /// ```
    301     #[expect(clippy::unreachable, reason = "want to crash when there is a bug")]
    302     #[inline]
    303     pub fn remove_trailing_dot(&mut self) {
    304         if *self
    305             .0
    306             .as_bytes()
    307             .last()
    308             .unwrap_or_else(|| unreachable!("there is a bug in AsciiDomain::from_slice"))
    309             == b'.'
    310         {
    311             _ = self.0.pop();
    312         }
    313     }
    314 }
    315 impl AsRef<str> for AsciiDomain {
    316     #[inline]
    317     fn as_ref(&self) -> &str {
    318         self.0.as_str()
    319     }
    320 }
    321 impl Borrow<str> for AsciiDomain {
    322     #[inline]
    323     fn borrow(&self) -> &str {
    324         self.0.as_str()
    325     }
    326 }
    327 impl From<AsciiDomain> for String {
    328     #[inline]
    329     fn from(value: AsciiDomain) -> Self {
    330         value.0
    331     }
    332 }
    333 impl PartialEq<&Self> for AsciiDomain {
    334     #[inline]
    335     fn eq(&self, other: &&Self) -> bool {
    336         *self == **other
    337     }
    338 }
    339 impl PartialEq<AsciiDomain> for &AsciiDomain {
    340     #[inline]
    341     fn eq(&self, other: &AsciiDomain) -> bool {
    342         **self == *other
    343     }
    344 }
    345 impl TryFrom<Vec<u8>> for AsciiDomain {
    346     type Error = AsciiDomainErr;
    347     /// Verifies `value` is an ASCII domain in representation format converting any uppercase ASCII into
    348     /// lowercase.
    349     ///
    350     /// Note it is _strongly_ encouraged for `value` to only contain letters, numbers, hyphens, and underscores;
    351     /// otherwise certain applications may consider it not a domain. If the original domain contains non-ASCII, then
    352     /// one must encode it in Punycode _before_ calling this function. Domains that have a trailing `'.'` will be
    353     /// considered differently than domains without it; thus one will likely want to trim it if it does exist
    354     /// (e.g., [`AsciiDomain::remove_trailing_dot`]). Because this allows any ASCII, one may want to ensure `value`
    355     /// is not an IP address.
    356     ///
    357     /// # Errors
    358     ///
    359     /// Errors iff `value` is not a valid ASCII domain.
    360     ///
    361     /// # Examples
    362     ///
    363     /// ```
    364     /// # use webauthn_rp::request::{error::AsciiDomainErr, AsciiDomain};
    365     /// // Root `'.'` is not removed if it exists.
    366     /// assert_ne!("example.com", AsciiDomain::try_from(b"example.com.".to_vec())?.as_ref());
    367     /// // Root domain (i.e., `'.'`) is not allowed.
    368     /// assert!(AsciiDomain::try_from(vec![b'.']).is_err());
    369     /// // Uppercase is transformed into lowercase.
    370     /// assert_eq!("example.com", AsciiDomain::try_from(b"ExAmPle.CoM".to_vec())?.as_ref());
    371     /// // The only ASCII character not allowed in a domain label is `'.'` as it is used exclusively to delimit
    372     /// // labels.
    373     /// assert_eq!("\x00", AsciiDomain::try_from(b"\x00".to_vec())?.as_ref());
    374     /// // Empty labels are not allowed.
    375     /// assert!(AsciiDomain::try_from(b"example..com".to_vec()).is_err());
    376     /// // Labels cannot have length greater than 63.
    377     /// let mut long_label = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned();
    378     /// assert_eq!(long_label.len(), 64);
    379     /// assert!(AsciiDomain::try_from(long_label.clone().into_bytes()).is_err());
    380     /// long_label.pop();
    381     /// assert_eq!(long_label, AsciiDomain::try_from(long_label.clone().into_bytes())?.as_ref());
    382     /// // The maximum length of a domain is 254 if a trailing `'.'` exists; otherwise the max length is 253.
    383     /// let mut long_domain = format!("{long_label}.{long_label}.{long_label}.{long_label}");
    384     /// long_domain.pop();
    385     /// long_domain.push('.');
    386     /// assert_eq!(long_domain.len(), 255);
    387     /// assert!(AsciiDomain::try_from(long_domain.clone().into_bytes()).is_err());
    388     /// long_domain.pop();
    389     /// long_domain.pop();
    390     /// long_domain.push('.');
    391     /// assert_eq!(long_domain.len(), 254);
    392     /// assert_eq!(long_domain, AsciiDomain::try_from(long_domain.clone().into_bytes())?.as_ref());
    393     /// long_domain.pop();
    394     /// long_domain.push('a');
    395     /// assert_eq!(long_domain.len(), 254);
    396     /// assert!(AsciiDomain::try_from(long_domain.clone().into_bytes()).is_err());
    397     /// long_domain.pop();
    398     /// assert_eq!(long_domain.len(), 253);
    399     /// assert_eq!(long_domain, AsciiDomain::try_from(long_domain.clone().into_bytes())?.as_ref());
    400     /// // Only ASCII is allowed; thus if a domain needs to be Punycode-encoded, then it must be _before_ calling
    401     /// // this function.
    402     /// assert!(AsciiDomain::try_from("λ.com".to_owned().into_bytes()).is_err());
    403     /// assert_eq!("xn--wxa.com", AsciiDomain::try_from(b"xn--wxa.com".to_vec())?.as_ref());
    404     /// # Ok::<_, AsciiDomainErr>(())
    405     /// ```
    406     #[expect(unsafe_code, reason = "comment justifies correctness")]
    407     #[expect(
    408         clippy::arithmetic_side_effects,
    409         reason = "comments justify correctness"
    410     )]
    411     #[inline]
    412     fn try_from(mut value: Vec<u8>) -> Result<Self, Self::Error> {
    413         /// Value to add to an uppercase ASCII `u8` to get the lowercase version.
    414         const DIFF: u8 = b'a' - b'A';
    415         let bytes = value.as_slice();
    416         bytes
    417             .as_ref()
    418             .last()
    419             .ok_or(AsciiDomainErr::Empty)
    420             .and_then(|b| {
    421                 let len = bytes.len();
    422                 if *b == b'.' {
    423                     if len == 1 {
    424                         Err(AsciiDomainErr::RootDomain)
    425                     } else if len > 254 {
    426                         Err(AsciiDomainErr::Len)
    427                     } else {
    428                         Ok(())
    429                     }
    430                 } else if len > 253 {
    431                     Err(AsciiDomainErr::Len)
    432                 } else {
    433                     Ok(())
    434                 }
    435             })
    436             .and_then(|()| {
    437                 value
    438                     .iter_mut()
    439                     .try_fold(0u8, |mut label_len, byt| {
    440                         let b = *byt;
    441                         if b == b'.' {
    442                             if label_len == 0 {
    443                                 Err(AsciiDomainErr::EmptyLabel)
    444                             } else {
    445                                 Ok(0)
    446                             }
    447                         } else if label_len == 63 {
    448                             Err(AsciiDomainErr::LabelLen)
    449                         } else {
    450                             // We know `label_len` is less than 63, thus this won't overflow.
    451                             label_len += 1;
    452                             match b {
    453                                 // Non-uppercase ASCII is allowed and doesn't need to be converted.
    454                                 ..b'A' | b'['..=0x7F => Ok(label_len),
    455                                 // Uppercase ASCII is allowed but needs to be transformed into lowercase.
    456                                 b'A'..=b'Z' => {
    457                                     // Lowercase ASCII is a contiguous block starting from `b'a'` as is uppercase
    458                                     // ASCII which starts from `b'A'` with uppercase ASCII coming before; thus we
    459                                     // simply need to shift by a fixed amount.
    460                                     *byt += DIFF;
    461                                     Ok(label_len)
    462                                 }
    463                                 // Non-ASCII is disallowed.
    464                                 0x80.. => Err(AsciiDomainErr::NotAscii),
    465                             }
    466                         }
    467                     })
    468                     .map(|_| {
    469                         // SAFETY:
    470                         // We just verified `value` only contains ASCII; thus this is safe.
    471                         let utf8 = unsafe { String::from_utf8_unchecked(value) };
    472                         Self(utf8)
    473                     })
    474             })
    475     }
    476 }
    477 impl TryFrom<String> for AsciiDomain {
    478     type Error = AsciiDomainErr;
    479     /// Same as [`Self::try_from`] except `value` is a `String`.
    480     #[inline]
    481     fn try_from(value: String) -> Result<Self, Self::Error> {
    482         Self::try_from(value.into_bytes())
    483     }
    484 }
    485 /// Similar to [`AsciiDomain`] except the contained data is a `&'static str`.
    486 ///
    487 /// Since [`Self::new`] and [`Option::unwrap`] are `const fn`s, one can define a global `const` or `static`
    488 /// variable that represents the RP ID.
    489 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    490 pub struct AsciiDomainStatic(&'static str);
    491 impl AsciiDomainStatic {
    492     /// Returns the contained `str`.
    493     #[inline]
    494     #[must_use]
    495     pub const fn as_str(self) -> &'static str {
    496         self.0
    497     }
    498     /// Verifies `domain` is a valid lowercase ASCII domain returning `None` when not valid or when
    499     /// uppercase ASCII exists.
    500     ///
    501     /// Read [`AsciiDomain`] for more information about what constitutes a valid domain.
    502     ///
    503     /// # Examples
    504     ///
    505     /// ```
    506     /// # use webauthn_rp::request::{AsciiDomainStatic, RpId};
    507     /// /// RP ID of our application.
    508     /// const RP_IP: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
    509     /// ```
    510     #[expect(
    511         clippy::arithmetic_side_effects,
    512         reason = "comment justifies correctness"
    513     )]
    514     #[expect(
    515         clippy::else_if_without_else,
    516         reason = "part of if branch and else branch are the same"
    517     )]
    518     #[inline]
    519     #[must_use]
    520     pub const fn new(domain: &'static str) -> Option<Self> {
    521         let mut utf8 = domain.as_bytes();
    522         if let Some(lst) = utf8.last() {
    523             let len = utf8.len();
    524             if *lst == b'.' {
    525                 if len == 1 || len > 254 {
    526                     return None;
    527                 }
    528             } else if len > 253 {
    529                 return None;
    530             }
    531             let mut label_len = 0;
    532             while let [first, ref rest @ ..] = *utf8 {
    533                 if first == b'.' {
    534                     if label_len == 0 {
    535                         return None;
    536                     }
    537                     label_len = 0;
    538                 } else if label_len == 63 {
    539                     return None;
    540                 } else {
    541                     match first {
    542                         // Any non-uppercase ASCII is allowed.
    543                         // We know `label_len` is less than 63, so this won't overflow.
    544                         ..b'A' | b'['..=0x7F => label_len += 1,
    545                         // Uppercase ASCII and non-ASCII are disallowed.
    546                         b'A'..=b'Z' | 0x80.. => return None,
    547                     }
    548                 }
    549                 utf8 = rest;
    550             }
    551             Some(Self(domain))
    552         } else {
    553             None
    554         }
    555     }
    556 }
    557 impl AsRef<str> for AsciiDomainStatic {
    558     #[inline]
    559     fn as_ref(&self) -> &str {
    560         self.as_str()
    561     }
    562 }
    563 impl Borrow<str> for AsciiDomainStatic {
    564     #[inline]
    565     fn borrow(&self) -> &str {
    566         self.as_str()
    567     }
    568 }
    569 impl From<AsciiDomainStatic> for &'static str {
    570     #[inline]
    571     fn from(value: AsciiDomainStatic) -> Self {
    572         value.0
    573     }
    574 }
    575 impl From<AsciiDomainStatic> for String {
    576     #[inline]
    577     fn from(value: AsciiDomainStatic) -> Self {
    578         value.0.to_owned()
    579     }
    580 }
    581 impl From<AsciiDomainStatic> for AsciiDomain {
    582     #[inline]
    583     fn from(value: AsciiDomainStatic) -> Self {
    584         Self(value.0.to_owned())
    585     }
    586 }
    587 impl PartialEq<&Self> for AsciiDomainStatic {
    588     #[inline]
    589     fn eq(&self, other: &&Self) -> bool {
    590         *self == **other
    591     }
    592 }
    593 impl PartialEq<AsciiDomainStatic> for &AsciiDomainStatic {
    594     #[inline]
    595     fn eq(&self, other: &AsciiDomainStatic) -> bool {
    596         **self == *other
    597     }
    598 }
    599 /// The output of the [URL serializer](https://url.spec.whatwg.org/#concept-url-serializer).
    600 #[derive(Clone, Debug, Eq, PartialEq)]
    601 pub struct Url(String);
    602 impl AsRef<str> for Url {
    603     #[inline]
    604     fn as_ref(&self) -> &str {
    605         self.0.as_str()
    606     }
    607 }
    608 impl Borrow<str> for Url {
    609     #[inline]
    610     fn borrow(&self) -> &str {
    611         self.0.as_str()
    612     }
    613 }
    614 impl From<Url> for String {
    615     #[inline]
    616     fn from(value: Url) -> Self {
    617         value.0
    618     }
    619 }
    620 impl PartialEq<&Self> for Url {
    621     #[inline]
    622     fn eq(&self, other: &&Self) -> bool {
    623         *self == **other
    624     }
    625 }
    626 impl PartialEq<Url> for &Url {
    627     #[inline]
    628     fn eq(&self, other: &Url) -> bool {
    629         **self == *other
    630     }
    631 }
    632 impl FromStr for Url {
    633     type Err = UrlErr;
    634     #[inline]
    635     fn from_str(s: &str) -> Result<Self, Self::Err> {
    636         let violation = Cell::new(false);
    637         Uri::options()
    638             .syntax_violation_callback(Some(&|_| {
    639                 violation.set(true);
    640             }))
    641             .parse(s)
    642             .map_err(|_e| UrlErr)
    643             .and_then(|url| {
    644                 if violation.into_inner() {
    645                     Err(UrlErr)
    646                 } else {
    647                     Ok(Self(url.into()))
    648                 }
    649             })
    650     }
    651 }
    652 /// [RP ID](https://w3c.github.io/webauthn/#rp-id).
    653 #[derive(Clone, Debug, Eq, PartialEq)]
    654 pub enum RpId {
    655     /// An ASCII domain.
    656     ///
    657     /// Note web platforms MUST use this variant; and if possible, non-web platforms should too. Also despite
    658     /// the spec currently requiring RP IDs to be
    659     /// [valid domain strings](https://url.spec.whatwg.org/#valid-domain-string), this is unnecessarily strict
    660     /// and will likely be relaxed in a [future version](https://github.com/w3c/webauthn/issues/2206); thus
    661     /// any ASCII domain is allowed.
    662     Domain(AsciiDomain),
    663     /// Similar to [`Self::Domain`] except the ASCII domain is static.
    664     ///
    665     /// Since [`AsciiDomainStatic::new`] is a `const fn`, one can define a `const` or `static` global variable
    666     /// the contains the RP ID.
    667     StaticDomain(AsciiDomainStatic),
    668     /// A URL.
    669     Url(Url),
    670 }
    671 impl RpId {
    672     /// Returns `Some` containing an [`AsciiDomainStatic`] iff [`AsciiDomainStatic::new`] does.
    673     #[inline]
    674     #[must_use]
    675     pub const fn from_static_domain(domain: &'static str) -> Option<Self> {
    676         if let Some(dom) = AsciiDomainStatic::new(domain) {
    677             Some(Self::StaticDomain(dom))
    678         } else {
    679             None
    680         }
    681     }
    682     /// Validates `hash` is the same as the SHA-256 hash of `self`.
    683     fn validate_rp_id_hash<E>(&self, hash: &[u8]) -> Result<(), CeremonyErr<E>> {
    684         if *hash == *Sha256::digest(self.as_ref()) {
    685             Ok(())
    686         } else {
    687             Err(CeremonyErr::RpIdHashMismatch)
    688         }
    689     }
    690 }
    691 impl AsRef<str> for RpId {
    692     #[inline]
    693     fn as_ref(&self) -> &str {
    694         match *self {
    695             Self::Domain(ref dom) => dom.as_ref(),
    696             Self::StaticDomain(dom) => dom.as_str(),
    697             Self::Url(ref url) => url.as_ref(),
    698         }
    699     }
    700 }
    701 impl Borrow<str> for RpId {
    702     #[inline]
    703     fn borrow(&self) -> &str {
    704         match *self {
    705             Self::Domain(ref dom) => dom.borrow(),
    706             Self::StaticDomain(dom) => dom.as_str(),
    707             Self::Url(ref url) => url.borrow(),
    708         }
    709     }
    710 }
    711 impl From<RpId> for String {
    712     #[inline]
    713     fn from(value: RpId) -> Self {
    714         match value {
    715             RpId::Domain(dom) => dom.into(),
    716             RpId::StaticDomain(dom) => dom.into(),
    717             RpId::Url(url) => url.into(),
    718         }
    719     }
    720 }
    721 impl PartialEq<&Self> for RpId {
    722     #[inline]
    723     fn eq(&self, other: &&Self) -> bool {
    724         *self == **other
    725     }
    726 }
    727 impl PartialEq<RpId> for &RpId {
    728     #[inline]
    729     fn eq(&self, other: &RpId) -> bool {
    730         **self == *other
    731     }
    732 }
    733 impl From<AsciiDomain> for RpId {
    734     #[inline]
    735     fn from(value: AsciiDomain) -> Self {
    736         Self::Domain(value)
    737     }
    738 }
    739 impl From<AsciiDomainStatic> for RpId {
    740     #[inline]
    741     fn from(value: AsciiDomainStatic) -> Self {
    742         Self::StaticDomain(value)
    743     }
    744 }
    745 impl From<Url> for RpId {
    746     #[inline]
    747     fn from(value: Url) -> Self {
    748         Self::Url(value)
    749     }
    750 }
    751 impl TryFrom<String> for RpId {
    752     type Error = RpIdErr;
    753     /// Returns `Ok` iff `value` is a valid [`Url`] or [`AsciiDomain`].
    754     ///
    755     /// Note when `value` is a valid `Url` and `AsciiDomain`, it will be treated as a `Url`.
    756     #[inline]
    757     fn try_from(value: String) -> Result<Self, Self::Error> {
    758         Url::from_str(value.as_str())
    759             .map(Self::Url)
    760             .or_else(|_err| {
    761                 AsciiDomain::try_from(value)
    762                     .map(Self::Domain)
    763                     .map_err(|_e| RpIdErr)
    764             })
    765     }
    766 }
    767 /// A URI scheme. This can be used to make
    768 /// [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin) more convenient.
    769 #[derive(Clone, Copy, Debug, Default)]
    770 pub enum Scheme<'a> {
    771     /// A scheme must not exist when validating the origin.
    772     None,
    773     /// Any scheme, or no scheme at all, is allowed to exist when validating the origin.
    774     Any,
    775     /// The HTTPS scheme must exist when validating the origin.
    776     #[default]
    777     Https,
    778     /// The SSH scheme must exist when validating the origin.
    779     Ssh,
    780     /// The contained `str` scheme must exist when validating the origin.
    781     Other(&'a str),
    782     /// [`Self::None`] or [`Self::Https`].
    783     NoneHttps,
    784     /// [`Self::None`] or [`Self::Ssh`].
    785     NoneSsh,
    786     /// [`Self::None`] or [`Self::Other`].
    787     NoneOther(&'a str),
    788 }
    789 impl Scheme<'_> {
    790     /// `self` is any `Scheme`; however `other` is assumed to only be a `Scheme` from a `DomainOrigin` returned
    791     /// from `DomainOrigin::try_from`. The latter implies that `other` is only `Scheme::None`, `Scheme::Https`,
    792     /// `Scheme::Ssh`, or `Scheme::Other`; furthermore when `Scheme::Other`, it won't contain a `str` that is
    793     /// empty or equal to "https" or "ssh".
    794     #[expect(clippy::unreachable, reason = "there is a bug, so we want to crash")]
    795     fn is_equal_to_origin_scheme(self, other: Self) -> bool {
    796         match self {
    797             Self::None => matches!(other, Self::None),
    798             Self::Any => true,
    799             Self::Https => matches!(other, Self::Https),
    800             Self::Ssh => matches!(other, Self::Ssh),
    801             Self::Other(scheme) => match other {
    802                 Self::None => false,
    803                 // We want to crash and burn since there is a bug in code.
    804                 Self::Any | Self::NoneHttps | Self::NoneSsh | Self::NoneOther(_) => {
    805                     unreachable!("there is a bug in DomainOrigin::try_from")
    806                 }
    807                 Self::Https => scheme == "https",
    808                 Self::Ssh => scheme == "ssh",
    809                 Self::Other(scheme_other) => scheme == scheme_other,
    810             },
    811             Self::NoneHttps => match other {
    812                 Self::None | Self::Https => true,
    813                 Self::Ssh | Self::Other(_) => false,
    814                 // We want to crash and burn since there is a bug in code.
    815                 Self::Any | Self::NoneHttps | Self::NoneSsh | Self::NoneOther(_) => {
    816                     unreachable!("there is a bug in DomainOrigin::try_from")
    817                 }
    818             },
    819             Self::NoneSsh => match other {
    820                 Self::None | Self::Ssh => true,
    821                 // We want to crash and burn since there is a bug in code.
    822                 Self::Any | Self::NoneHttps | Self::NoneSsh | Self::NoneOther(_) => {
    823                     unreachable!("there is a bug in DomainOrigin::try_from")
    824                 }
    825                 Self::Https | Self::Other(_) => false,
    826             },
    827             Self::NoneOther(scheme) => match other {
    828                 Self::None => true,
    829                 // We want to crash and burn since there is a bug in code.
    830                 Self::Any | Self::NoneHttps | Self::NoneSsh | Self::NoneOther(_) => {
    831                     unreachable!("there is a bug in DomainOrigin::try_from")
    832                 }
    833                 Self::Https => scheme == "https",
    834                 Self::Ssh => scheme == "ssh",
    835                 Self::Other(scheme_other) => scheme == scheme_other,
    836             },
    837         }
    838     }
    839 }
    840 impl<'a: 'b, 'b> TryFrom<&'a str> for Scheme<'b> {
    841     type Error = SchemeParseErr;
    842     /// `"https"` and `"ssh"` get mapped to [`Self::Https`] and [`Self::Ssh`] respectively. All other
    843     /// values get mapped to [`Self::Other`].
    844     ///
    845     /// # Errors
    846     ///
    847     /// Errors iff `s` is empty.
    848     ///
    849     /// # Examples
    850     ///
    851     /// ```
    852     /// # use webauthn_rp::request::Scheme;
    853     /// assert!(matches!(Scheme::try_from("https")?, Scheme::Https));
    854     /// assert!(matches!(Scheme::try_from("https ")?, Scheme::Other(scheme) if scheme == "https "));
    855     /// assert!(matches!(Scheme::try_from("ssh")?, Scheme::Ssh));
    856     /// assert!(matches!(Scheme::try_from("Ssh")?, Scheme::Other(scheme) if scheme == "Ssh"));
    857     /// // Even though one can construct an empty `Scheme` via `Scheme::Other` or `Scheme::NoneOther`,
    858     /// // one cannot parse one.
    859     /// assert!(Scheme::try_from("").is_err());
    860     /// # Ok::<_, webauthn_rp::AggErr>(())
    861     /// ```
    862     #[inline]
    863     fn try_from(value: &'a str) -> Result<Self, Self::Error> {
    864         match value {
    865             "" => Err(SchemeParseErr),
    866             "https" => Ok(Self::Https),
    867             "ssh" => Ok(Self::Ssh),
    868             _ => Ok(Self::Other(value)),
    869         }
    870     }
    871 }
    872 /// A TCP/UDP port. This can be used to make
    873 /// [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin) more convenient.
    874 #[derive(Clone, Copy, Debug, Default)]
    875 pub enum Port {
    876     /// A port must not exist when validating the origin.
    877     #[default]
    878     None,
    879     /// Any port, or no port at all, is allowed to exist when validating the origin.
    880     Any,
    881     /// The contained `u16` port must exist when validating the origin.
    882     Val(u16),
    883     /// [`Self::None`] or [`Self::Val`].
    884     NoneVal(u16),
    885 }
    886 impl Port {
    887     /// `self` is any `Port`; however `other` is assumed to only be a `Port` from a `DomainOrigin` returned
    888     /// from `DomainOrigin::try_from`. The latter implies that `other` is only `Port::None` or `Port::Val`.
    889     #[expect(clippy::unreachable, reason = "there is a bug, so we want to crash")]
    890     fn is_equal_to_origin_port(self, other: Self) -> bool {
    891         match self {
    892             Self::None => matches!(other, Self::None),
    893             Self::Any => true,
    894             Self::Val(port) => match other {
    895                 Self::None => false,
    896                 // There is a bug in code so we want to crash and burn.
    897                 Self::Any | Self::NoneVal(_) => {
    898                     unreachable!("there is a bug in DomainOrigin::try_from")
    899                 }
    900                 Self::Val(port_other) => port == port_other,
    901             },
    902             Self::NoneVal(port) => match other {
    903                 Self::None => true,
    904                 // There is a bug in code so we want to crash and burn.
    905                 Self::Any | Self::NoneVal(_) => {
    906                     unreachable!("there is a bug in DomainOrigin::try_from")
    907                 }
    908                 Self::Val(port_other) => port == port_other,
    909             },
    910         }
    911     }
    912 }
    913 impl FromStr for Port {
    914     type Err = PortParseErr;
    915     /// Parses `s` as a 16-bit unsigned integer without leading 0s returning [`Self::Val`] with the contained
    916     /// `u16`.
    917     ///
    918     /// # Errors
    919     ///
    920     /// Errors iff `s` is not a valid 16-bit unsigned integer in decimal notation without leading 0s.
    921     ///
    922     /// # Examples
    923     ///
    924     /// ```
    925     /// # use webauthn_rp::request::{error::PortParseErr, Port};
    926     /// assert!(matches!("443".parse()?, Port::Val(443)));
    927     /// // TCP/UDP ports have to be in canonical form:
    928     /// assert!("022"
    929     ///     .parse::<Port>()
    930     ///     .map_or_else(|err| matches!(err, PortParseErr::NotCanonical), |_| false));
    931     /// # Ok::<_, webauthn_rp::AggErr>(())
    932     /// ```
    933     #[inline]
    934     fn from_str(s: &str) -> Result<Self, Self::Err> {
    935         s.parse().map_err(PortParseErr::ParseInt).and_then(|port| {
    936             if s.len()
    937                 == match port {
    938                     ..=9 => 1,
    939                     10..=99 => 2,
    940                     100..=999 => 3,
    941                     1_000..=9_999 => 4,
    942                     10_000.. => 5,
    943                 }
    944             {
    945                 Ok(Self::Val(port))
    946             } else {
    947                 Err(PortParseErr::NotCanonical)
    948             }
    949         })
    950     }
    951 }
    952 /// A [`tuple origin`](https://html.spec.whatwg.org/multipage/browsers.html#concept-origin-tuple).
    953 ///
    954 /// This can be used to make [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin)
    955 /// more convenient.
    956 #[derive(Clone, Copy, Debug)]
    957 pub struct DomainOrigin<'a, 'b> {
    958     /// The scheme.
    959     pub scheme: Scheme<'a>,
    960     /// The host.
    961     pub host: &'b str,
    962     /// The TCP/UDP port.
    963     pub port: Port,
    964 }
    965 impl<'b> DomainOrigin<'_, 'b> {
    966     /// Returns a `DomainOrigin` with [`Self::scheme`] as [`Scheme::Https`], [`Self::host`] as `host`, and
    967     /// [`Self::port`] as [`Port::None`].
    968     ///
    969     /// # Examples
    970     ///
    971     /// ```
    972     /// # extern crate alloc;
    973     /// # use alloc::borrow::Cow;
    974     /// # use webauthn_rp::{request::DomainOrigin, response::Origin};
    975     /// assert_eq!(
    976     ///     DomainOrigin::new("www.example.com"),
    977     ///     Origin(Cow::Borrowed("https://www.example.com"))
    978     /// );
    979     /// // `DomainOrigin::new` does not allow _any_ port to exist.
    980     /// assert_ne!(
    981     ///     DomainOrigin::new("www.example.com"),
    982     ///     Origin(Cow::Borrowed("https://www.example.com:443"))
    983     /// );
    984     /// ```
    985     #[expect(single_use_lifetimes, reason = "false positive")]
    986     #[must_use]
    987     #[inline]
    988     pub const fn new<'c: 'b>(host: &'c str) -> Self {
    989         Self {
    990             scheme: Scheme::Https,
    991             host,
    992             port: Port::None,
    993         }
    994     }
    995     /// Returns a `DomainOrigin` with [`Self::scheme`] as [`Scheme::Https`], [`Self::host`] as `host`, and
    996     /// [`Self::port`] as [`Port::Any`].
    997     ///
    998     /// # Examples
    999     ///
   1000     /// ```
   1001     /// # extern crate alloc;
   1002     /// # use alloc::borrow::Cow;
   1003     /// # use webauthn_rp::{request::DomainOrigin, response::Origin};
   1004     /// // Any port is allowed to exist.
   1005     /// assert_eq!(
   1006     ///     DomainOrigin::new_ignore_port("www.example.com"),
   1007     ///     Origin(Cow::Borrowed("https://www.example.com:1234"))
   1008     /// );
   1009     /// // A port doesn't have to exist at all either.
   1010     /// assert_eq!(
   1011     ///     DomainOrigin::new_ignore_port("www.example.com"),
   1012     ///     Origin(Cow::Borrowed("https://www.example.com"))
   1013     /// );
   1014     /// ```
   1015     #[expect(single_use_lifetimes, reason = "false positive")]
   1016     #[must_use]
   1017     #[inline]
   1018     pub const fn new_ignore_port<'c: 'b>(host: &'c str) -> Self {
   1019         Self {
   1020             scheme: Scheme::Https,
   1021             host,
   1022             port: Port::Any,
   1023         }
   1024     }
   1025 }
   1026 impl PartialEq<Origin<'_>> for DomainOrigin<'_, '_> {
   1027     /// Returns `true` iff [`DomainOrigin::scheme`], [`DomainOrigin::host`], and [`DomainOrigin::port`] are the
   1028     /// same after calling [`DomainOrigin::try_from`] on `other.0.as_str()`.
   1029     ///
   1030     /// Note that [`Scheme`] and [`Port`] need not be the same variant. For example [`Scheme::Https`] and
   1031     /// [`Scheme::Other`] containing `"https"` will be treated the same.
   1032     #[inline]
   1033     fn eq(&self, other: &Origin<'_>) -> bool {
   1034         DomainOrigin::try_from(other.0.as_ref()).is_ok_and(|dom| {
   1035             self.scheme.is_equal_to_origin_scheme(dom.scheme)
   1036                 && self.host == dom.host
   1037                 && self.port.is_equal_to_origin_port(dom.port)
   1038         })
   1039     }
   1040 }
   1041 impl PartialEq<Origin<'_>> for &DomainOrigin<'_, '_> {
   1042     #[inline]
   1043     fn eq(&self, other: &Origin<'_>) -> bool {
   1044         **self == *other
   1045     }
   1046 }
   1047 impl PartialEq<&Origin<'_>> for DomainOrigin<'_, '_> {
   1048     #[inline]
   1049     fn eq(&self, other: &&Origin<'_>) -> bool {
   1050         *self == **other
   1051     }
   1052 }
   1053 impl PartialEq<DomainOrigin<'_, '_>> for Origin<'_> {
   1054     #[inline]
   1055     fn eq(&self, other: &DomainOrigin<'_, '_>) -> bool {
   1056         *other == *self
   1057     }
   1058 }
   1059 impl PartialEq<DomainOrigin<'_, '_>> for &Origin<'_> {
   1060     #[inline]
   1061     fn eq(&self, other: &DomainOrigin<'_, '_>) -> bool {
   1062         *other == **self
   1063     }
   1064 }
   1065 impl PartialEq<&DomainOrigin<'_, '_>> for Origin<'_> {
   1066     #[inline]
   1067     fn eq(&self, other: &&DomainOrigin<'_, '_>) -> bool {
   1068         **other == *self
   1069     }
   1070 }
   1071 impl<'a: 'b + 'c, 'b, 'c> TryFrom<&'a str> for DomainOrigin<'b, 'c> {
   1072     type Error = DomainOriginParseErr;
   1073     /// `value` is parsed according to the following extended regex:
   1074     ///
   1075     /// `^([^:]*:\/\/)?[^:]*(:.*)?$`
   1076     ///
   1077     /// where the `[^:]*` of the first capturing group is parsed according to [`Scheme::try_from`], and
   1078     /// the `.*` of the second capturing group is parsed according to [`Port::from_str`].
   1079     ///
   1080     /// # Errors
   1081     ///
   1082     /// Errors iff `Scheme::try_from` or `Port::from_str` fail when applicable.
   1083     ///
   1084     /// # Examples
   1085     ///
   1086     /// ```
   1087     /// # use webauthn_rp::request::{DomainOrigin, Port, Scheme};
   1088     /// assert!(
   1089     ///     DomainOrigin::try_from("https://www.example.com:443").map_or(false, |dom| matches!(
   1090     ///         dom.scheme,
   1091     ///         Scheme::Https
   1092     ///     ) && dom.host
   1093     ///         == "www.example.com"
   1094     ///         && matches!(dom.port, Port::Val(port) if port == 443))
   1095     /// );
   1096     /// // Parsing is done in a case sensitive way.
   1097     /// assert!(DomainOrigin::try_from("Https://www.EXample.com").map_or(
   1098     ///     false,
   1099     ///     |dom| matches!(dom.scheme, Scheme::Other(scheme) if scheme == "Https")
   1100     ///         && dom.host == "www.EXample.com"
   1101     ///         && matches!(dom.port, Port::None)
   1102     /// ));
   1103     /// ```
   1104     #[inline]
   1105     fn try_from(value: &'a str) -> Result<Self, Self::Error> {
   1106         // Any string that contains `':'` is not a [valid domain](https://url.spec.whatwg.org/#valid-domain), and
   1107         // and `"//"` never exists in a `Port`; thus if `"://"` exists, it's either invalid or delimits the scheme
   1108         // from the rest of the origin.
   1109         match value.split_once("://") {
   1110             None => Ok((Scheme::None, value)),
   1111             Some((poss_scheme, rem)) => Scheme::try_from(poss_scheme)
   1112                 .map_err(DomainOriginParseErr::Scheme)
   1113                 .map(|scheme| (scheme, rem)),
   1114         }
   1115         .and_then(|(scheme, rem)| {
   1116             // `':'` never exists in a valid domain; thus if it exists, it's either invalid or
   1117             // separates the domain from the port.
   1118             rem.split_once(':')
   1119                 .map_or_else(
   1120                     || Ok((rem, Port::None)),
   1121                     |(rem2, poss_port)| {
   1122                         Port::from_str(poss_port)
   1123                             .map_err(DomainOriginParseErr::Port)
   1124                             .map(|port| (rem2, port))
   1125                     },
   1126                 )
   1127                 .map(|(host, port)| Self { scheme, host, port })
   1128         })
   1129     }
   1130 }
   1131 /// [`PublicKeyCredentialDescriptor`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptor)
   1132 /// associated with a registered credential.
   1133 #[derive(Clone, Debug)]
   1134 pub struct PublicKeyCredentialDescriptor<T> {
   1135     /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-id).
   1136     pub id: CredentialId<T>,
   1137     /// [`transports`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-transports).
   1138     pub transports: AuthTransports,
   1139 }
   1140 /// [`UserVerificationRequirement`](https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement).
   1141 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
   1142 pub enum UserVerificationRequirement {
   1143     /// [`required`](https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-required).
   1144     Required,
   1145     /// [`discouraged`](https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-discouraged).
   1146     ///
   1147     /// Note some authenticators always require user verification when registering a credential (e.g.,
   1148     /// [CTAP 2.0](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html)
   1149     /// authenticators that have had a PIN enabled).
   1150     Discouraged,
   1151     /// [`preferred`](https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-preferred).
   1152     Preferred,
   1153 }
   1154 /// [`PublicKeyCredentialHint`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint).
   1155 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
   1156 pub enum PublicKeyCredentialHint {
   1157     /// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-security-key).
   1158     SecurityKey,
   1159     /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-client-device).
   1160     ClientDevice,
   1161     /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-hybrid).
   1162     Hybrid,
   1163 }
   1164 impl PublicKeyCredentialHint {
   1165     /// Returns `true` iff `self` is the same as `other`.
   1166     const fn is_eq(self, other: Self) -> bool {
   1167         match self {
   1168             Self::SecurityKey => matches!(other, Self::SecurityKey),
   1169             Self::ClientDevice => matches!(other, Self::ClientDevice),
   1170             Self::Hybrid => matches!(other, Self::Hybrid),
   1171         }
   1172     }
   1173 }
   1174 /// Unique sequence of [`PublicKeyCredentialHint`].
   1175 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
   1176 pub struct Hints([Option<PublicKeyCredentialHint>; 3]);
   1177 impl Hints {
   1178     /// Empty sequence of [`PublicKeyCredentialHint`]s.
   1179     pub const EMPTY: Self = Self([None; 3]);
   1180     /// Adds `hint` to `self` iff `self` doesn't already contain `hint`.
   1181     ///
   1182     /// # Examples
   1183     ///
   1184     /// ```
   1185     /// # use webauthn_rp::request::{Hints, PublicKeyCredentialHint};
   1186     /// assert_eq!(
   1187     ///     Hints::EMPTY
   1188     ///         .add(PublicKeyCredentialHint::SecurityKey)
   1189     ///         .first(),
   1190     ///     Some(PublicKeyCredentialHint::SecurityKey)
   1191     /// );
   1192     /// ```
   1193     #[inline]
   1194     #[must_use]
   1195     pub const fn add(mut self, hint: PublicKeyCredentialHint) -> Self {
   1196         let mut vals = self.0.as_mut_slice();
   1197         while let [ref mut first, ref mut rem @ ..] = *vals {
   1198             match *first {
   1199                 None => {
   1200                     *first = Some(hint);
   1201                     return self;
   1202                 }
   1203                 Some(h) => {
   1204                     if h.is_eq(hint) {
   1205                         return self;
   1206                     }
   1207                 }
   1208             }
   1209             vals = rem;
   1210         }
   1211         self
   1212     }
   1213     /// Returns the first `PublicKeyCredentialHint`.
   1214     ///
   1215     /// # Examples
   1216     ///
   1217     /// ```
   1218     /// # use webauthn_rp::request::Hints;
   1219     /// assert!(Hints::EMPTY.first().is_none());
   1220     /// ```
   1221     #[inline]
   1222     #[must_use]
   1223     pub const fn first(self) -> Option<PublicKeyCredentialHint> {
   1224         self.0[0]
   1225     }
   1226     /// Returns the second `PublicKeyCredentialHint`.
   1227     ///
   1228     /// # Examples
   1229     ///
   1230     /// ```
   1231     /// # use webauthn_rp::request::Hints;
   1232     /// assert!(Hints::EMPTY.second().is_none());
   1233     /// ```
   1234     #[inline]
   1235     #[must_use]
   1236     pub const fn second(self) -> Option<PublicKeyCredentialHint> {
   1237         self.0[1]
   1238     }
   1239     /// Returns the third `PublicKeyCredentialHint`.
   1240     ///
   1241     /// # Examples
   1242     ///
   1243     /// ```
   1244     /// # use webauthn_rp::request::Hints;
   1245     /// assert!(Hints::EMPTY.third().is_none());
   1246     /// ```
   1247     #[inline]
   1248     #[must_use]
   1249     pub const fn third(self) -> Option<PublicKeyCredentialHint> {
   1250         self.0[2]
   1251     }
   1252     /// Returns the number of [`PublicKeyCredentialHint`]s in `self`.
   1253     ///
   1254     /// # Examples
   1255     ///
   1256     /// ```
   1257     /// # use webauthn_rp::request::Hints;
   1258     /// assert_eq!(Hints::EMPTY.count(), 0);
   1259     /// ```
   1260     #[expect(
   1261         clippy::arithmetic_side_effects,
   1262         clippy::as_conversions,
   1263         reason = "comment justifies correctness"
   1264     )]
   1265     #[inline]
   1266     #[must_use]
   1267     pub const fn count(self) -> u8 {
   1268         // `bool as u8` is well-defined. This maxes at 3, so overflow isn't possible.
   1269         self.first().is_some() as u8 + self.second().is_some() as u8 + self.third().is_some() as u8
   1270     }
   1271     /// Returns `true` iff `self` is empty.
   1272     ///
   1273     /// # Examples
   1274     ///
   1275     /// ```
   1276     /// # use webauthn_rp::request::Hints;
   1277     /// assert!(Hints::EMPTY.is_empty());
   1278     /// ```
   1279     #[inline]
   1280     #[must_use]
   1281     pub const fn is_empty(self) -> bool {
   1282         self.count() == 0
   1283     }
   1284     /// Returns `true` iff `self` contains `hint`.
   1285     ///
   1286     /// # Examples
   1287     ///
   1288     /// ```
   1289     /// # use webauthn_rp::request::{Hints, PublicKeyCredentialHint};
   1290     /// assert!(!Hints::EMPTY.contains(PublicKeyCredentialHint::Hybrid));
   1291     /// ```
   1292     #[inline]
   1293     #[must_use]
   1294     pub const fn contains(self, hint: PublicKeyCredentialHint) -> bool {
   1295         let mut vals = self.0.as_slice();
   1296         while let [ref first, ref rem @ ..] = *vals {
   1297             match *first {
   1298                 None => return false,
   1299                 Some(h) => {
   1300                     if h.is_eq(hint) {
   1301                         return true;
   1302                     }
   1303                 }
   1304             }
   1305             vals = rem;
   1306         }
   1307         false
   1308     }
   1309     /// Returns `true` iff `self` contains a `hint` that maps to [`AuthenticatorAttachment::CrossPlatform`].
   1310     ///
   1311     /// # Examples
   1312     ///
   1313     /// ```
   1314     /// # use webauthn_rp::request::Hints;
   1315     /// assert!(!Hints::EMPTY.contains_platform_hints());
   1316     /// ```
   1317     #[inline]
   1318     #[must_use]
   1319     pub const fn contains_cross_platform_hints(self) -> bool {
   1320         let mut vals = self.0.as_slice();
   1321         while let [ref first, ref rem @ ..] = *vals {
   1322             match *first {
   1323                 None => return false,
   1324                 Some(h) => {
   1325                     if matches!(
   1326                         h,
   1327                         PublicKeyCredentialHint::SecurityKey | PublicKeyCredentialHint::Hybrid
   1328                     ) {
   1329                         return true;
   1330                     }
   1331                 }
   1332             }
   1333             vals = rem;
   1334         }
   1335         false
   1336     }
   1337     /// Returns `true` iff `self` contains a `hint` that maps to [`AuthenticatorAttachment::Platform`].
   1338     ///
   1339     /// # Examples
   1340     ///
   1341     /// ```
   1342     /// # use webauthn_rp::request::Hints;
   1343     /// assert!(!Hints::EMPTY.contains_platform_hints());
   1344     /// ```
   1345     #[inline]
   1346     #[must_use]
   1347     pub const fn contains_platform_hints(self) -> bool {
   1348         let mut vals = self.0.as_slice();
   1349         while let [ref first, ref rem @ ..] = *vals {
   1350             match *first {
   1351                 None => return false,
   1352                 Some(h) => {
   1353                     if h.is_eq(PublicKeyCredentialHint::ClientDevice) {
   1354                         return true;
   1355                     }
   1356                 }
   1357             }
   1358             vals = rem;
   1359         }
   1360         false
   1361     }
   1362 }
   1363 /// Controls if the response to a requested extension is required to be sent back.
   1364 ///
   1365 /// Note when requiring an extension, the extension must not only be sent back but also
   1366 /// contain at least one expected field (e.g., [`ClientExtensionsOutputs::cred_props`] must be
   1367 /// `Some(CredentialPropertiesOutput { rk: Some(_) })`.
   1368 ///
   1369 /// If one wants to additionally control the values of an extension, use [`ExtensionInfo`].
   1370 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
   1371 pub enum ExtensionReq {
   1372     /// The response to a requested extension is required to be sent back.
   1373     Require,
   1374     /// The response to a requested extension is allowed, but not required, to be sent back.
   1375     Allow,
   1376 }
   1377 /// Dictates how an extension should be processed.
   1378 ///
   1379 /// If one wants to only control if the extension should be returned, use [`ExtensionReq`].
   1380 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
   1381 pub enum ExtensionInfo {
   1382     /// Require the associated extension and enforce its value.
   1383     RequireEnforceValue,
   1384     /// Require the associated extension but don't enforce its value.
   1385     RequireDontEnforceValue,
   1386     /// Allow the associated extension to exist and enforce its value when it does exist.
   1387     AllowEnforceValue,
   1388     /// Allow the associated extension to exist but don't enforce its value.
   1389     AllowDontEnforceValue,
   1390 }
   1391 impl Display for ExtensionInfo {
   1392     #[inline]
   1393     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
   1394         f.write_str(match *self {
   1395             Self::RequireEnforceValue => "require the corresponding extension response and enforce its value",
   1396             Self::RequireDontEnforceValue => "require the corresponding extension response but don't enforce its value",
   1397             Self::AllowEnforceValue => "don't require the corresponding extension response; but if sent, enforce its value",
   1398             Self::AllowDontEnforceValue => "don't require the corresponding extension response; and if sent, don't enforce its value",
   1399         })
   1400     }
   1401 }
   1402 /// [`CredentialMediationRequirement`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement).
   1403 ///
   1404 /// Note [`silent`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-silent)
   1405 /// is not supported for WebAuthn credentials, and
   1406 /// [`optional`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-optional)
   1407 /// is just an alias for [`Self::Required`].
   1408 #[expect(clippy::doc_markdown, reason = "false positive")]
   1409 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
   1410 pub enum CredentialMediationRequirement {
   1411     /// [`required`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-required).
   1412     ///
   1413     /// This is the default mediation for ceremonies.
   1414     #[default]
   1415     Required,
   1416     /// [`conditional`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-conditional).
   1417     ///
   1418     /// Note that when registering a new credential with [`CredentialCreationOptions::mediation`] set to
   1419     /// `Self::Conditional`, [`UserVerificationRequirement::Required`] MUST NOT be used unless user verification
   1420     /// can be explicitly performed during the ceremony.
   1421     Conditional,
   1422 }
   1423 /// A container of "credentials".
   1424 ///
   1425 /// This is mainly a way to unify [`Vec`] of [`PublicKeyCredentialDescriptor`]
   1426 /// and [`AllowedCredentials`]. This can be useful in situations when one only
   1427 /// deals with [`AllowedCredential`]s with empty [`CredentialSpecificExtension`]s
   1428 /// essentially making them the same as [`PublicKeyCredentialDescriptor`]s.
   1429 ///
   1430 /// # Examples
   1431 ///
   1432 /// ```
   1433 /// # use webauthn_rp::{
   1434 /// #     request::{
   1435 /// #         auth::AllowedCredentials, register::UserHandle, Credentials, PublicKeyCredentialDescriptor,
   1436 /// #     },
   1437 /// #     response::{AuthTransports, CredentialId},
   1438 /// # };
   1439 /// /// Fetches all credentials under `user_handle` to be allowed during authentication for non-discoverable
   1440 /// /// requests.
   1441 /// # #[cfg(feature = "custom")]
   1442 /// fn get_allowed_credentials<const LEN: usize>(user_handle: &UserHandle<LEN>) -> AllowedCredentials {
   1443 ///     get_credentials(user_handle)
   1444 /// }
   1445 /// /// Fetches all credentials under `user_handle` to be excluded during registration.
   1446 /// # #[cfg(feature = "custom")]
   1447 /// fn get_excluded_credentials<const LEN: usize>(
   1448 ///     user_handle: &UserHandle<LEN>,
   1449 /// ) -> Vec<PublicKeyCredentialDescriptor<Box<[u8]>>> {
   1450 ///     get_credentials(user_handle)
   1451 /// }
   1452 /// /// Used to fetch the excluded `PublicKeyCredentialDescriptor`s associated with `user_handle` during
   1453 /// /// registration as well as the `AllowedCredentials` containing `AllowedCredential`s with no credential-specific
   1454 /// /// extensions which is used for non-discoverable requests.
   1455 /// # #[cfg(feature = "custom")]
   1456 /// fn get_credentials<const LEN: usize, T>(user_handle: &UserHandle<LEN>) -> T
   1457 /// where
   1458 ///     T: Credentials,
   1459 ///     PublicKeyCredentialDescriptor<Box<[u8]>>: Into<T::Credential>,
   1460 /// {
   1461 ///     let iter = get_cred_parts(user_handle);
   1462 ///     let len = iter.size_hint().0;
   1463 ///     iter.fold(T::with_capacity(len), |mut creds, parts| {
   1464 ///         creds.push(
   1465 ///             PublicKeyCredentialDescriptor {
   1466 ///                 id: parts.0,
   1467 ///                 transports: parts.1,
   1468 ///             }
   1469 ///             .into(),
   1470 ///         );
   1471 ///         creds
   1472 ///     })
   1473 /// }
   1474 /// /// Fetches all `CredentialId`s and associated `AuthTransports` under `user_handle`
   1475 /// /// from the database.
   1476 /// # #[cfg(feature = "custom")]
   1477 /// fn get_cred_parts<const LEN: usize>(
   1478 ///     user_handle: &UserHandle<LEN>,
   1479 /// ) -> impl Iterator<Item = (CredentialId<Box<[u8]>>, AuthTransports)> {
   1480 ///     // ⋮
   1481 /// #     [(
   1482 /// #         CredentialId::try_from(vec![0; 16].into_boxed_slice()).unwrap(),
   1483 /// #         AuthTransports::NONE,
   1484 /// #     )]
   1485 /// #     .into_iter()
   1486 /// }
   1487 /// ```
   1488 pub trait Credentials: Sized {
   1489     /// The "credential"s that make up `Self`.
   1490     type Credential;
   1491     /// Returns `Self`.
   1492     #[inline]
   1493     #[must_use]
   1494     fn new() -> Self {
   1495         Self::with_capacity(0)
   1496     }
   1497     /// Returns `Self` with at least `capacity` allocated.
   1498     fn with_capacity(capacity: usize) -> Self;
   1499     /// Adds `cred` to `self`.
   1500     ///
   1501     /// Returns `true` iff `cred` was added.
   1502     fn push(&mut self, cred: Self::Credential) -> bool;
   1503     /// Returns the number of [`Self::Credential`]s in `Self`.
   1504     fn len(&self) -> usize;
   1505     /// Returns `true` iff [`Self::len`] is `0`.
   1506     #[inline]
   1507     fn is_empty(&self) -> bool {
   1508         self.len() == 0
   1509     }
   1510 }
   1511 impl<T> Credentials for Vec<T> {
   1512     type Credential = T;
   1513     #[inline]
   1514     fn with_capacity(capacity: usize) -> Self {
   1515         Self::with_capacity(capacity)
   1516     }
   1517     #[inline]
   1518     fn push(&mut self, cred: Self::Credential) -> bool {
   1519         self.push(cred);
   1520         true
   1521     }
   1522     #[inline]
   1523     fn len(&self) -> usize {
   1524         self.len()
   1525     }
   1526 }
   1527 /// Additional options that control how [`Ceremony::partial_validate`] works.
   1528 struct CeremonyOptions<'origins, 'top_origins, O, T> {
   1529     /// Origins to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin).
   1530     ///
   1531     /// When this is empty, the origin that will be used will be based on
   1532     /// the [`RpId`] passed to [`RegistrationServerState::verify`]. If [`RpId::Domain`], then the [`DomainOrigin`] returned from
   1533     /// passing [`AsciiDomain::as_ref`] to [`DomainOrigin::new`] will be used; otherwise the [`Url`] in
   1534     /// [`RpId::Url`] will be used.
   1535     allowed_origins: &'origins [O],
   1536     /// [Top-level origins](https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-top-level-origin)
   1537     /// to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin).
   1538     ///
   1539     /// When this is `Some`, [`CollectedClientData::cross_origin`] is allowed to be `true`. When the contained
   1540     /// `slice` is empty, [`CollectedClientData::top_origin`] must be `None`. When this is `None`,
   1541     /// `CollectedClientData::cross_origin` must be `false` and `CollectedClientData::top_origin` must be `None`.
   1542     allowed_top_origins: Option<&'top_origins [T]>,
   1543     /// The required [`Backup`] state of the credential.
   1544     backup_requirement: BackupReq,
   1545     /// [`CollectedClientData::from_client_data_json_relaxed`] is used to extract [`CollectedClientData`] iff `true`.
   1546     #[cfg(feature = "serde_relaxed")]
   1547     client_data_json_relaxed: bool,
   1548 }
   1549 impl<'o, 't, O, T> From<&RegistrationVerificationOptions<'o, 't, O, T>>
   1550     for CeremonyOptions<'o, 't, O, T>
   1551 {
   1552     fn from(value: &RegistrationVerificationOptions<'o, 't, O, T>) -> Self {
   1553         Self {
   1554             allowed_origins: value.allowed_origins,
   1555             allowed_top_origins: value.allowed_top_origins,
   1556             backup_requirement: value.backup_requirement,
   1557             #[cfg(feature = "serde_relaxed")]
   1558             client_data_json_relaxed: value.client_data_json_relaxed,
   1559         }
   1560     }
   1561 }
   1562 /// Functionality common to both registration and authentication ceremonies.
   1563 ///
   1564 /// Designed to be implemented on the _request_ side.
   1565 trait Ceremony<const USER_LEN: usize, const DISCOVERABLE: bool> {
   1566     /// The type of response that is associated with the ceremony.
   1567     type R: Response;
   1568     /// Challenge.
   1569     fn rand_challenge(&self) -> SentChallenge;
   1570     /// `Instant` the ceremony was expires.
   1571     #[cfg(not(feature = "serializable_server_state"))]
   1572     fn expiry(&self) -> Instant;
   1573     /// `Instant` the ceremony was expires.
   1574     #[cfg(feature = "serializable_server_state")]
   1575     fn expiry(&self) -> SystemTime;
   1576     /// User verification requirement.
   1577     fn user_verification(&self) -> UserVerificationRequirement;
   1578     /// Performs validation of ceremony criteria common to both ceremony types.
   1579     #[expect(
   1580         clippy::type_complexity,
   1581         reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable"
   1582     )]
   1583     #[expect(clippy::too_many_lines, reason = "102 lines is fine")]
   1584     fn partial_validate<'a, O: PartialEq<Origin<'a>>, T: PartialEq<Origin<'a>>>(
   1585         &self,
   1586         rp_id: &RpId,
   1587         resp: &'a Self::R,
   1588         key: <<Self::R as Response>::Auth as AuthResponse>::CredKey<'_>,
   1589         options: &CeremonyOptions<'_, '_, O, T>,
   1590     ) -> Result<
   1591         <<Self::R as Response>::Auth as AuthResponse>::Auth<'a>,
   1592         CeremonyErr<
   1593             <<<Self::R as Response>::Auth as AuthResponse>::Auth<'a> as AuthDataContainer<'a>>::Err,
   1594         >,
   1595     > {
   1596         // [Registration ceremony](https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential)
   1597         // is handled by:
   1598         //
   1599         // 1. Calling code.
   1600         // 2. Client code and the construction of `resp` (hopefully via [`Registration::deserialize`]).
   1601         // 3. Client code and the construction of `resp` (hopefully via [`AuthenticatorAttestation::deserialize`]).
   1602         // 4. Client code and the construction of `resp` (hopefully via [`ClientExtensionsOutputs::deserialize`]).
   1603         // 5. Below via [`CollectedClientData::from_client_data_json_relaxed`].
   1604         // 6. Below via [`CollectedClientData::from_client_data_json_relaxed`] or [`CollectedClientData::from_client_data_json_relaxed`].
   1605         // 7. Below via [`CollectedClientData::from_client_data_json_relaxed`] or [`CollectedClientData::from_client_data_json_relaxed`].
   1606         // 8. Below.
   1607         // 9. Below.
   1608         // 10. Below.
   1609         // 11. Below.
   1610         // 12. Below via [`AuthenticatorAttestation::new`].
   1611         // 13. Below via [`AttestationObject::parse_data`].
   1612         // 14. Below.
   1613         // 15. [`RegistrationServerState::verify`].
   1614         // 16. Below.
   1615         // 17. Below via [`AuthenticatorData::from_cbor`].
   1616         // 18. Below.
   1617         // 19. Below.
   1618         // 20. [`RegistrationServerState::verify`].
   1619         // 21. Below via [`AttestationObject::parse_data`].
   1620         // 22. Below via [`AttestationObject::parse_data`].
   1621         // 23. N/A since only none and self attestations are supported.
   1622         // 24. Always satisfied since only none and self attestations are supported (Item 3 is N/A).
   1623         // 25. Below via [`AttestedCredentialData::from_cbor`].
   1624         // 26. Calling code.
   1625         // 27. [`RegistrationServerState::verify`].
   1626         // 28. N/A since only none and self attestations are supported.
   1627         // 29. [`RegistrationServerState::verify`].
   1628         //
   1629         //
   1630         // [Authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion)
   1631         // is handled by:
   1632         //
   1633         // 1. Calling code.
   1634         // 2. Client code and the construction of `resp` (hopefully via [`Authentication::deserialize`]).
   1635         // 3. Client code and the construction of `resp` (hopefully via [`AuthenticatorAssertion::deserialize`]).
   1636         // 4. Client code and the construction of `resp` (hopefully via [`ClientExtensionsOutputs::deserialize`]).
   1637         // 5. [`AuthenticationServerState::verify`].
   1638         // 6. [`AuthenticationServerState::verify`].
   1639         // 7. Informative only in that it defines variables.
   1640         // 8. Below via [`CollectedClientData::from_client_data_json_relaxed`].
   1641         // 9. Below via [`CollectedClientData::from_client_data_json_relaxed`] or [`CollectedClientData::from_client_data_json_relaxed`].
   1642         // 10. Below via [`CollectedClientData::from_client_data_json_relaxed`] or [`CollectedClientData::from_client_data_json_relaxed`].
   1643         // 11. Below.
   1644         // 12. Below.
   1645         // 13. Below.
   1646         // 14. Below.
   1647         // 15. Below.
   1648         // 16. Below via [`AuthenticatorData::from_cbor`].
   1649         // 17. Below.
   1650         // 18. Below via [`AuthenticatorData::from_cbor`].
   1651         // 19. Below.
   1652         // 20. Below via [`AuthenticatorAssertion::new`].
   1653         // 21. Below.
   1654         // 22. [`AuthenticationServerState::verify`].
   1655         // 23. [`AuthenticationServerState::verify`].
   1656         // 24. [`AuthenticationServerState::verify`].
   1657         // 25. [`AuthenticationServerState::verify`].
   1658 
   1659         // Enforce timeout.
   1660         #[cfg(not(feature = "serializable_server_state"))]
   1661         let active = self.expiry() >= Instant::now();
   1662         #[cfg(feature = "serializable_server_state")]
   1663         let active = self.expiry() >= SystemTime::now();
   1664         if active {
   1665             #[cfg(feature = "serde_relaxed")]
   1666             let relaxed = options.client_data_json_relaxed;
   1667             #[cfg(not(feature = "serde_relaxed"))]
   1668             let relaxed = false;
   1669             resp.auth()
   1670                 // Steps 5–7, 12–13, 17, 21–22, and 25 of the registration ceremony.
   1671                 // Steps 8–10, 16, 18, and 20–21 of the authentication ceremony.
   1672                 .parse_data_and_verify_sig(key, relaxed)
   1673                 .map_err(CeremonyErr::AuthResp)
   1674                 .and_then(|(client_data_json, auth_response)| {
   1675                     if options.allowed_origins.is_empty() {
   1676                         if match *rp_id {
   1677                             RpId::Domain(ref dom) => {
   1678                                 // Steps 9 and 12 of the registration and authentication ceremonies
   1679                                 // respectively.
   1680                                 DomainOrigin::new(dom.as_ref()) == client_data_json.origin
   1681                             }
   1682                             // Steps 9 and 12 of the registration and authentication ceremonies
   1683                             // respectively.
   1684                             RpId::Url(ref url) => url == client_data_json.origin,
   1685                             RpId::StaticDomain(dom) => {
   1686                                 DomainOrigin::new(dom.0) == client_data_json.origin
   1687                             }
   1688                         } {
   1689                             Ok(())
   1690                         } else {
   1691                             Err(CeremonyErr::OriginMismatch)
   1692                         }
   1693                     } else {
   1694                         options
   1695                             .allowed_origins
   1696                             .iter()
   1697                             // Steps 9 and 12 of the registration and authentication ceremonies
   1698                             // respectively.
   1699                             .find(|o| **o == client_data_json.origin)
   1700                             .ok_or(CeremonyErr::OriginMismatch)
   1701                             .map(|_| ())
   1702                     }
   1703                     .and_then(|()| {
   1704                         // Steps 10–11 of the registration ceremony.
   1705                         // Steps 13–14 of the authentication ceremony.
   1706                         match options.allowed_top_origins {
   1707                             None => {
   1708                                 if client_data_json.cross_origin {
   1709                                     Err(CeremonyErr::CrossOrigin)
   1710                                 } else if client_data_json.top_origin.is_some() {
   1711                                     Err(CeremonyErr::TopOriginMismatch)
   1712                                 } else {
   1713                                     Ok(())
   1714                                 }
   1715                             }
   1716                             Some(top_origins) => client_data_json.top_origin.map_or(Ok(()), |t| {
   1717                                 top_origins
   1718                                     .iter()
   1719                                     .find(|top| **top == t)
   1720                                     .ok_or(CeremonyErr::TopOriginMismatch)
   1721                                     .map(|_| ())
   1722                             }),
   1723                         }
   1724                         .and_then(|()| {
   1725                             // Steps 8 and 11 of the registration and authentication ceremonies
   1726                             // respectively.
   1727                             if self.rand_challenge() == client_data_json.challenge {
   1728                                 let auth_data = auth_response.authenticator_data();
   1729                                 rp_id
   1730                                     // Steps 14 and 15 of the registration and authentication ceremonies
   1731                                     // respectively.
   1732                                     .validate_rp_id_hash(auth_data.rp_hash())
   1733                                     .and_then(|()| {
   1734                                         let flag = auth_data.flag();
   1735                                         // Steps 16 and 17 of the registration and authentication ceremonies
   1736                                         // respectively.
   1737                                         if flag.user_verified
   1738                                             || !matches!(
   1739                                                 self.user_verification(),
   1740                                                 UserVerificationRequirement::Required
   1741                                             )
   1742                                         {
   1743                                             // Steps 18–19 of the registration ceremony.
   1744                                             // Step 19 of the authentication ceremony.
   1745                                             match options.backup_requirement {
   1746                                                 BackupReq::None => Ok(()),
   1747                                                 BackupReq::NotEligible => {
   1748                                                     if matches!(flag.backup, Backup::NotEligible) {
   1749                                                         Ok(())
   1750                                                     } else {
   1751                                                         Err(CeremonyErr::BackupEligible)
   1752                                                     }
   1753                                                 }
   1754                                                 BackupReq::Eligible => {
   1755                                                     if matches!(flag.backup, Backup::NotEligible) {
   1756                                                         Err(CeremonyErr::BackupNotEligible)
   1757                                                     } else {
   1758                                                         Ok(())
   1759                                                     }
   1760                                                 }
   1761                                                 BackupReq::EligibleNotExists => {
   1762                                                     if matches!(flag.backup, Backup::Eligible) {
   1763                                                         Ok(())
   1764                                                     } else {
   1765                                                         Err(CeremonyErr::BackupExists)
   1766                                                     }
   1767                                                 }
   1768                                                 BackupReq::Exists => {
   1769                                                     if matches!(flag.backup, Backup::Exists) {
   1770                                                         Ok(())
   1771                                                     } else {
   1772                                                         Err(CeremonyErr::BackupDoesNotExist)
   1773                                                     }
   1774                                                 }
   1775                                             }
   1776                                         } else {
   1777                                             Err(CeremonyErr::UserNotVerified)
   1778                                         }
   1779                                     })
   1780                                     .map(|()| auth_response)
   1781                             } else {
   1782                                 Err(CeremonyErr::ChallengeMismatch)
   1783                             }
   1784                         })
   1785                     })
   1786                 })
   1787         } else {
   1788             Err(CeremonyErr::Timeout)
   1789         }
   1790     }
   1791 }
   1792 /// "Ceremonies" stored on the server that expire after a certain duration.
   1793 ///
   1794 /// Types like [`RegistrationServerState`] and [`DiscoverableAuthenticationServerState`] are based on [`Challenge`]s
   1795 /// that expire after a certain duration.
   1796 pub trait TimedCeremony {
   1797     /// Returns the `Instant` the ceremony expires.
   1798     ///
   1799     /// Note when `serializable_server_state` is enabled, [`SystemTime`] is returned instead.
   1800     #[cfg_attr(docsrs, doc(auto_cfg = false))]
   1801     #[cfg(any(doc, not(feature = "serializable_server_state")))]
   1802     fn expiration(&self) -> Instant;
   1803     /// Returns the `SystemTime` the ceremony expires.
   1804     #[cfg(all(not(doc), feature = "serializable_server_state"))]
   1805     fn expiration(&self) -> SystemTime;
   1806 }
   1807 /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues).
   1808 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
   1809 pub struct PrfInput<'first, 'second> {
   1810     /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first).
   1811     pub first: &'first [u8],
   1812     /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second).
   1813     pub second: Option<&'second [u8]>,
   1814 }
   1815 impl<'first, 'second> PrfInput<'first, 'second> {
   1816     /// Returns a `PrfInput` with [`Self::first`] set to `first` and [`Self::second`] set to `None`.
   1817     #[expect(single_use_lifetimes, reason = "false positive")]
   1818     #[inline]
   1819     #[must_use]
   1820     pub const fn with_first<'a: 'first>(first: &'a [u8]) -> Self {
   1821         Self {
   1822             first,
   1823             second: None,
   1824         }
   1825     }
   1826     /// Same as [`Self::with_first`] except [`Self::second`] is set to `Some` containing `second`.
   1827     #[expect(single_use_lifetimes, reason = "false positive")]
   1828     #[inline]
   1829     #[must_use]
   1830     pub const fn with_two<'a: 'first, 'b: 'second>(first: &'a [u8], second: &'b [u8]) -> Self {
   1831         Self {
   1832             first,
   1833             second: Some(second),
   1834         }
   1835     }
   1836 }
   1837 /// The number of milliseconds in 5 minutes.
   1838 ///
   1839 /// This is the recommended default timeout duration for ceremonies
   1840 /// [in the spec](https://www.w3.org/TR/webauthn-3/#sctn-timeout-recommended-range).
   1841 pub const FIVE_MINUTES: NonZeroU32 = NonZeroU32::new(300_000).unwrap();