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