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