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