tests.rs (34031B)
1 #[cfg(all( 2 feature = "custom", 3 any( 4 feature = "serializable_server_state", 5 not(any(feature = "bin", feature = "serde")) 6 ) 7 ))] 8 use super::{ 9 super::{super::AggErr, ExtensionInfo}, 10 Challenge, CredProtect, CredentialCreationOptions, FourToSixtyThree, PrfInput, 11 PublicKeyCredentialUserEntity, RpId, UserHandle, 12 }; 13 #[cfg(all(feature = "custom", feature = "serializable_server_state"))] 14 use super::{ 15 super::{ 16 super::bin::{Decode as _, Encode as _}, 17 AsciiDomain, 18 }, 19 Extension, RegistrationServerState, 20 }; 21 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 22 use super::{ 23 super::{ 24 super::{ 25 CredentialErr, 26 response::register::{ 27 AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation, 28 ClientExtensionsOutputs, CredentialPropertiesOutput, CredentialProtectionPolicy, 29 HmacSecret, 30 }, 31 }, 32 AuthTransports, 33 }, 34 AuthenticatorAttachment, BackupReq, ExtensionErr, ExtensionReq, RegCeremonyErr, Registration, 35 RegistrationVerificationOptions, UserVerificationRequirement, 36 }; 37 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 38 use rsa::sha2::{Digest as _, Sha256}; 39 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 40 const CBOR_UINT: u8 = 0b000_00000; 41 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 42 const CBOR_NEG: u8 = 0b001_00000; 43 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 44 const CBOR_BYTES: u8 = 0b010_00000; 45 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 46 const CBOR_TEXT: u8 = 0b011_00000; 47 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 48 const CBOR_MAP: u8 = 0b101_00000; 49 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 50 const CBOR_SIMPLE: u8 = 0b111_00000; 51 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 52 const CBOR_FALSE: u8 = CBOR_SIMPLE | 20; 53 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 54 const CBOR_TRUE: u8 = CBOR_SIMPLE | 21; 55 #[expect(clippy::panic_in_result_fn, reason = "OK in tests")] 56 #[test] 57 #[cfg(all(feature = "custom", feature = "serializable_server_state"))] 58 fn eddsa_reg_ser() -> Result<(), AggErr> { 59 let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); 60 let id = UserHandle::from([0; 1]); 61 let mut opts = CredentialCreationOptions::passkey( 62 &rp_id, 63 PublicKeyCredentialUserEntity { 64 name: "foo", 65 id: &id, 66 display_name: "", 67 }, 68 Vec::new(), 69 ); 70 opts.public_key.challenge = Challenge(0); 71 opts.public_key.extensions = Extension { 72 cred_props: None, 73 cred_protect: CredProtect::UserVerificationRequired( 74 false, 75 ExtensionInfo::RequireEnforceValue, 76 ), 77 min_pin_length: Some((FourToSixtyThree::Ten, ExtensionInfo::RequireEnforceValue)), 78 prf: Some(( 79 PrfInput { 80 first: [0].as_slice(), 81 second: None, 82 }, 83 ExtensionInfo::RequireEnforceValue, 84 )), 85 }; 86 let server = opts.start_ceremony()?.0; 87 let enc_data = server 88 .encode() 89 .map_err(AggErr::EncodeRegistrationServerState)?; 90 assert_eq!(enc_data.capacity(), enc_data.len()); 91 assert_eq!(enc_data.len(), 1 + 16 + 1 + 3 + (1 + 3 + 3 + 2) + 12 + 1); 92 assert!(server.is_eq(&RegistrationServerState::decode(enc_data.as_slice())?)); 93 Ok(()) 94 } 95 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 96 #[derive(Clone, Copy)] 97 struct TestResponseOptions { 98 user_verified: bool, 99 cred_protect: CredentialProtectionPolicy, 100 prf: Option<bool>, 101 hmac: HmacSecret, 102 min_pin: Option<FourToSixtyThree>, 103 #[expect(clippy::option_option, reason = "fine")] 104 cred_props: Option<Option<bool>>, 105 } 106 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 107 #[derive(Clone, Copy)] 108 enum PrfUvOptions { 109 /// `true` iff `UserVerificationRequirement::Required` should be used; otherwise 110 /// `UserVerificationRequirement::Preferred` is used. 111 None(bool), 112 Prf(ExtensionInfo), 113 } 114 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 115 #[derive(Clone, Copy)] 116 struct TestRequestOptions { 117 error_unsolicited: bool, 118 protect: CredProtect, 119 prf_uv: PrfUvOptions, 120 props: Option<ExtensionReq>, 121 pin: Option<(FourToSixtyThree, ExtensionInfo)>, 122 } 123 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 124 #[derive(Clone, Copy)] 125 struct TestOptions { 126 request: TestRequestOptions, 127 response: TestResponseOptions, 128 } 129 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 130 fn generate_client_data_json() -> Vec<u8> { 131 let mut json = Vec::with_capacity(256); 132 json.extend_from_slice(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()); 133 json 134 } 135 #[expect(clippy::unreachable, reason = "want to crash when there is a bug")] 136 #[expect( 137 clippy::arithmetic_side_effects, 138 clippy::indexing_slicing, 139 reason = "comments justify correctness" 140 )] 141 #[expect( 142 clippy::cognitive_complexity, 143 clippy::too_many_lines, 144 reason = "a lot to test" 145 )] 146 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 147 fn generate_attestation_object(options: TestResponseOptions) -> Vec<u8> { 148 let mut attestation_object = Vec::with_capacity(256); 149 attestation_object.extend_from_slice( 150 [ 151 CBOR_MAP | 3, 152 CBOR_TEXT | 3, 153 b'f', 154 b'm', 155 b't', 156 CBOR_TEXT | 4, 157 b'n', 158 b'o', 159 b'n', 160 b'e', 161 CBOR_TEXT | 7, 162 b'a', 163 b't', 164 b't', 165 b'S', 166 b't', 167 b'm', 168 b't', 169 CBOR_MAP, 170 CBOR_TEXT | 8, 171 b'a', 172 b'u', 173 b't', 174 b'h', 175 b'D', 176 b'a', 177 b't', 178 b'a', 179 CBOR_BYTES | 24, 180 // Length. 181 // Addition won't overflow. 182 113 + if matches!(options.cred_protect, CredentialProtectionPolicy::None) { 183 if matches!(options.hmac, HmacSecret::None) { 184 options.min_pin.map_or(0, |_| 15) 185 } else { 186 14 + options.min_pin.map_or(0, |_| 14) 187 + match options.hmac { 188 HmacSecret::None => unreachable!("bug"), 189 HmacSecret::NotEnabled | HmacSecret::Enabled => 0, 190 HmacSecret::One => 65, 191 HmacSecret::Two => 97, 192 } 193 } 194 } else { 195 14 + if matches!(options.hmac, HmacSecret::None) { 196 0 197 } else { 198 13 199 } + options.min_pin.map_or(0, |_| 14) 200 + match options.hmac { 201 HmacSecret::None | HmacSecret::NotEnabled | HmacSecret::Enabled => 0, 202 HmacSecret::One => 65, 203 HmacSecret::Two => 97, 204 } 205 }, 206 // RP ID HASH. 207 // This will be overwritten later. 208 0, 209 0, 210 0, 211 0, 212 0, 213 0, 214 0, 215 0, 216 0, 217 0, 218 0, 219 0, 220 0, 221 0, 222 0, 223 0, 224 0, 225 0, 226 0, 227 0, 228 0, 229 0, 230 0, 231 0, 232 0, 233 0, 234 0, 235 0, 236 0, 237 0, 238 0, 239 0, 240 // FLAGS. 241 // UP, UV, AT, and ED (right-to-left). 242 0b0100_0001 243 | if options.user_verified { 244 0b0000_0100 245 } else { 246 0b0000_0000 247 } 248 | if matches!(options.cred_protect, CredentialProtectionPolicy::None) 249 && matches!(options.hmac, HmacSecret::None) 250 && options.min_pin.is_none() 251 { 252 0 253 } else { 254 0b1000_0000 255 }, 256 // COUNTER. 257 // 0 as 32-bit big endian. 258 0, 259 0, 260 0, 261 0, 262 // AAGUID. 263 0, 264 0, 265 0, 266 0, 267 0, 268 0, 269 0, 270 0, 271 0, 272 0, 273 0, 274 0, 275 0, 276 0, 277 0, 278 0, 279 // L. 280 // CREDENTIAL ID length is 16 as 16-bit big endian. 281 0, 282 16, 283 // CREDENTIAL ID. 284 0, 285 0, 286 0, 287 0, 288 0, 289 0, 290 0, 291 0, 292 0, 293 0, 294 0, 295 0, 296 0, 297 0, 298 0, 299 0, 300 CBOR_MAP | 4, 301 // COSE kty. 302 CBOR_UINT | 1, 303 // COSE OKP. 304 CBOR_UINT | 1, 305 // COSE alg. 306 CBOR_UINT | 3, 307 // COSE Eddsa. 308 CBOR_NEG | 7, 309 // COSE OKP crv. 310 CBOR_NEG, 311 // COSE Ed25519. 312 CBOR_UINT | 6, 313 // COSE OKP x. 314 CBOR_NEG | 1, 315 CBOR_BYTES | 24, 316 // Length is 32. 317 32, 318 // Compressed-y coordinate. 319 59, 320 106, 321 39, 322 188, 323 206, 324 182, 325 164, 326 45, 327 98, 328 163, 329 168, 330 208, 331 42, 332 111, 333 13, 334 115, 335 101, 336 50, 337 21, 338 119, 339 29, 340 226, 341 67, 342 166, 343 58, 344 192, 345 72, 346 161, 347 139, 348 89, 349 218, 350 41, 351 ] 352 .as_slice(), 353 ); 354 attestation_object[30..62].copy_from_slice(&Sha256::digest(b"example.com")); 355 if matches!(options.cred_protect, CredentialProtectionPolicy::None) { 356 if matches!(options.hmac, HmacSecret::None) { 357 if options.min_pin.is_some() { 358 attestation_object.push(CBOR_MAP | 1); 359 } 360 } else if options.min_pin.is_some() { 361 attestation_object.push( 362 // Addition won't overflow. 363 CBOR_MAP 364 | (2 + u8::from(matches!(options.hmac, HmacSecret::One | HmacSecret::Two))), 365 ); 366 } else { 367 attestation_object.push( 368 // Addition won't overflow. 369 CBOR_MAP 370 | (1 + u8::from(matches!(options.hmac, HmacSecret::One | HmacSecret::Two))), 371 ); 372 } 373 } else { 374 attestation_object.extend_from_slice( 375 [ 376 // Addition won't overflow. 377 CBOR_MAP 378 | (1 + match options.hmac { 379 HmacSecret::None => 0, 380 HmacSecret::NotEnabled | HmacSecret::Enabled => 1, 381 HmacSecret::One | HmacSecret::Two => 2, 382 } + u8::from(options.min_pin.is_some())), 383 // CBOR text of length 11. 384 CBOR_TEXT | 11, 385 b'c', 386 b'r', 387 b'e', 388 b'd', 389 b'P', 390 b'r', 391 b'o', 392 b't', 393 b'e', 394 b'c', 395 b't', 396 // Addition won't overflow. 397 match options.cred_protect { 398 CredentialProtectionPolicy::None => unreachable!("bug"), 399 CredentialProtectionPolicy::UserVerificationOptional => 1, 400 CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList => 2, 401 CredentialProtectionPolicy::UserVerificationRequired => 3, 402 }, 403 ] 404 .as_slice(), 405 ); 406 } 407 if !matches!(options.hmac, HmacSecret::None) { 408 attestation_object.extend_from_slice( 409 [ 410 // CBOR text of length 11. 411 CBOR_TEXT | 11, 412 b'h', 413 b'm', 414 b'a', 415 b'c', 416 b'-', 417 b's', 418 b'e', 419 b'c', 420 b'r', 421 b'e', 422 b't', 423 if matches!(options.hmac, HmacSecret::NotEnabled) { 424 CBOR_FALSE 425 } else { 426 CBOR_TRUE 427 }, 428 ] 429 .as_slice(), 430 ); 431 } 432 _ = options.min_pin.map(|p| { 433 assert!(p <= FourToSixtyThree::TwentyThree, "bug"); 434 attestation_object.extend_from_slice( 435 [ 436 // CBOR text of length 12. 437 CBOR_TEXT | 12, 438 b'm', 439 b'i', 440 b'n', 441 b'P', 442 b'i', 443 b'n', 444 b'L', 445 b'e', 446 b'n', 447 b'g', 448 b't', 449 b'h', 450 CBOR_UINT | p.into_u8(), 451 ] 452 .as_slice(), 453 ); 454 }); 455 if matches!(options.hmac, HmacSecret::One | HmacSecret::Two) { 456 attestation_object.extend_from_slice( 457 [ 458 // CBOR text of length 14. 459 CBOR_TEXT | 14, 460 b'h', 461 b'm', 462 b'a', 463 b'c', 464 b'-', 465 b's', 466 b'e', 467 b'c', 468 b'r', 469 b'e', 470 b't', 471 b'-', 472 b'm', 473 b'c', 474 CBOR_BYTES | 24, 475 ] 476 .as_slice(), 477 ); 478 if matches!(options.hmac, HmacSecret::One) { 479 attestation_object.push(48); 480 attestation_object.extend_from_slice([1; 48].as_slice()); 481 } else { 482 attestation_object.push(80); 483 attestation_object.extend_from_slice([1; 80].as_slice()); 484 } 485 } 486 attestation_object 487 } 488 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 489 fn validate(options: TestOptions) -> Result<(), AggErr> { 490 let rp_id = RpId::Domain("example.com".to_owned().try_into()?); 491 let registration = Registration::new( 492 AuthenticatorAttestation::new( 493 generate_client_data_json(), 494 generate_attestation_object(options.response), 495 AuthTransports::NONE, 496 ), 497 AuthenticatorAttachment::None, 498 ClientExtensionsOutputs { 499 cred_props: options 500 .response 501 .cred_props 502 .map(|rk| CredentialPropertiesOutput { rk }), 503 prf: options 504 .response 505 .prf 506 .map(|enabled| AuthenticationExtensionsPrfOutputs { enabled }), 507 }, 508 ); 509 let reg_opts = RegistrationVerificationOptions::<'static, 'static, &str, &str> { 510 allowed_origins: [].as_slice(), 511 allowed_top_origins: None, 512 backup_requirement: BackupReq::None, 513 error_on_unsolicited_extensions: options.request.error_unsolicited, 514 require_authenticator_attachment: false, 515 #[cfg(feature = "serde_relaxed")] 516 client_data_json_relaxed: false, 517 }; 518 let user = UserHandle::from([0; 1]); 519 let mut opts = CredentialCreationOptions::passkey( 520 &rp_id, 521 PublicKeyCredentialUserEntity { 522 id: &user, 523 name: "", 524 display_name: "", 525 }, 526 Vec::new(), 527 ); 528 opts.public_key.challenge = Challenge(0); 529 opts.public_key.authenticator_selection.user_verification = 530 UserVerificationRequirement::Preferred; 531 match options.request.prf_uv { 532 PrfUvOptions::None(required) => { 533 if required 534 || matches!( 535 options.request.protect, 536 CredProtect::UserVerificationRequired(_, _) 537 ) 538 { 539 opts.public_key.authenticator_selection.user_verification = 540 UserVerificationRequirement::Required; 541 } 542 } 543 PrfUvOptions::Prf(info) => { 544 opts.public_key.authenticator_selection.user_verification = 545 UserVerificationRequirement::Required; 546 opts.public_key.extensions.prf = Some(( 547 PrfInput { 548 first: [0].as_slice(), 549 second: None, 550 }, 551 info, 552 )); 553 } 554 } 555 opts.public_key.extensions.cred_protect = options.request.protect; 556 opts.public_key.extensions.cred_props = options.request.props; 557 opts.public_key.extensions.min_pin_length = options.request.pin; 558 opts.start_ceremony()? 559 .0 560 .verify(&rp_id, ®istration, ®_opts) 561 .map_err(AggErr::RegCeremony) 562 .map(|_| ()) 563 } 564 /// Test all, and only, possible `UserNotVerified` errors. 565 /// 4 * 3 * 5 * 2 * 13 * 5 * 3 * 5 * 4 * 4 = 1,872,000 tests. 566 /// We ignore this due to how long it takes (around 30 seconds or so). 567 #[expect(clippy::too_many_lines, reason = "a lot to test")] 568 #[test] 569 #[ignore = "slow"] 570 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 571 fn uv_required_err() { 572 const ALL_CRED_PROTECTION_OPTIONS: [CredentialProtectionPolicy; 4] = [ 573 CredentialProtectionPolicy::None, 574 CredentialProtectionPolicy::UserVerificationOptional, 575 CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, 576 CredentialProtectionPolicy::UserVerificationRequired, 577 ]; 578 const ALL_PRF_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)]; 579 const ALL_HMAC_OPTIONS: [HmacSecret; 5] = [ 580 HmacSecret::None, 581 HmacSecret::NotEnabled, 582 HmacSecret::Enabled, 583 HmacSecret::One, 584 HmacSecret::Two, 585 ]; 586 const ALL_UNSOLICIT_OPTIONS: [bool; 2] = [false, true]; 587 const ALL_CRED_PROTECT_OPTIONS: [CredProtect; 13] = [ 588 CredProtect::None, 589 CredProtect::UserVerificationOptional(false, ExtensionInfo::RequireEnforceValue), 590 CredProtect::UserVerificationOptional(true, ExtensionInfo::RequireDontEnforceValue), 591 CredProtect::UserVerificationOptional(false, ExtensionInfo::AllowEnforceValue), 592 CredProtect::UserVerificationOptional(true, ExtensionInfo::AllowDontEnforceValue), 593 CredProtect::UserVerificationOptionalWithCredentialIdList( 594 false, 595 ExtensionInfo::RequireEnforceValue, 596 ), 597 CredProtect::UserVerificationOptionalWithCredentialIdList( 598 true, 599 ExtensionInfo::RequireDontEnforceValue, 600 ), 601 CredProtect::UserVerificationOptionalWithCredentialIdList( 602 false, 603 ExtensionInfo::AllowEnforceValue, 604 ), 605 CredProtect::UserVerificationOptionalWithCredentialIdList( 606 true, 607 ExtensionInfo::AllowDontEnforceValue, 608 ), 609 CredProtect::UserVerificationRequired(false, ExtensionInfo::RequireEnforceValue), 610 CredProtect::UserVerificationRequired(true, ExtensionInfo::RequireDontEnforceValue), 611 CredProtect::UserVerificationRequired(false, ExtensionInfo::AllowEnforceValue), 612 CredProtect::UserVerificationRequired(true, ExtensionInfo::AllowDontEnforceValue), 613 ]; 614 const ALL_NOT_FALSE_PRF_UV_OPTIONS: [PrfUvOptions; 5] = [ 615 PrfUvOptions::None(true), 616 PrfUvOptions::Prf(ExtensionInfo::RequireEnforceValue), 617 PrfUvOptions::Prf(ExtensionInfo::RequireDontEnforceValue), 618 PrfUvOptions::Prf(ExtensionInfo::AllowEnforceValue), 619 PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue), 620 ]; 621 const ALL_PROPS_OPTIONS: [Option<ExtensionReq>; 3] = 622 [None, Some(ExtensionReq::Require), Some(ExtensionReq::Allow)]; 623 const ALL_PIN_OPTIONS: [Option<(FourToSixtyThree, ExtensionInfo)>; 5] = [ 624 None, 625 Some((FourToSixtyThree::Five, ExtensionInfo::RequireEnforceValue)), 626 Some(( 627 FourToSixtyThree::Five, 628 ExtensionInfo::RequireDontEnforceValue, 629 )), 630 Some((FourToSixtyThree::Five, ExtensionInfo::AllowEnforceValue)), 631 Some((FourToSixtyThree::Five, ExtensionInfo::AllowDontEnforceValue)), 632 ]; 633 #[expect(clippy::option_option, reason = "fine")] 634 const ALL_CRED_PROPS_OPTIONS: [Option<Option<bool>>; 4] = 635 [None, Some(None), Some(Some(false)), Some(Some(true))]; 636 const ALL_MIN_PIN_OPTIONS: [Option<FourToSixtyThree>; 4] = [ 637 None, 638 Some(FourToSixtyThree::Four), 639 Some(FourToSixtyThree::Five), 640 Some(FourToSixtyThree::Six), 641 ]; 642 for cred_protect in ALL_CRED_PROTECTION_OPTIONS { 643 for prf in ALL_PRF_OPTIONS { 644 for hmac in ALL_HMAC_OPTIONS { 645 for cred_props in ALL_CRED_PROPS_OPTIONS { 646 for min_pin in ALL_MIN_PIN_OPTIONS { 647 for error_unsolicited in ALL_UNSOLICIT_OPTIONS { 648 for protect in ALL_CRED_PROTECT_OPTIONS { 649 for prf_uv in ALL_NOT_FALSE_PRF_UV_OPTIONS { 650 for props in ALL_PROPS_OPTIONS { 651 for pin in ALL_PIN_OPTIONS { 652 assert!(validate(TestOptions { 653 request: TestRequestOptions { 654 error_unsolicited, 655 protect, 656 prf_uv, 657 props, 658 pin, 659 }, 660 response: TestResponseOptions { 661 user_verified: false, 662 hmac, 663 cred_protect, 664 prf, 665 min_pin, 666 cred_props, 667 }, 668 }).is_err_and(|err| matches!(err, AggErr::RegCeremony(reg_err) if matches!(reg_err, RegCeremonyErr::UserNotVerified)))); 669 } 670 } 671 } 672 } 673 } 674 } 675 } 676 } 677 } 678 } 679 } 680 /// Test all, and only, possible `ForbiddenCredProps` errors. 681 /// 4 * 3 * 5 * 2 * 13 * 6 * 5 * 3 * 4 = 561,600 682 /// - 683 /// 4 * 3 * 5 * 4 * 6 * 5 * 3 * 4 = 86,400 684 /// - 685 /// 4 * 3 * 5 * 13 * 5 * 5 * 3 * 4 = 234,000 686 /// + 687 /// 4 * 3 * 5 * 4 * 5 * 5 * 3 * 4 = 72,000 688 /// = 689 /// 313,200 total tests. 690 /// We ignore this due to how long it takes (around 6 seconds or so). 691 #[expect(clippy::too_many_lines, reason = "a lot to test")] 692 #[test] 693 #[ignore = "slow"] 694 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 695 fn forbidden_cred_props() { 696 const ALL_CRED_PROTECTION_OPTIONS: [CredentialProtectionPolicy; 4] = [ 697 CredentialProtectionPolicy::None, 698 CredentialProtectionPolicy::UserVerificationOptional, 699 CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, 700 CredentialProtectionPolicy::UserVerificationRequired, 701 ]; 702 const ALL_PRF_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)]; 703 const ALL_HMAC_OPTIONS: [HmacSecret; 5] = [ 704 HmacSecret::None, 705 HmacSecret::NotEnabled, 706 HmacSecret::Enabled, 707 HmacSecret::One, 708 HmacSecret::Two, 709 ]; 710 const ALL_UV_OPTIONS: [bool; 2] = [false, true]; 711 const ALL_CRED_PROTECT_OPTIONS: [CredProtect; 13] = [ 712 CredProtect::None, 713 CredProtect::UserVerificationOptional(false, ExtensionInfo::RequireEnforceValue), 714 CredProtect::UserVerificationOptional(true, ExtensionInfo::RequireDontEnforceValue), 715 CredProtect::UserVerificationOptional(false, ExtensionInfo::AllowEnforceValue), 716 CredProtect::UserVerificationOptional(true, ExtensionInfo::AllowDontEnforceValue), 717 CredProtect::UserVerificationOptionalWithCredentialIdList( 718 false, 719 ExtensionInfo::RequireEnforceValue, 720 ), 721 CredProtect::UserVerificationOptionalWithCredentialIdList( 722 true, 723 ExtensionInfo::RequireDontEnforceValue, 724 ), 725 CredProtect::UserVerificationOptionalWithCredentialIdList( 726 false, 727 ExtensionInfo::AllowEnforceValue, 728 ), 729 CredProtect::UserVerificationOptionalWithCredentialIdList( 730 true, 731 ExtensionInfo::AllowDontEnforceValue, 732 ), 733 CredProtect::UserVerificationRequired(false, ExtensionInfo::RequireEnforceValue), 734 CredProtect::UserVerificationRequired(true, ExtensionInfo::RequireDontEnforceValue), 735 CredProtect::UserVerificationRequired(false, ExtensionInfo::AllowEnforceValue), 736 CredProtect::UserVerificationRequired(true, ExtensionInfo::AllowDontEnforceValue), 737 ]; 738 const ALL_PRF_UV_OPTIONS: [PrfUvOptions; 6] = [ 739 PrfUvOptions::None(false), 740 PrfUvOptions::None(true), 741 PrfUvOptions::Prf(ExtensionInfo::RequireEnforceValue), 742 PrfUvOptions::Prf(ExtensionInfo::RequireDontEnforceValue), 743 PrfUvOptions::Prf(ExtensionInfo::AllowEnforceValue), 744 PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue), 745 ]; 746 const ALL_PIN_OPTIONS: [Option<(FourToSixtyThree, ExtensionInfo)>; 5] = [ 747 None, 748 Some((FourToSixtyThree::Five, ExtensionInfo::RequireEnforceValue)), 749 Some(( 750 FourToSixtyThree::Five, 751 ExtensionInfo::RequireDontEnforceValue, 752 )), 753 Some((FourToSixtyThree::Five, ExtensionInfo::AllowEnforceValue)), 754 Some((FourToSixtyThree::Five, ExtensionInfo::AllowDontEnforceValue)), 755 ]; 756 #[expect(clippy::option_option, reason = "fine")] 757 const ALL_NON_EMPTY_CRED_PROPS_OPTIONS: [Option<Option<bool>>; 3] = 758 [Some(None), Some(Some(false)), Some(Some(true))]; 759 const ALL_MIN_PIN_OPTIONS: [Option<FourToSixtyThree>; 4] = [ 760 None, 761 Some(FourToSixtyThree::Four), 762 Some(FourToSixtyThree::Five), 763 Some(FourToSixtyThree::Six), 764 ]; 765 for cred_protect in ALL_CRED_PROTECTION_OPTIONS { 766 for prf in ALL_PRF_OPTIONS { 767 for hmac in ALL_HMAC_OPTIONS { 768 for cred_props in ALL_NON_EMPTY_CRED_PROPS_OPTIONS { 769 for min_pin in ALL_MIN_PIN_OPTIONS { 770 for user_verified in ALL_UV_OPTIONS { 771 for protect in ALL_CRED_PROTECT_OPTIONS { 772 for prf_uv in ALL_PRF_UV_OPTIONS { 773 for pin in ALL_PIN_OPTIONS { 774 if user_verified 775 || (!matches!( 776 protect, 777 CredProtect::UserVerificationRequired(_, _) 778 ) && matches!(prf_uv, PrfUvOptions::None(uv) if !uv)) 779 { 780 assert!(validate(TestOptions { 781 request: TestRequestOptions { 782 error_unsolicited: true, 783 protect, 784 prf_uv, 785 props: None, 786 pin, 787 }, 788 response: TestResponseOptions { 789 user_verified, 790 cred_protect, 791 prf, 792 hmac, 793 min_pin, 794 cred_props, 795 }, 796 }).is_err_and(|err| matches!(err, AggErr::RegCeremony(reg_err) if matches!(reg_err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::ForbiddenCredProps))))); 797 } 798 } 799 } 800 } 801 } 802 } 803 } 804 } 805 } 806 } 807 } 808 #[expect(clippy::panic_in_result_fn, reason = "OK in tests")] 809 #[test] 810 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 811 fn prf() -> Result<(), AggErr> { 812 let mut opts = TestOptions { 813 request: TestRequestOptions { 814 error_unsolicited: false, 815 protect: CredProtect::None, 816 prf_uv: PrfUvOptions::Prf(ExtensionInfo::RequireEnforceValue), 817 props: None, 818 pin: None, 819 }, 820 response: TestResponseOptions { 821 user_verified: true, 822 hmac: HmacSecret::None, 823 cred_protect: CredentialProtectionPolicy::None, 824 prf: Some(true), 825 min_pin: None, 826 cred_props: None, 827 }, 828 }; 829 validate(opts)?; 830 opts.response.prf = Some(false); 831 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))))); 832 opts.response.hmac = HmacSecret::NotEnabled; 833 opts.response.prf = Some(true); 834 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))))); 835 opts.request.prf_uv = PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue); 836 opts.response.hmac = HmacSecret::Enabled; 837 opts.response.prf = None; 838 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))))); 839 opts.response.hmac = HmacSecret::NotEnabled; 840 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))))); 841 opts.response.prf = Some(true); 842 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))))); 843 opts.response.prf = Some(false); 844 validate(opts)?; 845 opts.request.prf_uv = PrfUvOptions::None(false); 846 opts.response.user_verified = false; 847 opts.response.hmac = HmacSecret::Enabled; 848 opts.response.prf = Some(true); 849 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))))); 850 opts.response.prf = None; 851 opts.response.hmac = HmacSecret::None; 852 validate(opts)?; 853 Ok(()) 854 } 855 #[expect(clippy::panic_in_result_fn, reason = "OK in tests")] 856 #[test] 857 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] 858 fn cred_protect() -> Result<(), AggErr> { 859 let mut opts = TestOptions { 860 request: TestRequestOptions { 861 error_unsolicited: false, 862 protect: CredProtect::UserVerificationRequired( 863 false, 864 ExtensionInfo::RequireEnforceValue, 865 ), 866 prf_uv: PrfUvOptions::None(false), 867 props: None, 868 pin: None, 869 }, 870 response: TestResponseOptions { 871 user_verified: true, 872 hmac: HmacSecret::None, 873 cred_protect: CredentialProtectionPolicy::UserVerificationRequired, 874 prf: None, 875 min_pin: None, 876 cred_props: None, 877 }, 878 }; 879 validate(opts)?; 880 opts.response.cred_protect = 881 CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList; 882 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)))))); 883 opts.request.protect = 884 CredProtect::UserVerificationOptional(true, ExtensionInfo::RequireEnforceValue); 885 opts.response.user_verified = false; 886 opts.response.cred_protect = CredentialProtectionPolicy::UserVerificationRequired; 887 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))))); 888 Ok(()) 889 }