webauthn_rp

WebAuthn RP library.
git clone https://git.philomathiclife.com/repos/webauthn_rp
Log | Files | Refs | README

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, &registration, &reg_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 }