1use crate::attestation::{verify_attestation, AttestationPolicy};
29use crate::challenge::check_sign_count;
30use crate::credential::{AuthenticationResult, RegistrationResult};
31use crate::crypto::{
32 alg_to_name, base64_decode_urlsafe, base64_encode_urlsafe, cbor_bytes, parse_cose_key,
33 verify_cose_signature,
34};
35use crate::error::WebauthnError;
36use crate::policy::{BackupPolicy, CredentialPolicy, UserVerificationPolicy};
37
38#[derive(Debug, Clone)]
40struct AuthenticatorData {
41 rp_id_hash: Vec<u8>,
43 flags: u8,
45 sign_count: u32,
47 credential_public_key_cose: Option<Vec<u8>>,
49 credential_id: Option<Vec<u8>>,
51 aaguid: Option<[u8; 16]>,
53 backup_eligible: bool,
56 backup_state: bool,
58}
59
60const FLAG_UP: u8 = 0x01;
62const FLAG_UV: u8 = 0x04;
64const FLAG_BE: u8 = 0x08;
66const FLAG_BS: u8 = 0x10;
68const FLAG_AT: u8 = 0x40;
70#[allow(dead_code)]
72const FLAG_ED: u8 = 0x80;
73
74const AUTH_DATA_MIN_LEN: usize = 37;
76
77const ATTESTED_CREDENTIAL_PREFIX_LEN: usize = 18;
79
80fn parse_authenticator_data(auth_data: &[u8]) -> Result<AuthenticatorData, WebauthnError> {
96 if auth_data.len() < AUTH_DATA_MIN_LEN {
97 return Err(WebauthnError::VerificationFailed(format!(
98 "Authenticator data too short: {} bytes (minimum {})",
99 auth_data.len(),
100 AUTH_DATA_MIN_LEN
101 )));
102 }
103
104 let truncated =
108 || WebauthnError::VerificationFailed("Authenticator data truncated".to_string());
109 let rp_id_hash = auth_data.get(..32).ok_or_else(truncated)?.to_vec();
110 let flags = *auth_data.get(32).ok_or_else(truncated)?;
111 let sign_count = u32::from_be_bytes(
112 auth_data
113 .get(33..37)
114 .ok_or_else(truncated)?
115 .try_into()
116 .map_err(|_| truncated())?,
117 );
118
119 let mut offset = 37;
120 let mut credential_id = None;
121 let mut credential_public_key_cose = None;
122 let mut aaguid = None;
123
124 if flags & FLAG_AT != 0 {
125 if auth_data.len() < offset + ATTESTED_CREDENTIAL_PREFIX_LEN {
126 return Err(WebauthnError::VerificationFailed(
127 "Attested credential data truncated (AAGUID + length)".to_string(),
128 ));
129 }
130 let aaguid_bytes = auth_data
131 .get(offset..offset + 16)
132 .ok_or_else(truncated)?
133 .to_vec();
134 let aaguid_arr: [u8; 16] = aaguid_bytes.try_into().map_err(|_| truncated())?;
135 aaguid = Some(aaguid_arr);
136 offset += 16;
137
138 let len_hi = *auth_data.get(offset).ok_or_else(truncated)?;
139 let len_lo = *auth_data.get(offset + 1).ok_or_else(truncated)?;
140 let cred_id_len = u16::from_be_bytes([len_hi, len_lo]) as usize;
141 offset += 2;
142
143 if auth_data.len() < offset + cred_id_len {
144 return Err(WebauthnError::VerificationFailed(
145 "Attested credential data truncated (credential ID)".to_string(),
146 ));
147 }
148 credential_id = Some(
149 auth_data
150 .get(offset..offset + cred_id_len)
151 .ok_or_else(truncated)?
152 .to_vec(),
153 );
154 offset += cred_id_len;
155
156 if offset >= auth_data.len() {
157 return Err(WebauthnError::VerificationFailed(
158 "Attested credential data truncated (public key)".to_string(),
159 ));
160 }
161 credential_public_key_cose = Some(auth_data.get(offset..).ok_or_else(truncated)?.to_vec());
162 }
163
164 Ok(AuthenticatorData {
165 rp_id_hash,
166 flags,
167 sign_count,
168 credential_public_key_cose,
169 credential_id,
170 aaguid,
171 backup_eligible: flags & FLAG_BE != 0,
172 backup_state: flags & FLAG_BS != 0,
173 })
174}
175
176fn enforce_flag_policy(flags: u8, policy: &CredentialPolicy) -> Result<(), WebauthnError> {
190 if policy.user_verification == UserVerificationPolicy::Required && flags & FLAG_UV == 0 {
191 return Err(WebauthnError::UserVerificationRequired);
192 }
193 if policy.backup == BackupPolicy::RequireDeviceBound && flags & FLAG_BE != 0 {
194 return Err(WebauthnError::PolicyViolation(
195 "backup policy requires device-bound credentials, but the credential \
196 is backup-eligible (BE=1)"
197 .to_string(),
198 ));
199 }
200 Ok(())
201}
202
203fn enforce_algorithm_allowlist(alg: i32, policy: &CredentialPolicy) -> Result<(), WebauthnError> {
210 if !policy.allowed_algorithms.is_empty() && !policy.allowed_algorithms.contains(&alg) {
211 return Err(WebauthnError::UnsupportedAlgorithm(alg));
212 }
213 Ok(())
214}
215
216fn validate_client_data(
224 client_data_json_b64: &str,
225 challenge_bytes: &[u8],
226 expected_type: &str,
227 rp_id: &str,
228 rp_origins: &[String],
229) -> Result<Vec<u8>, WebauthnError> {
230 let client_data_bytes = base64_decode_urlsafe(client_data_json_b64)?;
231 let client_data: serde_json::Value = serde_json::from_slice(&client_data_bytes)
232 .map_err(|e| WebauthnError::VerificationFailed(format!("client data parse error: {e}")))?;
233
234 let client_challenge = client_data
235 .get("challenge")
236 .and_then(|v| v.as_str())
237 .ok_or_else(|| {
238 WebauthnError::VerificationFailed("missing challenge in client data".to_string())
239 })?;
240 let client_challenge_bytes = base64_decode_urlsafe(client_challenge)?;
241 if client_challenge_bytes != challenge_bytes {
242 return Err(WebauthnError::VerificationFailed(
243 "challenge mismatch".to_string(),
244 ));
245 }
246
247 let typ = client_data
248 .get("type")
249 .and_then(|v| v.as_str())
250 .ok_or_else(|| {
251 WebauthnError::VerificationFailed("missing type in client data".to_string())
252 })?;
253 if typ != expected_type {
254 return Err(WebauthnError::VerificationFailed(format!(
255 "wrong type: {typ}"
256 )));
257 }
258
259 let origin = client_data
260 .get("origin")
261 .and_then(|v| v.as_str())
262 .ok_or_else(|| {
263 WebauthnError::VerificationFailed("missing origin in client data".to_string())
264 })?;
265 if !rp_origins.iter().any(|o| o == origin) {
266 return Err(WebauthnError::VerificationFailed(format!(
267 "origin '{origin}' not allowed"
268 )));
269 }
270
271 if let Some(rp) = client_data.get("rpId").and_then(|v| v.as_str()) {
273 if rp != rp_id {
274 return Err(WebauthnError::VerificationFailed(format!(
275 "rpId mismatch: client sent '{rp}', expected '{rp_id}'"
276 )));
277 }
278 }
279
280 Ok(client_data_bytes)
281}
282
283#[allow(clippy::too_many_arguments)]
316pub fn verify_registration(
317 challenge_bytes: &[u8],
318 client_data_json_b64: &str,
319 attestation_object_b64: &str,
320 existing_credential_id: &str,
321 rp_id: &str,
322 rp_origins: &[String],
323 attestation_policy: &AttestationPolicy,
324 credential_policy: &CredentialPolicy,
325) -> Result<RegistrationResult, WebauthnError> {
326 let client_data_bytes = validate_client_data(
327 client_data_json_b64,
328 challenge_bytes,
329 "webauthn.create",
330 rp_id,
331 rp_origins,
332 )?;
333
334 let attestation_bytes = base64_decode_urlsafe(attestation_object_b64)?;
335 let attestation_val: ciborium::Value = ciborium::de::from_reader(&attestation_bytes[..])
336 .map_err(|e| {
337 WebauthnError::AttestationError(format!("attestation object CBOR parse error: {e}"))
338 })?;
339
340 let attestation_entries = match &attestation_val {
343 ciborium::Value::Map(entries) => entries.clone(),
344 other => {
345 let hex = |b: &[u8]| {
346 let mut out = String::new();
347 for x in b.iter().take(8) {
348 use std::fmt::Write as _;
349 let _ = write!(out, "{x:02x}");
350 }
351 out
352 };
353 return Err(WebauthnError::AttestationError(format!(
354 "attestation object is not a CBOR map (decoded {} bytes, first: {}, value kind: {:?})",
355 attestation_bytes.len(),
356 hex(attestation_bytes.get(..8).unwrap_or(&attestation_bytes)),
357 match other {
358 ciborium::Value::Bytes(_) => "bytes",
359 ciborium::Value::Text(_) => "text",
360 ciborium::Value::Array(_) => "array",
361 ciborium::Value::Integer(_) => "integer",
362 ciborium::Value::Bool(_) => "bool",
363 _ => "other",
364 },
365 )));
366 }
367 };
368
369 let mut fmt: Option<String> = None;
370 let mut auth_data_bytes: Option<Vec<u8>> = None;
371 let mut att_stmt: Option<ciborium::Value> = None;
372
373 for (key, val) in &attestation_entries {
374 if let ciborium::Value::Text(k) = key {
375 match k.as_str() {
376 "fmt" => {
377 if let ciborium::Value::Text(s) = val {
378 fmt = Some(s.clone());
379 }
380 }
381 "authData" => {
382 if let Some(b) = cbor_bytes(val) {
383 auth_data_bytes = Some(b);
384 }
385 }
386 "attStmt" => {
387 att_stmt = Some(val.clone());
388 }
389 _ => {}
390 }
391 }
392 }
393
394 let fmt = fmt.unwrap_or_else(|| "none".to_string());
395 let auth_data_bytes = auth_data_bytes.ok_or_else(|| {
396 WebauthnError::AttestationError("missing authData in attestation object".to_string())
397 })?;
398
399 let auth_data = parse_authenticator_data(&auth_data_bytes)?;
400
401 use sha2::Digest;
402 let computed_rp_id_hash = sha2::Sha256::digest(rp_id.as_bytes()).to_vec();
403 if auth_data.rp_id_hash != computed_rp_id_hash {
404 return Err(WebauthnError::VerificationFailed(format!(
405 "rpId hash mismatch: computed {:x?}, got {:x?}",
406 computed_rp_id_hash, auth_data.rp_id_hash
407 )));
408 }
409
410 if auth_data.flags & FLAG_UP == 0 {
411 return Err(WebauthnError::VerificationFailed(
412 "User Present flag not set".to_string(),
413 ));
414 }
415
416 if auth_data.flags & FLAG_AT == 0 {
417 return Err(WebauthnError::VerificationFailed(
418 "Attested Credential Data flag not set during registration".to_string(),
419 ));
420 }
421
422 enforce_flag_policy(auth_data.flags, credential_policy)?;
424
425 let credential_id = auth_data.credential_id.ok_or_else(|| {
426 WebauthnError::VerificationFailed("no credential ID in attested data".to_string())
427 })?;
428 let credential_id_b64 = base64_encode_urlsafe(&credential_id);
429
430 if existing_credential_id == credential_id_b64 {
431 return Err(WebauthnError::DuplicateCredential(credential_id_b64));
432 }
433
434 let public_key_cose = auth_data.credential_public_key_cose.ok_or_else(|| {
435 WebauthnError::VerificationFailed("no public key in attested data".to_string())
436 })?;
437
438 let (alg, cose_key) = parse_cose_key(&public_key_cose)?;
439
440 enforce_algorithm_allowlist(alg, credential_policy)?;
442
443 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
447 let aaguid = auth_data.aaguid.unwrap_or([0u8; 16]);
448 let attestation = verify_attestation(
449 &fmt,
450 att_stmt.as_ref(),
451 &auth_data_bytes,
452 &client_data_hash,
453 &credential_id,
454 alg,
455 &cose_key,
456 aaguid,
457 attestation_policy,
458 )?;
459
460 let user_verified = auth_data.flags & FLAG_UV != 0;
461
462 Ok(RegistrationResult {
463 credential_id: credential_id_b64,
464 public_key_cose,
465 device_name: format!("WebAuthn ({})", alg_to_name(alg)),
466 attestation_format: fmt,
467 attestation,
468 user_verified,
469 backup_eligible: auth_data.backup_eligible,
470 backup_state: auth_data.backup_state,
471 })
472}
473
474#[derive(Debug, Clone)]
479pub struct AuthenticationParams {
480 pub challenge_bytes: Vec<u8>,
482 pub client_data_json_b64: String,
484 pub authenticator_data_b64: String,
486 pub signature_b64: String,
488 pub credential_id_b64: String,
490 pub public_key_cose: Vec<u8>,
492 pub current_sign_count: u32,
494 pub allowed_credential_ids: Vec<String>,
496 pub rp_id: String,
498 pub rp_origins: Vec<String>,
500 pub policy: CredentialPolicy,
509}
510
511pub fn verify_authentication(
541 params: &AuthenticationParams,
542) -> Result<AuthenticationResult, WebauthnError> {
543 let AuthenticationParams {
544 challenge_bytes,
545 client_data_json_b64,
546 authenticator_data_b64,
547 signature_b64,
548 credential_id_b64,
549 public_key_cose,
550 current_sign_count,
551 allowed_credential_ids,
552 rp_id,
553 rp_origins,
554 policy,
555 } = params;
556
557 if !allowed_credential_ids.contains(credential_id_b64) {
558 return Err(WebauthnError::VerificationFailed(
559 "credential ID not in allowed list".to_string(),
560 ));
561 }
562
563 let client_data_bytes = validate_client_data(
564 client_data_json_b64,
565 challenge_bytes,
566 "webauthn.get",
567 rp_id,
568 rp_origins,
569 )?;
570
571 let authenticator_data = base64_decode_urlsafe(authenticator_data_b64)?;
572 let auth_data = parse_authenticator_data(&authenticator_data)?;
573
574 use sha2::Digest;
575 let computed_rp_id_hash = sha2::Sha256::digest(rp_id.as_bytes()).to_vec();
576 if auth_data.rp_id_hash != computed_rp_id_hash {
577 return Err(WebauthnError::VerificationFailed(format!(
578 "rpId hash mismatch: computed {:x?}, got {:x?}",
579 computed_rp_id_hash, auth_data.rp_id_hash
580 )));
581 }
582
583 if auth_data.flags & FLAG_UP == 0 {
584 return Err(WebauthnError::VerificationFailed(
585 "User Present flag not set".to_string(),
586 ));
587 }
588
589 enforce_flag_policy(auth_data.flags, policy)?;
591
592 check_sign_count(*current_sign_count, auth_data.sign_count)?;
593
594 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
595
596 let mut signed_data = Vec::with_capacity(authenticator_data.len() + 32);
597 signed_data.extend_from_slice(&authenticator_data);
598 signed_data.extend_from_slice(&client_data_hash);
599
600 let signature = base64_decode_urlsafe(signature_b64)?;
601
602 let (alg, cose_key) = parse_cose_key(public_key_cose)?;
603 verify_cose_signature(alg, &cose_key, &signed_data, &signature)?;
604
605 let user_verified = auth_data.flags & FLAG_UV != 0;
606 let new_sign_count = auth_data.sign_count;
607
608 Ok(AuthenticationResult {
609 credential_id: credential_id_b64.clone(),
610 new_sign_count,
611 user_verified,
612 backup_state: auth_data.backup_state,
613 backup_eligible: auth_data.backup_eligible,
614 })
615}
616
617#[allow(
621 clippy::unwrap_used,
622 clippy::expect_used,
623 clippy::indexing_slicing,
624 clippy::panic
625)]
626#[cfg(test)]
627mod tests {
628 use super::*;
629 use crate::crypto::tests::{build_cose_ec2_key, build_cose_rsa_key};
630 use crate::crypto::{base64_encode_urlsafe, generate_challenge_bytes, COSE_ALG_RS256};
631 use ring::signature::KeyPair as _;
632
633 fn build_attestation_object(auth_data: &[u8]) -> Vec<u8> {
634 use ciborium::Value;
635 let map = vec![
637 (
638 Value::Text("fmt".to_string()),
639 Value::Text("none".to_string()),
640 ),
641 (Value::Text("attStmt".to_string()), Value::Map(vec![])),
642 (
643 Value::Text("authData".to_string()),
644 Value::Bytes(auth_data.to_vec()),
645 ),
646 ];
647 let mut buf = Vec::new();
648 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
649 buf
650 }
651
652 fn build_auth_data_with_credential(
653 rp_id: &str,
654 flags: u8,
655 sign_count: u32,
656 credential_id: &[u8],
657 cose_key: &[u8],
658 ) -> Vec<u8> {
659 use sha2::Digest;
660 let rp_id_hash = sha2::Sha256::digest(rp_id.as_bytes()).to_vec();
661 let mut auth_data = Vec::new();
662 auth_data.extend_from_slice(&rp_id_hash);
663 auth_data.push(flags);
664 auth_data.extend_from_slice(&sign_count.to_be_bytes());
665
666 if flags & FLAG_AT != 0 {
667 auth_data.extend_from_slice(&[0u8; 16]);
668 auth_data.extend_from_slice(&(credential_id.len() as u16).to_be_bytes());
669 auth_data.extend_from_slice(credential_id);
670 auth_data.extend_from_slice(cose_key);
671 }
672
673 auth_data
674 }
675
676 #[test]
677 fn test_parse_authenticator_data_minimal() {
678 let mut auth_data = vec![0u8; 37];
679 auth_data[..32].copy_from_slice(&[0xAA; 32]);
680 auth_data[32] = FLAG_UP;
681 auth_data[33..37].copy_from_slice(&1u32.to_be_bytes());
682
683 let parsed = parse_authenticator_data(&auth_data).unwrap();
684 assert_eq!(parsed.rp_id_hash, vec![0xAA; 32]);
685 assert_eq!(parsed.flags, FLAG_UP);
686 assert_eq!(parsed.sign_count, 1);
687 assert!(parsed.credential_public_key_cose.is_none());
688 assert!(parsed.credential_id.is_none());
689 }
690
691 #[test]
692 fn test_parse_authenticator_data_with_attested_credential() {
693 let credential_id = vec![0x01, 0x02, 0x03, 0x04];
694 let public_key_cose = vec![0x10, 0x20, 0x30];
695
696 let total_len = 37 + 16 + 2 + credential_id.len() + public_key_cose.len();
697 let mut auth_data = vec![0u8; total_len];
698 auth_data[32] = FLAG_UP | FLAG_AT;
699 auth_data[33..37].copy_from_slice(&5u32.to_be_bytes());
700
701 let offset = 37 + 16;
702 auth_data[offset..offset + 2].copy_from_slice(&(credential_id.len() as u16).to_be_bytes());
703 auth_data[offset + 2..offset + 2 + credential_id.len()].copy_from_slice(&credential_id);
704 let pk_offset = offset + 2 + credential_id.len();
705 auth_data[pk_offset..].copy_from_slice(&public_key_cose);
706
707 let parsed = parse_authenticator_data(&auth_data).unwrap();
708 assert_eq!(parsed.flags, FLAG_UP | FLAG_AT);
709 assert_eq!(parsed.sign_count, 5);
710 assert_eq!(parsed.credential_id, Some(credential_id));
711 assert_eq!(parsed.credential_public_key_cose, Some(public_key_cose));
712 }
713
714 #[test]
715 fn test_parse_authenticator_data_too_short() {
716 let auth_data = vec![0u8; 10];
717 let result = parse_authenticator_data(&auth_data);
718 assert!(result.is_err());
719 }
720
721 #[test]
722 fn test_verify_registration_valid_es256() {
723 use ring::signature::EcdsaKeyPair;
724
725 let rng = ring::rand::SystemRandom::new();
726 let pkcs8 =
727 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
728 .unwrap();
729 let key_pair = EcdsaKeyPair::from_pkcs8(
730 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
731 pkcs8.as_ref(),
732 &rng,
733 )
734 .unwrap();
735
736 let pub_bytes = key_pair.public_key().as_ref().to_vec();
737 let x = pub_bytes[1..33].to_vec();
738 let y = pub_bytes[33..65].to_vec();
739 let cose_key = build_cose_ec2_key(&x, &y);
740
741 let credential_id = vec![0x01, 0x02, 0x03, 0x04];
742 let auth_data = build_auth_data_with_credential(
743 "localhost",
744 FLAG_UP | FLAG_AT,
745 0,
746 &credential_id,
747 &cose_key,
748 );
749 let att_obj = build_attestation_object(&auth_data);
750
751 let challenge = generate_challenge_bytes();
752 let challenge_b64 = base64_encode_urlsafe(&challenge);
753
754 let client_data = serde_json::json!({
755 "type": "webauthn.create",
756 "challenge": challenge_b64,
757 "origin": "http://localhost:8080",
758 });
759 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
760 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
761
762 let result = verify_registration(
763 &challenge,
764 &client_data_b64,
765 &att_obj_b64,
766 "different-id",
767 "localhost",
768 &["http://localhost:8080".to_string()],
769 &AttestationPolicy::default(),
770 &CredentialPolicy::default(),
771 )
772 .unwrap();
773
774 assert_eq!(result.credential_id, base64_encode_urlsafe(&credential_id));
775 assert!(result.attestation_format == "none");
776 }
777
778 #[test]
779 fn test_verify_registration_challenge_mismatch() {
780 let auth_data = build_auth_data_with_credential(
781 "localhost",
782 FLAG_UP | FLAG_AT,
783 0,
784 &[0x01],
785 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
786 );
787 let att_obj = build_attestation_object(&auth_data);
788
789 let challenge = generate_challenge_bytes();
790 let _challenge_b64 = base64_encode_urlsafe(&challenge);
791
792 let client_data = serde_json::json!({
793 "type": "webauthn.create",
794 "challenge": base64_encode_urlsafe(&[1u8; 32]),
795 "origin": "http://localhost:8080",
796 });
797 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
798 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
799
800 let result = verify_registration(
801 &challenge,
802 &client_data_b64,
803 &att_obj_b64,
804 "different",
805 "localhost",
806 &["http://localhost:8080".to_string()],
807 &AttestationPolicy::default(),
808 &CredentialPolicy::default(),
809 );
810 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
811 }
812
813 #[test]
814 fn test_verify_registration_wrong_type() {
815 let auth_data = build_auth_data_with_credential(
816 "localhost",
817 FLAG_UP | FLAG_AT,
818 0,
819 &[0x01],
820 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
821 );
822 let att_obj = build_attestation_object(&auth_data);
823
824 let challenge = generate_challenge_bytes();
825 let challenge_b64 = base64_encode_urlsafe(&challenge);
826
827 let client_data = serde_json::json!({
828 "type": "webauthn.get",
829 "challenge": challenge_b64,
830 "origin": "http://localhost:8080",
831 });
832 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
833 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
834
835 let result = verify_registration(
836 &challenge,
837 &client_data_b64,
838 &att_obj_b64,
839 "different",
840 "localhost",
841 &["http://localhost:8080".to_string()],
842 &AttestationPolicy::default(),
843 &CredentialPolicy::default(),
844 );
845 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
846 }
847
848 #[test]
849 fn test_verify_registration_wrong_origin() {
850 let auth_data = build_auth_data_with_credential(
851 "localhost",
852 FLAG_UP | FLAG_AT,
853 0,
854 &[0x01],
855 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
856 );
857 let att_obj = build_attestation_object(&auth_data);
858
859 let challenge = generate_challenge_bytes();
860 let challenge_b64 = base64_encode_urlsafe(&challenge);
861
862 let client_data = serde_json::json!({
863 "type": "webauthn.create",
864 "challenge": challenge_b64,
865 "origin": "http://evil.com",
866 });
867 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
868 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
869
870 let result = verify_registration(
871 &challenge,
872 &client_data_b64,
873 &att_obj_b64,
874 "different",
875 "localhost",
876 &["http://localhost:8080".to_string()],
877 &AttestationPolicy::default(),
878 &CredentialPolicy::default(),
879 );
880 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
881 }
882
883 #[test]
884 fn test_verify_registration_duplicate() {
885 let auth_data = build_auth_data_with_credential(
886 "localhost",
887 FLAG_UP | FLAG_AT,
888 0,
889 &[0x01, 0x02, 0x03],
890 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
891 );
892 let att_obj = build_attestation_object(&auth_data);
893
894 let challenge = generate_challenge_bytes();
895 let challenge_b64 = base64_encode_urlsafe(&challenge);
896
897 let client_data = serde_json::json!({
898 "type": "webauthn.create",
899 "challenge": challenge_b64,
900 "origin": "http://localhost:8080",
901 });
902 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
903 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
904 let cred_id_b64 = base64_encode_urlsafe(&[0x01, 0x02, 0x03]);
905
906 let result = verify_registration(
907 &challenge,
908 &client_data_b64,
909 &att_obj_b64,
910 &cred_id_b64,
911 "localhost",
912 &["http://localhost:8080".to_string()],
913 &AttestationPolicy::default(),
914 &CredentialPolicy::default(),
915 );
916 assert!(matches!(result, Err(WebauthnError::DuplicateCredential(_))));
917 }
918
919 #[test]
920 fn test_verify_registration_wrong_rp_id_hash() {
921 let credential_id = vec![0x01];
922 let cose_key = build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]);
923 let auth_data = build_auth_data_with_credential(
924 "evil.com",
925 FLAG_UP | FLAG_AT,
926 0,
927 &credential_id,
928 &cose_key,
929 );
930 let att_obj = build_attestation_object(&auth_data);
931
932 let challenge = generate_challenge_bytes();
933 let challenge_b64 = base64_encode_urlsafe(&challenge);
934
935 let client_data = serde_json::json!({
936 "type": "webauthn.create",
937 "challenge": challenge_b64,
938 "origin": "http://localhost:8080",
939 });
940 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
941 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
942
943 let result = verify_registration(
944 &challenge,
945 &client_data_b64,
946 &att_obj_b64,
947 "different",
948 "localhost",
949 &["http://localhost:8080".to_string()],
950 &AttestationPolicy::default(),
951 &CredentialPolicy::default(),
952 );
953 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
954 }
955
956 #[test]
957 fn test_verify_registration_missing_up_flag() {
958 let credential_id = vec![0x01];
959 let cose_key = build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]);
960 let auth_data =
961 build_auth_data_with_credential("localhost", FLAG_AT, 0, &credential_id, &cose_key);
962 let att_obj = build_attestation_object(&auth_data);
963
964 let challenge = generate_challenge_bytes();
965 let challenge_b64 = base64_encode_urlsafe(&challenge);
966
967 let client_data = serde_json::json!({
968 "type": "webauthn.create",
969 "challenge": challenge_b64,
970 "origin": "http://localhost:8080",
971 });
972 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
973 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
974
975 let result = verify_registration(
976 &challenge,
977 &client_data_b64,
978 &att_obj_b64,
979 "different",
980 "localhost",
981 &["http://localhost:8080".to_string()],
982 &AttestationPolicy::default(),
983 &CredentialPolicy::default(),
984 );
985 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
986 }
987
988 #[test]
992 fn registration_rejects_missing_at_flag() {
993 let auth_data = build_auth_data_with_credential(
994 "localhost",
995 FLAG_UP, 0,
997 &[0x01],
998 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
999 );
1000 let att_obj = build_attestation_object(&auth_data);
1001
1002 let challenge = generate_challenge_bytes();
1003 let challenge_b64 = base64_encode_urlsafe(&challenge);
1004
1005 let client_data = serde_json::json!({
1006 "type": "webauthn.create",
1007 "challenge": challenge_b64,
1008 "origin": "http://localhost:8080",
1009 });
1010 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1011 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
1012
1013 let result = verify_registration(
1014 &challenge,
1015 &client_data_b64,
1016 &att_obj_b64,
1017 "different",
1018 "localhost",
1019 &["http://localhost:8080".to_string()],
1020 &AttestationPolicy::default(),
1021 &CredentialPolicy::default(),
1022 );
1023 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1024 }
1025
1026 #[test]
1027 fn test_verify_authentication_valid_es256() {
1028 use ring::signature::EcdsaKeyPair;
1029
1030 let rng = ring::rand::SystemRandom::new();
1031 let pkcs8 =
1032 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1033 .unwrap();
1034 let key_pair = EcdsaKeyPair::from_pkcs8(
1035 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1036 pkcs8.as_ref(),
1037 &rng,
1038 )
1039 .unwrap();
1040
1041 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1042 let x = pub_bytes[1..33].to_vec();
1043 let y = pub_bytes[33..65].to_vec();
1044 let cose_key = build_cose_ec2_key(&x, &y);
1045
1046 let challenge = generate_challenge_bytes();
1047 let challenge_b64 = base64_encode_urlsafe(&challenge);
1048
1049 use sha2::Digest;
1050 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1051 let mut auth_data = Vec::new();
1052 auth_data.extend_from_slice(&rp_id_hash);
1053 auth_data.push(FLAG_UP);
1054 auth_data.extend_from_slice(&1u32.to_be_bytes());
1055
1056 let client_data = serde_json::json!({
1057 "type": "webauthn.get",
1058 "challenge": challenge_b64,
1059 "origin": "http://localhost:8080",
1060 });
1061 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1062 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1063 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1064
1065 let mut signed_data = Vec::new();
1066 signed_data.extend_from_slice(&auth_data);
1067 signed_data.extend_from_slice(&client_data_hash);
1068
1069 let signature = key_pair.sign(&rng, &signed_data).unwrap();
1070
1071 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1072 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
1073 let cred_id_b64 = base64_encode_urlsafe(&[0x01, 0x02]);
1074
1075 let result = verify_authentication(&AuthenticationParams {
1076 challenge_bytes: challenge.clone(),
1077 client_data_json_b64: client_data_b64,
1078 authenticator_data_b64: auth_data_b64,
1079 signature_b64: sig_b64,
1080 credential_id_b64: cred_id_b64.clone(),
1081 public_key_cose: cose_key,
1082 current_sign_count: 0,
1083 allowed_credential_ids: vec![cred_id_b64.clone()],
1084 rp_id: "localhost".to_string(),
1085 rp_origins: vec!["http://localhost:8080".to_string()],
1086 policy: CredentialPolicy::default(),
1087 })
1088 .unwrap();
1089
1090 assert_eq!(result.credential_id, cred_id_b64);
1091 assert_eq!(result.new_sign_count, 1);
1092 }
1093
1094 #[test]
1095 fn test_verify_authentication_wrong_signature() {
1096 use ring::signature::EcdsaKeyPair;
1097
1098 let rng = ring::rand::SystemRandom::new();
1099 let pkcs8 =
1100 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1101 .unwrap();
1102 let key_pair = EcdsaKeyPair::from_pkcs8(
1103 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1104 pkcs8.as_ref(),
1105 &rng,
1106 )
1107 .unwrap();
1108
1109 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1110 let x = pub_bytes[1..33].to_vec();
1111 let y = pub_bytes[33..65].to_vec();
1112 let cose_key = build_cose_ec2_key(&x, &y);
1113
1114 let challenge = generate_challenge_bytes();
1115 let challenge_b64 = base64_encode_urlsafe(&challenge);
1116
1117 use sha2::Digest;
1118 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1119 let mut auth_data = Vec::new();
1120 auth_data.extend_from_slice(&rp_id_hash);
1121 auth_data.push(FLAG_UP);
1122 auth_data.extend_from_slice(&1u32.to_be_bytes());
1123
1124 let client_data = serde_json::json!({
1125 "type": "webauthn.get",
1126 "challenge": challenge_b64,
1127 "origin": "http://localhost:8080",
1128 });
1129 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1130 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1131 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1132
1133 let mut signed_data = Vec::new();
1134 signed_data.extend_from_slice(&auth_data);
1135 signed_data.extend_from_slice(&client_data_hash);
1136
1137 let mut wrong_sig = vec![0u8; 64];
1138 wrong_sig[0] = 0xFF;
1139
1140 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1141 let sig_b64 = base64_encode_urlsafe(&wrong_sig);
1142 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
1143
1144 let result = verify_authentication(&AuthenticationParams {
1145 challenge_bytes: challenge.clone(),
1146 client_data_json_b64: client_data_b64,
1147 authenticator_data_b64: auth_data_b64,
1148 signature_b64: sig_b64,
1149 credential_id_b64: cred_id_b64.clone(),
1150 public_key_cose: cose_key,
1151 current_sign_count: 0,
1152 allowed_credential_ids: vec![cred_id_b64],
1153 rp_id: "localhost".to_string(),
1154 rp_origins: vec!["http://localhost:8080".to_string()],
1155 policy: CredentialPolicy::default(),
1156 });
1157 assert!(matches!(
1158 result,
1159 Err(WebauthnError::SignatureVerificationFailed)
1160 ));
1161 }
1162
1163 #[test]
1164 fn test_verify_authentication_credential_not_allowed() {
1165 let challenge = generate_challenge_bytes();
1166 let challenge_b64 = base64_encode_urlsafe(&challenge);
1167
1168 let client_data = serde_json::json!({
1169 "type": "webauthn.get",
1170 "challenge": challenge_b64,
1171 "origin": "http://localhost:8080",
1172 });
1173 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1174
1175 use sha2::Digest;
1176 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1177 let mut auth_data = Vec::new();
1178 auth_data.extend_from_slice(&rp_id_hash);
1179 auth_data.push(FLAG_UP);
1180 auth_data.extend_from_slice(&0u32.to_be_bytes());
1181 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1182
1183 let result = verify_authentication(&AuthenticationParams {
1184 challenge_bytes: challenge.clone(),
1185 client_data_json_b64: client_data_b64,
1186 authenticator_data_b64: auth_data_b64,
1187 signature_b64: "sig".to_string(),
1188 credential_id_b64: "unauthorized-cred".to_string(),
1189 public_key_cose: vec![0x10, 0x20],
1190 current_sign_count: 0,
1191 allowed_credential_ids: vec!["allowed-cred".to_string()],
1192 rp_id: "localhost".to_string(),
1193 rp_origins: vec!["http://localhost:8080".to_string()],
1194 policy: CredentialPolicy::default(),
1195 });
1196 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1197 }
1198
1199 #[test]
1200 fn test_verify_authentication_wrong_origin() {
1201 let challenge = generate_challenge_bytes();
1202 let challenge_b64 = base64_encode_urlsafe(&challenge);
1203
1204 let client_data = serde_json::json!({
1205 "type": "webauthn.get",
1206 "challenge": challenge_b64,
1207 "origin": "http://evil.com",
1208 });
1209 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1210
1211 use sha2::Digest;
1212 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1213 let mut auth_data = Vec::new();
1214 auth_data.extend_from_slice(&rp_id_hash);
1215 auth_data.push(FLAG_UP);
1216 auth_data.extend_from_slice(&0u32.to_be_bytes());
1217 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1218
1219 let result = verify_authentication(&AuthenticationParams {
1220 challenge_bytes: challenge.clone(),
1221 client_data_json_b64: client_data_b64,
1222 authenticator_data_b64: auth_data_b64,
1223 signature_b64: "sig".to_string(),
1224 credential_id_b64: "cred".to_string(),
1225 public_key_cose: vec![0x10],
1226 current_sign_count: 0,
1227 allowed_credential_ids: vec!["cred".to_string()],
1228 rp_id: "localhost".to_string(),
1229 rp_origins: vec!["http://localhost:8080".to_string()],
1230 policy: CredentialPolicy::default(),
1231 });
1232 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1233 }
1234
1235 #[test]
1236 fn test_verify_authentication_wrong_type() {
1237 let challenge = generate_challenge_bytes();
1238 let challenge_b64 = base64_encode_urlsafe(&challenge);
1239
1240 let client_data = serde_json::json!({
1241 "type": "webauthn.create",
1242 "challenge": challenge_b64,
1243 "origin": "http://localhost:8080",
1244 });
1245 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1246
1247 use sha2::Digest;
1248 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1249 let mut auth_data = Vec::new();
1250 auth_data.extend_from_slice(&rp_id_hash);
1251 auth_data.push(FLAG_UP);
1252 auth_data.extend_from_slice(&0u32.to_be_bytes());
1253 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1254
1255 let result = verify_authentication(&AuthenticationParams {
1256 challenge_bytes: challenge.clone(),
1257 client_data_json_b64: client_data_b64,
1258 authenticator_data_b64: auth_data_b64,
1259 signature_b64: "sig".to_string(),
1260 credential_id_b64: "cred".to_string(),
1261 public_key_cose: vec![0x10],
1262 current_sign_count: 0,
1263 allowed_credential_ids: vec!["cred".to_string()],
1264 rp_id: "localhost".to_string(),
1265 rp_origins: vec!["http://localhost:8080".to_string()],
1266 policy: CredentialPolicy::default(),
1267 });
1268 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1269 }
1270
1271 #[test]
1272 fn test_verify_authentication_sign_count_decrease() {
1273 use ring::signature::EcdsaKeyPair;
1274
1275 let rng = ring::rand::SystemRandom::new();
1276 let pkcs8 =
1277 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1278 .unwrap();
1279 let key_pair = EcdsaKeyPair::from_pkcs8(
1280 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1281 pkcs8.as_ref(),
1282 &rng,
1283 )
1284 .unwrap();
1285
1286 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1287 let x = pub_bytes[1..33].to_vec();
1288 let y = pub_bytes[33..65].to_vec();
1289 let cose_key = build_cose_ec2_key(&x, &y);
1290
1291 let challenge = generate_challenge_bytes();
1292 let challenge_b64 = base64_encode_urlsafe(&challenge);
1293
1294 use sha2::Digest;
1295 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1296 let mut auth_data = Vec::new();
1297 auth_data.extend_from_slice(&rp_id_hash);
1298 auth_data.push(FLAG_UP);
1299 auth_data.extend_from_slice(&5u32.to_be_bytes());
1300
1301 let client_data = serde_json::json!({
1302 "type": "webauthn.get",
1303 "challenge": challenge_b64,
1304 "origin": "http://localhost:8080",
1305 });
1306 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1307 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1308 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1309
1310 let mut signed_data = Vec::new();
1311 signed_data.extend_from_slice(&auth_data);
1312 signed_data.extend_from_slice(&client_data_hash);
1313
1314 let signature = key_pair.sign(&rng, &signed_data).unwrap();
1315
1316 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1317 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
1318 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
1319
1320 let result = verify_authentication(&AuthenticationParams {
1321 challenge_bytes: challenge.clone(),
1322 client_data_json_b64: client_data_b64,
1323 authenticator_data_b64: auth_data_b64,
1324 signature_b64: sig_b64,
1325 credential_id_b64: cred_id_b64.clone(),
1326 public_key_cose: cose_key,
1327 current_sign_count: 10,
1328 allowed_credential_ids: vec![cred_id_b64],
1329 rp_id: "localhost".to_string(),
1330 rp_origins: vec!["http://localhost:8080".to_string()],
1331 policy: CredentialPolicy::default(),
1332 });
1333 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1334 }
1335
1336 #[test]
1337 fn test_verify_authentication_wrong_rp_id_hash() {
1338 let challenge = generate_challenge_bytes();
1339 let challenge_b64 = base64_encode_urlsafe(&challenge);
1340
1341 let client_data = serde_json::json!({
1342 "type": "webauthn.get",
1343 "challenge": challenge_b64,
1344 "origin": "http://localhost:8080",
1345 });
1346 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1347
1348 use sha2::Digest;
1349 let rp_id_hash = sha2::Sha256::digest(b"evil.com").to_vec();
1350 let mut auth_data = Vec::new();
1351 auth_data.extend_from_slice(&rp_id_hash);
1352 auth_data.push(FLAG_UP);
1353 auth_data.extend_from_slice(&0u32.to_be_bytes());
1354 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1355
1356 let result = verify_authentication(&AuthenticationParams {
1357 challenge_bytes: challenge.clone(),
1358 client_data_json_b64: client_data_b64,
1359 authenticator_data_b64: auth_data_b64,
1360 signature_b64: "sig".to_string(),
1361 credential_id_b64: "cred".to_string(),
1362 public_key_cose: vec![0x10],
1363 current_sign_count: 0,
1364 allowed_credential_ids: vec!["cred".to_string()],
1365 rp_id: "localhost".to_string(),
1366 rp_origins: vec!["http://localhost:8080".to_string()],
1367 policy: CredentialPolicy::default(),
1368 });
1369 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1370 }
1371
1372 #[test]
1373 fn test_verify_authentication_missing_up_flag() {
1374 use ring::signature::EcdsaKeyPair;
1375
1376 let rng = ring::rand::SystemRandom::new();
1377 let pkcs8 =
1378 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1379 .unwrap();
1380 let key_pair = EcdsaKeyPair::from_pkcs8(
1381 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1382 pkcs8.as_ref(),
1383 &rng,
1384 )
1385 .unwrap();
1386
1387 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1388 let x = pub_bytes[1..33].to_vec();
1389 let y = pub_bytes[33..65].to_vec();
1390 let cose_key = build_cose_ec2_key(&x, &y);
1391
1392 let challenge = generate_challenge_bytes();
1393 let challenge_b64 = base64_encode_urlsafe(&challenge);
1394
1395 use sha2::Digest;
1396 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1397 let mut auth_data = Vec::new();
1398 auth_data.extend_from_slice(&rp_id_hash);
1399 auth_data.push(0);
1400 auth_data.extend_from_slice(&1u32.to_be_bytes());
1401
1402 let client_data = serde_json::json!({
1403 "type": "webauthn.get",
1404 "challenge": challenge_b64,
1405 "origin": "http://localhost:8080",
1406 });
1407 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1408 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1409 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1410
1411 let mut signed_data = Vec::new();
1412 signed_data.extend_from_slice(&auth_data);
1413 signed_data.extend_from_slice(&client_data_hash);
1414
1415 let signature = key_pair.sign(&rng, &signed_data).unwrap();
1416
1417 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1418 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
1419 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
1420
1421 let result = verify_authentication(&AuthenticationParams {
1422 challenge_bytes: challenge.clone(),
1423 client_data_json_b64: client_data_b64,
1424 authenticator_data_b64: auth_data_b64,
1425 signature_b64: sig_b64,
1426 credential_id_b64: cred_id_b64.clone(),
1427 public_key_cose: cose_key,
1428 current_sign_count: 0,
1429 allowed_credential_ids: vec![cred_id_b64],
1430 rp_id: "localhost".to_string(),
1431 rp_origins: vec!["http://localhost:8080".to_string()],
1432 policy: CredentialPolicy::default(),
1433 });
1434 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1435 }
1436
1437 #[test]
1438 fn test_verify_authentication_wrong_rp_id_in_client_data() {
1439 use ring::signature::EcdsaKeyPair;
1440
1441 let rng = ring::rand::SystemRandom::new();
1442 let pkcs8 =
1443 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1444 .unwrap();
1445 let key_pair = EcdsaKeyPair::from_pkcs8(
1446 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1447 pkcs8.as_ref(),
1448 &rng,
1449 )
1450 .unwrap();
1451
1452 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1453 let x = pub_bytes[1..33].to_vec();
1454 let y = pub_bytes[33..65].to_vec();
1455 let cose_key = build_cose_ec2_key(&x, &y);
1456
1457 let challenge = generate_challenge_bytes();
1458 let challenge_b64 = base64_encode_urlsafe(&challenge);
1459
1460 let client_data = serde_json::json!({
1461 "type": "webauthn.get",
1462 "challenge": challenge_b64,
1463 "origin": "http://localhost:8080",
1464 "rpId": "evil.com",
1465 });
1466 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1467 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1468
1469 use sha2::Digest;
1470 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1471 let mut auth_data = Vec::new();
1472 auth_data.extend_from_slice(&rp_id_hash);
1473 auth_data.push(FLAG_UP);
1474 auth_data.extend_from_slice(&1u32.to_be_bytes());
1475
1476 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1477 let mut signed_data = Vec::new();
1478 signed_data.extend_from_slice(&auth_data);
1479 signed_data.extend_from_slice(&client_data_hash);
1480
1481 let signature = key_pair.sign(&rng, &signed_data).unwrap();
1482
1483 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1484 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
1485 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
1486
1487 let result = verify_authentication(&AuthenticationParams {
1488 challenge_bytes: challenge.clone(),
1489 client_data_json_b64: client_data_b64,
1490 authenticator_data_b64: auth_data_b64,
1491 signature_b64: sig_b64,
1492 credential_id_b64: cred_id_b64.clone(),
1493 public_key_cose: cose_key,
1494 current_sign_count: 0,
1495 allowed_credential_ids: vec![cred_id_b64],
1496 rp_id: "localhost".to_string(),
1497 rp_origins: vec!["http://localhost:8080".to_string()],
1498 policy: CredentialPolicy::default(),
1499 });
1500 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1501 }
1502
1503 #[test]
1504 fn test_verify_authentication_challenge_mismatch() {
1505 let challenge = generate_challenge_bytes();
1506 let _challenge_b64 = base64_encode_urlsafe(&challenge);
1507
1508 let client_data = serde_json::json!({
1509 "type": "webauthn.get",
1510 "challenge": base64_encode_urlsafe(&[99u8; 32]),
1511 "origin": "http://localhost:8080",
1512 });
1513 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1514
1515 use sha2::Digest;
1516 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1517 let mut auth_data = Vec::new();
1518 auth_data.extend_from_slice(&rp_id_hash);
1519 auth_data.push(FLAG_UP);
1520 auth_data.extend_from_slice(&0u32.to_be_bytes());
1521 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1522
1523 let result = verify_authentication(&AuthenticationParams {
1524 challenge_bytes: challenge.clone(),
1525 client_data_json_b64: client_data_b64,
1526 authenticator_data_b64: auth_data_b64,
1527 signature_b64: "sig".to_string(),
1528 credential_id_b64: "cred".to_string(),
1529 public_key_cose: vec![0x10],
1530 current_sign_count: 0,
1531 allowed_credential_ids: vec!["cred".to_string()],
1532 rp_id: "localhost".to_string(),
1533 rp_origins: vec!["http://localhost:8080".to_string()],
1534 policy: CredentialPolicy::default(),
1535 });
1536 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1537 }
1538
1539 #[test]
1540 fn test_verify_authentication_rs256_roundtrip() {
1541 let cose_key = build_cose_rsa_key(&[0xAA; 256], &[0x01, 0x00, 0x01]);
1546 let (alg, key) = crate::crypto::parse_cose_key(&cose_key).unwrap();
1547 assert_eq!(alg, COSE_ALG_RS256);
1548 let result = verify_cose_signature(COSE_ALG_RS256, &key, b"data", &[0u8; 256]);
1549 assert!(matches!(
1550 result,
1551 Err(WebauthnError::SignatureVerificationFailed)
1552 ));
1553 }
1554
1555 fn valid_registration(rp_id_field: bool, extra_att_key: bool) -> (Vec<u8>, String, String) {
1559 use ring::signature::EcdsaKeyPair;
1560
1561 let rng = ring::rand::SystemRandom::new();
1562 let pkcs8 =
1563 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1564 .unwrap();
1565 let key_pair = EcdsaKeyPair::from_pkcs8(
1566 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1567 pkcs8.as_ref(),
1568 &rng,
1569 )
1570 .unwrap();
1571 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1572 let cose_key = build_cose_ec2_key(&pub_bytes[1..33], &pub_bytes[33..65]);
1573
1574 let credential_id = vec![0x42u8; 8];
1575 let auth_data = build_auth_data_with_credential(
1576 "localhost",
1577 FLAG_UP | FLAG_AT,
1578 0,
1579 &credential_id,
1580 &cose_key,
1581 );
1582
1583 let challenge = generate_challenge_bytes();
1584 let challenge_b64 = base64_encode_urlsafe(&challenge);
1585
1586 let mut client_data = serde_json::json!({
1587 "type": "webauthn.create",
1588 "challenge": challenge_b64,
1589 "origin": "http://localhost:8080",
1590 });
1591 if rp_id_field {
1592 client_data["rpId"] = serde_json::json!("localhost");
1593 }
1594 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1595
1596 let mut entries = vec![
1597 (
1598 ciborium::Value::Integer(1.into()),
1599 ciborium::Value::Text("none".to_string()),
1600 ),
1601 (
1602 ciborium::Value::Text("authData".to_string()),
1603 ciborium::Value::Bytes(auth_data),
1604 ),
1605 ];
1606 if extra_att_key {
1607 entries.push((
1608 ciborium::Value::Text("unknownKey".to_string()),
1609 ciborium::Value::Bool(true),
1610 ));
1611 }
1612 entries.push((
1613 ciborium::Value::Text("attStmt".to_string()),
1614 ciborium::Value::Map(vec![]),
1615 ));
1616 let mut buf = Vec::new();
1617 ciborium::ser::into_writer(&ciborium::Value::Map(entries), &mut buf).unwrap();
1618
1619 (challenge, client_data_b64, base64_encode_urlsafe(&buf))
1620 }
1621
1622 #[test]
1625 fn registration_accepts_rpid_field_and_unknown_keys() {
1626 let (challenge, client_data_b64, att_obj_b64) = valid_registration(true, true);
1627 let result = verify_registration(
1628 &challenge,
1629 &client_data_b64,
1630 &att_obj_b64,
1631 "",
1632 "localhost",
1633 &["http://localhost:8080".to_string()],
1634 &AttestationPolicy::default(),
1635 &CredentialPolicy::default(),
1636 )
1637 .unwrap();
1638 assert_eq!(result.attestation_format, "none");
1639 assert_eq!(result.credential_id, base64_encode_urlsafe(&[0x42u8; 8]));
1640 }
1641
1642 #[test]
1643 fn registration_rejects_missing_type_in_client_data() {
1644 let challenge = generate_challenge_bytes();
1645 let client_data = serde_json::json!({
1646 "challenge": base64_encode_urlsafe(&challenge),
1647 "origin": "http://localhost:8080",
1648 });
1649 let result = verify_registration(
1650 &challenge,
1651 &base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
1652 &base64_encode_urlsafe(&[0xFF; 8]),
1653 "",
1654 "localhost",
1655 &["http://localhost:8080".to_string()],
1656 &AttestationPolicy::default(),
1657 &CredentialPolicy::default(),
1658 );
1659 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1660 }
1661
1662 #[test]
1663 fn registration_rejects_missing_origin_in_client_data() {
1664 let challenge = generate_challenge_bytes();
1665 let client_data = serde_json::json!({
1666 "type": "webauthn.create",
1667 "challenge": base64_encode_urlsafe(&challenge),
1668 });
1669 let result = verify_registration(
1670 &challenge,
1671 &base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
1672 &base64_encode_urlsafe(&[0xFF; 8]),
1673 "",
1674 "localhost",
1675 &["http://localhost:8080".to_string()],
1676 &AttestationPolicy::default(),
1677 &CredentialPolicy::default(),
1678 );
1679 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1680 }
1681
1682 #[test]
1686 fn parse_auth_data_rejects_truncated_public_key() {
1687 let mut auth_data = vec![0u8; 37 + 16 + 2 + 2];
1688 auth_data[32] = FLAG_UP | FLAG_AT;
1689 auth_data[53..55].copy_from_slice(&2u16.to_be_bytes());
1690 let result = parse_authenticator_data(&auth_data);
1691 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1692 }
1693
1694 use crate::crypto::tests::{build_cose_ec2_key_alg, build_cose_okp_key};
1697 use crate::crypto::{COSE_ALG_ES256, COSE_ALG_ES384};
1698
1699 #[test]
1701 fn registration_roundtrip_es384() {
1702 use ring::signature::{EcdsaKeyPair, KeyPair as _, ECDSA_P384_SHA384_FIXED_SIGNING};
1703
1704 let rng = ring::rand::SystemRandom::new();
1705 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, &rng).unwrap();
1706 let key_pair =
1707 EcdsaKeyPair::from_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, pkcs8.as_ref(), &rng)
1708 .unwrap();
1709 let pub_bytes = key_pair.public_key().as_ref();
1710 let cose_key = build_cose_ec2_key_alg(2, -35, &pub_bytes[1..49], &pub_bytes[49..97]);
1711
1712 let credential_id = vec![0xE3u8; 8];
1713 let auth_data = build_auth_data_with_credential(
1714 "localhost",
1715 FLAG_UP | FLAG_AT | FLAG_UV | FLAG_BE | FLAG_BS,
1716 0,
1717 &credential_id,
1718 &cose_key,
1719 );
1720 let att_obj = build_attestation_object(&auth_data);
1721
1722 let challenge = generate_challenge_bytes();
1723 let client_data = serde_json::json!({
1724 "type": "webauthn.create",
1725 "challenge": base64_encode_urlsafe(&challenge),
1726 "origin": "http://localhost:8080",
1727 });
1728 let result = verify_registration(
1729 &challenge,
1730 &base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
1731 &base64_encode_urlsafe(&att_obj),
1732 "",
1733 "localhost",
1734 &["http://localhost:8080".to_string()],
1735 &AttestationPolicy::default(),
1736 &CredentialPolicy::default(),
1737 )
1738 .unwrap();
1739
1740 assert!(result.device_name.contains("ES384"));
1741 assert!(result.user_verified);
1742 assert!(result.backup_eligible);
1743 assert!(result.backup_state);
1744 }
1745
1746 #[test]
1748 fn registration_authentication_roundtrip_eddsa() {
1749 use ring::signature::{Ed25519KeyPair, KeyPair as _};
1750
1751 let rng = ring::rand::SystemRandom::new();
1752 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1753 let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1754 let cose_key = build_cose_okp_key(key_pair.public_key().as_ref());
1755
1756 let credential_id = vec![0xEDu8; 8];
1758 let auth_data = build_auth_data_with_credential(
1759 "localhost",
1760 FLAG_UP | FLAG_AT,
1761 0,
1762 &credential_id,
1763 &cose_key,
1764 );
1765 let att_obj = build_attestation_object(&auth_data);
1766 let challenge = generate_challenge_bytes();
1767 let client_data = serde_json::json!({
1768 "type": "webauthn.create",
1769 "challenge": base64_encode_urlsafe(&challenge),
1770 "origin": "http://localhost:8080",
1771 });
1772 let registration = verify_registration(
1773 &challenge,
1774 &base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
1775 &base64_encode_urlsafe(&att_obj),
1776 "",
1777 "localhost",
1778 &["http://localhost:8080".to_string()],
1779 &AttestationPolicy::default(),
1780 &CredentialPolicy::default(),
1781 )
1782 .unwrap();
1783 assert!(registration.device_name.contains("EdDSA"));
1784 assert!(!registration.backup_eligible);
1785
1786 let auth_challenge = generate_challenge_bytes();
1788 let assertion_client_data = serde_json::json!({
1789 "type": "webauthn.get",
1790 "challenge": base64_encode_urlsafe(&auth_challenge),
1791 "origin": "http://localhost:8080",
1792 });
1793 let assertion_client_data_bytes = serde_json::to_vec(&assertion_client_data).unwrap();
1794 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1795 let mut assertion_auth_data = rp_id_hash;
1796 assertion_auth_data.push(FLAG_UP | FLAG_BS);
1797 assertion_auth_data.extend_from_slice(&1u32.to_be_bytes());
1798
1799 use sha2::Digest as _;
1800 let mut signed_data = assertion_auth_data.clone();
1801 signed_data.extend_from_slice(&sha2::Sha256::digest(&assertion_client_data_bytes));
1802 let signature = key_pair.sign(&signed_data);
1803
1804 let cred_b64 = base64_encode_urlsafe(&credential_id);
1805 let result = verify_authentication(&AuthenticationParams {
1806 challenge_bytes: auth_challenge,
1807 client_data_json_b64: base64_encode_urlsafe(&assertion_client_data_bytes),
1808 authenticator_data_b64: base64_encode_urlsafe(&assertion_auth_data),
1809 signature_b64: base64_encode_urlsafe(signature.as_ref()),
1810 credential_id_b64: cred_b64.clone(),
1811 public_key_cose: cose_key,
1812 current_sign_count: 0,
1813 allowed_credential_ids: vec![cred_b64],
1814 rp_id: "localhost".to_string(),
1815 rp_origins: vec!["http://localhost:8080".to_string()],
1816 policy: CredentialPolicy::default(),
1817 })
1818 .unwrap();
1819 assert_eq!(result.new_sign_count, 1);
1820 assert!(result.backup_state);
1821 assert!(!result.backup_eligible);
1822 }
1823
1824 #[test]
1828 fn registration_rejects_p256_key_claiming_es384() {
1829 let cose_key = build_cose_ec2_key_alg(1, -35, &[0xAA; 32], &[0xBB; 32]); let auth_data =
1831 build_auth_data_with_credential("localhost", FLAG_UP | FLAG_AT, 0, &[0x01], &cose_key);
1832 let att_obj = build_attestation_object(&auth_data);
1833 let challenge = generate_challenge_bytes();
1834 let client_data = serde_json::json!({
1835 "type": "webauthn.create",
1836 "challenge": base64_encode_urlsafe(&challenge),
1837 "origin": "http://localhost:8080",
1838 });
1839 let result = verify_registration(
1840 &challenge,
1841 &base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
1842 &base64_encode_urlsafe(&att_obj),
1843 "",
1844 "localhost",
1845 &["http://localhost:8080".to_string()],
1846 &AttestationPolicy::default(),
1847 &CredentialPolicy::default(),
1848 );
1849 assert!(matches!(
1850 result,
1851 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_ES384))
1852 ));
1853 }
1854
1855 fn registration_inputs(flags: u8) -> (Vec<u8>, String, String) {
1860 let cose_key = build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]);
1861 let auth_data =
1862 build_auth_data_with_credential("localhost", flags, 0, &[0x01, 0x02], &cose_key);
1863 let challenge = generate_challenge_bytes();
1864 let client_data = serde_json::json!({
1865 "type": "webauthn.create",
1866 "challenge": base64_encode_urlsafe(&challenge),
1867 "origin": "http://localhost:8080",
1868 });
1869 (
1870 challenge,
1871 base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
1872 base64_encode_urlsafe(&build_attestation_object(&auth_data)),
1873 )
1874 }
1875
1876 #[test]
1877 fn uv_required_rejects_clear_flag_and_accepts_set() {
1878 use crate::policy::UserVerificationPolicy;
1879 let uv_required = CredentialPolicy {
1880 user_verification: UserVerificationPolicy::Required,
1881 ..CredentialPolicy::default()
1882 };
1883
1884 let (challenge, cd, ao) = registration_inputs(FLAG_UP | FLAG_AT);
1886 let result = verify_registration(
1887 &challenge,
1888 &cd,
1889 &ao,
1890 "",
1891 "localhost",
1892 &["http://localhost:8080".to_string()],
1893 &AttestationPolicy::default(),
1894 &uv_required,
1895 );
1896 assert!(matches!(
1897 result,
1898 Err(WebauthnError::UserVerificationRequired)
1899 ));
1900
1901 let (challenge, cd, ao) = registration_inputs(FLAG_UP | FLAG_AT | FLAG_UV);
1903 let result = verify_registration(
1904 &challenge,
1905 &cd,
1906 &ao,
1907 "",
1908 "localhost",
1909 &["http://localhost:8080".to_string()],
1910 &AttestationPolicy::default(),
1911 &uv_required,
1912 );
1913 assert!(result.is_ok());
1914 }
1915
1916 #[test]
1918 fn uv_preferred_and_discouraged_report_only() {
1919 use crate::policy::UserVerificationPolicy;
1920 let (challenge, cd, ao) = registration_inputs(FLAG_UP | FLAG_AT);
1922
1923 for uv in [
1924 UserVerificationPolicy::Preferred,
1925 UserVerificationPolicy::Discouraged,
1926 ] {
1927 let policy = CredentialPolicy {
1928 user_verification: uv,
1929 ..CredentialPolicy::default()
1930 };
1931 let result = verify_registration(
1932 &challenge,
1933 &cd,
1934 &ao,
1935 "",
1936 "localhost",
1937 &["http://localhost:8080".to_string()],
1938 &AttestationPolicy::default(),
1939 &policy,
1940 )
1941 .unwrap();
1942 assert!(!result.user_verified, "{uv:?} must report, not reject");
1943 }
1944 }
1945
1946 #[test]
1951 fn registration_rejects_algorithm_outside_allowlist() {
1952 use ring::signature::{EcdsaKeyPair, KeyPair as _, ECDSA_P384_SHA384_FIXED_SIGNING};
1954 let rng = ring::rand::SystemRandom::new();
1955 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, &rng).unwrap();
1956 let key_pair =
1957 EcdsaKeyPair::from_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, pkcs8.as_ref(), &rng)
1958 .unwrap();
1959 let pub_bytes = key_pair.public_key().as_ref();
1960 let cose_key = build_cose_ec2_key_alg(2, -35, &pub_bytes[1..49], &pub_bytes[49..97]);
1961
1962 let auth_data =
1963 build_auth_data_with_credential("localhost", FLAG_UP | FLAG_AT, 0, &[0x01], &cose_key);
1964 let att_obj = build_attestation_object(&auth_data);
1965 let challenge = generate_challenge_bytes();
1966 let client_data = serde_json::json!({
1967 "type": "webauthn.create",
1968 "challenge": base64_encode_urlsafe(&challenge),
1969 "origin": "http://localhost:8080",
1970 });
1971 let common = (
1972 challenge,
1973 base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
1974 base64_encode_urlsafe(&att_obj),
1975 );
1976
1977 let es256_only = CredentialPolicy {
1979 allowed_algorithms: vec![COSE_ALG_ES256],
1980 ..CredentialPolicy::default()
1981 };
1982 let result = verify_registration(
1983 &common.0,
1984 &common.1,
1985 &common.2,
1986 "",
1987 "localhost",
1988 &["http://localhost:8080".to_string()],
1989 &AttestationPolicy::default(),
1990 &es256_only,
1991 );
1992 assert!(matches!(
1993 result,
1994 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_ES384))
1995 ));
1996
1997 let with_es384 = CredentialPolicy {
1999 allowed_algorithms: vec![COSE_ALG_ES256, COSE_ALG_ES384],
2000 ..CredentialPolicy::default()
2001 };
2002 let result = verify_registration(
2003 &common.0,
2004 &common.1,
2005 &common.2,
2006 "",
2007 "localhost",
2008 &["http://localhost:8080".to_string()],
2009 &AttestationPolicy::default(),
2010 &with_es384,
2011 );
2012 assert!(result.is_ok());
2013
2014 let result = verify_registration(
2016 &common.0,
2017 &common.1,
2018 &common.2,
2019 "",
2020 "localhost",
2021 &["http://localhost:8080".to_string()],
2022 &AttestationPolicy::default(),
2023 &CredentialPolicy::default(),
2024 );
2025 assert!(result.is_ok());
2026 }
2027
2028 #[test]
2033 fn backup_policy_device_bound_enforced_at_registration() {
2034 use crate::policy::BackupPolicy;
2035 let device_bound = CredentialPolicy {
2036 backup: BackupPolicy::RequireDeviceBound,
2037 ..CredentialPolicy::default()
2038 };
2039
2040 let (challenge, cd, ao) = registration_inputs(FLAG_UP | FLAG_AT | FLAG_BE);
2042 let result = verify_registration(
2043 &challenge,
2044 &cd,
2045 &ao,
2046 "",
2047 "localhost",
2048 &["http://localhost:8080".to_string()],
2049 &AttestationPolicy::default(),
2050 &device_bound,
2051 );
2052 assert!(matches!(result, Err(WebauthnError::PolicyViolation(_))));
2053
2054 let (challenge, cd, ao) = registration_inputs(FLAG_UP | FLAG_AT);
2056 let result = verify_registration(
2057 &challenge,
2058 &cd,
2059 &ao,
2060 "",
2061 "localhost",
2062 &["http://localhost:8080".to_string()],
2063 &AttestationPolicy::default(),
2064 &device_bound,
2065 );
2066 let registration = result.unwrap();
2067 assert!(!registration.backup_eligible);
2068 assert!(!registration.backup_state);
2069 }
2070
2071 #[test]
2073 fn backup_policy_allow_reports_flags() {
2074 let (challenge, cd, ao) = registration_inputs(FLAG_UP | FLAG_AT | FLAG_BE | FLAG_BS);
2075 let registration = verify_registration(
2076 &challenge,
2077 &cd,
2078 &ao,
2079 "",
2080 "localhost",
2081 &["http://localhost:8080".to_string()],
2082 &AttestationPolicy::default(),
2083 &CredentialPolicy::default(),
2084 )
2085 .unwrap();
2086 assert!(registration.backup_eligible);
2087 assert!(registration.backup_state);
2088 }
2089
2090 #[test]
2092 fn backup_policy_device_bound_enforced_at_authentication() {
2093 use crate::policy::BackupPolicy;
2094 use ring::signature::{EcdsaKeyPair, KeyPair as _};
2095
2096 let rng = ring::rand::SystemRandom::new();
2097 let pkcs8 =
2098 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
2099 .unwrap();
2100 let key_pair = EcdsaKeyPair::from_pkcs8(
2101 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
2102 pkcs8.as_ref(),
2103 &rng,
2104 )
2105 .unwrap();
2106 let pub_bytes = key_pair.public_key().as_ref();
2107 let cose_key = build_cose_ec2_key(&pub_bytes[1..33], &pub_bytes[33..65]);
2108
2109 let challenge = generate_challenge_bytes();
2110 let client_data = serde_json::json!({
2111 "type": "webauthn.get",
2112 "challenge": base64_encode_urlsafe(&challenge),
2113 "origin": "http://localhost:8080",
2114 });
2115 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
2116 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
2117 let mut auth_data = rp_id_hash;
2118 auth_data.push(FLAG_UP | FLAG_BE); auth_data.extend_from_slice(&1u32.to_be_bytes());
2120
2121 use sha2::Digest as _;
2122 let mut signed_data = auth_data.clone();
2123 signed_data.extend_from_slice(&sha2::Sha256::digest(&client_data_bytes));
2124 let signature = key_pair.sign(&rng, &signed_data).unwrap();
2125
2126 let cred_b64 = base64_encode_urlsafe(&[0x01]);
2127 let params = AuthenticationParams {
2128 challenge_bytes: challenge,
2129 client_data_json_b64: base64_encode_urlsafe(&client_data_bytes),
2130 authenticator_data_b64: base64_encode_urlsafe(&auth_data),
2131 signature_b64: base64_encode_urlsafe(signature.as_ref()),
2132 credential_id_b64: cred_b64.clone(),
2133 public_key_cose: cose_key,
2134 current_sign_count: 0,
2135 allowed_credential_ids: vec![cred_b64],
2136 rp_id: "localhost".to_string(),
2137 rp_origins: vec!["http://localhost:8080".to_string()],
2138 policy: CredentialPolicy {
2139 backup: BackupPolicy::RequireDeviceBound,
2140 ..CredentialPolicy::default()
2141 },
2142 };
2143 assert!(matches!(
2144 verify_authentication(¶ms),
2145 Err(WebauthnError::PolicyViolation(_))
2146 ));
2147 }
2148
2149 #[test]
2153 fn uv_required_enforced_at_authentication() {
2154 use crate::policy::UserVerificationPolicy;
2155 use ring::signature::{EcdsaKeyPair, KeyPair as _};
2156
2157 let rng = ring::rand::SystemRandom::new();
2158 let pkcs8 =
2159 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
2160 .unwrap();
2161 let key_pair = EcdsaKeyPair::from_pkcs8(
2162 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
2163 pkcs8.as_ref(),
2164 &rng,
2165 )
2166 .unwrap();
2167 let pub_bytes = key_pair.public_key().as_ref();
2168 let cose_key = build_cose_ec2_key(&pub_bytes[1..33], &pub_bytes[33..65]);
2169
2170 let challenge = generate_challenge_bytes();
2171 let client_data = serde_json::json!({
2172 "type": "webauthn.get",
2173 "challenge": base64_encode_urlsafe(&challenge),
2174 "origin": "http://localhost:8080",
2175 });
2176 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
2177 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
2178 let mut auth_data = rp_id_hash;
2179 auth_data.push(FLAG_UP); auth_data.extend_from_slice(&1u32.to_be_bytes());
2181
2182 use sha2::Digest as _;
2183 let mut signed_data = auth_data.clone();
2184 signed_data.extend_from_slice(&sha2::Sha256::digest(&client_data_bytes));
2185 let signature = key_pair.sign(&rng, &signed_data).unwrap();
2186
2187 let cred_b64 = base64_encode_urlsafe(&[0x01]);
2188 let params = AuthenticationParams {
2189 challenge_bytes: challenge,
2190 client_data_json_b64: base64_encode_urlsafe(&client_data_bytes),
2191 authenticator_data_b64: base64_encode_urlsafe(&auth_data),
2192 signature_b64: base64_encode_urlsafe(signature.as_ref()),
2193 credential_id_b64: cred_b64.clone(),
2194 public_key_cose: cose_key,
2195 current_sign_count: 0,
2196 allowed_credential_ids: vec![cred_b64],
2197 rp_id: "localhost".to_string(),
2198 rp_origins: vec!["http://localhost:8080".to_string()],
2199 policy: CredentialPolicy {
2200 user_verification: UserVerificationPolicy::Required,
2201 ..CredentialPolicy::default()
2202 },
2203 };
2204 assert!(matches!(
2205 verify_authentication(¶ms),
2206 Err(WebauthnError::UserVerificationRequired)
2207 ));
2208
2209 let mut default_params = params.clone();
2211 default_params.policy = CredentialPolicy::default();
2212 assert!(verify_authentication(&default_params).is_ok());
2213 }
2214
2215 #[test]
2218 fn registration_propagates_attestation_statement_errors() {
2219 let auth_data = build_auth_data_with_credential(
2220 "localhost",
2221 FLAG_UP | FLAG_AT,
2222 0,
2223 &[0x01],
2224 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
2225 );
2226 let entries = vec![
2227 (
2228 ciborium::Value::Integer(1.into()),
2229 ciborium::Value::Text("packed".to_string()),
2230 ),
2231 (
2232 ciborium::Value::Integer(2.into()),
2233 ciborium::Value::Bytes(auth_data),
2234 ),
2235 (
2236 ciborium::Value::Integer(3.into()),
2237 ciborium::Value::Map(vec![]), ),
2239 ];
2240 let mut buf = Vec::new();
2241 ciborium::ser::into_writer(&ciborium::Value::Map(entries), &mut buf).unwrap();
2242
2243 let challenge = generate_challenge_bytes();
2244 let client_data = serde_json::json!({
2245 "type": "webauthn.create",
2246 "challenge": base64_encode_urlsafe(&challenge),
2247 "origin": "http://localhost:8080",
2248 });
2249 let result = verify_registration(
2250 &challenge,
2251 &base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
2252 &base64_encode_urlsafe(&buf),
2253 "",
2254 "localhost",
2255 &["http://localhost:8080".to_string()],
2256 &AttestationPolicy::default(),
2257 &CredentialPolicy::default(),
2258 );
2259 assert!(matches!(result, Err(WebauthnError::AttestationError(_))));
2260 }
2261
2262 #[test]
2267 fn authenticator_data_truncation_boundaries_are_exact() {
2268 let at_minimum = build_auth_data_with_credential("localhost", FLAG_AT, 0, &[], &[]);
2271 assert_eq!(at_minimum.len(), 55);
2272 let err = parse_authenticator_data(&at_minimum)
2273 .unwrap_err()
2274 .to_string();
2275 assert!(
2276 err.contains("public key"),
2277 "exact-prefix blob must reach the public-key stage: {err}"
2278 );
2279
2280 let mut short = vec![0u8; 32];
2283 short.push(FLAG_AT);
2284 short.extend_from_slice(&[0u8; 7]);
2285 let err = parse_authenticator_data(&short).unwrap_err().to_string();
2286 assert!(
2287 err.contains("AAGUID + length"),
2288 "40-byte AT blob must fail at the prefix stage: {err}"
2289 );
2290
2291 let cred_exact =
2294 build_auth_data_with_credential("localhost", FLAG_AT, 0, &[0x01, 0x02], &[]);
2295 assert_eq!(cred_exact.len(), 57);
2296 let err = parse_authenticator_data(&cred_exact)
2297 .unwrap_err()
2298 .to_string();
2299 assert!(
2300 err.contains("public key"),
2301 "exact-fit credential id must reach the public-key stage: {err}"
2302 );
2303 }
2304
2305 #[test]
2309 fn registration_without_attested_credential_flag_names_the_missing_flag() {
2310 let auth_data = build_auth_data_with_credential(
2311 "localhost",
2312 FLAG_UP, 0,
2314 &[],
2315 &[],
2316 );
2317 let att_obj = build_attestation_object(&auth_data);
2318
2319 let challenge = generate_challenge_bytes();
2320 let client_data = serde_json::json!({
2321 "type": "webauthn.create",
2322 "challenge": base64_encode_urlsafe(&challenge),
2323 "origin": "http://localhost:8080",
2324 });
2325 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
2326
2327 let result = verify_registration(
2328 &challenge,
2329 &client_data_b64,
2330 &base64_encode_urlsafe(&att_obj),
2331 "",
2332 "localhost",
2333 &["http://localhost:8080".to_string()],
2334 &AttestationPolicy::default(),
2335 &CredentialPolicy::default(),
2336 );
2337 let err = result.unwrap_err().to_string();
2338 assert!(
2339 err.contains("Attested Credential Data flag not set"),
2340 "AT-less registration must be rejected at the flag gate: {err}"
2341 );
2342 }
2343}