webauthn_rp

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

request.rs (73354B)


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