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, cbor_map_entries,
33 parse_cose_key, 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 = cbor_map_entries(&attestation_val).ok_or_else(|| {
341 WebauthnError::AttestationError("attestation object is not a CBOR map".to_string())
342 })?;
343
344 let mut fmt: Option<String> = None;
345 let mut auth_data_bytes: Option<Vec<u8>> = None;
346 let mut att_stmt: Option<ciborium::Value> = None;
347
348 for (key, val) in &attestation_entries {
349 match *key {
350 1 => {
351 if let ciborium::Value::Text(s) = val {
352 fmt = Some(s.clone());
353 }
354 }
355 2 => {
356 if let Some(b) = cbor_bytes(val) {
357 auth_data_bytes = Some(b);
358 }
359 }
360 3 => {
361 att_stmt = Some(val.clone());
362 }
363 _ => {}
364 }
365 }
366
367 let fmt = fmt.unwrap_or_else(|| "none".to_string());
368 let auth_data_bytes = auth_data_bytes.ok_or_else(|| {
369 WebauthnError::AttestationError("missing authData in attestation object".to_string())
370 })?;
371
372 let auth_data = parse_authenticator_data(&auth_data_bytes)?;
373
374 use sha2::Digest;
375 let computed_rp_id_hash = sha2::Sha256::digest(rp_id.as_bytes()).to_vec();
376 if auth_data.rp_id_hash != computed_rp_id_hash {
377 return Err(WebauthnError::VerificationFailed(format!(
378 "rpId hash mismatch: computed {:x?}, got {:x?}",
379 computed_rp_id_hash, auth_data.rp_id_hash
380 )));
381 }
382
383 if auth_data.flags & FLAG_UP == 0 {
384 return Err(WebauthnError::VerificationFailed(
385 "User Present flag not set".to_string(),
386 ));
387 }
388
389 if auth_data.flags & FLAG_AT == 0 {
390 return Err(WebauthnError::VerificationFailed(
391 "Attested Credential Data flag not set during registration".to_string(),
392 ));
393 }
394
395 enforce_flag_policy(auth_data.flags, credential_policy)?;
397
398 let credential_id = auth_data.credential_id.ok_or_else(|| {
399 WebauthnError::VerificationFailed("no credential ID in attested data".to_string())
400 })?;
401 let credential_id_b64 = base64_encode_urlsafe(&credential_id);
402
403 if existing_credential_id == credential_id_b64 {
404 return Err(WebauthnError::DuplicateCredential(credential_id_b64));
405 }
406
407 let public_key_cose = auth_data.credential_public_key_cose.ok_or_else(|| {
408 WebauthnError::VerificationFailed("no public key in attested data".to_string())
409 })?;
410
411 let (alg, cose_key) = parse_cose_key(&public_key_cose)?;
412
413 enforce_algorithm_allowlist(alg, credential_policy)?;
415
416 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
420 let aaguid = auth_data.aaguid.unwrap_or([0u8; 16]);
421 let attestation = verify_attestation(
422 &fmt,
423 att_stmt.as_ref(),
424 &auth_data_bytes,
425 &client_data_hash,
426 &credential_id,
427 alg,
428 &cose_key,
429 aaguid,
430 attestation_policy,
431 )?;
432
433 let user_verified = auth_data.flags & FLAG_UV != 0;
434
435 Ok(RegistrationResult {
436 credential_id: credential_id_b64,
437 device_name: format!("WebAuthn ({})", alg_to_name(alg)),
438 attestation_format: fmt,
439 attestation,
440 user_verified,
441 backup_eligible: auth_data.backup_eligible,
442 backup_state: auth_data.backup_state,
443 })
444}
445
446#[derive(Debug, Clone)]
451pub struct AuthenticationParams {
452 pub challenge_bytes: Vec<u8>,
454 pub client_data_json_b64: String,
456 pub authenticator_data_b64: String,
458 pub signature_b64: String,
460 pub credential_id_b64: String,
462 pub public_key_cose: Vec<u8>,
464 pub current_sign_count: u32,
466 pub allowed_credential_ids: Vec<String>,
468 pub rp_id: String,
470 pub rp_origins: Vec<String>,
472 pub policy: CredentialPolicy,
481}
482
483pub fn verify_authentication(
513 params: &AuthenticationParams,
514) -> Result<AuthenticationResult, WebauthnError> {
515 let AuthenticationParams {
516 challenge_bytes,
517 client_data_json_b64,
518 authenticator_data_b64,
519 signature_b64,
520 credential_id_b64,
521 public_key_cose,
522 current_sign_count,
523 allowed_credential_ids,
524 rp_id,
525 rp_origins,
526 policy,
527 } = params;
528
529 if !allowed_credential_ids.contains(credential_id_b64) {
530 return Err(WebauthnError::VerificationFailed(
531 "credential ID not in allowed list".to_string(),
532 ));
533 }
534
535 let client_data_bytes = validate_client_data(
536 client_data_json_b64,
537 challenge_bytes,
538 "webauthn.get",
539 rp_id,
540 rp_origins,
541 )?;
542
543 let authenticator_data = base64_decode_urlsafe(authenticator_data_b64)?;
544 let auth_data = parse_authenticator_data(&authenticator_data)?;
545
546 use sha2::Digest;
547 let computed_rp_id_hash = sha2::Sha256::digest(rp_id.as_bytes()).to_vec();
548 if auth_data.rp_id_hash != computed_rp_id_hash {
549 return Err(WebauthnError::VerificationFailed(format!(
550 "rpId hash mismatch: computed {:x?}, got {:x?}",
551 computed_rp_id_hash, auth_data.rp_id_hash
552 )));
553 }
554
555 if auth_data.flags & FLAG_UP == 0 {
556 return Err(WebauthnError::VerificationFailed(
557 "User Present flag not set".to_string(),
558 ));
559 }
560
561 enforce_flag_policy(auth_data.flags, policy)?;
563
564 check_sign_count(*current_sign_count, auth_data.sign_count)?;
565
566 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
567
568 let mut signed_data = Vec::with_capacity(authenticator_data.len() + 32);
569 signed_data.extend_from_slice(&authenticator_data);
570 signed_data.extend_from_slice(&client_data_hash);
571
572 let signature = base64_decode_urlsafe(signature_b64)?;
573
574 let (alg, cose_key) = parse_cose_key(public_key_cose)?;
575 verify_cose_signature(alg, &cose_key, &signed_data, &signature)?;
576
577 let user_verified = auth_data.flags & FLAG_UV != 0;
578 let new_sign_count = auth_data.sign_count;
579
580 Ok(AuthenticationResult {
581 credential_id: credential_id_b64.clone(),
582 new_sign_count,
583 user_verified,
584 backup_state: auth_data.backup_state,
585 backup_eligible: auth_data.backup_eligible,
586 })
587}
588
589#[allow(
593 clippy::unwrap_used,
594 clippy::expect_used,
595 clippy::indexing_slicing,
596 clippy::panic
597)]
598#[cfg(test)]
599mod tests {
600 use super::*;
601 use crate::crypto::tests::{build_cose_ec2_key, build_cose_rsa_key};
602 use crate::crypto::{base64_encode_urlsafe, generate_challenge_bytes, COSE_ALG_RS256};
603 use ring::signature::KeyPair as _;
604
605 fn build_attestation_object(auth_data: &[u8]) -> Vec<u8> {
606 use ciborium::Value;
607 let map = vec![
608 (Value::Integer(1.into()), Value::Text("none".to_string())),
609 (Value::Integer(2.into()), Value::Bytes(auth_data.to_vec())),
610 (Value::Integer(3.into()), Value::Map(vec![])),
611 ];
612 let mut buf = Vec::new();
613 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
614 buf
615 }
616
617 fn build_auth_data_with_credential(
618 rp_id: &str,
619 flags: u8,
620 sign_count: u32,
621 credential_id: &[u8],
622 cose_key: &[u8],
623 ) -> Vec<u8> {
624 use sha2::Digest;
625 let rp_id_hash = sha2::Sha256::digest(rp_id.as_bytes()).to_vec();
626 let mut auth_data = Vec::new();
627 auth_data.extend_from_slice(&rp_id_hash);
628 auth_data.push(flags);
629 auth_data.extend_from_slice(&sign_count.to_be_bytes());
630
631 if flags & FLAG_AT != 0 {
632 auth_data.extend_from_slice(&[0u8; 16]);
633 auth_data.extend_from_slice(&(credential_id.len() as u16).to_be_bytes());
634 auth_data.extend_from_slice(credential_id);
635 auth_data.extend_from_slice(cose_key);
636 }
637
638 auth_data
639 }
640
641 #[test]
642 fn test_parse_authenticator_data_minimal() {
643 let mut auth_data = vec![0u8; 37];
644 auth_data[..32].copy_from_slice(&[0xAA; 32]);
645 auth_data[32] = FLAG_UP;
646 auth_data[33..37].copy_from_slice(&1u32.to_be_bytes());
647
648 let parsed = parse_authenticator_data(&auth_data).unwrap();
649 assert_eq!(parsed.rp_id_hash, vec![0xAA; 32]);
650 assert_eq!(parsed.flags, FLAG_UP);
651 assert_eq!(parsed.sign_count, 1);
652 assert!(parsed.credential_public_key_cose.is_none());
653 assert!(parsed.credential_id.is_none());
654 }
655
656 #[test]
657 fn test_parse_authenticator_data_with_attested_credential() {
658 let credential_id = vec![0x01, 0x02, 0x03, 0x04];
659 let public_key_cose = vec![0x10, 0x20, 0x30];
660
661 let total_len = 37 + 16 + 2 + credential_id.len() + public_key_cose.len();
662 let mut auth_data = vec![0u8; total_len];
663 auth_data[32] = FLAG_UP | FLAG_AT;
664 auth_data[33..37].copy_from_slice(&5u32.to_be_bytes());
665
666 let offset = 37 + 16;
667 auth_data[offset..offset + 2].copy_from_slice(&(credential_id.len() as u16).to_be_bytes());
668 auth_data[offset + 2..offset + 2 + credential_id.len()].copy_from_slice(&credential_id);
669 let pk_offset = offset + 2 + credential_id.len();
670 auth_data[pk_offset..].copy_from_slice(&public_key_cose);
671
672 let parsed = parse_authenticator_data(&auth_data).unwrap();
673 assert_eq!(parsed.flags, FLAG_UP | FLAG_AT);
674 assert_eq!(parsed.sign_count, 5);
675 assert_eq!(parsed.credential_id, Some(credential_id));
676 assert_eq!(parsed.credential_public_key_cose, Some(public_key_cose));
677 }
678
679 #[test]
680 fn test_parse_authenticator_data_too_short() {
681 let auth_data = vec![0u8; 10];
682 let result = parse_authenticator_data(&auth_data);
683 assert!(result.is_err());
684 }
685
686 #[test]
687 fn test_verify_registration_valid_es256() {
688 use ring::signature::EcdsaKeyPair;
689
690 let rng = ring::rand::SystemRandom::new();
691 let pkcs8 =
692 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
693 .unwrap();
694 let key_pair = EcdsaKeyPair::from_pkcs8(
695 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
696 pkcs8.as_ref(),
697 &rng,
698 )
699 .unwrap();
700
701 let pub_bytes = key_pair.public_key().as_ref().to_vec();
702 let x = pub_bytes[1..33].to_vec();
703 let y = pub_bytes[33..65].to_vec();
704 let cose_key = build_cose_ec2_key(&x, &y);
705
706 let credential_id = vec![0x01, 0x02, 0x03, 0x04];
707 let auth_data = build_auth_data_with_credential(
708 "localhost",
709 FLAG_UP | FLAG_AT,
710 0,
711 &credential_id,
712 &cose_key,
713 );
714 let att_obj = build_attestation_object(&auth_data);
715
716 let challenge = generate_challenge_bytes();
717 let challenge_b64 = base64_encode_urlsafe(&challenge);
718
719 let client_data = serde_json::json!({
720 "type": "webauthn.create",
721 "challenge": challenge_b64,
722 "origin": "http://localhost:8080",
723 });
724 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
725 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
726
727 let result = verify_registration(
728 &challenge,
729 &client_data_b64,
730 &att_obj_b64,
731 "different-id",
732 "localhost",
733 &["http://localhost:8080".to_string()],
734 &AttestationPolicy::default(),
735 &CredentialPolicy::default(),
736 )
737 .unwrap();
738
739 assert_eq!(result.credential_id, base64_encode_urlsafe(&credential_id));
740 assert!(result.attestation_format == "none");
741 }
742
743 #[test]
744 fn test_verify_registration_challenge_mismatch() {
745 let auth_data = build_auth_data_with_credential(
746 "localhost",
747 FLAG_UP | FLAG_AT,
748 0,
749 &[0x01],
750 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
751 );
752 let att_obj = build_attestation_object(&auth_data);
753
754 let challenge = generate_challenge_bytes();
755 let _challenge_b64 = base64_encode_urlsafe(&challenge);
756
757 let client_data = serde_json::json!({
758 "type": "webauthn.create",
759 "challenge": base64_encode_urlsafe(&[1u8; 32]),
760 "origin": "http://localhost:8080",
761 });
762 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
763 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
764
765 let result = verify_registration(
766 &challenge,
767 &client_data_b64,
768 &att_obj_b64,
769 "different",
770 "localhost",
771 &["http://localhost:8080".to_string()],
772 &AttestationPolicy::default(),
773 &CredentialPolicy::default(),
774 );
775 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
776 }
777
778 #[test]
779 fn test_verify_registration_wrong_type() {
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.get",
794 "challenge": challenge_b64,
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_origin() {
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.create",
829 "challenge": challenge_b64,
830 "origin": "http://evil.com",
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_duplicate() {
850 let auth_data = build_auth_data_with_credential(
851 "localhost",
852 FLAG_UP | FLAG_AT,
853 0,
854 &[0x01, 0x02, 0x03],
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://localhost:8080",
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 let cred_id_b64 = base64_encode_urlsafe(&[0x01, 0x02, 0x03]);
870
871 let result = verify_registration(
872 &challenge,
873 &client_data_b64,
874 &att_obj_b64,
875 &cred_id_b64,
876 "localhost",
877 &["http://localhost:8080".to_string()],
878 &AttestationPolicy::default(),
879 &CredentialPolicy::default(),
880 );
881 assert!(matches!(result, Err(WebauthnError::DuplicateCredential(_))));
882 }
883
884 #[test]
885 fn test_verify_registration_wrong_rp_id_hash() {
886 let credential_id = vec![0x01];
887 let cose_key = build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]);
888 let auth_data = build_auth_data_with_credential(
889 "evil.com",
890 FLAG_UP | FLAG_AT,
891 0,
892 &credential_id,
893 &cose_key,
894 );
895 let att_obj = build_attestation_object(&auth_data);
896
897 let challenge = generate_challenge_bytes();
898 let challenge_b64 = base64_encode_urlsafe(&challenge);
899
900 let client_data = serde_json::json!({
901 "type": "webauthn.create",
902 "challenge": challenge_b64,
903 "origin": "http://localhost:8080",
904 });
905 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
906 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
907
908 let result = verify_registration(
909 &challenge,
910 &client_data_b64,
911 &att_obj_b64,
912 "different",
913 "localhost",
914 &["http://localhost:8080".to_string()],
915 &AttestationPolicy::default(),
916 &CredentialPolicy::default(),
917 );
918 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
919 }
920
921 #[test]
922 fn test_verify_registration_missing_up_flag() {
923 let credential_id = vec![0x01];
924 let cose_key = build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]);
925 let auth_data =
926 build_auth_data_with_credential("localhost", FLAG_AT, 0, &credential_id, &cose_key);
927 let att_obj = build_attestation_object(&auth_data);
928
929 let challenge = generate_challenge_bytes();
930 let challenge_b64 = base64_encode_urlsafe(&challenge);
931
932 let client_data = serde_json::json!({
933 "type": "webauthn.create",
934 "challenge": challenge_b64,
935 "origin": "http://localhost:8080",
936 });
937 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
938 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
939
940 let result = verify_registration(
941 &challenge,
942 &client_data_b64,
943 &att_obj_b64,
944 "different",
945 "localhost",
946 &["http://localhost:8080".to_string()],
947 &AttestationPolicy::default(),
948 &CredentialPolicy::default(),
949 );
950 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
951 }
952
953 #[test]
957 fn registration_rejects_missing_at_flag() {
958 let auth_data = build_auth_data_with_credential(
959 "localhost",
960 FLAG_UP, 0,
962 &[0x01],
963 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
964 );
965 let att_obj = build_attestation_object(&auth_data);
966
967 let challenge = generate_challenge_bytes();
968 let challenge_b64 = base64_encode_urlsafe(&challenge);
969
970 let client_data = serde_json::json!({
971 "type": "webauthn.create",
972 "challenge": challenge_b64,
973 "origin": "http://localhost:8080",
974 });
975 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
976 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
977
978 let result = verify_registration(
979 &challenge,
980 &client_data_b64,
981 &att_obj_b64,
982 "different",
983 "localhost",
984 &["http://localhost:8080".to_string()],
985 &AttestationPolicy::default(),
986 &CredentialPolicy::default(),
987 );
988 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
989 }
990
991 #[test]
992 fn test_verify_authentication_valid_es256() {
993 use ring::signature::EcdsaKeyPair;
994
995 let rng = ring::rand::SystemRandom::new();
996 let pkcs8 =
997 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
998 .unwrap();
999 let key_pair = EcdsaKeyPair::from_pkcs8(
1000 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1001 pkcs8.as_ref(),
1002 &rng,
1003 )
1004 .unwrap();
1005
1006 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1007 let x = pub_bytes[1..33].to_vec();
1008 let y = pub_bytes[33..65].to_vec();
1009 let cose_key = build_cose_ec2_key(&x, &y);
1010
1011 let challenge = generate_challenge_bytes();
1012 let challenge_b64 = base64_encode_urlsafe(&challenge);
1013
1014 use sha2::Digest;
1015 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1016 let mut auth_data = Vec::new();
1017 auth_data.extend_from_slice(&rp_id_hash);
1018 auth_data.push(FLAG_UP);
1019 auth_data.extend_from_slice(&1u32.to_be_bytes());
1020
1021 let client_data = serde_json::json!({
1022 "type": "webauthn.get",
1023 "challenge": challenge_b64,
1024 "origin": "http://localhost:8080",
1025 });
1026 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1027 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1028 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1029
1030 let mut signed_data = Vec::new();
1031 signed_data.extend_from_slice(&auth_data);
1032 signed_data.extend_from_slice(&client_data_hash);
1033
1034 let signature = key_pair.sign(&rng, &signed_data).unwrap();
1035
1036 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1037 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
1038 let cred_id_b64 = base64_encode_urlsafe(&[0x01, 0x02]);
1039
1040 let result = verify_authentication(&AuthenticationParams {
1041 challenge_bytes: challenge.clone(),
1042 client_data_json_b64: client_data_b64,
1043 authenticator_data_b64: auth_data_b64,
1044 signature_b64: sig_b64,
1045 credential_id_b64: cred_id_b64.clone(),
1046 public_key_cose: cose_key,
1047 current_sign_count: 0,
1048 allowed_credential_ids: vec![cred_id_b64.clone()],
1049 rp_id: "localhost".to_string(),
1050 rp_origins: vec!["http://localhost:8080".to_string()],
1051 policy: CredentialPolicy::default(),
1052 })
1053 .unwrap();
1054
1055 assert_eq!(result.credential_id, cred_id_b64);
1056 assert_eq!(result.new_sign_count, 1);
1057 }
1058
1059 #[test]
1060 fn test_verify_authentication_wrong_signature() {
1061 use ring::signature::EcdsaKeyPair;
1062
1063 let rng = ring::rand::SystemRandom::new();
1064 let pkcs8 =
1065 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1066 .unwrap();
1067 let key_pair = EcdsaKeyPair::from_pkcs8(
1068 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1069 pkcs8.as_ref(),
1070 &rng,
1071 )
1072 .unwrap();
1073
1074 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1075 let x = pub_bytes[1..33].to_vec();
1076 let y = pub_bytes[33..65].to_vec();
1077 let cose_key = build_cose_ec2_key(&x, &y);
1078
1079 let challenge = generate_challenge_bytes();
1080 let challenge_b64 = base64_encode_urlsafe(&challenge);
1081
1082 use sha2::Digest;
1083 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1084 let mut auth_data = Vec::new();
1085 auth_data.extend_from_slice(&rp_id_hash);
1086 auth_data.push(FLAG_UP);
1087 auth_data.extend_from_slice(&1u32.to_be_bytes());
1088
1089 let client_data = serde_json::json!({
1090 "type": "webauthn.get",
1091 "challenge": challenge_b64,
1092 "origin": "http://localhost:8080",
1093 });
1094 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1095 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1096 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1097
1098 let mut signed_data = Vec::new();
1099 signed_data.extend_from_slice(&auth_data);
1100 signed_data.extend_from_slice(&client_data_hash);
1101
1102 let mut wrong_sig = vec![0u8; 64];
1103 wrong_sig[0] = 0xFF;
1104
1105 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1106 let sig_b64 = base64_encode_urlsafe(&wrong_sig);
1107 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
1108
1109 let result = verify_authentication(&AuthenticationParams {
1110 challenge_bytes: challenge.clone(),
1111 client_data_json_b64: client_data_b64,
1112 authenticator_data_b64: auth_data_b64,
1113 signature_b64: sig_b64,
1114 credential_id_b64: cred_id_b64.clone(),
1115 public_key_cose: cose_key,
1116 current_sign_count: 0,
1117 allowed_credential_ids: vec![cred_id_b64],
1118 rp_id: "localhost".to_string(),
1119 rp_origins: vec!["http://localhost:8080".to_string()],
1120 policy: CredentialPolicy::default(),
1121 });
1122 assert!(matches!(
1123 result,
1124 Err(WebauthnError::SignatureVerificationFailed)
1125 ));
1126 }
1127
1128 #[test]
1129 fn test_verify_authentication_credential_not_allowed() {
1130 let challenge = generate_challenge_bytes();
1131 let challenge_b64 = base64_encode_urlsafe(&challenge);
1132
1133 let client_data = serde_json::json!({
1134 "type": "webauthn.get",
1135 "challenge": challenge_b64,
1136 "origin": "http://localhost:8080",
1137 });
1138 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1139
1140 use sha2::Digest;
1141 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1142 let mut auth_data = Vec::new();
1143 auth_data.extend_from_slice(&rp_id_hash);
1144 auth_data.push(FLAG_UP);
1145 auth_data.extend_from_slice(&0u32.to_be_bytes());
1146 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1147
1148 let result = verify_authentication(&AuthenticationParams {
1149 challenge_bytes: challenge.clone(),
1150 client_data_json_b64: client_data_b64,
1151 authenticator_data_b64: auth_data_b64,
1152 signature_b64: "sig".to_string(),
1153 credential_id_b64: "unauthorized-cred".to_string(),
1154 public_key_cose: vec![0x10, 0x20],
1155 current_sign_count: 0,
1156 allowed_credential_ids: vec!["allowed-cred".to_string()],
1157 rp_id: "localhost".to_string(),
1158 rp_origins: vec!["http://localhost:8080".to_string()],
1159 policy: CredentialPolicy::default(),
1160 });
1161 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1162 }
1163
1164 #[test]
1165 fn test_verify_authentication_wrong_origin() {
1166 let challenge = generate_challenge_bytes();
1167 let challenge_b64 = base64_encode_urlsafe(&challenge);
1168
1169 let client_data = serde_json::json!({
1170 "type": "webauthn.get",
1171 "challenge": challenge_b64,
1172 "origin": "http://evil.com",
1173 });
1174 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1175
1176 use sha2::Digest;
1177 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1178 let mut auth_data = Vec::new();
1179 auth_data.extend_from_slice(&rp_id_hash);
1180 auth_data.push(FLAG_UP);
1181 auth_data.extend_from_slice(&0u32.to_be_bytes());
1182 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1183
1184 let result = verify_authentication(&AuthenticationParams {
1185 challenge_bytes: challenge.clone(),
1186 client_data_json_b64: client_data_b64,
1187 authenticator_data_b64: auth_data_b64,
1188 signature_b64: "sig".to_string(),
1189 credential_id_b64: "cred".to_string(),
1190 public_key_cose: vec![0x10],
1191 current_sign_count: 0,
1192 allowed_credential_ids: vec!["cred".to_string()],
1193 rp_id: "localhost".to_string(),
1194 rp_origins: vec!["http://localhost:8080".to_string()],
1195 policy: CredentialPolicy::default(),
1196 });
1197 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1198 }
1199
1200 #[test]
1201 fn test_verify_authentication_wrong_type() {
1202 let challenge = generate_challenge_bytes();
1203 let challenge_b64 = base64_encode_urlsafe(&challenge);
1204
1205 let client_data = serde_json::json!({
1206 "type": "webauthn.create",
1207 "challenge": challenge_b64,
1208 "origin": "http://localhost:8080",
1209 });
1210 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1211
1212 use sha2::Digest;
1213 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1214 let mut auth_data = Vec::new();
1215 auth_data.extend_from_slice(&rp_id_hash);
1216 auth_data.push(FLAG_UP);
1217 auth_data.extend_from_slice(&0u32.to_be_bytes());
1218 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1219
1220 let result = verify_authentication(&AuthenticationParams {
1221 challenge_bytes: challenge.clone(),
1222 client_data_json_b64: client_data_b64,
1223 authenticator_data_b64: auth_data_b64,
1224 signature_b64: "sig".to_string(),
1225 credential_id_b64: "cred".to_string(),
1226 public_key_cose: vec![0x10],
1227 current_sign_count: 0,
1228 allowed_credential_ids: vec!["cred".to_string()],
1229 rp_id: "localhost".to_string(),
1230 rp_origins: vec!["http://localhost:8080".to_string()],
1231 policy: CredentialPolicy::default(),
1232 });
1233 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1234 }
1235
1236 #[test]
1237 fn test_verify_authentication_sign_count_decrease() {
1238 use ring::signature::EcdsaKeyPair;
1239
1240 let rng = ring::rand::SystemRandom::new();
1241 let pkcs8 =
1242 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1243 .unwrap();
1244 let key_pair = EcdsaKeyPair::from_pkcs8(
1245 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1246 pkcs8.as_ref(),
1247 &rng,
1248 )
1249 .unwrap();
1250
1251 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1252 let x = pub_bytes[1..33].to_vec();
1253 let y = pub_bytes[33..65].to_vec();
1254 let cose_key = build_cose_ec2_key(&x, &y);
1255
1256 let challenge = generate_challenge_bytes();
1257 let challenge_b64 = base64_encode_urlsafe(&challenge);
1258
1259 use sha2::Digest;
1260 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1261 let mut auth_data = Vec::new();
1262 auth_data.extend_from_slice(&rp_id_hash);
1263 auth_data.push(FLAG_UP);
1264 auth_data.extend_from_slice(&5u32.to_be_bytes());
1265
1266 let client_data = serde_json::json!({
1267 "type": "webauthn.get",
1268 "challenge": challenge_b64,
1269 "origin": "http://localhost:8080",
1270 });
1271 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1272 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1273 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1274
1275 let mut signed_data = Vec::new();
1276 signed_data.extend_from_slice(&auth_data);
1277 signed_data.extend_from_slice(&client_data_hash);
1278
1279 let signature = key_pair.sign(&rng, &signed_data).unwrap();
1280
1281 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1282 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
1283 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
1284
1285 let result = verify_authentication(&AuthenticationParams {
1286 challenge_bytes: challenge.clone(),
1287 client_data_json_b64: client_data_b64,
1288 authenticator_data_b64: auth_data_b64,
1289 signature_b64: sig_b64,
1290 credential_id_b64: cred_id_b64.clone(),
1291 public_key_cose: cose_key,
1292 current_sign_count: 10,
1293 allowed_credential_ids: vec![cred_id_b64],
1294 rp_id: "localhost".to_string(),
1295 rp_origins: vec!["http://localhost:8080".to_string()],
1296 policy: CredentialPolicy::default(),
1297 });
1298 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1299 }
1300
1301 #[test]
1302 fn test_verify_authentication_wrong_rp_id_hash() {
1303 let challenge = generate_challenge_bytes();
1304 let challenge_b64 = base64_encode_urlsafe(&challenge);
1305
1306 let client_data = serde_json::json!({
1307 "type": "webauthn.get",
1308 "challenge": challenge_b64,
1309 "origin": "http://localhost:8080",
1310 });
1311 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1312
1313 use sha2::Digest;
1314 let rp_id_hash = sha2::Sha256::digest(b"evil.com").to_vec();
1315 let mut auth_data = Vec::new();
1316 auth_data.extend_from_slice(&rp_id_hash);
1317 auth_data.push(FLAG_UP);
1318 auth_data.extend_from_slice(&0u32.to_be_bytes());
1319 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1320
1321 let result = verify_authentication(&AuthenticationParams {
1322 challenge_bytes: challenge.clone(),
1323 client_data_json_b64: client_data_b64,
1324 authenticator_data_b64: auth_data_b64,
1325 signature_b64: "sig".to_string(),
1326 credential_id_b64: "cred".to_string(),
1327 public_key_cose: vec![0x10],
1328 current_sign_count: 0,
1329 allowed_credential_ids: vec!["cred".to_string()],
1330 rp_id: "localhost".to_string(),
1331 rp_origins: vec!["http://localhost:8080".to_string()],
1332 policy: CredentialPolicy::default(),
1333 });
1334 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1335 }
1336
1337 #[test]
1338 fn test_verify_authentication_missing_up_flag() {
1339 use ring::signature::EcdsaKeyPair;
1340
1341 let rng = ring::rand::SystemRandom::new();
1342 let pkcs8 =
1343 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1344 .unwrap();
1345 let key_pair = EcdsaKeyPair::from_pkcs8(
1346 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1347 pkcs8.as_ref(),
1348 &rng,
1349 )
1350 .unwrap();
1351
1352 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1353 let x = pub_bytes[1..33].to_vec();
1354 let y = pub_bytes[33..65].to_vec();
1355 let cose_key = build_cose_ec2_key(&x, &y);
1356
1357 let challenge = generate_challenge_bytes();
1358 let challenge_b64 = base64_encode_urlsafe(&challenge);
1359
1360 use sha2::Digest;
1361 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1362 let mut auth_data = Vec::new();
1363 auth_data.extend_from_slice(&rp_id_hash);
1364 auth_data.push(0);
1365 auth_data.extend_from_slice(&1u32.to_be_bytes());
1366
1367 let client_data = serde_json::json!({
1368 "type": "webauthn.get",
1369 "challenge": challenge_b64,
1370 "origin": "http://localhost:8080",
1371 });
1372 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1373 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1374 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1375
1376 let mut signed_data = Vec::new();
1377 signed_data.extend_from_slice(&auth_data);
1378 signed_data.extend_from_slice(&client_data_hash);
1379
1380 let signature = key_pair.sign(&rng, &signed_data).unwrap();
1381
1382 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1383 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
1384 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
1385
1386 let result = verify_authentication(&AuthenticationParams {
1387 challenge_bytes: challenge.clone(),
1388 client_data_json_b64: client_data_b64,
1389 authenticator_data_b64: auth_data_b64,
1390 signature_b64: sig_b64,
1391 credential_id_b64: cred_id_b64.clone(),
1392 public_key_cose: cose_key,
1393 current_sign_count: 0,
1394 allowed_credential_ids: vec![cred_id_b64],
1395 rp_id: "localhost".to_string(),
1396 rp_origins: vec!["http://localhost:8080".to_string()],
1397 policy: CredentialPolicy::default(),
1398 });
1399 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1400 }
1401
1402 #[test]
1403 fn test_verify_authentication_wrong_rp_id_in_client_data() {
1404 use ring::signature::EcdsaKeyPair;
1405
1406 let rng = ring::rand::SystemRandom::new();
1407 let pkcs8 =
1408 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1409 .unwrap();
1410 let key_pair = EcdsaKeyPair::from_pkcs8(
1411 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1412 pkcs8.as_ref(),
1413 &rng,
1414 )
1415 .unwrap();
1416
1417 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1418 let x = pub_bytes[1..33].to_vec();
1419 let y = pub_bytes[33..65].to_vec();
1420 let cose_key = build_cose_ec2_key(&x, &y);
1421
1422 let challenge = generate_challenge_bytes();
1423 let challenge_b64 = base64_encode_urlsafe(&challenge);
1424
1425 let client_data = serde_json::json!({
1426 "type": "webauthn.get",
1427 "challenge": challenge_b64,
1428 "origin": "http://localhost:8080",
1429 "rpId": "evil.com",
1430 });
1431 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1432 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1433
1434 use sha2::Digest;
1435 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1436 let mut auth_data = Vec::new();
1437 auth_data.extend_from_slice(&rp_id_hash);
1438 auth_data.push(FLAG_UP);
1439 auth_data.extend_from_slice(&1u32.to_be_bytes());
1440
1441 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1442 let mut signed_data = Vec::new();
1443 signed_data.extend_from_slice(&auth_data);
1444 signed_data.extend_from_slice(&client_data_hash);
1445
1446 let signature = key_pair.sign(&rng, &signed_data).unwrap();
1447
1448 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1449 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
1450 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
1451
1452 let result = verify_authentication(&AuthenticationParams {
1453 challenge_bytes: challenge.clone(),
1454 client_data_json_b64: client_data_b64,
1455 authenticator_data_b64: auth_data_b64,
1456 signature_b64: sig_b64,
1457 credential_id_b64: cred_id_b64.clone(),
1458 public_key_cose: cose_key,
1459 current_sign_count: 0,
1460 allowed_credential_ids: vec![cred_id_b64],
1461 rp_id: "localhost".to_string(),
1462 rp_origins: vec!["http://localhost:8080".to_string()],
1463 policy: CredentialPolicy::default(),
1464 });
1465 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1466 }
1467
1468 #[test]
1469 fn test_verify_authentication_challenge_mismatch() {
1470 let challenge = generate_challenge_bytes();
1471 let _challenge_b64 = base64_encode_urlsafe(&challenge);
1472
1473 let client_data = serde_json::json!({
1474 "type": "webauthn.get",
1475 "challenge": base64_encode_urlsafe(&[99u8; 32]),
1476 "origin": "http://localhost:8080",
1477 });
1478 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1479
1480 use sha2::Digest;
1481 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1482 let mut auth_data = Vec::new();
1483 auth_data.extend_from_slice(&rp_id_hash);
1484 auth_data.push(FLAG_UP);
1485 auth_data.extend_from_slice(&0u32.to_be_bytes());
1486 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1487
1488 let result = verify_authentication(&AuthenticationParams {
1489 challenge_bytes: challenge.clone(),
1490 client_data_json_b64: client_data_b64,
1491 authenticator_data_b64: auth_data_b64,
1492 signature_b64: "sig".to_string(),
1493 credential_id_b64: "cred".to_string(),
1494 public_key_cose: vec![0x10],
1495 current_sign_count: 0,
1496 allowed_credential_ids: vec!["cred".to_string()],
1497 rp_id: "localhost".to_string(),
1498 rp_origins: vec!["http://localhost:8080".to_string()],
1499 policy: CredentialPolicy::default(),
1500 });
1501 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1502 }
1503
1504 #[test]
1505 fn test_verify_authentication_rs256_roundtrip() {
1506 let cose_key = build_cose_rsa_key(&[0xAA; 256], &[0x01, 0x00, 0x01]);
1511 let (alg, key) = crate::crypto::parse_cose_key(&cose_key).unwrap();
1512 assert_eq!(alg, COSE_ALG_RS256);
1513 let result = verify_cose_signature(COSE_ALG_RS256, &key, b"data", &[0u8; 256]);
1514 assert!(matches!(
1515 result,
1516 Err(WebauthnError::SignatureVerificationFailed)
1517 ));
1518 }
1519
1520 fn valid_registration(rp_id_field: bool, extra_att_key: bool) -> (Vec<u8>, String, String) {
1524 use ring::signature::EcdsaKeyPair;
1525
1526 let rng = ring::rand::SystemRandom::new();
1527 let pkcs8 =
1528 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1529 .unwrap();
1530 let key_pair = EcdsaKeyPair::from_pkcs8(
1531 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1532 pkcs8.as_ref(),
1533 &rng,
1534 )
1535 .unwrap();
1536 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1537 let cose_key = build_cose_ec2_key(&pub_bytes[1..33], &pub_bytes[33..65]);
1538
1539 let credential_id = vec![0x42u8; 8];
1540 let auth_data = build_auth_data_with_credential(
1541 "localhost",
1542 FLAG_UP | FLAG_AT,
1543 0,
1544 &credential_id,
1545 &cose_key,
1546 );
1547
1548 let challenge = generate_challenge_bytes();
1549 let challenge_b64 = base64_encode_urlsafe(&challenge);
1550
1551 let mut client_data = serde_json::json!({
1552 "type": "webauthn.create",
1553 "challenge": challenge_b64,
1554 "origin": "http://localhost:8080",
1555 });
1556 if rp_id_field {
1557 client_data["rpId"] = serde_json::json!("localhost");
1558 }
1559 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1560
1561 let mut entries = vec![
1562 (
1563 ciborium::Value::Integer(1.into()),
1564 ciborium::Value::Text("none".to_string()),
1565 ),
1566 (
1567 ciborium::Value::Integer(2.into()),
1568 ciborium::Value::Bytes(auth_data),
1569 ),
1570 ];
1571 if extra_att_key {
1572 entries.push((
1573 ciborium::Value::Integer(9.into()),
1574 ciborium::Value::Bool(true),
1575 ));
1576 }
1577 entries.push((
1578 ciborium::Value::Integer(3.into()),
1579 ciborium::Value::Map(vec![]),
1580 ));
1581 let mut buf = Vec::new();
1582 ciborium::ser::into_writer(&ciborium::Value::Map(entries), &mut buf).unwrap();
1583
1584 (challenge, client_data_b64, base64_encode_urlsafe(&buf))
1585 }
1586
1587 #[test]
1590 fn registration_accepts_rpid_field_and_unknown_keys() {
1591 let (challenge, client_data_b64, att_obj_b64) = valid_registration(true, true);
1592 let result = verify_registration(
1593 &challenge,
1594 &client_data_b64,
1595 &att_obj_b64,
1596 "",
1597 "localhost",
1598 &["http://localhost:8080".to_string()],
1599 &AttestationPolicy::default(),
1600 &CredentialPolicy::default(),
1601 )
1602 .unwrap();
1603 assert_eq!(result.attestation_format, "none");
1604 assert_eq!(result.credential_id, base64_encode_urlsafe(&[0x42u8; 8]));
1605 }
1606
1607 #[test]
1608 fn registration_rejects_missing_type_in_client_data() {
1609 let challenge = generate_challenge_bytes();
1610 let client_data = serde_json::json!({
1611 "challenge": base64_encode_urlsafe(&challenge),
1612 "origin": "http://localhost:8080",
1613 });
1614 let result = verify_registration(
1615 &challenge,
1616 &base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
1617 &base64_encode_urlsafe(&[0xFF; 8]),
1618 "",
1619 "localhost",
1620 &["http://localhost:8080".to_string()],
1621 &AttestationPolicy::default(),
1622 &CredentialPolicy::default(),
1623 );
1624 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1625 }
1626
1627 #[test]
1628 fn registration_rejects_missing_origin_in_client_data() {
1629 let challenge = generate_challenge_bytes();
1630 let client_data = serde_json::json!({
1631 "type": "webauthn.create",
1632 "challenge": base64_encode_urlsafe(&challenge),
1633 });
1634 let result = verify_registration(
1635 &challenge,
1636 &base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
1637 &base64_encode_urlsafe(&[0xFF; 8]),
1638 "",
1639 "localhost",
1640 &["http://localhost:8080".to_string()],
1641 &AttestationPolicy::default(),
1642 &CredentialPolicy::default(),
1643 );
1644 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1645 }
1646
1647 #[test]
1651 fn parse_auth_data_rejects_truncated_public_key() {
1652 let mut auth_data = vec![0u8; 37 + 16 + 2 + 2];
1653 auth_data[32] = FLAG_UP | FLAG_AT;
1654 auth_data[53..55].copy_from_slice(&2u16.to_be_bytes());
1655 let result = parse_authenticator_data(&auth_data);
1656 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1657 }
1658
1659 use crate::crypto::tests::{build_cose_ec2_key_alg, build_cose_okp_key};
1662 use crate::crypto::{COSE_ALG_ES256, COSE_ALG_ES384};
1663
1664 #[test]
1666 fn registration_roundtrip_es384() {
1667 use ring::signature::{EcdsaKeyPair, KeyPair as _, ECDSA_P384_SHA384_FIXED_SIGNING};
1668
1669 let rng = ring::rand::SystemRandom::new();
1670 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, &rng).unwrap();
1671 let key_pair =
1672 EcdsaKeyPair::from_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, pkcs8.as_ref(), &rng)
1673 .unwrap();
1674 let pub_bytes = key_pair.public_key().as_ref();
1675 let cose_key = build_cose_ec2_key_alg(2, -35, &pub_bytes[1..49], &pub_bytes[49..97]);
1676
1677 let credential_id = vec![0xE3u8; 8];
1678 let auth_data = build_auth_data_with_credential(
1679 "localhost",
1680 FLAG_UP | FLAG_AT | FLAG_UV | FLAG_BE | FLAG_BS,
1681 0,
1682 &credential_id,
1683 &cose_key,
1684 );
1685 let att_obj = build_attestation_object(&auth_data);
1686
1687 let challenge = generate_challenge_bytes();
1688 let client_data = serde_json::json!({
1689 "type": "webauthn.create",
1690 "challenge": base64_encode_urlsafe(&challenge),
1691 "origin": "http://localhost:8080",
1692 });
1693 let result = verify_registration(
1694 &challenge,
1695 &base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
1696 &base64_encode_urlsafe(&att_obj),
1697 "",
1698 "localhost",
1699 &["http://localhost:8080".to_string()],
1700 &AttestationPolicy::default(),
1701 &CredentialPolicy::default(),
1702 )
1703 .unwrap();
1704
1705 assert!(result.device_name.contains("ES384"));
1706 assert!(result.user_verified);
1707 assert!(result.backup_eligible);
1708 assert!(result.backup_state);
1709 }
1710
1711 #[test]
1713 fn registration_authentication_roundtrip_eddsa() {
1714 use ring::signature::{Ed25519KeyPair, KeyPair as _};
1715
1716 let rng = ring::rand::SystemRandom::new();
1717 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1718 let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1719 let cose_key = build_cose_okp_key(key_pair.public_key().as_ref());
1720
1721 let credential_id = vec![0xEDu8; 8];
1723 let auth_data = build_auth_data_with_credential(
1724 "localhost",
1725 FLAG_UP | FLAG_AT,
1726 0,
1727 &credential_id,
1728 &cose_key,
1729 );
1730 let att_obj = build_attestation_object(&auth_data);
1731 let challenge = generate_challenge_bytes();
1732 let client_data = serde_json::json!({
1733 "type": "webauthn.create",
1734 "challenge": base64_encode_urlsafe(&challenge),
1735 "origin": "http://localhost:8080",
1736 });
1737 let registration = verify_registration(
1738 &challenge,
1739 &base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
1740 &base64_encode_urlsafe(&att_obj),
1741 "",
1742 "localhost",
1743 &["http://localhost:8080".to_string()],
1744 &AttestationPolicy::default(),
1745 &CredentialPolicy::default(),
1746 )
1747 .unwrap();
1748 assert!(registration.device_name.contains("EdDSA"));
1749 assert!(!registration.backup_eligible);
1750
1751 let auth_challenge = generate_challenge_bytes();
1753 let assertion_client_data = serde_json::json!({
1754 "type": "webauthn.get",
1755 "challenge": base64_encode_urlsafe(&auth_challenge),
1756 "origin": "http://localhost:8080",
1757 });
1758 let assertion_client_data_bytes = serde_json::to_vec(&assertion_client_data).unwrap();
1759 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1760 let mut assertion_auth_data = rp_id_hash;
1761 assertion_auth_data.push(FLAG_UP | FLAG_BS);
1762 assertion_auth_data.extend_from_slice(&1u32.to_be_bytes());
1763
1764 use sha2::Digest as _;
1765 let mut signed_data = assertion_auth_data.clone();
1766 signed_data.extend_from_slice(&sha2::Sha256::digest(&assertion_client_data_bytes));
1767 let signature = key_pair.sign(&signed_data);
1768
1769 let cred_b64 = base64_encode_urlsafe(&credential_id);
1770 let result = verify_authentication(&AuthenticationParams {
1771 challenge_bytes: auth_challenge,
1772 client_data_json_b64: base64_encode_urlsafe(&assertion_client_data_bytes),
1773 authenticator_data_b64: base64_encode_urlsafe(&assertion_auth_data),
1774 signature_b64: base64_encode_urlsafe(signature.as_ref()),
1775 credential_id_b64: cred_b64.clone(),
1776 public_key_cose: cose_key,
1777 current_sign_count: 0,
1778 allowed_credential_ids: vec![cred_b64],
1779 rp_id: "localhost".to_string(),
1780 rp_origins: vec!["http://localhost:8080".to_string()],
1781 policy: CredentialPolicy::default(),
1782 })
1783 .unwrap();
1784 assert_eq!(result.new_sign_count, 1);
1785 assert!(result.backup_state);
1786 assert!(!result.backup_eligible);
1787 }
1788
1789 #[test]
1793 fn registration_rejects_p256_key_claiming_es384() {
1794 let cose_key = build_cose_ec2_key_alg(1, -35, &[0xAA; 32], &[0xBB; 32]); let auth_data =
1796 build_auth_data_with_credential("localhost", FLAG_UP | FLAG_AT, 0, &[0x01], &cose_key);
1797 let att_obj = build_attestation_object(&auth_data);
1798 let challenge = generate_challenge_bytes();
1799 let client_data = serde_json::json!({
1800 "type": "webauthn.create",
1801 "challenge": base64_encode_urlsafe(&challenge),
1802 "origin": "http://localhost:8080",
1803 });
1804 let result = verify_registration(
1805 &challenge,
1806 &base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
1807 &base64_encode_urlsafe(&att_obj),
1808 "",
1809 "localhost",
1810 &["http://localhost:8080".to_string()],
1811 &AttestationPolicy::default(),
1812 &CredentialPolicy::default(),
1813 );
1814 assert!(matches!(
1815 result,
1816 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_ES384))
1817 ));
1818 }
1819
1820 fn registration_inputs(flags: u8) -> (Vec<u8>, String, String) {
1825 let cose_key = build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]);
1826 let auth_data =
1827 build_auth_data_with_credential("localhost", flags, 0, &[0x01, 0x02], &cose_key);
1828 let challenge = generate_challenge_bytes();
1829 let client_data = serde_json::json!({
1830 "type": "webauthn.create",
1831 "challenge": base64_encode_urlsafe(&challenge),
1832 "origin": "http://localhost:8080",
1833 });
1834 (
1835 challenge,
1836 base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
1837 base64_encode_urlsafe(&build_attestation_object(&auth_data)),
1838 )
1839 }
1840
1841 #[test]
1842 fn uv_required_rejects_clear_flag_and_accepts_set() {
1843 use crate::policy::UserVerificationPolicy;
1844 let uv_required = CredentialPolicy {
1845 user_verification: UserVerificationPolicy::Required,
1846 ..CredentialPolicy::default()
1847 };
1848
1849 let (challenge, cd, ao) = registration_inputs(FLAG_UP | FLAG_AT);
1851 let result = verify_registration(
1852 &challenge,
1853 &cd,
1854 &ao,
1855 "",
1856 "localhost",
1857 &["http://localhost:8080".to_string()],
1858 &AttestationPolicy::default(),
1859 &uv_required,
1860 );
1861 assert!(matches!(
1862 result,
1863 Err(WebauthnError::UserVerificationRequired)
1864 ));
1865
1866 let (challenge, cd, ao) = registration_inputs(FLAG_UP | FLAG_AT | FLAG_UV);
1868 let result = verify_registration(
1869 &challenge,
1870 &cd,
1871 &ao,
1872 "",
1873 "localhost",
1874 &["http://localhost:8080".to_string()],
1875 &AttestationPolicy::default(),
1876 &uv_required,
1877 );
1878 assert!(result.is_ok());
1879 }
1880
1881 #[test]
1883 fn uv_preferred_and_discouraged_report_only() {
1884 use crate::policy::UserVerificationPolicy;
1885 let (challenge, cd, ao) = registration_inputs(FLAG_UP | FLAG_AT);
1887
1888 for uv in [
1889 UserVerificationPolicy::Preferred,
1890 UserVerificationPolicy::Discouraged,
1891 ] {
1892 let policy = CredentialPolicy {
1893 user_verification: uv,
1894 ..CredentialPolicy::default()
1895 };
1896 let result = verify_registration(
1897 &challenge,
1898 &cd,
1899 &ao,
1900 "",
1901 "localhost",
1902 &["http://localhost:8080".to_string()],
1903 &AttestationPolicy::default(),
1904 &policy,
1905 )
1906 .unwrap();
1907 assert!(!result.user_verified, "{uv:?} must report, not reject");
1908 }
1909 }
1910
1911 #[test]
1916 fn registration_rejects_algorithm_outside_allowlist() {
1917 use ring::signature::{EcdsaKeyPair, KeyPair as _, ECDSA_P384_SHA384_FIXED_SIGNING};
1919 let rng = ring::rand::SystemRandom::new();
1920 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, &rng).unwrap();
1921 let key_pair =
1922 EcdsaKeyPair::from_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, pkcs8.as_ref(), &rng)
1923 .unwrap();
1924 let pub_bytes = key_pair.public_key().as_ref();
1925 let cose_key = build_cose_ec2_key_alg(2, -35, &pub_bytes[1..49], &pub_bytes[49..97]);
1926
1927 let auth_data =
1928 build_auth_data_with_credential("localhost", FLAG_UP | FLAG_AT, 0, &[0x01], &cose_key);
1929 let att_obj = build_attestation_object(&auth_data);
1930 let challenge = generate_challenge_bytes();
1931 let client_data = serde_json::json!({
1932 "type": "webauthn.create",
1933 "challenge": base64_encode_urlsafe(&challenge),
1934 "origin": "http://localhost:8080",
1935 });
1936 let common = (
1937 challenge,
1938 base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
1939 base64_encode_urlsafe(&att_obj),
1940 );
1941
1942 let es256_only = CredentialPolicy {
1944 allowed_algorithms: vec![COSE_ALG_ES256],
1945 ..CredentialPolicy::default()
1946 };
1947 let result = verify_registration(
1948 &common.0,
1949 &common.1,
1950 &common.2,
1951 "",
1952 "localhost",
1953 &["http://localhost:8080".to_string()],
1954 &AttestationPolicy::default(),
1955 &es256_only,
1956 );
1957 assert!(matches!(
1958 result,
1959 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_ES384))
1960 ));
1961
1962 let with_es384 = CredentialPolicy {
1964 allowed_algorithms: vec![COSE_ALG_ES256, COSE_ALG_ES384],
1965 ..CredentialPolicy::default()
1966 };
1967 let result = verify_registration(
1968 &common.0,
1969 &common.1,
1970 &common.2,
1971 "",
1972 "localhost",
1973 &["http://localhost:8080".to_string()],
1974 &AttestationPolicy::default(),
1975 &with_es384,
1976 );
1977 assert!(result.is_ok());
1978
1979 let result = verify_registration(
1981 &common.0,
1982 &common.1,
1983 &common.2,
1984 "",
1985 "localhost",
1986 &["http://localhost:8080".to_string()],
1987 &AttestationPolicy::default(),
1988 &CredentialPolicy::default(),
1989 );
1990 assert!(result.is_ok());
1991 }
1992
1993 #[test]
1998 fn backup_policy_device_bound_enforced_at_registration() {
1999 use crate::policy::BackupPolicy;
2000 let device_bound = CredentialPolicy {
2001 backup: BackupPolicy::RequireDeviceBound,
2002 ..CredentialPolicy::default()
2003 };
2004
2005 let (challenge, cd, ao) = registration_inputs(FLAG_UP | FLAG_AT | FLAG_BE);
2007 let result = verify_registration(
2008 &challenge,
2009 &cd,
2010 &ao,
2011 "",
2012 "localhost",
2013 &["http://localhost:8080".to_string()],
2014 &AttestationPolicy::default(),
2015 &device_bound,
2016 );
2017 assert!(matches!(result, Err(WebauthnError::PolicyViolation(_))));
2018
2019 let (challenge, cd, ao) = registration_inputs(FLAG_UP | FLAG_AT);
2021 let result = verify_registration(
2022 &challenge,
2023 &cd,
2024 &ao,
2025 "",
2026 "localhost",
2027 &["http://localhost:8080".to_string()],
2028 &AttestationPolicy::default(),
2029 &device_bound,
2030 );
2031 let registration = result.unwrap();
2032 assert!(!registration.backup_eligible);
2033 assert!(!registration.backup_state);
2034 }
2035
2036 #[test]
2038 fn backup_policy_allow_reports_flags() {
2039 let (challenge, cd, ao) = registration_inputs(FLAG_UP | FLAG_AT | FLAG_BE | FLAG_BS);
2040 let registration = verify_registration(
2041 &challenge,
2042 &cd,
2043 &ao,
2044 "",
2045 "localhost",
2046 &["http://localhost:8080".to_string()],
2047 &AttestationPolicy::default(),
2048 &CredentialPolicy::default(),
2049 )
2050 .unwrap();
2051 assert!(registration.backup_eligible);
2052 assert!(registration.backup_state);
2053 }
2054
2055 #[test]
2057 fn backup_policy_device_bound_enforced_at_authentication() {
2058 use crate::policy::BackupPolicy;
2059 use ring::signature::{EcdsaKeyPair, KeyPair as _};
2060
2061 let rng = ring::rand::SystemRandom::new();
2062 let pkcs8 =
2063 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
2064 .unwrap();
2065 let key_pair = EcdsaKeyPair::from_pkcs8(
2066 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
2067 pkcs8.as_ref(),
2068 &rng,
2069 )
2070 .unwrap();
2071 let pub_bytes = key_pair.public_key().as_ref();
2072 let cose_key = build_cose_ec2_key(&pub_bytes[1..33], &pub_bytes[33..65]);
2073
2074 let challenge = generate_challenge_bytes();
2075 let client_data = serde_json::json!({
2076 "type": "webauthn.get",
2077 "challenge": base64_encode_urlsafe(&challenge),
2078 "origin": "http://localhost:8080",
2079 });
2080 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
2081 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
2082 let mut auth_data = rp_id_hash;
2083 auth_data.push(FLAG_UP | FLAG_BE); auth_data.extend_from_slice(&1u32.to_be_bytes());
2085
2086 use sha2::Digest as _;
2087 let mut signed_data = auth_data.clone();
2088 signed_data.extend_from_slice(&sha2::Sha256::digest(&client_data_bytes));
2089 let signature = key_pair.sign(&rng, &signed_data).unwrap();
2090
2091 let cred_b64 = base64_encode_urlsafe(&[0x01]);
2092 let params = AuthenticationParams {
2093 challenge_bytes: challenge,
2094 client_data_json_b64: base64_encode_urlsafe(&client_data_bytes),
2095 authenticator_data_b64: base64_encode_urlsafe(&auth_data),
2096 signature_b64: base64_encode_urlsafe(signature.as_ref()),
2097 credential_id_b64: cred_b64.clone(),
2098 public_key_cose: cose_key,
2099 current_sign_count: 0,
2100 allowed_credential_ids: vec![cred_b64],
2101 rp_id: "localhost".to_string(),
2102 rp_origins: vec!["http://localhost:8080".to_string()],
2103 policy: CredentialPolicy {
2104 backup: BackupPolicy::RequireDeviceBound,
2105 ..CredentialPolicy::default()
2106 },
2107 };
2108 assert!(matches!(
2109 verify_authentication(¶ms),
2110 Err(WebauthnError::PolicyViolation(_))
2111 ));
2112 }
2113
2114 #[test]
2118 fn uv_required_enforced_at_authentication() {
2119 use crate::policy::UserVerificationPolicy;
2120 use ring::signature::{EcdsaKeyPair, KeyPair as _};
2121
2122 let rng = ring::rand::SystemRandom::new();
2123 let pkcs8 =
2124 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
2125 .unwrap();
2126 let key_pair = EcdsaKeyPair::from_pkcs8(
2127 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
2128 pkcs8.as_ref(),
2129 &rng,
2130 )
2131 .unwrap();
2132 let pub_bytes = key_pair.public_key().as_ref();
2133 let cose_key = build_cose_ec2_key(&pub_bytes[1..33], &pub_bytes[33..65]);
2134
2135 let challenge = generate_challenge_bytes();
2136 let client_data = serde_json::json!({
2137 "type": "webauthn.get",
2138 "challenge": base64_encode_urlsafe(&challenge),
2139 "origin": "http://localhost:8080",
2140 });
2141 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
2142 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
2143 let mut auth_data = rp_id_hash;
2144 auth_data.push(FLAG_UP); auth_data.extend_from_slice(&1u32.to_be_bytes());
2146
2147 use sha2::Digest as _;
2148 let mut signed_data = auth_data.clone();
2149 signed_data.extend_from_slice(&sha2::Sha256::digest(&client_data_bytes));
2150 let signature = key_pair.sign(&rng, &signed_data).unwrap();
2151
2152 let cred_b64 = base64_encode_urlsafe(&[0x01]);
2153 let params = AuthenticationParams {
2154 challenge_bytes: challenge,
2155 client_data_json_b64: base64_encode_urlsafe(&client_data_bytes),
2156 authenticator_data_b64: base64_encode_urlsafe(&auth_data),
2157 signature_b64: base64_encode_urlsafe(signature.as_ref()),
2158 credential_id_b64: cred_b64.clone(),
2159 public_key_cose: cose_key,
2160 current_sign_count: 0,
2161 allowed_credential_ids: vec![cred_b64],
2162 rp_id: "localhost".to_string(),
2163 rp_origins: vec!["http://localhost:8080".to_string()],
2164 policy: CredentialPolicy {
2165 user_verification: UserVerificationPolicy::Required,
2166 ..CredentialPolicy::default()
2167 },
2168 };
2169 assert!(matches!(
2170 verify_authentication(¶ms),
2171 Err(WebauthnError::UserVerificationRequired)
2172 ));
2173
2174 let mut default_params = params.clone();
2176 default_params.policy = CredentialPolicy::default();
2177 assert!(verify_authentication(&default_params).is_ok());
2178 }
2179
2180 #[test]
2183 fn registration_propagates_attestation_statement_errors() {
2184 let auth_data = build_auth_data_with_credential(
2185 "localhost",
2186 FLAG_UP | FLAG_AT,
2187 0,
2188 &[0x01],
2189 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
2190 );
2191 let entries = vec![
2192 (
2193 ciborium::Value::Integer(1.into()),
2194 ciborium::Value::Text("packed".to_string()),
2195 ),
2196 (
2197 ciborium::Value::Integer(2.into()),
2198 ciborium::Value::Bytes(auth_data),
2199 ),
2200 (
2201 ciborium::Value::Integer(3.into()),
2202 ciborium::Value::Map(vec![]), ),
2204 ];
2205 let mut buf = Vec::new();
2206 ciborium::ser::into_writer(&ciborium::Value::Map(entries), &mut buf).unwrap();
2207
2208 let challenge = generate_challenge_bytes();
2209 let client_data = serde_json::json!({
2210 "type": "webauthn.create",
2211 "challenge": base64_encode_urlsafe(&challenge),
2212 "origin": "http://localhost:8080",
2213 });
2214 let result = verify_registration(
2215 &challenge,
2216 &base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap()),
2217 &base64_encode_urlsafe(&buf),
2218 "",
2219 "localhost",
2220 &["http://localhost:8080".to_string()],
2221 &AttestationPolicy::default(),
2222 &CredentialPolicy::default(),
2223 );
2224 assert!(matches!(result, Err(WebauthnError::AttestationError(_))));
2225 }
2226}