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();