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;
36
37#[derive(Debug, Clone)]
39struct AuthenticatorData {
40 rp_id_hash: Vec<u8>,
42 flags: u8,
44 sign_count: u32,
46 credential_public_key_cose: Option<Vec<u8>>,
48 credential_id: Option<Vec<u8>>,
50 aaguid: Option<[u8; 16]>,
52}
53
54const FLAG_UP: u8 = 0x01;
56const FLAG_UV: u8 = 0x04;
58const FLAG_AT: u8 = 0x40;
60#[allow(dead_code)]
62const FLAG_ED: u8 = 0x80;
63
64const AUTH_DATA_MIN_LEN: usize = 37;
66
67const ATTESTED_CREDENTIAL_PREFIX_LEN: usize = 18;
69
70fn parse_authenticator_data(auth_data: &[u8]) -> Result<AuthenticatorData, WebauthnError> {
86 if auth_data.len() < AUTH_DATA_MIN_LEN {
87 return Err(WebauthnError::VerificationFailed(format!(
88 "Authenticator data too short: {} bytes (minimum {})",
89 auth_data.len(),
90 AUTH_DATA_MIN_LEN
91 )));
92 }
93
94 let truncated =
98 || WebauthnError::VerificationFailed("Authenticator data truncated".to_string());
99 let rp_id_hash = auth_data.get(..32).ok_or_else(truncated)?.to_vec();
100 let flags = *auth_data.get(32).ok_or_else(truncated)?;
101 let sign_count = u32::from_be_bytes(
102 auth_data
103 .get(33..37)
104 .ok_or_else(truncated)?
105 .try_into()
106 .map_err(|_| truncated())?,
107 );
108
109 let mut offset = 37;
110 let mut credential_id = None;
111 let mut credential_public_key_cose = None;
112 let mut aaguid = None;
113
114 if flags & FLAG_AT != 0 {
115 if auth_data.len() < offset + ATTESTED_CREDENTIAL_PREFIX_LEN {
116 return Err(WebauthnError::VerificationFailed(
117 "Attested credential data truncated (AAGUID + length)".to_string(),
118 ));
119 }
120 let aaguid_bytes = auth_data
121 .get(offset..offset + 16)
122 .ok_or_else(truncated)?
123 .to_vec();
124 let aaguid_arr: [u8; 16] = aaguid_bytes.try_into().map_err(|_| truncated())?;
125 aaguid = Some(aaguid_arr);
126 offset += 16;
127
128 let len_hi = *auth_data.get(offset).ok_or_else(truncated)?;
129 let len_lo = *auth_data.get(offset + 1).ok_or_else(truncated)?;
130 let cred_id_len = u16::from_be_bytes([len_hi, len_lo]) as usize;
131 offset += 2;
132
133 if auth_data.len() < offset + cred_id_len {
134 return Err(WebauthnError::VerificationFailed(
135 "Attested credential data truncated (credential ID)".to_string(),
136 ));
137 }
138 credential_id = Some(
139 auth_data
140 .get(offset..offset + cred_id_len)
141 .ok_or_else(truncated)?
142 .to_vec(),
143 );
144 offset += cred_id_len;
145
146 if offset >= auth_data.len() {
147 return Err(WebauthnError::VerificationFailed(
148 "Attested credential data truncated (public key)".to_string(),
149 ));
150 }
151 credential_public_key_cose = Some(auth_data.get(offset..).ok_or_else(truncated)?.to_vec());
152 }
153
154 Ok(AuthenticatorData {
155 rp_id_hash,
156 flags,
157 sign_count,
158 credential_public_key_cose,
159 credential_id,
160 aaguid,
161 })
162}
163
164fn validate_client_data(
172 client_data_json_b64: &str,
173 challenge_bytes: &[u8],
174 expected_type: &str,
175 rp_id: &str,
176 rp_origins: &[String],
177) -> Result<Vec<u8>, WebauthnError> {
178 let client_data_bytes = base64_decode_urlsafe(client_data_json_b64)?;
179 let client_data: serde_json::Value = serde_json::from_slice(&client_data_bytes)
180 .map_err(|e| WebauthnError::VerificationFailed(format!("client data parse error: {e}")))?;
181
182 let client_challenge = client_data
183 .get("challenge")
184 .and_then(|v| v.as_str())
185 .ok_or_else(|| {
186 WebauthnError::VerificationFailed("missing challenge in client data".to_string())
187 })?;
188 let client_challenge_bytes = base64_decode_urlsafe(client_challenge)?;
189 if client_challenge_bytes != challenge_bytes {
190 return Err(WebauthnError::VerificationFailed(
191 "challenge mismatch".to_string(),
192 ));
193 }
194
195 let typ = client_data
196 .get("type")
197 .and_then(|v| v.as_str())
198 .ok_or_else(|| {
199 WebauthnError::VerificationFailed("missing type in client data".to_string())
200 })?;
201 if typ != expected_type {
202 return Err(WebauthnError::VerificationFailed(format!(
203 "wrong type: {typ}"
204 )));
205 }
206
207 let origin = client_data
208 .get("origin")
209 .and_then(|v| v.as_str())
210 .ok_or_else(|| {
211 WebauthnError::VerificationFailed("missing origin in client data".to_string())
212 })?;
213 if !rp_origins.iter().any(|o| o == origin) {
214 return Err(WebauthnError::VerificationFailed(format!(
215 "origin '{origin}' not allowed"
216 )));
217 }
218
219 if let Some(rp) = client_data.get("rpId").and_then(|v| v.as_str()) {
221 if rp != rp_id {
222 return Err(WebauthnError::VerificationFailed(format!(
223 "rpId mismatch: client sent '{rp}', expected '{rp_id}'"
224 )));
225 }
226 }
227
228 Ok(client_data_bytes)
229}
230
231#[allow(clippy::too_many_arguments)]
262pub fn verify_registration(
263 challenge_bytes: &[u8],
264 client_data_json_b64: &str,
265 attestation_object_b64: &str,
266 existing_credential_id: &str,
267 rp_id: &str,
268 rp_origins: &[String],
269 attestation_policy: &AttestationPolicy,
270) -> Result<RegistrationResult, WebauthnError> {
271 let client_data_bytes = validate_client_data(
272 client_data_json_b64,
273 challenge_bytes,
274 "webauthn.create",
275 rp_id,
276 rp_origins,
277 )?;
278
279 let attestation_bytes = base64_decode_urlsafe(attestation_object_b64)?;
280 let attestation_val: ciborium::Value = ciborium::de::from_reader(&attestation_bytes[..])
281 .map_err(|e| {
282 WebauthnError::AttestationError(format!("attestation object CBOR parse error: {e}"))
283 })?;
284
285 let attestation_entries = cbor_map_entries(&attestation_val).ok_or_else(|| {
286 WebauthnError::AttestationError("attestation object is not a CBOR map".to_string())
287 })?;
288
289 let mut fmt: Option<String> = None;
290 let mut auth_data_bytes: Option<Vec<u8>> = None;
291 let mut att_stmt: Option<ciborium::Value> = None;
292
293 for (key, val) in &attestation_entries {
294 match *key {
295 1 => {
296 if let ciborium::Value::Text(s) = val {
297 fmt = Some(s.clone());
298 }
299 }
300 2 => {
301 if let Some(b) = cbor_bytes(val) {
302 auth_data_bytes = Some(b);
303 }
304 }
305 3 => {
306 att_stmt = Some(val.clone());
307 }
308 _ => {}
309 }
310 }
311
312 let fmt = fmt.unwrap_or_else(|| "none".to_string());
313 let auth_data_bytes = auth_data_bytes.ok_or_else(|| {
314 WebauthnError::AttestationError("missing authData in attestation object".to_string())
315 })?;
316
317 let auth_data = parse_authenticator_data(&auth_data_bytes)?;
318
319 use sha2::Digest;
320 let computed_rp_id_hash = sha2::Sha256::digest(rp_id.as_bytes()).to_vec();
321 if auth_data.rp_id_hash != computed_rp_id_hash {
322 return Err(WebauthnError::VerificationFailed(format!(
323 "rpId hash mismatch: computed {:x?}, got {:x?}",
324 computed_rp_id_hash, auth_data.rp_id_hash
325 )));
326 }
327
328 if auth_data.flags & FLAG_UP == 0 {
329 return Err(WebauthnError::VerificationFailed(
330 "User Present flag not set".to_string(),
331 ));
332 }
333
334 if auth_data.flags & FLAG_AT == 0 {
335 return Err(WebauthnError::VerificationFailed(
336 "Attested Credential Data flag not set during registration".to_string(),
337 ));
338 }
339
340 let credential_id = auth_data.credential_id.ok_or_else(|| {
341 WebauthnError::VerificationFailed("no credential ID in attested data".to_string())
342 })?;
343 let credential_id_b64 = base64_encode_urlsafe(&credential_id);
344
345 if existing_credential_id == credential_id_b64 {
346 return Err(WebauthnError::DuplicateCredential(credential_id_b64));
347 }
348
349 let public_key_cose = auth_data.credential_public_key_cose.ok_or_else(|| {
350 WebauthnError::VerificationFailed("no public key in attested data".to_string())
351 })?;
352
353 let (alg, cose_key) = parse_cose_key(&public_key_cose)?;
354
355 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
359 let aaguid = auth_data.aaguid.unwrap_or([0u8; 16]);
360 let attestation = verify_attestation(
361 &fmt,
362 att_stmt.as_ref(),
363 &auth_data_bytes,
364 &client_data_hash,
365 &credential_id,
366 alg,
367 &cose_key,
368 aaguid,
369 attestation_policy,
370 )?;
371
372 let user_verified = auth_data.flags & FLAG_UV != 0;
373
374 Ok(RegistrationResult {
375 credential_id: credential_id_b64,
376 device_name: format!("WebAuthn ({})", alg_to_name(alg)),
377 attestation_format: fmt,
378 attestation,
379 user_verified,
380 })
381}
382
383#[derive(Debug, Clone)]
388pub struct AuthenticationParams {
389 pub challenge_bytes: Vec<u8>,
391 pub client_data_json_b64: String,
393 pub authenticator_data_b64: String,
395 pub signature_b64: String,
397 pub credential_id_b64: String,
399 pub public_key_cose: Vec<u8>,
401 pub current_sign_count: u32,
403 pub allowed_credential_ids: Vec<String>,
405 pub rp_id: String,
407 pub rp_origins: Vec<String>,
409}
410
411pub fn verify_authentication(
439 params: &AuthenticationParams,
440) -> Result<AuthenticationResult, WebauthnError> {
441 let AuthenticationParams {
442 challenge_bytes,
443 client_data_json_b64,
444 authenticator_data_b64,
445 signature_b64,
446 credential_id_b64,
447 public_key_cose,
448 current_sign_count,
449 allowed_credential_ids,
450 rp_id,
451 rp_origins,
452 } = params;
453
454 if !allowed_credential_ids.contains(credential_id_b64) {
455 return Err(WebauthnError::VerificationFailed(
456 "credential ID not in allowed list".to_string(),
457 ));
458 }
459
460 let client_data_bytes = validate_client_data(
461 client_data_json_b64,
462 challenge_bytes,
463 "webauthn.get",
464 rp_id,
465 rp_origins,
466 )?;
467
468 let authenticator_data = base64_decode_urlsafe(authenticator_data_b64)?;
469 let auth_data = parse_authenticator_data(&authenticator_data)?;
470
471 use sha2::Digest;
472 let computed_rp_id_hash = sha2::Sha256::digest(rp_id.as_bytes()).to_vec();
473 if auth_data.rp_id_hash != computed_rp_id_hash {
474 return Err(WebauthnError::VerificationFailed(format!(
475 "rpId hash mismatch: computed {:x?}, got {:x?}",
476 computed_rp_id_hash, auth_data.rp_id_hash
477 )));
478 }
479
480 if auth_data.flags & FLAG_UP == 0 {
481 return Err(WebauthnError::VerificationFailed(
482 "User Present flag not set".to_string(),
483 ));
484 }
485
486 check_sign_count(*current_sign_count, auth_data.sign_count)?;
487
488 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
489
490 let mut signed_data = Vec::with_capacity(authenticator_data.len() + 32);
491 signed_data.extend_from_slice(&authenticator_data);
492 signed_data.extend_from_slice(&client_data_hash);
493
494 let signature = base64_decode_urlsafe(signature_b64)?;
495
496 let (alg, cose_key) = parse_cose_key(public_key_cose)?;
497 verify_cose_signature(alg, &cose_key, &signed_data, &signature)?;
498
499 let user_verified = auth_data.flags & FLAG_UV != 0;
500 let new_sign_count = auth_data.sign_count;
501
502 Ok(AuthenticationResult {
503 credential_id: credential_id_b64.clone(),
504 new_sign_count,
505 user_verified,
506 })
507}
508
509#[allow(
513 clippy::unwrap_used,
514 clippy::expect_used,
515 clippy::indexing_slicing,
516 clippy::panic
517)]
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use crate::crypto::tests::{build_cose_ec2_key, build_cose_rsa_key};
522 use crate::crypto::{base64_encode_urlsafe, generate_challenge_bytes, COSE_ALG_RS256};
523 use ring::signature::KeyPair as _;
524
525 fn build_attestation_object(auth_data: &[u8]) -> Vec<u8> {
526 use ciborium::Value;
527 let map = vec![
528 (Value::Integer(1.into()), Value::Text("none".to_string())),
529 (Value::Integer(2.into()), Value::Bytes(auth_data.to_vec())),
530 (Value::Integer(3.into()), Value::Map(vec![])),
531 ];
532 let mut buf = Vec::new();
533 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
534 buf
535 }
536
537 fn build_auth_data_with_credential(
538 rp_id: &str,
539 flags: u8,
540 sign_count: u32,
541 credential_id: &[u8],
542 cose_key: &[u8],
543 ) -> Vec<u8> {
544 use sha2::Digest;
545 let rp_id_hash = sha2::Sha256::digest(rp_id.as_bytes()).to_vec();
546 let mut auth_data = Vec::new();
547 auth_data.extend_from_slice(&rp_id_hash);
548 auth_data.push(flags);
549 auth_data.extend_from_slice(&sign_count.to_be_bytes());
550
551 if flags & FLAG_AT != 0 {
552 auth_data.extend_from_slice(&[0u8; 16]);
553 auth_data.extend_from_slice(&(credential_id.len() as u16).to_be_bytes());
554 auth_data.extend_from_slice(credential_id);
555 auth_data.extend_from_slice(cose_key);
556 }
557
558 auth_data
559 }
560
561 #[test]
562 fn test_parse_authenticator_data_minimal() {
563 let mut auth_data = vec![0u8; 37];
564 auth_data[..32].copy_from_slice(&[0xAA; 32]);
565 auth_data[32] = FLAG_UP;
566 auth_data[33..37].copy_from_slice(&1u32.to_be_bytes());
567
568 let parsed = parse_authenticator_data(&auth_data).unwrap();
569 assert_eq!(parsed.rp_id_hash, vec![0xAA; 32]);
570 assert_eq!(parsed.flags, FLAG_UP);
571 assert_eq!(parsed.sign_count, 1);
572 assert!(parsed.credential_public_key_cose.is_none());
573 assert!(parsed.credential_id.is_none());
574 }
575
576 #[test]
577 fn test_parse_authenticator_data_with_attested_credential() {
578 let credential_id = vec![0x01, 0x02, 0x03, 0x04];
579 let public_key_cose = vec![0x10, 0x20, 0x30];
580
581 let total_len = 37 + 16 + 2 + credential_id.len() + public_key_cose.len();
582 let mut auth_data = vec![0u8; total_len];
583 auth_data[32] = FLAG_UP | FLAG_AT;
584 auth_data[33..37].copy_from_slice(&5u32.to_be_bytes());
585
586 let offset = 37 + 16;
587 auth_data[offset..offset + 2].copy_from_slice(&(credential_id.len() as u16).to_be_bytes());
588 auth_data[offset + 2..offset + 2 + credential_id.len()].copy_from_slice(&credential_id);
589 let pk_offset = offset + 2 + credential_id.len();
590 auth_data[pk_offset..].copy_from_slice(&public_key_cose);
591
592 let parsed = parse_authenticator_data(&auth_data).unwrap();
593 assert_eq!(parsed.flags, FLAG_UP | FLAG_AT);
594 assert_eq!(parsed.sign_count, 5);
595 assert_eq!(parsed.credential_id, Some(credential_id));
596 assert_eq!(parsed.credential_public_key_cose, Some(public_key_cose));
597 }
598
599 #[test]
600 fn test_parse_authenticator_data_too_short() {
601 let auth_data = vec![0u8; 10];
602 let result = parse_authenticator_data(&auth_data);
603 assert!(result.is_err());
604 }
605
606 #[test]
607 fn test_verify_registration_valid_es256() {
608 use ring::signature::EcdsaKeyPair;
609
610 let rng = ring::rand::SystemRandom::new();
611 let pkcs8 =
612 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
613 .unwrap();
614 let key_pair = EcdsaKeyPair::from_pkcs8(
615 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
616 pkcs8.as_ref(),
617 &rng,
618 )
619 .unwrap();
620
621 let pub_bytes = key_pair.public_key().as_ref().to_vec();
622 let x = pub_bytes[1..33].to_vec();
623 let y = pub_bytes[33..65].to_vec();
624 let cose_key = build_cose_ec2_key(&x, &y);
625
626 let credential_id = vec![0x01, 0x02, 0x03, 0x04];
627 let auth_data = build_auth_data_with_credential(
628 "localhost",
629 FLAG_UP | FLAG_AT,
630 0,
631 &credential_id,
632 &cose_key,
633 );
634 let att_obj = build_attestation_object(&auth_data);
635
636 let challenge = generate_challenge_bytes();
637 let challenge_b64 = base64_encode_urlsafe(&challenge);
638
639 let client_data = serde_json::json!({
640 "type": "webauthn.create",
641 "challenge": challenge_b64,
642 "origin": "http://localhost:8080",
643 });
644 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
645 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
646
647 let result = verify_registration(
648 &challenge,
649 &client_data_b64,
650 &att_obj_b64,
651 "different-id",
652 "localhost",
653 &["http://localhost:8080".to_string()],
654 &AttestationPolicy::default(),
655 )
656 .unwrap();
657
658 assert_eq!(result.credential_id, base64_encode_urlsafe(&credential_id));
659 assert!(result.attestation_format == "none");
660 }
661
662 #[test]
663 fn test_verify_registration_challenge_mismatch() {
664 let auth_data = build_auth_data_with_credential(
665 "localhost",
666 FLAG_UP | FLAG_AT,
667 0,
668 &[0x01],
669 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
670 );
671 let att_obj = build_attestation_object(&auth_data);
672
673 let challenge = generate_challenge_bytes();
674 let _challenge_b64 = base64_encode_urlsafe(&challenge);
675
676 let client_data = serde_json::json!({
677 "type": "webauthn.create",
678 "challenge": base64_encode_urlsafe(&[1u8; 32]),
679 "origin": "http://localhost:8080",
680 });
681 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
682 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
683
684 let result = verify_registration(
685 &challenge,
686 &client_data_b64,
687 &att_obj_b64,
688 "different",
689 "localhost",
690 &["http://localhost:8080".to_string()],
691 &AttestationPolicy::default(),
692 );
693 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
694 }
695
696 #[test]
697 fn test_verify_registration_wrong_type() {
698 let auth_data = build_auth_data_with_credential(
699 "localhost",
700 FLAG_UP | FLAG_AT,
701 0,
702 &[0x01],
703 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
704 );
705 let att_obj = build_attestation_object(&auth_data);
706
707 let challenge = generate_challenge_bytes();
708 let challenge_b64 = base64_encode_urlsafe(&challenge);
709
710 let client_data = serde_json::json!({
711 "type": "webauthn.get",
712 "challenge": challenge_b64,
713 "origin": "http://localhost:8080",
714 });
715 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
716 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
717
718 let result = verify_registration(
719 &challenge,
720 &client_data_b64,
721 &att_obj_b64,
722 "different",
723 "localhost",
724 &["http://localhost:8080".to_string()],
725 &AttestationPolicy::default(),
726 );
727 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
728 }
729
730 #[test]
731 fn test_verify_registration_wrong_origin() {
732 let auth_data = build_auth_data_with_credential(
733 "localhost",
734 FLAG_UP | FLAG_AT,
735 0,
736 &[0x01],
737 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
738 );
739 let att_obj = build_attestation_object(&auth_data);
740
741 let challenge = generate_challenge_bytes();
742 let challenge_b64 = base64_encode_urlsafe(&challenge);
743
744 let client_data = serde_json::json!({
745 "type": "webauthn.create",
746 "challenge": challenge_b64,
747 "origin": "http://evil.com",
748 });
749 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
750 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
751
752 let result = verify_registration(
753 &challenge,
754 &client_data_b64,
755 &att_obj_b64,
756 "different",
757 "localhost",
758 &["http://localhost:8080".to_string()],
759 &AttestationPolicy::default(),
760 );
761 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
762 }
763
764 #[test]
765 fn test_verify_registration_duplicate() {
766 let auth_data = build_auth_data_with_credential(
767 "localhost",
768 FLAG_UP | FLAG_AT,
769 0,
770 &[0x01, 0x02, 0x03],
771 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
772 );
773 let att_obj = build_attestation_object(&auth_data);
774
775 let challenge = generate_challenge_bytes();
776 let challenge_b64 = base64_encode_urlsafe(&challenge);
777
778 let client_data = serde_json::json!({
779 "type": "webauthn.create",
780 "challenge": challenge_b64,
781 "origin": "http://localhost:8080",
782 });
783 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
784 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
785 let cred_id_b64 = base64_encode_urlsafe(&[0x01, 0x02, 0x03]);
786
787 let result = verify_registration(
788 &challenge,
789 &client_data_b64,
790 &att_obj_b64,
791 &cred_id_b64,
792 "localhost",
793 &["http://localhost:8080".to_string()],
794 &AttestationPolicy::default(),
795 );
796 assert!(matches!(result, Err(WebauthnError::DuplicateCredential(_))));
797 }
798
799 #[test]
800 fn test_verify_registration_wrong_rp_id_hash() {
801 let credential_id = vec![0x01];
802 let cose_key = build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]);
803 let auth_data = build_auth_data_with_credential(
804 "evil.com",
805 FLAG_UP | FLAG_AT,
806 0,
807 &credential_id,
808 &cose_key,
809 );
810 let att_obj = build_attestation_object(&auth_data);
811
812 let challenge = generate_challenge_bytes();
813 let challenge_b64 = base64_encode_urlsafe(&challenge);
814
815 let client_data = serde_json::json!({
816 "type": "webauthn.create",
817 "challenge": challenge_b64,
818 "origin": "http://localhost:8080",
819 });
820 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
821 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
822
823 let result = verify_registration(
824 &challenge,
825 &client_data_b64,
826 &att_obj_b64,
827 "different",
828 "localhost",
829 &["http://localhost:8080".to_string()],
830 &AttestationPolicy::default(),
831 );
832 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
833 }
834
835 #[test]
836 fn test_verify_registration_missing_up_flag() {
837 let credential_id = vec![0x01];
838 let cose_key = build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]);
839 let auth_data =
840 build_auth_data_with_credential("localhost", FLAG_AT, 0, &credential_id, &cose_key);
841 let att_obj = build_attestation_object(&auth_data);
842
843 let challenge = generate_challenge_bytes();
844 let challenge_b64 = base64_encode_urlsafe(&challenge);
845
846 let client_data = serde_json::json!({
847 "type": "webauthn.create",
848 "challenge": challenge_b64,
849 "origin": "http://localhost:8080",
850 });
851 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
852 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
853
854 let result = verify_registration(
855 &challenge,
856 &client_data_b64,
857 &att_obj_b64,
858 "different",
859 "localhost",
860 &["http://localhost:8080".to_string()],
861 &AttestationPolicy::default(),
862 );
863 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
864 }
865
866 #[test]
870 fn registration_rejects_missing_at_flag() {
871 let auth_data = build_auth_data_with_credential(
872 "localhost",
873 FLAG_UP, 0,
875 &[0x01],
876 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
877 );
878 let att_obj = build_attestation_object(&auth_data);
879
880 let challenge = generate_challenge_bytes();
881 let challenge_b64 = base64_encode_urlsafe(&challenge);
882
883 let client_data = serde_json::json!({
884 "type": "webauthn.create",
885 "challenge": challenge_b64,
886 "origin": "http://localhost:8080",
887 });
888 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
889 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
890
891 let result = verify_registration(
892 &challenge,
893 &client_data_b64,
894 &att_obj_b64,
895 "different",
896 "localhost",
897 &["http://localhost:8080".to_string()],
898 &AttestationPolicy::default(),
899 );
900 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
901 }
902
903 #[test]
904 fn test_verify_authentication_valid_es256() {
905 use ring::signature::EcdsaKeyPair;
906
907 let rng = ring::rand::SystemRandom::new();
908 let pkcs8 =
909 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
910 .unwrap();
911 let key_pair = EcdsaKeyPair::from_pkcs8(
912 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
913 pkcs8.as_ref(),
914 &rng,
915 )
916 .unwrap();
917
918 let pub_bytes = key_pair.public_key().as_ref().to_vec();
919 let x = pub_bytes[1..33].to_vec();
920 let y = pub_bytes[33..65].to_vec();
921 let cose_key = build_cose_ec2_key(&x, &y);
922
923 let challenge = generate_challenge_bytes();
924 let challenge_b64 = base64_encode_urlsafe(&challenge);
925
926 use sha2::Digest;
927 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
928 let mut auth_data = Vec::new();
929 auth_data.extend_from_slice(&rp_id_hash);
930 auth_data.push(FLAG_UP);
931 auth_data.extend_from_slice(&1u32.to_be_bytes());
932
933 let client_data = serde_json::json!({
934 "type": "webauthn.get",
935 "challenge": challenge_b64,
936 "origin": "http://localhost:8080",
937 });
938 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
939 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
940 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
941
942 let mut signed_data = Vec::new();
943 signed_data.extend_from_slice(&auth_data);
944 signed_data.extend_from_slice(&client_data_hash);
945
946 let signature = key_pair.sign(&rng, &signed_data).unwrap();
947
948 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
949 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
950 let cred_id_b64 = base64_encode_urlsafe(&[0x01, 0x02]);
951
952 let result = verify_authentication(&AuthenticationParams {
953 challenge_bytes: challenge.clone(),
954 client_data_json_b64: client_data_b64,
955 authenticator_data_b64: auth_data_b64,
956 signature_b64: sig_b64,
957 credential_id_b64: cred_id_b64.clone(),
958 public_key_cose: cose_key,
959 current_sign_count: 0,
960 allowed_credential_ids: vec![cred_id_b64.clone()],
961 rp_id: "localhost".to_string(),
962 rp_origins: vec!["http://localhost:8080".to_string()],
963 })
964 .unwrap();
965
966 assert_eq!(result.credential_id, cred_id_b64);
967 assert_eq!(result.new_sign_count, 1);
968 }
969
970 #[test]
971 fn test_verify_authentication_wrong_signature() {
972 use ring::signature::EcdsaKeyPair;
973
974 let rng = ring::rand::SystemRandom::new();
975 let pkcs8 =
976 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
977 .unwrap();
978 let key_pair = EcdsaKeyPair::from_pkcs8(
979 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
980 pkcs8.as_ref(),
981 &rng,
982 )
983 .unwrap();
984
985 let pub_bytes = key_pair.public_key().as_ref().to_vec();
986 let x = pub_bytes[1..33].to_vec();
987 let y = pub_bytes[33..65].to_vec();
988 let cose_key = build_cose_ec2_key(&x, &y);
989
990 let challenge = generate_challenge_bytes();
991 let challenge_b64 = base64_encode_urlsafe(&challenge);
992
993 use sha2::Digest;
994 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
995 let mut auth_data = Vec::new();
996 auth_data.extend_from_slice(&rp_id_hash);
997 auth_data.push(FLAG_UP);
998 auth_data.extend_from_slice(&1u32.to_be_bytes());
999
1000 let client_data = serde_json::json!({
1001 "type": "webauthn.get",
1002 "challenge": challenge_b64,
1003 "origin": "http://localhost:8080",
1004 });
1005 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1006 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1007 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1008
1009 let mut signed_data = Vec::new();
1010 signed_data.extend_from_slice(&auth_data);
1011 signed_data.extend_from_slice(&client_data_hash);
1012
1013 let mut wrong_sig = vec![0u8; 64];
1014 wrong_sig[0] = 0xFF;
1015
1016 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1017 let sig_b64 = base64_encode_urlsafe(&wrong_sig);
1018 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
1019
1020 let result = verify_authentication(&AuthenticationParams {
1021 challenge_bytes: challenge.clone(),
1022 client_data_json_b64: client_data_b64,
1023 authenticator_data_b64: auth_data_b64,
1024 signature_b64: sig_b64,
1025 credential_id_b64: cred_id_b64.clone(),
1026 public_key_cose: cose_key,
1027 current_sign_count: 0,
1028 allowed_credential_ids: vec![cred_id_b64],
1029 rp_id: "localhost".to_string(),
1030 rp_origins: vec!["http://localhost:8080".to_string()],
1031 });
1032 assert!(matches!(
1033 result,
1034 Err(WebauthnError::SignatureVerificationFailed)
1035 ));
1036 }
1037
1038 #[test]
1039 fn test_verify_authentication_credential_not_allowed() {
1040 let challenge = generate_challenge_bytes();
1041 let challenge_b64 = base64_encode_urlsafe(&challenge);
1042
1043 let client_data = serde_json::json!({
1044 "type": "webauthn.get",
1045 "challenge": challenge_b64,
1046 "origin": "http://localhost:8080",
1047 });
1048 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1049
1050 use sha2::Digest;
1051 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1052 let mut auth_data = Vec::new();
1053 auth_data.extend_from_slice(&rp_id_hash);
1054 auth_data.push(FLAG_UP);
1055 auth_data.extend_from_slice(&0u32.to_be_bytes());
1056 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1057
1058 let result = verify_authentication(&AuthenticationParams {
1059 challenge_bytes: challenge.clone(),
1060 client_data_json_b64: client_data_b64,
1061 authenticator_data_b64: auth_data_b64,
1062 signature_b64: "sig".to_string(),
1063 credential_id_b64: "unauthorized-cred".to_string(),
1064 public_key_cose: vec![0x10, 0x20],
1065 current_sign_count: 0,
1066 allowed_credential_ids: vec!["allowed-cred".to_string()],
1067 rp_id: "localhost".to_string(),
1068 rp_origins: vec!["http://localhost:8080".to_string()],
1069 });
1070 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1071 }
1072
1073 #[test]
1074 fn test_verify_authentication_wrong_origin() {
1075 let challenge = generate_challenge_bytes();
1076 let challenge_b64 = base64_encode_urlsafe(&challenge);
1077
1078 let client_data = serde_json::json!({
1079 "type": "webauthn.get",
1080 "challenge": challenge_b64,
1081 "origin": "http://evil.com",
1082 });
1083 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1084
1085 use sha2::Digest;
1086 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1087 let mut auth_data = Vec::new();
1088 auth_data.extend_from_slice(&rp_id_hash);
1089 auth_data.push(FLAG_UP);
1090 auth_data.extend_from_slice(&0u32.to_be_bytes());
1091 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1092
1093 let result = verify_authentication(&AuthenticationParams {
1094 challenge_bytes: challenge.clone(),
1095 client_data_json_b64: client_data_b64,
1096 authenticator_data_b64: auth_data_b64,
1097 signature_b64: "sig".to_string(),
1098 credential_id_b64: "cred".to_string(),
1099 public_key_cose: vec![0x10],
1100 current_sign_count: 0,
1101 allowed_credential_ids: vec!["cred".to_string()],
1102 rp_id: "localhost".to_string(),
1103 rp_origins: vec!["http://localhost:8080".to_string()],
1104 });
1105 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1106 }
1107
1108 #[test]
1109 fn test_verify_authentication_wrong_type() {
1110 let challenge = generate_challenge_bytes();
1111 let challenge_b64 = base64_encode_urlsafe(&challenge);
1112
1113 let client_data = serde_json::json!({
1114 "type": "webauthn.create",
1115 "challenge": challenge_b64,
1116 "origin": "http://localhost:8080",
1117 });
1118 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1119
1120 use sha2::Digest;
1121 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1122 let mut auth_data = Vec::new();
1123 auth_data.extend_from_slice(&rp_id_hash);
1124 auth_data.push(FLAG_UP);
1125 auth_data.extend_from_slice(&0u32.to_be_bytes());
1126 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1127
1128 let result = verify_authentication(&AuthenticationParams {
1129 challenge_bytes: challenge.clone(),
1130 client_data_json_b64: client_data_b64,
1131 authenticator_data_b64: auth_data_b64,
1132 signature_b64: "sig".to_string(),
1133 credential_id_b64: "cred".to_string(),
1134 public_key_cose: vec![0x10],
1135 current_sign_count: 0,
1136 allowed_credential_ids: vec!["cred".to_string()],
1137 rp_id: "localhost".to_string(),
1138 rp_origins: vec!["http://localhost:8080".to_string()],
1139 });
1140 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1141 }
1142
1143 #[test]
1144 fn test_verify_authentication_sign_count_decrease() {
1145 use ring::signature::EcdsaKeyPair;
1146
1147 let rng = ring::rand::SystemRandom::new();
1148 let pkcs8 =
1149 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1150 .unwrap();
1151 let key_pair = EcdsaKeyPair::from_pkcs8(
1152 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1153 pkcs8.as_ref(),
1154 &rng,
1155 )
1156 .unwrap();
1157
1158 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1159 let x = pub_bytes[1..33].to_vec();
1160 let y = pub_bytes[33..65].to_vec();
1161 let cose_key = build_cose_ec2_key(&x, &y);
1162
1163 let challenge = generate_challenge_bytes();
1164 let challenge_b64 = base64_encode_urlsafe(&challenge);
1165
1166 use sha2::Digest;
1167 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1168 let mut auth_data = Vec::new();
1169 auth_data.extend_from_slice(&rp_id_hash);
1170 auth_data.push(FLAG_UP);
1171 auth_data.extend_from_slice(&5u32.to_be_bytes());
1172
1173 let client_data = serde_json::json!({
1174 "type": "webauthn.get",
1175 "challenge": challenge_b64,
1176 "origin": "http://localhost:8080",
1177 });
1178 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1179 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1180 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1181
1182 let mut signed_data = Vec::new();
1183 signed_data.extend_from_slice(&auth_data);
1184 signed_data.extend_from_slice(&client_data_hash);
1185
1186 let signature = key_pair.sign(&rng, &signed_data).unwrap();
1187
1188 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1189 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
1190 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
1191
1192 let result = verify_authentication(&AuthenticationParams {
1193 challenge_bytes: challenge.clone(),
1194 client_data_json_b64: client_data_b64,
1195 authenticator_data_b64: auth_data_b64,
1196 signature_b64: sig_b64,
1197 credential_id_b64: cred_id_b64.clone(),
1198 public_key_cose: cose_key,
1199 current_sign_count: 10,
1200 allowed_credential_ids: vec![cred_id_b64],
1201 rp_id: "localhost".to_string(),
1202 rp_origins: vec!["http://localhost:8080".to_string()],
1203 });
1204 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1205 }
1206
1207 #[test]
1208 fn test_verify_authentication_wrong_rp_id_hash() {
1209 let challenge = generate_challenge_bytes();
1210 let challenge_b64 = base64_encode_urlsafe(&challenge);
1211
1212 let client_data = serde_json::json!({
1213 "type": "webauthn.get",
1214 "challenge": challenge_b64,
1215 "origin": "http://localhost:8080",
1216 });
1217 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1218
1219 use sha2::Digest;
1220 let rp_id_hash = sha2::Sha256::digest(b"evil.com").to_vec();
1221 let mut auth_data = Vec::new();
1222 auth_data.extend_from_slice(&rp_id_hash);
1223 auth_data.push(FLAG_UP);
1224 auth_data.extend_from_slice(&0u32.to_be_bytes());
1225 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1226
1227 let result = verify_authentication(&AuthenticationParams {
1228 challenge_bytes: challenge.clone(),
1229 client_data_json_b64: client_data_b64,
1230 authenticator_data_b64: auth_data_b64,
1231 signature_b64: "sig".to_string(),
1232 credential_id_b64: "cred".to_string(),
1233 public_key_cose: vec![0x10],
1234 current_sign_count: 0,
1235 allowed_credential_ids: vec!["cred".to_string()],
1236 rp_id: "localhost".to_string(),
1237 rp_origins: vec!["http://localhost:8080".to_string()],
1238 });
1239 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1240 }
1241
1242 #[test]
1243 fn test_verify_authentication_missing_up_flag() {
1244 use ring::signature::EcdsaKeyPair;
1245
1246 let rng = ring::rand::SystemRandom::new();
1247 let pkcs8 =
1248 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1249 .unwrap();
1250 let key_pair = EcdsaKeyPair::from_pkcs8(
1251 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1252 pkcs8.as_ref(),
1253 &rng,
1254 )
1255 .unwrap();
1256
1257 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1258 let x = pub_bytes[1..33].to_vec();
1259 let y = pub_bytes[33..65].to_vec();
1260 let cose_key = build_cose_ec2_key(&x, &y);
1261
1262 let challenge = generate_challenge_bytes();
1263 let challenge_b64 = base64_encode_urlsafe(&challenge);
1264
1265 use sha2::Digest;
1266 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1267 let mut auth_data = Vec::new();
1268 auth_data.extend_from_slice(&rp_id_hash);
1269 auth_data.push(0);
1270 auth_data.extend_from_slice(&1u32.to_be_bytes());
1271
1272 let client_data = serde_json::json!({
1273 "type": "webauthn.get",
1274 "challenge": challenge_b64,
1275 "origin": "http://localhost:8080",
1276 });
1277 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1278 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1279 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1280
1281 let mut signed_data = Vec::new();
1282 signed_data.extend_from_slice(&auth_data);
1283 signed_data.extend_from_slice(&client_data_hash);
1284
1285 let signature = key_pair.sign(&rng, &signed_data).unwrap();
1286
1287 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1288 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
1289 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
1290
1291 let result = verify_authentication(&AuthenticationParams {
1292 challenge_bytes: challenge.clone(),
1293 client_data_json_b64: client_data_b64,
1294 authenticator_data_b64: auth_data_b64,
1295 signature_b64: sig_b64,
1296 credential_id_b64: cred_id_b64.clone(),
1297 public_key_cose: cose_key,
1298 current_sign_count: 0,
1299 allowed_credential_ids: vec![cred_id_b64],
1300 rp_id: "localhost".to_string(),
1301 rp_origins: vec!["http://localhost:8080".to_string()],
1302 });
1303 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1304 }
1305
1306 #[test]
1307 fn test_verify_authentication_wrong_rp_id_in_client_data() {
1308 use ring::signature::EcdsaKeyPair;
1309
1310 let rng = ring::rand::SystemRandom::new();
1311 let pkcs8 =
1312 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1313 .unwrap();
1314 let key_pair = EcdsaKeyPair::from_pkcs8(
1315 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1316 pkcs8.as_ref(),
1317 &rng,
1318 )
1319 .unwrap();
1320
1321 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1322 let x = pub_bytes[1..33].to_vec();
1323 let y = pub_bytes[33..65].to_vec();
1324 let cose_key = build_cose_ec2_key(&x, &y);
1325
1326 let challenge = generate_challenge_bytes();
1327 let challenge_b64 = base64_encode_urlsafe(&challenge);
1328
1329 let client_data = serde_json::json!({
1330 "type": "webauthn.get",
1331 "challenge": challenge_b64,
1332 "origin": "http://localhost:8080",
1333 "rpId": "evil.com",
1334 });
1335 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1336 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1337
1338 use sha2::Digest;
1339 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1340 let mut auth_data = Vec::new();
1341 auth_data.extend_from_slice(&rp_id_hash);
1342 auth_data.push(FLAG_UP);
1343 auth_data.extend_from_slice(&1u32.to_be_bytes());
1344
1345 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1346 let mut signed_data = Vec::new();
1347 signed_data.extend_from_slice(&auth_data);
1348 signed_data.extend_from_slice(&client_data_hash);
1349
1350 let signature = key_pair.sign(&rng, &signed_data).unwrap();
1351
1352 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1353 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
1354 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
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_b64,
1361 credential_id_b64: cred_id_b64.clone(),
1362 public_key_cose: cose_key,
1363 current_sign_count: 0,
1364 allowed_credential_ids: vec![cred_id_b64],
1365 rp_id: "localhost".to_string(),
1366 rp_origins: vec!["http://localhost:8080".to_string()],
1367 });
1368 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1369 }
1370
1371 #[test]
1372 fn test_verify_authentication_challenge_mismatch() {
1373 let challenge = generate_challenge_bytes();
1374 let _challenge_b64 = base64_encode_urlsafe(&challenge);
1375
1376 let client_data = serde_json::json!({
1377 "type": "webauthn.get",
1378 "challenge": base64_encode_urlsafe(&[99u8; 32]),
1379 "origin": "http://localhost:8080",
1380 });
1381 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1382
1383 use sha2::Digest;
1384 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1385 let mut auth_data = Vec::new();
1386 auth_data.extend_from_slice(&rp_id_hash);
1387 auth_data.push(FLAG_UP);
1388 auth_data.extend_from_slice(&0u32.to_be_bytes());
1389 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1390
1391 let result = verify_authentication(&AuthenticationParams {
1392 challenge_bytes: challenge.clone(),
1393 client_data_json_b64: client_data_b64,
1394 authenticator_data_b64: auth_data_b64,
1395 signature_b64: "sig".to_string(),
1396 credential_id_b64: "cred".to_string(),
1397 public_key_cose: vec![0x10],
1398 current_sign_count: 0,
1399 allowed_credential_ids: vec!["cred".to_string()],
1400 rp_id: "localhost".to_string(),
1401 rp_origins: vec!["http://localhost:8080".to_string()],
1402 });
1403 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1404 }
1405
1406 #[test]
1407 fn test_verify_authentication_rs256_roundtrip() {
1408 let cose_key = build_cose_rsa_key(&[0xAA; 256], &[0x01, 0x00, 0x01]);
1413 let (alg, key) = crate::crypto::parse_cose_key(&cose_key).unwrap();
1414 assert_eq!(alg, COSE_ALG_RS256);
1415 let result = verify_cose_signature(COSE_ALG_RS256, &key, b"data", &[0u8; 256]);
1416 assert!(matches!(
1417 result,
1418 Err(WebauthnError::SignatureVerificationFailed)
1419 ));
1420 }
1421}