register.rs (142830B)
1 extern crate alloc; 2 use super::{ 3 super::{ 4 DynamicState, Metadata, RegisteredCredential, StaticState, 5 response::{ 6 AuthenticatorAttachment, 7 register::{ 8 Attestation, AttestationFormat, AuthenticatorExtensionOutput, 9 ClientExtensionsOutputs, CredentialProtectionPolicy, HmacSecret, Registration, 10 UncompressedPubKey, 11 error::{ExtensionErr, RegCeremonyErr}, 12 }, 13 }, 14 }, 15 BackupReq, Ceremony, Challenge, CredentialMediationRequirement, ExtensionInfo, ExtensionReq, 16 FIVE_MINUTES, Hint, Origin, PrfInput, PublicKeyCredentialDescriptor, RpId, SentChallenge, 17 TimedCeremony, UserVerificationRequirement, 18 register::error::{CreationOptionsErr, NicknameErr, UsernameErr}, 19 }; 20 #[cfg(doc)] 21 use crate::{ 22 request::{ 23 AsciiDomain, AsciiDomainStatic, DomainOrigin, Url, 24 auth::{AuthenticationVerificationOptions, PublicKeyCredentialRequestOptions}, 25 }, 26 response::{AuthTransports, AuthenticatorTransport, Backup, CollectedClientData, Flag}, 27 }; 28 use alloc::borrow::Cow; 29 use core::{ 30 borrow::Borrow, 31 cmp::Ordering, 32 fmt::{self, Display, Formatter}, 33 hash::{Hash, Hasher}, 34 mem, 35 num::{NonZeroU32, NonZeroU64}, 36 time::Duration, 37 }; 38 use precis_profiles::{UsernameCasePreserved, precis_core::profile::Profile as _}; 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 functionality to (de)serialize data to a data store. 44 #[cfg_attr(docsrs, doc(cfg(feature = "bin")))] 45 #[cfg(feature = "bin")] 46 pub mod bin; 47 /// Contains functionality that needs to be accessible when `bin` or `serde` are not enabled. 48 #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] 49 #[cfg(feature = "custom")] 50 mod custom; 51 /// Contains error types. 52 pub mod error; 53 /// Contains functionality to (de)serialize data to a client. 54 #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] 55 #[cfg(feature = "serde")] 56 pub mod ser; 57 /// Contains functionality to (de)serialize [`RegistrationServerState`] to a data store. 58 #[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))] 59 #[cfg(feature = "serializable_server_state")] 60 pub mod ser_server_state; 61 /// Used by [`Extension::cred_protect`] to enforce the [`CredentialProtectionPolicy`] sent by the client via 62 /// [`Registration`]. 63 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] 64 pub enum CredProtect { 65 /// No `credProtect` request. 66 #[default] 67 None, 68 /// Request 69 /// [`userVerificationOptional`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationoptional) 70 /// but allow any. 71 /// 72 /// The `bool` corresponds to 73 /// [`enforceCredentialProtectionPolicy`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#dom-authenticationextensionsclientinputs-enforcecredentialprotectionpolicy). 74 UserVerificationOptional(bool, ExtensionInfo), 75 /// Request 76 /// [`userVerificationOptionalWithCredentialIDList`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationoptionalwithcredentialidlist); 77 /// and when enforcing the value, disallow [`CredentialProtectionPolicy::UserVerificationOptional`]. 78 /// 79 /// The `bool` corresponds to 80 /// [`enforceCredentialProtectionPolicy`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#dom-authenticationextensionsclientinputs-enforcecredentialprotectionpolicy). 81 UserVerificationOptionalWithCredentialIdList(bool, ExtensionInfo), 82 /// Request 83 /// [`userVerificationRequired`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationrequired); 84 /// and when enforcing the value, only allow [`CredentialProtectionPolicy::UserVerificationRequired`]. 85 /// 86 /// The `bool` corresponds to 87 /// [`enforceCredentialProtectionPolicy`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#dom-authenticationextensionsclientinputs-enforcecredentialprotectionpolicy). 88 UserVerificationRequired(bool, ExtensionInfo), 89 } 90 impl CredProtect { 91 /// Validates `other` is allowed based on `self`. 92 /// 93 /// # Errors 94 /// 95 /// Errors iff other is a less "secure" policy than `self` when enforcing the value or other does not exist 96 /// despite requiring a value to be sent back. 97 /// 98 /// Note a missing response is OK when enforcing a value. 99 const fn validate(self, other: CredentialProtectionPolicy) -> Result<(), ExtensionErr> { 100 match self { 101 Self::None => Ok(()), 102 Self::UserVerificationOptional(_, info) => { 103 if matches!(other, CredentialProtectionPolicy::None) { 104 if matches!( 105 info, 106 ExtensionInfo::RequireEnforceValue | ExtensionInfo::RequireDontEnforceValue 107 ) { 108 Err(ExtensionErr::MissingCredProtect) 109 } else { 110 Ok(()) 111 } 112 } else { 113 Ok(()) 114 } 115 } 116 Self::UserVerificationOptionalWithCredentialIdList(_, info) => match info { 117 ExtensionInfo::RequireEnforceValue => match other { 118 CredentialProtectionPolicy::None => Err(ExtensionErr::MissingCredProtect), 119 CredentialProtectionPolicy::UserVerificationOptional => { 120 Err(ExtensionErr::InvalidCredProtectValue(self, other)) 121 } 122 CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList 123 | CredentialProtectionPolicy::UserVerificationRequired => Ok(()), 124 }, 125 ExtensionInfo::RequireDontEnforceValue => { 126 if matches!(other, CredentialProtectionPolicy::None) { 127 Err(ExtensionErr::MissingCredProtect) 128 } else { 129 Ok(()) 130 } 131 } 132 ExtensionInfo::AllowEnforceValue => { 133 if matches!(other, CredentialProtectionPolicy::UserVerificationOptional) { 134 Err(ExtensionErr::InvalidCredProtectValue(self, other)) 135 } else { 136 Ok(()) 137 } 138 } 139 ExtensionInfo::AllowDontEnforceValue => Ok(()), 140 }, 141 Self::UserVerificationRequired(_, info) => match info { 142 ExtensionInfo::RequireEnforceValue => match other { 143 CredentialProtectionPolicy::None => Err(ExtensionErr::MissingCredProtect), 144 CredentialProtectionPolicy::UserVerificationOptional 145 | CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList => { 146 Err(ExtensionErr::InvalidCredProtectValue(self, other)) 147 } 148 CredentialProtectionPolicy::UserVerificationRequired => Ok(()), 149 }, 150 ExtensionInfo::RequireDontEnforceValue => { 151 if matches!(other, CredentialProtectionPolicy::None) { 152 Err(ExtensionErr::MissingCredProtect) 153 } else { 154 Ok(()) 155 } 156 } 157 ExtensionInfo::AllowEnforceValue => { 158 if matches!( 159 other, 160 CredentialProtectionPolicy::None 161 | CredentialProtectionPolicy::UserVerificationRequired 162 ) { 163 Ok(()) 164 } else { 165 Err(ExtensionErr::InvalidCredProtectValue(self, other)) 166 } 167 } 168 ExtensionInfo::AllowDontEnforceValue => Ok(()), 169 }, 170 } 171 } 172 } 173 impl Display for CredProtect { 174 #[inline] 175 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 176 match *self { 177 Self::None => f.write_str("do not sent a credProtect request"), 178 Self::UserVerificationOptional(enforce, info) => { 179 write!( 180 f, 181 "request user verification optional with enforcement {enforce} and {info}" 182 ) 183 } 184 Self::UserVerificationOptionalWithCredentialIdList(enforce, info) => write!( 185 f, 186 "user verification optional with credential ID list with enforcement {enforce} and {info}" 187 ), 188 Self::UserVerificationRequired(enforce, info) => { 189 write!( 190 f, 191 "user verification required with enforcement {enforce} and {info}" 192 ) 193 } 194 } 195 } 196 } 197 /// String returned from the [Nickname Enforcement rule](https://www.rfc-editor.org/rfc/rfc8266#section-2.3) 198 /// as defined in RFC 8266. 199 /// 200 /// Note [string truncation](https://www.w3.org/TR/webauthn-3/#sctn-strings-truncation) is allowed, so one may 201 /// want to enforce [`Self::RECOMMENDED_MAX_LEN`]. 202 #[derive(Clone, Debug)] 203 pub struct Nickname<'a>(Cow<'a, str>); 204 impl<'a> Nickname<'a> { 205 /// The maximum allowed length. 206 pub const MAX_LEN: usize = 1023; 207 /// The recommended maximum length to allow. 208 pub const RECOMMENDED_MAX_LEN: usize = 64; 209 /// Returns a `Nickname` that consumes `self`. When `self` owns the data, the data is simply moved; 210 /// when the data is borrowed, then it is cloned into an owned instance. 211 #[inline] 212 #[must_use] 213 pub fn into_owned<'b>(self) -> Nickname<'b> { 214 Nickname(Cow::Owned(self.0.into_owned())) 215 } 216 /// Same as [`Self::with_max_len`] except the length must not exceed [`Self::RECOMMENDED_MAX_LEN`] instead of 217 /// [`Self::MAX_LEN`]. 218 /// 219 /// # Errors 220 /// 221 /// Errors iff `value` violates [RFC 8266 § 2.3](https://www.rfc-editor.org/rfc/rfc8266.html#section-2.3) 222 /// or the resulting length exceeds [`Self::RECOMMENDED_MAX_LEN`]. 223 #[expect(single_use_lifetimes, reason = "false positive")] 224 #[inline] 225 pub fn with_recommended_len<'b: 'a>(value: Cow<'b, str>) -> Result<Self, NicknameErr> { 226 precis_profiles::Nickname::new() 227 .enforce(value) 228 .map_err(|_e| NicknameErr::Rfc8266) 229 .and_then(|val| { 230 if val.len() <= Self::RECOMMENDED_MAX_LEN { 231 Ok(Self(val)) 232 } else { 233 Err(NicknameErr::Len) 234 } 235 }) 236 } 237 /// Same as [`Self::try_from`]. 238 /// # Errors 239 /// 240 /// Errors iff `value` violates [RFC 8266 § 2.3](https://www.rfc-editor.org/rfc/rfc8266.html#section-2.3) 241 /// or the resulting length exceeds [`Self::MAX_LEN`]. 242 #[expect(single_use_lifetimes, reason = "false positive")] 243 #[inline] 244 pub fn with_max_len<'b: 'a>(value: Cow<'b, str>) -> Result<Self, NicknameErr> { 245 Self::try_from(value) 246 } 247 } 248 impl AsRef<str> for Nickname<'_> { 249 #[inline] 250 fn as_ref(&self) -> &str { 251 self.0.as_ref() 252 } 253 } 254 impl Borrow<str> for Nickname<'_> { 255 #[inline] 256 fn borrow(&self) -> &str { 257 self.0.as_ref() 258 } 259 } 260 impl<'a: 'b, 'b> From<&'a Nickname<'_>> for Nickname<'b> { 261 #[inline] 262 fn from(value: &'a Nickname<'_>) -> Self { 263 match value.0 { 264 Cow::Borrowed(val) => Self(Cow::Borrowed(val)), 265 Cow::Owned(ref val) => Self(Cow::Borrowed(val.as_str())), 266 } 267 } 268 } 269 impl<'a: 'b, 'b> From<Nickname<'a>> for Cow<'b, str> { 270 #[inline] 271 fn from(value: Nickname<'a>) -> Self { 272 value.0 273 } 274 } 275 impl<'a: 'b, 'b> TryFrom<Cow<'a, str>> for Nickname<'b> { 276 type Error = NicknameErr; 277 /// # Examples 278 /// 279 /// ``` 280 /// # use std::borrow::Cow; 281 /// # use webauthn_rp::request::register::{error::NicknameErr, Nickname}; 282 /// assert_eq!( 283 /// Nickname::try_from(Cow::Borrowed("Srinivasa Ramanujan"))?.as_ref(), 284 /// "Srinivasa Ramanujan" 285 /// ); 286 /// assert_eq!( 287 /// Nickname::try_from(Cow::Borrowed("श्रीनिवास रामानुजन्"))?.as_ref(), 288 /// "श्रीनिवास रामानुजन्" 289 /// ); 290 /// // Empty strings are not valid. 291 /// assert!(Nickname::try_from(Cow::Borrowed("")).map_or_else( 292 /// |e| matches!(e, NicknameErr::Rfc8266), 293 /// |_| false 294 /// )); 295 /// # Ok::<_, webauthn_rp::AggErr>(()) 296 /// ``` 297 #[inline] 298 fn try_from(value: Cow<'a, str>) -> Result<Self, Self::Error> { 299 precis_profiles::Nickname::new() 300 .enforce(value) 301 .map_err(|_e| NicknameErr::Rfc8266) 302 .and_then(|val| { 303 if val.len() <= Self::MAX_LEN { 304 Ok(Self(val)) 305 } else { 306 Err(NicknameErr::Len) 307 } 308 }) 309 } 310 } 311 impl<'a: 'b, 'b> TryFrom<&'a str> for Nickname<'b> { 312 type Error = NicknameErr; 313 /// Same as [`Nickname::try_from`] except the input is a `str`. 314 #[inline] 315 fn try_from(value: &'a str) -> Result<Self, Self::Error> { 316 Self::try_from(Cow::Borrowed(value)) 317 } 318 } 319 impl TryFrom<String> for Nickname<'_> { 320 type Error = NicknameErr; 321 /// Same as [`Nickname::try_from`] except the input is a `String`. 322 #[inline] 323 fn try_from(value: String) -> Result<Self, Self::Error> { 324 Self::try_from(Cow::Owned(value)) 325 } 326 } 327 impl PartialEq<Nickname<'_>> for Nickname<'_> { 328 #[inline] 329 fn eq(&self, other: &Nickname<'_>) -> bool { 330 self.0 == other.0 331 } 332 } 333 impl PartialEq<&Nickname<'_>> for Nickname<'_> { 334 #[inline] 335 fn eq(&self, other: &&Nickname<'_>) -> bool { 336 *self == **other 337 } 338 } 339 impl PartialEq<Nickname<'_>> for &Nickname<'_> { 340 #[inline] 341 fn eq(&self, other: &Nickname<'_>) -> bool { 342 **self == *other 343 } 344 } 345 impl Eq for Nickname<'_> {} 346 impl Hash for Nickname<'_> { 347 #[inline] 348 fn hash<H: Hasher>(&self, state: &mut H) { 349 self.0.hash(state); 350 } 351 } 352 impl PartialOrd<Nickname<'_>> for Nickname<'_> { 353 #[inline] 354 fn partial_cmp(&self, other: &Nickname<'_>) -> Option<Ordering> { 355 self.0.partial_cmp(&other.0) 356 } 357 } 358 impl Ord for Nickname<'_> { 359 #[inline] 360 fn cmp(&self, other: &Self) -> Ordering { 361 self.0.cmp(&other.0) 362 } 363 } 364 /// String returned from the 365 /// [UsernameCasePreserved Enforcement rule](https://www.rfc-editor.org/rfc/rfc8265#section-3.4.3) as defined in 366 /// RFC 8265. 367 /// 368 /// Note [string truncation](https://www.w3.org/TR/webauthn-3/#sctn-strings-truncation) is allowed, so one may 369 /// want to enforce [`Self::RECOMMENDED_MAX_LEN`]. 370 #[derive(Clone, Debug)] 371 pub struct Username<'a>(Cow<'a, str>); 372 impl<'a> Username<'a> { 373 /// The maximum allowed length. 374 pub const MAX_LEN: usize = 1023; 375 /// The recommended maximum length to allow. 376 pub const RECOMMENDED_MAX_LEN: usize = 64; 377 /// Returns a `Username` that consumes `self`. When `self` owns the data, the data is simply moved; 378 /// when the data is borrowed, then it is cloned into an owned instance. 379 #[inline] 380 #[must_use] 381 pub fn into_owned<'b>(self) -> Username<'b> { 382 Username(Cow::Owned(self.0.into_owned())) 383 } 384 /// Same as [`Self::with_max_len`] except the length must not exceed [`Self::RECOMMENDED_MAX_LEN`] instead of 385 /// [`Self::MAX_LEN`]. 386 /// 387 /// # Errors 388 /// 389 /// Errors iff `value` violates [RFC 8265 § 3.4.3](https://www.rfc-editor.org/rfc/rfc8265.html#section-3.4.3) 390 /// or the resulting length exceeds [`Self::RECOMMENDED_MAX_LEN`]. 391 #[expect(single_use_lifetimes, reason = "false positive")] 392 #[inline] 393 pub fn with_recommended_len<'b: 'a>(value: Cow<'b, str>) -> Result<Self, UsernameErr> { 394 UsernameCasePreserved::default() 395 .enforce(value) 396 .map_err(|_e| UsernameErr::Rfc8265) 397 .and_then(|val| { 398 if val.len() <= Self::RECOMMENDED_MAX_LEN { 399 Ok(Self(val)) 400 } else { 401 Err(UsernameErr::Len) 402 } 403 }) 404 } 405 /// Same as [`Self::try_from`]. 406 /// # Errors 407 /// 408 /// Errors iff `value` violates [RFC 8265 § 3.4.3](https://www.rfc-editor.org/rfc/rfc8265.html#section-3.4.3) 409 /// or the resulting length exceeds [`Self::MAX_LEN`]. 410 #[expect(single_use_lifetimes, reason = "false positive")] 411 #[inline] 412 pub fn with_max_len<'b: 'a>(value: Cow<'b, str>) -> Result<Self, UsernameErr> { 413 Self::try_from(value) 414 } 415 /// Returns `Self` containing `"blank"`. 416 #[expect(clippy::unreachable, reason = "want to crash when there is a bug")] 417 fn blank() -> Self { 418 Self::try_from("blank") 419 .unwrap_or_else(|_e| unreachable!("'blank' is no longer a valid Username")) 420 } 421 } 422 impl AsRef<str> for Username<'_> { 423 #[inline] 424 fn as_ref(&self) -> &str { 425 self.0.as_ref() 426 } 427 } 428 impl Borrow<str> for Username<'_> { 429 #[inline] 430 fn borrow(&self) -> &str { 431 self.0.as_ref() 432 } 433 } 434 impl<'a: 'b, 'b> TryFrom<Cow<'a, str>> for Username<'b> { 435 type Error = UsernameErr; 436 /// # Examples 437 /// 438 /// ``` 439 /// # use std::borrow::Cow; 440 /// # use webauthn_rp::request::register::{error::UsernameErr, Username}; 441 /// assert_eq!( 442 /// Username::try_from(Cow::Borrowed("leonhard.euler"))?.as_ref(), 443 /// "leonhard.euler" 444 /// ); 445 /// // Empty strings are not valid. 446 /// assert!(Username::try_from(Cow::Borrowed("")).map_or_else( 447 /// |e| matches!(e, UsernameErr::Rfc8265), 448 /// |_| false 449 /// )); 450 /// # Ok::<_, webauthn_rp::AggErr>(()) 451 /// ``` 452 #[inline] 453 fn try_from(value: Cow<'a, str>) -> Result<Self, Self::Error> { 454 UsernameCasePreserved::default() 455 .enforce(value) 456 .map_err(|_e| UsernameErr::Rfc8265) 457 .and_then(|val| { 458 if val.len() <= Self::MAX_LEN { 459 Ok(Self(val)) 460 } else { 461 Err(UsernameErr::Len) 462 } 463 }) 464 } 465 } 466 impl<'a: 'b, 'b> TryFrom<&'a str> for Username<'b> { 467 type Error = UsernameErr; 468 /// Same as [`Username::try_from`] except the input is a `str`. 469 #[inline] 470 fn try_from(value: &'a str) -> Result<Self, Self::Error> { 471 Self::try_from(Cow::Borrowed(value)) 472 } 473 } 474 impl TryFrom<String> for Username<'_> { 475 type Error = UsernameErr; 476 /// Same as [`Username::try_from`] except the input is a `String`. 477 #[inline] 478 fn try_from(value: String) -> Result<Self, Self::Error> { 479 Self::try_from(Cow::Owned(value)) 480 } 481 } 482 impl<'a: 'b, 'b> From<Username<'a>> for Cow<'b, str> { 483 #[inline] 484 fn from(value: Username<'a>) -> Self { 485 value.0 486 } 487 } 488 impl<'a: 'b, 'b> From<&'a Username<'_>> for Username<'b> { 489 #[inline] 490 fn from(value: &'a Username<'_>) -> Self { 491 match value.0 { 492 Cow::Borrowed(val) => Self(Cow::Borrowed(val)), 493 Cow::Owned(ref val) => Self(Cow::Borrowed(val.as_str())), 494 } 495 } 496 } 497 impl PartialEq<Username<'_>> for Username<'_> { 498 #[inline] 499 fn eq(&self, other: &Username<'_>) -> bool { 500 self.0 == other.0 501 } 502 } 503 impl PartialEq<&Username<'_>> for Username<'_> { 504 #[inline] 505 fn eq(&self, other: &&Username<'_>) -> bool { 506 *self == **other 507 } 508 } 509 impl PartialEq<Username<'_>> for &Username<'_> { 510 #[inline] 511 fn eq(&self, other: &Username<'_>) -> bool { 512 **self == *other 513 } 514 } 515 impl Eq for Username<'_> {} 516 impl Hash for Username<'_> { 517 #[inline] 518 fn hash<H: Hasher>(&self, state: &mut H) { 519 self.0.hash(state); 520 } 521 } 522 impl PartialOrd<Username<'_>> for Username<'_> { 523 #[inline] 524 fn partial_cmp(&self, other: &Username<'_>) -> Option<Ordering> { 525 self.0.partial_cmp(&other.0) 526 } 527 } 528 impl Ord for Username<'_> { 529 #[inline] 530 fn cmp(&self, other: &Self) -> Ordering { 531 self.0.cmp(&other.0) 532 } 533 } 534 /// [`COSEAlgorithmIdentifier`](https://www.w3.org/TR/webauthn-3/#typedefdef-cosealgorithmidentifier). 535 /// 536 /// Note the order of variants is the following: 537 /// 538 /// [`Self::Eddsa`] `<` [`Self::Es256`] `<` [`Self::Es384`] `<` [`Self::Rs256`]. 539 /// 540 /// This is relevant for [`CoseAlgorithmIdentifiers`]. For example a `CoseAlgorithmIdentifiers` 541 /// that contains `Self::Eddsa` will prioritize it over all others. 542 #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] 543 pub enum CoseAlgorithmIdentifier { 544 /// [EdDSA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms). 545 /// 546 /// Note that Ed25519 must be used for the `crv` parameter 547 /// [per the spec](https://www.w3.org/TR/webauthn-3/#sctn-alg-identifier). 548 Eddsa, 549 /// [ES256](https://www.iana.org/assignments/cose/cose.xhtml#algorithms). 550 /// 551 /// Note the uncompressed form must be used and P-256 must be used for the `crv` parameter 552 /// [per the spec](https://www.w3.org/TR/webauthn-3/#sctn-alg-identifier). 553 Es256, 554 /// [ES384](https://www.iana.org/assignments/cose/cose.xhtml#algorithms). 555 /// 556 /// Note the uncompressed form must be used and P-384 must be used for the `crv` parameter 557 /// [per the spec](https://www.w3.org/TR/webauthn-3/#sctn-alg-identifier). 558 Es384, 559 /// [RS256](https://www.iana.org/assignments/cose/cose.xhtml#algorithms). 560 Rs256, 561 } 562 impl CoseAlgorithmIdentifier { 563 /// Transforms `self` into a `u8`. 564 const fn to_u8(self) -> u8 { 565 match self { 566 Self::Eddsa => 1, 567 Self::Es256 => 2, 568 Self::Es384 => 4, 569 Self::Rs256 => 8, 570 } 571 } 572 } 573 impl PartialEq<&Self> for CoseAlgorithmIdentifier { 574 #[inline] 575 fn eq(&self, other: &&Self) -> bool { 576 *self == **other 577 } 578 } 579 impl PartialEq<CoseAlgorithmIdentifier> for &CoseAlgorithmIdentifier { 580 #[inline] 581 fn eq(&self, other: &CoseAlgorithmIdentifier) -> bool { 582 **self == *other 583 } 584 } 585 /// Non-empty ordered set of [`CoseAlgorithmIdentifier`]s. 586 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 587 pub struct CoseAlgorithmIdentifiers(u8); 588 impl CoseAlgorithmIdentifiers { 589 /// Contains all [`CoseAlgorithmIdentifier`]s. 590 pub const ALL: Self = Self(0) 591 .add(CoseAlgorithmIdentifier::Eddsa) 592 .add(CoseAlgorithmIdentifier::Es256) 593 .add(CoseAlgorithmIdentifier::Es384) 594 .add(CoseAlgorithmIdentifier::Rs256); 595 /// Returns a `CoseAlgorithmIdentifiers` containing all `CoseAlgorithmIdentifier`s in `self` plus `alg`. 596 const fn add(self, alg: CoseAlgorithmIdentifier) -> Self { 597 Self(self.0 | alg.to_u8()) 598 } 599 /// Returns a copy of `self` with `alg` removed if there would be at least one `CoseAlgorithmIdentifier` 600 /// remaining; otherwise returns `self`. 601 #[inline] 602 #[must_use] 603 pub const fn remove(self, alg: CoseAlgorithmIdentifier) -> Self { 604 let val = alg.to_u8(); 605 if self.0 == val { 606 self 607 } else { 608 Self(self.0 & !val) 609 } 610 } 611 /// Returns `true` iff `self` contains `alg`. 612 #[inline] 613 #[must_use] 614 pub const fn contains(self, alg: CoseAlgorithmIdentifier) -> bool { 615 let val = alg.to_u8(); 616 self.0 & val == val 617 } 618 /// Validates `other` is allowed based on `self`. 619 const fn validate(self, other: UncompressedPubKey<'_>) -> Result<(), RegCeremonyErr> { 620 if match other { 621 UncompressedPubKey::Ed25519(_) => self.contains(CoseAlgorithmIdentifier::Eddsa), 622 UncompressedPubKey::P256(_) => self.contains(CoseAlgorithmIdentifier::Es256), 623 UncompressedPubKey::P384(_) => self.contains(CoseAlgorithmIdentifier::Es384), 624 UncompressedPubKey::Rsa(_) => self.contains(CoseAlgorithmIdentifier::Rs256), 625 } { 626 Ok(()) 627 } else { 628 Err(RegCeremonyErr::PublicKeyAlgorithmMismatch) 629 } 630 } 631 } 632 impl Default for CoseAlgorithmIdentifiers { 633 /// Returns [`Self::ALL`]. 634 #[inline] 635 fn default() -> Self { 636 Self::ALL 637 } 638 } 639 /// Four to sixty-three inclusively. 640 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 641 #[repr(u8)] 642 pub enum FourToSixtyThree { 643 /// 4. 644 Four = 4, 645 /// 5. 646 Five, 647 /// 6. 648 Six, 649 /// 7. 650 Seven, 651 /// 8. 652 Eight, 653 /// 9. 654 Nine, 655 /// 10. 656 Ten, 657 /// 11. 658 Eleven, 659 /// 12. 660 Twelve, 661 /// 13. 662 Thirteen, 663 /// 14. 664 Fourteen, 665 /// 15. 666 Fifteen, 667 /// 16. 668 Sixteen, 669 /// 17. 670 Seventeen, 671 /// 18. 672 Eighteen, 673 /// 19. 674 Nineteen, 675 /// 20. 676 Twenty, 677 /// 21. 678 TwentyOne, 679 /// 22. 680 TwentyTwo, 681 /// 23. 682 TwentyThree, 683 /// 24. 684 TwentyFour, 685 /// 25. 686 TwentyFive, 687 /// 26. 688 TwentySix, 689 /// 27. 690 TwentySeven, 691 /// 28. 692 TwentyEight, 693 /// 29. 694 TwentyNine, 695 /// 30. 696 Thirty, 697 /// 31. 698 ThirtyOne, 699 /// 32. 700 ThirtyTwo, 701 /// 33. 702 ThirtyThree, 703 /// 34. 704 ThirtyFour, 705 /// 35. 706 ThirtyFive, 707 /// 36. 708 ThirtySix, 709 /// 37. 710 ThirtySeven, 711 /// 38. 712 ThirtyEight, 713 /// 39. 714 ThirtyNine, 715 /// 40. 716 Fourty, 717 /// 41. 718 FourtyOne, 719 /// 42. 720 FourtyTwo, 721 /// 43. 722 FourtyThree, 723 /// 44. 724 FourtyFour, 725 /// 45. 726 FourtyFive, 727 /// 46. 728 FourtySix, 729 /// 47. 730 FourtySeven, 731 /// 48. 732 FourtyEight, 733 /// 49. 734 FourtyNine, 735 /// 50. 736 Fifty, 737 /// 51. 738 FiftyOne, 739 /// 52. 740 FiftyTwo, 741 /// 53. 742 FiftyThree, 743 /// 54. 744 FiftyFour, 745 /// 55. 746 FiftyFive, 747 /// 56. 748 FiftySix, 749 /// 57. 750 FiftySeven, 751 /// 58. 752 FiftyEight, 753 /// 59. 754 FiftyNine, 755 /// 60. 756 Sixty, 757 /// 61. 758 SixtyOne, 759 /// 62. 760 SixtyTwo, 761 /// 63. 762 SixtyThree, 763 } 764 impl FourToSixtyThree { 765 /// Returns the equivalent `u8`. 766 #[expect(clippy::as_conversions, reason = "comment justifies correctness")] 767 #[inline] 768 #[must_use] 769 pub const fn into_u8(self) -> u8 { 770 // This is correct since `Self` is `repr(u8)`, and the initial discriminant has the value `4` 771 // and subsequent discriminants are implicitly incremented by 1. 772 self as u8 773 } 774 /// Returns `Some` representing `val` iff `val` is inclusively between 4 and 63. 775 #[expect(unsafe_code, reason = "comment justifies correctness")] 776 #[inline] 777 #[must_use] 778 pub const fn from_u8(val: u8) -> Option<Self> { 779 match val { 780 0..=3 | 64.. => None, 781 _ => { 782 // SAFETY: 783 // `val` is inclusively between 4 and 63, and `Self` is `repr(u8)`; thus this 784 // is safe and correct. 785 Some(unsafe { mem::transmute::<u8, Self>(val) }) 786 } 787 } 788 } 789 } 790 impl Display for FourToSixtyThree { 791 #[inline] 792 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 793 self.into_u8().fmt(f) 794 } 795 } 796 impl From<FourToSixtyThree> for u8 { 797 #[inline] 798 fn from(value: FourToSixtyThree) -> Self { 799 value.into_u8() 800 } 801 } 802 impl Default for FourToSixtyThree { 803 #[inline] 804 fn default() -> Self { 805 Self::Four 806 } 807 } 808 /// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client. 809 #[derive(Clone, Copy, Debug)] 810 pub struct Extension<'prf_first, 'prf_second> { 811 /// [`credProps`](https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension). 812 /// 813 /// The best one can do to ensure a server-side credential is created is by sending 814 /// [`ResidentKeyRequirement::Discouraged`]; however authenticators are still allowed 815 /// to create a client-side credential. To more definitively check that a server-side credential is 816 /// created, send this extension. Note that it can be difficult to impossible for a client/user agent to 817 /// know that a server-side credential is created; thus even when the response is 818 /// `Some(CredentialPropertiesOutput { rk: Some(false) })`, a client-side/"resident" credential could still 819 /// have been created. One may have better luck checking if [`AuthTransports::contains`] 820 /// [`AuthenticatorTransport::Internal`] and using that as an indicator if a client-side credential was created. 821 /// 822 /// In the event [`ClientExtensionsOutputs::cred_props`] is `Some(CredentialPropertiesOutput { rk: Some(false) })` 823 /// and [`ResidentKeyRequirement::Required`] was sent, an error will happen regardless of this value. 824 pub cred_props: Option<ExtensionReq>, 825 /// [`credProtect`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension). 826 pub cred_protect: CredProtect, 827 /// [`minPinLength`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-minpinlength-extension). 828 /// 829 /// When the value is enforced, that corresponds to 830 /// [`minPinLength`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-minpinlength-extension) 831 /// in [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) set to a value at least as large 832 /// as the contained `FourToSixtyThree`. 833 pub min_pin_length: Option<(FourToSixtyThree, ExtensionInfo)>, 834 /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension). 835 /// 836 /// When the value is enforced, that corresponds to 837 /// [`enabled`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-enabled) set to `true`. 838 /// In contrast [`results`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-results) 839 /// must not exist, be `null`, or be an 840 /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues) 841 /// such that [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first) is `null` 842 /// and [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) does not 843 /// exist or is `null`. This is to ensure the decrypted outputs stay on the client. 844 /// 845 /// Note some authenticators can only enable `prf` during registration (e.g., CTAP authenticators that only 846 /// support 847 /// [`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) 848 /// and not 849 /// [`hmac-secret-mc`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-make-cred-extension)); 850 /// thus the value of `PrfInput` is ignored and only used as a signal to enable `prf`. For many such 851 /// authenticators, not using this extension during registration will not preclude them from being used during 852 /// authentication; however it is still encouraged to use the extension during registration since some 853 /// authenticators actually require it. 854 /// 855 /// When the underlying credential is expected to be used during discoverable requests, it is likely that 856 /// `'prf_first` will be `'static` and [`PrfInput::second`] is `None` since one will not be able to 857 /// realistically rotate the underlying inputs and further the same input will likely be used for all credentials. 858 /// For credentials intended to be used during non-discoverable requests, however, one is encouraged to rotate 859 /// the inputs and have unique values for each credential. 860 pub prf: Option<(PrfInput<'prf_first, 'prf_second>, ExtensionInfo)>, 861 } 862 impl<'prf_first, 'prf_second> Extension<'prf_first, 'prf_second> { 863 /// Returns an empty `Extension`. 864 #[inline] 865 #[must_use] 866 pub const fn none() -> Self { 867 Self { 868 cred_props: None, 869 cred_protect: CredProtect::None, 870 min_pin_length: None, 871 prf: None, 872 } 873 } 874 /// Same as [`Self::none`] except [`Self::cred_props`] is `Some` containing `req`. 875 #[inline] 876 #[must_use] 877 pub const fn with_cred_props(req: ExtensionReq) -> Self { 878 Self { 879 cred_props: Some(req), 880 ..Self::none() 881 } 882 } 883 /// Same as [`Self::none`] except [`Self::cred_protect`] is `cred_protect`. 884 #[inline] 885 #[must_use] 886 pub const fn with_cred_protect(cred_protect: CredProtect) -> Self { 887 Self { 888 cred_protect, 889 ..Self::none() 890 } 891 } 892 /// Same as [`Self::none`] except [`Self::min_pin_length`] is `Some` containing `min_len` and `info`. 893 #[inline] 894 #[must_use] 895 pub const fn with_min_pin_length(min_len: FourToSixtyThree, info: ExtensionInfo) -> Self { 896 Self { 897 min_pin_length: Some((min_len, info)), 898 ..Self::none() 899 } 900 } 901 /// Same as [`Self::none`] except [`Self::prf`] is `Some` containing `input` and `info`. 902 #[expect(single_use_lifetimes, reason = "false positive")] 903 #[inline] 904 #[must_use] 905 pub const fn with_prf<'a: 'prf_first, 'b: 'prf_second>( 906 input: PrfInput<'a, 'b>, 907 info: ExtensionInfo, 908 ) -> Self { 909 Self { 910 prf: Some((input, info)), 911 ..Self::none() 912 } 913 } 914 } 915 impl Default for Extension<'_, '_> { 916 /// Same as [`Self::none`]. 917 #[inline] 918 fn default() -> Self { 919 Self::none() 920 } 921 } 922 #[cfg(test)] 923 impl PartialEq for Extension<'_, '_> { 924 fn eq(&self, other: &Self) -> bool { 925 self.cred_props == other.cred_props 926 && self.cred_protect == other.cred_protect 927 && self.min_pin_length == other.min_pin_length 928 && self.prf == other.prf 929 } 930 } 931 /// The maximum number of bytes a [`UserHandle`] can be made of per 932 /// [WebAuthn](https://www.w3.org/TR/webauthn-3/#user-handle). 933 pub const USER_HANDLE_MAX_LEN: usize = 64; 934 /// The minimum number of bytes a [`UserHandle`] can be made of per 935 /// [WebAuthn](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-id). 936 pub const USER_HANDLE_MIN_LEN: usize = 1; 937 /// A [user handle](https://www.w3.org/TR/webauthn-3/#user-handle) that is made up of 938 /// [`USER_HANDLE_MIN_LEN`]–[`USER_HANDLE_MAX_LEN`] bytes. 939 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 940 pub struct UserHandle<const LEN: usize>([u8; LEN]); 941 impl<const LEN: usize> UserHandle<LEN> { 942 /// Returns the contained data as a `slice`. 943 #[inline] 944 #[must_use] 945 pub const fn as_slice(&self) -> &[u8] { 946 self.0.as_slice() 947 } 948 /// Returns the contained data as a shared reference to the array. 949 #[inline] 950 #[must_use] 951 pub const fn as_array(&self) -> &[u8; LEN] { 952 &self.0 953 } 954 /// Returns the contained data. 955 #[inline] 956 #[must_use] 957 pub const fn into_array(self) -> [u8; LEN] { 958 self.0 959 } 960 } 961 /// Implements [`Default`] for [`UserHandle`] of array of length of the passed `usize` literal. 962 /// 963 /// Only [`USER_HANDLE_MIN_LEN`]–[`USER_HANDLE_MAX_LEN`] inclusively are allowed to be passed. 964 macro_rules! user { 965 ( $( $x:literal),* ) => { 966 $( 967 impl Default for UserHandle<$x> { 968 #[inline] 969 fn default() -> Self { 970 let mut data = [0; $x]; 971 rand::fill(data.as_mut_slice()); 972 Self(data) 973 } 974 } 975 )* 976 }; 977 } 978 // MUST only pass [`USER_HANDLE_MIN_LEN`]–[`USER_HANDLE_MAX_LEN`] inclusively. 979 user!( 980 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 981 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 982 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64 983 ); 984 impl<const LEN: usize> UserHandle<LEN> 985 where 986 Self: Default, 987 { 988 /// Returns a new `UserHandle` based on `LEN` randomly-generated [`u8`]s. 989 /// 990 /// # Examples 991 /// 992 /// ``` 993 /// # use webauthn_rp::request::register::{UserHandle, UserHandle64, USER_HANDLE_MIN_LEN, USER_HANDLE_MAX_LEN}; 994 /// assert_eq!( 995 /// UserHandle::<USER_HANDLE_MIN_LEN>::new() 996 /// .as_ref() 997 /// .len(), 998 /// 1 999 /// ); 1000 /// // The probability of an all-zero `UserHandle` being generated (assuming a good entropy 1001 /// // source) is 2^-512 ≈ 7.5 x 10^-155. 1002 /// assert_ne!( 1003 /// UserHandle64::new().as_ref(), 1004 /// [0; USER_HANDLE_MAX_LEN] 1005 /// ); 1006 /// ``` 1007 #[inline] 1008 #[must_use] 1009 pub fn new() -> Self { 1010 Self::default() 1011 } 1012 } 1013 impl<const LEN: usize> AsRef<[u8]> for UserHandle<LEN> { 1014 #[inline] 1015 fn as_ref(&self) -> &[u8] { 1016 self.as_slice() 1017 } 1018 } 1019 impl<const LEN: usize> Borrow<[u8]> for UserHandle<LEN> { 1020 #[inline] 1021 fn borrow(&self) -> &[u8] { 1022 self.as_slice() 1023 } 1024 } 1025 impl<const LEN: usize> PartialEq<&Self> for UserHandle<LEN> { 1026 #[inline] 1027 fn eq(&self, other: &&Self) -> bool { 1028 *self == **other 1029 } 1030 } 1031 impl<const LEN: usize> PartialEq<UserHandle<LEN>> for &UserHandle<LEN> { 1032 #[inline] 1033 fn eq(&self, other: &UserHandle<LEN>) -> bool { 1034 **self == *other 1035 } 1036 } 1037 impl<const LEN: usize> From<UserHandle<LEN>> for [u8; LEN] { 1038 #[inline] 1039 fn from(value: UserHandle<LEN>) -> Self { 1040 value.into_array() 1041 } 1042 } 1043 impl<'a: 'b, 'b, const LEN: usize> From<&'a UserHandle<LEN>> for &'b [u8; LEN] { 1044 #[inline] 1045 fn from(value: &'a UserHandle<LEN>) -> Self { 1046 value.as_array() 1047 } 1048 } 1049 /// `UserHandle` that is based on the [spec recommendation](https://www.w3.org/TR/webauthn-3/#user-handle). 1050 pub type UserHandle64 = UserHandle<USER_HANDLE_MAX_LEN>; 1051 /// `UserHandle` that is based on 16 bytes. 1052 /// 1053 /// While not the recommended size like [`UserHandle64`], 16 bytes is common for many deployments since 1054 /// it's the same size as [Universally Unique IDentifiers (UUIDs)](https://www.rfc-editor.org/rfc/rfc9562). 1055 pub type UserHandle16 = UserHandle<16>; 1056 impl UserHandle16 { 1057 /// Same as [`Self::new`] except 6 bits of metadata is encoded to conform with 1058 /// [UUID Version 4](https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-4). 1059 /// 1060 /// # Examples 1061 /// 1062 /// ``` 1063 /// # use webauthn_rp::request::register::UserHandle16; 1064 /// assert!(UserHandle16::new_uuid_v4().is_uuid_v4()); 1065 /// ``` 1066 #[inline] 1067 #[must_use] 1068 pub fn new_uuid_v4() -> Self { 1069 let mut this = Self::new(); 1070 // The first 4 bits of the 6th octet (0-based index) represents the UUID version (i.e., 4). 1071 // We first 0-out the version bits retaining the other 4 bits, then set the version bits to 4. 1072 this.0[6] = (this.0[6] & 0x0F) | 0x40; 1073 // The first 2 bits of the 8th octet (0-based index) represents the UUID variant (i.e., 8,9,A,B) 1074 // which is defined to be 2. 1075 // We first 0-out the variant bits retaining the other 6 bits, then set the variant bits to 2. 1076 this.0[8] = (this.0[8] & 0x3F) | 0x80; 1077 this 1078 } 1079 /// Returns `true` iff `self` is a valid 1080 /// [UUID Version 4](https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-4). 1081 /// 1082 /// # Examples 1083 /// 1084 /// ``` 1085 /// # use webauthn_rp::request::register::UserHandle16; 1086 /// assert!(UserHandle16::new_uuid_v4().is_uuid_v4()); 1087 /// let mut user = UserHandle16::new_uuid_v4().into_array(); 1088 /// user[6] = 255; 1089 /// # #[cfg(feature = "custom")] 1090 /// assert!(!UserHandle16::from(user).is_uuid_v4()); 1091 /// ``` 1092 #[inline] 1093 #[must_use] 1094 pub const fn is_uuid_v4(&self) -> bool { 1095 // The first 4 bits of the 6th octet (0-based index) represents the UUID version (i.e., 4). 1096 // The first 2 bits of the 8th octet (0-based index) represents the UUID variant (i.e., 8,9,A,B) which 1097 // is defined to be 2. 1098 self.0[6] >> 4 == 0x4 && self.0[8] >> 6 == 0x2 1099 } 1100 /// Returns `Some` containing `uuid_v4` iff `uuid_v4` is a valid 1101 /// [UUID Version 4](https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-4). 1102 /// 1103 /// # Examples 1104 /// 1105 /// ``` 1106 /// # use webauthn_rp::request::register::UserHandle16; 1107 /// let mut user = UserHandle16::new_uuid_v4().into_array(); 1108 /// assert!(UserHandle16::from_uuid_v4(user).is_some()); 1109 /// user[8] = 255; 1110 /// assert!(UserHandle16::from_uuid_v4(user).is_none()); 1111 /// ``` 1112 #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] 1113 #[cfg(feature = "custom")] 1114 #[inline] 1115 #[must_use] 1116 pub fn from_uuid_v4(uuid_v4: [u8; 16]) -> Option<Self> { 1117 let this = Self(uuid_v4); 1118 this.is_uuid_v4().then_some(this) 1119 } 1120 } 1121 /// [The `PublicKeyCredentialUserEntity`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentity) 1122 /// sent to the client. 1123 #[derive(Clone, Debug)] 1124 pub struct PublicKeyCredentialUserEntity<'name, 'display_name, 'id, const LEN: usize> { 1125 /// [`name`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialentity-name). 1126 pub name: Username<'name>, 1127 /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-id). 1128 pub id: &'id UserHandle<LEN>, 1129 /// [`displayName`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-displayname). 1130 /// 1131 /// `None` iff the display name should be the empty string. 1132 pub display_name: Option<Nickname<'display_name>>, 1133 } 1134 impl<'a: 'b, 'b, const LEN: usize> From<&'a UserHandle<LEN>> 1135 for PublicKeyCredentialUserEntity<'_, '_, 'b, LEN> 1136 { 1137 /// Returns a `PublicKeyCredentialUserEntity` with [`Self::name`] set to `"blank"`, 1138 /// [`Self::id`] set to `value`, and [`Self::display_name`] set to `None`. 1139 /// 1140 /// One should let users set their own user name and user display name; however this can technically happen 1141 /// _after_ successfully completing the registration ceremony since this information is not used during 1142 /// [`RegistrationServerState::verify`]. For example the client can send such information along with 1143 /// [`Registration`]. 1144 /// 1145 /// # Examples 1146 /// 1147 /// ``` 1148 /// # use webauthn_rp::request::register::{PublicKeyCredentialUserEntity, UserHandle64}; 1149 /// let user_handle = UserHandle64::new(); 1150 /// let entity = PublicKeyCredentialUserEntity::from(&user_handle); 1151 /// assert_eq!("blank", entity.name.as_ref()); 1152 /// assert_eq!(user_handle, *entity.id); 1153 /// assert!(entity.display_name.is_none()); 1154 /// # Ok::<_, webauthn_rp::AggErr>(()) 1155 /// ``` 1156 #[inline] 1157 fn from(value: &'a UserHandle<LEN>) -> Self { 1158 Self { 1159 name: Username::blank(), 1160 id: value, 1161 display_name: None, 1162 } 1163 } 1164 } 1165 /// `PublicKeyCredentialUserEntity` based on a [`UserHandle64`]. 1166 pub type PublicKeyCredentialUserEntity64<'name, 'display_name, 'id> = 1167 PublicKeyCredentialUserEntity<'name, 'display_name, 'id, USER_HANDLE_MAX_LEN>; 1168 /// `PublicKeyCredentialUserEntity` based on a [`UserHandle16`]. 1169 pub type PublicKeyCredentialUserEntity16<'name, 'display_name, 'id> = 1170 PublicKeyCredentialUserEntity<'name, 'display_name, 'id, 16>; 1171 /// [`ResidentKeyRequirement`](https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement) sent to the client. 1172 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 1173 pub enum ResidentKeyRequirement { 1174 /// [`required`](https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-required). 1175 Required, 1176 /// [`discouraged`](https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-discouraged). 1177 Discouraged, 1178 /// [`preferred`](https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-preferred). 1179 Preferred, 1180 } 1181 /// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint) 1182 /// for [`AuthenticatorAttachment::CrossPlatform`] authenticators. 1183 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] 1184 pub enum CrossPlatformHint { 1185 /// No hints. 1186 #[default] 1187 None, 1188 /// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-security-key). 1189 SecurityKey, 1190 /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-hybrid). 1191 Hybrid, 1192 /// [`Self::SecurityKey`] and [`Self::Hybrid`]. 1193 SecurityKeyHybrid, 1194 /// [`Self::Hybrid`] and [`Self::SecurityKey`]. 1195 HybridSecurityKey, 1196 } 1197 impl From<CrossPlatformHint> for Hint { 1198 #[inline] 1199 fn from(value: CrossPlatformHint) -> Self { 1200 match value { 1201 CrossPlatformHint::None => Self::None, 1202 CrossPlatformHint::SecurityKey => Self::SecurityKey, 1203 CrossPlatformHint::Hybrid => Self::Hybrid, 1204 CrossPlatformHint::SecurityKeyHybrid => Self::SecurityKeyHybrid, 1205 CrossPlatformHint::HybridSecurityKey => Self::HybridSecurityKey, 1206 } 1207 } 1208 } 1209 /// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint) 1210 /// for [`AuthenticatorAttachment::Platform`] authenticators. 1211 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] 1212 pub enum PlatformHint { 1213 /// No hints. 1214 #[default] 1215 None, 1216 /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-client-device). 1217 ClientDevice, 1218 } 1219 impl From<PlatformHint> for Hint { 1220 #[inline] 1221 fn from(value: PlatformHint) -> Self { 1222 match value { 1223 PlatformHint::None => Self::None, 1224 PlatformHint::ClientDevice => Self::ClientDevice, 1225 } 1226 } 1227 } 1228 /// [`AuthenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment) 1229 /// requirement with associated hints for further refinement. 1230 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 1231 pub enum AuthenticatorAttachmentReq { 1232 /// No attachment information (i.e., any [`AuthenticatorAttachment`]). 1233 None(Hint), 1234 /// [`platform`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattachment-platform) is required 1235 /// to be used. 1236 Platform(PlatformHint), 1237 /// [`cross-platform`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattachment-cross-platform) is 1238 /// required to be used. 1239 CrossPlatform(CrossPlatformHint), 1240 } 1241 impl Default for AuthenticatorAttachmentReq { 1242 #[inline] 1243 fn default() -> Self { 1244 Self::None(Hint::default()) 1245 } 1246 } 1247 impl AuthenticatorAttachmentReq { 1248 /// Validates `self` against `other` ignoring [`Self::immutable_attachment`]. 1249 const fn validate( 1250 self, 1251 require_response: bool, 1252 other: AuthenticatorAttachment, 1253 ) -> Result<(), RegCeremonyErr> { 1254 match self { 1255 Self::None(_) => { 1256 if require_response && matches!(other, AuthenticatorAttachment::None) { 1257 Err(RegCeremonyErr::MissingAuthenticatorAttachment) 1258 } else { 1259 Ok(()) 1260 } 1261 } 1262 Self::Platform(_) => match other { 1263 AuthenticatorAttachment::None => { 1264 if require_response { 1265 Err(RegCeremonyErr::MissingAuthenticatorAttachment) 1266 } else { 1267 Ok(()) 1268 } 1269 } 1270 AuthenticatorAttachment::Platform => Ok(()), 1271 AuthenticatorAttachment::CrossPlatform => { 1272 Err(RegCeremonyErr::AuthenticatorAttachmentMismatch) 1273 } 1274 }, 1275 Self::CrossPlatform(_) => match other { 1276 AuthenticatorAttachment::None => { 1277 if require_response { 1278 Err(RegCeremonyErr::MissingAuthenticatorAttachment) 1279 } else { 1280 Ok(()) 1281 } 1282 } 1283 AuthenticatorAttachment::CrossPlatform => Ok(()), 1284 AuthenticatorAttachment::Platform => { 1285 Err(RegCeremonyErr::AuthenticatorAttachmentMismatch) 1286 } 1287 }, 1288 } 1289 } 1290 } 1291 /// [`AuthenticatorSelectionCriteria`](https://www.w3.org/TR/webauthn-3/#dictionary-authenticatorSelection). 1292 #[derive(Clone, Copy, Debug)] 1293 pub struct AuthenticatorSelectionCriteria { 1294 /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-authenticatorattachment). 1295 pub authenticator_attachment: AuthenticatorAttachmentReq, 1296 /// [`residentKey`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-residentkey). 1297 pub resident_key: ResidentKeyRequirement, 1298 /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification). 1299 pub user_verification: UserVerificationRequirement, 1300 } 1301 impl AuthenticatorSelectionCriteria { 1302 /// Returns an `AuthenticatorSelectionCriteria` useful for passkeys (i.e., [`Self::resident_key`] is set to 1303 /// [`ResidentKeyRequirement::Required`] and [`Self::user_verification`] is set to 1304 /// [`UserVerificationRequirement::Required`]). 1305 /// 1306 /// # Examples 1307 /// 1308 /// ``` 1309 /// # use webauthn_rp::request::{ 1310 /// # register::{ 1311 /// # AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, ResidentKeyRequirement, 1312 /// # }, 1313 /// # Hint, UserVerificationRequirement, 1314 /// # }; 1315 /// let crit = AuthenticatorSelectionCriteria::passkey(); 1316 /// assert!( 1317 /// matches!(crit.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) 1318 /// ); 1319 /// assert!(matches!( 1320 /// crit.resident_key, 1321 /// ResidentKeyRequirement::Required 1322 /// )); 1323 /// assert!(matches!( 1324 /// crit.user_verification, 1325 /// UserVerificationRequirement::Required 1326 /// )); 1327 /// ``` 1328 #[inline] 1329 #[must_use] 1330 pub fn passkey() -> Self { 1331 Self { 1332 authenticator_attachment: AuthenticatorAttachmentReq::default(), 1333 resident_key: ResidentKeyRequirement::Required, 1334 user_verification: UserVerificationRequirement::Required, 1335 } 1336 } 1337 /// Returns an `AuthenticatorSelectionCriteria` useful for second-factor flows (i.e., [`Self::resident_key`] 1338 /// is set to [`ResidentKeyRequirement::Discouraged`] and [`Self::user_verification`] is set to 1339 /// [`UserVerificationRequirement::Discouraged`]). 1340 /// 1341 /// Note some authenticators require user verification during credential registration (e.g., 1342 /// [CTAP 2.0 authenticators](https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#authenticatorMakeCredential)). 1343 /// When an authenticator supports both CTAP 2.0 and 1344 /// [Universal 2nd Factor (U2F)](https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-overview-v1.2-ps-20170411.html#registration-creating-a-key-pair) 1345 /// protocols, user agents will sometimes fall back to U2F when `UserVerificationRequirement::Discouraged` 1346 /// is requested since the latter allows for registration without user verification. If the user agent does not 1347 /// do this, then users will have an inconsistent experience when authenticating an already-registered 1348 /// credential. If this is undesirable, one can use [`UserVerificationRequirement::Required`] for this and 1349 /// [`PublicKeyCredentialRequestOptions::user_verification`] at the expense of requiring a user to verify 1350 /// themselves twice: once for the first factor and again here. 1351 /// 1352 /// # Examples 1353 /// 1354 /// ``` 1355 /// # use webauthn_rp::request::{ 1356 /// # register::{ 1357 /// # AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, ResidentKeyRequirement, 1358 /// # }, 1359 /// # Hint, UserVerificationRequirement, 1360 /// # }; 1361 /// let crit = AuthenticatorSelectionCriteria::second_factor(); 1362 /// assert!( 1363 /// matches!(crit.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) 1364 /// ); 1365 /// assert!(matches!( 1366 /// crit.resident_key, 1367 /// ResidentKeyRequirement::Discouraged 1368 /// )); 1369 /// assert!(matches!( 1370 /// crit.user_verification, 1371 /// UserVerificationRequirement::Discouraged 1372 /// )); 1373 /// ``` 1374 #[inline] 1375 #[must_use] 1376 pub fn second_factor() -> Self { 1377 Self { 1378 authenticator_attachment: AuthenticatorAttachmentReq::default(), 1379 resident_key: ResidentKeyRequirement::Discouraged, 1380 user_verification: UserVerificationRequirement::Discouraged, 1381 } 1382 } 1383 /// Ensures a client-side credential was created when applicable. Also enforces `auth_attachment` when 1384 /// applicable. 1385 const fn validate( 1386 self, 1387 require_auth_attachment: bool, 1388 auth_attachment: AuthenticatorAttachment, 1389 ) -> Result<(), RegCeremonyErr> { 1390 self.authenticator_attachment 1391 .validate(require_auth_attachment, auth_attachment) 1392 } 1393 } 1394 #[cfg(test)] 1395 impl PartialEq for AuthenticatorSelectionCriteria { 1396 fn eq(&self, other: &Self) -> bool { 1397 self.authenticator_attachment == other.authenticator_attachment 1398 && self.resident_key == other.resident_key 1399 && self.user_verification == other.user_verification 1400 } 1401 } 1402 /// Helper that verifies the overlap of [`CredentialCreationOptions::start_ceremony`] and 1403 /// [`RegistrationServerState::decode`]. 1404 const fn validate_options_helper( 1405 auth_crit: AuthenticatorSelectionCriteria, 1406 extensions: ServerExtensionInfo, 1407 ) -> Result<(), CreationOptionsErr> { 1408 if matches!( 1409 auth_crit.user_verification, 1410 UserVerificationRequirement::Required 1411 ) { 1412 Ok(()) 1413 } else if matches!( 1414 extensions.cred_protect, 1415 CredProtect::UserVerificationRequired(_, _) 1416 ) { 1417 Err(CreationOptionsErr::CredProtectRequiredWithoutUserVerification) 1418 } else { 1419 Ok(()) 1420 } 1421 } 1422 /// The [`CredentialCreationOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialcreationoptions) 1423 /// to send to the client when registering a new credential. 1424 /// 1425 /// Upon saving the [`RegistrationServerState`] returned from [`Self::start_ceremony`], one MUST send 1426 /// [`RegistrationClientState`] to the client ASAP. After receiving the newly created [`Registration`], it is 1427 /// validated using [`RegistrationServerState::verify`]. 1428 #[derive(Debug)] 1429 pub struct CredentialCreationOptions< 1430 'rp_id, 1431 'user_name, 1432 'user_display_name, 1433 'user_id, 1434 'prf_first, 1435 'prf_second, 1436 const USER_LEN: usize, 1437 > { 1438 /// [`mediation`](https://www.w3.org/TR/credential-management-1/#dom-credentialcreationoptions-mediation). 1439 /// 1440 /// Note if this is [`CredentialMediationRequirement::Conditional`], one may want to ensure 1441 /// [`AuthenticatorSelectionCriteria::user_verification`] is not [`UserVerificationRequirement::Required`] 1442 /// since some authenticators cannot enforce user verification during registration ceremonies when conditional 1443 /// mediation is used. Do note that in the event passkeys are to be created, one may want to set 1444 /// [`AuthenticationVerificationOptions::update_uv`] to `true` since [`Flag::user_verified`] will 1445 /// potentially be `false`. 1446 pub mediation: CredentialMediationRequirement, 1447 /// `public-key` [credential type](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry). 1448 pub public_key: PublicKeyCredentialCreationOptions< 1449 'rp_id, 1450 'user_name, 1451 'user_display_name, 1452 'user_id, 1453 'prf_first, 1454 'prf_second, 1455 USER_LEN, 1456 >, 1457 } 1458 impl< 1459 'rp_id, 1460 'user_name, 1461 'user_display_name, 1462 'user_id, 1463 'prf_first, 1464 'prf_second, 1465 const USER_LEN: usize, 1466 > 1467 CredentialCreationOptions< 1468 'rp_id, 1469 'user_name, 1470 'user_display_name, 1471 'user_id, 1472 'prf_first, 1473 'prf_second, 1474 USER_LEN, 1475 > 1476 { 1477 /// Sets [`Self::mediation`] to [`CredentialMediationRequirement::default`] and 1478 /// [`Self::public_key`] to [`PublicKeyCredentialCreationOptions::passkey`]. 1479 #[expect(single_use_lifetimes, reason = "false positive")] 1480 #[inline] 1481 #[must_use] 1482 pub fn passkey<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>( 1483 rp_id: &'a RpId, 1484 user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>, 1485 exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, 1486 ) -> Self { 1487 Self { 1488 mediation: CredentialMediationRequirement::default(), 1489 public_key: PublicKeyCredentialCreationOptions::passkey( 1490 rp_id, 1491 user, 1492 exclude_credentials, 1493 ), 1494 } 1495 } 1496 /// Convenience function for [`Self::passkey`] passing an empty `Vec`. 1497 /// 1498 /// This MUST only be used when this is the first credential for a user. 1499 #[expect(single_use_lifetimes, reason = "false positive")] 1500 #[inline] 1501 #[must_use] 1502 pub fn first_passkey<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>( 1503 rp_id: &'a RpId, 1504 user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>, 1505 ) -> Self { 1506 Self::passkey(rp_id, user, Vec::new()) 1507 } 1508 /// Convenience function for [`Self::first_passkey`] passing [`PublicKeyCredentialUserEntity::from`] applied 1509 /// to `user_id` for `user`. 1510 /// 1511 /// This MUST only be used when user information is provided _after_ registration (e.g., when the client 1512 /// sends user name and user display name along with [`Registration`]). 1513 /// 1514 /// Because user information is likely known for existing accounts, this will often only be called during 1515 /// greenfield deployments. 1516 #[expect(single_use_lifetimes, reason = "false positive")] 1517 #[inline] 1518 #[must_use] 1519 pub fn first_passkey_with_blank_user_info<'a: 'rp_id, 'b: 'user_id>( 1520 rp_id: &'a RpId, 1521 user_id: &'b UserHandle<USER_LEN>, 1522 ) -> Self { 1523 Self::first_passkey(rp_id, user_id.into()) 1524 } 1525 /// Sets [`Self::mediation`] to [`CredentialMediationRequirement::default`] and 1526 /// [`Self::public_key`] to [`PublicKeyCredentialCreationOptions::second_factor`]. 1527 #[expect(single_use_lifetimes, reason = "false positive")] 1528 #[inline] 1529 #[must_use] 1530 pub fn second_factor<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>( 1531 rp_id: &'a RpId, 1532 user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>, 1533 exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, 1534 ) -> Self { 1535 let mut opts = Self::passkey(rp_id, user, exclude_credentials); 1536 opts.public_key.authenticator_selection = AuthenticatorSelectionCriteria::second_factor(); 1537 opts.public_key.extensions.cred_props = Some(ExtensionReq::Allow); 1538 opts.public_key.extensions.cred_protect = 1539 CredProtect::UserVerificationOptionalWithCredentialIdList( 1540 false, 1541 ExtensionInfo::AllowEnforceValue, 1542 ); 1543 opts 1544 } 1545 /// Convenience function for [`Self::second_factor`] passing an empty `Vec`. 1546 /// 1547 /// This MUST only be used when this is the first credential for a user. 1548 #[expect(single_use_lifetimes, reason = "false positive")] 1549 #[inline] 1550 #[must_use] 1551 pub fn first_second_factor<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>( 1552 rp_id: &'a RpId, 1553 user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>, 1554 ) -> Self { 1555 Self::second_factor(rp_id, user, Vec::new()) 1556 } 1557 /// Begins the [registration ceremony](https://www.w3.org/TR/webauthn-3/#registration-ceremony) consuming 1558 /// `self`. Note that the expiration [`Instant`]/[`SystemTime`] is saved, so `RegistrationClientState` MUST be 1559 /// sent ASAP. In order to complete registration, the returned `RegistrationServerState` MUST be saved so that 1560 /// it can later be used to verify the new credential with [`RegistrationServerState::verify`]. 1561 /// 1562 /// # Errors 1563 /// 1564 /// Errors iff `self` contains incompatible configuration. 1565 /// 1566 /// # Examples 1567 /// 1568 /// ``` 1569 /// # #[cfg(not(feature = "serializable_server_state"))] 1570 /// # use std::time::Instant; 1571 /// # #[cfg(not(feature = "serializable_server_state"))] 1572 /// # use webauthn_rp::request::TimedCeremony as _; 1573 /// # use webauthn_rp::request::{ 1574 /// # register::{CredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle64}, 1575 /// # AsciiDomain, RpId 1576 /// # }; 1577 /// # #[cfg(not(feature = "serializable_server_state"))] 1578 /// assert!( 1579 /// CredentialCreationOptions::passkey( 1580 /// &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?), 1581 /// PublicKeyCredentialUserEntity { 1582 /// name: "bernard.riemann".try_into()?, 1583 /// id: &UserHandle64::new(), 1584 /// display_name: Some("Georg Friedrich Bernhard Riemann".try_into()?) 1585 /// }, 1586 /// Vec::new() 1587 /// ).start_ceremony()?.0.expiration() > Instant::now() 1588 /// ); 1589 /// # Ok::<_, webauthn_rp::AggErr>(()) 1590 /// ``` 1591 #[inline] 1592 pub fn start_ceremony( 1593 mut self, 1594 ) -> Result< 1595 ( 1596 RegistrationServerState<USER_LEN>, 1597 RegistrationClientState< 1598 'rp_id, 1599 'user_name, 1600 'user_display_name, 1601 'user_id, 1602 'prf_first, 1603 'prf_second, 1604 USER_LEN, 1605 >, 1606 ), 1607 CreationOptionsErr, 1608 > { 1609 let extensions = self.public_key.extensions.into(); 1610 validate_options_helper(self.public_key.authenticator_selection, extensions).and_then( 1611 |()| { 1612 #[cfg(not(feature = "serializable_server_state"))] 1613 let now = Instant::now(); 1614 #[cfg(feature = "serializable_server_state")] 1615 let now = SystemTime::now(); 1616 now.checked_add(Duration::from_millis( 1617 NonZeroU64::from(self.public_key.timeout).get(), 1618 )) 1619 .ok_or(CreationOptionsErr::InvalidTimeout) 1620 .map(|expiration| { 1621 // We remove duplicates. The order has no significance, so this is OK. 1622 self.public_key 1623 .exclude_credentials 1624 .sort_unstable_by(|a, b| a.id.as_ref().cmp(b.id.as_ref())); 1625 self.public_key 1626 .exclude_credentials 1627 .dedup_by(|a, b| a.id.as_ref() == b.id.as_ref()); 1628 ( 1629 RegistrationServerState { 1630 mediation: self.mediation, 1631 challenge: SentChallenge(self.public_key.challenge.0), 1632 pub_key_cred_params: self.public_key.pub_key_cred_params, 1633 authenticator_selection: self.public_key.authenticator_selection, 1634 extensions, 1635 expiration, 1636 user_id: *self.public_key.user.id, 1637 }, 1638 RegistrationClientState(self), 1639 ) 1640 }) 1641 }, 1642 ) 1643 } 1644 } 1645 /// `CredentialCreationOptions` based on a [`UserHandle64`]. 1646 pub type CredentialCreationOptions64< 1647 'rp_id, 1648 'user_name, 1649 'user_display_name, 1650 'user_id, 1651 'prf_first, 1652 'prf_second, 1653 > = CredentialCreationOptions< 1654 'rp_id, 1655 'user_name, 1656 'user_display_name, 1657 'user_id, 1658 'prf_first, 1659 'prf_second, 1660 USER_HANDLE_MAX_LEN, 1661 >; 1662 /// `CredentialCreationOptions` based on a [`UserHandle16`]. 1663 pub type CredentialCreationOptions16< 1664 'rp_id, 1665 'user_name, 1666 'user_display_name, 1667 'user_id, 1668 'prf_first, 1669 'prf_second, 1670 > = CredentialCreationOptions< 1671 'rp_id, 1672 'user_name, 1673 'user_display_name, 1674 'user_id, 1675 'prf_first, 1676 'prf_second, 1677 16, 1678 >; 1679 /// The [`PublicKeyCredentialCreationOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptions) 1680 /// to send to the client when registering a new credential. 1681 #[derive(Debug)] 1682 pub struct PublicKeyCredentialCreationOptions< 1683 'rp_id, 1684 'user_name, 1685 'user_display_name, 1686 'user_id, 1687 'prf_first, 1688 'prf_second, 1689 const USER_LEN: usize, 1690 > { 1691 /// [`rp`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-rp). 1692 pub rp_id: &'rp_id RpId, 1693 /// [`user`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-user). 1694 pub user: PublicKeyCredentialUserEntity<'user_name, 'user_display_name, 'user_id, USER_LEN>, 1695 /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-challenge). 1696 pub challenge: Challenge, 1697 /// [`pubKeyCredParams`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-pubkeycredparams). 1698 pub pub_key_cred_params: CoseAlgorithmIdentifiers, 1699 /// [`timeout`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-timeout). 1700 /// 1701 /// Note we require a positive value despite the spec allowing an optional nonnegative value. This jives 1702 /// with the fact that in-memory storage is required when `serializable_server_state` is not enabled 1703 /// when attesting credentials as no timeout would make out-of-memory (OOM) conditions more likely. 1704 pub timeout: NonZeroU32, 1705 /// [`excludeCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-excludecredentials). 1706 pub exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, 1707 /// [`authenticatorSelection`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-authenticatorselection). 1708 pub authenticator_selection: AuthenticatorSelectionCriteria, 1709 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions). 1710 pub extensions: Extension<'prf_first, 'prf_second>, 1711 } 1712 impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize> 1713 PublicKeyCredentialCreationOptions< 1714 'rp_id, 1715 'user_name, 1716 'user_display_name, 1717 'user_id, 1718 '_, 1719 '_, 1720 USER_LEN, 1721 > 1722 { 1723 /// Most deployments of passkeys should use this function. Specifically deployments that are both userless and 1724 /// passwordless and desire multi-factor authentication (MFA) to be done entirely on the authenticator. It 1725 /// is important `exclude_credentials` contains the information for _all_ [`RegisteredCredential`]s registered to 1726 /// [`PublicKeyCredentialUserEntity::id`] to avoid accidentally overwriting existing credentials that 1727 /// have been previously registered. 1728 /// 1729 /// Creates a `PublicKeyCredentialCreationOptions` that requires the authenticator to create a client-side 1730 /// discoverable credential enforcing any form of user verification. [`Self::timeout`] is [`FIVE_MINUTES`]. 1731 /// [`Extension::cred_protect`] with [`CredProtect::UserVerificationRequired`] with `false` and 1732 /// [`ExtensionInfo::AllowEnforceValue`] is used. 1733 /// 1734 /// # Examples 1735 /// 1736 /// ``` 1737 /// # use webauthn_rp::request::{ 1738 /// # register::{ 1739 /// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle64 1740 /// # }, 1741 /// # AsciiDomain, RpId, UserVerificationRequirement 1742 /// # }; 1743 /// assert!(matches!( 1744 /// PublicKeyCredentialCreationOptions::passkey( 1745 /// &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?), 1746 /// PublicKeyCredentialUserEntity { 1747 /// name: "archimedes.of.syracuse".try_into()?, 1748 /// id: &UserHandle64::new(), 1749 /// display_name: Some("Αρχιμήδης ο Συρακούσιος".try_into()?), 1750 /// }, 1751 /// Vec::new() 1752 /// ) 1753 /// .authenticator_selection.user_verification, UserVerificationRequirement::Required 1754 /// )); 1755 /// # Ok::<_, webauthn_rp::AggErr>(()) 1756 /// ``` 1757 #[expect(single_use_lifetimes, reason = "false positive")] 1758 #[inline] 1759 #[must_use] 1760 pub fn passkey<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>( 1761 rp_id: &'a RpId, 1762 user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>, 1763 exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, 1764 ) -> Self { 1765 Self { 1766 rp_id, 1767 user, 1768 challenge: Challenge::new(), 1769 pub_key_cred_params: CoseAlgorithmIdentifiers::default(), 1770 timeout: FIVE_MINUTES, 1771 exclude_credentials, 1772 authenticator_selection: AuthenticatorSelectionCriteria::passkey(), 1773 extensions: Extension { 1774 cred_props: None, 1775 cred_protect: CredProtect::UserVerificationRequired( 1776 false, 1777 ExtensionInfo::AllowEnforceValue, 1778 ), 1779 min_pin_length: None, 1780 prf: None, 1781 }, 1782 } 1783 } 1784 /// Convenience function for [`Self::passkey`] passing an empty `Vec`. 1785 /// 1786 /// This MUST only be used when this is the first credential for a user. 1787 #[expect(single_use_lifetimes, reason = "false positive")] 1788 #[inline] 1789 #[must_use] 1790 pub fn first_passkey<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>( 1791 rp_id: &'a RpId, 1792 user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>, 1793 ) -> Self { 1794 Self::passkey(rp_id, user, Vec::new()) 1795 } 1796 /// Convenience function for [`Self::first_passkey`] passing [`PublicKeyCredentialUserEntity::from`] applied 1797 /// to `user_id` for `user`. 1798 /// 1799 /// This MUST only be used when user information is provided _after_ registration (e.g., when the client 1800 /// sends user name and user display name along with [`Registration`]). 1801 /// 1802 /// Because user information is likely known for existing accounts, this will often only be called during 1803 /// greenfield deployments. 1804 #[expect(single_use_lifetimes, reason = "false positive")] 1805 #[inline] 1806 #[must_use] 1807 pub fn first_passkey_with_blank_user_info<'a: 'rp_id, 'b: 'user_id>( 1808 rp_id: &'a RpId, 1809 user_id: &'b UserHandle<USER_LEN>, 1810 ) -> Self { 1811 Self::first_passkey(rp_id, user_id.into()) 1812 } 1813 /// Deployments that want to incorporate a "something a user has" factor into a larger multi-factor 1814 /// authentication (MFA) setup. Specifically deployments that are _not_ userless or passwordless. It 1815 /// is important `exclude_credentials` contains the information for _all_ [`RegisteredCredential`]s registered 1816 /// to [`PublicKeyCredentialUserEntity::id`] to avoid accidentally overwriting existing credentials that 1817 /// have been previously registered. 1818 /// 1819 /// Creates a `PublicKeyCredentialCreationOptions` that prefers the authenticator to create a server-side 1820 /// credential without requiring user verification. [`Self::timeout`] is [`FIVE_MINUTES`]. 1821 /// [`Extension::cred_props`] is [`ExtensionReq::Allow`]. [`Extension::cred_protect`] is 1822 /// [`CredProtect::UserVerificationOptionalWithCredentialIdList`] with `false` and 1823 /// [`ExtensionInfo::AllowEnforceValue`]. 1824 /// 1825 /// Note some authenticators require user verification during credential registration (e.g., 1826 /// [CTAP 2.0 authenticators](https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#authenticatorMakeCredential)). 1827 /// When an authenticator supports both CTAP 2.0 and 1828 /// [Universal 2nd Factor (U2F)](https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-overview-v1.2-ps-20170411.html#registration-creating-a-key-pair) 1829 /// protocols, user agents will sometimes fall back to U2F when [`UserVerificationRequirement::Discouraged`] 1830 /// is requested since the latter allows for registration without user verification. If the user agent does not 1831 /// do this, then users will have an inconsistent experience when authenticating an already-registered 1832 /// credential. If this is undesirable, one can use [`UserVerificationRequirement::Required`] for this and 1833 /// [`PublicKeyCredentialRequestOptions::user_verification`] at the expense of requiring a user to verify 1834 /// themselves twice: once for the first factor and again here. 1835 /// 1836 /// # Examples 1837 /// 1838 /// ``` 1839 /// # use webauthn_rp::request::{register::{ 1840 /// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle64 1841 /// # }, AsciiDomain, RpId}; 1842 /// assert_eq!( 1843 /// PublicKeyCredentialCreationOptions::second_factor( 1844 /// &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?), 1845 /// PublicKeyCredentialUserEntity { 1846 /// name: "carl.gauss".try_into()?, 1847 /// id: &UserHandle64::new(), 1848 /// display_name: Some("Johann Carl Friedrich Gauß".try_into()?), 1849 /// }, 1850 /// Vec::new() 1851 /// ) 1852 /// .timeout 1853 /// .get(), 1854 /// 300_000 1855 /// ); 1856 /// # Ok::<_, webauthn_rp::AggErr>(()) 1857 /// ``` 1858 #[expect(single_use_lifetimes, reason = "false positive")] 1859 #[inline] 1860 #[must_use] 1861 pub fn second_factor<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>( 1862 rp_id: &'a RpId, 1863 user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>, 1864 exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, 1865 ) -> Self { 1866 let mut opts = Self::passkey(rp_id, user, exclude_credentials); 1867 opts.authenticator_selection = AuthenticatorSelectionCriteria::second_factor(); 1868 opts.extensions.cred_props = Some(ExtensionReq::Allow); 1869 opts.extensions.cred_protect = CredProtect::UserVerificationOptionalWithCredentialIdList( 1870 false, 1871 ExtensionInfo::AllowEnforceValue, 1872 ); 1873 opts 1874 } 1875 /// Convenience function for [`Self::second_factor`] passing an empty `Vec`. 1876 /// 1877 /// This MUST only be used when this is the first credential for a user. 1878 #[expect(single_use_lifetimes, reason = "false positive")] 1879 #[inline] 1880 #[must_use] 1881 pub fn first_second_factor<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>( 1882 rp_id: &'a RpId, 1883 user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>, 1884 ) -> Self { 1885 Self::second_factor(rp_id, user, Vec::new()) 1886 } 1887 } 1888 /// `PublicKeyCredentialCreationOptions` based on a [`UserHandle64`]. 1889 pub type PublicKeyCredentialCreationOptions64< 1890 'rp_id, 1891 'user_name, 1892 'user_display_name, 1893 'user_id, 1894 'prf_first, 1895 'prf_second, 1896 > = PublicKeyCredentialCreationOptions< 1897 'rp_id, 1898 'user_name, 1899 'user_display_name, 1900 'user_id, 1901 'prf_first, 1902 'prf_second, 1903 USER_HANDLE_MAX_LEN, 1904 >; 1905 /// `PublicKeyCredentialCreationOptions` based on a [`UserHandle16`]. 1906 pub type PublicKeyCredentialCreationOptions16< 1907 'rp_id, 1908 'user_name, 1909 'user_display_name, 1910 'user_id, 1911 'prf_first, 1912 'prf_second, 1913 > = PublicKeyCredentialCreationOptions< 1914 'rp_id, 1915 'user_name, 1916 'user_display_name, 1917 'user_id, 1918 'prf_first, 1919 'prf_second, 1920 16, 1921 >; 1922 /// Container of a [`CredentialCreationOptions`] that has been used to start the registration ceremony. 1923 /// This gets sent to the client ASAP. 1924 #[derive(Debug)] 1925 pub struct RegistrationClientState< 1926 'rp_id, 1927 'user_name, 1928 'user_display_name, 1929 'user_id, 1930 'prf_first, 1931 'prf_second, 1932 const USER_LEN: usize, 1933 >( 1934 CredentialCreationOptions< 1935 'rp_id, 1936 'user_name, 1937 'user_display_name, 1938 'user_id, 1939 'prf_first, 1940 'prf_second, 1941 USER_LEN, 1942 >, 1943 ); 1944 impl< 1945 'rp_id, 1946 'user_name, 1947 'user_display_name, 1948 'user_id, 1949 'prf_first, 1950 'prf_second, 1951 const USER_LEN: usize, 1952 > 1953 RegistrationClientState< 1954 'rp_id, 1955 'user_name, 1956 'user_display_name, 1957 'user_id, 1958 'prf_first, 1959 'prf_second, 1960 USER_LEN, 1961 > 1962 { 1963 /// Returns the `CredentialCreationOptions` that was used to start a registration ceremony. 1964 /// 1965 /// # Examples 1966 /// 1967 /// ``` 1968 /// # use webauthn_rp::request::{register::{ 1969 /// # CoseAlgorithmIdentifiers, CredentialCreationOptions, 1970 /// # PublicKeyCredentialUserEntity, UserHandle64, 1971 /// # }, AsciiDomain, RpId}; 1972 /// assert_eq!( 1973 /// CredentialCreationOptions::passkey( 1974 /// &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?), 1975 /// PublicKeyCredentialUserEntity { 1976 /// name: "david.hilbert".try_into()?, 1977 /// id: &UserHandle64::new(), 1978 /// display_name: Some("David Hilbert".try_into()?) 1979 /// }, 1980 /// Vec::new() 1981 /// ) 1982 /// .start_ceremony()? 1983 /// .1 1984 /// .options() 1985 /// .public_key 1986 /// .rp_id.as_ref(), 1987 /// "example.com" 1988 /// ); 1989 /// # Ok::<_, webauthn_rp::AggErr>(()) 1990 /// ``` 1991 #[inline] 1992 #[must_use] 1993 pub const fn options( 1994 &self, 1995 ) -> &CredentialCreationOptions< 1996 'rp_id, 1997 'user_name, 1998 'user_display_name, 1999 'user_id, 2000 'prf_first, 2001 'prf_second, 2002 USER_LEN, 2003 > { 2004 &self.0 2005 } 2006 } 2007 /// `RegistrationClientState` based on a [`UserHandle64`]. 2008 pub type RegistrationClientState64< 2009 'rp_id, 2010 'user_name, 2011 'user_display_name, 2012 'user_id, 2013 'prf_first, 2014 'prf_second, 2015 > = RegistrationClientState< 2016 'rp_id, 2017 'user_name, 2018 'user_display_name, 2019 'user_id, 2020 'prf_first, 2021 'prf_second, 2022 USER_HANDLE_MAX_LEN, 2023 >; 2024 /// `RegistrationClientState` based on a [`UserHandle16`]. 2025 pub type RegistrationClientState16< 2026 'rp_id, 2027 'user_name, 2028 'user_display_name, 2029 'user_id, 2030 'prf_first, 2031 'prf_second, 2032 > = RegistrationClientState< 2033 'rp_id, 2034 'user_name, 2035 'user_display_name, 2036 'user_id, 2037 'prf_first, 2038 'prf_second, 2039 16, 2040 >; 2041 /// Additional verification options to perform in [`RegistrationServerState::verify`]. 2042 #[derive(Clone, Copy, Debug)] 2043 pub struct RegistrationVerificationOptions<'origins, 'top_origins, O, T> { 2044 /// Origins to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin). 2045 /// 2046 /// When this is empty, the origin that will be used will be based on 2047 /// the [`RpId`] passed to [`RegistrationServerState::verify`]. If [`RpId::Domain`] or [`RpId::StaticDomain`], 2048 /// then the [`DomainOrigin`] returned from passing [`AsciiDomain::as_ref`] and [`AsciiDomainStatic::as_str`] 2049 /// to [`DomainOrigin::new`] respectively will be used; otherwise the [`Url`] in [`RpId::Url`] will be used. 2050 pub allowed_origins: &'origins [O], 2051 /// [Top-level origins](https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-top-level-origin) 2052 /// to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin). 2053 /// 2054 /// When this is `Some`, [`CollectedClientData::cross_origin`] is allowed to be `true`. When the contained 2055 /// `slice` is empty, [`CollectedClientData::top_origin`] must be `None`. When this is `None`, 2056 /// `CollectedClientData::cross_origin` must be `false` and `CollectedClientData::top_origin` must be `None`. 2057 pub allowed_top_origins: Option<&'top_origins [T]>, 2058 /// The required [`Backup`] state of the credential. 2059 pub backup_requirement: BackupReq, 2060 /// Error when unsolicited extensions are sent back iff `true`. 2061 pub error_on_unsolicited_extensions: bool, 2062 /// [`AuthenticatorAttachment`] must be sent iff `true`. 2063 pub require_authenticator_attachment: bool, 2064 /// [`CollectedClientData::from_client_data_json_relaxed`] is used to extract [`CollectedClientData`] iff `true`. 2065 #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] 2066 #[cfg(feature = "serde_relaxed")] 2067 pub client_data_json_relaxed: bool, 2068 } 2069 impl<O, T> RegistrationVerificationOptions<'_, '_, O, T> { 2070 /// Returns `Self` such that [`Self::allowed_origins`] is empty, [`Self::allowed_top_origins`] is `None`, 2071 /// [`Self::backup_requirement`] is [`BackupReq::None`], [`Self::error_on_unsolicited_extensions`] is `true`, 2072 /// [`Self::require_authenticator_attachment`] is `false`, and [`Self::client_data_json_relaxed`] is 2073 /// `true`. 2074 /// 2075 /// Note `O` and `T` should implement `PartialEq<Origin<'_>>` (e.g., `&str`). 2076 #[inline] 2077 #[must_use] 2078 pub const fn new() -> Self { 2079 Self { 2080 allowed_origins: [].as_slice(), 2081 allowed_top_origins: None, 2082 backup_requirement: BackupReq::None, 2083 error_on_unsolicited_extensions: true, 2084 require_authenticator_attachment: false, 2085 #[cfg(feature = "serde_relaxed")] 2086 client_data_json_relaxed: true, 2087 } 2088 } 2089 } 2090 impl<O, T> Default for RegistrationVerificationOptions<'_, '_, O, T> { 2091 /// Same as [`Self::new`]. 2092 #[inline] 2093 fn default() -> Self { 2094 Self { 2095 allowed_origins: &[], 2096 allowed_top_origins: None, 2097 backup_requirement: BackupReq::default(), 2098 error_on_unsolicited_extensions: true, 2099 require_authenticator_attachment: false, 2100 #[cfg(feature = "serde_relaxed")] 2101 client_data_json_relaxed: true, 2102 } 2103 } 2104 } 2105 /// `PrfInput` without the actual data sent to reduce memory usage when storing [`RegistrationServerState`] in an 2106 /// in-memory collection. 2107 #[derive(Clone, Copy, Debug)] 2108 enum ServerPrfInfo { 2109 /// No `PrfInput`. 2110 None, 2111 /// `PrfInput::second` was `None`. 2112 One(ExtensionInfo), 2113 /// `PrfInput::second` was `Some`. 2114 Two(ExtensionInfo), 2115 } 2116 #[cfg(test)] 2117 impl PartialEq for ServerPrfInfo { 2118 fn eq(&self, other: &Self) -> bool { 2119 match *self { 2120 Self::None => matches!(*other, Self::None), 2121 Self::One(info) => matches!(*other, Self::One(info2) if info == info2), 2122 Self::Two(info) => matches!(*other, Self::Two(info2) if info == info2), 2123 } 2124 } 2125 } 2126 impl From<Option<(PrfInput<'_, '_>, ExtensionInfo)>> for ServerPrfInfo { 2127 fn from(value: Option<(PrfInput<'_, '_>, ExtensionInfo)>) -> Self { 2128 value.map_or(Self::None, |val| { 2129 val.0 2130 .second 2131 .map_or_else(|| Self::One(val.1), |_| Self::Two(val.1)) 2132 }) 2133 } 2134 } 2135 /// `Extension` without the actual data sent to reduce memory usage when storing [`AuthenticationServerState`] 2136 /// in an in-memory collection. 2137 #[derive(Clone, Copy, Debug)] 2138 struct ServerExtensionInfo { 2139 /// `Extension::cred_props`. 2140 cred_props: Option<ExtensionReq>, 2141 /// `Extension::cred_protect`. 2142 cred_protect: CredProtect, 2143 /// `Extension::min_pin_length`. 2144 min_pin_length: Option<(FourToSixtyThree, ExtensionInfo)>, 2145 /// `Extension::prf`. 2146 prf: ServerPrfInfo, 2147 } 2148 impl ServerExtensionInfo { 2149 /// Validates the extensions. 2150 fn validate( 2151 self, 2152 client_ext: ClientExtensionsOutputs, 2153 auth_ext: AuthenticatorExtensionOutput, 2154 error_unsolicited: bool, 2155 ) -> Result<(), ExtensionErr> { 2156 if error_unsolicited { 2157 self.validate_unsolicited(client_ext, auth_ext) 2158 } else { 2159 Ok(()) 2160 } 2161 .and_then(|()| { 2162 self.validate_required(client_ext, auth_ext) 2163 .and_then(|()| self.validate_value(client_ext, auth_ext)) 2164 }) 2165 } 2166 /// Validates if there are any unsolicited extensions. 2167 /// 2168 /// Note no distinction is made between an extension that is empty and one that is not (i.e., we are checking 2169 /// purely for the existence of extension keys). 2170 fn validate_unsolicited( 2171 mut self, 2172 client_ext: ClientExtensionsOutputs, 2173 auth_ext: AuthenticatorExtensionOutput, 2174 ) -> Result<(), ExtensionErr> { 2175 // For simpler code, we artificially set non-requested extensions after verifying there was not an error 2176 // and recursively call this function. There are so few extensions and the checks are fast that there 2177 // should be no worry of stack overflow or performance overhead. 2178 if self.cred_props.is_some() { 2179 if !matches!(self.cred_protect, CredProtect::None) { 2180 if self.min_pin_length.is_some() { 2181 // This is the last extension, so recursion stops here. 2182 if !matches!(self.prf, ServerPrfInfo::None) { 2183 Ok(()) 2184 } else if client_ext.prf.is_some() { 2185 Err(ExtensionErr::ForbiddenPrf) 2186 } else if !matches!(auth_ext.hmac_secret, HmacSecret::None) { 2187 Err(ExtensionErr::ForbiddenHmacSecret) 2188 } else { 2189 Ok(()) 2190 } 2191 } else if auth_ext.min_pin_length.is_some() { 2192 Err(ExtensionErr::ForbiddenMinPinLength) 2193 } else { 2194 // Pretend to set `minPinLength`, so we can check `prf`. 2195 self.min_pin_length = 2196 Some((FourToSixtyThree::Four, ExtensionInfo::RequireEnforceValue)); 2197 self.validate_unsolicited(client_ext, auth_ext) 2198 } 2199 } else if !matches!(auth_ext.cred_protect, CredentialProtectionPolicy::None) { 2200 Err(ExtensionErr::ForbiddenCredProtect) 2201 } else { 2202 // Pretend to set `credProtect`, so we can check `minPinLength` and `prf` extensions. 2203 self.cred_protect = CredProtect::UserVerificationOptional( 2204 false, 2205 ExtensionInfo::RequireEnforceValue, 2206 ); 2207 self.validate_unsolicited(client_ext, auth_ext) 2208 } 2209 } else if client_ext.cred_props.is_some() { 2210 Err(ExtensionErr::ForbiddenCredProps) 2211 } else { 2212 // Pretend to set `credProps`; so we can check `credProtect`, `minPinLength`, and `prf` extensions. 2213 self.cred_props = Some(ExtensionReq::Require); 2214 self.validate_unsolicited(client_ext, auth_ext) 2215 } 2216 } 2217 /// Validates if any required extensions don't have a corresponding response. 2218 /// 2219 /// Note empty extensions are treated as missing. For example when requiring the `credProps` extension, 2220 /// all of the following responses would lead to a failure: 2221 /// `{"clientExtensionResults":{}}`: no extensions. 2222 /// `{"clientExtensionResults":{"prf":true}}`: only the `prf` extension. 2223 /// `{"clientExtensionResults":{"credProps":{}}}`: empty `credProps` extension. 2224 /// `{"clientExtensionResults":{"credProps":{"foo":false}}}`: `credProps` extension doesn't contain at least one 2225 /// expected field (i.e., still "empty"). 2226 fn validate_required( 2227 self, 2228 client_ext: ClientExtensionsOutputs, 2229 auth_ext: AuthenticatorExtensionOutput, 2230 ) -> Result<(), ExtensionErr> { 2231 // We don't check `self.cred_protect` since `CredProtect::validate` checks for both a required response 2232 // and value enforcement; thus it only needs to be checked once (which it is in `Self::validate_value`). 2233 self.cred_props 2234 .map_or(Ok(()), |info| { 2235 if matches!(info, ExtensionReq::Require) { 2236 if client_ext 2237 .cred_props 2238 .is_some_and(|props| props.rk.is_some()) 2239 { 2240 Ok(()) 2241 } else { 2242 Err(ExtensionErr::MissingCredProps) 2243 } 2244 } else { 2245 Ok(()) 2246 } 2247 }) 2248 .and_then(|()| { 2249 self.min_pin_length 2250 .map_or(Ok(()), |info| { 2251 if matches!( 2252 info.1, 2253 ExtensionInfo::RequireEnforceValue 2254 | ExtensionInfo::RequireDontEnforceValue 2255 ) { 2256 auth_ext 2257 .min_pin_length 2258 .ok_or(ExtensionErr::MissingMinPinLength) 2259 .map(|_| ()) 2260 } else { 2261 Ok(()) 2262 } 2263 }) 2264 .and_then(|()| match self.prf { 2265 ServerPrfInfo::None => Ok(()), 2266 ServerPrfInfo::One(info) | ServerPrfInfo::Two(info) => { 2267 if matches!( 2268 info, 2269 ExtensionInfo::RequireEnforceValue 2270 | ExtensionInfo::RequireDontEnforceValue 2271 ) { 2272 if client_ext.prf.is_some() { 2273 Ok(()) 2274 } else { 2275 Err(ExtensionErr::MissingPrf) 2276 } 2277 } else { 2278 Ok(()) 2279 } 2280 } 2281 }) 2282 }) 2283 } 2284 /// Validates the value of any extensions sent from the client. 2285 /// 2286 /// Note missing and empty extensions are always OK. 2287 fn validate_value( 2288 self, 2289 client_ext: ClientExtensionsOutputs, 2290 auth_ext: AuthenticatorExtensionOutput, 2291 ) -> Result<(), ExtensionErr> { 2292 // This also checks for a missing response. Instead of duplicating that check, we only call 2293 // `self.cred_protect.validate` once here and not also in `Self::validate_required`. 2294 self.cred_protect 2295 .validate(auth_ext.cred_protect) 2296 .and_then(|()| { 2297 self.min_pin_length 2298 .map_or(Ok(()), |info| { 2299 if matches!( 2300 info.1, 2301 ExtensionInfo::RequireEnforceValue | ExtensionInfo::AllowEnforceValue 2302 ) { 2303 auth_ext.min_pin_length.map_or(Ok(()), |pin| { 2304 if pin >= info.0 { 2305 Ok(()) 2306 } else { 2307 Err(ExtensionErr::InvalidMinPinLength(info.0, pin)) 2308 } 2309 }) 2310 } else { 2311 Ok(()) 2312 } 2313 }) 2314 .and_then(|()| match self.prf { 2315 ServerPrfInfo::None => Ok(()), 2316 ServerPrfInfo::One(info) | ServerPrfInfo::Two(info) => { 2317 if matches!( 2318 info, 2319 ExtensionInfo::RequireEnforceValue 2320 | ExtensionInfo::AllowEnforceValue 2321 ) { 2322 client_ext 2323 .prf 2324 .map_or(Ok(()), |prf| { 2325 if prf.enabled { 2326 Ok(()) 2327 } else { 2328 Err(ExtensionErr::InvalidPrfValue) 2329 } 2330 }) 2331 .and({ 2332 if matches!(auth_ext.hmac_secret, HmacSecret::NotEnabled) { 2333 Err(ExtensionErr::InvalidHmacSecretValue) 2334 } else { 2335 Ok(()) 2336 } 2337 }) 2338 } else { 2339 Ok(()) 2340 } 2341 } 2342 }) 2343 }) 2344 } 2345 } 2346 impl From<Extension<'_, '_>> for ServerExtensionInfo { 2347 fn from(value: Extension<'_, '_>) -> Self { 2348 Self { 2349 cred_props: value.cred_props, 2350 cred_protect: value.cred_protect, 2351 min_pin_length: value.min_pin_length, 2352 prf: value.prf.into(), 2353 } 2354 } 2355 } 2356 #[cfg(test)] 2357 impl PartialEq for ServerExtensionInfo { 2358 fn eq(&self, other: &Self) -> bool { 2359 self.prf == other.prf 2360 } 2361 } 2362 // This is essentially the `PublicKeyCredentialCreationOptions` used to create it; however to reduce 2363 // memory usage, we remove all unnecessary data making an instance of this 48 bytes in size on 2364 // `x86_64-unknown-linux-gnu` platforms when `USER_LEN` is `USER_HANDLE_MIN_LEN`. 2365 /// State needed to be saved when beginning the registration ceremony. 2366 /// 2367 /// Saves the necessary information associated with the [`CredentialCreationOptions`] used to create it 2368 /// via [`CredentialCreationOptions::start_ceremony`] so that registration of a new credential can be 2369 /// performed with [`Self::verify`]. 2370 /// 2371 /// `RegistrationServerState` implements [`Borrow`] of [`SentChallenge`]; thus to obtain the correct 2372 /// `RegistrationServerState` associated with a [`Registration`], one should use its corresponding 2373 /// [`Registration::challenge`]. 2374 #[derive(Debug)] 2375 pub struct RegistrationServerState<const USER_LEN: usize> { 2376 /// [`mediation`](https://www.w3.org/TR/credential-management-1/#dom-credentialcreationoptions-mediation). 2377 mediation: CredentialMediationRequirement, 2378 // This is a `SentChallenge` since we need `RegistrationServerState` to be fetchable after receiving the 2379 // response from the client. This response must obviously be constructable; thus its challenge is a 2380 // `SentChallenge`. 2381 // 2382 // This must never be mutated since we want to ensure it is actually a `Challenge` (which 2383 // can only be constructed via `Challenge::new`). This is guaranteed to be true iff 2384 // `serializable_server_state` is not enabled. We avoid implementing `trait`s like `Hash` when that 2385 // is enabled. 2386 /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-challenge). 2387 challenge: SentChallenge, 2388 /// [`pubKeyCredParams`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-pubkeycredparams). 2389 pub_key_cred_params: CoseAlgorithmIdentifiers, 2390 /// [`authenticatorSelection`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-authenticatorselection). 2391 authenticator_selection: AuthenticatorSelectionCriteria, 2392 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions). 2393 extensions: ServerExtensionInfo, 2394 /// `Instant` the ceremony expires. 2395 #[cfg(not(feature = "serializable_server_state"))] 2396 expiration: Instant, 2397 /// `SystemTime` the ceremony expires. 2398 #[cfg(feature = "serializable_server_state")] 2399 expiration: SystemTime, 2400 /// User handle. 2401 user_id: UserHandle<USER_LEN>, 2402 } 2403 impl<const USER_LEN: usize> RegistrationServerState<USER_LEN> { 2404 /// Verifies `response` is valid based on `self` consuming `self` and returning a `RegisteredCredential` that 2405 /// borrows the necessary data from `response`. 2406 /// 2407 /// `rp_id` MUST be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] used when starting the 2408 /// ceremony. 2409 /// 2410 /// It is _essential_ to ensure [`RegisteredCredential::id`] has not been previously registered; if 2411 /// so, the ceremony SHOULD be aborted and a failure reported. When saving `RegisteredCredential`, one may 2412 /// want to save the [`RpId`] and [`PublicKeyCredentialUserEntity`] information; however since [`RpId`] is 2413 /// likely static, that may not be necessary. User information is also likely static for a given [`UserHandle`] 2414 /// (which is saved in `RegisteredCredential`); so if such info is saved, one may want to save it once per 2415 /// `UserHandle` and not per `RegisteredCredential`. 2416 /// 2417 /// # Errors 2418 /// 2419 /// Errors iff `response` is not valid according to the 2420 /// [registration ceremony criteria](https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential) 2421 /// or violates any of the settings in `options`. 2422 #[inline] 2423 pub fn verify<'a, O: PartialEq<Origin<'a>>, T: PartialEq<Origin<'a>>>( 2424 self, 2425 rp_id: &RpId, 2426 response: &'a Registration, 2427 options: &RegistrationVerificationOptions<'_, '_, O, T>, 2428 ) -> Result<RegisteredCredential<'a, USER_LEN>, RegCeremonyErr> { 2429 // [Registration ceremony](https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential) 2430 // is handled by: 2431 // 2432 // 1. Calling code. 2433 // 2. Client code and the construction of `resp` (hopefully via [`Registration::deserialize`]). 2434 // 3. Client code and the construction of `resp` (hopefully via [`AuthenticatorAttestation::deserialize`]). 2435 // 4. Client code and the construction of `resp` (hopefully via [`ClientExtensionsOutputs::deserialize`]). 2436 // 5. [`Self::partial_validate`]. 2437 // 6. [`Self::partial_validate`]. 2438 // 7. [`Self::partial_validate`]. 2439 // 8. [`Self::partial_validate`]. 2440 // 9. [`Self::partial_validate`]. 2441 // 10. [`Self::partial_validate`]. 2442 // 11. [`Self::partial_validate`]. 2443 // 12. [`Self::partial_validate`]. 2444 // 13. [`Self::partial_validate`]. 2445 // 14. [`Self::partial_validate`]. 2446 // 15. Below. 2447 // 16. [`Self::partial_validate`]. 2448 // 17. [`Self::partial_validate`]. 2449 // 18. [`Self::partial_validate`]. 2450 // 19. [`Self::partial_validate`]. 2451 // 20. Below. 2452 // 21. [`Self::partial_validate`]. 2453 // 22. [`Self::partial_validate`]. 2454 // 23. N/A since only none and self attestations are supported. 2455 // 24. Always satisfied since only none and self attestations are supported (Item 3 is N/A). 2456 // 25. [`Self::partial_validate`]. 2457 // 26. Calling code. 2458 // 27. Below. 2459 // 28. N/A since only none and self attestations are supported. 2460 // 29. Below. 2461 2462 // Steps 5–14, 16–19, 21–22, and 25. 2463 self.partial_validate(rp_id, response, (), &options.into()) 2464 .map_err(RegCeremonyErr::from) 2465 .and_then(|attestation_object| { 2466 let auth_data = attestation_object.auth_data(); 2467 let flags = auth_data.flags(); 2468 // Step 15. 2469 if matches!(self.mediation, CredentialMediationRequirement::Conditional) 2470 || flags.user_present 2471 { 2472 self.authenticator_selection 2473 // Verify any required authenticator attachment modality. 2474 .validate( 2475 options.require_authenticator_attachment, 2476 response.authenticator_attachment, 2477 ) 2478 .and_then(|()| { 2479 let attested_credential_data = auth_data.attested_credential_data(); 2480 self.pub_key_cred_params 2481 // Step 20. 2482 .validate(attested_credential_data.credential_public_key) 2483 .and_then(|()| { 2484 let extensions = auth_data.extensions(); 2485 // Step 27. 2486 self.extensions 2487 .validate( 2488 response.client_extension_results, 2489 extensions, 2490 options.error_on_unsolicited_extensions, 2491 ) 2492 .map_err(RegCeremonyErr::Extension) 2493 .and_then(|()| { 2494 // Step 29. 2495 RegisteredCredential::new( 2496 attested_credential_data.credential_id, 2497 response.response.transports(), 2498 self.user_id, 2499 StaticState { 2500 credential_public_key: attested_credential_data 2501 .credential_public_key, 2502 extensions: extensions.into(), 2503 client_extension_results: response 2504 .client_extension_results 2505 .into(), 2506 }, 2507 DynamicState { 2508 user_verified: flags.user_verified, 2509 backup: flags.backup, 2510 sign_count: auth_data.sign_count(), 2511 authenticator_attachment: response 2512 .authenticator_attachment, 2513 }, 2514 Metadata { 2515 attestation: match attestation_object 2516 .attestation() 2517 { 2518 AttestationFormat::None => { 2519 Attestation::None 2520 } 2521 AttestationFormat::Packed(_) => { 2522 Attestation::Surrogate 2523 } 2524 }, 2525 aaguid: attested_credential_data.aaguid, 2526 extensions: extensions.into(), 2527 client_extension_results: response 2528 .client_extension_results 2529 .into(), 2530 resident_key: self 2531 .authenticator_selection 2532 .resident_key, 2533 }, 2534 ) 2535 .map_err(RegCeremonyErr::Credential) 2536 }) 2537 }) 2538 }) 2539 } else { 2540 Err(RegCeremonyErr::UserNotPresent) 2541 } 2542 }) 2543 } 2544 } 2545 #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))] 2546 impl<const USER_LEN: usize> RegistrationServerState<USER_LEN> { 2547 fn is_eq(&self, other: &Self) -> bool { 2548 self.mediation == other.mediation 2549 && self.challenge == other.challenge 2550 && self.pub_key_cred_params == other.pub_key_cred_params 2551 && self.authenticator_selection == other.authenticator_selection 2552 && self.extensions == other.extensions 2553 && self.expiration == other.expiration 2554 && self.user_id == other.user_id 2555 } 2556 } 2557 impl<const USER_LEN: usize> TimedCeremony for RegistrationServerState<USER_LEN> { 2558 #[cfg(any(doc, not(feature = "serializable_server_state")))] 2559 #[inline] 2560 fn expiration(&self) -> Instant { 2561 self.expiration 2562 } 2563 #[cfg(all(not(doc), feature = "serializable_server_state"))] 2564 #[inline] 2565 fn expiration(&self) -> SystemTime { 2566 self.expiration 2567 } 2568 } 2569 impl<const USER_LEN: usize> Ceremony<USER_LEN, false> for RegistrationServerState<USER_LEN> { 2570 type R = Registration; 2571 fn rand_challenge(&self) -> SentChallenge { 2572 self.challenge 2573 } 2574 #[cfg(not(feature = "serializable_server_state"))] 2575 fn expiry(&self) -> Instant { 2576 self.expiration 2577 } 2578 #[cfg(feature = "serializable_server_state")] 2579 fn expiry(&self) -> SystemTime { 2580 self.expiration 2581 } 2582 fn user_verification(&self) -> UserVerificationRequirement { 2583 self.authenticator_selection.user_verification 2584 } 2585 } 2586 impl<const USER_LEN: usize> Borrow<SentChallenge> for RegistrationServerState<USER_LEN> { 2587 #[inline] 2588 fn borrow(&self) -> &SentChallenge { 2589 &self.challenge 2590 } 2591 } 2592 impl<const USER_LEN: usize> PartialEq for RegistrationServerState<USER_LEN> { 2593 #[inline] 2594 fn eq(&self, other: &Self) -> bool { 2595 self.challenge == other.challenge 2596 } 2597 } 2598 impl<const USER_LEN: usize> PartialEq<&Self> for RegistrationServerState<USER_LEN> { 2599 #[inline] 2600 fn eq(&self, other: &&Self) -> bool { 2601 *self == **other 2602 } 2603 } 2604 impl<const USER_LEN: usize> PartialEq<RegistrationServerState<USER_LEN>> 2605 for &RegistrationServerState<USER_LEN> 2606 { 2607 #[inline] 2608 fn eq(&self, other: &RegistrationServerState<USER_LEN>) -> bool { 2609 **self == *other 2610 } 2611 } 2612 impl<const USER_LEN: usize> Eq for RegistrationServerState<USER_LEN> {} 2613 impl<const USER_LEN: usize> Hash for RegistrationServerState<USER_LEN> { 2614 #[inline] 2615 fn hash<H: Hasher>(&self, state: &mut H) { 2616 self.challenge.hash(state); 2617 } 2618 } 2619 impl<const USER_LEN: usize> PartialOrd for RegistrationServerState<USER_LEN> { 2620 #[inline] 2621 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { 2622 Some(self.cmp(other)) 2623 } 2624 } 2625 impl<const USER_LEN: usize> Ord for RegistrationServerState<USER_LEN> { 2626 #[inline] 2627 fn cmp(&self, other: &Self) -> Ordering { 2628 self.challenge.cmp(&other.challenge) 2629 } 2630 } 2631 /// `RegistrationServerState` based on a [`UserHandle64`]. 2632 pub type RegistrationServerState64 = RegistrationServerState<USER_HANDLE_MAX_LEN>; 2633 /// `RegistrationServerState` based on a [`UserHandle16`]. 2634 pub type RegistrationServerState16 = RegistrationServerState<16>; 2635 #[cfg(test)] 2636 mod tests { 2637 #[cfg(all( 2638 feature = "custom", 2639 any( 2640 feature = "serializable_server_state", 2641 not(any(feature = "bin", feature = "serde")) 2642 ) 2643 ))] 2644 use super::{ 2645 super::{super::AggErr, ExtensionInfo}, 2646 Challenge, CredProtect, CredentialCreationOptions, FourToSixtyThree, PrfInput, RpId, 2647 UserHandle, 2648 }; 2649 #[cfg(all(feature = "custom", feature = "serializable_server_state"))] 2650 use super::{ 2651 super::{ 2652 super::bin::{Decode as _, Encode as _}, 2653 AsciiDomain, 2654 }, 2655 Extension, PublicKeyCredentialUserEntity, RegistrationServerState, 2656 }; 2657 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2658 use super::{ 2659 super::{ 2660 super::{ 2661 CredentialErr, 2662 response::register::{ 2663 AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation, 2664 ClientExtensionsOutputs, CredentialPropertiesOutput, 2665 CredentialProtectionPolicy, HmacSecret, 2666 }, 2667 }, 2668 AuthTransports, 2669 }, 2670 AuthenticatorAttachment, BackupReq, ExtensionErr, ExtensionReq, RegCeremonyErr, 2671 Registration, RegistrationVerificationOptions, UserVerificationRequirement, 2672 }; 2673 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2674 use rsa::sha2::{Digest as _, Sha256}; 2675 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2676 const CBOR_UINT: u8 = 0b000_00000; 2677 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2678 const CBOR_NEG: u8 = 0b001_00000; 2679 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2680 const CBOR_BYTES: u8 = 0b010_00000; 2681 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2682 const CBOR_TEXT: u8 = 0b011_00000; 2683 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2684 const CBOR_MAP: u8 = 0b101_00000; 2685 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2686 const CBOR_SIMPLE: u8 = 0b111_00000; 2687 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2688 const CBOR_FALSE: u8 = CBOR_SIMPLE | 20; 2689 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2690 const CBOR_TRUE: u8 = CBOR_SIMPLE | 21; 2691 #[test] 2692 #[cfg(all(feature = "custom", feature = "serializable_server_state"))] 2693 fn eddsa_reg_ser() -> Result<(), AggErr> { 2694 let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); 2695 let id = UserHandle::from([0; 1]); 2696 let mut opts = CredentialCreationOptions::passkey( 2697 &rp_id, 2698 PublicKeyCredentialUserEntity { 2699 name: "foo".try_into()?, 2700 id: &id, 2701 display_name: None, 2702 }, 2703 Vec::new(), 2704 ); 2705 opts.public_key.challenge = Challenge(0); 2706 opts.public_key.extensions = Extension { 2707 cred_props: None, 2708 cred_protect: CredProtect::UserVerificationRequired( 2709 false, 2710 ExtensionInfo::RequireEnforceValue, 2711 ), 2712 min_pin_length: Some((FourToSixtyThree::Ten, ExtensionInfo::RequireEnforceValue)), 2713 prf: Some(( 2714 PrfInput { 2715 first: [0].as_slice(), 2716 second: None, 2717 }, 2718 ExtensionInfo::RequireEnforceValue, 2719 )), 2720 }; 2721 let server = opts.start_ceremony()?.0; 2722 let enc_data = server 2723 .encode() 2724 .map_err(AggErr::EncodeRegistrationServerState)?; 2725 assert_eq!(enc_data.capacity(), enc_data.len()); 2726 assert_eq!(enc_data.len(), 1 + 16 + 1 + 4 + (1 + 3 + 3 + 2) + 12 + 1); 2727 assert!(server.is_eq(&RegistrationServerState::decode(enc_data.as_slice())?)); 2728 Ok(()) 2729 } 2730 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2731 #[derive(Clone, Copy)] 2732 struct TestResponseOptions { 2733 user_verified: bool, 2734 cred_protect: CredentialProtectionPolicy, 2735 prf: Option<bool>, 2736 hmac: HmacSecret, 2737 min_pin: Option<FourToSixtyThree>, 2738 cred_props: Option<Option<bool>>, 2739 } 2740 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2741 #[derive(Clone, Copy)] 2742 enum PrfUvOptions { 2743 /// `true` iff `UserVerificationRequirement::Required` should be used; otherwise 2744 /// `UserVerificationRequirement::Preferred` is used. 2745 None(bool), 2746 Prf(ExtensionInfo), 2747 } 2748 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2749 #[derive(Clone, Copy)] 2750 struct TestRequestOptions { 2751 error_unsolicited: bool, 2752 protect: CredProtect, 2753 prf_uv: PrfUvOptions, 2754 props: Option<ExtensionReq>, 2755 pin: Option<(FourToSixtyThree, ExtensionInfo)>, 2756 } 2757 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2758 #[derive(Clone, Copy)] 2759 struct TestOptions { 2760 request: TestRequestOptions, 2761 response: TestResponseOptions, 2762 } 2763 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2764 fn generate_client_data_json() -> Vec<u8> { 2765 let mut json = Vec::with_capacity(256); 2766 json.extend_from_slice(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()); 2767 json 2768 } 2769 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 2770 fn generate_attestation_object(options: TestResponseOptions) -> Vec<u8> { 2771 let mut attestation_object = Vec::with_capacity(256); 2772 attestation_object.extend_from_slice( 2773 [ 2774 CBOR_MAP | 3, 2775 CBOR_TEXT | 3, 2776 b'f', 2777 b'm', 2778 b't', 2779 CBOR_TEXT | 4, 2780 b'n', 2781 b'o', 2782 b'n', 2783 b'e', 2784 CBOR_TEXT | 7, 2785 b'a', 2786 b't', 2787 b't', 2788 b'S', 2789 b't', 2790 b'm', 2791 b't', 2792 CBOR_MAP, 2793 CBOR_TEXT | 8, 2794 b'a', 2795 b'u', 2796 b't', 2797 b'h', 2798 b'D', 2799 b'a', 2800 b't', 2801 b'a', 2802 CBOR_BYTES | 24, 2803 // Length. 2804 113 + if matches!(options.cred_protect, CredentialProtectionPolicy::None) { 2805 if matches!(options.hmac, HmacSecret::None) { 2806 options.min_pin.map_or(0, |_| 15) 2807 } else { 2808 14 + options.min_pin.map_or(0, |_| 14) 2809 + match options.hmac { 2810 HmacSecret::None => unreachable!("bug"), 2811 HmacSecret::NotEnabled | HmacSecret::Enabled => 0, 2812 HmacSecret::One => 65, 2813 HmacSecret::Two => 97, 2814 } 2815 } 2816 } else { 2817 14 + if matches!(options.hmac, HmacSecret::None) { 2818 0 2819 } else { 2820 13 2821 } + options.min_pin.map_or(0, |_| 14) 2822 + match options.hmac { 2823 HmacSecret::None | HmacSecret::NotEnabled | HmacSecret::Enabled => 0, 2824 HmacSecret::One => 65, 2825 HmacSecret::Two => 97, 2826 } 2827 }, 2828 // RP ID HASH. 2829 // This will be overwritten later. 2830 0, 2831 0, 2832 0, 2833 0, 2834 0, 2835 0, 2836 0, 2837 0, 2838 0, 2839 0, 2840 0, 2841 0, 2842 0, 2843 0, 2844 0, 2845 0, 2846 0, 2847 0, 2848 0, 2849 0, 2850 0, 2851 0, 2852 0, 2853 0, 2854 0, 2855 0, 2856 0, 2857 0, 2858 0, 2859 0, 2860 0, 2861 0, 2862 // FLAGS. 2863 // UP, UV, AT, and ED (right-to-left). 2864 0b0100_0001 2865 | if options.user_verified { 2866 0b0000_0100 2867 } else { 2868 0b0000_0000 2869 } 2870 | if matches!(options.cred_protect, CredentialProtectionPolicy::None) 2871 && matches!(options.hmac, HmacSecret::None) 2872 && options.min_pin.is_none() 2873 { 2874 0 2875 } else { 2876 0b1000_0000 2877 }, 2878 // COUNTER. 2879 // 0 as 32-bit big endian. 2880 0, 2881 0, 2882 0, 2883 0, 2884 // AAGUID. 2885 0, 2886 0, 2887 0, 2888 0, 2889 0, 2890 0, 2891 0, 2892 0, 2893 0, 2894 0, 2895 0, 2896 0, 2897 0, 2898 0, 2899 0, 2900 0, 2901 // L. 2902 // CREDENTIAL ID length is 16 as 16-bit big endian. 2903 0, 2904 16, 2905 // CREDENTIAL ID. 2906 0, 2907 0, 2908 0, 2909 0, 2910 0, 2911 0, 2912 0, 2913 0, 2914 0, 2915 0, 2916 0, 2917 0, 2918 0, 2919 0, 2920 0, 2921 0, 2922 CBOR_MAP | 4, 2923 // COSE kty. 2924 CBOR_UINT | 1, 2925 // COSE OKP. 2926 CBOR_UINT | 1, 2927 // COSE alg. 2928 CBOR_UINT | 3, 2929 // COSE Eddsa. 2930 CBOR_NEG | 7, 2931 // COSE OKP crv. 2932 CBOR_NEG, 2933 // COSE Ed25519. 2934 CBOR_UINT | 6, 2935 // COSE OKP x. 2936 CBOR_NEG | 1, 2937 CBOR_BYTES | 24, 2938 // Length is 32. 2939 32, 2940 // Compressed-y coordinate. 2941 59, 2942 106, 2943 39, 2944 188, 2945 206, 2946 182, 2947 164, 2948 45, 2949 98, 2950 163, 2951 168, 2952 208, 2953 42, 2954 111, 2955 13, 2956 115, 2957 101, 2958 50, 2959 21, 2960 119, 2961 29, 2962 226, 2963 67, 2964 166, 2965 58, 2966 192, 2967 72, 2968 161, 2969 139, 2970 89, 2971 218, 2972 41, 2973 ] 2974 .as_slice(), 2975 ); 2976 attestation_object[30..62] 2977 .copy_from_slice(Sha256::digest("example.com".as_bytes()).as_slice()); 2978 if matches!(options.cred_protect, CredentialProtectionPolicy::None) { 2979 if matches!(options.hmac, HmacSecret::None) { 2980 if options.min_pin.is_some() { 2981 attestation_object.push(CBOR_MAP | 1) 2982 } 2983 } else if options.min_pin.is_some() { 2984 attestation_object.push( 2985 CBOR_MAP 2986 | 2 + u8::from(matches!(options.hmac, HmacSecret::One | HmacSecret::Two)), 2987 ); 2988 } else { 2989 attestation_object.push( 2990 CBOR_MAP 2991 | 1 + u8::from(matches!(options.hmac, HmacSecret::One | HmacSecret::Two)), 2992 ); 2993 } 2994 } else { 2995 attestation_object.extend_from_slice( 2996 [ 2997 CBOR_MAP | 1 + match options.hmac { HmacSecret::None => 0, HmacSecret::NotEnabled | HmacSecret::Enabled => 1, HmacSecret::One | HmacSecret::Two => 2, } + u8::from(options.min_pin.is_some()), 2998 // CBOR text of length 11. 2999 CBOR_TEXT | 11, 3000 b'c', 3001 b'r', 3002 b'e', 3003 b'd', 3004 b'P', 3005 b'r', 3006 b'o', 3007 b't', 3008 b'e', 3009 b'c', 3010 b't', 3011 match options.cred_protect { CredentialProtectionPolicy::None => unreachable!("bug"), CredentialProtectionPolicy::UserVerificationOptional => 1, CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList => 2, CredentialProtectionPolicy::UserVerificationRequired => 3, }, 3012 ].as_slice() 3013 ) 3014 } 3015 if !matches!(options.hmac, HmacSecret::None) { 3016 attestation_object.extend_from_slice( 3017 [ 3018 // CBOR text of length 11. 3019 CBOR_TEXT | 11, 3020 b'h', 3021 b'm', 3022 b'a', 3023 b'c', 3024 b'-', 3025 b's', 3026 b'e', 3027 b'c', 3028 b'r', 3029 b'e', 3030 b't', 3031 if matches!(options.hmac, HmacSecret::NotEnabled) { 3032 CBOR_FALSE 3033 } else { 3034 CBOR_TRUE 3035 }, 3036 ] 3037 .as_slice(), 3038 ); 3039 } 3040 _ = options.min_pin.map(|p| { 3041 assert!(p <= FourToSixtyThree::TwentyThree, "bug"); 3042 attestation_object.extend_from_slice( 3043 [ 3044 // CBOR text of length 12. 3045 CBOR_TEXT | 12, 3046 b'm', 3047 b'i', 3048 b'n', 3049 b'P', 3050 b'i', 3051 b'n', 3052 b'L', 3053 b'e', 3054 b'n', 3055 b'g', 3056 b't', 3057 b'h', 3058 CBOR_UINT | p.into_u8(), 3059 ] 3060 .as_slice(), 3061 ); 3062 }); 3063 if matches!(options.hmac, HmacSecret::One | HmacSecret::Two) { 3064 attestation_object.extend_from_slice( 3065 [ 3066 // CBOR text of length 14. 3067 CBOR_TEXT | 14, 3068 b'h', 3069 b'm', 3070 b'a', 3071 b'c', 3072 b'-', 3073 b's', 3074 b'e', 3075 b'c', 3076 b'r', 3077 b'e', 3078 b't', 3079 b'-', 3080 b'm', 3081 b'c', 3082 CBOR_BYTES | 24, 3083 ] 3084 .as_slice(), 3085 ); 3086 if matches!(options.hmac, HmacSecret::One) { 3087 attestation_object.push(48); 3088 attestation_object.extend_from_slice([1; 48].as_slice()); 3089 } else { 3090 attestation_object.push(80); 3091 attestation_object.extend_from_slice([1; 80].as_slice()); 3092 } 3093 } 3094 attestation_object 3095 } 3096 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 3097 fn validate(options: TestOptions) -> Result<(), AggErr> { 3098 let rp_id = RpId::Domain("example.com".to_owned().try_into()?); 3099 let registration = Registration::new( 3100 AuthenticatorAttestation::new( 3101 generate_client_data_json(), 3102 generate_attestation_object(options.response), 3103 AuthTransports::NONE, 3104 ), 3105 AuthenticatorAttachment::None, 3106 ClientExtensionsOutputs { 3107 cred_props: options 3108 .response 3109 .cred_props 3110 .map(|rk| CredentialPropertiesOutput { rk }), 3111 prf: options 3112 .response 3113 .prf 3114 .map(|enabled| AuthenticationExtensionsPrfOutputs { enabled }), 3115 }, 3116 ); 3117 let reg_opts = RegistrationVerificationOptions::<'static, 'static, &str, &str> { 3118 allowed_origins: [].as_slice(), 3119 allowed_top_origins: None, 3120 backup_requirement: BackupReq::None, 3121 error_on_unsolicited_extensions: options.request.error_unsolicited, 3122 require_authenticator_attachment: false, 3123 #[cfg(feature = "serde_relaxed")] 3124 client_data_json_relaxed: false, 3125 }; 3126 let user = UserHandle::from([0; 1]); 3127 let mut opts = CredentialCreationOptions::first_passkey_with_blank_user_info(&rp_id, &user); 3128 opts.public_key.challenge = Challenge(0); 3129 opts.public_key.authenticator_selection.user_verification = 3130 UserVerificationRequirement::Preferred; 3131 match options.request.prf_uv { 3132 PrfUvOptions::None(required) => { 3133 if required 3134 || matches!( 3135 options.request.protect, 3136 CredProtect::UserVerificationRequired(_, _) 3137 ) 3138 { 3139 opts.public_key.authenticator_selection.user_verification = 3140 UserVerificationRequirement::Required; 3141 } 3142 } 3143 PrfUvOptions::Prf(info) => { 3144 opts.public_key.authenticator_selection.user_verification = 3145 UserVerificationRequirement::Required; 3146 opts.public_key.extensions.prf = Some(( 3147 PrfInput { 3148 first: [0].as_slice(), 3149 second: None, 3150 }, 3151 info, 3152 )); 3153 } 3154 } 3155 opts.public_key.extensions.cred_protect = options.request.protect; 3156 opts.public_key.extensions.cred_props = options.request.props; 3157 opts.public_key.extensions.min_pin_length = options.request.pin; 3158 opts.start_ceremony()? 3159 .0 3160 .verify(&rp_id, ®istration, ®_opts) 3161 .map_err(AggErr::RegCeremony) 3162 .map(|_| ()) 3163 } 3164 /// Test all, and only, possible `UserNotVerified` errors. 3165 /// 4 * 3 * 5 * 2 * 13 * 5 * 3 * 5 * 4 * 4 = 1,872,000 tests. 3166 /// We ignore this due to how long it takes (around 30 seconds or so). 3167 #[test] 3168 #[ignore] 3169 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 3170 fn test_uv_required_err() { 3171 const ALL_CRED_PROTECTION_OPTIONS: [CredentialProtectionPolicy; 4] = [ 3172 CredentialProtectionPolicy::None, 3173 CredentialProtectionPolicy::UserVerificationOptional, 3174 CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, 3175 CredentialProtectionPolicy::UserVerificationRequired, 3176 ]; 3177 const ALL_PRF_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)]; 3178 const ALL_HMAC_OPTIONS: [HmacSecret; 5] = [ 3179 HmacSecret::None, 3180 HmacSecret::NotEnabled, 3181 HmacSecret::Enabled, 3182 HmacSecret::One, 3183 HmacSecret::Two, 3184 ]; 3185 const ALL_UNSOLICIT_OPTIONS: [bool; 2] = [false, true]; 3186 const ALL_CRED_PROTECT_OPTIONS: [CredProtect; 13] = [ 3187 CredProtect::None, 3188 CredProtect::UserVerificationOptional(false, ExtensionInfo::RequireEnforceValue), 3189 CredProtect::UserVerificationOptional(true, ExtensionInfo::RequireDontEnforceValue), 3190 CredProtect::UserVerificationOptional(false, ExtensionInfo::AllowEnforceValue), 3191 CredProtect::UserVerificationOptional(true, ExtensionInfo::AllowDontEnforceValue), 3192 CredProtect::UserVerificationOptionalWithCredentialIdList( 3193 false, 3194 ExtensionInfo::RequireEnforceValue, 3195 ), 3196 CredProtect::UserVerificationOptionalWithCredentialIdList( 3197 true, 3198 ExtensionInfo::RequireDontEnforceValue, 3199 ), 3200 CredProtect::UserVerificationOptionalWithCredentialIdList( 3201 false, 3202 ExtensionInfo::AllowEnforceValue, 3203 ), 3204 CredProtect::UserVerificationOptionalWithCredentialIdList( 3205 true, 3206 ExtensionInfo::AllowDontEnforceValue, 3207 ), 3208 CredProtect::UserVerificationRequired(false, ExtensionInfo::RequireEnforceValue), 3209 CredProtect::UserVerificationRequired(true, ExtensionInfo::RequireDontEnforceValue), 3210 CredProtect::UserVerificationRequired(false, ExtensionInfo::AllowEnforceValue), 3211 CredProtect::UserVerificationRequired(true, ExtensionInfo::AllowDontEnforceValue), 3212 ]; 3213 const ALL_NOT_FALSE_PRF_UV_OPTIONS: [PrfUvOptions; 5] = [ 3214 PrfUvOptions::None(true), 3215 PrfUvOptions::Prf(ExtensionInfo::RequireEnforceValue), 3216 PrfUvOptions::Prf(ExtensionInfo::RequireDontEnforceValue), 3217 PrfUvOptions::Prf(ExtensionInfo::AllowEnforceValue), 3218 PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue), 3219 ]; 3220 const ALL_PROPS_OPTIONS: [Option<ExtensionReq>; 3] = 3221 [None, Some(ExtensionReq::Require), Some(ExtensionReq::Allow)]; 3222 const ALL_PIN_OPTIONS: [Option<(FourToSixtyThree, ExtensionInfo)>; 5] = [ 3223 None, 3224 Some((FourToSixtyThree::Five, ExtensionInfo::RequireEnforceValue)), 3225 Some(( 3226 FourToSixtyThree::Five, 3227 ExtensionInfo::RequireDontEnforceValue, 3228 )), 3229 Some((FourToSixtyThree::Five, ExtensionInfo::AllowEnforceValue)), 3230 Some((FourToSixtyThree::Five, ExtensionInfo::AllowDontEnforceValue)), 3231 ]; 3232 const ALL_CRED_PROPS_OPTIONS: [Option<Option<bool>>; 4] = 3233 [None, Some(None), Some(Some(false)), Some(Some(true))]; 3234 const ALL_MIN_PIN_OPTIONS: [Option<FourToSixtyThree>; 4] = [ 3235 None, 3236 Some(FourToSixtyThree::Four), 3237 Some(FourToSixtyThree::Five), 3238 Some(FourToSixtyThree::Six), 3239 ]; 3240 for cred_protect in ALL_CRED_PROTECTION_OPTIONS { 3241 for prf in ALL_PRF_OPTIONS { 3242 for hmac in ALL_HMAC_OPTIONS { 3243 for cred_props in ALL_CRED_PROPS_OPTIONS { 3244 for min_pin in ALL_MIN_PIN_OPTIONS { 3245 for error_unsolicited in ALL_UNSOLICIT_OPTIONS { 3246 for protect in ALL_CRED_PROTECT_OPTIONS { 3247 for prf_uv in ALL_NOT_FALSE_PRF_UV_OPTIONS { 3248 for props in ALL_PROPS_OPTIONS { 3249 for pin in ALL_PIN_OPTIONS { 3250 assert!(validate(TestOptions { 3251 request: TestRequestOptions { 3252 error_unsolicited, 3253 protect, 3254 prf_uv, 3255 props, 3256 pin, 3257 }, 3258 response: TestResponseOptions { 3259 user_verified: false, 3260 hmac, 3261 cred_protect, 3262 prf, 3263 min_pin, 3264 cred_props, 3265 }, 3266 }).map_or_else(|err| matches!(err, AggErr::RegCeremony(reg_err) if matches!(reg_err, RegCeremonyErr::UserNotVerified)), |_| false)); 3267 } 3268 } 3269 } 3270 } 3271 } 3272 } 3273 } 3274 } 3275 } 3276 } 3277 } 3278 /// Test all, and only, possible `ForbiddenCredProps` errors. 3279 /// 4 * 3 * 5 * 2 * 13 * 6 * 5 * 3 * 4 = 561,600 3280 /// - 3281 /// 4 * 3 * 5 * 4 * 6 * 5 * 3 * 4 = 86,400 3282 /// - 3283 /// 4 * 3 * 5 * 13 * 5 * 5 * 3 * 4 = 234,000 3284 /// + 3285 /// 4 * 3 * 5 * 4 * 5 * 5 * 3 * 4 = 72,000 3286 /// = 3287 /// 313,200 total tests. 3288 /// We ignore this due to how long it takes (around 6 seconds or so). 3289 #[test] 3290 #[ignore] 3291 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 3292 fn test_forbidden_cred_props() { 3293 const ALL_CRED_PROTECTION_OPTIONS: [CredentialProtectionPolicy; 4] = [ 3294 CredentialProtectionPolicy::None, 3295 CredentialProtectionPolicy::UserVerificationOptional, 3296 CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, 3297 CredentialProtectionPolicy::UserVerificationRequired, 3298 ]; 3299 const ALL_PRF_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)]; 3300 const ALL_HMAC_OPTIONS: [HmacSecret; 5] = [ 3301 HmacSecret::None, 3302 HmacSecret::NotEnabled, 3303 HmacSecret::Enabled, 3304 HmacSecret::One, 3305 HmacSecret::Two, 3306 ]; 3307 const ALL_UV_OPTIONS: [bool; 2] = [false, true]; 3308 const ALL_CRED_PROTECT_OPTIONS: [CredProtect; 13] = [ 3309 CredProtect::None, 3310 CredProtect::UserVerificationOptional(false, ExtensionInfo::RequireEnforceValue), 3311 CredProtect::UserVerificationOptional(true, ExtensionInfo::RequireDontEnforceValue), 3312 CredProtect::UserVerificationOptional(false, ExtensionInfo::AllowEnforceValue), 3313 CredProtect::UserVerificationOptional(true, ExtensionInfo::AllowDontEnforceValue), 3314 CredProtect::UserVerificationOptionalWithCredentialIdList( 3315 false, 3316 ExtensionInfo::RequireEnforceValue, 3317 ), 3318 CredProtect::UserVerificationOptionalWithCredentialIdList( 3319 true, 3320 ExtensionInfo::RequireDontEnforceValue, 3321 ), 3322 CredProtect::UserVerificationOptionalWithCredentialIdList( 3323 false, 3324 ExtensionInfo::AllowEnforceValue, 3325 ), 3326 CredProtect::UserVerificationOptionalWithCredentialIdList( 3327 true, 3328 ExtensionInfo::AllowDontEnforceValue, 3329 ), 3330 CredProtect::UserVerificationRequired(false, ExtensionInfo::RequireEnforceValue), 3331 CredProtect::UserVerificationRequired(true, ExtensionInfo::RequireDontEnforceValue), 3332 CredProtect::UserVerificationRequired(false, ExtensionInfo::AllowEnforceValue), 3333 CredProtect::UserVerificationRequired(true, ExtensionInfo::AllowDontEnforceValue), 3334 ]; 3335 const ALL_PRF_UV_OPTIONS: [PrfUvOptions; 6] = [ 3336 PrfUvOptions::None(false), 3337 PrfUvOptions::None(true), 3338 PrfUvOptions::Prf(ExtensionInfo::RequireEnforceValue), 3339 PrfUvOptions::Prf(ExtensionInfo::RequireDontEnforceValue), 3340 PrfUvOptions::Prf(ExtensionInfo::AllowEnforceValue), 3341 PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue), 3342 ]; 3343 const ALL_PIN_OPTIONS: [Option<(FourToSixtyThree, ExtensionInfo)>; 5] = [ 3344 None, 3345 Some((FourToSixtyThree::Five, ExtensionInfo::RequireEnforceValue)), 3346 Some(( 3347 FourToSixtyThree::Five, 3348 ExtensionInfo::RequireDontEnforceValue, 3349 )), 3350 Some((FourToSixtyThree::Five, ExtensionInfo::AllowEnforceValue)), 3351 Some((FourToSixtyThree::Five, ExtensionInfo::AllowDontEnforceValue)), 3352 ]; 3353 const ALL_NON_EMPTY_CRED_PROPS_OPTIONS: [Option<Option<bool>>; 3] = 3354 [Some(None), Some(Some(false)), Some(Some(true))]; 3355 const ALL_MIN_PIN_OPTIONS: [Option<FourToSixtyThree>; 4] = [ 3356 None, 3357 Some(FourToSixtyThree::Four), 3358 Some(FourToSixtyThree::Five), 3359 Some(FourToSixtyThree::Six), 3360 ]; 3361 for cred_protect in ALL_CRED_PROTECTION_OPTIONS { 3362 for prf in ALL_PRF_OPTIONS { 3363 for hmac in ALL_HMAC_OPTIONS { 3364 for cred_props in ALL_NON_EMPTY_CRED_PROPS_OPTIONS { 3365 for min_pin in ALL_MIN_PIN_OPTIONS { 3366 for user_verified in ALL_UV_OPTIONS { 3367 for protect in ALL_CRED_PROTECT_OPTIONS { 3368 for prf_uv in ALL_PRF_UV_OPTIONS { 3369 for pin in ALL_PIN_OPTIONS { 3370 if user_verified 3371 || (!matches!( 3372 protect, 3373 CredProtect::UserVerificationRequired(_, _) 3374 ) && matches!(prf_uv, PrfUvOptions::None(uv) if !uv)) 3375 { 3376 assert!(validate(TestOptions { 3377 request: TestRequestOptions { 3378 error_unsolicited: true, 3379 protect, 3380 prf_uv, 3381 props: None, 3382 pin, 3383 }, 3384 response: TestResponseOptions { 3385 user_verified, 3386 hmac, 3387 cred_protect, 3388 prf, 3389 min_pin, 3390 cred_props, 3391 }, 3392 }).map_or_else(|err| matches!(err, AggErr::RegCeremony(reg_err) if matches!(reg_err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::ForbiddenCredProps))), |_| false)); 3393 } 3394 } 3395 } 3396 } 3397 } 3398 } 3399 } 3400 } 3401 } 3402 } 3403 } 3404 #[test] 3405 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 3406 fn test_prf() -> Result<(), AggErr> { 3407 let mut opts = TestOptions { 3408 request: TestRequestOptions { 3409 error_unsolicited: false, 3410 protect: CredProtect::None, 3411 prf_uv: PrfUvOptions::Prf(ExtensionInfo::RequireEnforceValue), 3412 props: None, 3413 pin: None, 3414 }, 3415 response: TestResponseOptions { 3416 user_verified: true, 3417 hmac: HmacSecret::None, 3418 cred_protect: CredentialProtectionPolicy::None, 3419 prf: Some(true), 3420 min_pin: None, 3421 cred_props: None, 3422 }, 3423 }; 3424 validate(opts)?; 3425 opts.response.prf = Some(false); 3426 assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidPrfValue))), |_| false)); 3427 opts.response.hmac = HmacSecret::NotEnabled; 3428 opts.response.prf = Some(true); 3429 assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidHmacSecretValue))), |_| false)); 3430 opts.request.prf_uv = PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue); 3431 opts.response.hmac = HmacSecret::Enabled; 3432 opts.response.prf = None; 3433 assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::HmacSecretWithoutPrf))), |_| false)); 3434 opts.response.hmac = HmacSecret::NotEnabled; 3435 assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::HmacSecretWithoutPrf))), |_| false)); 3436 opts.response.prf = Some(true); 3437 assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::PrfWithoutHmacSecret))), |_| false)); 3438 opts.response.prf = Some(false); 3439 validate(opts)?; 3440 opts.request.prf_uv = PrfUvOptions::None(false); 3441 opts.response.user_verified = false; 3442 opts.response.hmac = HmacSecret::Enabled; 3443 opts.response.prf = Some(true); 3444 assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::HmacSecretWithoutUserVerified))), |_| false)); 3445 opts.response.prf = None; 3446 opts.response.hmac = HmacSecret::None; 3447 validate(opts)?; 3448 Ok(()) 3449 } 3450 #[test] 3451 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 3452 fn test_cred_protect() -> Result<(), AggErr> { 3453 let mut opts = TestOptions { 3454 request: TestRequestOptions { 3455 error_unsolicited: false, 3456 protect: CredProtect::UserVerificationRequired( 3457 false, 3458 ExtensionInfo::RequireEnforceValue, 3459 ), 3460 prf_uv: PrfUvOptions::None(false), 3461 props: None, 3462 pin: None, 3463 }, 3464 response: TestResponseOptions { 3465 user_verified: true, 3466 hmac: HmacSecret::None, 3467 cred_protect: CredentialProtectionPolicy::UserVerificationRequired, 3468 prf: None, 3469 min_pin: None, 3470 cred_props: None, 3471 }, 3472 }; 3473 validate(opts)?; 3474 opts.response.cred_protect = 3475 CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList; 3476 assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidCredProtectValue(CredProtect::UserVerificationRequired(false, ExtensionInfo::RequireEnforceValue), CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList)))), |_| false)); 3477 opts.request.protect = 3478 CredProtect::UserVerificationOptional(true, ExtensionInfo::RequireEnforceValue); 3479 opts.response.user_verified = false; 3480 opts.response.cred_protect = CredentialProtectionPolicy::UserVerificationRequired; 3481 assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::CredProtectUserVerificationRequiredWithoutUserVerified))), |_| false)); 3482 Ok(()) 3483 } 3484 }