webauthn_rp

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

tests.rs (20340B)


      1 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
      2 use super::{
      3     super::super::{
      4         AuthenticatedCredential, DynamicState, StaticState, UserHandle,
      5         response::{
      6             Backup,
      7             auth::{DiscoverableAuthenticatorAssertion, HmacSecret},
      8             register::{
      9                 AuthenticationExtensionsPrfOutputs, AuthenticatorExtensionOutputStaticState,
     10                 ClientExtensionsOutputsStaticState, CompressedPubKeyOwned,
     11                 CredentialProtectionPolicy, Ed25519PubKey,
     12             },
     13         },
     14     },
     15     AuthCeremonyErr, AuthenticationVerificationOptions, AuthenticatorAttachment,
     16     AuthenticatorAttachmentEnforcement, BackupStateReq, DiscoverableAuthentication, ExtensionErr,
     17     OneOrTwo, PrfInput, SignatureCounterEnforcement,
     18 };
     19 #[cfg(all(
     20     feature = "custom",
     21     any(
     22         feature = "serializable_server_state",
     23         not(any(feature = "bin", feature = "serde"))
     24     )
     25 ))]
     26 use super::{
     27     super::{super::AggErr, Challenge, CredentialId, RpId, UserVerificationRequirement},
     28     DiscoverableCredentialRequestOptions, ExtensionReq,
     29 };
     30 #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
     31 use super::{
     32     super::{
     33         super::bin::{Decode as _, Encode as _},
     34         AsciiDomain, AuthTransports,
     35     },
     36     AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Credentials as _,
     37     DiscoverableAuthenticationServerState, Extension, NonDiscoverableAuthenticationServerState,
     38     NonDiscoverableCredentialRequestOptions, PrfInputOwned, PublicKeyCredentialDescriptor,
     39 };
     40 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
     41 use ed25519_dalek::{Signer as _, SigningKey};
     42 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
     43 use rsa::sha2::{Digest as _, Sha256};
     44 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
     45 const CBOR_BYTES: u8 = 0b010_00000;
     46 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
     47 const CBOR_TEXT: u8 = 0b011_00000;
     48 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
     49 const CBOR_MAP: u8 = 0b101_00000;
     50 #[expect(clippy::panic_in_result_fn, reason = "OK in tests")]
     51 #[test]
     52 #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
     53 fn eddsa_auth_ser() -> Result<(), AggErr> {
     54     let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
     55     let mut creds = AllowedCredentials::with_capacity(1);
     56     _ = creds.push(AllowedCredential {
     57         credential: PublicKeyCredentialDescriptor {
     58             id: CredentialId::try_from(vec![0; 16].into_boxed_slice())?,
     59             transports: AuthTransports::NONE,
     60         },
     61         extension: CredentialSpecificExtension {
     62             prf: Some(PrfInputOwned {
     63                 first: Vec::new(),
     64                 second: Some(Vec::new()),
     65                 ext_req: ExtensionReq::Require,
     66             }),
     67         },
     68     });
     69     let mut opts = NonDiscoverableCredentialRequestOptions::second_factor(&rp_id, creds);
     70     opts.options.user_verification = UserVerificationRequirement::Required;
     71     opts.options.challenge = Challenge(0);
     72     opts.options.extensions = Extension { prf: None };
     73     let server = opts.start_ceremony()?.0;
     74     let enc_data = server.encode()?;
     75     assert_eq!(enc_data.capacity(), 16 + 2 + 1 + 1 + 12 + 2 + 128 + 1);
     76     assert_eq!(enc_data.len(), 16 + 2 + 1 + 1 + 12 + 2 + 16 + 2);
     77     assert!(
     78         server.is_eq(&NonDiscoverableAuthenticationServerState::decode(
     79             enc_data.as_slice()
     80         )?)
     81     );
     82     let mut opts_2 = DiscoverableCredentialRequestOptions::passkey(&rp_id);
     83     opts_2.public_key.challenge = Challenge(0);
     84     opts_2.public_key.extensions = Extension { prf: None };
     85     let server_2 = opts_2.start_ceremony()?.0;
     86     let enc_data_2 = server_2
     87         .encode()
     88         .map_err(AggErr::EncodeDiscoverableAuthenticationServerState)?;
     89     assert_eq!(enc_data_2.capacity(), enc_data_2.len());
     90     assert_eq!(enc_data_2.len(), 16 + 1 + 1 + 12);
     91     assert!(
     92         server_2.is_eq(&DiscoverableAuthenticationServerState::decode(
     93             enc_data_2.as_slice()
     94         )?)
     95     );
     96     Ok(())
     97 }
     98 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
     99 #[derive(Clone, Copy)]
    100 struct TestResponseOptions {
    101     user_verified: bool,
    102     hmac: HmacSecret,
    103 }
    104 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
    105 #[derive(Clone, Copy)]
    106 enum PrfCredOptions {
    107     None,
    108     FalseNoHmac,
    109     FalseHmacFalse,
    110     TrueNoHmac,
    111     TrueHmacTrue,
    112 }
    113 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
    114 #[derive(Clone, Copy)]
    115 struct TestCredOptions {
    116     cred_protect: CredentialProtectionPolicy,
    117     prf: PrfCredOptions,
    118 }
    119 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
    120 #[derive(Clone, Copy)]
    121 enum PrfUvOptions {
    122     /// `true` iff `UserVerificationRequirement::Required` should be used; otherwise
    123     /// `UserVerificationRequirement::Preferred` is used.
    124     None(bool),
    125     Prf((PrfInput<'static, 'static>, ExtensionReq)),
    126 }
    127 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
    128 #[derive(Clone, Copy)]
    129 struct TestRequestOptions {
    130     error_unsolicited: bool,
    131     prf_uv: PrfUvOptions,
    132 }
    133 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
    134 #[derive(Clone, Copy)]
    135 struct TestOptions {
    136     request: TestRequestOptions,
    137     response: TestResponseOptions,
    138     cred: TestCredOptions,
    139 }
    140 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
    141 fn generate_client_data_json() -> Vec<u8> {
    142     let mut json = Vec::with_capacity(256);
    143     json.extend_from_slice(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice());
    144     json
    145 }
    146 #[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
    147 #[expect(clippy::too_many_lines, reason = "a lot to test")]
    148 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
    149 fn generate_authenticator_data_public_key_sig(
    150     opts: TestResponseOptions,
    151 ) -> (Vec<u8>, CompressedPubKeyOwned, Vec<u8>) {
    152     let mut authenticator_data = Vec::with_capacity(256);
    153     authenticator_data.extend_from_slice(
    154         [
    155             // RP ID HASH.
    156             // This will be overwritten later.
    157             0,
    158             0,
    159             0,
    160             0,
    161             0,
    162             0,
    163             0,
    164             0,
    165             0,
    166             0,
    167             0,
    168             0,
    169             0,
    170             0,
    171             0,
    172             0,
    173             0,
    174             0,
    175             0,
    176             0,
    177             0,
    178             0,
    179             0,
    180             0,
    181             0,
    182             0,
    183             0,
    184             0,
    185             0,
    186             0,
    187             0,
    188             0,
    189             // FLAGS.
    190             // UP, UV, AT, and ED (right-to-left).
    191             0b0000_0001
    192                 | if opts.user_verified {
    193                     0b0000_0100
    194                 } else {
    195                     0b0000_0000
    196                 }
    197                 | if matches!(opts.hmac, HmacSecret::None) {
    198                     0
    199                 } else {
    200                     0b1000_0000
    201                 },
    202             // COUNTER.
    203             // 0 as 32-bit big endian.
    204             0,
    205             0,
    206             0,
    207             0,
    208         ]
    209         .as_slice(),
    210     );
    211     authenticator_data[..32].copy_from_slice(&Sha256::digest(b"example.com"));
    212     match opts.hmac {
    213         HmacSecret::None => {}
    214         HmacSecret::One => {
    215             authenticator_data.extend_from_slice(
    216                 [
    217                     CBOR_MAP | 1,
    218                     // CBOR text of length 11.
    219                     CBOR_TEXT | 11,
    220                     b'h',
    221                     b'm',
    222                     b'a',
    223                     b'c',
    224                     b'-',
    225                     b's',
    226                     b'e',
    227                     b'c',
    228                     b'r',
    229                     b'e',
    230                     b't',
    231                     CBOR_BYTES | 24,
    232                     48,
    233                 ]
    234                 .as_slice(),
    235             );
    236             authenticator_data.extend_from_slice([0; 48].as_slice());
    237         }
    238         HmacSecret::Two => {
    239             authenticator_data.extend_from_slice(
    240                 [
    241                     CBOR_MAP | 1,
    242                     // CBOR text of length 11.
    243                     CBOR_TEXT | 11,
    244                     b'h',
    245                     b'm',
    246                     b'a',
    247                     b'c',
    248                     b'-',
    249                     b's',
    250                     b'e',
    251                     b'c',
    252                     b'r',
    253                     b'e',
    254                     b't',
    255                     CBOR_BYTES | 24,
    256                     80,
    257                 ]
    258                 .as_slice(),
    259             );
    260             authenticator_data.extend_from_slice([0; 80].as_slice());
    261         }
    262     }
    263     let len = authenticator_data.len();
    264     authenticator_data.extend_from_slice(&Sha256::digest(generate_client_data_json().as_slice()));
    265     let sig_key = SigningKey::from_bytes(&[0; 32]);
    266     let sig = sig_key.sign(authenticator_data.as_slice()).to_vec();
    267     authenticator_data.truncate(len);
    268     (
    269         authenticator_data,
    270         CompressedPubKeyOwned::Ed25519(Ed25519PubKey::from(sig_key.verifying_key().to_bytes())),
    271         sig,
    272     )
    273 }
    274 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
    275 fn validate(options: TestOptions) -> Result<(), AggErr> {
    276     let rp_id = RpId::Domain("example.com".to_owned().try_into()?);
    277     let user = UserHandle::from([0; 1]);
    278     let (authenticator_data, credential_public_key, signature) =
    279         generate_authenticator_data_public_key_sig(options.response);
    280     let credential_id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?;
    281     let authentication = DiscoverableAuthentication::new(
    282         credential_id.clone(),
    283         DiscoverableAuthenticatorAssertion::new(
    284             generate_client_data_json(),
    285             authenticator_data,
    286             signature,
    287             user,
    288         ),
    289         AuthenticatorAttachment::None,
    290     );
    291     let auth_opts = AuthenticationVerificationOptions::<'static, 'static, &str, &str> {
    292         allowed_origins: [].as_slice(),
    293         allowed_top_origins: None,
    294         auth_attachment_enforcement: AuthenticatorAttachmentEnforcement::Update(false),
    295         backup_state_requirement: BackupStateReq::None,
    296         error_on_unsolicited_extensions: options.request.error_unsolicited,
    297         sig_counter_enforcement: SignatureCounterEnforcement::Fail,
    298         update_uv: false,
    299         #[cfg(feature = "serde_relaxed")]
    300         client_data_json_relaxed: false,
    301     };
    302     let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
    303     opts.public_key.challenge = Challenge(0);
    304     opts.public_key.user_verification = UserVerificationRequirement::Preferred;
    305     match options.request.prf_uv {
    306         PrfUvOptions::None(required) => {
    307             if required {
    308                 opts.public_key.user_verification = UserVerificationRequirement::Required;
    309             }
    310         }
    311         PrfUvOptions::Prf(input) => {
    312             opts.public_key.user_verification = UserVerificationRequirement::Required;
    313             opts.public_key.extensions.prf = Some(input);
    314         }
    315     }
    316     let mut cred = AuthenticatedCredential::new(
    317         (&credential_id).into(),
    318         &user,
    319         StaticState {
    320             credential_public_key,
    321             extensions: AuthenticatorExtensionOutputStaticState {
    322                 cred_protect: options.cred.cred_protect,
    323                 hmac_secret: match options.cred.prf {
    324                     PrfCredOptions::None
    325                     | PrfCredOptions::FalseNoHmac
    326                     | PrfCredOptions::TrueNoHmac => None,
    327                     PrfCredOptions::FalseHmacFalse => Some(false),
    328                     PrfCredOptions::TrueHmacTrue => Some(true),
    329                 },
    330             },
    331             client_extension_results: ClientExtensionsOutputsStaticState {
    332                 prf: match options.cred.prf {
    333                     PrfCredOptions::None => None,
    334                     PrfCredOptions::FalseNoHmac | PrfCredOptions::FalseHmacFalse => {
    335                         Some(AuthenticationExtensionsPrfOutputs { enabled: false })
    336                     }
    337                     PrfCredOptions::TrueNoHmac | PrfCredOptions::TrueHmacTrue => {
    338                         Some(AuthenticationExtensionsPrfOutputs { enabled: true })
    339                     }
    340                 },
    341             },
    342         },
    343         DynamicState {
    344             user_verified: true,
    345             backup: Backup::NotEligible,
    346             sign_count: 0,
    347             authenticator_attachment: AuthenticatorAttachment::None,
    348         },
    349     )?;
    350     opts.start_ceremony()?
    351         .0
    352         .verify(&rp_id, &authentication, &mut cred, &auth_opts)
    353         .map_err(AggErr::AuthCeremony)
    354         .map(|_| ())
    355 }
    356 /// Test all, and only, possible `UserNotVerified` errors.
    357 /// 4 * 5 * 3 * 2 * 5 = 600 tests.
    358 /// We ignore this due to how long it takes (around 4 seconds or so).
    359 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
    360 #[ignore = "slow"]
    361 #[test]
    362 fn uv_required_err() {
    363     const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [
    364         CredentialProtectionPolicy::None,
    365         CredentialProtectionPolicy::UserVerificationOptional,
    366         CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList,
    367         CredentialProtectionPolicy::UserVerificationRequired,
    368     ];
    369     const ALL_PRF_CRED_OPTIONS: [PrfCredOptions; 5] = [
    370         PrfCredOptions::None,
    371         PrfCredOptions::FalseNoHmac,
    372         PrfCredOptions::FalseHmacFalse,
    373         PrfCredOptions::TrueNoHmac,
    374         PrfCredOptions::TrueHmacTrue,
    375     ];
    376     const ALL_HMAC_OPTIONS: [HmacSecret; 3] = [HmacSecret::None, HmacSecret::One, HmacSecret::Two];
    377     const ALL_UNSOLICIT_OPTIONS: [bool; 2] = [false, true];
    378     const ALL_NOT_FALSE_PRF_UV_OPTIONS: [PrfUvOptions; 5] = [
    379         PrfUvOptions::None(true),
    380         PrfUvOptions::Prf((
    381             PrfInput {
    382                 first: [].as_slice(),
    383                 second: None,
    384             },
    385             ExtensionReq::Require,
    386         )),
    387         PrfUvOptions::Prf((
    388             PrfInput {
    389                 first: [].as_slice(),
    390                 second: None,
    391             },
    392             ExtensionReq::Allow,
    393         )),
    394         PrfUvOptions::Prf((
    395             PrfInput {
    396                 first: [].as_slice(),
    397                 second: Some([].as_slice()),
    398             },
    399             ExtensionReq::Require,
    400         )),
    401         PrfUvOptions::Prf((
    402             PrfInput {
    403                 first: [].as_slice(),
    404                 second: Some([].as_slice()),
    405             },
    406             ExtensionReq::Allow,
    407         )),
    408     ];
    409     for cred_protect in ALL_CRED_PROTECT_OPTIONS {
    410         for prf in ALL_PRF_CRED_OPTIONS {
    411             for hmac in ALL_HMAC_OPTIONS {
    412                 for error_unsolicited in ALL_UNSOLICIT_OPTIONS {
    413                     for prf_uv in ALL_NOT_FALSE_PRF_UV_OPTIONS {
    414                         assert!(validate(TestOptions {
    415                             request: TestRequestOptions {
    416                                 error_unsolicited,
    417                                 prf_uv,
    418                             },
    419                             response: TestResponseOptions {
    420                                 user_verified: false,
    421                                 hmac,
    422                             },
    423                             cred: TestCredOptions { cred_protect, prf, },
    424                         }).is_err_and(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::UserNotVerified))));
    425                     }
    426                 }
    427             }
    428         }
    429     }
    430 }
    431 /// Test all, and only, possible `UserNotVerified` errors.
    432 /// 4 * 5 * 2 * 2 = 80 tests.
    433 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
    434 #[test]
    435 fn forbidden_hmac() {
    436     const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [
    437         CredentialProtectionPolicy::None,
    438         CredentialProtectionPolicy::UserVerificationOptional,
    439         CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList,
    440         CredentialProtectionPolicy::UserVerificationRequired,
    441     ];
    442     const ALL_PRF_CRED_OPTIONS: [PrfCredOptions; 5] = [
    443         PrfCredOptions::None,
    444         PrfCredOptions::FalseNoHmac,
    445         PrfCredOptions::FalseHmacFalse,
    446         PrfCredOptions::TrueNoHmac,
    447         PrfCredOptions::TrueHmacTrue,
    448     ];
    449     const ALL_HMAC_OPTIONS: [HmacSecret; 2] = [HmacSecret::One, HmacSecret::Two];
    450     const ALL_UV_OPTIONS: [bool; 2] = [false, true];
    451     for cred_protect in ALL_CRED_PROTECT_OPTIONS {
    452         for prf in ALL_PRF_CRED_OPTIONS {
    453             for hmac in ALL_HMAC_OPTIONS {
    454                 for user_verified in ALL_UV_OPTIONS {
    455                     assert!(validate(TestOptions {
    456                         request: TestRequestOptions {
    457                             error_unsolicited: true,
    458                             prf_uv: PrfUvOptions::None(false),
    459                         },
    460                         response: TestResponseOptions {
    461                             user_verified,
    462                             hmac,
    463                         },
    464                         cred: TestCredOptions { cred_protect, prf, },
    465                     }).is_err_and(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::ForbiddenHmacSecret)))));
    466                 }
    467             }
    468         }
    469     }
    470 }
    471 #[expect(clippy::panic_in_result_fn, reason = "OK in tests")]
    472 #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
    473 #[test]
    474 fn prf() -> Result<(), AggErr> {
    475     let mut opts = TestOptions {
    476         request: TestRequestOptions {
    477             error_unsolicited: false,
    478             prf_uv: PrfUvOptions::Prf((
    479                 PrfInput {
    480                     first: [].as_slice(),
    481                     second: None,
    482                 },
    483                 ExtensionReq::Allow,
    484             )),
    485         },
    486         response: TestResponseOptions {
    487             user_verified: true,
    488             hmac: HmacSecret::None,
    489         },
    490         cred: TestCredOptions {
    491             cred_protect: CredentialProtectionPolicy::None,
    492             prf: PrfCredOptions::None,
    493         },
    494     };
    495     validate(opts)?;
    496     opts.request.prf_uv = PrfUvOptions::Prf((
    497         PrfInput {
    498             first: [].as_slice(),
    499             second: None,
    500         },
    501         ExtensionReq::Require,
    502     ));
    503     opts.cred.prf = PrfCredOptions::TrueHmacTrue;
    504     assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::MissingHmacSecret)))));
    505     opts.response.hmac = HmacSecret::One;
    506     opts.request.prf_uv = PrfUvOptions::Prf((
    507         PrfInput {
    508             first: [].as_slice(),
    509             second: None,
    510         },
    511         ExtensionReq::Allow,
    512     ));
    513     opts.cred.prf = PrfCredOptions::TrueNoHmac;
    514     assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::HmacSecretForNonHmacSecretCredential)))));
    515     opts.response.hmac = HmacSecret::Two;
    516     assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidHmacSecretValue(OneOrTwo::One, OneOrTwo::Two))))));
    517     opts.response.hmac = HmacSecret::One;
    518     opts.cred.prf = PrfCredOptions::FalseNoHmac;
    519     assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::HmacSecretForNonHmacSecretCredential)))));
    520     opts.response.user_verified = false;
    521     opts.request.prf_uv = PrfUvOptions::None(false);
    522     opts.cred.prf = PrfCredOptions::TrueHmacTrue;
    523     assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::UserNotVerifiedHmacSecret)))));
    524     Ok(())
    525 }