auth.rs (91170B)
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, FIVE_MINUTES, Hint, Origin, PrfInput, PublicKeyCredentialDescriptor, 29 RpId, SentChallenge, TimedCeremony, UserVerificationRequirement, 30 auth::error::{InvalidTimeout, NonDiscoverableCredentialRequestOptionsErr}, 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(feature = "serde")] 47 pub mod ser; 48 /// Contains functionality to (de)serialize [`DiscoverableAuthenticationServerState`] and 49 /// [`NonDiscoverableAuthenticationServerState`] to a data store. 50 #[cfg(feature = "serializable_server_state")] 51 pub mod ser_server_state; 52 /// Controls how [signature counter](https://www.w3.org/TR/webauthn-3/#signature-counter) is enforced. 53 /// 54 /// Note that if the previous signature counter is positive and the new counter is not strictly greater, then the 55 /// authenticator is likely a clone (i.e., there are at least two copies of the private key). 56 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] 57 pub enum SignatureCounterEnforcement { 58 /// Fail the authentication ceremony if the counter is less than or equal to the previous value when the 59 /// previous value is positive. 60 #[default] 61 Fail, 62 /// When the counter is less than the previous value, don't fail and update the value. 63 /// 64 /// Note in the special case that the new signature counter is 0, [`DynamicState::sign_count`] _won't_ 65 /// be updated since that would allow an attacker to permanently disable the counter. 66 Update, 67 /// When the counter is less than the previous value, don't fail but don't update the value. 68 Ignore, 69 } 70 impl SignatureCounterEnforcement { 71 /// Validates the signature counter based on `self`. 72 const fn validate(self, prev: u32, cur: u32) -> Result<u32, AuthCeremonyErr> { 73 if prev == 0 || cur > prev { 74 Ok(cur) 75 } else { 76 match self { 77 Self::Fail => Err(AuthCeremonyErr::SignatureCounter), 78 // When the new counter is `0`, we use the previous counter to avoid an attacker from 79 // being able to permanently disable it. 80 Self::Update => Ok(if cur == 0 { prev } else { cur }), 81 Self::Ignore => Ok(prev), 82 } 83 } 84 } 85 } 86 /// Owned version of [`PrfInput`]. 87 /// 88 /// When relying on [`NonDiscoverableCredentialRequestOptions`], it's recommended to use credential-specific PRF 89 /// inputs that are continuously rolled over. One uses this type for such a thing. 90 #[derive(Clone, Debug, Eq, PartialEq)] 91 pub struct PrfInputOwned { 92 /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first). 93 pub first: Vec<u8>, 94 /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second). 95 pub second: Option<Vec<u8>>, 96 /// Note this is only applicable for authenticators that implement the 97 /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) extension on top of the 98 /// [`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) 99 /// extension since the data is encrypted and is part of the [`AuthenticatorData`]. 100 pub ext_req: ExtensionReq, 101 } 102 /// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client. 103 #[derive(Clone, Copy, Debug)] 104 pub struct Extension<'prf_first, 'prf_second> { 105 /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension). 106 /// 107 /// If both [`CredentialSpecificExtension::prf`] and this are [`Some`], then `CredentialSpecificExtension::prf` 108 /// takes priority. 109 /// 110 /// Note `ExtensionReq` is only applicable for authenticators that implement the 111 /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) extension on top of the 112 /// [`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) 113 /// extension since the data is encrypted and is part of the [`AuthenticatorData`]. 114 pub prf: Option<(PrfInput<'prf_first, 'prf_second>, ExtensionReq)>, 115 } 116 impl<'prf_first, 'prf_second> Extension<'prf_first, 'prf_second> { 117 /// Returns an `Extension` with [`Self::prf`] set to `None`. 118 #[inline] 119 #[must_use] 120 pub const fn none() -> Self { 121 Self { prf: None } 122 } 123 /// Returns an `Extension` with [`Self::prf`] set to `None`. 124 #[expect(single_use_lifetimes, reason = "false positive")] 125 #[inline] 126 #[must_use] 127 pub const fn with_prf<'a: 'prf_first, 'b: 'prf_second>( 128 input: PrfInput<'a, 'b>, 129 req: ExtensionReq, 130 ) -> Self { 131 Self { 132 prf: Some((input, req)), 133 } 134 } 135 } 136 impl Default for Extension<'_, '_> { 137 /// Same as [`Self::none`]. 138 #[inline] 139 fn default() -> Self { 140 Self::none() 141 } 142 } 143 /// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client that 144 /// are credential-specific which among other things implies a non-discoverable request. 145 #[derive(Clone, Debug, Default)] 146 pub struct CredentialSpecificExtension { 147 /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension). 148 /// 149 /// If both [`Extension::prf`] and this are [`Some`], then this take priority. 150 pub prf: Option<PrfInputOwned>, 151 } 152 /// Registered credential used in 153 /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials). 154 #[derive(Clone, Debug)] 155 pub struct AllowedCredential { 156 /// The registered credential. 157 pub credential: PublicKeyCredentialDescriptor<Box<[u8]>>, 158 /// Credential-specific extensions. 159 pub extension: CredentialSpecificExtension, 160 } 161 impl From<PublicKeyCredentialDescriptor<Box<[u8]>>> for AllowedCredential { 162 #[inline] 163 fn from(credential: PublicKeyCredentialDescriptor<Box<[u8]>>) -> Self { 164 Self { 165 credential, 166 extension: CredentialSpecificExtension::default(), 167 } 168 } 169 } 170 impl From<AllowedCredential> for PublicKeyCredentialDescriptor<Box<[u8]>> { 171 #[inline] 172 fn from(credential: AllowedCredential) -> Self { 173 credential.credential 174 } 175 } 176 /// Queue of unique [`AllowedCredential`]s. 177 #[derive(Clone, Debug, Default)] 178 pub struct AllowedCredentials { 179 /// Allowed credentials. 180 creds: Vec<AllowedCredential>, 181 /// Number of `AllowedCredential`s that have PRF inputs. 182 /// 183 /// Useful to help serialization. 184 prf_count: usize, 185 } 186 impl Credentials for AllowedCredentials { 187 type Credential = AllowedCredential; 188 /// # Examples 189 /// 190 /// ``` 191 /// # use webauthn_rp::request::{auth::AllowedCredentials, Credentials}; 192 /// assert!(AllowedCredentials::with_capacity(1).as_ref().is_empty()); 193 /// ``` 194 #[inline] 195 fn with_capacity(capacity: usize) -> Self { 196 Self { 197 creds: Vec::with_capacity(capacity), 198 prf_count: 0, 199 } 200 } 201 /// # Examples 202 /// 203 /// ``` 204 /// # #[cfg(all(feature = "bin", feature = "custom"))] 205 /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr}; 206 /// # use webauthn_rp::{ 207 /// # request::{auth::AllowedCredentials, PublicKeyCredentialDescriptor, Credentials}, 208 /// # response::{AuthTransports, CredentialId}, 209 /// # }; 210 /// /// Retrieves the `AuthTransports` associated with the unique `cred_id` 211 /// /// from the database. 212 /// # #[cfg(all(feature = "bin", feature = "custom"))] 213 /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> { 214 /// // ⋮ 215 /// # AuthTransports::decode(32) 216 /// } 217 /// let mut creds = AllowedCredentials::with_capacity(1); 218 /// assert!(creds.as_ref().is_empty()); 219 /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is 220 /// // likely never needed since the `CredentialId` was originally sent from the client and is likely 221 /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`. 222 /// # #[cfg(all(feature = "bin", feature = "custom"))] 223 /// let id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?; 224 /// # #[cfg(all(feature = "bin", feature = "custom"))] 225 /// let transports = get_transports((&id).into())?; 226 /// # #[cfg(all(feature = "bin", feature = "custom"))] 227 /// assert!(creds.push(PublicKeyCredentialDescriptor { id, transports }.into())); 228 /// # #[cfg(all(feature = "bin", feature = "custom"))] 229 /// let id_copy = CredentialId::try_from(vec![0; 16].into_boxed_slice())?; 230 /// # #[cfg(all(feature = "bin", feature = "custom"))] 231 /// let transports_2 = AuthTransports::NONE; 232 /// // Duplicate `CredentialId`s don't get added. 233 /// # #[cfg(all(feature = "bin", feature = "custom"))] 234 /// assert!(!creds.push( 235 /// PublicKeyCredentialDescriptor { 236 /// id: id_copy, 237 /// transports: transports_2 238 /// } 239 /// .into() 240 /// )); 241 /// # Ok::<_, webauthn_rp::AggErr>(()) 242 /// ``` 243 #[expect( 244 clippy::arithmetic_side_effects, 245 reason = "comment explains how overflow is not possible" 246 )] 247 #[inline] 248 fn push(&mut self, cred: Self::Credential) -> bool { 249 self.creds 250 .iter() 251 .try_fold((), |(), c| { 252 if c.credential.id == cred.credential.id { 253 Err(()) 254 } else { 255 Ok(()) 256 } 257 }) 258 .is_ok_and(|()| { 259 // This can't overflow since `self.creds.push` would `panic` since 260 // `self.prf_count <= self.creds.len()`. 261 self.prf_count += usize::from(cred.extension.prf.is_some()); 262 self.creds.push(cred); 263 true 264 }) 265 } 266 #[inline] 267 fn len(&self) -> usize { 268 self.creds.len() 269 } 270 } 271 impl AsRef<[AllowedCredential]> for AllowedCredentials { 272 #[inline] 273 fn as_ref(&self) -> &[AllowedCredential] { 274 self.creds.as_slice() 275 } 276 } 277 impl From<&AllowedCredentials> for Box<[CredInfo]> { 278 #[inline] 279 fn from(value: &AllowedCredentials) -> Self { 280 let len = value.creds.len(); 281 value 282 .creds 283 .iter() 284 .fold(Vec::with_capacity(len), |mut creds, cred| { 285 creds.push(CredInfo { 286 id: cred.credential.id.clone(), 287 ext: (&cred.extension).into(), 288 }); 289 creds 290 }) 291 .into_boxed_slice() 292 } 293 } 294 impl From<AllowedCredentials> for Vec<PublicKeyCredentialDescriptor<Box<[u8]>>> { 295 #[inline] 296 fn from(value: AllowedCredentials) -> Self { 297 let mut creds = Self::with_capacity(value.creds.len()); 298 value.creds.into_iter().fold((), |(), cred| { 299 creds.push(cred.credential); 300 }); 301 creds 302 } 303 } 304 impl From<Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>> for AllowedCredentials { 305 #[inline] 306 fn from(value: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>) -> Self { 307 let mut creds = Self::with_capacity(value.len()); 308 value.into_iter().fold((), |(), credential| { 309 _ = creds.push(AllowedCredential { 310 credential, 311 extension: CredentialSpecificExtension { prf: None }, 312 }); 313 }); 314 creds 315 } 316 } 317 /// The [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions) 318 /// to send to the client when authenticating a discoverable credential. 319 /// 320 /// Upon saving the [`DiscoverableAuthenticationServerState`] returned from [`Self::start_ceremony`], one MUST send 321 /// [`DiscoverableAuthenticationClientState`] to the client ASAP. After receiving the newly created 322 /// [`DiscoverableAuthentication`], it is validated using [`DiscoverableAuthenticationServerState::verify`]. 323 #[derive(Debug)] 324 pub struct DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> { 325 /// [`mediation`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement). 326 /// 327 /// Note if this is [`CredentialMediationRequirement::Conditional`], user agents are instructed to not 328 /// enforce any timeout; as result, one may want to set [`PublicKeyCredentialRequestOptions::timeout`] to 329 /// [`NonZeroU32::MAX`]. 330 pub mediation: CredentialMediationRequirement, 331 /// `public-key` [credential type](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry). 332 pub public_key: PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>, 333 } 334 impl<'rp_id, 'prf_first, 'prf_second> 335 DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> 336 { 337 /// Creates a `DiscoverableCredentialRequestOptions` containing [`CredentialMediationRequirement::default`] and 338 /// [`PublicKeyCredentialRequestOptions::passkey`]. 339 /// 340 /// # Examples 341 /// 342 /// ``` 343 /// # use webauthn_rp::request::{auth::DiscoverableCredentialRequestOptions, AsciiDomain, RpId, UserVerificationRequirement}; 344 /// assert!(matches!( 345 /// DiscoverableCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).public_key.user_verification, 346 /// UserVerificationRequirement::Required 347 /// )); 348 /// # Ok::<_, webauthn_rp::AggErr>(()) 349 /// ``` 350 #[expect(single_use_lifetimes, reason = "false positive")] 351 #[inline] 352 #[must_use] 353 pub fn passkey<'a: 'rp_id>(rp_id: &'a RpId) -> Self { 354 Self { 355 mediation: CredentialMediationRequirement::default(), 356 public_key: PublicKeyCredentialRequestOptions::passkey(rp_id), 357 } 358 } 359 /// Begins the [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony) consuming 360 /// `self`. Note that the expiration [`Instant`]/[`SystemTime`] is saved, so 361 /// `DiscoverableAuthenticationClientState` MUST be sent ASAP. In order to complete authentication, the returned 362 /// `DiscoverableAuthenticationServerState` MUST be saved so that it can later be used to verify the credential 363 /// assertion with [`DiscoverableAuthenticationServerState::verify`]. 364 /// 365 /// # Errors 366 /// 367 /// Errors iff `self` contains incompatible configuration. 368 #[inline] 369 pub fn start_ceremony( 370 self, 371 ) -> Result< 372 ( 373 DiscoverableAuthenticationServerState, 374 DiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>, 375 ), 376 InvalidTimeout, 377 > { 378 #[cfg(not(feature = "serializable_server_state"))] 379 let res = Instant::now(); 380 #[cfg(feature = "serializable_server_state")] 381 let res = SystemTime::now(); 382 res.checked_add(Duration::from_millis( 383 NonZeroU64::from(self.public_key.timeout).get(), 384 )) 385 .ok_or(InvalidTimeout) 386 .map(|expiration| { 387 ( 388 DiscoverableAuthenticationServerState(AuthenticationServerState { 389 challenge: SentChallenge(self.public_key.challenge.0), 390 user_verification: self.public_key.user_verification, 391 extensions: self.public_key.extensions.into(), 392 expiration, 393 }), 394 DiscoverableAuthenticationClientState(self), 395 ) 396 }) 397 } 398 /// Same as [`Self::start_ceremony`] except the raw challenge is returned instead of 399 /// [`DiscoverableAuthenticationClientState`]. 400 /// 401 /// Note this is useful when one configures the authentication ceremony client-side and only needs the 402 /// server-generated challenge. It's of course essential that `self` is configured exactly the same as 403 /// how it is configured client-side. See 404 /// [`challengeURL`](https://github.com/w3c/webauthn/wiki/Explainer:-WebAuthn-challengeURL) for more 405 /// information. 406 /// 407 /// # Errors 408 /// 409 /// Read [`Self::start_ceremony`]. 410 #[inline] 411 pub fn start_ceremony_challenge_only( 412 self, 413 ) -> Result<(DiscoverableAuthenticationServerState, [u8; 16]), InvalidTimeout> { 414 self.start_ceremony() 415 .map(|(server, client)| (server, client.0.public_key.challenge.into_array())) 416 } 417 } 418 /// The [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions) 419 /// to send to the client when authenticating non-discoverable credentials. 420 /// 421 /// Upon saving the [`NonDiscoverableAuthenticationServerState`] returned from [`Self::start_ceremony`], one MUST send 422 /// [`NonDiscoverableAuthenticationClientState`] to the client ASAP. After receiving the newly created 423 /// [`NonDiscoverableAuthentication`], it is validated using [`NonDiscoverableAuthenticationServerState::verify`]. 424 #[derive(Debug)] 425 pub struct NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> { 426 /// [`mediation`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement). 427 pub mediation: CredentialMediationRequirement, 428 /// [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions). 429 pub options: PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>, 430 /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials). 431 pub allow_credentials: AllowedCredentials, 432 } 433 impl<'rp_id, 'prf_first, 'prf_second> 434 NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> 435 { 436 /// Creates a `NonDiscoverableCredentialRequestOptions` containing 437 /// [`CredentialMediationRequirement::default`], 438 /// [`PublicKeyCredentialRequestOptions::second_factor`], and the passed [`AllowedCredentials`]. 439 /// 440 /// # Examples 441 /// 442 /// ``` 443 /// # #[cfg(all(feature = "bin", feature = "custom"))] 444 /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr}; 445 /// # use webauthn_rp::{ 446 /// # request::{ 447 /// # auth::{AllowedCredentials, NonDiscoverableCredentialRequestOptions}, 448 /// # AsciiDomain, RpId, PublicKeyCredentialDescriptor, Credentials 449 /// # }, 450 /// # response::{AuthTransports, CredentialId}, 451 /// # }; 452 /// /// Retrieves the `AuthTransports` associated with the unique `cred_id` 453 /// /// from the database. 454 /// # #[cfg(all(feature = "bin", feature = "custom"))] 455 /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> { 456 /// // ⋮ 457 /// # AuthTransports::decode(32) 458 /// } 459 /// let mut creds = AllowedCredentials::with_capacity(1); 460 /// assert!(creds.as_ref().is_empty()); 461 /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is 462 /// // likely never needed since the `CredentialId` was originally sent from the client and is likely 463 /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`. 464 /// # #[cfg(all(feature = "bin", feature = "custom"))] 465 /// let id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?; 466 /// # #[cfg(all(feature = "bin", feature = "custom"))] 467 /// let transports = get_transports((&id).into())?; 468 /// # #[cfg(all(feature = "bin", feature = "custom"))] 469 /// assert!(creds.push(PublicKeyCredentialDescriptor { id, transports }.into())); 470 /// # #[cfg(all(feature = "bin", feature = "custom"))] 471 /// assert_eq!( 472 /// NonDiscoverableCredentialRequestOptions::second_factor(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?), creds) 473 /// .allow_credentials 474 /// .len(), 475 /// 1 476 /// ); 477 /// # Ok::<_, webauthn_rp::AggErr>(()) 478 /// ``` 479 #[expect(single_use_lifetimes, reason = "false positive")] 480 #[inline] 481 #[must_use] 482 pub fn second_factor<'a: 'rp_id>( 483 rp_id: &'a RpId, 484 allow_credentials: AllowedCredentials, 485 ) -> Self { 486 Self { 487 mediation: CredentialMediationRequirement::default(), 488 options: PublicKeyCredentialRequestOptions::second_factor(rp_id), 489 allow_credentials, 490 } 491 } 492 /// Begins the [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony) consuming 493 /// `self`. Note that the expiration [`Instant`]/[`SystemTime`] is saved, so `NonDiscoverableAuthenticationClientState` 494 /// MUST be sent ASAP. In order to complete authentication, the returned `NonDiscoverableAuthenticationServerState` 495 /// MUST be saved so that it can later be used to verify the credential assertion with 496 /// [`NonDiscoverableAuthenticationServerState::verify`]. 497 /// 498 /// # Errors 499 /// 500 /// Errors iff `self` contains incompatible configuration. 501 #[inline] 502 pub fn start_ceremony( 503 self, 504 ) -> Result< 505 ( 506 NonDiscoverableAuthenticationServerState, 507 NonDiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>, 508 ), 509 NonDiscoverableCredentialRequestOptionsErr, 510 > { 511 if self.allow_credentials.is_empty() { 512 Err(NonDiscoverableCredentialRequestOptionsErr::EmptyAllowedCredentials) 513 } else if matches!(self.mediation, CredentialMediationRequirement::Conditional) { 514 Err(NonDiscoverableCredentialRequestOptionsErr::ConditionalMediationRequested) 515 } else { 516 #[cfg(not(feature = "serializable_server_state"))] 517 let res = Instant::now(); 518 #[cfg(feature = "serializable_server_state")] 519 let res = SystemTime::now(); 520 res.checked_add(Duration::from_millis( 521 NonZeroU64::from(self.options.timeout).get(), 522 )) 523 .ok_or(NonDiscoverableCredentialRequestOptionsErr::InvalidTimeout) 524 .map(|expiration| { 525 ( 526 NonDiscoverableAuthenticationServerState { 527 state: AuthenticationServerState { 528 challenge: SentChallenge(self.options.challenge.0), 529 user_verification: self.options.user_verification, 530 extensions: self.options.extensions.into(), 531 expiration, 532 }, 533 allow_credentials: Box::from(&self.allow_credentials), 534 }, 535 NonDiscoverableAuthenticationClientState(self), 536 ) 537 }) 538 } 539 } 540 } 541 /// The [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions) 542 /// to send to the client when authenticating a credential. 543 /// 544 /// This does _not_ contain [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials). 545 #[derive(Debug)] 546 pub struct PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> { 547 /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge). 548 pub challenge: Challenge, 549 /// [`timeout`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-timeout). 550 /// 551 /// Note we require a positive value despite the spec allowing an optional nonnegative value. This jives 552 /// with the fact that in-memory storage is required when `serializable_server_state` is not enabled 553 /// when authenticating credentials as no timeout would make out-of-memory (OOM) conditions more likely. 554 pub timeout: NonZeroU32, 555 /// [`rpId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-rpid). 556 /// 557 /// This MUST be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] used when the credential was registered. 558 pub rp_id: &'rp_id RpId, 559 /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification). 560 pub user_verification: UserVerificationRequirement, 561 /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-hints). 562 pub hints: Hint, 563 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-extensions). 564 pub extensions: Extension<'prf_first, 'prf_second>, 565 } 566 impl<'rp_id> PublicKeyCredentialRequestOptions<'rp_id, '_, '_> { 567 /// Creates a `PublicKeyCredentialRequestOptions` with [`Self::user_verification`] set to 568 /// [`UserVerificationRequirement::Required`] and [`Self::timeout`] set to [`FIVE_MINUTES`]. 569 /// 570 /// Note `rp_id` _must_ be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] when the 571 /// credential was registered. 572 /// 573 /// # Examples 574 /// 575 /// ``` 576 /// # use webauthn_rp::request::{auth::PublicKeyCredentialRequestOptions, AsciiDomain, RpId, UserVerificationRequirement}; 577 /// assert!(matches!( 578 /// PublicKeyCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).user_verification, 579 /// UserVerificationRequirement::Required 580 /// )); 581 /// # Ok::<_, webauthn_rp::AggErr>(()) 582 /// ``` 583 #[expect(single_use_lifetimes, reason = "false positive")] 584 #[inline] 585 #[must_use] 586 pub fn passkey<'a: 'rp_id>(rp_id: &'a RpId) -> Self { 587 Self { 588 challenge: Challenge::new(), 589 timeout: FIVE_MINUTES, 590 rp_id, 591 user_verification: UserVerificationRequirement::Required, 592 hints: Hint::None, 593 extensions: Extension::default(), 594 } 595 } 596 /// Creates a `PublicKeyCredentialRequestOptions` with [`Self::user_verification`] set to 597 /// [`UserVerificationRequirement::Discouraged`] and [`Self::timeout`] set to [`FIVE_MINUTES`]. 598 /// 599 /// Note `rp_id` _must_ be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] when the 600 /// credentials were registered. 601 /// 602 /// # Examples 603 /// 604 /// ``` 605 /// # use webauthn_rp::request::{auth::PublicKeyCredentialRequestOptions, AsciiDomain, RpId, UserVerificationRequirement}; 606 /// assert!(matches!( 607 /// PublicKeyCredentialRequestOptions::second_factor(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).user_verification, 608 /// UserVerificationRequirement::Discouraged 609 /// )); 610 /// # Ok::<_, webauthn_rp::AggErr>(()) 611 /// ``` 612 #[expect(single_use_lifetimes, reason = "false positive")] 613 #[inline] 614 #[must_use] 615 pub fn second_factor<'a: 'rp_id>(rp_id: &'a RpId) -> Self { 616 let mut opts = Self::passkey(rp_id); 617 opts.user_verification = UserVerificationRequirement::Discouraged; 618 opts 619 } 620 } 621 /// Container of a [`DiscoverableCredentialRequestOptions`] that has been used to start the authentication ceremony. 622 /// This gets sent to the client ASAP. 623 #[derive(Debug)] 624 pub struct DiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>( 625 DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>, 626 ); 627 impl<'rp_id, 'prf_first, 'prf_second> 628 DiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second> 629 { 630 /// Returns the `DiscoverableCredentialRequestOptions` that was used to start an authentication ceremony. 631 #[inline] 632 #[must_use] 633 pub const fn options( 634 &self, 635 ) -> &DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> { 636 &self.0 637 } 638 } 639 /// Container of a [`NonDiscoverableCredentialRequestOptions`] that has been used to start the authentication 640 /// ceremony. This gets sent to the client ASAP. 641 #[derive(Debug)] 642 pub struct NonDiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>( 643 NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>, 644 ); 645 impl<'rp_id, 'prf_first, 'prf_second> 646 NonDiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second> 647 { 648 /// Returns the `NonDiscoverableCredentialRequestOptions` that was used to start an authentication ceremony. 649 #[inline] 650 #[must_use] 651 pub const fn options( 652 &self, 653 ) -> &NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> { 654 &self.0 655 } 656 } 657 /// The possible combinations of an [`AuthenticatedCredential`]'s [`StaticState`]'s 658 /// `extensions.hmac_secret` and `client_extension_results.prf`. 659 /// 660 /// Note we ensure in `crate::verify_static_and_dynamic_state` that `hmac_secret` does not exist when 661 /// `prf` does not exist, `hmac_secret` does not exist or is `false` when `prf` is `false`, or 662 /// `hmac_secret` does not exist or is `true` when `prf` is `true`. 663 #[derive(Clone, Copy)] 664 enum CredPrf { 665 /// No `prf` or `hmac_secret`. 666 None, 667 /// `prf.enabled` is `false` but there is no `hmac_secret`. 668 FalseNoHmac, 669 /// `prf.enabled` and `hmac_secret` are `false`. 670 FalseFalseHmac, 671 /// `prf.enabled` is `true` but there is no `hmac_secret`. 672 TrueNoHmac, 673 /// `prf.enabled` and `hmac_secret` are `true`. 674 TrueTrueHmac, 675 } 676 /// `PrfInput` and `PrfInputOwned` without the actual data sent to reduce memory usage when storing 677 /// [`DiscoverableAuthenticationServerState`] in an in-memory collection. 678 #[derive(Clone, Copy, Debug)] 679 enum ServerPrfInfo { 680 /// No `PrfInput`. 681 None, 682 /// `PrfInput::second` was `None`. 683 One(ExtensionReq), 684 /// `PrfInput::second` was `Some`. 685 Two(ExtensionReq), 686 } 687 impl ServerPrfInfo { 688 /// Validates `val` based on the passed arguments. 689 /// 690 /// It's not possible to request the PRF extension without sending `UserVerificationRequirement::Required`; 691 /// thus `user_verified` will always be `true` when sending PRF; otherwise ceremony validation will error. 692 /// However when we _don't_ send the PRF extension _and_ we don't error on an unsolicited response, it's 693 /// possible to receive an `HmacSecret` without the user having been verified; thus we only ensure 694 /// `user_verified` is true when we don't error on unsolicted responses _and_ we didn't send the PRF extension. 695 const fn validate( 696 self, 697 user_verified: bool, 698 cred_prf: CredPrf, 699 hmac: HmacSecret, 700 err_unsolicited: bool, 701 ) -> Result<(), ExtensionErr> { 702 match hmac { 703 HmacSecret::None => match self { 704 Self::None => Ok(()), 705 Self::One(req) | Self::Two(req) => { 706 if matches!(req, ExtensionReq::Allow) 707 || !matches!(cred_prf, CredPrf::TrueTrueHmac) 708 { 709 Ok(()) 710 } else { 711 Err(ExtensionErr::MissingHmacSecret) 712 } 713 } 714 }, 715 HmacSecret::One => match self { 716 Self::None => { 717 if err_unsolicited { 718 Err(ExtensionErr::ForbiddenHmacSecret) 719 } else if matches!(cred_prf, CredPrf::None | CredPrf::TrueTrueHmac) { 720 if user_verified { 721 Ok(()) 722 } else { 723 Err(ExtensionErr::UserNotVerifiedHmacSecret) 724 } 725 } else { 726 Err(ExtensionErr::HmacSecretForNonHmacSecretCredential) 727 } 728 } 729 Self::One(_) => { 730 if matches!(cred_prf, CredPrf::None | CredPrf::TrueTrueHmac) { 731 Ok(()) 732 } else { 733 Err(ExtensionErr::HmacSecretForNonHmacSecretCredential) 734 } 735 } 736 Self::Two(_) => Err(ExtensionErr::InvalidHmacSecretValue( 737 OneOrTwo::Two, 738 OneOrTwo::One, 739 )), 740 }, 741 HmacSecret::Two => match self { 742 Self::None => { 743 if err_unsolicited { 744 Err(ExtensionErr::ForbiddenHmacSecret) 745 } else if matches!(cred_prf, CredPrf::None | CredPrf::TrueTrueHmac) { 746 if user_verified { 747 Ok(()) 748 } else { 749 Err(ExtensionErr::UserNotVerifiedHmacSecret) 750 } 751 } else { 752 Err(ExtensionErr::HmacSecretForNonHmacSecretCredential) 753 } 754 } 755 Self::One(_) => Err(ExtensionErr::InvalidHmacSecretValue( 756 OneOrTwo::One, 757 OneOrTwo::Two, 758 )), 759 Self::Two(_) => { 760 if matches!(cred_prf, CredPrf::None | CredPrf::TrueTrueHmac) { 761 Ok(()) 762 } else { 763 Err(ExtensionErr::HmacSecretForNonHmacSecretCredential) 764 } 765 } 766 }, 767 } 768 } 769 } 770 #[cfg(test)] 771 impl PartialEq for ServerPrfInfo { 772 fn eq(&self, other: &Self) -> bool { 773 match *self { 774 Self::None => matches!(*other, Self::None), 775 Self::One(req) => matches!(*other, Self::One(req2) if req == req2), 776 Self::Two(req) => matches!(*other, Self::Two(req2) if req == req2), 777 } 778 } 779 } 780 impl From<Option<(PrfInput<'_, '_>, ExtensionReq)>> for ServerPrfInfo { 781 fn from(value: Option<(PrfInput<'_, '_>, ExtensionReq)>) -> Self { 782 value.map_or(Self::None, |val| { 783 val.0 784 .second 785 .map_or_else(|| Self::One(val.1), |_| Self::Two(val.1)) 786 }) 787 } 788 } 789 impl From<&PrfInputOwned> for ServerPrfInfo { 790 fn from(value: &PrfInputOwned) -> Self { 791 value 792 .second 793 .as_ref() 794 .map_or_else(|| Self::One(value.ext_req), |_| Self::Two(value.ext_req)) 795 } 796 } 797 /// `Extension` without the actual data sent to reduce memory usage when storing [`AuthenticationServerState`] 798 /// in an in-memory collection. 799 #[derive(Clone, Copy, Debug)] 800 struct ServerExtensionInfo { 801 /// `Extension::prf`. 802 prf: ServerPrfInfo, 803 } 804 impl From<Extension<'_, '_>> for ServerExtensionInfo { 805 fn from(value: Extension<'_, '_>) -> Self { 806 Self { 807 prf: value.prf.into(), 808 } 809 } 810 } 811 #[cfg(test)] 812 impl PartialEq for ServerExtensionInfo { 813 fn eq(&self, other: &Self) -> bool { 814 self.prf == other.prf 815 } 816 } 817 /// `CredentialSpecificExtension` without the actual data sent to reduce memory usage when storing [`AuthenticationServerState`] 818 /// in an in-memory collection. 819 #[derive(Clone, Copy, Debug)] 820 struct ServerCredSpecificExtensionInfo { 821 /// `CredentialSpecificExtension::prf`. 822 prf: ServerPrfInfo, 823 } 824 #[cfg(test)] 825 impl PartialEq for ServerCredSpecificExtensionInfo { 826 fn eq(&self, other: &Self) -> bool { 827 self.prf == other.prf 828 } 829 } 830 impl From<&CredentialSpecificExtension> for ServerCredSpecificExtensionInfo { 831 fn from(value: &CredentialSpecificExtension) -> Self { 832 Self { 833 prf: value 834 .prf 835 .as_ref() 836 .map_or(ServerPrfInfo::None, ServerPrfInfo::from), 837 } 838 } 839 } 840 impl ServerExtensionInfo { 841 /// Validates the extensions. 842 /// 843 /// Note that this MUST only be called internally by `auth::validate_extensions`. 844 const fn validate_extensions( 845 self, 846 user_verified: bool, 847 auth_ext: AuthenticatorExtensionOutput, 848 error_unsolicited: bool, 849 cred_prf: CredPrf, 850 ) -> Result<(), ExtensionErr> { 851 ServerPrfInfo::validate( 852 self.prf, 853 user_verified, 854 cred_prf, 855 auth_ext.hmac_secret, 856 error_unsolicited, 857 ) 858 } 859 } 860 /// Validates the extensions. 861 fn validate_extensions( 862 ext: ServerExtensionInfo, 863 user_verified: bool, 864 cred_ext: Option<ServerCredSpecificExtensionInfo>, 865 auth_ext: AuthenticatorExtensionOutput, 866 error_unsolicited: bool, 867 cred_prf: CredPrf, 868 ) -> Result<(), ExtensionErr> { 869 cred_ext.map_or_else( 870 || { 871 // No credental-specific extensions, so we can simply focus on `ext`. 872 ext.validate_extensions(user_verified, auth_ext, error_unsolicited, cred_prf) 873 }, 874 |c_ext| { 875 // Must carefully process each extension based on overlap and which gets priority over the other. 876 if matches!(c_ext.prf, ServerPrfInfo::None) { 877 ext.prf.validate( 878 user_verified, 879 cred_prf, 880 auth_ext.hmac_secret, 881 error_unsolicited, 882 ) 883 } else { 884 c_ext.prf.validate( 885 user_verified, 886 cred_prf, 887 auth_ext.hmac_secret, 888 error_unsolicited, 889 ) 890 } 891 }, 892 ) 893 } 894 /// [`AllowedCredential`] with less data to reduce memory usage when storing [`AuthenticationServerState`] 895 /// in an in-memory collection. 896 #[derive(Debug)] 897 struct CredInfo { 898 /// The Credential ID. 899 id: CredentialId<Box<[u8]>>, 900 /// Any credential-specific extensions. 901 ext: ServerCredSpecificExtensionInfo, 902 } 903 #[cfg(test)] 904 impl PartialEq for CredInfo { 905 fn eq(&self, other: &Self) -> bool { 906 self.id == other.id && self.ext == other.ext 907 } 908 } 909 /// Controls how to handle a change in [`DynamicState::authenticator_attachment`]. 910 /// 911 /// Note when `DynamicState::authenticator_attachment` is [`AuthenticatorAttachment::None`], then it will 912 /// be updated regardless. Similarly when [`Authentication::authenticator_attachment`] is 913 /// `AuthenticatorAttachment::None`, it will never update `DynamicState::authenticator_attachment`. 914 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 915 pub enum AuthenticatorAttachmentEnforcement { 916 /// Fail the authentication ceremony if [`AuthenticatorAttachment`] is not the same. 917 /// 918 /// The contained `bool` represents if `AuthenticatorAttachment` must be sent. 919 Fail(bool), 920 /// Update [`DynamicState::authenticator_attachment`] to the sent [`AuthenticatorAttachment`]. 921 /// 922 /// The contained `bool` represents if `AuthenticatorAttachment` must be sent. 923 Update(bool), 924 /// Do not update [`DynamicState::authenticator_attachment`] to the sent [`AuthenticatorAttachment`]. 925 /// 926 /// The contained `bool` represents if `AuthenticatorAttachment` must be sent. 927 Ignore(bool), 928 } 929 impl AuthenticatorAttachmentEnforcement { 930 /// Validates `cur` based on `self` and `prev`. 931 const fn validate( 932 self, 933 prev: AuthenticatorAttachment, 934 cur: AuthenticatorAttachment, 935 ) -> Result<AuthenticatorAttachment, AuthCeremonyErr> { 936 match cur { 937 AuthenticatorAttachment::None => match self { 938 Self::Fail(require) | Self::Update(require) | Self::Ignore(require) => { 939 if require { 940 Err(AuthCeremonyErr::MissingAuthenticatorAttachment) 941 } else { 942 // We don't overwrite the previous one with [`AuthenticatorAttachment::None`]. 943 Ok(prev) 944 } 945 } 946 }, 947 AuthenticatorAttachment::Platform => match self { 948 Self::Fail(_) => { 949 if matches!(prev, AuthenticatorAttachment::CrossPlatform) { 950 Err(AuthCeremonyErr::AuthenticatorAttachmentMismatch) 951 } else { 952 // We don't fail when we previously had [`AuthenticatorAttachment::None`]. 953 Ok(cur) 954 } 955 } 956 Self::Update(_) => Ok(cur), 957 Self::Ignore(_) => { 958 if matches!(prev, AuthenticatorAttachment::None) { 959 // We overwrite the previous one when it is [`AuthenticatorAttachment::None`]. 960 Ok(cur) 961 } else { 962 Ok(prev) 963 } 964 } 965 }, 966 AuthenticatorAttachment::CrossPlatform => match self { 967 Self::Fail(_) => { 968 if matches!(prev, AuthenticatorAttachment::Platform) { 969 Err(AuthCeremonyErr::AuthenticatorAttachmentMismatch) 970 } else { 971 // We don't fail when we previously had [`AuthenticatorAttachment::None`]. 972 Ok(cur) 973 } 974 } 975 Self::Update(_) => Ok(cur), 976 Self::Ignore(_) => { 977 if matches!(prev, AuthenticatorAttachment::None) { 978 // We overwrite the previous one when it is [`AuthenticatorAttachment::None`]. 979 Ok(cur) 980 } else { 981 Ok(prev) 982 } 983 } 984 }, 985 } 986 } 987 } 988 impl Default for AuthenticatorAttachmentEnforcement { 989 /// Returns [`Self::Ignore`] containing `false`. 990 #[inline] 991 fn default() -> Self { 992 Self::Ignore(false) 993 } 994 } 995 /// Additional verification options to perform in [`DiscoverableAuthenticationServerState::verify`] and 996 /// [`NonDiscoverableAuthenticationServerState::verify`]. 997 #[derive(Clone, Copy, Debug)] 998 pub struct AuthenticationVerificationOptions<'origins, 'top_origins, O, T> { 999 /// Origins to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin). 1000 /// 1001 /// When this is empty, the origin that will be used will be based on the [`RpId`] passed to 1002 /// [`DiscoverableAuthenticationServerState::verify`] or [`NonDiscoverableAuthenticationServerState::verify`]. 1003 /// If [`RpId::Domain`] or [`RpId::StaticDomain`], then the [`DomainOrigin`] returned from passing 1004 /// [`AsciiDomain::as_ref`] and [`AsciiDomainStatic::as_str`] to [`DomainOrigin::new`] respectively will be 1005 /// used; otherwise the [`Url`] in [`RpId::Url`] will be used. 1006 pub allowed_origins: &'origins [O], 1007 /// [Top-level origins](https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-top-level-origin) 1008 /// to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin). 1009 /// 1010 /// When this is `Some`, [`CollectedClientData::cross_origin`] is allowed to be `true`. When the contained 1011 /// `slice` is empty, [`CollectedClientData::top_origin`] must be `None`. When this is `None`, 1012 /// `CollectedClientData::cross_origin` must be `false` and `CollectedClientData::top_origin` must be `None`. 1013 pub allowed_top_origins: Option<&'top_origins [T]>, 1014 /// The required [`Backup`] state of the credential. 1015 /// 1016 /// Note that `None` is _not_ the same as `Some(BackupReq::None)` as the latter indicates that any [`Backup`] 1017 /// is allowed. This is rarely what you want; instead, `None` indicates that [`BackupReq::from`] applied to 1018 /// [`DynamicState::backup`] will be used in [`DiscoverableAuthenticationServerState::verify`] and 1019 /// [`NonDiscoverableAuthenticationServerState::verify`] and 1020 pub backup_requirement: Option<BackupReq>, 1021 /// Error when unsolicited extensions are sent back iff `true`. 1022 pub error_on_unsolicited_extensions: bool, 1023 /// Dictates what happens when [`Authentication::authenticator_attachment`] is not the same as 1024 /// [`DynamicState::authenticator_attachment`]. 1025 pub auth_attachment_enforcement: AuthenticatorAttachmentEnforcement, 1026 /// [`DynamicState::user_verified`] will be set to `true` iff [`Flag::user_verified`] when `true`. 1027 pub update_uv: bool, 1028 /// Dictates what happens when [`AuthenticatorData::sign_count`] is not updated to a strictly greater value. 1029 pub sig_counter_enforcement: SignatureCounterEnforcement, 1030 /// [`CollectedClientData::from_client_data_json_relaxed`] is used to extract [`CollectedClientData`] iff `true`. 1031 #[cfg(feature = "serde_relaxed")] 1032 pub client_data_json_relaxed: bool, 1033 } 1034 impl<O, T> AuthenticationVerificationOptions<'_, '_, O, T> { 1035 /// Returns `Self` such that [`Self::allowed_origins`] is empty, [`Self::allowed_top_origins`] is `None`, 1036 /// [`Self::backup_requirement`] is `None`, [`Self::error_on_unsolicited_extensions`] is `true`, 1037 /// [`Self::auth_attachment_enforcement`] is [`AuthenticatorAttachmentEnforcement::default`], 1038 /// [`Self::update_uv`] is `false`, [`Self::sig_counter_enforcement`] is 1039 /// [`SignatureCounterEnforcement::default`], and [`Self::client_data_json_relaxed`] is `true`. 1040 /// 1041 /// Note `O` and `T` should implement `PartialEq<Origin<'_>>` (e.g., `&str`). 1042 #[inline] 1043 #[must_use] 1044 pub const fn new() -> Self { 1045 Self { 1046 allowed_origins: [].as_slice(), 1047 allowed_top_origins: None, 1048 backup_requirement: None, 1049 error_on_unsolicited_extensions: true, 1050 auth_attachment_enforcement: AuthenticatorAttachmentEnforcement::Ignore(false), 1051 update_uv: false, 1052 sig_counter_enforcement: SignatureCounterEnforcement::Fail, 1053 #[cfg(feature = "serde_relaxed")] 1054 client_data_json_relaxed: true, 1055 } 1056 } 1057 } 1058 impl<O, T> Default for AuthenticationVerificationOptions<'_, '_, O, T> { 1059 /// Same as [`Self::new`]. 1060 #[inline] 1061 fn default() -> Self { 1062 Self::new() 1063 } 1064 } 1065 // This is essentially the `DiscoverableCredentialRequestOptions` used to create it; however to reduce 1066 // memory usage, we remove all unnecessary data making an instance of this 48 bytes in size 1067 // `x86_64-unknown-linux-gnu` platforms. 1068 // 1069 /// State needed to be saved when beginning the authentication ceremony. 1070 /// 1071 /// Saves the necessary information associated with the [`DiscoverableCredentialRequestOptions`] used to create it 1072 /// via [`DiscoverableCredentialRequestOptions::start_ceremony`] so that authentication of a credential can be 1073 /// performed with [`Self::verify`]. 1074 /// 1075 /// `DiscoverableAuthenticationServerState` implements [`Borrow`] of [`SentChallenge`]; thus to obtain the correct 1076 /// `DiscoverableAuthenticationServerState` associated with a [`DiscoverableAuthentication`], one should use its 1077 /// corresponding [`DiscoverableAuthentication::challenge`]. 1078 #[derive(Debug)] 1079 pub struct DiscoverableAuthenticationServerState(AuthenticationServerState); 1080 impl DiscoverableAuthenticationServerState { 1081 /// Verifies `response` is valid based on `self` consuming `self` and updating `cred`. Returns `true` 1082 /// iff `cred` was mutated. 1083 /// 1084 /// `rp_id` MUST be the same as the [`PublicKeyCredentialRequestOptions::rp_id`] used when starting the 1085 /// ceremony. 1086 /// 1087 /// It is _essential_ to save [`AuthenticatedCredential::dynamic_state`] overwriting the original value iff `Ok(true)` 1088 /// is returned. 1089 /// 1090 /// # Errors 1091 /// 1092 /// Errors iff `response` is not valid according to the 1093 /// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) or violates any 1094 /// of the settings in `options`. 1095 #[inline] 1096 pub fn verify< 1097 'a, 1098 const USER_LEN: usize, 1099 O: PartialEq<Origin<'a>>, 1100 T: PartialEq<Origin<'a>>, 1101 MlDsa87Key: AsRef<[u8]>, 1102 MlDsa65Key: AsRef<[u8]>, 1103 MlDsa44Key: AsRef<[u8]>, 1104 EdKey: AsRef<[u8]>, 1105 P256Key: AsRef<[u8]>, 1106 P384Key: AsRef<[u8]>, 1107 RsaKey: AsRef<[u8]>, 1108 >( 1109 self, 1110 rp_id: &RpId, 1111 response: &'a DiscoverableAuthentication<USER_LEN>, 1112 cred: &mut AuthenticatedCredential< 1113 '_, 1114 '_, 1115 USER_LEN, 1116 CompressedPubKey<MlDsa87Key, MlDsa65Key, MlDsa44Key, EdKey, P256Key, P384Key, RsaKey>, 1117 >, 1118 options: &AuthenticationVerificationOptions<'_, '_, O, T>, 1119 ) -> Result<bool, AuthCeremonyErr> { 1120 // Step 6 item 2. 1121 if cred.user_id == response.response.user_handle() { 1122 // Step 6 item 2. 1123 if cred.id.as_ref() == response.raw_id.as_ref() { 1124 self.0.verify(rp_id, response, cred, options, None) 1125 } else { 1126 Err(AuthCeremonyErr::CredentialIdMismatch) 1127 } 1128 } else { 1129 Err(AuthCeremonyErr::UserHandleMismatch) 1130 } 1131 } 1132 #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))] 1133 fn is_eq(&self, other: &Self) -> bool { 1134 self.0.is_eq(&other.0) 1135 } 1136 } 1137 // This is essentially the `NonDiscoverableCredentialRequestOptions` used to create it; however to reduce 1138 // memory usage, we remove all unnecessary data making an instance of this as small as 64 bytes in size on 1139 // `x86_64-unknown-linux-gnu` platforms. This does not include the size of each `CredInfo` which should exist 1140 // elsewhere on the heap but obviously contributes memory overall. 1141 /// State needed to be saved when beginning the authentication ceremony. 1142 /// 1143 /// Saves the necessary information associated with the [`NonDiscoverableCredentialRequestOptions`] used to create 1144 /// it via [`NonDiscoverableCredentialRequestOptions::start_ceremony`] so that authentication of a credential can be 1145 /// performed with [`Self::verify`]. 1146 /// 1147 /// `NonDiscoverableAuthenticationServerState` implements [`Borrow`] of [`SentChallenge`]; thus to obtain the 1148 /// correct `NonDiscoverableAuthenticationServerState` associated with a [`NonDiscoverableAuthentication`], one 1149 /// should use its corresponding [`NonDiscoverableAuthentication::challenge`]. 1150 #[derive(Debug)] 1151 pub struct NonDiscoverableAuthenticationServerState { 1152 /// Most server state. 1153 state: AuthenticationServerState, 1154 /// The set of credentials that are allowed. 1155 allow_credentials: Box<[CredInfo]>, 1156 } 1157 impl NonDiscoverableAuthenticationServerState { 1158 /// Verifies `response` is valid based on `self` consuming `self` and updating `cred`. Returns `true` 1159 /// iff `cred` was mutated. 1160 /// 1161 /// `rp_id` MUST be the same as the [`PublicKeyCredentialRequestOptions::rp_id`] used when starting the 1162 /// ceremony. 1163 /// 1164 /// It is _essential_ to save [`AuthenticatedCredential::dynamic_state`] overwriting the original value iff `Ok(true)` 1165 /// is returned. 1166 /// 1167 /// # Errors 1168 /// 1169 /// Errors iff `response` is not valid according to the 1170 /// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) or violates any 1171 /// of the settings in `options`. 1172 #[inline] 1173 pub fn verify< 1174 'a, 1175 const USER_LEN: usize, 1176 O: PartialEq<Origin<'a>>, 1177 T: PartialEq<Origin<'a>>, 1178 MlDsa87Key: AsRef<[u8]>, 1179 MlDsa65Key: AsRef<[u8]>, 1180 MlDsa44Key: AsRef<[u8]>, 1181 EdKey: AsRef<[u8]>, 1182 P256Key: AsRef<[u8]>, 1183 P384Key: AsRef<[u8]>, 1184 RsaKey: AsRef<[u8]>, 1185 >( 1186 self, 1187 rp_id: &RpId, 1188 response: &'a NonDiscoverableAuthentication<USER_LEN>, 1189 cred: &mut AuthenticatedCredential< 1190 '_, 1191 '_, 1192 USER_LEN, 1193 CompressedPubKey<MlDsa87Key, MlDsa65Key, MlDsa44Key, EdKey, P256Key, P384Key, RsaKey>, 1194 >, 1195 options: &AuthenticationVerificationOptions<'_, '_, O, T>, 1196 ) -> Result<bool, AuthCeremonyErr> { 1197 response 1198 .response 1199 .user_handle() 1200 .as_ref() 1201 .map_or(Ok(()), |user| { 1202 // Step 6 item 1. 1203 if *user == cred.user_id() { 1204 Ok(()) 1205 } else { 1206 Err(AuthCeremonyErr::UserHandleMismatch) 1207 } 1208 }) 1209 .and_then(|()| { 1210 self.allow_credentials 1211 .iter() 1212 // Step 5. 1213 .find(|c| c.id == response.raw_id) 1214 .ok_or(AuthCeremonyErr::NoMatchingAllowedCredential) 1215 .and_then(|c| { 1216 // Step 6 item 1. 1217 if c.id.as_ref() == cred.id.as_ref() { 1218 self.state 1219 .verify(rp_id, response, cred, options, Some(c.ext)) 1220 } else { 1221 Err(AuthCeremonyErr::CredentialIdMismatch) 1222 } 1223 }) 1224 }) 1225 } 1226 #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))] 1227 fn is_eq(&self, other: &Self) -> bool { 1228 self.state.is_eq(&other.state) && self.allow_credentials == other.allow_credentials 1229 } 1230 } 1231 // This is essentially the `PublicKeyCredentialRequestOptions` used to create it; however to reduce 1232 // memory usage, we remove all unnecessary data making an instance of this 48 bytes in size on 1233 // `x86_64-unknown-linux-gnu` platforms. 1234 /// Shared state used by [`DiscoverableAuthenticationServerState`] and [`NonDiscoverableAuthenticationServerState`]. 1235 #[derive(Debug)] 1236 struct AuthenticationServerState { 1237 // This is a `SentChallenge` since we need `AuthenticationServerState` to be fetchable after receiving the 1238 // response from the client. This response must obviously be constructable; thus its challenge is a 1239 // `SentChallenge`. 1240 // 1241 // This must never be mutated since we want to ensure it is actually a `Challenge` (which 1242 // can only be constructed via `Challenge::new`). This is guaranteed to be true iff 1243 // `serializable_server_state` is not enabled. 1244 /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge). 1245 challenge: SentChallenge, 1246 /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification). 1247 user_verification: UserVerificationRequirement, 1248 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-extensions). 1249 extensions: ServerExtensionInfo, 1250 /// `Instant` the ceremony expires. 1251 #[cfg(not(feature = "serializable_server_state"))] 1252 expiration: Instant, 1253 /// `SystemTime` the ceremony expires. 1254 #[cfg(feature = "serializable_server_state")] 1255 expiration: SystemTime, 1256 } 1257 impl AuthenticationServerState { 1258 /// Verifies `response` is valid based on `self` consuming `self` and updating `cred`. Returns `true` 1259 /// iff `cred` was mutated. 1260 /// 1261 /// `rp_id` MUST be the same as the [`PublicKeyCredentialRequestOptions::rp_id`] used when starting the 1262 /// ceremony. 1263 /// 1264 /// It is _essential_ to save [`AuthenticatedCredential::dynamic_state`] overwriting the original value iff `Ok(true)` 1265 /// is returned. 1266 /// 1267 /// # Errors 1268 /// 1269 /// Errors iff `response` is not valid according to the 1270 /// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) or violates any 1271 /// of the settings in `options`. 1272 fn verify< 1273 'a, 1274 const USER_LEN: usize, 1275 const DISCOVERABLE: bool, 1276 O: PartialEq<Origin<'a>>, 1277 T: PartialEq<Origin<'a>>, 1278 MlDsa87Key: AsRef<[u8]>, 1279 MlDsa65Key: AsRef<[u8]>, 1280 MlDsa44Key: AsRef<[u8]>, 1281 EdKey: AsRef<[u8]>, 1282 P256Key: AsRef<[u8]>, 1283 P384Key: AsRef<[u8]>, 1284 RsaKey: AsRef<[u8]>, 1285 >( 1286 self, 1287 rp_id: &RpId, 1288 response: &'a Authentication<USER_LEN, DISCOVERABLE>, 1289 cred: &mut AuthenticatedCredential< 1290 '_, 1291 '_, 1292 USER_LEN, 1293 CompressedPubKey<MlDsa87Key, MlDsa65Key, MlDsa44Key, 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, CompressedPubKeyOwned, 1626 CredentialProtectionPolicy, Ed25519PubKey, 1627 }, 1628 }, 1629 }, 1630 AuthCeremonyErr, AuthenticationVerificationOptions, AuthenticatorAttachment, 1631 AuthenticatorAttachmentEnforcement, DiscoverableAuthentication, ExtensionErr, OneOrTwo, 1632 PrfInput, SignatureCounterEnforcement, 1633 }; 1634 #[cfg(all( 1635 feature = "custom", 1636 any( 1637 feature = "serializable_server_state", 1638 not(any(feature = "bin", feature = "serde")) 1639 ) 1640 ))] 1641 use super::{ 1642 super::{super::AggErr, Challenge, CredentialId, RpId, UserVerificationRequirement}, 1643 DiscoverableCredentialRequestOptions, ExtensionReq, 1644 }; 1645 #[cfg(all(feature = "custom", feature = "serializable_server_state"))] 1646 use super::{ 1647 super::{ 1648 super::bin::{Decode as _, Encode as _}, 1649 AsciiDomain, AuthTransports, 1650 }, 1651 AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Credentials as _, 1652 DiscoverableAuthenticationServerState, Extension, NonDiscoverableAuthenticationServerState, 1653 NonDiscoverableCredentialRequestOptions, PrfInputOwned, PublicKeyCredentialDescriptor, 1654 }; 1655 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1656 use ed25519_dalek::{Signer as _, SigningKey}; 1657 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1658 use rsa::sha2::{Digest as _, Sha256}; 1659 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1660 const CBOR_BYTES: u8 = 0b010_00000; 1661 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1662 const CBOR_TEXT: u8 = 0b011_00000; 1663 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1664 const CBOR_MAP: u8 = 0b101_00000; 1665 #[expect(clippy::panic_in_result_fn, reason = "OK in tests")] 1666 #[test] 1667 #[cfg(all(feature = "custom", feature = "serializable_server_state"))] 1668 fn eddsa_auth_ser() -> Result<(), AggErr> { 1669 let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); 1670 let mut creds = AllowedCredentials::with_capacity(1); 1671 _ = creds.push(AllowedCredential { 1672 credential: PublicKeyCredentialDescriptor { 1673 id: CredentialId::try_from(vec![0; 16].into_boxed_slice())?, 1674 transports: AuthTransports::NONE, 1675 }, 1676 extension: CredentialSpecificExtension { 1677 prf: Some(PrfInputOwned { 1678 first: Vec::new(), 1679 second: Some(Vec::new()), 1680 ext_req: ExtensionReq::Require, 1681 }), 1682 }, 1683 }); 1684 let mut opts = NonDiscoverableCredentialRequestOptions::second_factor(&rp_id, creds); 1685 opts.options.user_verification = UserVerificationRequirement::Required; 1686 opts.options.challenge = Challenge(0); 1687 opts.options.extensions = Extension { prf: None }; 1688 let server = opts.start_ceremony()?.0; 1689 let enc_data = server.encode()?; 1690 assert_eq!(enc_data.capacity(), 16 + 2 + 1 + 1 + 12 + 2 + 128 + 1); 1691 assert_eq!(enc_data.len(), 16 + 2 + 1 + 1 + 12 + 2 + 16 + 2); 1692 assert!( 1693 server.is_eq(&NonDiscoverableAuthenticationServerState::decode( 1694 enc_data.as_slice() 1695 )?) 1696 ); 1697 let mut opts_2 = DiscoverableCredentialRequestOptions::passkey(&rp_id); 1698 opts_2.public_key.challenge = Challenge(0); 1699 opts_2.public_key.extensions = Extension { prf: None }; 1700 let server_2 = opts_2.start_ceremony()?.0; 1701 let enc_data_2 = server_2 1702 .encode() 1703 .map_err(AggErr::EncodeDiscoverableAuthenticationServerState)?; 1704 assert_eq!(enc_data_2.capacity(), enc_data_2.len()); 1705 assert_eq!(enc_data_2.len(), 16 + 1 + 1 + 12); 1706 assert!( 1707 server_2.is_eq(&DiscoverableAuthenticationServerState::decode( 1708 enc_data_2.as_slice() 1709 )?) 1710 ); 1711 Ok(()) 1712 } 1713 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1714 #[derive(Clone, Copy)] 1715 struct TestResponseOptions { 1716 user_verified: bool, 1717 hmac: HmacSecret, 1718 } 1719 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1720 #[derive(Clone, Copy)] 1721 enum PrfCredOptions { 1722 None, 1723 FalseNoHmac, 1724 FalseHmacFalse, 1725 TrueNoHmac, 1726 TrueHmacTrue, 1727 } 1728 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1729 #[derive(Clone, Copy)] 1730 struct TestCredOptions { 1731 cred_protect: CredentialProtectionPolicy, 1732 prf: PrfCredOptions, 1733 } 1734 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1735 #[derive(Clone, Copy)] 1736 enum PrfUvOptions { 1737 /// `true` iff `UserVerificationRequirement::Required` should be used; otherwise 1738 /// `UserVerificationRequirement::Preferred` is used. 1739 None(bool), 1740 Prf((PrfInput<'static, 'static>, ExtensionReq)), 1741 } 1742 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1743 #[derive(Clone, Copy)] 1744 struct TestRequestOptions { 1745 error_unsolicited: bool, 1746 prf_uv: PrfUvOptions, 1747 } 1748 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1749 #[derive(Clone, Copy)] 1750 struct TestOptions { 1751 request: TestRequestOptions, 1752 response: TestResponseOptions, 1753 cred: TestCredOptions, 1754 } 1755 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1756 fn generate_client_data_json() -> Vec<u8> { 1757 let mut json = Vec::with_capacity(256); 1758 json.extend_from_slice(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()); 1759 json 1760 } 1761 #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] 1762 #[expect(clippy::too_many_lines, reason = "a lot to test")] 1763 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1764 fn generate_authenticator_data_public_key_sig( 1765 opts: TestResponseOptions, 1766 ) -> (Vec<u8>, CompressedPubKeyOwned, Vec<u8>) { 1767 let mut authenticator_data = Vec::with_capacity(256); 1768 authenticator_data.extend_from_slice( 1769 [ 1770 // RP ID HASH. 1771 // This will be overwritten later. 1772 0, 1773 0, 1774 0, 1775 0, 1776 0, 1777 0, 1778 0, 1779 0, 1780 0, 1781 0, 1782 0, 1783 0, 1784 0, 1785 0, 1786 0, 1787 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 // FLAGS. 1805 // UP, UV, AT, and ED (right-to-left). 1806 0b0000_0001 1807 | if opts.user_verified { 1808 0b0000_0100 1809 } else { 1810 0b0000_0000 1811 } 1812 | if matches!(opts.hmac, HmacSecret::None) { 1813 0 1814 } else { 1815 0b1000_0000 1816 }, 1817 // COUNTER. 1818 // 0 as 32-bit big endian. 1819 0, 1820 0, 1821 0, 1822 0, 1823 ] 1824 .as_slice(), 1825 ); 1826 authenticator_data[..32].copy_from_slice(&Sha256::digest(b"example.com")); 1827 match opts.hmac { 1828 HmacSecret::None => {} 1829 HmacSecret::One => { 1830 authenticator_data.extend_from_slice( 1831 [ 1832 CBOR_MAP | 1, 1833 // CBOR text of length 11. 1834 CBOR_TEXT | 11, 1835 b'h', 1836 b'm', 1837 b'a', 1838 b'c', 1839 b'-', 1840 b's', 1841 b'e', 1842 b'c', 1843 b'r', 1844 b'e', 1845 b't', 1846 CBOR_BYTES | 24, 1847 48, 1848 ] 1849 .as_slice(), 1850 ); 1851 authenticator_data.extend_from_slice([0; 48].as_slice()); 1852 } 1853 HmacSecret::Two => { 1854 authenticator_data.extend_from_slice( 1855 [ 1856 CBOR_MAP | 1, 1857 // CBOR text of length 11. 1858 CBOR_TEXT | 11, 1859 b'h', 1860 b'm', 1861 b'a', 1862 b'c', 1863 b'-', 1864 b's', 1865 b'e', 1866 b'c', 1867 b'r', 1868 b'e', 1869 b't', 1870 CBOR_BYTES | 24, 1871 80, 1872 ] 1873 .as_slice(), 1874 ); 1875 authenticator_data.extend_from_slice([0; 80].as_slice()); 1876 } 1877 } 1878 let len = authenticator_data.len(); 1879 authenticator_data 1880 .extend_from_slice(&Sha256::digest(generate_client_data_json().as_slice())); 1881 let sig_key = SigningKey::from_bytes(&[0; 32]); 1882 let sig = sig_key.sign(authenticator_data.as_slice()).to_vec(); 1883 authenticator_data.truncate(len); 1884 ( 1885 authenticator_data, 1886 CompressedPubKeyOwned::Ed25519(Ed25519PubKey::from(sig_key.verifying_key().to_bytes())), 1887 sig, 1888 ) 1889 } 1890 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1891 fn validate(options: TestOptions) -> Result<(), AggErr> { 1892 let rp_id = RpId::Domain("example.com".to_owned().try_into()?); 1893 let user = UserHandle::from([0; 1]); 1894 let (authenticator_data, credential_public_key, signature) = 1895 generate_authenticator_data_public_key_sig(options.response); 1896 let credential_id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?; 1897 let authentication = DiscoverableAuthentication::new( 1898 credential_id.clone(), 1899 DiscoverableAuthenticatorAssertion::new( 1900 generate_client_data_json(), 1901 authenticator_data, 1902 signature, 1903 user, 1904 ), 1905 AuthenticatorAttachment::None, 1906 ); 1907 let auth_opts = AuthenticationVerificationOptions::<'static, 'static, &str, &str> { 1908 allowed_origins: [].as_slice(), 1909 allowed_top_origins: None, 1910 auth_attachment_enforcement: AuthenticatorAttachmentEnforcement::Update(false), 1911 backup_requirement: None, 1912 error_on_unsolicited_extensions: options.request.error_unsolicited, 1913 sig_counter_enforcement: SignatureCounterEnforcement::Fail, 1914 update_uv: false, 1915 #[cfg(feature = "serde_relaxed")] 1916 client_data_json_relaxed: false, 1917 }; 1918 let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id); 1919 opts.public_key.challenge = Challenge(0); 1920 opts.public_key.user_verification = UserVerificationRequirement::Preferred; 1921 match options.request.prf_uv { 1922 PrfUvOptions::None(required) => { 1923 if required { 1924 opts.public_key.user_verification = UserVerificationRequirement::Required; 1925 } 1926 } 1927 PrfUvOptions::Prf(input) => { 1928 opts.public_key.user_verification = UserVerificationRequirement::Required; 1929 opts.public_key.extensions.prf = Some(input); 1930 } 1931 } 1932 let mut cred = AuthenticatedCredential::new( 1933 (&credential_id).into(), 1934 &user, 1935 StaticState { 1936 credential_public_key, 1937 extensions: AuthenticatorExtensionOutputStaticState { 1938 cred_protect: options.cred.cred_protect, 1939 hmac_secret: match options.cred.prf { 1940 PrfCredOptions::None 1941 | PrfCredOptions::FalseNoHmac 1942 | PrfCredOptions::TrueNoHmac => None, 1943 PrfCredOptions::FalseHmacFalse => Some(false), 1944 PrfCredOptions::TrueHmacTrue => Some(true), 1945 }, 1946 }, 1947 client_extension_results: ClientExtensionsOutputsStaticState { 1948 prf: match options.cred.prf { 1949 PrfCredOptions::None => None, 1950 PrfCredOptions::FalseNoHmac | PrfCredOptions::FalseHmacFalse => { 1951 Some(AuthenticationExtensionsPrfOutputs { enabled: false }) 1952 } 1953 PrfCredOptions::TrueNoHmac | PrfCredOptions::TrueHmacTrue => { 1954 Some(AuthenticationExtensionsPrfOutputs { enabled: true }) 1955 } 1956 }, 1957 }, 1958 }, 1959 DynamicState { 1960 user_verified: true, 1961 backup: Backup::NotEligible, 1962 sign_count: 0, 1963 authenticator_attachment: AuthenticatorAttachment::None, 1964 }, 1965 )?; 1966 opts.start_ceremony()? 1967 .0 1968 .verify(&rp_id, &authentication, &mut cred, &auth_opts) 1969 .map_err(AggErr::AuthCeremony) 1970 .map(|_| ()) 1971 } 1972 /// Test all, and only, possible `UserNotVerified` errors. 1973 /// 4 * 5 * 3 * 2 * 5 = 600 tests. 1974 /// We ignore this due to how long it takes (around 4 seconds or so). 1975 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 1976 #[ignore = "slow"] 1977 #[test] 1978 fn uv_required_err() { 1979 const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [ 1980 CredentialProtectionPolicy::None, 1981 CredentialProtectionPolicy::UserVerificationOptional, 1982 CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, 1983 CredentialProtectionPolicy::UserVerificationRequired, 1984 ]; 1985 const ALL_PRF_CRED_OPTIONS: [PrfCredOptions; 5] = [ 1986 PrfCredOptions::None, 1987 PrfCredOptions::FalseNoHmac, 1988 PrfCredOptions::FalseHmacFalse, 1989 PrfCredOptions::TrueNoHmac, 1990 PrfCredOptions::TrueHmacTrue, 1991 ]; 1992 const ALL_HMAC_OPTIONS: [HmacSecret; 3] = 1993 [HmacSecret::None, HmacSecret::One, HmacSecret::Two]; 1994 const ALL_UNSOLICIT_OPTIONS: [bool; 2] = [false, true]; 1995 const ALL_NOT_FALSE_PRF_UV_OPTIONS: [PrfUvOptions; 5] = [ 1996 PrfUvOptions::None(true), 1997 PrfUvOptions::Prf(( 1998 PrfInput { 1999 first: [].as_slice(), 2000 second: None, 2001 }, 2002 ExtensionReq::Require, 2003 )), 2004 PrfUvOptions::Prf(( 2005 PrfInput { 2006 first: [].as_slice(), 2007 second: None, 2008 }, 2009 ExtensionReq::Allow, 2010 )), 2011 PrfUvOptions::Prf(( 2012 PrfInput { 2013 first: [].as_slice(), 2014 second: Some([].as_slice()), 2015 }, 2016 ExtensionReq::Require, 2017 )), 2018 PrfUvOptions::Prf(( 2019 PrfInput { 2020 first: [].as_slice(), 2021 second: Some([].as_slice()), 2022 }, 2023 ExtensionReq::Allow, 2024 )), 2025 ]; 2026 for cred_protect in ALL_CRED_PROTECT_OPTIONS { 2027 for prf in ALL_PRF_CRED_OPTIONS { 2028 for hmac in ALL_HMAC_OPTIONS { 2029 for error_unsolicited in ALL_UNSOLICIT_OPTIONS { 2030 for prf_uv in ALL_NOT_FALSE_PRF_UV_OPTIONS { 2031 assert!(validate(TestOptions { 2032 request: TestRequestOptions { 2033 error_unsolicited, 2034 prf_uv, 2035 }, 2036 response: TestResponseOptions { 2037 user_verified: false, 2038 hmac, 2039 }, 2040 cred: TestCredOptions { cred_protect, prf, }, 2041 }).is_err_and(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::UserNotVerified)))); 2042 } 2043 } 2044 } 2045 } 2046 } 2047 } 2048 /// Test all, and only, possible `UserNotVerified` errors. 2049 /// 4 * 5 * 2 * 2 = 80 tests. 2050 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2051 #[test] 2052 fn forbidden_hmac() { 2053 const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [ 2054 CredentialProtectionPolicy::None, 2055 CredentialProtectionPolicy::UserVerificationOptional, 2056 CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, 2057 CredentialProtectionPolicy::UserVerificationRequired, 2058 ]; 2059 const ALL_PRF_CRED_OPTIONS: [PrfCredOptions; 5] = [ 2060 PrfCredOptions::None, 2061 PrfCredOptions::FalseNoHmac, 2062 PrfCredOptions::FalseHmacFalse, 2063 PrfCredOptions::TrueNoHmac, 2064 PrfCredOptions::TrueHmacTrue, 2065 ]; 2066 const ALL_HMAC_OPTIONS: [HmacSecret; 2] = [HmacSecret::One, HmacSecret::Two]; 2067 const ALL_UV_OPTIONS: [bool; 2] = [false, true]; 2068 for cred_protect in ALL_CRED_PROTECT_OPTIONS { 2069 for prf in ALL_PRF_CRED_OPTIONS { 2070 for hmac in ALL_HMAC_OPTIONS { 2071 for user_verified in ALL_UV_OPTIONS { 2072 assert!(validate(TestOptions { 2073 request: TestRequestOptions { 2074 error_unsolicited: true, 2075 prf_uv: PrfUvOptions::None(false), 2076 }, 2077 response: TestResponseOptions { 2078 user_verified, 2079 hmac, 2080 }, 2081 cred: TestCredOptions { cred_protect, prf, }, 2082 }).is_err_and(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::ForbiddenHmacSecret))))); 2083 } 2084 } 2085 } 2086 } 2087 } 2088 #[expect(clippy::panic_in_result_fn, reason = "OK in tests")] 2089 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2090 #[test] 2091 fn prf() -> Result<(), AggErr> { 2092 let mut opts = TestOptions { 2093 request: TestRequestOptions { 2094 error_unsolicited: false, 2095 prf_uv: PrfUvOptions::Prf(( 2096 PrfInput { 2097 first: [].as_slice(), 2098 second: None, 2099 }, 2100 ExtensionReq::Allow, 2101 )), 2102 }, 2103 response: TestResponseOptions { 2104 user_verified: true, 2105 hmac: HmacSecret::None, 2106 }, 2107 cred: TestCredOptions { 2108 cred_protect: CredentialProtectionPolicy::None, 2109 prf: PrfCredOptions::None, 2110 }, 2111 }; 2112 validate(opts)?; 2113 opts.request.prf_uv = PrfUvOptions::Prf(( 2114 PrfInput { 2115 first: [].as_slice(), 2116 second: None, 2117 }, 2118 ExtensionReq::Require, 2119 )); 2120 opts.cred.prf = PrfCredOptions::TrueHmacTrue; 2121 assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::MissingHmacSecret))))); 2122 opts.response.hmac = HmacSecret::One; 2123 opts.request.prf_uv = PrfUvOptions::Prf(( 2124 PrfInput { 2125 first: [].as_slice(), 2126 second: None, 2127 }, 2128 ExtensionReq::Allow, 2129 )); 2130 opts.cred.prf = PrfCredOptions::TrueNoHmac; 2131 assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::HmacSecretForNonHmacSecretCredential))))); 2132 opts.response.hmac = HmacSecret::Two; 2133 assert!(validate(opts).is_err_and(|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)))))); 2134 opts.response.hmac = HmacSecret::One; 2135 opts.cred.prf = PrfCredOptions::FalseNoHmac; 2136 assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::HmacSecretForNonHmacSecretCredential))))); 2137 opts.response.user_verified = false; 2138 opts.request.prf_uv = PrfUvOptions::None(false); 2139 opts.cred.prf = PrfCredOptions::TrueHmacTrue; 2140 assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::UserNotVerifiedHmacSecret))))); 2141 Ok(()) 2142 } 2143 }