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