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