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