1use crate::challenge::check_sign_count;
25use crate::credential::{AuthenticationResult, RegistrationResult};
26use crate::crypto::{
27 alg_to_name, base64_decode_urlsafe, base64_encode_urlsafe, cbor_bytes, cbor_map_entries,
28 parse_cose_key, verify_cose_signature,
29};
30use crate::error::WebauthnError;
31
32#[derive(Debug, Clone)]
34struct AuthenticatorData {
35 rp_id_hash: Vec<u8>,
37 flags: u8,
39 sign_count: u32,
41 credential_public_key_cose: Option<Vec<u8>>,
43 credential_id: Option<Vec<u8>>,
45}
46
47const FLAG_UP: u8 = 0x01;
49const FLAG_UV: u8 = 0x04;
51const FLAG_AT: u8 = 0x40;
53#[allow(dead_code)]
55const FLAG_ED: u8 = 0x80;
56
57const AUTH_DATA_MIN_LEN: usize = 37;
59
60const ATTESTED_CREDENTIAL_PREFIX_LEN: usize = 18;
62
63fn parse_authenticator_data(auth_data: &[u8]) -> Result<AuthenticatorData, WebauthnError> {
76 if auth_data.len() < AUTH_DATA_MIN_LEN {
77 return Err(WebauthnError::VerificationFailed(format!(
78 "Authenticator data too short: {} bytes (minimum {})",
79 auth_data.len(),
80 AUTH_DATA_MIN_LEN
81 )));
82 }
83
84 let rp_id_hash = auth_data[..32].to_vec();
85 let flags = auth_data[32];
86 let sign_count =
87 u32::from_be_bytes(auth_data[33..37].try_into().map_err(|_| {
88 WebauthnError::VerificationFailed("sign count bytes invalid".to_string())
89 })?);
90
91 let mut offset = 37;
92 let mut credential_id = None;
93 let mut credential_public_key_cose = None;
94
95 if flags & FLAG_AT != 0 {
96 if auth_data.len() < offset + ATTESTED_CREDENTIAL_PREFIX_LEN {
97 return Err(WebauthnError::VerificationFailed(
98 "Attested credential data truncated (AAGUID + length)".to_string(),
99 ));
100 }
101 offset += 16; let cred_id_len = u16::from_be_bytes([auth_data[offset], auth_data[offset + 1]]) as usize;
104 offset += 2;
105
106 if auth_data.len() < offset + cred_id_len {
107 return Err(WebauthnError::VerificationFailed(
108 "Attested credential data truncated (credential ID)".to_string(),
109 ));
110 }
111 credential_id = Some(auth_data[offset..offset + cred_id_len].to_vec());
112 offset += cred_id_len;
113
114 if offset >= auth_data.len() {
115 return Err(WebauthnError::VerificationFailed(
116 "Attested credential data truncated (public key)".to_string(),
117 ));
118 }
119 credential_public_key_cose = Some(auth_data[offset..].to_vec());
120 }
121
122 Ok(AuthenticatorData {
123 rp_id_hash,
124 flags,
125 sign_count,
126 credential_public_key_cose,
127 credential_id,
128 })
129}
130
131fn validate_client_data(
136 client_data_json_b64: &str,
137 challenge_bytes: &[u8],
138 expected_type: &str,
139 rp_id: &str,
140 rp_origins: &[String],
141) -> Result<Vec<u8>, WebauthnError> {
142 let client_data_bytes = base64_decode_urlsafe(client_data_json_b64)?;
143 let client_data: serde_json::Value = serde_json::from_slice(&client_data_bytes)
144 .map_err(|e| WebauthnError::VerificationFailed(format!("client data parse error: {e}")))?;
145
146 let client_challenge = client_data
147 .get("challenge")
148 .and_then(|v| v.as_str())
149 .ok_or_else(|| {
150 WebauthnError::VerificationFailed("missing challenge in client data".to_string())
151 })?;
152 let client_challenge_bytes = base64_decode_urlsafe(client_challenge)?;
153 if client_challenge_bytes != challenge_bytes {
154 return Err(WebauthnError::VerificationFailed(
155 "challenge mismatch".to_string(),
156 ));
157 }
158
159 let typ = client_data
160 .get("type")
161 .and_then(|v| v.as_str())
162 .ok_or_else(|| {
163 WebauthnError::VerificationFailed("missing type in client data".to_string())
164 })?;
165 if typ != expected_type {
166 return Err(WebauthnError::VerificationFailed(format!(
167 "wrong type: {typ}"
168 )));
169 }
170
171 let origin = client_data
172 .get("origin")
173 .and_then(|v| v.as_str())
174 .ok_or_else(|| {
175 WebauthnError::VerificationFailed("missing origin in client data".to_string())
176 })?;
177 if !rp_origins.iter().any(|o| o == origin) {
178 return Err(WebauthnError::VerificationFailed(format!(
179 "origin '{origin}' not allowed"
180 )));
181 }
182
183 if let Some(rp) = client_data.get("rpId").and_then(|v| v.as_str()) {
185 if rp != rp_id {
186 return Err(WebauthnError::VerificationFailed(format!(
187 "rpId mismatch: client sent '{rp}', expected '{rp_id}'"
188 )));
189 }
190 }
191
192 Ok(client_data_bytes)
193}
194
195pub fn verify_registration(
214 challenge_bytes: &[u8],
215 client_data_json_b64: &str,
216 attestation_object_b64: &str,
217 existing_credential_id: &str,
218 rp_id: &str,
219 rp_origins: &[String],
220) -> Result<RegistrationResult, WebauthnError> {
221 validate_client_data(
222 client_data_json_b64,
223 challenge_bytes,
224 "webauthn.create",
225 rp_id,
226 rp_origins,
227 )?;
228
229 let attestation_bytes = base64_decode_urlsafe(attestation_object_b64)?;
230 let attestation_val: ciborium::Value = ciborium::de::from_reader(&attestation_bytes[..])
231 .map_err(|e| {
232 WebauthnError::AttestationError(format!("attestation object CBOR parse error: {e}"))
233 })?;
234
235 let attestation_entries = cbor_map_entries(&attestation_val).ok_or_else(|| {
236 WebauthnError::AttestationError("attestation object is not a CBOR map".to_string())
237 })?;
238
239 let mut fmt: Option<String> = None;
240 let mut auth_data_bytes: Option<Vec<u8>> = None;
241
242 for (key, val) in &attestation_entries {
243 match *key {
244 1 => {
245 if let ciborium::Value::Text(s) = val {
246 fmt = Some(s.clone());
247 }
248 }
249 2 => {
250 if let Some(b) = cbor_bytes(val) {
251 auth_data_bytes = Some(b);
252 }
253 }
254 _ => {}
255 }
256 }
257
258 let fmt = fmt.unwrap_or_else(|| "none".to_string());
259 let auth_data_bytes = auth_data_bytes.ok_or_else(|| {
260 WebauthnError::AttestationError("missing authData in attestation object".to_string())
261 })?;
262
263 let auth_data = parse_authenticator_data(&auth_data_bytes)?;
264
265 use sha2::Digest;
266 let computed_rp_id_hash = sha2::Sha256::digest(rp_id.as_bytes()).to_vec();
267 if auth_data.rp_id_hash != computed_rp_id_hash {
268 return Err(WebauthnError::VerificationFailed(format!(
269 "rpId hash mismatch: computed {:x?}, got {:x?}",
270 computed_rp_id_hash, auth_data.rp_id_hash
271 )));
272 }
273
274 if auth_data.flags & FLAG_UP == 0 {
275 return Err(WebauthnError::VerificationFailed(
276 "User Present flag not set".to_string(),
277 ));
278 }
279
280 if auth_data.flags & FLAG_AT == 0 {
281 return Err(WebauthnError::VerificationFailed(
282 "Attested Credential Data flag not set during registration".to_string(),
283 ));
284 }
285
286 let credential_id = auth_data.credential_id.ok_or_else(|| {
287 WebauthnError::VerificationFailed("no credential ID in attested data".to_string())
288 })?;
289 let credential_id_b64 = base64_encode_urlsafe(&credential_id);
290
291 if existing_credential_id == credential_id_b64 {
292 return Err(WebauthnError::DuplicateCredential(credential_id_b64));
293 }
294
295 let public_key_cose = auth_data.credential_public_key_cose.ok_or_else(|| {
296 WebauthnError::VerificationFailed("no public key in attested data".to_string())
297 })?;
298
299 let (alg, _cose_key) = parse_cose_key(&public_key_cose)?;
300
301 let user_verified = auth_data.flags & FLAG_UV != 0;
302
303 Ok(RegistrationResult {
304 credential_id: credential_id_b64,
305 device_name: format!("WebAuthn ({})", alg_to_name(alg)),
306 attestation_format: fmt,
307 user_verified,
308 })
309}
310
311#[derive(Debug, Clone)]
316pub struct AuthenticationParams {
317 pub challenge_bytes: Vec<u8>,
319 pub client_data_json_b64: String,
321 pub authenticator_data_b64: String,
323 pub signature_b64: String,
325 pub credential_id_b64: String,
327 pub public_key_cose: Vec<u8>,
329 pub current_sign_count: u32,
331 pub allowed_credential_ids: Vec<String>,
333 pub rp_id: String,
335 pub rp_origins: Vec<String>,
337}
338
339pub fn verify_authentication(
363 params: &AuthenticationParams,
364) -> Result<AuthenticationResult, WebauthnError> {
365 let AuthenticationParams {
366 challenge_bytes,
367 client_data_json_b64,
368 authenticator_data_b64,
369 signature_b64,
370 credential_id_b64,
371 public_key_cose,
372 current_sign_count,
373 allowed_credential_ids,
374 rp_id,
375 rp_origins,
376 } = params;
377
378 if !allowed_credential_ids.contains(credential_id_b64) {
379 return Err(WebauthnError::VerificationFailed(
380 "credential ID not in allowed list".to_string(),
381 ));
382 }
383
384 let client_data_bytes = validate_client_data(
385 client_data_json_b64,
386 challenge_bytes,
387 "webauthn.get",
388 rp_id,
389 rp_origins,
390 )?;
391
392 let authenticator_data = base64_decode_urlsafe(authenticator_data_b64)?;
393 let auth_data = parse_authenticator_data(&authenticator_data)?;
394
395 use sha2::Digest;
396 let computed_rp_id_hash = sha2::Sha256::digest(rp_id.as_bytes()).to_vec();
397 if auth_data.rp_id_hash != computed_rp_id_hash {
398 return Err(WebauthnError::VerificationFailed(format!(
399 "rpId hash mismatch: computed {:x?}, got {:x?}",
400 computed_rp_id_hash, auth_data.rp_id_hash
401 )));
402 }
403
404 if auth_data.flags & FLAG_UP == 0 {
405 return Err(WebauthnError::VerificationFailed(
406 "User Present flag not set".to_string(),
407 ));
408 }
409
410 check_sign_count(*current_sign_count, auth_data.sign_count)?;
411
412 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
413
414 let mut signed_data = Vec::with_capacity(authenticator_data.len() + 32);
415 signed_data.extend_from_slice(&authenticator_data);
416 signed_data.extend_from_slice(&client_data_hash);
417
418 let signature = base64_decode_urlsafe(signature_b64)?;
419
420 let (alg, cose_key) = parse_cose_key(public_key_cose)?;
421 verify_cose_signature(alg, &cose_key, &signed_data, &signature)?;
422
423 let user_verified = auth_data.flags & FLAG_UV != 0;
424 let new_sign_count = auth_data.sign_count;
425
426 Ok(AuthenticationResult {
427 credential_id: credential_id_b64.clone(),
428 new_sign_count,
429 user_verified,
430 })
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436 use crate::crypto::tests::{build_cose_ec2_key, build_cose_rsa_key};
437 use crate::crypto::{base64_encode_urlsafe, generate_challenge_bytes, COSE_ALG_RS256};
438 use ring::signature::KeyPair as _;
439
440 fn build_attestation_object(auth_data: &[u8]) -> Vec<u8> {
441 use ciborium::Value;
442 let map = vec![
443 (Value::Integer(1.into()), Value::Text("none".to_string())),
444 (Value::Integer(2.into()), Value::Bytes(auth_data.to_vec())),
445 (Value::Integer(3.into()), Value::Map(vec![])),
446 ];
447 let mut buf = Vec::new();
448 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
449 buf
450 }
451
452 fn build_auth_data_with_credential(
453 rp_id: &str,
454 flags: u8,
455 sign_count: u32,
456 credential_id: &[u8],
457 cose_key: &[u8],
458 ) -> Vec<u8> {
459 use sha2::Digest;
460 let rp_id_hash = sha2::Sha256::digest(rp_id.as_bytes()).to_vec();
461 let mut auth_data = Vec::new();
462 auth_data.extend_from_slice(&rp_id_hash);
463 auth_data.push(flags);
464 auth_data.extend_from_slice(&sign_count.to_be_bytes());
465
466 if flags & FLAG_AT != 0 {
467 auth_data.extend_from_slice(&[0u8; 16]);
468 auth_data.extend_from_slice(&(credential_id.len() as u16).to_be_bytes());
469 auth_data.extend_from_slice(credential_id);
470 auth_data.extend_from_slice(cose_key);
471 }
472
473 auth_data
474 }
475
476 #[test]
477 fn test_parse_authenticator_data_minimal() {
478 let mut auth_data = vec![0u8; 37];
479 auth_data[..32].copy_from_slice(&[0xAA; 32]);
480 auth_data[32] = FLAG_UP;
481 auth_data[33..37].copy_from_slice(&1u32.to_be_bytes());
482
483 let parsed = parse_authenticator_data(&auth_data).unwrap();
484 assert_eq!(parsed.rp_id_hash, vec![0xAA; 32]);
485 assert_eq!(parsed.flags, FLAG_UP);
486 assert_eq!(parsed.sign_count, 1);
487 assert!(parsed.credential_public_key_cose.is_none());
488 assert!(parsed.credential_id.is_none());
489 }
490
491 #[test]
492 fn test_parse_authenticator_data_with_attested_credential() {
493 let credential_id = vec![0x01, 0x02, 0x03, 0x04];
494 let public_key_cose = vec![0x10, 0x20, 0x30];
495
496 let total_len = 37 + 16 + 2 + credential_id.len() + public_key_cose.len();
497 let mut auth_data = vec![0u8; total_len];
498 auth_data[32] = FLAG_UP | FLAG_AT;
499 auth_data[33..37].copy_from_slice(&5u32.to_be_bytes());
500
501 let offset = 37 + 16;
502 auth_data[offset..offset + 2].copy_from_slice(&(credential_id.len() as u16).to_be_bytes());
503 auth_data[offset + 2..offset + 2 + credential_id.len()].copy_from_slice(&credential_id);
504 let pk_offset = offset + 2 + credential_id.len();
505 auth_data[pk_offset..].copy_from_slice(&public_key_cose);
506
507 let parsed = parse_authenticator_data(&auth_data).unwrap();
508 assert_eq!(parsed.flags, FLAG_UP | FLAG_AT);
509 assert_eq!(parsed.sign_count, 5);
510 assert_eq!(parsed.credential_id, Some(credential_id));
511 assert_eq!(parsed.credential_public_key_cose, Some(public_key_cose));
512 }
513
514 #[test]
515 fn test_parse_authenticator_data_too_short() {
516 let auth_data = vec![0u8; 10];
517 let result = parse_authenticator_data(&auth_data);
518 assert!(result.is_err());
519 }
520
521 #[test]
522 fn test_verify_registration_valid_es256() {
523 use ring::signature::EcdsaKeyPair;
524
525 let rng = ring::rand::SystemRandom::new();
526 let pkcs8 =
527 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
528 .unwrap();
529 let key_pair = EcdsaKeyPair::from_pkcs8(
530 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
531 pkcs8.as_ref(),
532 &rng,
533 )
534 .unwrap();
535
536 let pub_bytes = key_pair.public_key().as_ref().to_vec();
537 let x = pub_bytes[1..33].to_vec();
538 let y = pub_bytes[33..65].to_vec();
539 let cose_key = build_cose_ec2_key(&x, &y);
540
541 let credential_id = vec![0x01, 0x02, 0x03, 0x04];
542 let auth_data = build_auth_data_with_credential(
543 "localhost",
544 FLAG_UP | FLAG_AT,
545 0,
546 &credential_id,
547 &cose_key,
548 );
549 let att_obj = build_attestation_object(&auth_data);
550
551 let challenge = generate_challenge_bytes();
552 let challenge_b64 = base64_encode_urlsafe(&challenge);
553
554 let client_data = serde_json::json!({
555 "type": "webauthn.create",
556 "challenge": challenge_b64,
557 "origin": "http://localhost:8080",
558 });
559 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
560 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
561
562 let result = verify_registration(
563 &challenge,
564 &client_data_b64,
565 &att_obj_b64,
566 "different-id",
567 "localhost",
568 &["http://localhost:8080".to_string()],
569 )
570 .unwrap();
571
572 assert_eq!(result.credential_id, base64_encode_urlsafe(&credential_id));
573 assert!(result.attestation_format == "none");
574 }
575
576 #[test]
577 fn test_verify_registration_challenge_mismatch() {
578 let auth_data = build_auth_data_with_credential(
579 "localhost",
580 FLAG_UP | FLAG_AT,
581 0,
582 &[0x01],
583 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
584 );
585 let att_obj = build_attestation_object(&auth_data);
586
587 let challenge = generate_challenge_bytes();
588 let _challenge_b64 = base64_encode_urlsafe(&challenge);
589
590 let client_data = serde_json::json!({
591 "type": "webauthn.create",
592 "challenge": base64_encode_urlsafe(&[1u8; 32]),
593 "origin": "http://localhost:8080",
594 });
595 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
596 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
597
598 let result = verify_registration(
599 &challenge,
600 &client_data_b64,
601 &att_obj_b64,
602 "different",
603 "localhost",
604 &["http://localhost:8080".to_string()],
605 );
606 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
607 }
608
609 #[test]
610 fn test_verify_registration_wrong_type() {
611 let auth_data = build_auth_data_with_credential(
612 "localhost",
613 FLAG_UP | FLAG_AT,
614 0,
615 &[0x01],
616 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
617 );
618 let att_obj = build_attestation_object(&auth_data);
619
620 let challenge = generate_challenge_bytes();
621 let challenge_b64 = base64_encode_urlsafe(&challenge);
622
623 let client_data = serde_json::json!({
624 "type": "webauthn.get",
625 "challenge": challenge_b64,
626 "origin": "http://localhost:8080",
627 });
628 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
629 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
630
631 let result = verify_registration(
632 &challenge,
633 &client_data_b64,
634 &att_obj_b64,
635 "different",
636 "localhost",
637 &["http://localhost:8080".to_string()],
638 );
639 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
640 }
641
642 #[test]
643 fn test_verify_registration_wrong_origin() {
644 let auth_data = build_auth_data_with_credential(
645 "localhost",
646 FLAG_UP | FLAG_AT,
647 0,
648 &[0x01],
649 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
650 );
651 let att_obj = build_attestation_object(&auth_data);
652
653 let challenge = generate_challenge_bytes();
654 let challenge_b64 = base64_encode_urlsafe(&challenge);
655
656 let client_data = serde_json::json!({
657 "type": "webauthn.create",
658 "challenge": challenge_b64,
659 "origin": "http://evil.com",
660 });
661 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
662 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
663
664 let result = verify_registration(
665 &challenge,
666 &client_data_b64,
667 &att_obj_b64,
668 "different",
669 "localhost",
670 &["http://localhost:8080".to_string()],
671 );
672 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
673 }
674
675 #[test]
676 fn test_verify_registration_duplicate() {
677 let auth_data = build_auth_data_with_credential(
678 "localhost",
679 FLAG_UP | FLAG_AT,
680 0,
681 &[0x01, 0x02, 0x03],
682 &build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]),
683 );
684 let att_obj = build_attestation_object(&auth_data);
685
686 let challenge = generate_challenge_bytes();
687 let challenge_b64 = base64_encode_urlsafe(&challenge);
688
689 let client_data = serde_json::json!({
690 "type": "webauthn.create",
691 "challenge": challenge_b64,
692 "origin": "http://localhost:8080",
693 });
694 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
695 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
696 let cred_id_b64 = base64_encode_urlsafe(&[0x01, 0x02, 0x03]);
697
698 let result = verify_registration(
699 &challenge,
700 &client_data_b64,
701 &att_obj_b64,
702 &cred_id_b64,
703 "localhost",
704 &["http://localhost:8080".to_string()],
705 );
706 assert!(matches!(result, Err(WebauthnError::DuplicateCredential(_))));
707 }
708
709 #[test]
710 fn test_verify_registration_wrong_rp_id_hash() {
711 let credential_id = vec![0x01];
712 let cose_key = build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]);
713 let auth_data = build_auth_data_with_credential(
714 "evil.com",
715 FLAG_UP | FLAG_AT,
716 0,
717 &credential_id,
718 &cose_key,
719 );
720 let att_obj = build_attestation_object(&auth_data);
721
722 let challenge = generate_challenge_bytes();
723 let challenge_b64 = base64_encode_urlsafe(&challenge);
724
725 let client_data = serde_json::json!({
726 "type": "webauthn.create",
727 "challenge": challenge_b64,
728 "origin": "http://localhost:8080",
729 });
730 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
731 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
732
733 let result = verify_registration(
734 &challenge,
735 &client_data_b64,
736 &att_obj_b64,
737 "different",
738 "localhost",
739 &["http://localhost:8080".to_string()],
740 );
741 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
742 }
743
744 #[test]
745 fn test_verify_registration_missing_up_flag() {
746 let credential_id = vec![0x01];
747 let cose_key = build_cose_ec2_key(&[0xAA; 32], &[0xBB; 32]);
748 let auth_data =
749 build_auth_data_with_credential("localhost", FLAG_AT, 0, &credential_id, &cose_key);
750 let att_obj = build_attestation_object(&auth_data);
751
752 let challenge = generate_challenge_bytes();
753 let challenge_b64 = base64_encode_urlsafe(&challenge);
754
755 let client_data = serde_json::json!({
756 "type": "webauthn.create",
757 "challenge": challenge_b64,
758 "origin": "http://localhost:8080",
759 });
760 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
761 let att_obj_b64 = base64_encode_urlsafe(&att_obj);
762
763 let result = verify_registration(
764 &challenge,
765 &client_data_b64,
766 &att_obj_b64,
767 "different",
768 "localhost",
769 &["http://localhost:8080".to_string()],
770 );
771 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
772 }
773
774 #[test]
775 fn test_verify_authentication_valid_es256() {
776 use ring::signature::EcdsaKeyPair;
777
778 let rng = ring::rand::SystemRandom::new();
779 let pkcs8 =
780 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
781 .unwrap();
782 let key_pair = EcdsaKeyPair::from_pkcs8(
783 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
784 pkcs8.as_ref(),
785 &rng,
786 )
787 .unwrap();
788
789 let pub_bytes = key_pair.public_key().as_ref().to_vec();
790 let x = pub_bytes[1..33].to_vec();
791 let y = pub_bytes[33..65].to_vec();
792 let cose_key = build_cose_ec2_key(&x, &y);
793
794 let challenge = generate_challenge_bytes();
795 let challenge_b64 = base64_encode_urlsafe(&challenge);
796
797 use sha2::Digest;
798 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
799 let mut auth_data = Vec::new();
800 auth_data.extend_from_slice(&rp_id_hash);
801 auth_data.push(FLAG_UP);
802 auth_data.extend_from_slice(&1u32.to_be_bytes());
803
804 let client_data = serde_json::json!({
805 "type": "webauthn.get",
806 "challenge": challenge_b64,
807 "origin": "http://localhost:8080",
808 });
809 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
810 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
811 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
812
813 let mut signed_data = Vec::new();
814 signed_data.extend_from_slice(&auth_data);
815 signed_data.extend_from_slice(&client_data_hash);
816
817 let signature = key_pair.sign(&rng, &signed_data).unwrap();
818
819 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
820 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
821 let cred_id_b64 = base64_encode_urlsafe(&[0x01, 0x02]);
822
823 let result = verify_authentication(&AuthenticationParams {
824 challenge_bytes: challenge.clone(),
825 client_data_json_b64: client_data_b64,
826 authenticator_data_b64: auth_data_b64,
827 signature_b64: sig_b64,
828 credential_id_b64: cred_id_b64.clone(),
829 public_key_cose: cose_key,
830 current_sign_count: 0,
831 allowed_credential_ids: vec![cred_id_b64.clone()],
832 rp_id: "localhost".to_string(),
833 rp_origins: vec!["http://localhost:8080".to_string()],
834 })
835 .unwrap();
836
837 assert_eq!(result.credential_id, cred_id_b64);
838 assert_eq!(result.new_sign_count, 1);
839 }
840
841 #[test]
842 fn test_verify_authentication_wrong_signature() {
843 use ring::signature::EcdsaKeyPair;
844
845 let rng = ring::rand::SystemRandom::new();
846 let pkcs8 =
847 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
848 .unwrap();
849 let key_pair = EcdsaKeyPair::from_pkcs8(
850 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
851 pkcs8.as_ref(),
852 &rng,
853 )
854 .unwrap();
855
856 let pub_bytes = key_pair.public_key().as_ref().to_vec();
857 let x = pub_bytes[1..33].to_vec();
858 let y = pub_bytes[33..65].to_vec();
859 let cose_key = build_cose_ec2_key(&x, &y);
860
861 let challenge = generate_challenge_bytes();
862 let challenge_b64 = base64_encode_urlsafe(&challenge);
863
864 use sha2::Digest;
865 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
866 let mut auth_data = Vec::new();
867 auth_data.extend_from_slice(&rp_id_hash);
868 auth_data.push(FLAG_UP);
869 auth_data.extend_from_slice(&1u32.to_be_bytes());
870
871 let client_data = serde_json::json!({
872 "type": "webauthn.get",
873 "challenge": challenge_b64,
874 "origin": "http://localhost:8080",
875 });
876 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
877 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
878 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
879
880 let mut signed_data = Vec::new();
881 signed_data.extend_from_slice(&auth_data);
882 signed_data.extend_from_slice(&client_data_hash);
883
884 let mut wrong_sig = vec![0u8; 64];
885 wrong_sig[0] = 0xFF;
886
887 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
888 let sig_b64 = base64_encode_urlsafe(&wrong_sig);
889 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
890
891 let result = verify_authentication(&AuthenticationParams {
892 challenge_bytes: challenge.clone(),
893 client_data_json_b64: client_data_b64,
894 authenticator_data_b64: auth_data_b64,
895 signature_b64: sig_b64,
896 credential_id_b64: cred_id_b64.clone(),
897 public_key_cose: cose_key,
898 current_sign_count: 0,
899 allowed_credential_ids: vec![cred_id_b64],
900 rp_id: "localhost".to_string(),
901 rp_origins: vec!["http://localhost:8080".to_string()],
902 });
903 assert!(matches!(
904 result,
905 Err(WebauthnError::SignatureVerificationFailed)
906 ));
907 }
908
909 #[test]
910 fn test_verify_authentication_credential_not_allowed() {
911 let challenge = generate_challenge_bytes();
912 let challenge_b64 = base64_encode_urlsafe(&challenge);
913
914 let client_data = serde_json::json!({
915 "type": "webauthn.get",
916 "challenge": challenge_b64,
917 "origin": "http://localhost:8080",
918 });
919 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
920
921 use sha2::Digest;
922 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
923 let mut auth_data = Vec::new();
924 auth_data.extend_from_slice(&rp_id_hash);
925 auth_data.push(FLAG_UP);
926 auth_data.extend_from_slice(&0u32.to_be_bytes());
927 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
928
929 let result = verify_authentication(&AuthenticationParams {
930 challenge_bytes: challenge.clone(),
931 client_data_json_b64: client_data_b64,
932 authenticator_data_b64: auth_data_b64,
933 signature_b64: "sig".to_string(),
934 credential_id_b64: "unauthorized-cred".to_string(),
935 public_key_cose: vec![0x10, 0x20],
936 current_sign_count: 0,
937 allowed_credential_ids: vec!["allowed-cred".to_string()],
938 rp_id: "localhost".to_string(),
939 rp_origins: vec!["http://localhost:8080".to_string()],
940 });
941 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
942 }
943
944 #[test]
945 fn test_verify_authentication_wrong_origin() {
946 let challenge = generate_challenge_bytes();
947 let challenge_b64 = base64_encode_urlsafe(&challenge);
948
949 let client_data = serde_json::json!({
950 "type": "webauthn.get",
951 "challenge": challenge_b64,
952 "origin": "http://evil.com",
953 });
954 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
955
956 use sha2::Digest;
957 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
958 let mut auth_data = Vec::new();
959 auth_data.extend_from_slice(&rp_id_hash);
960 auth_data.push(FLAG_UP);
961 auth_data.extend_from_slice(&0u32.to_be_bytes());
962 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
963
964 let result = verify_authentication(&AuthenticationParams {
965 challenge_bytes: challenge.clone(),
966 client_data_json_b64: client_data_b64,
967 authenticator_data_b64: auth_data_b64,
968 signature_b64: "sig".to_string(),
969 credential_id_b64: "cred".to_string(),
970 public_key_cose: vec![0x10],
971 current_sign_count: 0,
972 allowed_credential_ids: vec!["cred".to_string()],
973 rp_id: "localhost".to_string(),
974 rp_origins: vec!["http://localhost:8080".to_string()],
975 });
976 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
977 }
978
979 #[test]
980 fn test_verify_authentication_wrong_type() {
981 let challenge = generate_challenge_bytes();
982 let challenge_b64 = base64_encode_urlsafe(&challenge);
983
984 let client_data = serde_json::json!({
985 "type": "webauthn.create",
986 "challenge": challenge_b64,
987 "origin": "http://localhost:8080",
988 });
989 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
990
991 use sha2::Digest;
992 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
993 let mut auth_data = Vec::new();
994 auth_data.extend_from_slice(&rp_id_hash);
995 auth_data.push(FLAG_UP);
996 auth_data.extend_from_slice(&0u32.to_be_bytes());
997 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
998
999 let result = verify_authentication(&AuthenticationParams {
1000 challenge_bytes: challenge.clone(),
1001 client_data_json_b64: client_data_b64,
1002 authenticator_data_b64: auth_data_b64,
1003 signature_b64: "sig".to_string(),
1004 credential_id_b64: "cred".to_string(),
1005 public_key_cose: vec![0x10],
1006 current_sign_count: 0,
1007 allowed_credential_ids: vec!["cred".to_string()],
1008 rp_id: "localhost".to_string(),
1009 rp_origins: vec!["http://localhost:8080".to_string()],
1010 });
1011 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1012 }
1013
1014 #[test]
1015 fn test_verify_authentication_sign_count_decrease() {
1016 use ring::signature::EcdsaKeyPair;
1017
1018 let rng = ring::rand::SystemRandom::new();
1019 let pkcs8 =
1020 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1021 .unwrap();
1022 let key_pair = EcdsaKeyPair::from_pkcs8(
1023 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1024 pkcs8.as_ref(),
1025 &rng,
1026 )
1027 .unwrap();
1028
1029 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1030 let x = pub_bytes[1..33].to_vec();
1031 let y = pub_bytes[33..65].to_vec();
1032 let cose_key = build_cose_ec2_key(&x, &y);
1033
1034 let challenge = generate_challenge_bytes();
1035 let challenge_b64 = base64_encode_urlsafe(&challenge);
1036
1037 use sha2::Digest;
1038 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1039 let mut auth_data = Vec::new();
1040 auth_data.extend_from_slice(&rp_id_hash);
1041 auth_data.push(FLAG_UP);
1042 auth_data.extend_from_slice(&5u32.to_be_bytes());
1043
1044 let client_data = serde_json::json!({
1045 "type": "webauthn.get",
1046 "challenge": challenge_b64,
1047 "origin": "http://localhost:8080",
1048 });
1049 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1050 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1051 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1052
1053 let mut signed_data = Vec::new();
1054 signed_data.extend_from_slice(&auth_data);
1055 signed_data.extend_from_slice(&client_data_hash);
1056
1057 let signature = key_pair.sign(&rng, &signed_data).unwrap();
1058
1059 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1060 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
1061 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
1062
1063 let result = verify_authentication(&AuthenticationParams {
1064 challenge_bytes: challenge.clone(),
1065 client_data_json_b64: client_data_b64,
1066 authenticator_data_b64: auth_data_b64,
1067 signature_b64: sig_b64,
1068 credential_id_b64: cred_id_b64.clone(),
1069 public_key_cose: cose_key,
1070 current_sign_count: 10,
1071 allowed_credential_ids: vec![cred_id_b64],
1072 rp_id: "localhost".to_string(),
1073 rp_origins: vec!["http://localhost:8080".to_string()],
1074 });
1075 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1076 }
1077
1078 #[test]
1079 fn test_verify_authentication_wrong_rp_id_hash() {
1080 let challenge = generate_challenge_bytes();
1081 let challenge_b64 = base64_encode_urlsafe(&challenge);
1082
1083 let client_data = serde_json::json!({
1084 "type": "webauthn.get",
1085 "challenge": challenge_b64,
1086 "origin": "http://localhost:8080",
1087 });
1088 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1089
1090 use sha2::Digest;
1091 let rp_id_hash = sha2::Sha256::digest(b"evil.com").to_vec();
1092 let mut auth_data = Vec::new();
1093 auth_data.extend_from_slice(&rp_id_hash);
1094 auth_data.push(FLAG_UP);
1095 auth_data.extend_from_slice(&0u32.to_be_bytes());
1096 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1097
1098 let result = verify_authentication(&AuthenticationParams {
1099 challenge_bytes: challenge.clone(),
1100 client_data_json_b64: client_data_b64,
1101 authenticator_data_b64: auth_data_b64,
1102 signature_b64: "sig".to_string(),
1103 credential_id_b64: "cred".to_string(),
1104 public_key_cose: vec![0x10],
1105 current_sign_count: 0,
1106 allowed_credential_ids: vec!["cred".to_string()],
1107 rp_id: "localhost".to_string(),
1108 rp_origins: vec!["http://localhost:8080".to_string()],
1109 });
1110 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1111 }
1112
1113 #[test]
1114 fn test_verify_authentication_missing_up_flag() {
1115 use ring::signature::EcdsaKeyPair;
1116
1117 let rng = ring::rand::SystemRandom::new();
1118 let pkcs8 =
1119 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1120 .unwrap();
1121 let key_pair = EcdsaKeyPair::from_pkcs8(
1122 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1123 pkcs8.as_ref(),
1124 &rng,
1125 )
1126 .unwrap();
1127
1128 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1129 let x = pub_bytes[1..33].to_vec();
1130 let y = pub_bytes[33..65].to_vec();
1131 let cose_key = build_cose_ec2_key(&x, &y);
1132
1133 let challenge = generate_challenge_bytes();
1134 let challenge_b64 = base64_encode_urlsafe(&challenge);
1135
1136 use sha2::Digest;
1137 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1138 let mut auth_data = Vec::new();
1139 auth_data.extend_from_slice(&rp_id_hash);
1140 auth_data.push(0);
1141 auth_data.extend_from_slice(&1u32.to_be_bytes());
1142
1143 let client_data = serde_json::json!({
1144 "type": "webauthn.get",
1145 "challenge": challenge_b64,
1146 "origin": "http://localhost:8080",
1147 });
1148 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1149 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1150 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1151
1152 let mut signed_data = Vec::new();
1153 signed_data.extend_from_slice(&auth_data);
1154 signed_data.extend_from_slice(&client_data_hash);
1155
1156 let signature = key_pair.sign(&rng, &signed_data).unwrap();
1157
1158 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1159 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
1160 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
1161
1162 let result = verify_authentication(&AuthenticationParams {
1163 challenge_bytes: challenge.clone(),
1164 client_data_json_b64: client_data_b64,
1165 authenticator_data_b64: auth_data_b64,
1166 signature_b64: sig_b64,
1167 credential_id_b64: cred_id_b64.clone(),
1168 public_key_cose: cose_key,
1169 current_sign_count: 0,
1170 allowed_credential_ids: vec![cred_id_b64],
1171 rp_id: "localhost".to_string(),
1172 rp_origins: vec!["http://localhost:8080".to_string()],
1173 });
1174 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1175 }
1176
1177 #[test]
1178 fn test_verify_authentication_wrong_rp_id_in_client_data() {
1179 use ring::signature::EcdsaKeyPair;
1180
1181 let rng = ring::rand::SystemRandom::new();
1182 let pkcs8 =
1183 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1184 .unwrap();
1185 let key_pair = EcdsaKeyPair::from_pkcs8(
1186 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
1187 pkcs8.as_ref(),
1188 &rng,
1189 )
1190 .unwrap();
1191
1192 let pub_bytes = key_pair.public_key().as_ref().to_vec();
1193 let x = pub_bytes[1..33].to_vec();
1194 let y = pub_bytes[33..65].to_vec();
1195 let cose_key = build_cose_ec2_key(&x, &y);
1196
1197 let challenge = generate_challenge_bytes();
1198 let challenge_b64 = base64_encode_urlsafe(&challenge);
1199
1200 let client_data = serde_json::json!({
1201 "type": "webauthn.get",
1202 "challenge": challenge_b64,
1203 "origin": "http://localhost:8080",
1204 "rpId": "evil.com",
1205 });
1206 let client_data_bytes = serde_json::to_vec(&client_data).unwrap();
1207 let client_data_b64 = base64_encode_urlsafe(&client_data_bytes);
1208
1209 use sha2::Digest;
1210 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1211 let mut auth_data = Vec::new();
1212 auth_data.extend_from_slice(&rp_id_hash);
1213 auth_data.push(FLAG_UP);
1214 auth_data.extend_from_slice(&1u32.to_be_bytes());
1215
1216 let client_data_hash = sha2::Sha256::digest(&client_data_bytes).to_vec();
1217 let mut signed_data = Vec::new();
1218 signed_data.extend_from_slice(&auth_data);
1219 signed_data.extend_from_slice(&client_data_hash);
1220
1221 let signature = key_pair.sign(&rng, &signed_data).unwrap();
1222
1223 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1224 let sig_b64 = base64_encode_urlsafe(signature.as_ref());
1225 let cred_id_b64 = base64_encode_urlsafe(&[0x01]);
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_b64,
1232 credential_id_b64: cred_id_b64.clone(),
1233 public_key_cose: cose_key,
1234 current_sign_count: 0,
1235 allowed_credential_ids: vec![cred_id_b64],
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_challenge_mismatch() {
1244 let challenge = generate_challenge_bytes();
1245 let _challenge_b64 = base64_encode_urlsafe(&challenge);
1246
1247 let client_data = serde_json::json!({
1248 "type": "webauthn.get",
1249 "challenge": base64_encode_urlsafe(&[99u8; 32]),
1250 "origin": "http://localhost:8080",
1251 });
1252 let client_data_b64 = base64_encode_urlsafe(&serde_json::to_vec(&client_data).unwrap());
1253
1254 use sha2::Digest;
1255 let rp_id_hash = sha2::Sha256::digest(b"localhost").to_vec();
1256 let mut auth_data = Vec::new();
1257 auth_data.extend_from_slice(&rp_id_hash);
1258 auth_data.push(FLAG_UP);
1259 auth_data.extend_from_slice(&0u32.to_be_bytes());
1260 let auth_data_b64 = base64_encode_urlsafe(&auth_data);
1261
1262 let result = verify_authentication(&AuthenticationParams {
1263 challenge_bytes: challenge.clone(),
1264 client_data_json_b64: client_data_b64,
1265 authenticator_data_b64: auth_data_b64,
1266 signature_b64: "sig".to_string(),
1267 credential_id_b64: "cred".to_string(),
1268 public_key_cose: vec![0x10],
1269 current_sign_count: 0,
1270 allowed_credential_ids: vec!["cred".to_string()],
1271 rp_id: "localhost".to_string(),
1272 rp_origins: vec!["http://localhost:8080".to_string()],
1273 });
1274 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
1275 }
1276
1277 #[test]
1278 fn test_verify_authentication_rs256_roundtrip() {
1279 let cose_key = build_cose_rsa_key(&[0xAA; 256], &[0x01, 0x00, 0x01]);
1284 let (alg, key) = crate::crypto::parse_cose_key(&cose_key).unwrap();
1285 assert_eq!(alg, COSE_ALG_RS256);
1286 let result = verify_cose_signature(COSE_ALG_RS256, &key, b"data", &[0u8; 256]);
1287 assert!(matches!(
1288 result,
1289 Err(WebauthnError::SignatureVerificationFailed)
1290 ));
1291 }
1292}