auth.rs (94356B)
1 #[cfg(doc)] 2 use super::{ 3 super::response::{ 4 Backup, CollectedClientData, Flag, 5 auth::AuthenticatorData, 6 register::{ 7 AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputsStaticState, 8 DynamicState, StaticState, 9 }, 10 }, 11 AsciiDomain, AsciiDomainStatic, DomainOrigin, Url, 12 register::{self, PublicKeyCredentialCreationOptions}, 13 }; 14 use super::{ 15 super::{ 16 AuthenticatedCredential, 17 response::{ 18 AuthenticatorAttachment, 19 auth::{ 20 Authentication, AuthenticatorExtensionOutput, DiscoverableAuthentication, 21 HmacSecret, NonDiscoverableAuthentication, 22 error::{AuthCeremonyErr, ExtensionErr, OneOrTwo}, 23 }, 24 register::{CompressedPubKey, CredentialProtectionPolicy}, 25 }, 26 }, 27 BackupReq, Ceremony, CeremonyOptions, Challenge, CredentialId, CredentialMediationRequirement, 28 Credentials, ExtensionReq, Hint, Origin, PrfInput, PublicKeyCredentialDescriptor, RpId, 29 SentChallenge, THREE_HUNDRED_THOUSAND, TimedCeremony, UserVerificationRequirement, 30 auth::error::{InvalidTimeout, SecondFactorErr}, 31 }; 32 use core::{ 33 borrow::Borrow, 34 cmp::Ordering, 35 hash::{Hash, Hasher}, 36 num::{NonZeroU32, NonZeroU64}, 37 time::Duration, 38 }; 39 #[cfg(any(doc, not(feature = "serializable_server_state")))] 40 use std::time::Instant; 41 #[cfg(any(doc, feature = "serializable_server_state"))] 42 use std::time::SystemTime; 43 /// Contains error types. 44 pub mod error; 45 /// Contains functionality to serialize data to a client. 46 #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] 47 #[cfg(feature = "serde")] 48 mod ser; 49 /// Contains functionality to (de)serialize [`DiscoverableAuthenticationServerState`] and 50 /// [`NonDiscoverableAuthenticationServerState`] to a data store. 51 #[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))] 52 #[cfg(feature = "serializable_server_state")] 53 pub mod ser_server_state; 54 /// Controls how [signature counter](https://www.w3.org/TR/webauthn-3/#signature-counter) is enforced. 55 /// 56 /// Note that if the previous signature counter is positive and the new counter is not strictly greater, then the 57 /// authenticator is likely a clone (i.e., there are at least two copies of the private key). 58 #[derive(Clone, Copy, Debug, Default)] 59 pub enum SignatureCounterEnforcement { 60 /// Fail the authentication ceremony if the counter is less than or equal to the previous value when the 61 /// previous value is positive. 62 #[default] 63 Fail, 64 /// When the counter is less than the previous value, don't fail and update the value. 65 /// 66 /// Note in the special case that the new signature counter is 0, [`DynamicState::sign_count`] _won't_ 67 /// be updated since that would allow an attacker to permanently disable the counter. 68 Update, 69 /// When the counter is less than the previous value, don't fail but don't update the value. 70 Ignore, 71 } 72 impl SignatureCounterEnforcement { 73 /// Validates the signature counter based on `self`. 74 const fn validate(self, prev: u32, cur: u32) -> Result<u32, AuthCeremonyErr> { 75 if prev == 0 || cur > prev { 76 Ok(cur) 77 } else { 78 match self { 79 Self::Fail => Err(AuthCeremonyErr::SignatureCounter), 80 // When the new counter is `0`, we use the previous counter to avoid an attacker from 81 // being able to permanently disable it. 82 Self::Update => Ok(if cur == 0 { prev } else { cur }), 83 Self::Ignore => Ok(prev), 84 } 85 } 86 } 87 } 88 /// Owned version of [`PrfInput`]. 89 /// 90 /// When relying on [`NonDiscoverableCredentialRequestOptions`], it's recommended to use credential-specific PRF 91 /// inputs that are continuously rolled over. One uses this type for such a thing. 92 #[derive(Clone, Debug)] 93 pub struct PrfInputOwned { 94 /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first). 95 pub first: Vec<u8>, 96 /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second). 97 pub second: Option<Vec<u8>>, 98 /// Note this is only applicable for authenticators that implement the 99 /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) extension on top of the 100 /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-extension) 101 /// extension since the data is encrypted and is part of the [`AuthenticatorData`]. 102 pub ext_req: ExtensionReq, 103 } 104 /// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client. 105 #[derive(Clone, Copy, Debug, Default)] 106 pub struct Extension<'prf_first, 'prf_second> { 107 /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension). 108 /// 109 /// If both [`CredentialSpecificExtension::prf`] and this are [`Some`], then `CredentialSpecificExtension::prf` 110 /// takes priority. 111 /// 112 /// Note `ExtensionReq` is only applicable for authenticators that implement the 113 /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) extension on top of the 114 /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-extension) 115 /// extension since the data is encrypted and is part of the [`AuthenticatorData`]. 116 pub prf: Option<(PrfInput<'prf_first, 'prf_second>, ExtensionReq)>, 117 } 118 /// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client that 119 /// are credential-specific which among other things implies a non-discoverable request. 120 #[derive(Clone, Debug, Default)] 121 pub struct CredentialSpecificExtension { 122 /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension). 123 /// 124 /// If both [`Extension::prf`] and this are [`Some`], then this take priority. 125 pub prf: Option<PrfInputOwned>, 126 } 127 /// Registered credential used in 128 /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials). 129 #[derive(Clone, Debug)] 130 pub struct AllowedCredential { 131 /// The registered credential. 132 pub credential: PublicKeyCredentialDescriptor<Vec<u8>>, 133 /// Credential-specific extensions. 134 pub extension: CredentialSpecificExtension, 135 } 136 impl From<PublicKeyCredentialDescriptor<Vec<u8>>> for AllowedCredential { 137 #[inline] 138 fn from(credential: PublicKeyCredentialDescriptor<Vec<u8>>) -> Self { 139 Self { 140 credential, 141 extension: CredentialSpecificExtension::default(), 142 } 143 } 144 } 145 impl From<AllowedCredential> for PublicKeyCredentialDescriptor<Vec<u8>> { 146 #[inline] 147 fn from(credential: AllowedCredential) -> Self { 148 credential.credential 149 } 150 } 151 /// Queue of unique [`AllowedCredential`]s. 152 #[derive(Clone, Debug, Default)] 153 pub struct AllowedCredentials { 154 /// Allowed credentials. 155 creds: Vec<AllowedCredential>, 156 /// Number of `AllowedCredential`s that have PRF inputs. 157 /// 158 /// Useful to help serialization. 159 prf_count: usize, 160 } 161 impl Credentials for AllowedCredentials { 162 type Credential = AllowedCredential; 163 /// # Examples 164 /// 165 /// ``` 166 /// # use webauthn_rp::request::{auth::AllowedCredentials, Credentials}; 167 /// assert!(AllowedCredentials::with_capacity(1).as_ref().is_empty()); 168 /// ``` 169 #[inline] 170 fn with_capacity(capacity: usize) -> Self { 171 Self { 172 creds: Vec::with_capacity(capacity), 173 prf_count: 0, 174 } 175 } 176 /// # Examples 177 /// 178 /// ``` 179 /// # #[cfg(all(feature = "bin", feature = "custom"))] 180 /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr}; 181 /// # use webauthn_rp::{ 182 /// # request::{auth::AllowedCredentials, PublicKeyCredentialDescriptor, Credentials}, 183 /// # response::{AuthTransports, CredentialId}, 184 /// # }; 185 /// /// Retrieves the `AuthTransports` associated with the unique `cred_id` 186 /// /// from the database. 187 /// # #[cfg(all(feature = "bin", feature = "custom"))] 188 /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> { 189 /// // ⋮ 190 /// # AuthTransports::decode(32) 191 /// } 192 /// let mut creds = AllowedCredentials::with_capacity(1); 193 /// assert!(creds.as_ref().is_empty()); 194 /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is 195 /// // likely never needed since the `CredentialId` was originally sent from the client and is likely 196 /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`. 197 /// # #[cfg(all(feature = "bin", feature = "custom"))] 198 /// let id = CredentialId::try_from(vec![0; 16])?; 199 /// # #[cfg(all(feature = "bin", feature = "custom"))] 200 /// let transports = get_transports((&id).into())?; 201 /// # #[cfg(all(feature = "bin", feature = "custom"))] 202 /// assert!(creds.push(PublicKeyCredentialDescriptor { id, transports }.into())); 203 /// # #[cfg(all(feature = "bin", feature = "custom"))] 204 /// let id_copy = CredentialId::try_from(vec![0; 16])?; 205 /// # #[cfg(all(feature = "bin", feature = "custom"))] 206 /// let transports_2 = AuthTransports::NONE; 207 /// // Duplicate `CredentialId`s don't get added. 208 /// # #[cfg(all(feature = "bin", feature = "custom"))] 209 /// assert!(!creds.push( 210 /// PublicKeyCredentialDescriptor { 211 /// id: id_copy, 212 /// transports: transports_2 213 /// } 214 /// .into() 215 /// )); 216 /// # Ok::<_, webauthn_rp::AggErr>(()) 217 /// ``` 218 #[expect( 219 clippy::arithmetic_side_effects, 220 reason = "comment explains how overflow is not possible" 221 )] 222 #[inline] 223 fn push(&mut self, cred: Self::Credential) -> bool { 224 self.creds 225 .iter() 226 .try_fold((), |(), c| { 227 if c.credential.id == cred.credential.id { 228 Err(()) 229 } else { 230 Ok(()) 231 } 232 }) 233 .is_ok_and(|()| { 234 // This can't overflow since `self.creds.push` would `panic` since 235 // `self.prf_count <= self.creds.len()`. 236 self.prf_count += usize::from(cred.extension.prf.is_some()); 237 self.creds.push(cred); 238 true 239 }) 240 } 241 #[inline] 242 fn len(&self) -> usize { 243 self.creds.len() 244 } 245 } 246 impl AsRef<[AllowedCredential]> for AllowedCredentials { 247 #[inline] 248 fn as_ref(&self) -> &[AllowedCredential] { 249 self.creds.as_slice() 250 } 251 } 252 impl From<&AllowedCredentials> for Vec<CredInfo> { 253 #[inline] 254 fn from(value: &AllowedCredentials) -> Self { 255 let len = value.creds.len(); 256 value 257 .creds 258 .iter() 259 .fold(Self::with_capacity(len), |mut creds, cred| { 260 creds.push(CredInfo { 261 id: cred.credential.id.clone(), 262 ext: (&cred.extension).into(), 263 }); 264 creds 265 }) 266 } 267 } 268 impl From<AllowedCredentials> for Vec<PublicKeyCredentialDescriptor<Vec<u8>>> { 269 #[inline] 270 fn from(value: AllowedCredentials) -> Self { 271 let mut creds = Self::with_capacity(value.creds.len()); 272 value.creds.into_iter().fold((), |(), cred| { 273 creds.push(cred.credential); 274 }); 275 creds 276 } 277 } 278 impl From<Vec<PublicKeyCredentialDescriptor<Vec<u8>>>> for AllowedCredentials { 279 #[inline] 280 fn from(value: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>) -> Self { 281 let mut creds = Self::with_capacity(value.len()); 282 value.into_iter().fold((), |(), credential| { 283 _ = creds.push(AllowedCredential { 284 credential, 285 extension: CredentialSpecificExtension { prf: None }, 286 }); 287 }); 288 creds 289 } 290 } 291 /// The [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions) 292 /// to send to the client when authenticating a discoverable credentential. 293 /// 294 /// Upon saving the [`DiscoverableAuthenticationServerState`] returned from [`Self::start_ceremony`], one MUST send 295 /// [`DiscoverableAuthenticationClientState`] to the client ASAP. After receiving the newly created 296 /// [`DiscoverableAuthentication`], it is validated using [`DiscoverableAuthenticationServerState::verify`]. 297 #[derive(Debug)] 298 pub struct DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> { 299 /// [`mediation`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement). 300 pub mediation: CredentialMediationRequirement, 301 /// `public-key` [credential type](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry). 302 pub public_key: PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>, 303 } 304 impl<'rp_id, 'prf_first, 'prf_second> 305 DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> 306 { 307 /// Creates a `DiscoverableCredentialRequestOptions` containing [`CredentialMediationRequirement::default`] and 308 /// [`PublicKeyCredentialRequestOptions::passkey`]. 309 /// 310 /// # Examples 311 /// 312 /// ``` 313 /// # use webauthn_rp::request::{auth::DiscoverableCredentialRequestOptions, AsciiDomain, RpId, UserVerificationRequirement}; 314 /// assert!(matches!( 315 /// DiscoverableCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).public_key.user_verification, 316 /// UserVerificationRequirement::Required 317 /// )); 318 /// # Ok::<_, webauthn_rp::AggErr>(()) 319 /// ``` 320 #[expect(single_use_lifetimes, reason = "false positive")] 321 #[inline] 322 #[must_use] 323 pub fn passkey<'a: 'rp_id>(rp_id: &'a RpId) -> Self { 324 Self { 325 mediation: CredentialMediationRequirement::default(), 326 public_key: PublicKeyCredentialRequestOptions::passkey(rp_id), 327 } 328 } 329 /// Begins the [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony) consuming 330 /// `self`. Note that the expiration [`Instant`]/[`SystemTime`] is saved, so 331 /// `DiscoverableAuthenticationClientState` MUST be sent ASAP. In order to complete authentication, the returned 332 /// `DiscoverableAuthenticationServerState` MUST be saved so that it can later be used to verify the credential 333 /// assertion with [`DiscoverableAuthenticationServerState::verify`]. 334 /// 335 /// # Errors 336 /// 337 /// Errors iff `self` contains incompatible configuration. 338 #[inline] 339 pub fn start_ceremony( 340 self, 341 ) -> Result< 342 ( 343 DiscoverableAuthenticationServerState, 344 DiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>, 345 ), 346 InvalidTimeout, 347 > { 348 #[cfg(not(feature = "serializable_server_state"))] 349 let res = Instant::now(); 350 #[cfg(feature = "serializable_server_state")] 351 let res = SystemTime::now(); 352 res.checked_add(Duration::from_millis( 353 NonZeroU64::from(self.public_key.timeout).get(), 354 )) 355 .ok_or(InvalidTimeout) 356 .map(|expiration| { 357 ( 358 DiscoverableAuthenticationServerState(AuthenticationServerState { 359 challenge: SentChallenge(self.public_key.challenge.0), 360 user_verification: self.public_key.user_verification, 361 extensions: self.public_key.extensions.into(), 362 expiration, 363 }), 364 DiscoverableAuthenticationClientState(self), 365 ) 366 }) 367 } 368 /// Same as [`Self::start_ceremony`] except the raw challenge is returned instead of 369 /// [`DiscoverableAuthenticationClientState`]. 370 /// 371 /// Note this is useful when one configures the authentication ceremony client-side and only needs the 372 /// server-generated challenge. It's of course essential that `self` is configured exactly the same as 373 /// how it is configured client-side. See 374 /// [`challengeURL`](https://github.com/w3c/webauthn/wiki/Explainer:-WebAuthn-challengeURL) for more 375 /// information. 376 /// 377 /// # Errors 378 /// 379 /// Read [`Self::start_ceremony`]. 380 #[inline] 381 pub fn start_ceremony_challenge_only( 382 self, 383 ) -> Result<(DiscoverableAuthenticationServerState, [u8; 16]), InvalidTimeout> { 384 self.start_ceremony() 385 .map(|(server, client)| (server, client.0.public_key.challenge.into_array())) 386 } 387 } 388 /// The [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions) 389 /// to send to the client when authenticating non-discoverable credententials. 390 /// 391 /// Upon saving the [`NonDiscoverableAuthenticationServerState`] returned from [`Self::start_ceremony`], one MUST send 392 /// [`NonDiscoverableAuthenticationClientState`] to the client ASAP. After receiving the newly created 393 /// [`NonDiscoverableAuthentication`], it is validated using [`NonDiscoverableAuthenticationServerState::verify`]. 394 #[derive(Debug)] 395 pub struct NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> { 396 /// [`mediation`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement). 397 mediation: CredentialMediationRequirement, 398 /// [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions). 399 options: PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>, 400 /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials). 401 allow_credentials: AllowedCredentials, 402 } 403 impl<'rp_id, 'prf_first, 'prf_second> 404 NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> 405 { 406 /// Returns a mutable reference to the `CredentialMediationRequirement`. 407 #[inline] 408 pub const fn mediation(&mut self) -> &mut CredentialMediationRequirement { 409 &mut self.mediation 410 } 411 /// Returns a mutable reference to the configurable options. 412 #[inline] 413 pub const fn options( 414 &mut self, 415 ) -> &mut PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> { 416 &mut self.options 417 } 418 /// Returns a reference to the [`AllowedCredential`]s. 419 #[inline] 420 #[must_use] 421 pub const fn allow_credentials(&self) -> &AllowedCredentials { 422 &self.allow_credentials 423 } 424 /// Creates a `NonDiscoverableCredentialRequestOptions` containing 425 /// [`CredentialMediationRequirement::Optional`], 426 /// [`PublicKeyCredentialRequestOptions::second_factor`], and the passed [`AllowedCredentials`]. 427 /// 428 /// # Errors 429 /// 430 /// Errors iff `allow_credentials` is empty. 431 /// 432 /// # Examples 433 /// 434 /// ``` 435 /// # #[cfg(all(feature = "bin", feature = "custom"))] 436 /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr}; 437 /// # use webauthn_rp::{ 438 /// # request::{ 439 /// # auth::{AllowedCredentials, NonDiscoverableCredentialRequestOptions}, 440 /// # AsciiDomain, RpId, PublicKeyCredentialDescriptor, Credentials 441 /// # }, 442 /// # response::{AuthTransports, CredentialId}, 443 /// # }; 444 /// /// Retrieves the `AuthTransports` associated with the unique `cred_id` 445 /// /// from the database. 446 /// # #[cfg(all(feature = "bin", feature = "custom"))] 447 /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> { 448 /// // ⋮ 449 /// # AuthTransports::decode(32) 450 /// } 451 /// let mut creds = AllowedCredentials::with_capacity(1); 452 /// assert!(creds.as_ref().is_empty()); 453 /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is 454 /// // likely never needed since the `CredentialId` was originally sent from the client and is likely 455 /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`. 456 /// # #[cfg(all(feature = "bin", feature = "custom"))] 457 /// let id = CredentialId::try_from(vec![0; 16])?; 458 /// # #[cfg(all(feature = "bin", feature = "custom"))] 459 /// let transports = get_transports((&id).into())?; 460 /// # #[cfg(all(feature = "bin", feature = "custom"))] 461 /// assert!(creds.push(PublicKeyCredentialDescriptor { id, transports }.into())); 462 /// # #[cfg(all(feature = "bin", feature = "custom"))] 463 /// assert_eq!( 464 /// NonDiscoverableCredentialRequestOptions::second_factor(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?), creds)? 465 /// .allow_credentials() 466 /// .len(), 467 /// 1 468 /// ); 469 /// # Ok::<_, webauthn_rp::AggErr>(()) 470 /// ``` 471 #[expect(single_use_lifetimes, reason = "false positive")] 472 #[inline] 473 pub fn second_factor<'a: 'rp_id>( 474 rp_id: &'a RpId, 475 allow_credentials: AllowedCredentials, 476 ) -> Result<Self, SecondFactorErr> { 477 if allow_credentials.is_empty() { 478 Err(SecondFactorErr) 479 } else { 480 Ok(Self { 481 mediation: CredentialMediationRequirement::default(), 482 options: PublicKeyCredentialRequestOptions::second_factor(rp_id), 483 allow_credentials, 484 }) 485 } 486 } 487 /// Begins the [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony) consuming 488 /// `self`. Note that the expiration [`Instant`]/[`SystemTime`] is saved, so `NonDiscoverableAuthenticationClientState` 489 /// MUST be sent ASAP. In order to complete authentication, the returned `NonDiscoverableAuthenticationServerState` 490 /// MUST be saved so that it can later be used to verify the credential assertion with 491 /// [`NonDiscoverableAuthenticationServerState::verify`]. 492 /// 493 /// # Errors 494 /// 495 /// Errors iff `self` contains incompatible configuration. 496 #[inline] 497 pub fn start_ceremony( 498 self, 499 ) -> Result< 500 ( 501 NonDiscoverableAuthenticationServerState, 502 NonDiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>, 503 ), 504 InvalidTimeout, 505 > { 506 #[cfg(not(feature = "serializable_server_state"))] 507 let res = Instant::now(); 508 #[cfg(feature = "serializable_server_state")] 509 let res = SystemTime::now(); 510 res.checked_add(Duration::from_millis( 511 NonZeroU64::from(self.options.timeout).get(), 512 )) 513 .ok_or(InvalidTimeout) 514 .map(|expiration| { 515 ( 516 NonDiscoverableAuthenticationServerState { 517 state: AuthenticationServerState { 518 challenge: SentChallenge(self.options.challenge.0), 519 user_verification: self.options.user_verification, 520 extensions: self.options.extensions.into(), 521 expiration, 522 }, 523 allow_credentials: Vec::from(&self.allow_credentials), 524 }, 525 NonDiscoverableAuthenticationClientState(self), 526 ) 527 }) 528 } 529 } 530 /// The [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions) 531 /// to send to the client when authenticating a credential. 532 /// 533 /// This does _not_ contain [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials). 534 #[derive(Debug)] 535 pub struct PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> { 536 /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge). 537 pub challenge: Challenge, 538 /// [`timeout`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-timeout). 539 /// 540 /// Note we require a positive value despite the spec allowing an optional nonnegative value. This jives 541 /// with the fact that in-memory storage is required when `serializable_server_state` is not enabled 542 /// when authenticating credentials as no timeout would make out-of-memory (OOM) conditions more likely. 543 pub timeout: NonZeroU32, 544 /// [`rpId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-rpid). 545 /// 546 /// This MUST be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] used when the credential was registered. 547 pub rp_id: &'rp_id RpId, 548 /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification). 549 pub user_verification: UserVerificationRequirement, 550 /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-hints). 551 pub hints: Hint, 552 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-extensions). 553 pub extensions: Extension<'prf_first, 'prf_second>, 554 } 555 impl<'rp_id> PublicKeyCredentialRequestOptions<'rp_id, '_, '_> { 556 /// Creates a `PublicKeyCredentialRequestOptions` with [`Self::user_verification`] set to 557 /// [`UserVerificationRequirement::Required`] and [`Self::timeout`] set to 5 minutes, 558 /// 559 /// Note `rp_id` _must_ be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] when the 560 /// credential was registered. 561 /// 562 /// # Examples 563 /// 564 /// ``` 565 /// # use webauthn_rp::request::{auth::PublicKeyCredentialRequestOptions, AsciiDomain, RpId, UserVerificationRequirement}; 566 /// assert!(matches!( 567 /// PublicKeyCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).user_verification, 568 /// UserVerificationRequirement::Required 569 /// )); 570 /// # Ok::<_, webauthn_rp::AggErr>(()) 571 /// ``` 572 #[expect(single_use_lifetimes, reason = "false positive")] 573 #[inline] 574 #[must_use] 575 pub fn passkey<'a: 'rp_id>(rp_id: &'a RpId) -> Self { 576 Self { 577 challenge: Challenge::new(), 578 timeout: THREE_HUNDRED_THOUSAND, 579 rp_id, 580 user_verification: UserVerificationRequirement::Required, 581 hints: Hint::None, 582 extensions: Extension::default(), 583 } 584 } 585 /// Creates a `PublicKeyCredentialRequestOptions` with [`Self::user_verification`] set to 586 /// [`UserVerificationRequirement::Discouraged`] and [`Self::timeout`] set to 5 minutes. 587 /// 588 /// Note `rp_id` _must_ be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] when the 589 /// credentials were registered. 590 /// 591 /// # Examples 592 /// 593 /// ``` 594 /// # use webauthn_rp::request::{auth::PublicKeyCredentialRequestOptions, AsciiDomain, RpId, UserVerificationRequirement}; 595 /// assert!(matches!( 596 /// PublicKeyCredentialRequestOptions::second_factor(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).user_verification, 597 /// UserVerificationRequirement::Discouraged 598 /// )); 599 /// # Ok::<_, webauthn_rp::AggErr>(()) 600 /// ``` 601 #[expect(single_use_lifetimes, reason = "false positive")] 602 #[inline] 603 #[must_use] 604 pub fn second_factor<'a: 'rp_id>(rp_id: &'a RpId) -> Self { 605 let mut opts = Self::passkey(rp_id); 606 opts.user_verification = UserVerificationRequirement::Discouraged; 607 opts 608 } 609 } 610 /// Container of a [`DiscoverableCredentialRequestOptions`] that has been used to start the authentication ceremony. 611 /// This gets sent to the client ASAP. 612 #[derive(Debug)] 613 pub struct DiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>( 614 DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>, 615 ); 616 impl<'rp_id, 'prf_first, 'prf_second> 617 DiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second> 618 { 619 /// Returns the `DiscoverableCredentialRequestOptions` that was used to start an authentication ceremony. 620 #[inline] 621 #[must_use] 622 pub const fn options( 623 &self, 624 ) -> &DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> { 625 &self.0 626 } 627 } 628 /// Container of a [`NonDiscoverableCredentialRequestOptions`] that has been used to start the authentication 629 /// ceremony. This gets sent to the client ASAP. 630 #[derive(Debug)] 631 pub struct NonDiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>( 632 NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>, 633 ); 634 impl<'rp_id, 'prf_first, 'prf_second> 635 NonDiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second> 636 { 637 /// Returns the `NonDiscoverableCredentialRequestOptions` that was used to start an authentication ceremony. 638 #[inline] 639 #[must_use] 640 pub const fn options( 641 &self, 642 ) -> &NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> { 643 &self.0 644 } 645 } 646 /// The possible combinations of an [`AuthenticatedCredential`]'s [`StaticState`]'s 647 /// `extensions.hmac_secret` and `client_extension_results.prf`. 648 /// 649 /// Note we ensure in `crate::verify_static_and_dynamic_state` that `hmac_secret` does not exist when 650 /// `prf` does not exist, `hmac_secret` does not exist or is `false` when `prf` is `false`, or 651 /// `hmac_secret` does not exist or is `true` when `prf` is `true`. 652 #[derive(Clone, Copy)] 653 enum CredPrf { 654 /// No `prf` or `hmac_secret`. 655 None, 656 /// `prf.enabled` is `false` but there is no `hmac_secret`. 657 FalseNoHmac, 658 /// `prf.enabled` and `hmac_secret` are `false`. 659 FalseFalseHmac, 660 /// `prf.enabled` is `true` but there is no `hmac_secret`. 661 TrueNoHmac, 662 /// `prf.enabled` and `hmac_secret` are `true`. 663 TrueTrueHmac, 664 } 665 impl CredPrf { 666 /// Returns `true` iff `self` is allowed to have an `HmacSecret` response. 667 /// 668 /// Note many authenticators allow PRF to be used during authentication even when not requested during 669 /// registration even for authenticators (e.g., CTAP-based ones) that implement PRF on top of the `hmac-secret` 670 /// extension; thus we allow `Self::None` and `Self::TrueNoHmac`. 671 const fn is_prf_capable(self) -> bool { 672 matches!(self, Self::None | Self::TrueNoHmac | Self::TrueTrueHmac) 673 } 674 } 675 /// `PrfInput` and `PrfInputOwned` without the actual data sent to reduce memory usage when storing 676 /// [`DiscoverableAuthenticationServerState`] in an in-memory collection. 677 #[derive(Clone, Copy, Debug)] 678 enum ServerPrfInfo { 679 /// No `PrfInput`. 680 None, 681 /// `PrfInput::second` was `None`. 682 One(ExtensionReq), 683 /// `PrfInput::second` was `Some`. 684 Two(ExtensionReq), 685 } 686 impl ServerPrfInfo { 687 /// Validates `val` based on the passed arguments. 688 /// 689 /// It's not possible to request the PRF extension without sending `UserVerificationRequirement::Required`; 690 /// thus `user_verified` will always be `true` when sending PRF; otherwise ceremony validation will error. 691 /// However when we _don't_ send the PRF extension _and_ we don't error on an unsolicited response, it's 692 /// possible to receive an `HmacSecret` without the user having been verified; thus we only ensure 693 /// `user_verified` is true when we don't error on unsolicted responses _and_ we didn't send the PRF extension. 694 const fn validate( 695 self, 696 user_verified: bool, 697 cred_prf: CredPrf, 698 hmac: HmacSecret, 699 err_unsolicited: bool, 700 ) -> Result<(), ExtensionErr> { 701 match hmac { 702 HmacSecret::None => match self { 703 Self::None => Ok(()), 704 Self::One(req) | Self::Two(req) => { 705 if matches!(req, ExtensionReq::Allow) { 706 if cred_prf.is_prf_capable() { 707 Ok(()) 708 } else { 709 Err(ExtensionErr::PrfRequestedForPrfIncapableCred) 710 } 711 } else { 712 match cred_prf { 713 CredPrf::None | CredPrf::TrueNoHmac => Ok(()), 714 CredPrf::FalseNoHmac | CredPrf::FalseFalseHmac => { 715 Err(ExtensionErr::PrfRequestedForPrfIncapableCred) 716 } 717 CredPrf::TrueTrueHmac => Err(ExtensionErr::MissingHmacSecret), 718 } 719 } 720 } 721 }, 722 HmacSecret::One => match self { 723 Self::None => { 724 if err_unsolicited { 725 Err(ExtensionErr::ForbiddenHmacSecret) 726 } else if cred_prf.is_prf_capable() { 727 if user_verified { 728 Ok(()) 729 } else { 730 Err(ExtensionErr::UserNotVerifiedHmacSecret) 731 } 732 } else { 733 Err(ExtensionErr::PrfRequestedForPrfIncapableCred) 734 } 735 } 736 Self::One(_) => { 737 if cred_prf.is_prf_capable() { 738 Ok(()) 739 } else { 740 Err(ExtensionErr::PrfRequestedForPrfIncapableCred) 741 } 742 } 743 Self::Two(_) => Err(ExtensionErr::InvalidHmacSecretValue( 744 OneOrTwo::Two, 745 OneOrTwo::One, 746 )), 747 }, 748 HmacSecret::Two => match self { 749 Self::None => { 750 if err_unsolicited { 751 Err(ExtensionErr::ForbiddenHmacSecret) 752 } else if cred_prf.is_prf_capable() { 753 if user_verified { 754 Ok(()) 755 } else { 756 Err(ExtensionErr::UserNotVerifiedHmacSecret) 757 } 758 } else { 759 Err(ExtensionErr::PrfRequestedForPrfIncapableCred) 760 } 761 } 762 Self::One(_) => Err(ExtensionErr::InvalidHmacSecretValue( 763 OneOrTwo::One, 764 OneOrTwo::Two, 765 )), 766 Self::Two(_) => { 767 if cred_prf.is_prf_capable() { 768 Ok(()) 769 } else { 770 Err(ExtensionErr::PrfRequestedForPrfIncapableCred) 771 } 772 } 773 }, 774 } 775 } 776 } 777 #[cfg(test)] 778 impl PartialEq for ServerPrfInfo { 779 fn eq(&self, other: &Self) -> bool { 780 match *self { 781 Self::None => matches!(*other, Self::None), 782 Self::One(req) => matches!(*other, Self::One(req2) if req == req2), 783 Self::Two(req) => matches!(*other, Self::Two(req2) if req == req2), 784 } 785 } 786 } 787 impl From<Option<(PrfInput<'_, '_>, ExtensionReq)>> for ServerPrfInfo { 788 fn from(value: Option<(PrfInput<'_, '_>, ExtensionReq)>) -> Self { 789 value.map_or(Self::None, |val| { 790 val.0 791 .second 792 .map_or_else(|| Self::One(val.1), |_| Self::Two(val.1)) 793 }) 794 } 795 } 796 impl From<&PrfInputOwned> for ServerPrfInfo { 797 fn from(value: &PrfInputOwned) -> Self { 798 value 799 .second 800 .as_ref() 801 .map_or_else(|| Self::One(value.ext_req), |_| Self::Two(value.ext_req)) 802 } 803 } 804 /// `Extension` without the actual data sent to reduce memory usage when storing [`AuthenticationServerState`] 805 /// in an in-memory collection. 806 #[derive(Clone, Copy, Debug)] 807 struct ServerExtensionInfo { 808 /// `Extension::prf`. 809 prf: ServerPrfInfo, 810 } 811 impl From<Extension<'_, '_>> for ServerExtensionInfo { 812 fn from(value: Extension<'_, '_>) -> Self { 813 Self { 814 prf: value.prf.into(), 815 } 816 } 817 } 818 #[cfg(test)] 819 impl PartialEq for ServerExtensionInfo { 820 fn eq(&self, other: &Self) -> bool { 821 self.prf == other.prf 822 } 823 } 824 /// `CredentialSpecificExtension` without the actual data sent to reduce memory usage when storing [`AuthenticationServerState`] 825 /// in an in-memory collection. 826 #[derive(Clone, Copy, Debug)] 827 struct ServerCredSpecificExtensionInfo { 828 /// `CredentialSpecificExtension::prf`. 829 prf: ServerPrfInfo, 830 } 831 #[cfg(test)] 832 impl PartialEq for ServerCredSpecificExtensionInfo { 833 fn eq(&self, other: &Self) -> bool { 834 self.prf == other.prf 835 } 836 } 837 impl From<&CredentialSpecificExtension> for ServerCredSpecificExtensionInfo { 838 fn from(value: &CredentialSpecificExtension) -> Self { 839 Self { 840 prf: value 841 .prf 842 .as_ref() 843 .map_or(ServerPrfInfo::None, ServerPrfInfo::from), 844 } 845 } 846 } 847 impl ServerExtensionInfo { 848 /// Validates the extensions. 849 /// 850 /// Note that this MUST only be called internally by `auth::validate_extensions`. 851 const fn validate_extensions( 852 self, 853 user_verified: bool, 854 auth_ext: AuthenticatorExtensionOutput, 855 error_unsolicited: bool, 856 cred_prf: CredPrf, 857 ) -> Result<(), ExtensionErr> { 858 ServerPrfInfo::validate( 859 self.prf, 860 user_verified, 861 cred_prf, 862 auth_ext.hmac_secret, 863 error_unsolicited, 864 ) 865 } 866 } 867 /// Validates the extensions. 868 fn validate_extensions( 869 ext: ServerExtensionInfo, 870 user_verified: bool, 871 cred_ext: Option<ServerCredSpecificExtensionInfo>, 872 auth_ext: AuthenticatorExtensionOutput, 873 error_unsolicited: bool, 874 cred_prf: CredPrf, 875 ) -> Result<(), ExtensionErr> { 876 cred_ext.map_or_else( 877 || { 878 // No credental-specific extensions, so we can simply focus on `ext`. 879 ext.validate_extensions(user_verified, auth_ext, error_unsolicited, cred_prf) 880 }, 881 |c_ext| { 882 // Must carefully process each extension based on overlap and which gets priority over the other. 883 if matches!(c_ext.prf, ServerPrfInfo::None) { 884 ext.prf.validate( 885 user_verified, 886 cred_prf, 887 auth_ext.hmac_secret, 888 error_unsolicited, 889 ) 890 } else { 891 c_ext.prf.validate( 892 user_verified, 893 cred_prf, 894 auth_ext.hmac_secret, 895 error_unsolicited, 896 ) 897 } 898 }, 899 ) 900 } 901 /// [`AllowedCredential`] with less data to reduce memory usage when storing [`AuthenticationServerState`] 902 /// in an in-memory collection. 903 #[derive(Debug)] 904 struct CredInfo { 905 /// The Credential ID. 906 id: CredentialId<Vec<u8>>, 907 /// Any credential-specific extensions. 908 ext: ServerCredSpecificExtensionInfo, 909 } 910 #[cfg(test)] 911 impl PartialEq for CredInfo { 912 fn eq(&self, other: &Self) -> bool { 913 self.id == other.id && self.ext == other.ext 914 } 915 } 916 /// Controls how to handle a change in [`DynamicState::authenticator_attachment`]. 917 /// 918 /// Note when `DynamicState::authenticator_attachment` is [`AuthenticatorAttachment::None`], then it will 919 /// be updated regardless. Similarly when [`Authentication::authenticator_attachment`] is 920 /// `AuthenticatorAttachment::None`, it will never update `DynamicState::authenticator_attachment`. 921 #[derive(Clone, Copy, Debug)] 922 pub enum AuthenticatorAttachmentEnforcement { 923 /// Fail the authentication ceremony if [`AuthenticatorAttachment`] is not the same. 924 /// 925 /// The contained `bool` represents if `AuthenticatorAttachment` must be sent. 926 Fail(bool), 927 /// Update [`DynamicState::authenticator_attachment`] to the sent [`AuthenticatorAttachment`]. 928 /// 929 /// The contained `bool` represents if `AuthenticatorAttachment` must be sent. 930 Update(bool), 931 /// Do not update [`DynamicState::authenticator_attachment`] to the sent [`AuthenticatorAttachment`]. 932 /// 933 /// The contained `bool` represents if `AuthenticatorAttachment` must be sent. 934 Ignore(bool), 935 } 936 impl AuthenticatorAttachmentEnforcement { 937 /// Validates `cur` based on `self` and `prev`. 938 const fn validate( 939 self, 940 prev: AuthenticatorAttachment, 941 cur: AuthenticatorAttachment, 942 ) -> Result<AuthenticatorAttachment, AuthCeremonyErr> { 943 match cur { 944 AuthenticatorAttachment::None => match self { 945 Self::Fail(require) | Self::Update(require) | Self::Ignore(require) => { 946 if require { 947 Err(AuthCeremonyErr::MissingAuthenticatorAttachment) 948 } else { 949 // We don't overwrite the previous one with [`AuthenticatorAttachment::None`]. 950 Ok(prev) 951 } 952 } 953 }, 954 AuthenticatorAttachment::Platform => match self { 955 Self::Fail(_) => { 956 if matches!(prev, AuthenticatorAttachment::CrossPlatform) { 957 Err(AuthCeremonyErr::AuthenticatorAttachmentMismatch) 958 } else { 959 // We don't fail when we previously had [`AuthenticatorAttachment::None`]. 960 Ok(cur) 961 } 962 } 963 Self::Update(_) => Ok(cur), 964 Self::Ignore(_) => { 965 if matches!(prev, AuthenticatorAttachment::None) { 966 // We overwrite the previous one when it is [`AuthenticatorAttachment::None`]. 967 Ok(cur) 968 } else { 969 Ok(prev) 970 } 971 } 972 }, 973 AuthenticatorAttachment::CrossPlatform => match self { 974 Self::Fail(_) => { 975 if matches!(prev, AuthenticatorAttachment::Platform) { 976 Err(AuthCeremonyErr::AuthenticatorAttachmentMismatch) 977 } else { 978 // We don't fail when we previously had [`AuthenticatorAttachment::None`]. 979 Ok(cur) 980 } 981 } 982 Self::Update(_) => Ok(cur), 983 Self::Ignore(_) => { 984 if matches!(prev, AuthenticatorAttachment::None) { 985 // We overwrite the previous one when it is [`AuthenticatorAttachment::None`]. 986 Ok(cur) 987 } else { 988 Ok(prev) 989 } 990 } 991 }, 992 } 993 } 994 } 995 impl Default for AuthenticatorAttachmentEnforcement { 996 /// Returns [`Self::Ignore`] containing `false`. 997 #[inline] 998 fn default() -> Self { 999 Self::Ignore(false) 1000 } 1001 } 1002 /// Additional verification options to perform in [`DiscoverableAuthenticationServerState::verify`] and 1003 /// [`NonDiscoverableAuthenticationServerState::verify`]. 1004 #[derive(Clone, Copy, Debug)] 1005 pub struct AuthenticationVerificationOptions<'origins, 'top_origins, O, T> { 1006 /// Origins to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin). 1007 /// 1008 /// When this is empty, the origin that will be used will be based on the [`RpId`] passed to 1009 /// [`DiscoverableAuthenticationServerState::verify`] or [`NonDiscoverableAuthenticationServerState::verify`]. 1010 /// If [`RpId::Domain`] or [`RpId::StaticDomain`], then the [`DomainOrigin`] returned from passing 1011 /// [`AsciiDomain::as_ref`] and [`AsciiDomainStatic::as_str`] to [`DomainOrigin::new`] respectively will be 1012 /// used; otherwise the [`Url`] in [`RpId::Url`] will be used. 1013 pub allowed_origins: &'origins [O], 1014 /// [Top-level origins](https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-top-level-origin) 1015 /// to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin). 1016 /// 1017 /// When this is `Some`, [`CollectedClientData::cross_origin`] is allowed to be `true`. When the contained 1018 /// `slice` is empty, [`CollectedClientData::top_origin`] must be `None`. When this is `None`, 1019 /// `CollectedClientData::cross_origin` must be `false` and `CollectedClientData::top_origin` must be `None`. 1020 pub allowed_top_origins: Option<&'top_origins [T]>, 1021 /// The required [`Backup`] state of the credential. 1022 /// 1023 /// Note that `None` is _not_ the same as `Some(BackupReq::None)` as the latter indicates that any [`Backup`] 1024 /// is allowed. This is rarely what you want; instead, `None` indicates that [`BackupReq::from`] applied to 1025 /// [`DynamicState::backup`] will be used in [`DiscoverableAuthenticationServerState::verify`] and 1026 /// [`NonDiscoverableAuthenticationServerState::verify`] and 1027 pub backup_requirement: Option<BackupReq>, 1028 /// Error when unsolicited extensions are sent back iff `true`. 1029 pub error_on_unsolicited_extensions: bool, 1030 /// Dictates what happens when [`Authentication::authenticator_attachment`] is not the same as 1031 /// [`DynamicState::authenticator_attachment`]. 1032 pub auth_attachment_enforcement: AuthenticatorAttachmentEnforcement, 1033 /// [`DynamicState::user_verified`] will be set to `true` iff [`Flag::user_verified`] when `true`. 1034 pub update_uv: bool, 1035 /// Dictates what happens when [`AuthenticatorData::sign_count`] is not updated to a strictly greater value. 1036 pub sig_counter_enforcement: SignatureCounterEnforcement, 1037 /// [`CollectedClientData::from_client_data_json_relaxed`] is used to extract [`CollectedClientData`] iff `true`. 1038 #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] 1039 #[cfg(feature = "serde_relaxed")] 1040 pub client_data_json_relaxed: bool, 1041 } 1042 impl<O, T> AuthenticationVerificationOptions<'_, '_, O, T> { 1043 /// Returns `Self` such that [`Self::allowed_origins`] is empty, [`Self::allowed_top_origins`] is `None`, 1044 /// [`Self::backup_requirement`] is `None`, [`Self::error_on_unsolicited_extensions`] is `true`, 1045 /// [`Self::auth_attachment_enforcement`] is [`AuthenticatorAttachmentEnforcement::default`], 1046 /// [`Self::update_uv`] is `false`, [`Self::sig_counter_enforcement`] is 1047 /// [`SignatureCounterEnforcement::default`], and [`Self::client_data_json_relaxed`] is `true`. 1048 /// 1049 /// Note `O` and `T` should implement `PartialEq<Origin<'_>>` (e.g., `&str`). 1050 #[inline] 1051 #[must_use] 1052 pub const fn new() -> Self { 1053 Self { 1054 allowed_origins: [].as_slice(), 1055 allowed_top_origins: None, 1056 backup_requirement: None, 1057 error_on_unsolicited_extensions: true, 1058 auth_attachment_enforcement: AuthenticatorAttachmentEnforcement::Ignore(false), 1059 update_uv: false, 1060 sig_counter_enforcement: SignatureCounterEnforcement::Fail, 1061 #[cfg(feature = "serde_relaxed")] 1062 client_data_json_relaxed: true, 1063 } 1064 } 1065 } 1066 impl<O, T> Default for AuthenticationVerificationOptions<'_, '_, O, T> { 1067 /// Same as [`Self::new`]. 1068 #[inline] 1069 fn default() -> Self { 1070 Self::new() 1071 } 1072 } 1073 // This is essentially the `DiscoverableCredentialRequestOptions` used to create it; however to reduce 1074 // memory usage, we remove all unnecessary data making an instance of this 48 bytes in size 1075 // `x86_64-unknown-linux-gnu` platforms. 1076 // 1077 /// State needed to be saved when beginning the authentication ceremony. 1078 /// 1079 /// Saves the necessary information associated with the [`DiscoverableCredentialRequestOptions`] used to create it 1080 /// via [`DiscoverableCredentialRequestOptions::start_ceremony`] so that authentication of a credential can be 1081 /// performed with [`Self::verify`]. 1082 /// 1083 /// `DiscoverableAuthenticationServerState` implements [`Borrow`] of [`SentChallenge`]; thus to obtain the correct 1084 /// `DiscoverableAuthenticationServerState` associated with a [`DiscoverableAuthentication`], one should use its 1085 /// corresponding [`DiscoverableAuthentication::challenge`]. 1086 #[derive(Debug)] 1087 pub struct DiscoverableAuthenticationServerState(AuthenticationServerState); 1088 impl DiscoverableAuthenticationServerState { 1089 /// Verifies `response` is valid based on `self` consuming `self` and updating `cred`. Returns `true` 1090 /// iff `cred` was mutated. 1091 /// 1092 /// `rp_id` MUST be the same as the [`PublicKeyCredentialRequestOptions::rp_id`] used when starting the 1093 /// ceremony. 1094 /// 1095 /// It is _essential_ to save [`AuthenticatedCredential::dynamic_state`] overwriting the original value iff `Ok(true)` 1096 /// is returned. 1097 /// 1098 /// # Errors 1099 /// 1100 /// Errors iff `response` is not valid according to the 1101 /// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) or violates any 1102 /// of the settings in `options`. 1103 #[inline] 1104 pub fn verify< 1105 'a, 1106 const USER_LEN: usize, 1107 O: PartialEq<Origin<'a>>, 1108 T: PartialEq<Origin<'a>>, 1109 EdKey: AsRef<[u8]>, 1110 P256Key: AsRef<[u8]>, 1111 P384Key: AsRef<[u8]>, 1112 RsaKey: AsRef<[u8]>, 1113 >( 1114 self, 1115 rp_id: &RpId, 1116 response: &'a DiscoverableAuthentication<USER_LEN>, 1117 cred: &mut AuthenticatedCredential< 1118 '_, 1119 '_, 1120 USER_LEN, 1121 CompressedPubKey<EdKey, P256Key, P384Key, RsaKey>, 1122 >, 1123 options: &AuthenticationVerificationOptions<'_, '_, O, T>, 1124 ) -> Result<bool, AuthCeremonyErr> { 1125 // Step 6 item 2. 1126 if cred.user_id == response.response.user_handle() { 1127 // Step 6 item 2. 1128 if cred.id == response.raw_id { 1129 self.0.verify(true, rp_id, response, cred, options, None) 1130 } else { 1131 Err(AuthCeremonyErr::CredentialIdMismatch) 1132 } 1133 } else { 1134 Err(AuthCeremonyErr::UserHandleMismatch) 1135 } 1136 } 1137 #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))] 1138 fn is_eq(&self, other: &Self) -> bool { 1139 self.0.is_eq(&other.0) 1140 } 1141 } 1142 // This is essentially the `NonDiscoverableCredentialRequestOptions` used to create it; however to reduce 1143 // memory usage, we remove all unnecessary data making an instance of this as small as 80 bytes in size on 1144 // `x86_64-unknown-linux-gnu` platforms. This does not include the size of each `CredInfo` which should exist 1145 // elsewhere on the heap but obviously contributes memory overall. 1146 /// State needed to be saved when beginning the authentication ceremony. 1147 /// 1148 /// Saves the necessary information associated with the [`NonDiscoverableCredentialRequestOptions`] used to create 1149 /// it via [`NonDiscoverableCredentialRequestOptions::start_ceremony`] so that authentication of a credential can be 1150 /// performed with [`Self::verify`]. 1151 /// 1152 /// `NonDiscoverableAuthenticationServerState` implements [`Borrow`] of [`SentChallenge`]; thus to obtain the 1153 /// correct `NonDiscoverableAuthenticationServerState` associated with a [`NonDiscoverableAuthentication`], one 1154 /// should use its corresponding [`NonDiscoverableAuthentication::challenge`]. 1155 #[derive(Debug)] 1156 pub struct NonDiscoverableAuthenticationServerState { 1157 /// Most server state. 1158 state: AuthenticationServerState, 1159 /// The set of credentials that are allowed. 1160 allow_credentials: Vec<CredInfo>, 1161 } 1162 impl NonDiscoverableAuthenticationServerState { 1163 /// Verifies `response` is valid based on `self` consuming `self` and updating `cred`. Returns `true` 1164 /// iff `cred` was mutated. 1165 /// 1166 /// `rp_id` MUST be the same as the [`PublicKeyCredentialRequestOptions::rp_id`] used when starting the 1167 /// ceremony. 1168 /// 1169 /// It is _essential_ to save [`AuthenticatedCredential::dynamic_state`] overwriting the original value iff `Ok(true)` 1170 /// is returned. 1171 /// 1172 /// # Errors 1173 /// 1174 /// Errors iff `response` is not valid according to the 1175 /// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) or violates any 1176 /// of the settings in `options`. 1177 #[inline] 1178 pub fn verify< 1179 'a, 1180 const USER_LEN: usize, 1181 O: PartialEq<Origin<'a>>, 1182 T: PartialEq<Origin<'a>>, 1183 EdKey: AsRef<[u8]>, 1184 P256Key: AsRef<[u8]>, 1185 P384Key: AsRef<[u8]>, 1186 RsaKey: AsRef<[u8]>, 1187 >( 1188 self, 1189 rp_id: &RpId, 1190 response: &'a NonDiscoverableAuthentication<USER_LEN>, 1191 cred: &mut AuthenticatedCredential< 1192 '_, 1193 '_, 1194 USER_LEN, 1195 CompressedPubKey<EdKey, P256Key, P384Key, RsaKey>, 1196 >, 1197 options: &AuthenticationVerificationOptions<'_, '_, O, T>, 1198 ) -> Result<bool, AuthCeremonyErr> { 1199 response 1200 .response 1201 .user_handle() 1202 .as_ref() 1203 .map_or(Ok(()), |user| { 1204 // Step 6 item 1. 1205 if *user == cred.user_id() { 1206 Ok(()) 1207 } else { 1208 Err(AuthCeremonyErr::UserHandleMismatch) 1209 } 1210 }) 1211 .and_then(|()| { 1212 self.allow_credentials 1213 .iter() 1214 // Step 5. 1215 .find(|c| c.id == response.raw_id) 1216 .ok_or(AuthCeremonyErr::NoMatchingAllowedCredential) 1217 .and_then(|c| { 1218 // Step 6 item 1. 1219 if c.id == cred.id { 1220 self.state 1221 .verify(false, rp_id, response, cred, options, Some(c.ext)) 1222 } else { 1223 Err(AuthCeremonyErr::CredentialIdMismatch) 1224 } 1225 }) 1226 }) 1227 } 1228 #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))] 1229 fn is_eq(&self, other: &Self) -> bool { 1230 self.state.is_eq(&other.state) && self.allow_credentials == other.allow_credentials 1231 } 1232 } 1233 // This is essentially the `PublicKeyCredentialRequestOptions` used to create it; however to reduce 1234 // memory usage, we remove all unnecessary data making an instance of this 48 bytes in size on 1235 // `x86_64-unknown-linux-gnu` platforms. 1236 /// Shared state used by [`DiscoverableAuthenticationServerState`] and [`NonDiscoverableAuthenticationServerState`]. 1237 #[derive(Debug)] 1238 struct AuthenticationServerState { 1239 // This is a `SentChallenge` since we need `AuthenticationServerState` to be fetchable after receiving the 1240 // response from the client. This response must obviously be constructable; thus its challenge is a 1241 // `SentChallenge`. 1242 // 1243 // This must never be mutated since we want to ensure it is actually a `Challenge` (which 1244 // can only be constructed via `Challenge::new`). This is guaranteed to be true iff 1245 // `serializable_server_state` is not enabled. 1246 /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge). 1247 challenge: SentChallenge, 1248 /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification). 1249 user_verification: UserVerificationRequirement, 1250 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-extensions). 1251 extensions: ServerExtensionInfo, 1252 /// `Instant` the ceremony expires. 1253 #[cfg(not(feature = "serializable_server_state"))] 1254 expiration: Instant, 1255 /// `SystemTime` the ceremony expires. 1256 #[cfg(feature = "serializable_server_state")] 1257 expiration: SystemTime, 1258 } 1259 impl AuthenticationServerState { 1260 /// Verifies `response` is valid based on `self` consuming `self` and updating `cred`. Returns `true` 1261 /// iff `cred` was mutated. 1262 /// 1263 /// `rp_id` MUST be the same as the [`PublicKeyCredentialRequestOptions::rp_id`] used when starting the 1264 /// ceremony. 1265 /// 1266 /// It is _essential_ to save [`AuthenticatedCredential::dynamic_state`] overwriting the original value iff `Ok(true)` 1267 /// is returned. 1268 /// 1269 /// # Errors 1270 /// 1271 /// Errors iff `response` is not valid according to the 1272 /// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) or violates any 1273 /// of the settings in `options`. 1274 fn verify< 1275 'a, 1276 const USER_LEN: usize, 1277 const DISCOVERABLE: bool, 1278 O: PartialEq<Origin<'a>>, 1279 T: PartialEq<Origin<'a>>, 1280 EdKey: AsRef<[u8]>, 1281 P256Key: AsRef<[u8]>, 1282 P384Key: AsRef<[u8]>, 1283 RsaKey: AsRef<[u8]>, 1284 >( 1285 self, 1286 discoverable: bool, 1287 rp_id: &RpId, 1288 response: &'a Authentication<USER_LEN, DISCOVERABLE>, 1289 cred: &mut AuthenticatedCredential< 1290 '_, 1291 '_, 1292 USER_LEN, 1293 CompressedPubKey<EdKey, P256Key, P384Key, RsaKey>, 1294 >, 1295 options: &AuthenticationVerificationOptions<'_, '_, O, T>, 1296 cred_ext: Option<ServerCredSpecificExtensionInfo>, 1297 ) -> Result<bool, AuthCeremonyErr> { 1298 // [Authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) 1299 // is handled by: 1300 // 1301 // 1. Calling code. 1302 // 2. Client code and the construction of `resp` (hopefully via [`Authentication::deserialize`]). 1303 // 3. Client code and the construction of `resp` (hopefully via [`AuthenticatorAssertion::deserialize`]). 1304 // 4. Client code and the construction of `resp` (hopefully via [`ClientExtensionsOutputs::deserialize`]). 1305 // 5. [`NonDiscoverableAuthenticationServerState::verify`]. 1306 // 6. [`DiscoverableAuthenticationServerState::verify`] and [`NonDiscoverableAuthenticationServerState::verify`]. 1307 // 7. Informative only in that it defines variables. 1308 // 8. [`Self::partial_validate`]. 1309 // 9. [`Self::partial_validate`]. 1310 // 10. [`Self::partial_validate`]. 1311 // 11. [`Self::partial_validate`]. 1312 // 12. [`Self::partial_validate`]. 1313 // 13. [`Self::partial_validate`]. 1314 // 14. [`Self::partial_validate`]. 1315 // 15. [`Self::partial_validate`]. 1316 // 16. [`Self::partial_validate`]. 1317 // 17. [`Self::partial_validate`]. 1318 // 18. [`Self::partial_validate`]. 1319 // 19. [`Self::partial_validate`]. 1320 // 20. [`Self::partial_validate`]. 1321 // 21. [`Self::partial_validate`]. 1322 // 22. Below. 1323 // 23. Below. 1324 // 24. Below. 1325 // 25. Below. 1326 1327 // Steps 8–21. 1328 self.partial_validate( 1329 rp_id, 1330 response, 1331 (&cred.static_state.credential_public_key).into(), 1332 &CeremonyOptions { 1333 allowed_origins: options.allowed_origins, 1334 allowed_top_origins: options.allowed_top_origins, 1335 backup_requirement: options 1336 .backup_requirement 1337 .unwrap_or_else(|| BackupReq::from(cred.dynamic_state.backup)), 1338 #[cfg(feature = "serde_relaxed")] 1339 client_data_json_relaxed: options.client_data_json_relaxed, 1340 }, 1341 ) 1342 .map_err(AuthCeremonyErr::from) 1343 .and_then(|auth_data| { 1344 options 1345 .auth_attachment_enforcement 1346 .validate( 1347 cred.dynamic_state.authenticator_attachment, 1348 response.authenticator_attachment, 1349 ) 1350 .and_then(|auth_attachment| { 1351 let flags = auth_data.flags(); 1352 // Step 23. 1353 validate_extensions( 1354 self.extensions, 1355 flags.user_verified, 1356 cred_ext, 1357 auth_data.extensions(), 1358 options.error_on_unsolicited_extensions, 1359 cred.static_state.client_extension_results.prf.map_or( 1360 CredPrf::None, 1361 |prf| { 1362 if prf.enabled { 1363 cred.static_state 1364 .extensions 1365 .hmac_secret 1366 .map_or(CredPrf::TrueNoHmac, |_| CredPrf::TrueTrueHmac) 1367 } else { 1368 cred.static_state 1369 .extensions 1370 .hmac_secret 1371 .map_or(CredPrf::FalseNoHmac, |_| CredPrf::FalseFalseHmac) 1372 } 1373 }, 1374 ), 1375 ) 1376 .map_err(AuthCeremonyErr::Extension) 1377 .and_then(|()| { 1378 // Step 22. 1379 options 1380 .sig_counter_enforcement 1381 .validate(cred.dynamic_state.sign_count, auth_data.sign_count()) 1382 .and_then(|sig_counter| { 1383 let prev_dyn_state = cred.dynamic_state; 1384 // Step 24 item 2. 1385 cred.dynamic_state.backup = flags.backup; 1386 if options.update_uv && flags.user_verified { 1387 // Step 24 item 3. 1388 cred.dynamic_state.user_verified = true; 1389 } 1390 // Step 24 item 1. 1391 cred.dynamic_state.sign_count = sig_counter; 1392 cred.dynamic_state.authenticator_attachment = auth_attachment; 1393 // Step 25. 1394 if flags.user_verified { 1395 Ok(()) 1396 } else { 1397 match cred.static_state.extensions.cred_protect { 1398 CredentialProtectionPolicy::None | CredentialProtectionPolicy::UserVerificationOptional => Ok(()), 1399 CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList => { 1400 if discoverable { 1401 Err(AuthCeremonyErr::DiscoverableCredProtectCredentialIdList) 1402 } else { 1403 Ok(()) 1404 } 1405 } 1406 CredentialProtectionPolicy::UserVerificationRequired => { 1407 Err(AuthCeremonyErr::UserNotVerifiedCredProtectRequired) 1408 } 1409 } 1410 }.inspect_err(|_| { 1411 cred.dynamic_state = prev_dyn_state; 1412 }).map(|()| { 1413 prev_dyn_state != cred.dynamic_state 1414 }) 1415 }) 1416 }) 1417 }) 1418 }) 1419 } 1420 #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))] 1421 fn is_eq(&self, other: &Self) -> bool { 1422 self.challenge == other.challenge 1423 && self.user_verification == other.user_verification 1424 && self.extensions == other.extensions 1425 && self.expiration == other.expiration 1426 } 1427 } 1428 impl TimedCeremony for AuthenticationServerState { 1429 #[cfg(any(doc, not(feature = "serializable_server_state")))] 1430 #[inline] 1431 fn expiration(&self) -> Instant { 1432 self.expiration 1433 } 1434 #[cfg(all(not(doc), feature = "serializable_server_state"))] 1435 #[inline] 1436 fn expiration(&self) -> SystemTime { 1437 self.expiration 1438 } 1439 } 1440 impl TimedCeremony for DiscoverableAuthenticationServerState { 1441 #[cfg(any(doc, not(feature = "serializable_server_state")))] 1442 #[inline] 1443 fn expiration(&self) -> Instant { 1444 self.0.expiration() 1445 } 1446 #[cfg(all(not(doc), feature = "serializable_server_state"))] 1447 #[inline] 1448 fn expiration(&self) -> SystemTime { 1449 self.0.expiration() 1450 } 1451 } 1452 impl TimedCeremony for NonDiscoverableAuthenticationServerState { 1453 #[cfg(any(doc, not(feature = "serializable_server_state")))] 1454 #[inline] 1455 fn expiration(&self) -> Instant { 1456 self.state.expiration() 1457 } 1458 #[cfg(all(not(doc), feature = "serializable_server_state"))] 1459 #[inline] 1460 fn expiration(&self) -> SystemTime { 1461 self.state.expiration() 1462 } 1463 } 1464 impl<const USER_LEN: usize, const DISCOVERABLE: bool> Ceremony<USER_LEN, DISCOVERABLE> 1465 for AuthenticationServerState 1466 { 1467 type R = Authentication<USER_LEN, DISCOVERABLE>; 1468 fn rand_challenge(&self) -> SentChallenge { 1469 self.challenge 1470 } 1471 #[cfg(not(feature = "serializable_server_state"))] 1472 fn expiry(&self) -> Instant { 1473 self.expiration 1474 } 1475 #[cfg(feature = "serializable_server_state")] 1476 fn expiry(&self) -> SystemTime { 1477 self.expiration 1478 } 1479 fn user_verification(&self) -> UserVerificationRequirement { 1480 self.user_verification 1481 } 1482 } 1483 impl Borrow<SentChallenge> for AuthenticationServerState { 1484 #[inline] 1485 fn borrow(&self) -> &SentChallenge { 1486 &self.challenge 1487 } 1488 } 1489 impl PartialEq for AuthenticationServerState { 1490 #[inline] 1491 fn eq(&self, other: &Self) -> bool { 1492 self.challenge == other.challenge 1493 } 1494 } 1495 impl PartialEq<&Self> for AuthenticationServerState { 1496 #[inline] 1497 fn eq(&self, other: &&Self) -> bool { 1498 *self == **other 1499 } 1500 } 1501 impl PartialEq<AuthenticationServerState> for &AuthenticationServerState { 1502 #[inline] 1503 fn eq(&self, other: &AuthenticationServerState) -> bool { 1504 **self == *other 1505 } 1506 } 1507 impl Eq for AuthenticationServerState {} 1508 impl Hash for AuthenticationServerState { 1509 #[inline] 1510 fn hash<H: Hasher>(&self, state: &mut H) { 1511 self.challenge.hash(state); 1512 } 1513 } 1514 impl PartialOrd for AuthenticationServerState { 1515 #[inline] 1516 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { 1517 Some(self.cmp(other)) 1518 } 1519 } 1520 impl Ord for AuthenticationServerState { 1521 #[inline] 1522 fn cmp(&self, other: &Self) -> Ordering { 1523 self.challenge.cmp(&other.challenge) 1524 } 1525 } 1526 impl Borrow<SentChallenge> for DiscoverableAuthenticationServerState { 1527 #[inline] 1528 fn borrow(&self) -> &SentChallenge { 1529 self.0.borrow() 1530 } 1531 } 1532 impl PartialEq for DiscoverableAuthenticationServerState { 1533 #[inline] 1534 fn eq(&self, other: &Self) -> bool { 1535 self.0 == other.0 1536 } 1537 } 1538 impl PartialEq<&Self> for DiscoverableAuthenticationServerState { 1539 #[inline] 1540 fn eq(&self, other: &&Self) -> bool { 1541 self.0 == other.0 1542 } 1543 } 1544 impl PartialEq<DiscoverableAuthenticationServerState> for &DiscoverableAuthenticationServerState { 1545 #[inline] 1546 fn eq(&self, other: &DiscoverableAuthenticationServerState) -> bool { 1547 self.0 == other.0 1548 } 1549 } 1550 impl Eq for DiscoverableAuthenticationServerState {} 1551 impl Hash for DiscoverableAuthenticationServerState { 1552 #[inline] 1553 fn hash<H: Hasher>(&self, state: &mut H) { 1554 self.0.hash(state); 1555 } 1556 } 1557 impl PartialOrd for DiscoverableAuthenticationServerState { 1558 #[inline] 1559 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { 1560 Some(self.cmp(other)) 1561 } 1562 } 1563 impl Ord for DiscoverableAuthenticationServerState { 1564 #[inline] 1565 fn cmp(&self, other: &Self) -> Ordering { 1566 self.0.cmp(&other.0) 1567 } 1568 } 1569 impl Borrow<SentChallenge> for NonDiscoverableAuthenticationServerState { 1570 #[inline] 1571 fn borrow(&self) -> &SentChallenge { 1572 self.state.borrow() 1573 } 1574 } 1575 impl PartialEq for NonDiscoverableAuthenticationServerState { 1576 #[inline] 1577 fn eq(&self, other: &Self) -> bool { 1578 self.state == other.state 1579 } 1580 } 1581 impl PartialEq<&Self> for NonDiscoverableAuthenticationServerState { 1582 #[inline] 1583 fn eq(&self, other: &&Self) -> bool { 1584 self.state == other.state 1585 } 1586 } 1587 impl PartialEq<NonDiscoverableAuthenticationServerState> 1588 for &NonDiscoverableAuthenticationServerState 1589 { 1590 #[inline] 1591 fn eq(&self, other: &NonDiscoverableAuthenticationServerState) -> bool { 1592 self.state == other.state 1593 } 1594 } 1595 impl Eq for NonDiscoverableAuthenticationServerState {} 1596 impl Hash for NonDiscoverableAuthenticationServerState { 1597 #[inline] 1598 fn hash<H: Hasher>(&self, state: &mut H) { 1599 self.state.hash(state); 1600 } 1601 } 1602 impl PartialOrd for NonDiscoverableAuthenticationServerState { 1603 #[inline] 1604 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { 1605 Some(self.cmp(other)) 1606 } 1607 } 1608 impl Ord for NonDiscoverableAuthenticationServerState { 1609 #[inline] 1610 fn cmp(&self, other: &Self) -> Ordering { 1611 self.state.cmp(&other.state) 1612 } 1613 } 1614 #[cfg(test)] 1615 mod tests { 1616 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1617 use super::{ 1618 super::super::{ 1619 AuthenticatedCredential, DynamicState, StaticState, UserHandle, 1620 response::{ 1621 Backup, 1622 auth::{DiscoverableAuthenticatorAssertion, HmacSecret}, 1623 register::{ 1624 AuthenticationExtensionsPrfOutputs, AuthenticatorExtensionOutputStaticState, 1625 ClientExtensionsOutputsStaticState, CredentialProtectionPolicy, Ed25519PubKey, 1626 }, 1627 }, 1628 }, 1629 AuthCeremonyErr, AuthenticationVerificationOptions, AuthenticatorAttachment, 1630 AuthenticatorAttachmentEnforcement, CompressedPubKey, DiscoverableAuthentication, 1631 ExtensionErr, OneOrTwo, PrfInput, SignatureCounterEnforcement, 1632 }; 1633 #[cfg(all( 1634 feature = "custom", 1635 any( 1636 feature = "serializable_server_state", 1637 not(any(feature = "bin", feature = "serde")) 1638 ) 1639 ))] 1640 use super::{ 1641 super::{super::AggErr, Challenge, CredentialId, RpId, UserVerificationRequirement}, 1642 DiscoverableCredentialRequestOptions, ExtensionReq, 1643 }; 1644 #[cfg(all(feature = "custom", feature = "serializable_server_state"))] 1645 use super::{ 1646 super::{ 1647 super::bin::{Decode as _, Encode as _}, 1648 AsciiDomain, AuthTransports, 1649 }, 1650 AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Credentials as _, 1651 DiscoverableAuthenticationServerState, Extension, NonDiscoverableAuthenticationServerState, 1652 NonDiscoverableCredentialRequestOptions, PrfInputOwned, PublicKeyCredentialDescriptor, 1653 }; 1654 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1655 use ed25519_dalek::{Signer as _, SigningKey}; 1656 #[cfg(all( 1657 feature = "custom", 1658 any( 1659 feature = "serializable_server_state", 1660 not(any(feature = "bin", feature = "serde")) 1661 ) 1662 ))] 1663 use rsa::sha2::{Digest as _, Sha256}; 1664 #[cfg(all( 1665 feature = "custom", 1666 any( 1667 feature = "serializable_server_state", 1668 not(any(feature = "bin", feature = "serde")) 1669 ) 1670 ))] 1671 const CBOR_BYTES: u8 = 0b010_00000; 1672 #[cfg(all( 1673 feature = "custom", 1674 any( 1675 feature = "serializable_server_state", 1676 not(any(feature = "bin", feature = "serde")) 1677 ) 1678 ))] 1679 const CBOR_TEXT: u8 = 0b011_00000; 1680 #[cfg(all( 1681 feature = "custom", 1682 any( 1683 feature = "serializable_server_state", 1684 not(any(feature = "bin", feature = "serde")) 1685 ) 1686 ))] 1687 const CBOR_MAP: u8 = 0b101_00000; 1688 #[test] 1689 #[cfg(all(feature = "custom", feature = "serializable_server_state"))] 1690 fn eddsa_auth_ser() -> Result<(), AggErr> { 1691 let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); 1692 let mut creds = AllowedCredentials::with_capacity(1); 1693 _ = creds.push(AllowedCredential { 1694 credential: PublicKeyCredentialDescriptor { 1695 id: CredentialId::try_from(vec![0; 16])?, 1696 transports: AuthTransports::NONE, 1697 }, 1698 extension: CredentialSpecificExtension { 1699 prf: Some(PrfInputOwned { 1700 first: Vec::new(), 1701 second: Some(Vec::new()), 1702 ext_req: ExtensionReq::Require, 1703 }), 1704 }, 1705 }); 1706 let mut opts = NonDiscoverableCredentialRequestOptions::second_factor(&rp_id, creds)?; 1707 opts.options.user_verification = UserVerificationRequirement::Required; 1708 opts.options.challenge = Challenge(0); 1709 opts.options.extensions = Extension { prf: None }; 1710 let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec(); 1711 // We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information. 1712 let mut authenticator_data = Vec::with_capacity(164); 1713 authenticator_data.extend_from_slice( 1714 [ 1715 // rpIdHash. 1716 // This will be overwritten later. 1717 0, 1718 0, 1719 0, 1720 0, 1721 0, 1722 0, 1723 0, 1724 0, 1725 0, 1726 0, 1727 0, 1728 0, 1729 0, 1730 0, 1731 0, 1732 0, 1733 0, 1734 0, 1735 0, 1736 0, 1737 0, 1738 0, 1739 0, 1740 0, 1741 0, 1742 0, 1743 0, 1744 0, 1745 0, 1746 0, 1747 0, 1748 0, 1749 // flags. 1750 // UP, UV, and ED (right-to-left). 1751 0b1000_0101, 1752 // signCount. 1753 // 0 as 32-bit big endian. 1754 0, 1755 0, 1756 0, 1757 0, 1758 CBOR_MAP | 1, 1759 CBOR_TEXT | 11, 1760 b'h', 1761 b'm', 1762 b'a', 1763 b'c', 1764 b'-', 1765 b's', 1766 b'e', 1767 b'c', 1768 b'r', 1769 b'e', 1770 b't', 1771 CBOR_BYTES | 24, 1772 // Length is 80. 1773 80, 1774 // Two HMAC outputs concatenated and encrypted. 1775 0, 1776 0, 1777 0, 1778 0, 1779 0, 1780 0, 1781 0, 1782 0, 1783 0, 1784 0, 1785 0, 1786 0, 1787 0, 1788 0, 1789 0, 1790 0, 1791 0, 1792 0, 1793 0, 1794 0, 1795 0, 1796 0, 1797 0, 1798 0, 1799 0, 1800 0, 1801 0, 1802 0, 1803 0, 1804 0, 1805 0, 1806 0, 1807 0, 1808 0, 1809 0, 1810 0, 1811 0, 1812 0, 1813 0, 1814 0, 1815 0, 1816 0, 1817 0, 1818 0, 1819 0, 1820 0, 1821 0, 1822 0, 1823 0, 1824 0, 1825 0, 1826 0, 1827 0, 1828 0, 1829 0, 1830 0, 1831 0, 1832 0, 1833 0, 1834 0, 1835 0, 1836 0, 1837 0, 1838 0, 1839 0, 1840 0, 1841 0, 1842 0, 1843 0, 1844 0, 1845 0, 1846 0, 1847 0, 1848 0, 1849 0, 1850 0, 1851 0, 1852 0, 1853 0, 1854 0, 1855 ] 1856 .as_slice(), 1857 ); 1858 authenticator_data[..32] 1859 .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice()); 1860 authenticator_data 1861 .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice()); 1862 authenticator_data.truncate(132); 1863 let server = opts.start_ceremony()?.0; 1864 assert!( 1865 server.is_eq(&NonDiscoverableAuthenticationServerState::decode( 1866 server.encode()?.as_slice() 1867 )?) 1868 ); 1869 let mut opts_2 = DiscoverableCredentialRequestOptions::passkey(&rp_id); 1870 opts_2.public_key.challenge = Challenge(0); 1871 opts_2.public_key.extensions = Extension { prf: None }; 1872 let server_2 = opts_2.start_ceremony()?.0; 1873 assert!( 1874 server_2.is_eq(&DiscoverableAuthenticationServerState::decode( 1875 server_2 1876 .encode() 1877 .map_err(AggErr::EncodeDiscoverableAuthenticationServerState)? 1878 .as_slice() 1879 )?) 1880 ); 1881 Ok(()) 1882 } 1883 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1884 #[derive(Clone, Copy)] 1885 struct TestResponseOptions { 1886 user_verified: bool, 1887 hmac: HmacSecret, 1888 } 1889 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1890 #[derive(Clone, Copy)] 1891 enum PrfCredOptions { 1892 None, 1893 FalseNoHmac, 1894 FalseHmacFalse, 1895 TrueNoHmac, 1896 TrueHmacTrue, 1897 } 1898 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1899 #[derive(Clone, Copy)] 1900 struct TestCredOptions { 1901 cred_protect: CredentialProtectionPolicy, 1902 prf: PrfCredOptions, 1903 } 1904 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1905 #[derive(Clone, Copy)] 1906 enum PrfUvOptions { 1907 /// `true` iff `UserVerificationRequirement::Required` should be used; otherwise 1908 /// `UserVerificationRequirement::Preferred` is used. 1909 None(bool), 1910 Prf((PrfInput<'static, 'static>, ExtensionReq)), 1911 } 1912 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1913 #[derive(Clone, Copy)] 1914 struct TestRequestOptions { 1915 error_unsolicited: bool, 1916 prf_uv: PrfUvOptions, 1917 } 1918 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1919 #[derive(Clone, Copy)] 1920 struct TestOptions { 1921 request: TestRequestOptions, 1922 response: TestResponseOptions, 1923 cred: TestCredOptions, 1924 } 1925 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1926 fn generate_client_data_json() -> Vec<u8> { 1927 let mut json = Vec::with_capacity(256); 1928 json.extend_from_slice(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()); 1929 json 1930 } 1931 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1932 fn generate_authenticator_data_public_key_sig( 1933 opts: TestResponseOptions, 1934 ) -> ( 1935 Vec<u8>, 1936 CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>, 1937 Vec<u8>, 1938 ) { 1939 let mut authenticator_data = Vec::with_capacity(256); 1940 authenticator_data.extend_from_slice( 1941 [ 1942 // RP ID HASH. 1943 // This will be overwritten later. 1944 0, 1945 0, 1946 0, 1947 0, 1948 0, 1949 0, 1950 0, 1951 0, 1952 0, 1953 0, 1954 0, 1955 0, 1956 0, 1957 0, 1958 0, 1959 0, 1960 0, 1961 0, 1962 0, 1963 0, 1964 0, 1965 0, 1966 0, 1967 0, 1968 0, 1969 0, 1970 0, 1971 0, 1972 0, 1973 0, 1974 0, 1975 0, 1976 // FLAGS. 1977 // UP, UV, AT, and ED (right-to-left). 1978 0b0000_0001 1979 | if opts.user_verified { 1980 0b0000_0100 1981 } else { 1982 0b0000_0000 1983 } 1984 | if matches!(opts.hmac, HmacSecret::None) { 1985 0 1986 } else { 1987 0b1000_0000 1988 }, 1989 // COUNTER. 1990 // 0 as 32-bit big endian. 1991 0, 1992 0, 1993 0, 1994 0, 1995 ] 1996 .as_slice(), 1997 ); 1998 authenticator_data[..32] 1999 .copy_from_slice(Sha256::digest("example.com".as_bytes()).as_slice()); 2000 match opts.hmac { 2001 HmacSecret::None => {} 2002 HmacSecret::One => { 2003 authenticator_data.extend_from_slice( 2004 [ 2005 CBOR_MAP | 1, 2006 // CBOR text of length 11. 2007 CBOR_TEXT | 11, 2008 b'h', 2009 b'm', 2010 b'a', 2011 b'c', 2012 b'-', 2013 b's', 2014 b'e', 2015 b'c', 2016 b'r', 2017 b'e', 2018 b't', 2019 CBOR_BYTES | 24, 2020 48, 2021 ] 2022 .as_slice(), 2023 ); 2024 authenticator_data.extend_from_slice([0; 48].as_slice()); 2025 } 2026 HmacSecret::Two => { 2027 authenticator_data.extend_from_slice( 2028 [ 2029 CBOR_MAP | 1, 2030 // CBOR text of length 11. 2031 CBOR_TEXT | 11, 2032 b'h', 2033 b'm', 2034 b'a', 2035 b'c', 2036 b'-', 2037 b's', 2038 b'e', 2039 b'c', 2040 b'r', 2041 b'e', 2042 b't', 2043 CBOR_BYTES | 24, 2044 80, 2045 ] 2046 .as_slice(), 2047 ); 2048 authenticator_data.extend_from_slice([0; 80].as_slice()); 2049 } 2050 } 2051 let len = authenticator_data.len(); 2052 authenticator_data 2053 .extend_from_slice(Sha256::digest(generate_client_data_json().as_slice()).as_slice()); 2054 let sig_key = SigningKey::from_bytes(&[0; 32]); 2055 let sig = sig_key.sign(authenticator_data.as_slice()).to_vec(); 2056 authenticator_data.truncate(len); 2057 ( 2058 authenticator_data, 2059 CompressedPubKey::Ed25519(Ed25519PubKey::from(sig_key.verifying_key().to_bytes())), 2060 sig, 2061 ) 2062 } 2063 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2064 fn validate(options: TestOptions) -> Result<(), AggErr> { 2065 let rp_id = RpId::Domain("example.com".to_owned().try_into()?); 2066 let user = UserHandle::from([0; 1]); 2067 let (authenticator_data, credential_public_key, signature) = 2068 generate_authenticator_data_public_key_sig(options.response); 2069 let credential_id = CredentialId::try_from(vec![0; 16])?; 2070 let authentication = DiscoverableAuthentication::new( 2071 credential_id.clone(), 2072 DiscoverableAuthenticatorAssertion::new( 2073 generate_client_data_json(), 2074 authenticator_data, 2075 signature, 2076 user, 2077 ), 2078 AuthenticatorAttachment::None, 2079 ); 2080 let auth_opts = AuthenticationVerificationOptions::<'static, 'static, &str, &str> { 2081 allowed_origins: [].as_slice(), 2082 allowed_top_origins: None, 2083 auth_attachment_enforcement: AuthenticatorAttachmentEnforcement::Update(false), 2084 backup_requirement: None, 2085 error_on_unsolicited_extensions: options.request.error_unsolicited, 2086 sig_counter_enforcement: SignatureCounterEnforcement::Fail, 2087 update_uv: false, 2088 #[cfg(feature = "serde_relaxed")] 2089 client_data_json_relaxed: false, 2090 }; 2091 let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id); 2092 opts.public_key.challenge = Challenge(0); 2093 opts.public_key.user_verification = UserVerificationRequirement::Preferred; 2094 match options.request.prf_uv { 2095 PrfUvOptions::None(required) => { 2096 if required { 2097 opts.public_key.user_verification = UserVerificationRequirement::Required; 2098 }; 2099 } 2100 PrfUvOptions::Prf(input) => { 2101 opts.public_key.user_verification = UserVerificationRequirement::Required; 2102 opts.public_key.extensions.prf = Some(input); 2103 } 2104 } 2105 let mut cred = AuthenticatedCredential::new( 2106 (&credential_id).into(), 2107 &user, 2108 StaticState { 2109 credential_public_key, 2110 extensions: AuthenticatorExtensionOutputStaticState { 2111 cred_protect: options.cred.cred_protect, 2112 hmac_secret: match options.cred.prf { 2113 PrfCredOptions::None 2114 | PrfCredOptions::FalseNoHmac 2115 | PrfCredOptions::TrueNoHmac => None, 2116 PrfCredOptions::FalseHmacFalse => Some(false), 2117 PrfCredOptions::TrueHmacTrue => Some(true), 2118 }, 2119 }, 2120 client_extension_results: ClientExtensionsOutputsStaticState { 2121 prf: match options.cred.prf { 2122 PrfCredOptions::None => None, 2123 PrfCredOptions::FalseNoHmac | PrfCredOptions::FalseHmacFalse => { 2124 Some(AuthenticationExtensionsPrfOutputs { enabled: false }) 2125 } 2126 PrfCredOptions::TrueNoHmac | PrfCredOptions::TrueHmacTrue => { 2127 Some(AuthenticationExtensionsPrfOutputs { enabled: true }) 2128 } 2129 }, 2130 }, 2131 }, 2132 DynamicState { 2133 user_verified: true, 2134 backup: Backup::NotEligible, 2135 sign_count: 0, 2136 authenticator_attachment: AuthenticatorAttachment::None, 2137 }, 2138 )?; 2139 opts.start_ceremony()? 2140 .0 2141 .verify(&rp_id, &authentication, &mut cred, &auth_opts) 2142 .map_err(AggErr::AuthCeremony) 2143 .map(|_| ()) 2144 } 2145 /// Test all, and only, possible `UserNotVerified` errors. 2146 /// 4 * 5 * 3 * 2 * 5 = 600 tests. 2147 /// We ignore this due to how long it takes (around 4 seconds or so). 2148 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2149 #[ignore] 2150 #[test] 2151 fn test_uv_required_err() { 2152 const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [ 2153 CredentialProtectionPolicy::None, 2154 CredentialProtectionPolicy::UserVerificationOptional, 2155 CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, 2156 CredentialProtectionPolicy::UserVerificationRequired, 2157 ]; 2158 const ALL_PRF_CRED_OPTIONS: [PrfCredOptions; 5] = [ 2159 PrfCredOptions::None, 2160 PrfCredOptions::FalseNoHmac, 2161 PrfCredOptions::FalseHmacFalse, 2162 PrfCredOptions::TrueNoHmac, 2163 PrfCredOptions::TrueHmacTrue, 2164 ]; 2165 const ALL_HMAC_OPTIONS: [HmacSecret; 3] = 2166 [HmacSecret::None, HmacSecret::One, HmacSecret::Two]; 2167 const ALL_UNSOLICIT_OPTIONS: [bool; 2] = [false, true]; 2168 const ALL_NOT_FALSE_PRF_UV_OPTIONS: [PrfUvOptions; 5] = [ 2169 PrfUvOptions::None(true), 2170 PrfUvOptions::Prf(( 2171 PrfInput { 2172 first: [].as_slice(), 2173 second: None, 2174 }, 2175 ExtensionReq::Require, 2176 )), 2177 PrfUvOptions::Prf(( 2178 PrfInput { 2179 first: [].as_slice(), 2180 second: None, 2181 }, 2182 ExtensionReq::Allow, 2183 )), 2184 PrfUvOptions::Prf(( 2185 PrfInput { 2186 first: [].as_slice(), 2187 second: Some([].as_slice()), 2188 }, 2189 ExtensionReq::Require, 2190 )), 2191 PrfUvOptions::Prf(( 2192 PrfInput { 2193 first: [].as_slice(), 2194 second: Some([].as_slice()), 2195 }, 2196 ExtensionReq::Allow, 2197 )), 2198 ]; 2199 for cred_protect in ALL_CRED_PROTECT_OPTIONS { 2200 for prf in ALL_PRF_CRED_OPTIONS { 2201 for hmac in ALL_HMAC_OPTIONS { 2202 for error_unsolicited in ALL_UNSOLICIT_OPTIONS { 2203 for prf_uv in ALL_NOT_FALSE_PRF_UV_OPTIONS { 2204 assert!(validate(TestOptions { 2205 request: TestRequestOptions { 2206 error_unsolicited, 2207 prf_uv, 2208 }, 2209 response: TestResponseOptions { 2210 user_verified: false, 2211 hmac, 2212 }, 2213 cred: TestCredOptions { cred_protect, prf, }, 2214 }).map_or_else(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::UserNotVerified)), |_| false)); 2215 } 2216 } 2217 } 2218 } 2219 } 2220 } 2221 /// Test all, and only, possible `UserNotVerified` errors. 2222 /// 4 * 5 * 2 * 2 = 80 tests. 2223 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2224 #[test] 2225 fn test_forbidden_hmac() { 2226 const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [ 2227 CredentialProtectionPolicy::None, 2228 CredentialProtectionPolicy::UserVerificationOptional, 2229 CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, 2230 CredentialProtectionPolicy::UserVerificationRequired, 2231 ]; 2232 const ALL_PRF_CRED_OPTIONS: [PrfCredOptions; 5] = [ 2233 PrfCredOptions::None, 2234 PrfCredOptions::FalseNoHmac, 2235 PrfCredOptions::FalseHmacFalse, 2236 PrfCredOptions::TrueNoHmac, 2237 PrfCredOptions::TrueHmacTrue, 2238 ]; 2239 const ALL_HMAC_OPTIONS: [HmacSecret; 2] = [HmacSecret::One, HmacSecret::Two]; 2240 const ALL_UV_OPTIONS: [bool; 2] = [false, true]; 2241 for cred_protect in ALL_CRED_PROTECT_OPTIONS { 2242 for prf in ALL_PRF_CRED_OPTIONS { 2243 for hmac in ALL_HMAC_OPTIONS { 2244 for user_verified in ALL_UV_OPTIONS { 2245 assert!(validate(TestOptions { 2246 request: TestRequestOptions { 2247 error_unsolicited: true, 2248 prf_uv: PrfUvOptions::None(false), 2249 }, 2250 response: TestResponseOptions { 2251 user_verified, 2252 hmac, 2253 }, 2254 cred: TestCredOptions { cred_protect, prf, }, 2255 }).map_or_else(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::ForbiddenHmacSecret))), |_| false)); 2256 } 2257 } 2258 } 2259 } 2260 } 2261 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2262 #[test] 2263 fn test_prf() -> Result<(), AggErr> { 2264 let mut opts = TestOptions { 2265 request: TestRequestOptions { 2266 error_unsolicited: false, 2267 prf_uv: PrfUvOptions::Prf(( 2268 PrfInput { 2269 first: [].as_slice(), 2270 second: None, 2271 }, 2272 ExtensionReq::Allow, 2273 )), 2274 }, 2275 response: TestResponseOptions { 2276 user_verified: true, 2277 hmac: HmacSecret::None, 2278 }, 2279 cred: TestCredOptions { 2280 cred_protect: CredentialProtectionPolicy::None, 2281 prf: PrfCredOptions::None, 2282 }, 2283 }; 2284 validate(opts)?; 2285 opts.request.prf_uv = PrfUvOptions::Prf(( 2286 PrfInput { 2287 first: [].as_slice(), 2288 second: None, 2289 }, 2290 ExtensionReq::Require, 2291 )); 2292 opts.cred.prf = PrfCredOptions::TrueHmacTrue; 2293 assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::MissingHmacSecret))), |_| false)); 2294 opts.response.hmac = HmacSecret::One; 2295 opts.request.prf_uv = PrfUvOptions::Prf(( 2296 PrfInput { 2297 first: [].as_slice(), 2298 second: None, 2299 }, 2300 ExtensionReq::Allow, 2301 )); 2302 opts.cred.prf = PrfCredOptions::TrueNoHmac; 2303 validate(opts)?; 2304 opts.response.hmac = HmacSecret::Two; 2305 assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidHmacSecretValue(OneOrTwo::One, OneOrTwo::Two)))), |_| false)); 2306 opts.response.hmac = HmacSecret::One; 2307 opts.cred.prf = PrfCredOptions::FalseNoHmac; 2308 assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::PrfRequestedForPrfIncapableCred))), |_| false)); 2309 opts.response.user_verified = false; 2310 opts.request.prf_uv = PrfUvOptions::None(false); 2311 opts.cred.prf = PrfCredOptions::TrueHmacTrue; 2312 assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::UserNotVerifiedHmacSecret))), |_| false)); 2313 Ok(()) 2314 } 2315 }