request.rs (138628B)
1 #[cfg(doc)] 2 use super::{ 3 hash::hash_set::MaxLenHashSet, 4 request::{ 5 auth::{ 6 AllowedCredential, AllowedCredentials, CredentialSpecificExtension, 7 DiscoverableAuthenticationServerState, DiscoverableCredentialRequestOptions, 8 NonDiscoverableAuthenticationServerState, NonDiscoverableCredentialRequestOptions, 9 PublicKeyCredentialRequestOptions, 10 }, 11 register::{CredentialCreationOptions, RegistrationServerState}, 12 }, 13 response::register::ClientExtensionsOutputs, 14 }; 15 use crate::{ 16 request::{ 17 error::{ 18 AsciiDomainErr, DomainOriginParseErr, PortParseErr, RpIdErr, SchemeParseErr, UrlErr, 19 }, 20 register::RegistrationVerificationOptions, 21 }, 22 response::{ 23 AuthData as _, AuthDataContainer, AuthResponse, AuthTransports, Backup, CeremonyErr, 24 CredentialId, Origin, Response, SentChallenge, 25 }, 26 }; 27 use core::{ 28 borrow::Borrow, 29 fmt::{self, Display, Formatter}, 30 num::NonZeroU32, 31 str::FromStr, 32 }; 33 use rsa::sha2::{Digest as _, Sha256}; 34 #[cfg(any(doc, not(feature = "serializable_server_state")))] 35 use std::time::Instant; 36 #[cfg(feature = "serializable_server_state")] 37 use std::time::SystemTime; 38 use url::Url as Uri; 39 /// Contains functionality for beginning the 40 /// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony). 41 /// 42 /// # Examples 43 /// 44 /// ``` 45 /// # use core::convert; 46 /// # use webauthn_rp::{ 47 /// # hash::hash_set::MaxLenHashSet, 48 /// # request::{ 49 /// # auth::{AllowedCredentials, DiscoverableCredentialRequestOptions, NonDiscoverableCredentialRequestOptions}, 50 /// # register::UserHandle64, 51 /// # Credentials, PublicKeyCredentialDescriptor, RpId, 52 /// # }, 53 /// # response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN}, 54 /// # AggErr, 55 /// # }; 56 /// const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap(); 57 /// let mut ceremonies = MaxLenHashSet::new(128); 58 /// let (server, client) = DiscoverableCredentialRequestOptions::passkey(RP_ID).start_ceremony()?; 59 /// assert!( 60 /// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity) 61 /// ); 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!( 74 /// ceremonies_2.insert_remove_all_expired(server_2).map_or(false, convert::identity) 75 /// ); 76 /// # #[cfg(all(feature = "custom", feature = "serde"))] 77 /// assert!(serde_json::to_string(&client_2).is_ok()); 78 /// /// Extract `UserHandle` from session cookie. 79 /// fn get_user_handle() -> UserHandle64 { 80 /// // ⋮ 81 /// # UserHandle64::new() 82 /// } 83 /// # #[cfg(feature = "custom")] 84 /// /// Fetch the `AllowedCredentials` associated with `user`. 85 /// fn get_registered_credentials(user: &UserHandle64) -> Result<AllowedCredentials, AggErr> { 86 /// // ⋮ 87 /// # let mut creds = AllowedCredentials::new(); 88 /// # creds.push( 89 /// # PublicKeyCredentialDescriptor { 90 /// # id: CredentialId::try_from(vec![0; CRED_ID_MIN_LEN])?, 91 /// # transports: AuthTransports::NONE, 92 /// # } 93 /// # .into(), 94 /// # ); 95 /// # Ok(creds) 96 /// } 97 /// # Ok::<_, AggErr>(()) 98 /// ``` 99 pub mod auth; 100 /// Contains error types. 101 pub mod error; 102 /// Contains functionality for beginning the 103 /// [registration ceremony](https://www.w3.org/TR/webauthn-3/#registration-ceremony). 104 /// 105 /// # Examples 106 /// 107 /// ``` 108 /// # use core::convert; 109 /// # use webauthn_rp::{ 110 /// # hash::hash_set::MaxLenHashSet, 111 /// # request::{ 112 /// # register::{ 113 /// # CredentialCreationOptions, DisplayName, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MAX_LEN, UserHandle64, 114 /// # }, 115 /// # PublicKeyCredentialDescriptor, RpId 116 /// # }, 117 /// # response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN}, 118 /// # AggErr, 119 /// # }; 120 /// const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap(); 121 /// # #[cfg(feature = "custom")] 122 /// let mut ceremonies = MaxLenHashSet::new(128); 123 /// # #[cfg(feature = "custom")] 124 /// let user_handle = get_user_handle(); 125 /// # #[cfg(feature = "custom")] 126 /// let user = get_user_entity(&user_handle)?; 127 /// # #[cfg(feature = "custom")] 128 /// let creds = get_registered_credentials(&user_handle)?; 129 /// # #[cfg(feature = "custom")] 130 /// let (server, client) = CredentialCreationOptions::passkey(RP_ID, user.clone(), creds) 131 /// .start_ceremony()?; 132 /// # #[cfg(feature = "custom")] 133 /// assert!( 134 /// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity) 135 /// ); 136 /// # #[cfg(all(feature = "serde", feature = "custom"))] 137 /// assert!(serde_json::to_string(&client).is_ok()); 138 /// # #[cfg(feature = "custom")] 139 /// let creds_2 = get_registered_credentials(&user_handle)?; 140 /// # #[cfg(feature = "custom")] 141 /// let (server_2, client_2) = 142 /// CredentialCreationOptions::second_factor(RP_ID, user, creds_2).start_ceremony()?; 143 /// # #[cfg(feature = "custom")] 144 /// assert!( 145 /// ceremonies.insert_remove_all_expired(server_2).map_or(false, convert::identity) 146 /// ); 147 /// # #[cfg(all(feature = "serde", feature = "custom"))] 148 /// assert!(serde_json::to_string(&client_2).is_ok()); 149 /// /// Extract `UserHandle` from session cookie or storage if this is not the first credential registered. 150 /// # #[cfg(feature = "custom")] 151 /// fn get_user_handle() -> UserHandle64 { 152 /// // ⋮ 153 /// # [0; USER_HANDLE_MAX_LEN].into() 154 /// } 155 /// /// Fetch `PublicKeyCredentialUserEntity` info associated with `user`. 156 /// /// 157 /// /// If this is the first time a credential is being registered, then `PublicKeyCredentialUserEntity` 158 /// /// will need to be constructed with `name` and `display_name` passed from the client and `UserHandle::new` 159 /// /// used for `id`. Once created, this info can be stored such that the entity information 160 /// /// does not need to be requested for subsequent registrations. 161 /// # #[cfg(feature = "custom")] 162 /// fn get_user_entity(user: &UserHandle64) -> Result<PublicKeyCredentialUserEntity<'_, '_, '_, USER_HANDLE_MAX_LEN>, AggErr> { 163 /// // ⋮ 164 /// # Ok(PublicKeyCredentialUserEntity { 165 /// # name: "foo".try_into()?, 166 /// # id: user, 167 /// # display_name: DisplayName::Blank, 168 /// # }) 169 /// } 170 /// /// Fetch the `PublicKeyCredentialDescriptor`s associated with `user`. 171 /// /// 172 /// /// This doesn't need to be called when this is the first credential registered for `user`; instead 173 /// /// an empty `Vec` should be passed. 174 /// fn get_registered_credentials( 175 /// user: &UserHandle64, 176 /// ) -> Result<Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, AggErr> { 177 /// // ⋮ 178 /// # Ok(Vec::new()) 179 /// } 180 /// # Ok::<_, AggErr>(()) 181 /// ``` 182 pub mod register; 183 /// Contains functionality to serialize data to a client. 184 #[cfg(feature = "serde")] 185 mod ser; 186 /// Contains functionality to (de)serialize data needed for [`RegistrationServerState`], 187 /// [`DiscoverableAuthenticationServerState`], and [`NonDiscoverableAuthenticationServerState`] to a data store. 188 #[cfg(feature = "serializable_server_state")] 189 pub(super) mod ser_server_state; 190 // `Challenge` must _never_ be constructable directly or indirectly; thus its tuple field must always be private, 191 // and it must never implement `trait`s (e.g., `Clone`) that would allow indirect creation. It must only ever 192 // be constructed via `Self::new` or `Self::default`. In contrast downstream code must be able to construct 193 // `SentChallenge` since it is used during ceremony validation; thus we must keep `Challenge` and `SentChallenge` 194 // as separate types. 195 /// [Cryptographic challenge](https://www.w3.org/TR/webauthn-3/#sctn-cryptographic-challenges). 196 #[expect( 197 missing_copy_implementations, 198 reason = "want to enforce randomly-generated challenges" 199 )] 200 #[derive(Debug)] 201 pub struct Challenge(u128); 202 impl Challenge { 203 /// The number of bytes a `Challenge` takes to encode in base64url. 204 pub(super) const BASE64_LEN: usize = base64url_nopad::encode_len(16); 205 /// Generates a random `Challenge`. 206 /// 207 /// # Examples 208 /// 209 /// ``` 210 /// # use webauthn_rp::request::Challenge; 211 /// // The probability of a `Challenge` being 0 (assuming a good entropy 212 /// // source) is 2^-128 ≈ 2.9 x 10^-39. 213 /// assert_ne!(Challenge::new().into_data(), 0); 214 /// ``` 215 #[inline] 216 #[must_use] 217 pub fn new() -> Self { 218 Self(rand::random()) 219 } 220 /// Returns the contained `u128` consuming `self`. 221 #[inline] 222 #[must_use] 223 pub const fn into_data(self) -> u128 { 224 self.0 225 } 226 /// Returns the contained `u128`. 227 #[inline] 228 #[must_use] 229 pub const fn as_data(&self) -> u128 { 230 self.0 231 } 232 /// Returns the contained `u128` as a little-endian `array` consuming `self`. 233 #[inline] 234 #[must_use] 235 pub const fn into_array(self) -> [u8; 16] { 236 self.as_array() 237 } 238 /// Returns the contained `u128` as a little-endian `array`. 239 #[expect( 240 clippy::little_endian_bytes, 241 reason = "Challenge and SentChallenge need to be compatible, and we need to ensure the data is sent and received in the same order" 242 )] 243 #[inline] 244 #[must_use] 245 pub const fn as_array(&self) -> [u8; 16] { 246 self.0.to_le_bytes() 247 } 248 } 249 impl Default for Challenge { 250 /// Same as [`Self::new`]. 251 #[inline] 252 fn default() -> Self { 253 Self::new() 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 u128 { 263 #[inline] 264 fn from(value: &Challenge) -> Self { 265 value.0 266 } 267 } 268 impl From<Challenge> for [u8; 16] { 269 #[inline] 270 fn from(value: Challenge) -> Self { 271 value.into_array() 272 } 273 } 274 impl From<&Challenge> for [u8; 16] { 275 #[inline] 276 fn from(value: &Challenge) -> Self { 277 value.as_array() 278 } 279 } 280 /// A [domain](https://url.spec.whatwg.org/#concept-domain) in representation format consisting of only and any 281 /// ASCII. 282 /// 283 /// The only ASCII character disallowed in a label is `'.'` since it is used exclusively as a separator. Every 284 /// label must have length inclusively between 1 and 63, and the total length of the domain must be at most 253 285 /// when a trailing `'.'` does not exist; otherwise the max length is 254. The root domain (i.e., `'.'`) is not 286 /// allowed. 287 /// 288 /// Note if the domain is a `&'static str`, then use [`AsciiDomainStatic`] instead. 289 #[derive(Clone, Debug, Eq, PartialEq)] 290 pub struct AsciiDomain(String); 291 impl AsciiDomain { 292 /// Removes a trailing `'.'` if it exists. 293 /// 294 /// # Examples 295 /// 296 /// ``` 297 /// # use webauthn_rp::request::{AsciiDomain, error::AsciiDomainErr}; 298 /// let mut dom = AsciiDomain::try_from("example.com.".to_owned())?; 299 /// assert_eq!(dom.as_ref(), "example.com."); 300 /// dom.remove_trailing_dot(); 301 /// assert_eq!(dom.as_ref(), "example.com"); 302 /// dom.remove_trailing_dot(); 303 /// assert_eq!(dom.as_ref(), "example.com"); 304 /// # Ok::<_, AsciiDomainErr>(()) 305 /// ``` 306 #[expect(clippy::unreachable, reason = "want to crash when there is a bug")] 307 #[inline] 308 pub fn remove_trailing_dot(&mut self) { 309 if *self 310 .0 311 .as_bytes() 312 .last() 313 .unwrap_or_else(|| unreachable!("there is a bug in AsciiDomain::from_slice")) 314 == b'.' 315 { 316 _ = self.0.pop(); 317 } 318 } 319 } 320 impl AsRef<str> for AsciiDomain { 321 #[inline] 322 fn as_ref(&self) -> &str { 323 self.0.as_str() 324 } 325 } 326 impl Borrow<str> for AsciiDomain { 327 #[inline] 328 fn borrow(&self) -> &str { 329 self.0.as_str() 330 } 331 } 332 impl From<AsciiDomain> for String { 333 #[inline] 334 fn from(value: AsciiDomain) -> Self { 335 value.0 336 } 337 } 338 impl PartialEq<&Self> for AsciiDomain { 339 #[inline] 340 fn eq(&self, other: &&Self) -> bool { 341 *self == **other 342 } 343 } 344 impl PartialEq<AsciiDomain> for &AsciiDomain { 345 #[inline] 346 fn eq(&self, other: &AsciiDomain) -> bool { 347 **self == *other 348 } 349 } 350 impl TryFrom<Vec<u8>> for AsciiDomain { 351 type Error = AsciiDomainErr; 352 /// Verifies `value` is an ASCII domain in representation format converting any uppercase ASCII into 353 /// lowercase. 354 /// 355 /// Note it is _strongly_ encouraged for `value` to only contain letters, numbers, hyphens, and underscores; 356 /// otherwise certain applications may consider it not a domain. If the original domain contains non-ASCII, then 357 /// one must encode it in Punycode _before_ calling this function. Domains that have a trailing `'.'` will be 358 /// considered differently than domains without it; thus one will likely want to trim it if it does exist 359 /// (e.g., [`AsciiDomain::remove_trailing_dot`]). Because this allows any ASCII, one may want to ensure `value` 360 /// is not an IP address. 361 /// 362 /// # Errors 363 /// 364 /// Errors iff `value` is not a valid ASCII domain. 365 /// 366 /// # Examples 367 /// 368 /// ``` 369 /// # use webauthn_rp::request::{error::AsciiDomainErr, AsciiDomain}; 370 /// // Root `'.'` is not removed if it exists. 371 /// assert_ne!("example.com", AsciiDomain::try_from(b"example.com.".to_vec())?.as_ref()); 372 /// // Root domain (i.e., `'.'`) is not allowed. 373 /// assert!(AsciiDomain::try_from(vec![b'.']).is_err()); 374 /// // Uppercase is transformed into lowercase. 375 /// assert_eq!("example.com", AsciiDomain::try_from(b"ExAmPle.CoM".to_vec())?.as_ref()); 376 /// // The only ASCII character not allowed in a domain label is `'.'` as it is used exclusively to delimit 377 /// // labels. 378 /// assert_eq!("\x00", AsciiDomain::try_from(b"\x00".to_vec())?.as_ref()); 379 /// // Empty labels are not allowed. 380 /// assert!(AsciiDomain::try_from(b"example..com".to_vec()).is_err()); 381 /// // Labels cannot have length greater than 63. 382 /// let mut long_label = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned(); 383 /// assert_eq!(long_label.len(), 64); 384 /// assert!(AsciiDomain::try_from(long_label.clone().into_bytes()).is_err()); 385 /// long_label.pop(); 386 /// assert_eq!(long_label, AsciiDomain::try_from(long_label.clone().into_bytes())?.as_ref()); 387 /// // The maximum length of a domain is 254 if a trailing `'.'` exists; otherwise the max length is 253. 388 /// let mut long_domain = format!("{long_label}.{long_label}.{long_label}.{long_label}"); 389 /// long_domain.pop(); 390 /// long_domain.push('.'); 391 /// assert_eq!(long_domain.len(), 255); 392 /// assert!(AsciiDomain::try_from(long_domain.clone().into_bytes()).is_err()); 393 /// long_domain.pop(); 394 /// long_domain.pop(); 395 /// long_domain.push('.'); 396 /// assert_eq!(long_domain.len(), 254); 397 /// assert_eq!(long_domain, AsciiDomain::try_from(long_domain.clone().into_bytes())?.as_ref()); 398 /// long_domain.pop(); 399 /// long_domain.push('a'); 400 /// assert_eq!(long_domain.len(), 254); 401 /// assert!(AsciiDomain::try_from(long_domain.clone().into_bytes()).is_err()); 402 /// long_domain.pop(); 403 /// assert_eq!(long_domain.len(), 253); 404 /// assert_eq!(long_domain, AsciiDomain::try_from(long_domain.clone().into_bytes())?.as_ref()); 405 /// // Only ASCII is allowed; thus if a domain needs to be Punycode-encoded, then it must be _before_ calling 406 /// // this function. 407 /// assert!(AsciiDomain::try_from("λ.com".to_owned().into_bytes()).is_err()); 408 /// assert_eq!("xn--wxa.com", AsciiDomain::try_from(b"xn--wxa.com".to_vec())?.as_ref()); 409 /// # Ok::<_, AsciiDomainErr>(()) 410 /// ``` 411 #[expect(unsafe_code, reason = "comment justifies correctness")] 412 #[expect( 413 clippy::arithmetic_side_effects, 414 reason = "comments justify correctness" 415 )] 416 #[inline] 417 fn try_from(mut value: Vec<u8>) -> Result<Self, Self::Error> { 418 /// Value to add to an uppercase ASCII `u8` to get the lowercase version. 419 const DIFF: u8 = b'a' - b'A'; 420 let bytes = value.as_slice(); 421 bytes 422 .as_ref() 423 .last() 424 .ok_or(AsciiDomainErr::Empty) 425 .and_then(|b| { 426 let len = bytes.len(); 427 if *b == b'.' { 428 if len == 1 { 429 Err(AsciiDomainErr::RootDomain) 430 } else if len > 254 { 431 Err(AsciiDomainErr::Len) 432 } else { 433 Ok(()) 434 } 435 } else if len > 253 { 436 Err(AsciiDomainErr::Len) 437 } else { 438 Ok(()) 439 } 440 }) 441 .and_then(|()| { 442 value 443 .iter_mut() 444 .try_fold(0u8, |mut label_len, byt| { 445 let b = *byt; 446 if b == b'.' { 447 if label_len == 0 { 448 Err(AsciiDomainErr::EmptyLabel) 449 } else { 450 Ok(0) 451 } 452 } else if label_len == 63 { 453 Err(AsciiDomainErr::LabelLen) 454 } else { 455 // We know `label_len` is less than 63, thus this won't overflow. 456 label_len += 1; 457 match *byt { 458 // Non-uppercase ASCII is allowed and doesn't need to be converted. 459 ..b'A' | b'['..=0x7F => Ok(label_len), 460 // Uppercase ASCII is allowed but needs to be transformed into lowercase. 461 b'A'..=b'Z' => { 462 // Lowercase ASCII is a contiguous block starting from `b'a'` as is uppercase 463 // ASCII which starts from `b'A'` with uppercase ASCII coming before; thus we 464 // simply need to shift by a fixed amount. 465 *byt += DIFF; 466 Ok(label_len) 467 } 468 // Non-ASCII is disallowed. 469 0x80.. => Err(AsciiDomainErr::NotAscii), 470 } 471 } 472 }) 473 .map(|_| { 474 // SAFETY: 475 // We just verified `value` only contains ASCII; thus this is safe. 476 let utf8 = unsafe { String::from_utf8_unchecked(value) }; 477 Self(utf8) 478 }) 479 }) 480 } 481 } 482 impl TryFrom<String> for AsciiDomain { 483 type Error = AsciiDomainErr; 484 /// Same as [`Self::try_from`] except `value` is a `String`. 485 #[inline] 486 fn try_from(value: String) -> Result<Self, Self::Error> { 487 Self::try_from(value.into_bytes()) 488 } 489 } 490 /// Similar to [`AsciiDomain`] except the contained data is a `&'static str`. 491 /// 492 /// Since [`Self::new`] and [`Option::unwrap`] are `const fn`s, one can define a global `const` or `static` 493 /// variable that represents the RP ID. 494 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 495 pub struct AsciiDomainStatic(&'static str); 496 impl AsciiDomainStatic { 497 /// Returns the contained `str`. 498 #[inline] 499 #[must_use] 500 pub const fn as_str(self) -> &'static str { 501 self.0 502 } 503 /// Verifies `domain` is a valid lowercase ASCII domain returning `None` when not valid or when 504 /// uppercase ASCII exists. 505 /// 506 /// Read [`AsciiDomain`] for more information about what constitutes a valid domain. 507 /// 508 /// # Examples 509 /// 510 /// ``` 511 /// # use webauthn_rp::request::{AsciiDomainStatic, RpId}; 512 /// /// RP ID of our application. 513 /// const RP_IP: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap()); 514 /// ``` 515 #[expect( 516 clippy::arithmetic_side_effects, 517 reason = "comment justifies correctness" 518 )] 519 #[expect( 520 clippy::else_if_without_else, 521 reason = "part of if branch and else branch are the same" 522 )] 523 #[inline] 524 #[must_use] 525 pub const fn new(domain: &'static str) -> Option<Self> { 526 let mut utf8 = domain.as_bytes(); 527 if let Some(lst) = utf8.last() { 528 let len = utf8.len(); 529 if *lst == b'.' { 530 if len == 1 || len > 254 { 531 return None; 532 } 533 } else if len > 253 { 534 return None; 535 } 536 let mut label_len = 0; 537 while let [first, ref rest @ ..] = *utf8 { 538 if first == b'.' { 539 if label_len == 0 { 540 return None; 541 } 542 label_len = 0; 543 } else if label_len == 63 { 544 return None; 545 } else { 546 match first { 547 // Any non-uppercase ASCII is allowed. 548 // We know `label_len` is less than 63, so this won't overflow. 549 ..b'A' | b'['..=0x7F => label_len += 1, 550 // Uppercase ASCII and non-ASCII are disallowed. 551 b'A'..=b'Z' | 0x80.. => return None, 552 } 553 } 554 utf8 = rest; 555 } 556 Some(Self(domain)) 557 } else { 558 None 559 } 560 } 561 } 562 impl AsRef<str> for AsciiDomainStatic { 563 #[inline] 564 fn as_ref(&self) -> &str { 565 self.as_str() 566 } 567 } 568 impl Borrow<str> for AsciiDomainStatic { 569 #[inline] 570 fn borrow(&self) -> &str { 571 self.as_str() 572 } 573 } 574 impl From<AsciiDomainStatic> for &'static str { 575 #[inline] 576 fn from(value: AsciiDomainStatic) -> Self { 577 value.0 578 } 579 } 580 impl From<AsciiDomainStatic> for String { 581 #[inline] 582 fn from(value: AsciiDomainStatic) -> Self { 583 value.0.to_owned() 584 } 585 } 586 impl From<AsciiDomainStatic> for AsciiDomain { 587 #[inline] 588 fn from(value: AsciiDomainStatic) -> Self { 589 Self(value.0.to_owned()) 590 } 591 } 592 impl PartialEq<&Self> for AsciiDomainStatic { 593 #[inline] 594 fn eq(&self, other: &&Self) -> bool { 595 *self == **other 596 } 597 } 598 impl PartialEq<AsciiDomainStatic> for &AsciiDomainStatic { 599 #[inline] 600 fn eq(&self, other: &AsciiDomainStatic) -> bool { 601 **self == *other 602 } 603 } 604 /// The output of the [URL serializer](https://url.spec.whatwg.org/#concept-url-serializer). 605 /// 606 /// The returned URL must consist of a [scheme](https://url.spec.whatwg.org/#concept-url-scheme) and 607 /// optional [path](https://url.spec.whatwg.org/#url-path) but nothing else. 608 #[derive(Clone, Debug, Eq, PartialEq)] 609 pub struct Url(String); 610 impl AsRef<str> for Url { 611 #[inline] 612 fn as_ref(&self) -> &str { 613 self.0.as_str() 614 } 615 } 616 impl Borrow<str> for Url { 617 #[inline] 618 fn borrow(&self) -> &str { 619 self.0.as_str() 620 } 621 } 622 impl From<Url> for String { 623 #[inline] 624 fn from(value: Url) -> Self { 625 value.0 626 } 627 } 628 impl PartialEq<&Self> for Url { 629 #[inline] 630 fn eq(&self, other: &&Self) -> bool { 631 *self == **other 632 } 633 } 634 impl PartialEq<Url> for &Url { 635 #[inline] 636 fn eq(&self, other: &Url) -> bool { 637 **self == *other 638 } 639 } 640 impl FromStr for Url { 641 type Err = UrlErr; 642 #[inline] 643 fn from_str(s: &str) -> Result<Self, Self::Err> { 644 Uri::from_str(s).map_err(|_e| UrlErr).and_then(|url| { 645 if url.scheme().is_empty() 646 || url.has_host() 647 || url.query().is_some() 648 || url.fragment().is_some() 649 { 650 Err(UrlErr) 651 } else { 652 Ok(Self(url.into())) 653 } 654 }) 655 } 656 } 657 /// [RP ID](https://w3c.github.io/webauthn/#rp-id). 658 #[derive(Clone, Debug, Eq, PartialEq)] 659 pub enum RpId { 660 /// An ASCII domain. 661 /// 662 /// Note web platforms MUST use this variant; and if possible, non-web platforms should too. Also despite 663 /// the spec currently requiring RP IDs to be 664 /// [valid domain strings](https://url.spec.whatwg.org/#valid-domain-string), this is unnecessarily strict 665 /// and will likely be relaxed in a [future version](https://github.com/w3c/webauthn/issues/2206); thus 666 /// any ASCII domain is allowed. 667 Domain(AsciiDomain), 668 /// Similar to [`Self::Domain`] except the ASCII domain is static. 669 /// 670 /// Since [`AsciiDomainStatic::new`] is a `const fn`, one can define a `const` or `static` global variable 671 /// the contains the RP ID. 672 StaticDomain(AsciiDomainStatic), 673 /// A URL with only scheme and path. 674 Url(Url), 675 } 676 impl RpId { 677 /// Returns `Some` containing an [`AsciiDomainStatic`] iff [`AsciiDomainStatic::new`] does. 678 #[inline] 679 #[must_use] 680 pub const fn from_static_domain(domain: &'static str) -> Option<Self> { 681 if let Some(dom) = AsciiDomainStatic::new(domain) { 682 Some(Self::StaticDomain(dom)) 683 } else { 684 None 685 } 686 } 687 /// Validates `hash` is the same as the SHA-256 hash of `self`. 688 fn validate_rp_id_hash<E>(&self, hash: &[u8]) -> Result<(), CeremonyErr<E>> { 689 if *hash == *Sha256::digest(self.as_ref()) { 690 Ok(()) 691 } else { 692 Err(CeremonyErr::RpIdHashMismatch) 693 } 694 } 695 } 696 impl AsRef<str> for RpId { 697 #[inline] 698 fn as_ref(&self) -> &str { 699 match *self { 700 Self::Domain(ref dom) => dom.as_ref(), 701 Self::StaticDomain(dom) => dom.as_str(), 702 Self::Url(ref url) => url.as_ref(), 703 } 704 } 705 } 706 impl Borrow<str> for RpId { 707 #[inline] 708 fn borrow(&self) -> &str { 709 match *self { 710 Self::Domain(ref dom) => dom.borrow(), 711 Self::StaticDomain(dom) => dom.as_str(), 712 Self::Url(ref url) => url.borrow(), 713 } 714 } 715 } 716 impl From<RpId> for String { 717 #[inline] 718 fn from(value: RpId) -> Self { 719 match value { 720 RpId::Domain(dom) => dom.into(), 721 RpId::StaticDomain(dom) => dom.into(), 722 RpId::Url(url) => url.into(), 723 } 724 } 725 } 726 impl PartialEq<&Self> for RpId { 727 #[inline] 728 fn eq(&self, other: &&Self) -> bool { 729 *self == **other 730 } 731 } 732 impl PartialEq<RpId> for &RpId { 733 #[inline] 734 fn eq(&self, other: &RpId) -> bool { 735 **self == *other 736 } 737 } 738 impl From<AsciiDomain> for RpId { 739 #[inline] 740 fn from(value: AsciiDomain) -> Self { 741 Self::Domain(value) 742 } 743 } 744 impl From<AsciiDomainStatic> for RpId { 745 #[inline] 746 fn from(value: AsciiDomainStatic) -> Self { 747 Self::StaticDomain(value) 748 } 749 } 750 impl From<Url> for RpId { 751 #[inline] 752 fn from(value: Url) -> Self { 753 Self::Url(value) 754 } 755 } 756 impl TryFrom<String> for RpId { 757 type Error = RpIdErr; 758 /// Returns `Ok` iff `value` is a valid [`Url`] or [`AsciiDomain`]. 759 /// 760 /// Note when `value` is a valid `Url` and `AsciiDomain`, it will be treated as a `Url`. 761 #[inline] 762 fn try_from(value: String) -> Result<Self, Self::Error> { 763 Url::from_str(value.as_str()) 764 .map(Self::Url) 765 .or_else(|_err| { 766 AsciiDomain::try_from(value) 767 .map(Self::Domain) 768 .map_err(|_e| RpIdErr) 769 }) 770 } 771 } 772 /// A URI scheme. This can be used to make 773 /// [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin) more convenient. 774 #[derive(Clone, Copy, Debug, Default)] 775 pub enum Scheme<'a> { 776 /// A scheme must not exist when validating the origin. 777 None, 778 /// Any scheme, or no scheme at all, is allowed to exist when validating the origin. 779 Any, 780 /// The HTTPS scheme must exist when validating the origin. 781 #[default] 782 Https, 783 /// The SSH scheme must exist when validating the origin. 784 Ssh, 785 /// The contained `str` scheme must exist when validating the origin. 786 Other(&'a str), 787 /// [`Self::None`] or [`Self::Https`]. 788 NoneHttps, 789 /// [`Self::None`] or [`Self::Ssh`]. 790 NoneSsh, 791 /// [`Self::None`] or [`Self::Other`]. 792 NoneOther(&'a str), 793 } 794 impl Scheme<'_> { 795 /// `self` is any `Scheme`; however `other` is assumed to only be a `Scheme` from a `DomainOrigin` returned 796 /// from `DomainOrigin::try_from`. The latter implies that `other` is only `Scheme::None`, `Scheme::Https`, 797 /// `Scheme::Ssh`, or `Scheme::Other`; furthermore when `Scheme::Other`, it won't contain a `str` that is 798 /// empty or equal to "https" or "ssh". 799 #[expect(clippy::unreachable, reason = "there is a bug, so we want to crash")] 800 fn is_equal_to_origin_scheme(self, other: Self) -> bool { 801 match self { 802 Self::None => matches!(other, Self::None), 803 Self::Any => true, 804 Self::Https => matches!(other, Self::Https), 805 Self::Ssh => matches!(other, Self::Ssh), 806 Self::Other(scheme) => match other { 807 Self::None => false, 808 // We want to crash and burn since there is a bug in code. 809 Self::Any | Self::NoneHttps | Self::NoneSsh | Self::NoneOther(_) => { 810 unreachable!("there is a bug in DomainOrigin::try_from") 811 } 812 Self::Https => scheme == "https", 813 Self::Ssh => scheme == "ssh", 814 Self::Other(scheme_other) => scheme == scheme_other, 815 }, 816 Self::NoneHttps => match other { 817 Self::None | Self::Https => true, 818 Self::Ssh | Self::Other(_) => false, 819 // We want to crash and burn since there is a bug in code. 820 Self::Any | Self::NoneHttps | Self::NoneSsh | Self::NoneOther(_) => { 821 unreachable!("there is a bug in DomainOrigin::try_from") 822 } 823 }, 824 Self::NoneSsh => match other { 825 Self::None | Self::Ssh => true, 826 // We want to crash and burn since there is a bug in code. 827 Self::Any | Self::NoneHttps | Self::NoneSsh | Self::NoneOther(_) => { 828 unreachable!("there is a bug in DomainOrigin::try_from") 829 } 830 Self::Https | Self::Other(_) => false, 831 }, 832 Self::NoneOther(scheme) => match other { 833 Self::None => true, 834 // We want to crash and burn since there is a bug in code. 835 Self::Any | Self::NoneHttps | Self::NoneSsh | Self::NoneOther(_) => { 836 unreachable!("there is a bug in DomainOrigin::try_from") 837 } 838 Self::Https => scheme == "https", 839 Self::Ssh => scheme == "ssh", 840 Self::Other(scheme_other) => scheme == scheme_other, 841 }, 842 } 843 } 844 } 845 impl<'a: 'b, 'b> TryFrom<&'a str> for Scheme<'b> { 846 type Error = SchemeParseErr; 847 /// `"https"` and `"ssh"` get mapped to [`Self::Https`] and [`Self::Ssh`] respectively. All other 848 /// values get mapped to [`Self::Other`]. 849 /// 850 /// # Errors 851 /// 852 /// Errors iff `s` is empty. 853 /// 854 /// # Examples 855 /// 856 /// ``` 857 /// # use webauthn_rp::request::Scheme; 858 /// assert!(matches!(Scheme::try_from("https")?, Scheme::Https)); 859 /// assert!(matches!(Scheme::try_from("https ")?, Scheme::Other(scheme) if scheme == "https ")); 860 /// assert!(matches!(Scheme::try_from("ssh")?, Scheme::Ssh)); 861 /// assert!(matches!(Scheme::try_from("Ssh")?, Scheme::Other(scheme) if scheme == "Ssh")); 862 /// // Even though one can construct an empty `Scheme` via `Scheme::Other` or `Scheme::NoneOther`, 863 /// // one cannot parse one. 864 /// assert!(Scheme::try_from("").is_err()); 865 /// # Ok::<_, webauthn_rp::AggErr>(()) 866 /// ``` 867 #[inline] 868 fn try_from(value: &'a str) -> Result<Self, Self::Error> { 869 match value { 870 "" => Err(SchemeParseErr), 871 "https" => Ok(Self::Https), 872 "ssh" => Ok(Self::Ssh), 873 _ => Ok(Self::Other(value)), 874 } 875 } 876 } 877 /// A TCP/UDP port. This can be used to make 878 /// [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin) more convenient. 879 #[derive(Clone, Copy, Debug, Default)] 880 pub enum Port { 881 /// A port must not exist when validating the origin. 882 #[default] 883 None, 884 /// Any port, or no port at all, is allowed to exist when validating the origin. 885 Any, 886 /// The contained `u16` port must exist when validating the origin. 887 Val(u16), 888 /// [`Self::None`] or [`Self::Val`]. 889 NoneVal(u16), 890 } 891 impl Port { 892 /// `self` is any `Port`; however `other` is assumed to only be a `Port` from a `DomainOrigin` returned 893 /// from `DomainOrigin::try_from`. The latter implies that `other` is only `Port::None` or `Port::Val`. 894 #[expect(clippy::unreachable, reason = "there is a bug, so we want to crash")] 895 fn is_equal_to_origin_port(self, other: Self) -> bool { 896 match self { 897 Self::None => matches!(other, Self::None), 898 Self::Any => true, 899 Self::Val(port) => match other { 900 Self::None => false, 901 // There is a bug in code so we want to crash and burn. 902 Self::Any | Self::NoneVal(_) => { 903 unreachable!("there is a bug in DomainOrigin::try_from") 904 } 905 Self::Val(port_other) => port == port_other, 906 }, 907 Self::NoneVal(port) => match other { 908 Self::None => true, 909 // There is a bug in code so we want to crash and burn. 910 Self::Any | Self::NoneVal(_) => { 911 unreachable!("there is a bug in DomainOrigin::try_from") 912 } 913 Self::Val(port_other) => port == port_other, 914 }, 915 } 916 } 917 } 918 impl FromStr for Port { 919 type Err = PortParseErr; 920 /// Parses `s` as a 16-bit unsigned integer without leading 0s returning [`Self::Val`] with the contained 921 /// `u16`. 922 /// 923 /// # Errors 924 /// 925 /// Errors iff `s` is not a valid 16-bit unsigned integer in decimal notation without leading 0s. 926 /// 927 /// # Examples 928 /// 929 /// ``` 930 /// # use webauthn_rp::request::{error::PortParseErr, Port}; 931 /// assert!(matches!("443".parse()?, Port::Val(443))); 932 /// // TCP/UDP ports have to be in canonical form: 933 /// assert!("022" 934 /// .parse::<Port>() 935 /// .map_or_else(|err| matches!(err, PortParseErr::NotCanonical), |_| false)); 936 /// # Ok::<_, webauthn_rp::AggErr>(()) 937 /// ``` 938 #[inline] 939 fn from_str(s: &str) -> Result<Self, Self::Err> { 940 s.parse().map_err(PortParseErr::ParseInt).and_then(|port| { 941 if s.len() 942 == match port { 943 ..=9 => 1, 944 10..=99 => 2, 945 100..=999 => 3, 946 1_000..=9_999 => 4, 947 10_000.. => 5, 948 } 949 { 950 Ok(Self::Val(port)) 951 } else { 952 Err(PortParseErr::NotCanonical) 953 } 954 }) 955 } 956 } 957 /// A [`tuple origin`](https://html.spec.whatwg.org/multipage/browsers.html#concept-origin-tuple). 958 /// 959 /// This can be used to make [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin) 960 /// more convenient. 961 #[derive(Clone, Copy, Debug)] 962 pub struct DomainOrigin<'a, 'b> { 963 /// The scheme. 964 pub scheme: Scheme<'a>, 965 /// The host. 966 pub host: &'b str, 967 /// The TCP/UDP port. 968 pub port: Port, 969 } 970 impl<'b> DomainOrigin<'_, 'b> { 971 /// Returns a `DomainOrigin` with [`Self::scheme`] as [`Scheme::Https`], [`Self::host`] as `host`, and 972 /// [`Self::port`] as [`Port::None`]. 973 /// 974 /// # Examples 975 /// 976 /// ``` 977 /// # extern crate alloc; 978 /// # use alloc::borrow::Cow; 979 /// # use webauthn_rp::{request::DomainOrigin, response::Origin}; 980 /// assert_eq!( 981 /// DomainOrigin::new("www.example.com"), 982 /// Origin(Cow::Borrowed("https://www.example.com")) 983 /// ); 984 /// // `DomainOrigin::new` does not allow _any_ port to exist. 985 /// assert_ne!( 986 /// DomainOrigin::new("www.example.com"), 987 /// Origin(Cow::Borrowed("https://www.example.com:443")) 988 /// ); 989 /// ``` 990 #[expect(single_use_lifetimes, reason = "false positive")] 991 #[must_use] 992 #[inline] 993 pub const fn new<'c: 'b>(host: &'c str) -> Self { 994 Self { 995 scheme: Scheme::Https, 996 host, 997 port: Port::None, 998 } 999 } 1000 /// Returns a `DomainOrigin` with [`Self::scheme`] as [`Scheme::Https`], [`Self::host`] as `host`, and 1001 /// [`Self::port`] as [`Port::Any`]. 1002 /// 1003 /// # Examples 1004 /// 1005 /// ``` 1006 /// # extern crate alloc; 1007 /// # use alloc::borrow::Cow; 1008 /// # use webauthn_rp::{request::DomainOrigin, response::Origin}; 1009 /// // Any port is allowed to exist. 1010 /// assert_eq!( 1011 /// DomainOrigin::new_ignore_port("www.example.com"), 1012 /// Origin(Cow::Borrowed("https://www.example.com:1234")) 1013 /// ); 1014 /// // A port doesn't have to exist at all either. 1015 /// assert_eq!( 1016 /// DomainOrigin::new_ignore_port("www.example.com"), 1017 /// Origin(Cow::Borrowed("https://www.example.com")) 1018 /// ); 1019 /// ``` 1020 #[expect(single_use_lifetimes, reason = "false positive")] 1021 #[must_use] 1022 #[inline] 1023 pub const fn new_ignore_port<'c: 'b>(host: &'c str) -> Self { 1024 Self { 1025 scheme: Scheme::Https, 1026 host, 1027 port: Port::Any, 1028 } 1029 } 1030 } 1031 impl PartialEq<Origin<'_>> for DomainOrigin<'_, '_> { 1032 /// Returns `true` iff [`DomainOrigin::scheme`], [`DomainOrigin::host`], and [`DomainOrigin::port`] are the 1033 /// same after calling [`DomainOrigin::try_from`] on `other.0.as_str()`. 1034 /// 1035 /// Note that [`Scheme`] and [`Port`] need not be the same variant. For example [`Scheme::Https`] and 1036 /// [`Scheme::Other`] containing `"https"` will be treated the same. 1037 #[inline] 1038 fn eq(&self, other: &Origin<'_>) -> bool { 1039 DomainOrigin::try_from(other.0.as_ref()).is_ok_and(|dom| { 1040 self.scheme.is_equal_to_origin_scheme(dom.scheme) 1041 && self.host == dom.host 1042 && self.port.is_equal_to_origin_port(dom.port) 1043 }) 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<&Origin<'_>> for DomainOrigin<'_, '_> { 1053 #[inline] 1054 fn eq(&self, other: &&Origin<'_>) -> bool { 1055 *self == **other 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 PartialEq<&DomainOrigin<'_, '_>> for Origin<'_> { 1071 #[inline] 1072 fn eq(&self, other: &&DomainOrigin<'_, '_>) -> bool { 1073 **other == *self 1074 } 1075 } 1076 impl<'a: 'b + 'c, 'b, 'c> TryFrom<&'a str> for DomainOrigin<'b, 'c> { 1077 type Error = DomainOriginParseErr; 1078 /// `value` is parsed according to the following extended regex: 1079 /// 1080 /// `^([^:]*:\/\/)?[^:]*(:.*)?$` 1081 /// 1082 /// where the `[^:]*` of the first capturing group is parsed according to [`Scheme::try_from`], and 1083 /// the `.*` of the second capturing group is parsed according to [`Port::from_str`]. 1084 /// 1085 /// # Errors 1086 /// 1087 /// Errors iff `Scheme::try_from` or `Port::from_str` fail when applicable. 1088 /// 1089 /// # Examples 1090 /// 1091 /// ``` 1092 /// # use webauthn_rp::request::{DomainOrigin, Port, Scheme}; 1093 /// assert!( 1094 /// DomainOrigin::try_from("https://www.example.com:443").map_or(false, |dom| matches!( 1095 /// dom.scheme, 1096 /// Scheme::Https 1097 /// ) && dom.host 1098 /// == "www.example.com" 1099 /// && matches!(dom.port, Port::Val(port) if port == 443)) 1100 /// ); 1101 /// // Parsing is done in a case sensitive way. 1102 /// assert!(DomainOrigin::try_from("Https://www.EXample.com").map_or( 1103 /// false, 1104 /// |dom| matches!(dom.scheme, Scheme::Other(scheme) if scheme == "Https") 1105 /// && dom.host == "www.EXample.com" 1106 /// && matches!(dom.port, Port::None) 1107 /// )); 1108 /// ``` 1109 #[inline] 1110 fn try_from(value: &'a str) -> Result<Self, Self::Error> { 1111 // Any string that contains `':'` is not a [valid domain](https://url.spec.whatwg.org/#valid-domain), and 1112 // and `"//"` never exists in a `Port`; thus if `"://"` exists, it's either invalid or delimits the scheme 1113 // from the rest of the origin. 1114 match value.split_once("://") { 1115 None => Ok((Scheme::None, value)), 1116 Some((poss_scheme, rem)) => Scheme::try_from(poss_scheme) 1117 .map_err(DomainOriginParseErr::Scheme) 1118 .map(|scheme| (scheme, rem)), 1119 } 1120 .and_then(|(scheme, rem)| { 1121 // `':'` never exists in a valid domain; thus if it exists, it's either invalid or 1122 // separates the domain from the port. 1123 rem.split_once(':') 1124 .map_or_else( 1125 || Ok((rem, Port::None)), 1126 |(rem2, poss_port)| { 1127 Port::from_str(poss_port) 1128 .map_err(DomainOriginParseErr::Port) 1129 .map(|port| (rem2, port)) 1130 }, 1131 ) 1132 .map(|(host, port)| Self { scheme, host, port }) 1133 }) 1134 } 1135 } 1136 /// [`PublicKeyCredentialDescriptor`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptor) 1137 /// associated with a registered credential. 1138 #[derive(Clone, Debug)] 1139 pub struct PublicKeyCredentialDescriptor<T> { 1140 /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-id). 1141 pub id: CredentialId<T>, 1142 /// [`transports`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-transports). 1143 pub transports: AuthTransports, 1144 } 1145 /// [`UserVerificationRequirement`](https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement). 1146 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 1147 pub enum UserVerificationRequirement { 1148 /// [`required`](https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-required). 1149 Required, 1150 /// [`discouraged`](https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-discouraged). 1151 /// 1152 /// Note some authenticators always require user verification when registering a credential (e.g., 1153 /// [CTAP 2.0](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html) 1154 /// authenticators that have had a PIN enabled). 1155 Discouraged, 1156 /// [`preferred`](https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-preferred). 1157 Preferred, 1158 } 1159 /// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint). 1160 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] 1161 pub enum Hint { 1162 /// No hints. 1163 #[default] 1164 None, 1165 /// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-security-key). 1166 SecurityKey, 1167 /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-client-device). 1168 ClientDevice, 1169 /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-hybrid). 1170 Hybrid, 1171 /// [`Self::SecurityKey`] and [`Self::ClientDevice`]. 1172 SecurityKeyClientDevice, 1173 /// [`Self::ClientDevice`] and [`Self::SecurityKey`]. 1174 ClientDeviceSecurityKey, 1175 /// [`Self::SecurityKey`] and [`Self::Hybrid`]. 1176 SecurityKeyHybrid, 1177 /// [`Self::Hybrid`] and [`Self::SecurityKey`]. 1178 HybridSecurityKey, 1179 /// [`Self::ClientDevice`] and [`Self::Hybrid`]. 1180 ClientDeviceHybrid, 1181 /// [`Self::Hybrid`] and [`Self::ClientDevice`]. 1182 HybridClientDevice, 1183 /// [`Self::SecurityKeyClientDevice`] and [`Self::Hybrid`]. 1184 SecurityKeyClientDeviceHybrid, 1185 /// [`Self::SecurityKeyHybrid`] and [`Self::ClientDevice`]. 1186 SecurityKeyHybridClientDevice, 1187 /// [`Self::ClientDeviceSecurityKey`] and [`Self::Hybrid`]. 1188 ClientDeviceSecurityKeyHybrid, 1189 /// [`Self::ClientDeviceHybrid`] and [`Self::SecurityKey`]. 1190 ClientDeviceHybridSecurityKey, 1191 /// [`Self::HybridSecurityKey`] and [`Self::ClientDevice`]. 1192 HybridSecurityKeyClientDevice, 1193 /// [`Self::HybridClientDevice`] and [`Self::SecurityKey`]. 1194 HybridClientDeviceSecurityKey, 1195 } 1196 /// Controls if the response to a requested extension is required to be sent back. 1197 /// 1198 /// Note when requiring an extension, the extension must not only be sent back but also 1199 /// contain at least one expected field (e.g., [`ClientExtensionsOutputs::cred_props`] must be 1200 /// `Some(CredentialPropertiesOutput { rk: Some(_) })`. 1201 /// 1202 /// If one wants to additionally control the values of an extension, use [`ExtensionInfo`]. 1203 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 1204 pub enum ExtensionReq { 1205 /// The response to a requested extension is required to be sent back. 1206 Require, 1207 /// The response to a requested extension is allowed, but not required, to be sent back. 1208 Allow, 1209 } 1210 /// Dictates how an extension should be processed. 1211 /// 1212 /// If one wants to only control if the extension should be returned, use [`ExtensionReq`]. 1213 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 1214 pub enum ExtensionInfo { 1215 /// Require the associated extension and enforce its value. 1216 RequireEnforceValue, 1217 /// Require the associated extension but don't enforce its value. 1218 RequireDontEnforceValue, 1219 /// Allow the associated extension to exist and enforce its value when it does exist. 1220 AllowEnforceValue, 1221 /// Allow the associated extension to exist but don't enforce its value. 1222 AllowDontEnforceValue, 1223 } 1224 impl Display for ExtensionInfo { 1225 #[inline] 1226 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 1227 f.write_str(match *self { 1228 Self::RequireEnforceValue => "require the corresponding extension response and enforce its value", 1229 Self::RequireDontEnforceValue => "require the corresponding extension response but don't enforce its value", 1230 Self::AllowEnforceValue => "don't require the corresponding extension response; but if sent, enforce its value", 1231 Self::AllowDontEnforceValue => "don't require the corresponding extension response; and if sent, don't enforce its value", 1232 }) 1233 } 1234 } 1235 /// [`CredentialMediationRequirement`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement). 1236 /// 1237 /// Note [`silent`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-silent) 1238 /// is not supported for WebAuthn credentials, and 1239 /// [`optional`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-optional) 1240 /// is just an alias for [`Self::Required`]. 1241 #[expect(clippy::doc_markdown, reason = "false positive")] 1242 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] 1243 pub enum CredentialMediationRequirement { 1244 /// [`required`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-required). 1245 /// 1246 /// This is the default mediation for ceremonies. 1247 #[default] 1248 Required, 1249 /// [`conditional`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-conditional). 1250 /// 1251 /// Note that when registering a new credential with [`CredentialCreationOptions::mediation`] set to 1252 /// `Self::Conditional`, [`UserVerificationRequirement::Required`] MUST NOT be used unless user verification 1253 /// can be explicitly performed during the ceremony. 1254 Conditional, 1255 } 1256 /// Backup requirements for the credential. 1257 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] 1258 pub enum BackupReq { 1259 /// No requirements (i.e., any [`Backup`] is allowed). 1260 #[default] 1261 None, 1262 /// Credential must not be eligible for backup. 1263 NotEligible, 1264 /// Credential must be eligible for backup. 1265 /// 1266 /// Note the existence of a backup is ignored. If a backup must exist, then use [`Self::Exists`]; if a 1267 /// backup must not exist, then use [`Self::EligibleNotExists`]. 1268 Eligible, 1269 /// Credential must be eligible for backup, but a backup must not exist. 1270 EligibleNotExists, 1271 /// Credential must be backed up. 1272 Exists, 1273 } 1274 impl From<Backup> for BackupReq { 1275 /// One may want to create `BackupReq` based on the previous `Backup` such that the subsequent `Backup` is 1276 /// essentially unchanged. 1277 /// 1278 /// Specifically this transforms [`Backup::NotEligible`] to [`Self::NotEligible`] and [`Backup::Eligible`] and 1279 /// [`Backup::Exists`] to [`Self::Eligible`]. Note this means that a credential that 1280 /// is eligible to be backed up but currently does not have a backup will be allowed to change such that it 1281 /// is backed up. Similarly, a credential that is backed up is allowed to change such that a backup no longer 1282 /// exists. 1283 #[inline] 1284 fn from(value: Backup) -> Self { 1285 if matches!(value, Backup::NotEligible) { 1286 Self::NotEligible 1287 } else { 1288 Self::Eligible 1289 } 1290 } 1291 } 1292 /// A container of "credentials". 1293 /// 1294 /// This is mainly a way to unify [`Vec`] of [`PublicKeyCredentialDescriptor`] 1295 /// and [`AllowedCredentials`]. This can be useful in situations when one only 1296 /// deals with [`AllowedCredential`]s with empty [`CredentialSpecificExtension`]s 1297 /// essentially making them the same as [`PublicKeyCredentialDescriptor`]s. 1298 /// 1299 /// # Examples 1300 /// 1301 /// ``` 1302 /// # use webauthn_rp::{ 1303 /// # request::{ 1304 /// # auth::AllowedCredentials, register::UserHandle, Credentials, PublicKeyCredentialDescriptor, 1305 /// # }, 1306 /// # response::{AuthTransports, CredentialId}, 1307 /// # }; 1308 /// /// Fetches all credentials under `user_handle` to be allowed during authentication for non-discoverable 1309 /// /// requests. 1310 /// # #[cfg(feature = "custom")] 1311 /// fn get_allowed_credentials<const LEN: usize>(user_handle: &UserHandle<LEN>) -> AllowedCredentials { 1312 /// get_credentials(user_handle) 1313 /// } 1314 /// /// Fetches all credentials under `user_handle` to be excluded during registration. 1315 /// # #[cfg(feature = "custom")] 1316 /// fn get_excluded_credentials<const LEN: usize>( 1317 /// user_handle: &UserHandle<LEN>, 1318 /// ) -> Vec<PublicKeyCredentialDescriptor<Vec<u8>>> { 1319 /// get_credentials(user_handle) 1320 /// } 1321 /// /// Used to fetch the excluded `PublicKeyCredentialDescriptor`s associated with `user_handle` during 1322 /// /// registration as well as the `AllowedCredentials` containing `AllowedCredential`s with no credential-specific 1323 /// /// extensions which is used for non-discoverable requests. 1324 /// # #[cfg(feature = "custom")] 1325 /// fn get_credentials<const LEN: usize, T>(user_handle: &UserHandle<LEN>) -> T 1326 /// where 1327 /// T: Credentials, 1328 /// PublicKeyCredentialDescriptor<Vec<u8>>: Into<T::Credential>, 1329 /// { 1330 /// let iter = get_cred_parts(user_handle); 1331 /// let len = iter.size_hint().0; 1332 /// iter.fold(T::with_capacity(len), |mut creds, parts| { 1333 /// creds.push( 1334 /// PublicKeyCredentialDescriptor { 1335 /// id: parts.0, 1336 /// transports: parts.1, 1337 /// } 1338 /// .into(), 1339 /// ); 1340 /// creds 1341 /// }) 1342 /// } 1343 /// /// Fetches all `CredentialId`s and associated `AuthTransports` under `user_handle` 1344 /// /// from the database. 1345 /// # #[cfg(feature = "custom")] 1346 /// fn get_cred_parts<const LEN: usize>( 1347 /// user_handle: &UserHandle<LEN>, 1348 /// ) -> impl Iterator<Item = (CredentialId<Vec<u8>>, AuthTransports)> { 1349 /// // ⋮ 1350 /// # [( 1351 /// # CredentialId::try_from(vec![0; 16]).unwrap(), 1352 /// # AuthTransports::NONE, 1353 /// # )] 1354 /// # .into_iter() 1355 /// } 1356 /// ``` 1357 pub trait Credentials: Sized { 1358 /// The "credential"s that make up `Self`. 1359 type Credential; 1360 /// Returns `Self`. 1361 #[inline] 1362 #[must_use] 1363 fn new() -> Self { 1364 Self::with_capacity(0) 1365 } 1366 /// Returns `Self` with at least `capacity` allocated. 1367 fn with_capacity(capacity: usize) -> Self; 1368 /// Adds `cred` to `self`. 1369 /// 1370 /// Returns `true` iff `cred` was added. 1371 fn push(&mut self, cred: Self::Credential) -> bool; 1372 /// Returns the number of [`Self::Credential`]s in `Self`. 1373 fn len(&self) -> usize; 1374 /// Returns `true` iff [`Self::len`] is `0`. 1375 #[inline] 1376 fn is_empty(&self) -> bool { 1377 self.len() == 0 1378 } 1379 } 1380 impl<T> Credentials for Vec<T> { 1381 type Credential = T; 1382 #[inline] 1383 fn with_capacity(capacity: usize) -> Self { 1384 Self::with_capacity(capacity) 1385 } 1386 #[inline] 1387 fn push(&mut self, cred: Self::Credential) -> bool { 1388 self.push(cred); 1389 true 1390 } 1391 #[inline] 1392 fn len(&self) -> usize { 1393 self.len() 1394 } 1395 } 1396 /// Additional options that control how [`Ceremony::partial_validate`] works. 1397 struct CeremonyOptions<'origins, 'top_origins, O, T> { 1398 /// Origins to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin). 1399 /// 1400 /// When this is empty, the origin that will be used will be based on 1401 /// the [`RpId`] passed to [`RegistrationServerState::verify`]. If [`RpId::Domain`], then the [`DomainOrigin`] returned from 1402 /// passing [`AsciiDomain::as_ref`] to [`DomainOrigin::new`] will be used; otherwise the [`Url`] in 1403 /// [`RpId::Url`] will be used. 1404 allowed_origins: &'origins [O], 1405 /// [Top-level origins](https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-top-level-origin) 1406 /// to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin). 1407 /// 1408 /// When this is `Some`, [`CollectedClientData::cross_origin`] is allowed to be `true`. When the contained 1409 /// `slice` is empty, [`CollectedClientData::top_origin`] must be `None`. When this is `None`, 1410 /// `CollectedClientData::cross_origin` must be `false` and `CollectedClientData::top_origin` must be `None`. 1411 allowed_top_origins: Option<&'top_origins [T]>, 1412 /// The required [`Backup`] state of the credential. 1413 backup_requirement: BackupReq, 1414 /// [`CollectedClientData::from_client_data_json_relaxed`] is used to extract [`CollectedClientData`] iff `true`. 1415 #[cfg(feature = "serde_relaxed")] 1416 client_data_json_relaxed: bool, 1417 } 1418 impl<'o, 't, O, T> From<&RegistrationVerificationOptions<'o, 't, O, T>> 1419 for CeremonyOptions<'o, 't, O, T> 1420 { 1421 fn from(value: &RegistrationVerificationOptions<'o, 't, O, T>) -> Self { 1422 Self { 1423 allowed_origins: value.allowed_origins, 1424 allowed_top_origins: value.allowed_top_origins, 1425 backup_requirement: value.backup_requirement, 1426 #[cfg(feature = "serde_relaxed")] 1427 client_data_json_relaxed: value.client_data_json_relaxed, 1428 } 1429 } 1430 } 1431 /// Functionality common to both registration and authentication ceremonies. 1432 /// 1433 /// Designed to be implemented on the _request_ side. 1434 trait Ceremony<const USER_LEN: usize, const DISCOVERABLE: bool> { 1435 /// The type of response that is associated with the ceremony. 1436 type R: Response; 1437 /// Challenge. 1438 fn rand_challenge(&self) -> SentChallenge; 1439 /// `Instant` the ceremony was expires. 1440 #[cfg(not(feature = "serializable_server_state"))] 1441 fn expiry(&self) -> Instant; 1442 /// `Instant` the ceremony was expires. 1443 #[cfg(feature = "serializable_server_state")] 1444 fn expiry(&self) -> SystemTime; 1445 /// User verification requirement. 1446 fn user_verification(&self) -> UserVerificationRequirement; 1447 /// Performs validation of ceremony criteria common to both ceremony types. 1448 #[expect( 1449 clippy::type_complexity, 1450 reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable" 1451 )] 1452 #[expect(clippy::too_many_lines, reason = "102 lines is fine")] 1453 fn partial_validate<'a, O: PartialEq<Origin<'a>>, T: PartialEq<Origin<'a>>>( 1454 &self, 1455 rp_id: &RpId, 1456 resp: &'a Self::R, 1457 key: <<Self::R as Response>::Auth as AuthResponse>::CredKey<'_>, 1458 options: &CeremonyOptions<'_, '_, O, T>, 1459 ) -> Result< 1460 <<Self::R as Response>::Auth as AuthResponse>::Auth<'a>, 1461 CeremonyErr< 1462 <<<Self::R as Response>::Auth as AuthResponse>::Auth<'a> as AuthDataContainer<'a>>::Err, 1463 >, 1464 > { 1465 // [Registration ceremony](https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential) 1466 // is handled by: 1467 // 1468 // 1. Calling code. 1469 // 2. Client code and the construction of `resp` (hopefully via [`Registration::deserialize`]). 1470 // 3. Client code and the construction of `resp` (hopefully via [`AuthenticatorAttestation::deserialize`]). 1471 // 4. Client code and the construction of `resp` (hopefully via [`ClientExtensionsOutputs::deserialize`]). 1472 // 5. Below via [`CollectedClientData::from_client_data_json_relaxed`]. 1473 // 6. Below via [`CollectedClientData::from_client_data_json_relaxed`] or [`CollectedClientData::from_client_data_json_relaxed`]. 1474 // 7. Below via [`CollectedClientData::from_client_data_json_relaxed`] or [`CollectedClientData::from_client_data_json_relaxed`]. 1475 // 8. Below. 1476 // 9. Below. 1477 // 10. Below. 1478 // 11. Below. 1479 // 12. Below via [`AuthenticatorAttestation::new`]. 1480 // 13. Below via [`AttestationObject::parse_data`]. 1481 // 14. Below. 1482 // 15. [`RegistrationServerState::verify`]. 1483 // 16. Below. 1484 // 17. Below via [`AuthenticatorData::from_cbor`]. 1485 // 18. Below. 1486 // 19. Below. 1487 // 20. [`RegistrationServerState::verify`]. 1488 // 21. Below via [`AttestationObject::parse_data`]. 1489 // 22. Below via [`AttestationObject::parse_data`]. 1490 // 23. N/A since only none and self attestations are supported. 1491 // 24. Always satisfied since only none and self attestations are supported (Item 3 is N/A). 1492 // 25. Below via [`AttestedCredentialData::from_cbor`]. 1493 // 26. Calling code. 1494 // 27. [`RegistrationServerState::verify`]. 1495 // 28. N/A since only none and self attestations are supported. 1496 // 29. [`RegistrationServerState::verify`]. 1497 // 1498 // 1499 // [Authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) 1500 // is handled by: 1501 // 1502 // 1. Calling code. 1503 // 2. Client code and the construction of `resp` (hopefully via [`Authentication::deserialize`]). 1504 // 3. Client code and the construction of `resp` (hopefully via [`AuthenticatorAssertion::deserialize`]). 1505 // 4. Client code and the construction of `resp` (hopefully via [`ClientExtensionsOutputs::deserialize`]). 1506 // 5. [`AuthenticationServerState::verify`]. 1507 // 6. [`AuthenticationServerState::verify`]. 1508 // 7. Informative only in that it defines variables. 1509 // 8. Below via [`CollectedClientData::from_client_data_json_relaxed`]. 1510 // 9. Below via [`CollectedClientData::from_client_data_json_relaxed`] or [`CollectedClientData::from_client_data_json_relaxed`]. 1511 // 10. Below via [`CollectedClientData::from_client_data_json_relaxed`] or [`CollectedClientData::from_client_data_json_relaxed`]. 1512 // 11. Below. 1513 // 12. Below. 1514 // 13. Below. 1515 // 14. Below. 1516 // 15. Below. 1517 // 16. Below via [`AuthenticatorData::from_cbor`]. 1518 // 17. Below. 1519 // 18. Below via [`AuthenticatorData::from_cbor`]. 1520 // 19. Below. 1521 // 20. Below via [`AuthenticatorAssertion::new`]. 1522 // 21. Below. 1523 // 22. [`AuthenticationServerState::verify`]. 1524 // 23. [`AuthenticationServerState::verify`]. 1525 // 24. [`AuthenticationServerState::verify`]. 1526 // 25. [`AuthenticationServerState::verify`]. 1527 1528 // Enforce timeout. 1529 #[cfg(not(feature = "serializable_server_state"))] 1530 let active = self.expiry() >= Instant::now(); 1531 #[cfg(feature = "serializable_server_state")] 1532 let active = self.expiry() >= SystemTime::now(); 1533 if active { 1534 #[cfg(feature = "serde_relaxed")] 1535 let relaxed = options.client_data_json_relaxed; 1536 #[cfg(not(feature = "serde_relaxed"))] 1537 let relaxed = false; 1538 resp.auth() 1539 // Steps 5–7, 12–13, 17, 21–22, and 25 of the registration ceremony. 1540 // Steps 8–10, 16, 18, and 20–21 of the authentication ceremony. 1541 .parse_data_and_verify_sig(key, relaxed) 1542 .map_err(CeremonyErr::AuthResp) 1543 .and_then(|(client_data_json, auth_response)| { 1544 if options.allowed_origins.is_empty() { 1545 if match *rp_id { 1546 RpId::Domain(ref dom) => { 1547 // Steps 9 and 12 of the registration and authentication ceremonies 1548 // respectively. 1549 DomainOrigin::new(dom.as_ref()) == client_data_json.origin 1550 } 1551 // Steps 9 and 12 of the registration and authentication ceremonies 1552 // respectively. 1553 RpId::Url(ref url) => url == client_data_json.origin, 1554 RpId::StaticDomain(dom) => { 1555 DomainOrigin::new(dom.0) == client_data_json.origin 1556 } 1557 } { 1558 Ok(()) 1559 } else { 1560 Err(CeremonyErr::OriginMismatch) 1561 } 1562 } else { 1563 options 1564 .allowed_origins 1565 .iter() 1566 // Steps 9 and 12 of the registration and authentication ceremonies 1567 // respectively. 1568 .find(|o| **o == client_data_json.origin) 1569 .ok_or(CeremonyErr::OriginMismatch) 1570 .map(|_| ()) 1571 } 1572 .and_then(|()| { 1573 // Steps 10–11 of the registration ceremony. 1574 // Steps 13–14 of the authentication ceremony. 1575 match options.allowed_top_origins { 1576 None => { 1577 if client_data_json.cross_origin { 1578 Err(CeremonyErr::CrossOrigin) 1579 } else if client_data_json.top_origin.is_some() { 1580 Err(CeremonyErr::TopOriginMismatch) 1581 } else { 1582 Ok(()) 1583 } 1584 } 1585 Some(top_origins) => client_data_json.top_origin.map_or(Ok(()), |t| { 1586 top_origins 1587 .iter() 1588 .find(|top| **top == t) 1589 .ok_or(CeremonyErr::TopOriginMismatch) 1590 .map(|_| ()) 1591 }), 1592 } 1593 .and_then(|()| { 1594 // Steps 8 and 11 of the registration and authentication ceremonies 1595 // respectively. 1596 if self.rand_challenge() == client_data_json.challenge { 1597 let auth_data = auth_response.authenticator_data(); 1598 rp_id 1599 // Steps 14 and 15 of the registration and authentication ceremonies 1600 // respectively. 1601 .validate_rp_id_hash(auth_data.rp_hash()) 1602 .and_then(|()| { 1603 let flag = auth_data.flag(); 1604 // Steps 16 and 17 of the registration and authentication ceremonies 1605 // respectively. 1606 if flag.user_verified 1607 || !matches!( 1608 self.user_verification(), 1609 UserVerificationRequirement::Required 1610 ) 1611 { 1612 // Steps 18–19 of the registration ceremony. 1613 // Step 19 of the authentication ceremony. 1614 match options.backup_requirement { 1615 BackupReq::None => Ok(()), 1616 BackupReq::NotEligible => { 1617 if matches!(flag.backup, Backup::NotEligible) { 1618 Ok(()) 1619 } else { 1620 Err(CeremonyErr::BackupEligible) 1621 } 1622 } 1623 BackupReq::Eligible => { 1624 if matches!(flag.backup, Backup::NotEligible) { 1625 Err(CeremonyErr::BackupNotEligible) 1626 } else { 1627 Ok(()) 1628 } 1629 } 1630 BackupReq::EligibleNotExists => { 1631 if matches!(flag.backup, Backup::Eligible) { 1632 Ok(()) 1633 } else { 1634 Err(CeremonyErr::BackupExists) 1635 } 1636 } 1637 BackupReq::Exists => { 1638 if matches!(flag.backup, Backup::Exists) { 1639 Ok(()) 1640 } else { 1641 Err(CeremonyErr::BackupDoesNotExist) 1642 } 1643 } 1644 } 1645 } else { 1646 Err(CeremonyErr::UserNotVerified) 1647 } 1648 }) 1649 .map(|()| auth_response) 1650 } else { 1651 Err(CeremonyErr::ChallengeMismatch) 1652 } 1653 }) 1654 }) 1655 }) 1656 } else { 1657 Err(CeremonyErr::Timeout) 1658 } 1659 } 1660 } 1661 /// "Ceremonies" stored on the server that expire after a certain duration. 1662 /// 1663 /// Types like [`RegistrationServerState`] and [`DiscoverableAuthenticationServerState`] are based on [`Challenge`]s 1664 /// that expire after a certain duration. 1665 pub trait TimedCeremony { 1666 /// Returns the `Instant` the ceremony expires. 1667 /// 1668 /// Note when `serializable_server_state` is enabled, [`SystemTime`] is returned instead. 1669 #[cfg(any(doc, not(feature = "serializable_server_state")))] 1670 fn expiration(&self) -> Instant; 1671 /// Returns the `SystemTime` the ceremony expires. 1672 #[cfg(all(not(doc), feature = "serializable_server_state"))] 1673 fn expiration(&self) -> SystemTime; 1674 } 1675 /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues). 1676 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 1677 pub struct PrfInput<'first, 'second> { 1678 /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first). 1679 pub first: &'first [u8], 1680 /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second). 1681 pub second: Option<&'second [u8]>, 1682 } 1683 impl<'first, 'second> PrfInput<'first, 'second> { 1684 /// Returns a `PrfInput` with [`Self::first`] set to `first` and [`Self::second`] set to `None`. 1685 #[expect(single_use_lifetimes, reason = "false positive")] 1686 #[inline] 1687 #[must_use] 1688 pub const fn with_first<'a: 'first>(first: &'a [u8]) -> Self { 1689 Self { 1690 first, 1691 second: None, 1692 } 1693 } 1694 /// Same as [`Self::with_first`] except [`Self::second`] is set to `Some` containing `second`. 1695 #[expect(single_use_lifetimes, reason = "false positive")] 1696 #[inline] 1697 #[must_use] 1698 pub const fn with_two<'a: 'first, 'b: 'second>(first: &'a [u8], second: &'b [u8]) -> Self { 1699 Self { 1700 first, 1701 second: Some(second), 1702 } 1703 } 1704 } 1705 /// The number of milliseconds in 5 minutes. 1706 /// 1707 /// This is the recommended default timeout duration for ceremonies 1708 /// [in the spec](https://www.w3.org/TR/webauthn-3/#sctn-timeout-recommended-range). 1709 pub const FIVE_MINUTES: NonZeroU32 = NonZeroU32::new(300_000).unwrap(); 1710 #[cfg(test)] 1711 mod tests { 1712 use super::AsciiDomainStatic; 1713 #[cfg(feature = "custom")] 1714 use super::{ 1715 super::{ 1716 AggErr, AuthenticatedCredential, 1717 response::{ 1718 AuthTransports, AuthenticatorAttachment, Backup, CredentialId, 1719 auth::{ 1720 DiscoverableAuthentication, DiscoverableAuthenticatorAssertion, 1721 NonDiscoverableAuthentication, NonDiscoverableAuthenticatorAssertion, 1722 }, 1723 register::{ 1724 AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation, 1725 AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputs, 1726 ClientExtensionsOutputsStaticState, CompressedP256PubKey, CompressedP384PubKey, 1727 CompressedPubKey, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, 1728 Registration, RsaPubKey, StaticState, UncompressedPubKey, 1729 }, 1730 }, 1731 }, 1732 Challenge, Credentials as _, ExtensionInfo, ExtensionReq, PrfInput, 1733 PublicKeyCredentialDescriptor, RpId, UserVerificationRequirement, 1734 auth::{ 1735 AllowedCredential, AllowedCredentials, AuthenticationVerificationOptions, 1736 CredentialSpecificExtension, DiscoverableCredentialRequestOptions, 1737 Extension as AuthExt, NonDiscoverableCredentialRequestOptions, PrfInputOwned, 1738 }, 1739 register::{ 1740 CredProtect, CredentialCreationOptions, DisplayName, Extension as RegExt, 1741 FourToSixtyThree, PublicKeyCredentialUserEntity, RegistrationVerificationOptions, 1742 UserHandle, 1743 }, 1744 }; 1745 #[cfg(feature = "custom")] 1746 use ed25519_dalek::{Signer as _, SigningKey}; 1747 #[cfg(feature = "custom")] 1748 use p256::{ 1749 ecdsa::{DerSignature as P256DerSig, SigningKey as P256Key}, 1750 elliptic_curve::sec1::Tag, 1751 }; 1752 #[cfg(feature = "custom")] 1753 use p384::ecdsa::{DerSignature as P384DerSig, SigningKey as P384Key}; 1754 #[cfg(feature = "custom")] 1755 use rsa::{ 1756 BigUint, RsaPrivateKey, 1757 pkcs1v15::SigningKey as RsaKey, 1758 sha2::{Digest as _, Sha256}, 1759 signature::{Keypair as _, SignatureEncoding as _}, 1760 traits::PublicKeyParts as _, 1761 }; 1762 use serde_json as _; 1763 #[cfg(feature = "custom")] 1764 const CBOR_UINT: u8 = 0b000_00000; 1765 #[cfg(feature = "custom")] 1766 const CBOR_NEG: u8 = 0b001_00000; 1767 #[cfg(feature = "custom")] 1768 const CBOR_BYTES: u8 = 0b010_00000; 1769 #[cfg(feature = "custom")] 1770 const CBOR_TEXT: u8 = 0b011_00000; 1771 #[cfg(feature = "custom")] 1772 const CBOR_MAP: u8 = 0b101_00000; 1773 #[cfg(feature = "custom")] 1774 const CBOR_SIMPLE: u8 = 0b111_00000; 1775 #[cfg(feature = "custom")] 1776 const CBOR_TRUE: u8 = CBOR_SIMPLE | 21; 1777 #[test] 1778 fn ascii_domain_static() { 1779 /// No trailing dot, max label length, max domain length. 1780 const LONG: AsciiDomainStatic = AsciiDomainStatic::new( 1781 "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww", 1782 ) 1783 .unwrap(); 1784 /// Trailing dot, min label length, max domain length. 1785 const LONG_TRAILING: AsciiDomainStatic = AsciiDomainStatic::new("w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.").unwrap(); 1786 /// Single character domain. 1787 const SHORT: AsciiDomainStatic = AsciiDomainStatic::new("w").unwrap(); 1788 let long_label = "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"; 1789 assert_eq!(long_label.len(), 63); 1790 let mut long = format!("{long_label}.{long_label}.{long_label}.{long_label}"); 1791 _ = long.pop(); 1792 _ = long.pop(); 1793 assert_eq!(LONG.0.len(), 253); 1794 assert_eq!(LONG.0, long.as_str()); 1795 let trailing = "w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w."; 1796 assert_eq!(LONG_TRAILING.0.len(), 254); 1797 assert_eq!(LONG_TRAILING.0, trailing); 1798 assert_eq!(SHORT.0.len(), 1); 1799 assert_eq!(SHORT.0, "w"); 1800 assert!(AsciiDomainStatic::new("www.Example.com").is_none()); 1801 assert!(AsciiDomainStatic::new("").is_none()); 1802 assert!(AsciiDomainStatic::new(".").is_none()); 1803 assert!(AsciiDomainStatic::new("www..c").is_none()); 1804 let too_long_label = "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"; 1805 assert_eq!(too_long_label.len(), 64); 1806 assert!(AsciiDomainStatic::new(too_long_label).is_none()); 1807 let dom_254_no_trailing_dot = "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"; 1808 assert_eq!(dom_254_no_trailing_dot.len(), 254); 1809 assert!(AsciiDomainStatic::new(dom_254_no_trailing_dot).is_none()); 1810 assert!(AsciiDomainStatic::new("\u{3bb}.com").is_none()); 1811 } 1812 #[cfg(feature = "custom")] 1813 const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap(); 1814 #[expect(clippy::panic_in_result_fn, reason = "OK in tests")] 1815 #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] 1816 #[expect(clippy::too_many_lines, reason = "a lot to test")] 1817 #[test] 1818 #[cfg(feature = "custom")] 1819 fn eddsa_reg() -> Result<(), AggErr> { 1820 let id = UserHandle::from([0]); 1821 let mut opts = CredentialCreationOptions::passkey( 1822 RP_ID, 1823 PublicKeyCredentialUserEntity { 1824 name: "foo".try_into()?, 1825 id: &id, 1826 display_name: DisplayName::Blank, 1827 }, 1828 Vec::new(), 1829 ); 1830 opts.public_key.challenge = Challenge(0); 1831 opts.public_key.extensions = RegExt { 1832 cred_props: None, 1833 cred_protect: CredProtect::UserVerificationRequired( 1834 false, 1835 ExtensionInfo::RequireEnforceValue, 1836 ), 1837 min_pin_length: Some((FourToSixtyThree::Ten, ExtensionInfo::RequireEnforceValue)), 1838 prf: Some(( 1839 PrfInput { 1840 first: [0].as_slice(), 1841 second: None, 1842 }, 1843 ExtensionInfo::RequireEnforceValue, 1844 )), 1845 }; 1846 let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec(); 1847 // We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information. 1848 let mut attestation_object = Vec::new(); 1849 attestation_object.extend_from_slice( 1850 [ 1851 CBOR_MAP | 3, 1852 CBOR_TEXT | 3, 1853 b'f', 1854 b'm', 1855 b't', 1856 CBOR_TEXT | 6, 1857 b'p', 1858 b'a', 1859 b'c', 1860 b'k', 1861 b'e', 1862 b'd', 1863 CBOR_TEXT | 7, 1864 b'a', 1865 b't', 1866 b't', 1867 b'S', 1868 b't', 1869 b'm', 1870 b't', 1871 CBOR_MAP | 2, 1872 CBOR_TEXT | 3, 1873 b'a', 1874 b'l', 1875 b'g', 1876 // COSE EdDSA. 1877 CBOR_NEG | 7, 1878 CBOR_TEXT | 3, 1879 b's', 1880 b'i', 1881 b'g', 1882 CBOR_BYTES | 24, 1883 64, 1884 0, 1885 0, 1886 0, 1887 0, 1888 0, 1889 0, 1890 0, 1891 0, 1892 0, 1893 0, 1894 0, 1895 0, 1896 0, 1897 0, 1898 0, 1899 0, 1900 0, 1901 0, 1902 0, 1903 0, 1904 0, 1905 0, 1906 0, 1907 0, 1908 0, 1909 0, 1910 0, 1911 0, 1912 0, 1913 0, 1914 0, 1915 0, 1916 0, 1917 0, 1918 0, 1919 0, 1920 0, 1921 0, 1922 0, 1923 0, 1924 0, 1925 0, 1926 0, 1927 0, 1928 0, 1929 0, 1930 0, 1931 0, 1932 0, 1933 0, 1934 0, 1935 0, 1936 0, 1937 0, 1938 0, 1939 0, 1940 0, 1941 0, 1942 0, 1943 0, 1944 0, 1945 0, 1946 0, 1947 0, 1948 CBOR_TEXT | 8, 1949 b'a', 1950 b'u', 1951 b't', 1952 b'h', 1953 b'D', 1954 b'a', 1955 b't', 1956 b'a', 1957 CBOR_BYTES | 24, 1958 // Length is 154. 1959 154, 1960 // RP ID HASH. 1961 // This will be overwritten later. 1962 0, 1963 0, 1964 0, 1965 0, 1966 0, 1967 0, 1968 0, 1969 0, 1970 0, 1971 0, 1972 0, 1973 0, 1974 0, 1975 0, 1976 0, 1977 0, 1978 0, 1979 0, 1980 0, 1981 0, 1982 0, 1983 0, 1984 0, 1985 0, 1986 0, 1987 0, 1988 0, 1989 0, 1990 0, 1991 0, 1992 0, 1993 0, 1994 // FLAGS. 1995 // UP, UV, AT, and ED (right-to-left). 1996 0b1100_0101, 1997 // COUNTER. 1998 // 0 as 32-bit big endian. 1999 0, 2000 0, 2001 0, 2002 0, 2003 // AAGUID. 2004 0, 2005 0, 2006 0, 2007 0, 2008 0, 2009 0, 2010 0, 2011 0, 2012 0, 2013 0, 2014 0, 2015 0, 2016 0, 2017 0, 2018 0, 2019 0, 2020 // L. 2021 // CREDENTIAL ID length is 16 as 16-bit big endian. 2022 0, 2023 16, 2024 // CREDENTIAL ID. 2025 0, 2026 0, 2027 0, 2028 0, 2029 0, 2030 0, 2031 0, 2032 0, 2033 0, 2034 0, 2035 0, 2036 0, 2037 0, 2038 0, 2039 0, 2040 0, 2041 CBOR_MAP | 4, 2042 // COSE kty. 2043 CBOR_UINT | 1, 2044 // COSE OKP. 2045 CBOR_UINT | 1, 2046 // COSE alg. 2047 CBOR_UINT | 3, 2048 // COSE EdDSA. 2049 CBOR_NEG | 7, 2050 // COSE OKP crv. 2051 CBOR_NEG, 2052 // COSE Ed25519. 2053 CBOR_UINT | 6, 2054 // COSE OKP x. 2055 CBOR_NEG | 1, 2056 CBOR_BYTES | 24, 2057 // Length is 32. 2058 32, 2059 // Compressed-y coordinate. 2060 // This will be overwritten later. 2061 0, 2062 0, 2063 0, 2064 0, 2065 0, 2066 0, 2067 0, 2068 0, 2069 0, 2070 0, 2071 0, 2072 0, 2073 0, 2074 0, 2075 0, 2076 0, 2077 0, 2078 0, 2079 0, 2080 0, 2081 0, 2082 0, 2083 0, 2084 0, 2085 0, 2086 0, 2087 0, 2088 0, 2089 0, 2090 0, 2091 0, 2092 0, 2093 CBOR_MAP | 3, 2094 CBOR_TEXT | 11, 2095 b'c', 2096 b'r', 2097 b'e', 2098 b'd', 2099 b'P', 2100 b'r', 2101 b'o', 2102 b't', 2103 b'e', 2104 b'c', 2105 b't', 2106 // userVerificationRequired. 2107 CBOR_UINT | 3, 2108 // CBOR text of length 11. 2109 CBOR_TEXT | 11, 2110 b'h', 2111 b'm', 2112 b'a', 2113 b'c', 2114 b'-', 2115 b's', 2116 b'e', 2117 b'c', 2118 b'r', 2119 b'e', 2120 b't', 2121 CBOR_TRUE, 2122 CBOR_TEXT | 12, 2123 b'm', 2124 b'i', 2125 b'n', 2126 b'P', 2127 b'i', 2128 b'n', 2129 b'L', 2130 b'e', 2131 b'n', 2132 b'g', 2133 b't', 2134 b'h', 2135 CBOR_UINT | 16, 2136 ] 2137 .as_slice(), 2138 ); 2139 attestation_object.extend_from_slice(&Sha256::digest(client_data_json.as_slice())); 2140 let sig_key = SigningKey::from_bytes(&[0; 32]); 2141 let ver_key = sig_key.verifying_key(); 2142 let pub_key = ver_key.as_bytes(); 2143 attestation_object[107..139].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes())); 2144 attestation_object[188..220].copy_from_slice(pub_key); 2145 let sig = sig_key.sign(&attestation_object[107..]); 2146 attestation_object[32..96].copy_from_slice(sig.to_bytes().as_slice()); 2147 attestation_object.truncate(261); 2148 assert!(matches!(opts.start_ceremony()?.0.verify( 2149 RP_ID, 2150 &Registration { 2151 response: AuthenticatorAttestation::new( 2152 client_data_json, 2153 attestation_object, 2154 AuthTransports::NONE, 2155 ), 2156 authenticator_attachment: AuthenticatorAttachment::None, 2157 client_extension_results: ClientExtensionsOutputs { 2158 cred_props: None, 2159 prf: Some(AuthenticationExtensionsPrfOutputs { enabled: true, }), 2160 }, 2161 }, 2162 &RegistrationVerificationOptions::<&str, &str>::default(), 2163 )?.static_state.credential_public_key, UncompressedPubKey::Ed25519(k) if k.into_inner() == pub_key)); 2164 Ok(()) 2165 } 2166 #[expect(clippy::panic_in_result_fn, reason = "OK in tests")] 2167 #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] 2168 #[expect(clippy::too_many_lines, reason = "a lot to test")] 2169 #[test] 2170 #[cfg(feature = "custom")] 2171 fn eddsa_auth() -> Result<(), AggErr> { 2172 let mut creds = AllowedCredentials::with_capacity(1); 2173 _ = creds.push(AllowedCredential { 2174 credential: PublicKeyCredentialDescriptor { 2175 id: CredentialId::try_from(vec![0; 16])?, 2176 transports: AuthTransports::NONE, 2177 }, 2178 extension: CredentialSpecificExtension { 2179 prf: Some(PrfInputOwned { 2180 first: Vec::new(), 2181 second: Some(Vec::new()), 2182 ext_req: ExtensionReq::Require, 2183 }), 2184 }, 2185 }); 2186 let mut opts = NonDiscoverableCredentialRequestOptions::second_factor(RP_ID, creds); 2187 opts.options.user_verification = UserVerificationRequirement::Required; 2188 opts.options.challenge = Challenge(0); 2189 opts.options.extensions = AuthExt { prf: None }; 2190 let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec(); 2191 // We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information. 2192 let mut authenticator_data = Vec::with_capacity(164); 2193 authenticator_data.extend_from_slice( 2194 [ 2195 // rpIdHash. 2196 // This will be overwritten later. 2197 0, 2198 0, 2199 0, 2200 0, 2201 0, 2202 0, 2203 0, 2204 0, 2205 0, 2206 0, 2207 0, 2208 0, 2209 0, 2210 0, 2211 0, 2212 0, 2213 0, 2214 0, 2215 0, 2216 0, 2217 0, 2218 0, 2219 0, 2220 0, 2221 0, 2222 0, 2223 0, 2224 0, 2225 0, 2226 0, 2227 0, 2228 0, 2229 // flags. 2230 // UP, UV, and ED (right-to-left). 2231 0b1000_0101, 2232 // signCount. 2233 // 0 as 32-bit big endian. 2234 0, 2235 0, 2236 0, 2237 0, 2238 CBOR_MAP | 1, 2239 CBOR_TEXT | 11, 2240 b'h', 2241 b'm', 2242 b'a', 2243 b'c', 2244 b'-', 2245 b's', 2246 b'e', 2247 b'c', 2248 b'r', 2249 b'e', 2250 b't', 2251 CBOR_BYTES | 24, 2252 // Length is 80. 2253 80, 2254 // Two HMAC outputs concatenated and encrypted. 2255 0, 2256 0, 2257 0, 2258 0, 2259 0, 2260 0, 2261 0, 2262 0, 2263 0, 2264 0, 2265 0, 2266 0, 2267 0, 2268 0, 2269 0, 2270 0, 2271 0, 2272 0, 2273 0, 2274 0, 2275 0, 2276 0, 2277 0, 2278 0, 2279 0, 2280 0, 2281 0, 2282 0, 2283 0, 2284 0, 2285 0, 2286 0, 2287 0, 2288 0, 2289 0, 2290 0, 2291 0, 2292 0, 2293 0, 2294 0, 2295 0, 2296 0, 2297 0, 2298 0, 2299 0, 2300 0, 2301 0, 2302 0, 2303 0, 2304 0, 2305 0, 2306 0, 2307 0, 2308 0, 2309 0, 2310 0, 2311 0, 2312 0, 2313 0, 2314 0, 2315 0, 2316 0, 2317 0, 2318 0, 2319 0, 2320 0, 2321 0, 2322 0, 2323 0, 2324 0, 2325 0, 2326 0, 2327 0, 2328 0, 2329 0, 2330 0, 2331 0, 2332 0, 2333 0, 2334 0, 2335 ] 2336 .as_slice(), 2337 ); 2338 authenticator_data[..32].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes())); 2339 authenticator_data.extend_from_slice(&Sha256::digest(client_data_json.as_slice())); 2340 let ed_priv = SigningKey::from([0; 32]); 2341 let sig = ed_priv.sign(authenticator_data.as_slice()).to_vec(); 2342 authenticator_data.truncate(132); 2343 assert!(!opts.start_ceremony()?.0.verify( 2344 RP_ID, 2345 &NonDiscoverableAuthentication { 2346 raw_id: CredentialId::try_from(vec![0; 16])?, 2347 response: NonDiscoverableAuthenticatorAssertion::with_user( 2348 client_data_json, 2349 authenticator_data, 2350 sig, 2351 UserHandle::from([0]), 2352 ), 2353 authenticator_attachment: AuthenticatorAttachment::None, 2354 }, 2355 &mut AuthenticatedCredential::new( 2356 CredentialId::try_from([0; 16].as_slice())?, 2357 &UserHandle::from([0]), 2358 StaticState { 2359 credential_public_key: CompressedPubKey::<_, &[u8], &[u8], &[u8]>::Ed25519( 2360 Ed25519PubKey::from(ed_priv.verifying_key().to_bytes()), 2361 ), 2362 extensions: AuthenticatorExtensionOutputStaticState { 2363 cred_protect: CredentialProtectionPolicy::None, 2364 hmac_secret: Some(true), 2365 }, 2366 client_extension_results: ClientExtensionsOutputsStaticState { 2367 prf: Some(AuthenticationExtensionsPrfOutputs { enabled: true }), 2368 } 2369 }, 2370 DynamicState { 2371 user_verified: true, 2372 backup: Backup::NotEligible, 2373 sign_count: 0, 2374 authenticator_attachment: AuthenticatorAttachment::None, 2375 }, 2376 )?, 2377 &AuthenticationVerificationOptions::<&str, &str>::default(), 2378 )?); 2379 Ok(()) 2380 } 2381 #[expect( 2382 clippy::panic_in_result_fn, 2383 clippy::unwrap_in_result, 2384 clippy::unwrap_used, 2385 reason = "OK in tests" 2386 )] 2387 #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] 2388 #[expect(clippy::too_many_lines, reason = "a lot to test")] 2389 #[test] 2390 #[cfg(feature = "custom")] 2391 fn es256_reg() -> Result<(), AggErr> { 2392 let id = UserHandle::from([0]); 2393 let mut opts = CredentialCreationOptions::passkey( 2394 RP_ID, 2395 PublicKeyCredentialUserEntity { 2396 name: "foo".try_into()?, 2397 id: &id, 2398 display_name: DisplayName::Blank, 2399 }, 2400 Vec::new(), 2401 ); 2402 opts.public_key.challenge = Challenge(0); 2403 let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec(); 2404 // We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information. 2405 let mut attestation_object = Vec::with_capacity(210); 2406 attestation_object.extend_from_slice( 2407 [ 2408 CBOR_MAP | 3, 2409 CBOR_TEXT | 3, 2410 b'f', 2411 b'm', 2412 b't', 2413 CBOR_TEXT | 4, 2414 b'n', 2415 b'o', 2416 b'n', 2417 b'e', 2418 CBOR_TEXT | 7, 2419 b'a', 2420 b't', 2421 b't', 2422 b'S', 2423 b't', 2424 b'm', 2425 b't', 2426 CBOR_MAP, 2427 CBOR_TEXT | 8, 2428 b'a', 2429 b'u', 2430 b't', 2431 b'h', 2432 b'D', 2433 b'a', 2434 b't', 2435 b'a', 2436 CBOR_BYTES | 24, 2437 // Length is 148. 2438 148, 2439 // RP ID HASH. 2440 // This will be overwritten later. 2441 0, 2442 0, 2443 0, 2444 0, 2445 0, 2446 0, 2447 0, 2448 0, 2449 0, 2450 0, 2451 0, 2452 0, 2453 0, 2454 0, 2455 0, 2456 0, 2457 0, 2458 0, 2459 0, 2460 0, 2461 0, 2462 0, 2463 0, 2464 0, 2465 0, 2466 0, 2467 0, 2468 0, 2469 0, 2470 0, 2471 0, 2472 0, 2473 // FLAGS. 2474 // UP, UV, and AT (right-to-left). 2475 0b0100_0101, 2476 // COUNTER. 2477 // 0 as 32-bit big endian. 2478 0, 2479 0, 2480 0, 2481 0, 2482 // AAGUID. 2483 0, 2484 0, 2485 0, 2486 0, 2487 0, 2488 0, 2489 0, 2490 0, 2491 0, 2492 0, 2493 0, 2494 0, 2495 0, 2496 0, 2497 0, 2498 0, 2499 // L. 2500 // CREDENTIAL ID length is 16 as 16-bit big endian. 2501 0, 2502 16, 2503 // CREDENTIAL ID. 2504 0, 2505 0, 2506 0, 2507 0, 2508 0, 2509 0, 2510 0, 2511 0, 2512 0, 2513 0, 2514 0, 2515 0, 2516 0, 2517 0, 2518 0, 2519 0, 2520 CBOR_MAP | 5, 2521 // COSE kty. 2522 CBOR_UINT | 1, 2523 // COSE EC2. 2524 CBOR_UINT | 2, 2525 // COSE alg. 2526 CBOR_UINT | 3, 2527 // COSE ES256. 2528 CBOR_NEG | 6, 2529 // COSE EC2 crv. 2530 CBOR_NEG, 2531 // COSE P-256. 2532 CBOR_UINT | 1, 2533 // COSE EC2 x. 2534 CBOR_NEG | 1, 2535 CBOR_BYTES | 24, 2536 // Length is 32. 2537 32, 2538 // X-coordinate. This will be overwritten later. 2539 0, 2540 0, 2541 0, 2542 0, 2543 0, 2544 0, 2545 0, 2546 0, 2547 0, 2548 0, 2549 0, 2550 0, 2551 0, 2552 0, 2553 0, 2554 0, 2555 0, 2556 0, 2557 0, 2558 0, 2559 0, 2560 0, 2561 0, 2562 0, 2563 0, 2564 0, 2565 0, 2566 0, 2567 0, 2568 0, 2569 0, 2570 0, 2571 // COSE EC2 y. 2572 CBOR_NEG | 2, 2573 CBOR_BYTES | 24, 2574 // Length is 32. 2575 32, 2576 // Y-coordinate. This will be overwritten later. 2577 0, 2578 0, 2579 0, 2580 0, 2581 0, 2582 0, 2583 0, 2584 0, 2585 0, 2586 0, 2587 0, 2588 0, 2589 0, 2590 0, 2591 0, 2592 0, 2593 0, 2594 0, 2595 0, 2596 0, 2597 0, 2598 0, 2599 0, 2600 0, 2601 0, 2602 0, 2603 0, 2604 0, 2605 0, 2606 0, 2607 0, 2608 0, 2609 ] 2610 .as_slice(), 2611 ); 2612 attestation_object[30..62].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes())); 2613 let p256_key = P256Key::from_bytes( 2614 &[ 2615 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115, 2616 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95, 2617 ] 2618 .into(), 2619 ) 2620 .unwrap() 2621 .verifying_key() 2622 .to_encoded_point(false); 2623 let x = p256_key.x().unwrap(); 2624 let y = p256_key.y().unwrap(); 2625 attestation_object[111..143].copy_from_slice(x); 2626 attestation_object[146..].copy_from_slice(y); 2627 assert!(matches!(opts.start_ceremony()?.0.verify( 2628 RP_ID, 2629 &Registration { 2630 response: AuthenticatorAttestation::new( 2631 client_data_json, 2632 attestation_object, 2633 AuthTransports::NONE, 2634 ), 2635 authenticator_attachment: AuthenticatorAttachment::None, 2636 client_extension_results: ClientExtensionsOutputs { 2637 cred_props: None, 2638 prf: None, 2639 }, 2640 }, 2641 &RegistrationVerificationOptions::<&str, &str>::default(), 2642 )?.static_state.credential_public_key, UncompressedPubKey::P256(k) if *k.x() == **x && *k.y() == **y)); 2643 Ok(()) 2644 } 2645 #[expect( 2646 clippy::panic_in_result_fn, 2647 clippy::unwrap_in_result, 2648 clippy::unwrap_used, 2649 reason = "OK in tests" 2650 )] 2651 #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] 2652 #[test] 2653 #[cfg(feature = "custom")] 2654 fn es256_auth() -> Result<(), AggErr> { 2655 let mut opts = DiscoverableCredentialRequestOptions::passkey(RP_ID); 2656 opts.public_key.challenge = Challenge(0); 2657 let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec(); 2658 // We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information. 2659 let mut authenticator_data = Vec::with_capacity(69); 2660 authenticator_data.extend_from_slice( 2661 [ 2662 // rpIdHash. 2663 // This will be overwritten later. 2664 0, 2665 0, 2666 0, 2667 0, 2668 0, 2669 0, 2670 0, 2671 0, 2672 0, 2673 0, 2674 0, 2675 0, 2676 0, 2677 0, 2678 0, 2679 0, 2680 0, 2681 0, 2682 0, 2683 0, 2684 0, 2685 0, 2686 0, 2687 0, 2688 0, 2689 0, 2690 0, 2691 0, 2692 0, 2693 0, 2694 0, 2695 0, 2696 // flags. 2697 // UP and UV (right-to-left). 2698 0b0000_0101, 2699 // signCount. 2700 // 0 as 32-bit big endian. 2701 0, 2702 0, 2703 0, 2704 0, 2705 ] 2706 .as_slice(), 2707 ); 2708 authenticator_data[..32].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes())); 2709 authenticator_data.extend_from_slice(&Sha256::digest(client_data_json.as_slice())); 2710 let p256_key = P256Key::from_bytes( 2711 &[ 2712 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115, 2713 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95, 2714 ] 2715 .into(), 2716 ) 2717 .unwrap(); 2718 let der_sig: P256DerSig = p256_key.sign(authenticator_data.as_slice()); 2719 let pub_key = p256_key.verifying_key().to_encoded_point(true); 2720 authenticator_data.truncate(37); 2721 assert!(!opts.start_ceremony()?.0.verify( 2722 RP_ID, 2723 &DiscoverableAuthentication { 2724 raw_id: CredentialId::try_from(vec![0; 16])?, 2725 response: DiscoverableAuthenticatorAssertion::new( 2726 client_data_json, 2727 authenticator_data, 2728 der_sig.as_bytes().into(), 2729 UserHandle::from([0]), 2730 ), 2731 authenticator_attachment: AuthenticatorAttachment::None, 2732 }, 2733 &mut AuthenticatedCredential::new( 2734 CredentialId::try_from([0; 16].as_slice())?, 2735 &UserHandle::from([0]), 2736 StaticState { 2737 credential_public_key: CompressedPubKey::<&[u8], _, &[u8], &[u8]>::P256( 2738 CompressedP256PubKey::from(( 2739 (*pub_key.x().unwrap()).into(), 2740 pub_key.tag() == Tag::CompressedOddY 2741 )), 2742 ), 2743 extensions: AuthenticatorExtensionOutputStaticState { 2744 cred_protect: CredentialProtectionPolicy::None, 2745 hmac_secret: None, 2746 }, 2747 client_extension_results: ClientExtensionsOutputsStaticState { prf: None } 2748 }, 2749 DynamicState { 2750 user_verified: true, 2751 backup: Backup::NotEligible, 2752 sign_count: 0, 2753 authenticator_attachment: AuthenticatorAttachment::None, 2754 }, 2755 )?, 2756 &AuthenticationVerificationOptions::<&str, &str>::default(), 2757 )?); 2758 Ok(()) 2759 } 2760 #[expect( 2761 clippy::panic_in_result_fn, 2762 clippy::unwrap_in_result, 2763 clippy::unwrap_used, 2764 reason = "OK in tests" 2765 )] 2766 #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] 2767 #[expect(clippy::too_many_lines, reason = "a lot to test")] 2768 #[test] 2769 #[cfg(feature = "custom")] 2770 fn es384_reg() -> Result<(), AggErr> { 2771 let id = UserHandle::from([0]); 2772 let mut opts = CredentialCreationOptions::passkey( 2773 RP_ID, 2774 PublicKeyCredentialUserEntity { 2775 name: "foo".try_into()?, 2776 id: &id, 2777 display_name: DisplayName::Blank, 2778 }, 2779 Vec::new(), 2780 ); 2781 opts.public_key.challenge = Challenge(0); 2782 let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec(); 2783 // We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information. 2784 let mut attestation_object = Vec::with_capacity(243); 2785 attestation_object.extend_from_slice( 2786 [ 2787 CBOR_MAP | 3, 2788 CBOR_TEXT | 3, 2789 b'f', 2790 b'm', 2791 b't', 2792 CBOR_TEXT | 4, 2793 b'n', 2794 b'o', 2795 b'n', 2796 b'e', 2797 // CBOR text of length 7. 2798 CBOR_TEXT | 7, 2799 b'a', 2800 b't', 2801 b't', 2802 b'S', 2803 b't', 2804 b'm', 2805 b't', 2806 CBOR_MAP, 2807 CBOR_TEXT | 8, 2808 b'a', 2809 b'u', 2810 b't', 2811 b'h', 2812 b'D', 2813 b'a', 2814 b't', 2815 b'a', 2816 CBOR_BYTES | 24, 2817 // Length is 181. 2818 181, 2819 // RP ID HASH. 2820 // This will be overwritten later. 2821 0, 2822 0, 2823 0, 2824 0, 2825 0, 2826 0, 2827 0, 2828 0, 2829 0, 2830 0, 2831 0, 2832 0, 2833 0, 2834 0, 2835 0, 2836 0, 2837 0, 2838 0, 2839 0, 2840 0, 2841 0, 2842 0, 2843 0, 2844 0, 2845 0, 2846 0, 2847 0, 2848 0, 2849 0, 2850 0, 2851 0, 2852 0, 2853 // FLAGS. 2854 // UP, UV, and AT (right-to-left). 2855 0b0100_0101, 2856 // COUNTER. 2857 // 0 as 32-bit big-endian. 2858 0, 2859 0, 2860 0, 2861 0, 2862 // AAGUID. 2863 0, 2864 0, 2865 0, 2866 0, 2867 0, 2868 0, 2869 0, 2870 0, 2871 0, 2872 0, 2873 0, 2874 0, 2875 0, 2876 0, 2877 0, 2878 0, 2879 // L. 2880 // CREDENTIAL ID length is 16 as 16-bit big endian. 2881 0, 2882 16, 2883 // CREDENTIAL ID. 2884 0, 2885 0, 2886 0, 2887 0, 2888 0, 2889 0, 2890 0, 2891 0, 2892 0, 2893 0, 2894 0, 2895 0, 2896 0, 2897 0, 2898 0, 2899 0, 2900 CBOR_MAP | 5, 2901 // COSE kty. 2902 CBOR_UINT | 1, 2903 // COSE EC2. 2904 CBOR_UINT | 2, 2905 // COSE alg. 2906 CBOR_UINT | 3, 2907 CBOR_NEG | 24, 2908 // COSE ES384. 2909 34, 2910 // COSE EC2 crv. 2911 CBOR_NEG, 2912 // COSE P-384. 2913 CBOR_UINT | 2, 2914 // COSE EC2 x. 2915 CBOR_NEG | 1, 2916 CBOR_BYTES | 24, 2917 // Length is 48. 2918 48, 2919 // X-coordinate. This will be overwritten later. 2920 0, 2921 0, 2922 0, 2923 0, 2924 0, 2925 0, 2926 0, 2927 0, 2928 0, 2929 0, 2930 0, 2931 0, 2932 0, 2933 0, 2934 0, 2935 0, 2936 0, 2937 0, 2938 0, 2939 0, 2940 0, 2941 0, 2942 0, 2943 0, 2944 0, 2945 0, 2946 0, 2947 0, 2948 0, 2949 0, 2950 0, 2951 0, 2952 0, 2953 0, 2954 0, 2955 0, 2956 0, 2957 0, 2958 0, 2959 0, 2960 0, 2961 0, 2962 0, 2963 0, 2964 0, 2965 0, 2966 0, 2967 0, 2968 // COSE EC2 y. 2969 CBOR_NEG | 2, 2970 CBOR_BYTES | 24, 2971 // Length is 48. 2972 48, 2973 // Y-coordinate. This will be overwritten later. 2974 0, 2975 0, 2976 0, 2977 0, 2978 0, 2979 0, 2980 0, 2981 0, 2982 0, 2983 0, 2984 0, 2985 0, 2986 0, 2987 0, 2988 0, 2989 0, 2990 0, 2991 0, 2992 0, 2993 0, 2994 0, 2995 0, 2996 0, 2997 0, 2998 0, 2999 0, 3000 0, 3001 0, 3002 0, 3003 0, 3004 0, 3005 0, 3006 0, 3007 0, 3008 0, 3009 0, 3010 0, 3011 0, 3012 0, 3013 0, 3014 0, 3015 0, 3016 0, 3017 0, 3018 0, 3019 0, 3020 0, 3021 0, 3022 ] 3023 .as_slice(), 3024 ); 3025 attestation_object[30..62].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes())); 3026 let p384_key = P384Key::from_bytes( 3027 &[ 3028 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232, 3029 42, 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1, 3030 32, 86, 220, 68, 182, 11, 105, 223, 75, 70, 3031 ] 3032 .into(), 3033 ) 3034 .unwrap() 3035 .verifying_key() 3036 .to_encoded_point(false); 3037 let x = p384_key.x().unwrap(); 3038 let y = p384_key.y().unwrap(); 3039 attestation_object[112..160].copy_from_slice(x); 3040 attestation_object[163..].copy_from_slice(y); 3041 assert!(matches!(opts.start_ceremony()?.0.verify( 3042 RP_ID, 3043 &Registration { 3044 response: AuthenticatorAttestation::new( 3045 client_data_json, 3046 attestation_object, 3047 AuthTransports::NONE, 3048 ), 3049 authenticator_attachment: AuthenticatorAttachment::None, 3050 client_extension_results: ClientExtensionsOutputs { 3051 cred_props: None, 3052 prf: None, 3053 }, 3054 }, 3055 &RegistrationVerificationOptions::<&str, &str>::default(), 3056 )?.static_state.credential_public_key, UncompressedPubKey::P384(k) if *k.x() == **x && *k.y() == **y)); 3057 Ok(()) 3058 } 3059 #[expect( 3060 clippy::panic_in_result_fn, 3061 clippy::unwrap_in_result, 3062 clippy::unwrap_used, 3063 reason = "OK in tests" 3064 )] 3065 #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] 3066 #[test] 3067 #[cfg(feature = "custom")] 3068 fn es384_auth() -> Result<(), AggErr> { 3069 let mut opts = DiscoverableCredentialRequestOptions::passkey(RP_ID); 3070 opts.public_key.challenge = Challenge(0); 3071 let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec(); 3072 // We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information. 3073 let mut authenticator_data = Vec::with_capacity(69); 3074 authenticator_data.extend_from_slice( 3075 [ 3076 // rpIdHash. 3077 // This will be overwritten later. 3078 0, 3079 0, 3080 0, 3081 0, 3082 0, 3083 0, 3084 0, 3085 0, 3086 0, 3087 0, 3088 0, 3089 0, 3090 0, 3091 0, 3092 0, 3093 0, 3094 0, 3095 0, 3096 0, 3097 0, 3098 0, 3099 0, 3100 0, 3101 0, 3102 0, 3103 0, 3104 0, 3105 0, 3106 0, 3107 0, 3108 0, 3109 0, 3110 // flags. 3111 // UP and UV (right-to-left). 3112 0b0000_0101, 3113 // signCount. 3114 // 0 as 32-bit big-endian. 3115 0, 3116 0, 3117 0, 3118 0, 3119 ] 3120 .as_slice(), 3121 ); 3122 authenticator_data[..32].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes())); 3123 authenticator_data.extend_from_slice(&Sha256::digest(client_data_json.as_slice())); 3124 let p384_key = P384Key::from_bytes( 3125 &[ 3126 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232, 3127 42, 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1, 3128 32, 86, 220, 68, 182, 11, 105, 223, 75, 70, 3129 ] 3130 .into(), 3131 ) 3132 .unwrap(); 3133 let der_sig: P384DerSig = p384_key.sign(authenticator_data.as_slice()); 3134 let pub_key = p384_key.verifying_key().to_encoded_point(true); 3135 authenticator_data.truncate(37); 3136 assert!(!opts.start_ceremony()?.0.verify( 3137 RP_ID, 3138 &DiscoverableAuthentication { 3139 raw_id: CredentialId::try_from(vec![0; 16])?, 3140 response: DiscoverableAuthenticatorAssertion::new( 3141 client_data_json, 3142 authenticator_data, 3143 der_sig.as_bytes().into(), 3144 UserHandle::from([0]), 3145 ), 3146 authenticator_attachment: AuthenticatorAttachment::None, 3147 }, 3148 &mut AuthenticatedCredential::new( 3149 CredentialId::try_from([0; 16].as_slice())?, 3150 &UserHandle::from([0]), 3151 StaticState { 3152 credential_public_key: CompressedPubKey::<&[u8], &[u8], _, &[u8]>::P384( 3153 CompressedP384PubKey::from(( 3154 (*pub_key.x().unwrap()).into(), 3155 pub_key.tag() == Tag::CompressedOddY 3156 )), 3157 ), 3158 extensions: AuthenticatorExtensionOutputStaticState { 3159 cred_protect: CredentialProtectionPolicy::None, 3160 hmac_secret: None, 3161 }, 3162 client_extension_results: ClientExtensionsOutputsStaticState { prf: None } 3163 }, 3164 DynamicState { 3165 user_verified: true, 3166 backup: Backup::NotEligible, 3167 sign_count: 0, 3168 authenticator_attachment: AuthenticatorAttachment::None, 3169 }, 3170 )?, 3171 &AuthenticationVerificationOptions::<&str, &str>::default(), 3172 )?); 3173 Ok(()) 3174 } 3175 #[expect( 3176 clippy::panic_in_result_fn, 3177 clippy::unwrap_in_result, 3178 clippy::unwrap_used, 3179 reason = "OK in tests" 3180 )] 3181 #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] 3182 #[expect(clippy::too_many_lines, reason = "a lot to test")] 3183 #[expect(clippy::many_single_char_names, reason = "fine")] 3184 #[test] 3185 #[cfg(feature = "custom")] 3186 fn rs256_reg() -> Result<(), AggErr> { 3187 let id = UserHandle::from([0]); 3188 let mut opts = CredentialCreationOptions::passkey( 3189 RP_ID, 3190 PublicKeyCredentialUserEntity { 3191 name: "foo".try_into()?, 3192 id: &id, 3193 display_name: DisplayName::Blank, 3194 }, 3195 Vec::new(), 3196 ); 3197 opts.public_key.challenge = Challenge(0); 3198 let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec(); 3199 // We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information. 3200 let mut attestation_object = Vec::with_capacity(406); 3201 attestation_object.extend_from_slice( 3202 [ 3203 CBOR_MAP | 3, 3204 CBOR_TEXT | 3, 3205 b'f', 3206 b'm', 3207 b't', 3208 CBOR_TEXT | 4, 3209 b'n', 3210 b'o', 3211 b'n', 3212 b'e', 3213 CBOR_TEXT | 7, 3214 b'a', 3215 b't', 3216 b't', 3217 b'S', 3218 b't', 3219 b'm', 3220 b't', 3221 CBOR_MAP, 3222 CBOR_TEXT | 8, 3223 b'a', 3224 b'u', 3225 b't', 3226 b'h', 3227 b'D', 3228 b'a', 3229 b't', 3230 b'a', 3231 CBOR_BYTES | 25, 3232 // Length is 343 as 16-bit big-endian. 3233 1, 3234 87, 3235 // RP ID HASH. 3236 // This will be overwritten later. 3237 0, 3238 0, 3239 0, 3240 0, 3241 0, 3242 0, 3243 0, 3244 0, 3245 0, 3246 0, 3247 0, 3248 0, 3249 0, 3250 0, 3251 0, 3252 0, 3253 0, 3254 0, 3255 0, 3256 0, 3257 0, 3258 0, 3259 0, 3260 0, 3261 0, 3262 0, 3263 0, 3264 0, 3265 0, 3266 0, 3267 0, 3268 0, 3269 // FLAGS. 3270 // UP, UV, and AT (right-to-left). 3271 0b0100_0101, 3272 // COUNTER. 3273 // 0 as 32-bit big-endian. 3274 0, 3275 0, 3276 0, 3277 0, 3278 // AAGUID. 3279 0, 3280 0, 3281 0, 3282 0, 3283 0, 3284 0, 3285 0, 3286 0, 3287 0, 3288 0, 3289 0, 3290 0, 3291 0, 3292 0, 3293 0, 3294 0, 3295 // L. 3296 // CREDENTIAL ID length is 16 as 16-bit big endian. 3297 0, 3298 16, 3299 // CREDENTIAL ID. 3300 0, 3301 0, 3302 0, 3303 0, 3304 0, 3305 0, 3306 0, 3307 0, 3308 0, 3309 0, 3310 0, 3311 0, 3312 0, 3313 0, 3314 0, 3315 0, 3316 CBOR_MAP | 4, 3317 // COSE kty. 3318 CBOR_UINT | 1, 3319 // COSE RSA. 3320 CBOR_UINT | 3, 3321 // COSE alg. 3322 CBOR_UINT | 3, 3323 CBOR_NEG | 25, 3324 // COSE RS256. 3325 1, 3326 0, 3327 // COSE n. 3328 CBOR_NEG, 3329 CBOR_BYTES | 25, 3330 // Length is 256 as 16-bit big-endian. 3331 1, 3332 0, 3333 // N. This will be overwritten later. 3334 0, 3335 0, 3336 0, 3337 0, 3338 0, 3339 0, 3340 0, 3341 0, 3342 0, 3343 0, 3344 0, 3345 0, 3346 0, 3347 0, 3348 0, 3349 0, 3350 0, 3351 0, 3352 0, 3353 0, 3354 0, 3355 0, 3356 0, 3357 0, 3358 0, 3359 0, 3360 0, 3361 0, 3362 0, 3363 0, 3364 0, 3365 0, 3366 0, 3367 0, 3368 0, 3369 0, 3370 0, 3371 0, 3372 0, 3373 0, 3374 0, 3375 0, 3376 0, 3377 0, 3378 0, 3379 0, 3380 0, 3381 0, 3382 0, 3383 0, 3384 0, 3385 0, 3386 0, 3387 0, 3388 0, 3389 0, 3390 0, 3391 0, 3392 0, 3393 0, 3394 0, 3395 0, 3396 0, 3397 0, 3398 0, 3399 0, 3400 0, 3401 0, 3402 0, 3403 0, 3404 0, 3405 0, 3406 0, 3407 0, 3408 0, 3409 0, 3410 0, 3411 0, 3412 0, 3413 0, 3414 0, 3415 0, 3416 0, 3417 0, 3418 0, 3419 0, 3420 0, 3421 0, 3422 0, 3423 0, 3424 0, 3425 0, 3426 0, 3427 0, 3428 0, 3429 0, 3430 0, 3431 0, 3432 0, 3433 0, 3434 0, 3435 0, 3436 0, 3437 0, 3438 0, 3439 0, 3440 0, 3441 0, 3442 0, 3443 0, 3444 0, 3445 0, 3446 0, 3447 0, 3448 0, 3449 0, 3450 0, 3451 0, 3452 0, 3453 0, 3454 0, 3455 0, 3456 0, 3457 0, 3458 0, 3459 0, 3460 0, 3461 0, 3462 0, 3463 0, 3464 0, 3465 0, 3466 0, 3467 0, 3468 0, 3469 0, 3470 0, 3471 0, 3472 0, 3473 0, 3474 0, 3475 0, 3476 0, 3477 0, 3478 0, 3479 0, 3480 0, 3481 0, 3482 0, 3483 0, 3484 0, 3485 0, 3486 0, 3487 0, 3488 0, 3489 0, 3490 0, 3491 0, 3492 0, 3493 0, 3494 0, 3495 0, 3496 0, 3497 0, 3498 0, 3499 0, 3500 0, 3501 0, 3502 0, 3503 0, 3504 0, 3505 0, 3506 0, 3507 0, 3508 0, 3509 0, 3510 0, 3511 0, 3512 0, 3513 0, 3514 0, 3515 0, 3516 0, 3517 0, 3518 0, 3519 0, 3520 0, 3521 0, 3522 0, 3523 0, 3524 0, 3525 0, 3526 0, 3527 0, 3528 0, 3529 0, 3530 0, 3531 0, 3532 0, 3533 0, 3534 0, 3535 0, 3536 0, 3537 0, 3538 0, 3539 0, 3540 0, 3541 0, 3542 0, 3543 0, 3544 0, 3545 0, 3546 0, 3547 0, 3548 0, 3549 0, 3550 0, 3551 0, 3552 0, 3553 0, 3554 0, 3555 0, 3556 0, 3557 0, 3558 0, 3559 0, 3560 0, 3561 0, 3562 0, 3563 0, 3564 0, 3565 0, 3566 0, 3567 0, 3568 0, 3569 0, 3570 0, 3571 0, 3572 0, 3573 0, 3574 0, 3575 0, 3576 0, 3577 0, 3578 0, 3579 0, 3580 0, 3581 0, 3582 0, 3583 0, 3584 0, 3585 0, 3586 0, 3587 0, 3588 0, 3589 0, 3590 // COSE e. 3591 CBOR_NEG | 1, 3592 CBOR_BYTES | 3, 3593 // 65537 as 24-bit big-endian. 3594 1, 3595 0, 3596 1, 3597 ] 3598 .as_slice(), 3599 ); 3600 attestation_object[31..63].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes())); 3601 let n = [ 3602 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107, 3603 195, 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206, 3604 185, 19, 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165, 3605 100, 128, 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204, 3606 145, 50, 174, 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64, 3607 87, 145, 45, 32, 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105, 3608 81, 217, 151, 162, 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55, 3609 117, 248, 233, 151, 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21, 3610 254, 81, 202, 64, 232, 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157, 3611 108, 139, 253, 230, 80, 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188, 3612 42, 198, 212, 23, 182, 222, 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56, 3613 104, 68, 94, 198, 196, 35, 200, 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13, 3614 72, 93, 53, 65, 111, 59, 242, 122, 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132, 3615 153, 79, 0, 133, 78, 7, 218, 165, 241, 3616 ]; 3617 let e = 0x0001_0001; 3618 let d = [ 3619 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0, 3620 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168, 3621 35, 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108, 3622 154, 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102, 3623 6, 219, 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247, 3624 82, 104, 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166, 3625 216, 152, 94, 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111, 3626 215, 107, 212, 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242, 3627 140, 252, 248, 162, 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212, 3628 249, 237, 203, 202, 48, 26, 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83, 3629 31, 253, 55, 43, 158, 90, 248, 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153, 3630 162, 60, 91, 99, 220, 51, 44, 85, 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142, 3631 24, 245, 127, 122, 247, 152, 212, 75, 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45, 3632 50, 23, 3, 25, 253, 87, 227, 79, 119, 161, 3633 ]; 3634 let p = BigUint::from_bytes_le( 3635 [ 3636 215, 166, 5, 21, 11, 179, 41, 77, 198, 92, 165, 48, 77, 162, 42, 41, 206, 141, 60, 3637 69, 47, 164, 19, 92, 46, 72, 100, 238, 100, 53, 214, 197, 163, 185, 6, 140, 229, 3638 250, 195, 77, 8, 12, 5, 236, 178, 173, 86, 201, 43, 213, 165, 51, 108, 101, 161, 3639 99, 76, 240, 14, 234, 76, 197, 137, 53, 198, 168, 135, 205, 212, 198, 120, 29, 16, 3640 82, 98, 233, 236, 177, 12, 171, 141, 100, 107, 146, 33, 176, 125, 202, 172, 79, 3641 147, 179, 30, 62, 247, 206, 169, 19, 168, 114, 26, 73, 108, 178, 105, 84, 89, 191, 3642 168, 253, 228, 214, 54, 16, 212, 199, 111, 72, 3, 41, 247, 227, 165, 244, 32, 188, 3643 24, 247, 3644 ] 3645 .as_slice(), 3646 ); 3647 let p_2 = BigUint::from_bytes_le( 3648 [ 3649 41, 25, 198, 240, 134, 206, 121, 57, 11, 5, 134, 192, 212, 77, 229, 197, 14, 78, 3650 85, 212, 190, 114, 179, 188, 21, 171, 174, 12, 104, 74, 15, 164, 136, 173, 62, 177, 3651 141, 213, 93, 102, 147, 83, 59, 124, 146, 59, 175, 213, 55, 27, 25, 248, 154, 29, 3652 39, 85, 50, 235, 134, 60, 203, 106, 186, 195, 190, 185, 71, 169, 142, 236, 92, 11, 3653 250, 187, 198, 8, 201, 184, 120, 178, 227, 87, 63, 243, 89, 227, 234, 184, 28, 252, 3654 112, 211, 193, 69, 23, 92, 5, 72, 93, 53, 69, 159, 73, 160, 105, 244, 249, 94, 214, 3655 173, 9, 236, 4, 255, 129, 11, 224, 140, 252, 168, 57, 143, 176, 241, 60, 219, 90, 3656 250, 3657 ] 3658 .as_slice(), 3659 ); 3660 let rsa_key = RsaKey::<Sha256>::new( 3661 RsaPrivateKey::from_components( 3662 BigUint::from_bytes_le(n.as_slice()), 3663 e.into(), 3664 BigUint::from_bytes_le(d.as_slice()), 3665 vec![p, p_2], 3666 ) 3667 .unwrap(), 3668 ) 3669 .verifying_key(); 3670 let n_other = rsa_key.as_ref().n().to_bytes_be(); 3671 attestation_object[113..369].copy_from_slice(n_other.as_slice()); 3672 assert!(matches!(opts.start_ceremony()?.0.verify( 3673 RP_ID, 3674 &Registration { 3675 response: AuthenticatorAttestation::new( 3676 client_data_json, 3677 attestation_object, 3678 AuthTransports::NONE, 3679 ), 3680 authenticator_attachment: AuthenticatorAttachment::None, 3681 client_extension_results: ClientExtensionsOutputs { 3682 cred_props: None, 3683 prf: None, 3684 }, 3685 }, 3686 &RegistrationVerificationOptions::<&str, &str>::default(), 3687 )?.static_state.credential_public_key, UncompressedPubKey::Rsa(k) if *k.n() == n_other.as_slice() && k.e() == e)); 3688 Ok(()) 3689 } 3690 #[expect( 3691 clippy::panic_in_result_fn, 3692 clippy::unwrap_in_result, 3693 clippy::unwrap_used, 3694 reason = "OK in tests" 3695 )] 3696 #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] 3697 #[expect(clippy::too_many_lines, reason = "a lot to test")] 3698 #[test] 3699 #[cfg(feature = "custom")] 3700 fn rs256_auth() -> Result<(), AggErr> { 3701 let mut opts = DiscoverableCredentialRequestOptions::passkey(RP_ID); 3702 opts.public_key.challenge = Challenge(0); 3703 let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec(); 3704 // We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information. 3705 let mut authenticator_data = Vec::with_capacity(69); 3706 authenticator_data.extend_from_slice( 3707 [ 3708 // rpIdHash. 3709 // This will be overwritten later. 3710 0, 3711 0, 3712 0, 3713 0, 3714 0, 3715 0, 3716 0, 3717 0, 3718 0, 3719 0, 3720 0, 3721 0, 3722 0, 3723 0, 3724 0, 3725 0, 3726 0, 3727 0, 3728 0, 3729 0, 3730 0, 3731 0, 3732 0, 3733 0, 3734 0, 3735 0, 3736 0, 3737 0, 3738 0, 3739 0, 3740 0, 3741 0, 3742 // flags. 3743 // UP and UV (right-to-left). 3744 0b0000_0101, 3745 // signCount. 3746 // 0 as 32-bit big-endian. 3747 0, 3748 0, 3749 0, 3750 0, 3751 ] 3752 .as_slice(), 3753 ); 3754 authenticator_data[..32].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes())); 3755 authenticator_data.extend_from_slice(&Sha256::digest(client_data_json.as_slice())); 3756 let n = [ 3757 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107, 3758 195, 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206, 3759 185, 19, 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165, 3760 100, 128, 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204, 3761 145, 50, 174, 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64, 3762 87, 145, 45, 32, 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105, 3763 81, 217, 151, 162, 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55, 3764 117, 248, 233, 151, 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21, 3765 254, 81, 202, 64, 232, 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157, 3766 108, 139, 253, 230, 80, 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188, 3767 42, 198, 212, 23, 182, 222, 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56, 3768 104, 68, 94, 198, 196, 35, 200, 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13, 3769 72, 93, 53, 65, 111, 59, 242, 122, 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132, 3770 153, 79, 0, 133, 78, 7, 218, 165, 241, 3771 ]; 3772 let e = 0x0001_0001; 3773 let d = [ 3774 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0, 3775 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168, 3776 35, 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108, 3777 154, 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102, 3778 6, 219, 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247, 3779 82, 104, 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166, 3780 216, 152, 94, 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111, 3781 215, 107, 212, 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242, 3782 140, 252, 248, 162, 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212, 3783 249, 237, 203, 202, 48, 26, 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83, 3784 31, 253, 55, 43, 158, 90, 248, 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153, 3785 162, 60, 91, 99, 220, 51, 44, 85, 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142, 3786 24, 245, 127, 122, 247, 152, 212, 75, 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45, 3787 50, 23, 3, 25, 253, 87, 227, 79, 119, 161, 3788 ]; 3789 let p = BigUint::from_bytes_le( 3790 [ 3791 215, 166, 5, 21, 11, 179, 41, 77, 198, 92, 165, 48, 77, 162, 42, 41, 206, 141, 60, 3792 69, 47, 164, 19, 92, 46, 72, 100, 238, 100, 53, 214, 197, 163, 185, 6, 140, 229, 3793 250, 195, 77, 8, 12, 5, 236, 178, 173, 86, 201, 43, 213, 165, 51, 108, 101, 161, 3794 99, 76, 240, 14, 234, 76, 197, 137, 53, 198, 168, 135, 205, 212, 198, 120, 29, 16, 3795 82, 98, 233, 236, 177, 12, 171, 141, 100, 107, 146, 33, 176, 125, 202, 172, 79, 3796 147, 179, 30, 62, 247, 206, 169, 19, 168, 114, 26, 73, 108, 178, 105, 84, 89, 191, 3797 168, 253, 228, 214, 54, 16, 212, 199, 111, 72, 3, 41, 247, 227, 165, 244, 32, 188, 3798 24, 247, 3799 ] 3800 .as_slice(), 3801 ); 3802 let p_2 = BigUint::from_bytes_le( 3803 [ 3804 41, 25, 198, 240, 134, 206, 121, 57, 11, 5, 134, 192, 212, 77, 229, 197, 14, 78, 3805 85, 212, 190, 114, 179, 188, 21, 171, 174, 12, 104, 74, 15, 164, 136, 173, 62, 177, 3806 141, 213, 93, 102, 147, 83, 59, 124, 146, 59, 175, 213, 55, 27, 25, 248, 154, 29, 3807 39, 85, 50, 235, 134, 60, 203, 106, 186, 195, 190, 185, 71, 169, 142, 236, 92, 11, 3808 250, 187, 198, 8, 201, 184, 120, 178, 227, 87, 63, 243, 89, 227, 234, 184, 28, 252, 3809 112, 211, 193, 69, 23, 92, 5, 72, 93, 53, 69, 159, 73, 160, 105, 244, 249, 94, 214, 3810 173, 9, 236, 4, 255, 129, 11, 224, 140, 252, 168, 57, 143, 176, 241, 60, 219, 90, 3811 250, 3812 ] 3813 .as_slice(), 3814 ); 3815 let rsa_key = RsaKey::<Sha256>::new( 3816 RsaPrivateKey::from_components( 3817 BigUint::from_bytes_le(n.as_slice()), 3818 e.into(), 3819 BigUint::from_bytes_le(d.as_slice()), 3820 vec![p, p_2], 3821 ) 3822 .unwrap(), 3823 ); 3824 let rsa_pub = rsa_key.verifying_key(); 3825 let sig = rsa_key.sign(authenticator_data.as_slice()).to_vec(); 3826 authenticator_data.truncate(37); 3827 assert!(!opts.start_ceremony()?.0.verify( 3828 RP_ID, 3829 &DiscoverableAuthentication { 3830 raw_id: CredentialId::try_from(vec![0; 16])?, 3831 response: DiscoverableAuthenticatorAssertion::new( 3832 client_data_json, 3833 authenticator_data, 3834 sig, 3835 UserHandle::from([0]), 3836 ), 3837 authenticator_attachment: AuthenticatorAttachment::None, 3838 }, 3839 &mut AuthenticatedCredential::new( 3840 CredentialId::try_from([0; 16].as_slice())?, 3841 &UserHandle::from([0]), 3842 StaticState { 3843 credential_public_key: CompressedPubKey::<&[u8], &[u8], &[u8], _>::Rsa( 3844 RsaPubKey::try_from((rsa_pub.as_ref().n().to_bytes_be(), e)).unwrap(), 3845 ), 3846 extensions: AuthenticatorExtensionOutputStaticState { 3847 cred_protect: CredentialProtectionPolicy::None, 3848 hmac_secret: None, 3849 }, 3850 client_extension_results: ClientExtensionsOutputsStaticState { prf: None } 3851 }, 3852 DynamicState { 3853 user_verified: true, 3854 backup: Backup::NotEligible, 3855 sign_count: 0, 3856 authenticator_attachment: AuthenticatorAttachment::None, 3857 }, 3858 )?, 3859 &AuthenticationVerificationOptions::<&str, &str>::default(), 3860 )?); 3861 Ok(()) 3862 } 3863 }