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 }