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