1use std::collections::HashMap;
26use std::io::Cursor;
27
28use ciborium::value::Value;
29
30use crate::algorithm::{
31 COSE_CRV_ED25519, COSE_CRV_P256, COSE_CRV_P384, COSE_EDDSA, COSE_ES256, COSE_ES384,
32 COSE_KTY_EC2, COSE_KTY_OKP, COSE_KTY_RSA, COSE_RS256,
33};
34use crate::error::{Result, WebAuthnError};
35
36const FLAG_UP: u8 = 0x01;
40const FLAG_UV: u8 = 0x04;
42const FLAG_BE: u8 = 0x08;
44const FLAG_BS: u8 = 0x10;
46const FLAG_AT: u8 = 0x40;
48const FLAG_ED: u8 = 0x80;
50
51const MAX_CREDENTIAL_ID_LEN: usize = 1023;
53
54const MIN_RSA_N_LEN: usize = 256;
56
57#[derive(Debug, Clone, Copy)]
61pub struct AuthenticatorFlags {
62 pub user_present: bool,
64 pub user_verified: bool,
66 pub backup_eligible: bool,
69 pub backup_state: bool,
71 pub attested_credential_data: bool,
73 pub extension_data: bool,
75}
76
77#[derive(Debug, Clone)]
87pub enum CoseKey {
88 EC2 {
90 alg: i64,
92 crv: i64,
94 x: Vec<u8>,
96 y: Vec<u8>,
98 },
99 OKP {
101 alg: i64,
103 x: Vec<u8>,
105 },
106 RSA {
108 alg: i64,
110 n: Vec<u8>,
112 e: Vec<u8>,
114 },
115}
116
117impl CoseKey {
118 pub fn alg(&self) -> i64 {
120 match self {
121 CoseKey::EC2 { alg, .. } => *alg,
122 CoseKey::OKP { alg, .. } => *alg,
123 CoseKey::RSA { alg, .. } => *alg,
124 }
125 }
126}
127
128#[derive(Debug)]
130pub struct AttestedCredentialData {
131 pub aaguid: [u8; 16],
134
135 pub credential_id: Vec<u8>,
137
138 pub public_key: CoseKey,
140}
141
142#[derive(Debug)]
144pub struct AuthenticatorData {
145 pub rp_id_hash: [u8; 32],
147
148 pub flags: AuthenticatorFlags,
150
151 pub sign_count: u32,
153
154 pub attested_credential_data: Option<AttestedCredentialData>,
156
157 pub extensions: Option<HashMap<String, Value>>,
162
163 pub raw: Vec<u8>,
168}
169
170pub fn parse_authenticator_data(data: &[u8]) -> Result<AuthenticatorData> {
179 if data.len() < 37 {
181 return Err(WebAuthnError::InvalidAuthenticatorData(format!(
182 "too short: expected at least 37 bytes, got {}",
183 data.len()
184 )));
185 }
186
187 let rp_id_hash: [u8; 32] = data[0..32]
189 .try_into()
190 .expect("slice of exactly 32 bytes always converts");
191
192 let flags_byte = data[32];
194 let flags = AuthenticatorFlags {
195 user_present: flags_byte & FLAG_UP != 0,
196 user_verified: flags_byte & FLAG_UV != 0,
197 backup_eligible: flags_byte & FLAG_BE != 0,
198 backup_state: flags_byte & FLAG_BS != 0,
199 attested_credential_data: flags_byte & FLAG_AT != 0,
200 extension_data: flags_byte & FLAG_ED != 0,
201 };
202
203 if flags.backup_state && !flags.backup_eligible {
206 return Err(WebAuthnError::InvalidAuthenticatorData(
207 "BS flag (backup state) is set but BE flag (backup eligibility) is not — invalid per §6.1".to_string(),
208 ));
209 }
210
211 let sign_count = u32::from_be_bytes(
213 data[33..37]
214 .try_into()
215 .expect("slice of exactly 4 bytes always converts"),
216 );
217
218 let (attested_credential_data, at_bytes_consumed) = if flags.attested_credential_data {
220 let (cred_data, n) = parse_attested_credential_data(&data[37..])?;
221 (Some(cred_data), n)
222 } else {
223 (None, 0)
224 };
225
226 let extensions = if flags.extension_data {
230 let ext_start = 37 + at_bytes_consumed;
231 Some(parse_extension_map(&data[ext_start..])?)
232 } else {
233 None
234 };
235
236 Ok(AuthenticatorData {
237 rp_id_hash,
238 flags,
239 sign_count,
240 attested_credential_data,
241 extensions,
242 raw: data.to_vec(),
243 })
244}
245
246pub fn parse_cose_key(data: &[u8]) -> Result<CoseKey> {
259 let value: Value = ciborium::from_reader(data)
260 .map_err(|e| WebAuthnError::CborDecodeError(format!("COSE key: {e}")))?;
261
262 let map = match value {
263 Value::Map(m) => m,
264 _ => {
265 return Err(WebAuthnError::InvalidPublicKey(
266 "COSE key must be a CBOR map".to_string(),
267 ))
268 }
269 };
270
271 {
273 let mut seen: Vec<i64> = Vec::new();
274 for (k, _) in &map {
275 if let Value::Integer(ki) = k {
276 if let Ok(n) = i64::try_from(*ki) {
277 if seen.contains(&n) {
278 return Err(WebAuthnError::InvalidPublicKey(format!(
279 "duplicate COSE key: {n}"
280 )));
281 }
282 seen.push(n);
283 }
284 }
285 }
286 }
287
288 let get_int = |key: i64| -> Option<i64> {
289 map.iter().find_map(|(k, v)| {
290 if let (Value::Integer(ki), Value::Integer(vi)) = (k, v) {
291 if i64::try_from(*ki).ok()? == key {
292 return i64::try_from(*vi).ok();
293 }
294 }
295 None
296 })
297 };
298
299 let get_bytes = |key: i64| -> Option<Vec<u8>> {
300 map.iter().find_map(|(k, v)| {
301 if let Value::Integer(ki) = k {
302 if i64::try_from(*ki).ok()? == key {
303 if let Value::Bytes(b) = v {
304 return Some(b.clone());
305 }
306 }
307 }
308 None
309 })
310 };
311
312 let kty = get_int(1).ok_or_else(|| {
314 WebAuthnError::InvalidPublicKey("missing required field: kty".to_string())
315 })?;
316
317 if kty == COSE_KTY_OKP {
318 parse_okp_key(&get_int, &get_bytes)
319 } else if kty == COSE_KTY_EC2 {
320 parse_ec2_key(&get_int, &get_bytes)
321 } else if kty == COSE_KTY_RSA {
322 parse_rsa_key(&get_int, &get_bytes)
323 } else {
324 Err(WebAuthnError::InvalidPublicKey(format!(
325 "unsupported key type: {kty} (supported: OKP=1, EC2=2, RSA=3)"
326 )))
327 }
328}
329
330fn parse_okp_key(
333 get_int: &impl Fn(i64) -> Option<i64>,
334 get_bytes: &impl Fn(i64) -> Option<Vec<u8>>,
335) -> Result<CoseKey> {
336 let alg = get_int(3).ok_or_else(|| {
338 WebAuthnError::InvalidPublicKey("missing required field: alg".to_string())
339 })?;
340
341 if alg != COSE_EDDSA {
342 return Err(WebAuthnError::UnsupportedAlgorithm(alg));
343 }
344
345 let crv = get_int(-1).ok_or_else(|| {
347 WebAuthnError::InvalidPublicKey("missing required field: crv".to_string())
348 })?;
349
350 if crv != COSE_CRV_ED25519 {
351 return Err(WebAuthnError::InvalidPublicKey(format!(
352 "unsupported OKP curve: {crv} (only Ed25519 / crv=6 is supported)"
353 )));
354 }
355
356 let x = get_bytes(-2)
358 .ok_or_else(|| WebAuthnError::InvalidPublicKey("missing required field: x".to_string()))?;
359
360 if x.len() != 32 {
361 return Err(WebAuthnError::InvalidPublicKey(format!(
362 "Ed25519 public key must be 32 bytes, got {}",
363 x.len()
364 )));
365 }
366
367 Ok(CoseKey::OKP { alg, x })
368}
369
370fn parse_ec2_key(
371 get_int: &impl Fn(i64) -> Option<i64>,
372 get_bytes: &impl Fn(i64) -> Option<Vec<u8>>,
373) -> Result<CoseKey> {
374 let alg = get_int(3).ok_or_else(|| {
377 WebAuthnError::InvalidPublicKey("missing required field: alg".to_string())
378 })?;
379
380 let crv = get_int(-1).ok_or_else(|| {
382 WebAuthnError::InvalidPublicKey("missing required field: crv".to_string())
383 })?;
384
385 let coord_size: usize = match alg {
386 COSE_ES256 => {
387 if crv != COSE_CRV_P256 {
389 return Err(WebAuthnError::InvalidPublicKey(format!(
390 "ES256 requires curve P-256 (crv=1), got {crv}"
391 )));
392 }
393 32
394 }
395 COSE_ES384 => {
396 if crv != COSE_CRV_P384 {
398 return Err(WebAuthnError::InvalidPublicKey(format!(
399 "ES384 requires curve P-384 (crv=2), got {crv}"
400 )));
401 }
402 48
403 }
404 _ => return Err(WebAuthnError::UnsupportedAlgorithm(alg)),
405 };
406
407 let x = get_bytes(-2)
409 .ok_or_else(|| WebAuthnError::InvalidPublicKey("missing required field: x".to_string()))?;
410
411 let y = get_bytes(-3)
413 .ok_or_else(|| WebAuthnError::InvalidPublicKey("missing required field: y".to_string()))?;
414
415 if x.len() != coord_size {
416 return Err(WebAuthnError::InvalidPublicKey(format!(
417 "x coordinate must be {coord_size} bytes, got {}",
418 x.len()
419 )));
420 }
421 if y.len() != coord_size {
422 return Err(WebAuthnError::InvalidPublicKey(format!(
423 "y coordinate must be {coord_size} bytes, got {}",
424 y.len()
425 )));
426 }
427
428 Ok(CoseKey::EC2 { alg, crv, x, y })
429}
430
431fn parse_rsa_key(
432 get_int: &impl Fn(i64) -> Option<i64>,
433 get_bytes: &impl Fn(i64) -> Option<Vec<u8>>,
434) -> Result<CoseKey> {
435 let alg = get_int(3).ok_or_else(|| {
437 WebAuthnError::InvalidPublicKey("missing required field: alg".to_string())
438 })?;
439
440 if alg != COSE_RS256 {
441 return Err(WebAuthnError::UnsupportedAlgorithm(alg));
442 }
443
444 let n = get_bytes(-1)
446 .ok_or_else(|| WebAuthnError::InvalidPublicKey("missing required field: n".to_string()))?;
447
448 if n.len() < MIN_RSA_N_LEN {
449 return Err(WebAuthnError::InvalidPublicKey(format!(
450 "RSA modulus must be at least {} bytes (2048-bit), got {}",
451 MIN_RSA_N_LEN,
452 n.len()
453 )));
454 }
455
456 let e = get_bytes(-2)
458 .ok_or_else(|| WebAuthnError::InvalidPublicKey("missing required field: e".to_string()))?;
459
460 if e.is_empty() {
461 return Err(WebAuthnError::InvalidPublicKey(
462 "RSA exponent (e) must not be empty".to_string(),
463 ));
464 }
465
466 Ok(CoseKey::RSA { alg, n, e })
467}
468
469fn parse_attested_credential_data(data: &[u8]) -> Result<(AttestedCredentialData, usize)> {
472 if data.len() < 18 {
474 return Err(WebAuthnError::InvalidAuthenticatorData(format!(
475 "attested credential data too short: expected at least 18 bytes after flags/counter, got {}",
476 data.len()
477 )));
478 }
479
480 let mut offset = 0usize;
481
482 let aaguid: [u8; 16] = data
484 .get(offset..offset + 16)
485 .ok_or_else(|| {
486 WebAuthnError::InvalidAuthenticatorData("truncated before aaguid".to_string())
487 })?
488 .try_into()
489 .expect("slice of exactly 16 bytes always converts");
490 offset += 16;
491
492 let cred_id_len = u16::from_be_bytes([
494 *data.get(offset).ok_or_else(|| {
495 WebAuthnError::InvalidAuthenticatorData(
496 "truncated before credentialIdLength high byte".to_string(),
497 )
498 })?,
499 *data.get(offset + 1).ok_or_else(|| {
500 WebAuthnError::InvalidAuthenticatorData(
501 "truncated before credentialIdLength low byte".to_string(),
502 )
503 })?,
504 ]) as usize;
505 offset += 2;
506
507 if cred_id_len == 0 {
508 return Err(WebAuthnError::InvalidAuthenticatorData(
509 "credentialIdLength is 0 — empty credential ID is not valid".to_string(),
510 ));
511 }
512
513 if cred_id_len > MAX_CREDENTIAL_ID_LEN {
514 return Err(WebAuthnError::InvalidAuthenticatorData(format!(
515 "credentialIdLength {cred_id_len} exceeds maximum {MAX_CREDENTIAL_ID_LEN} — likely corrupt data"
516 )));
517 }
518
519 let credential_id = data
520 .get(offset..offset + cred_id_len)
521 .ok_or_else(|| {
522 WebAuthnError::InvalidAuthenticatorData(format!(
523 "credentialIdLength ({cred_id_len}) extends past end of buffer"
524 ))
525 })?
526 .to_vec();
527 offset += cred_id_len;
528
529 let remaining = data.get(offset..).ok_or_else(|| {
533 WebAuthnError::InvalidAuthenticatorData("truncated before public key CBOR".to_string())
534 })?;
535
536 if remaining.is_empty() {
537 return Err(WebAuthnError::InvalidAuthenticatorData(
538 "no bytes remaining for credentialPublicKey CBOR".to_string(),
539 ));
540 }
541
542 let mut cursor = Cursor::new(remaining);
545 let _: Value = ciborium::from_reader(&mut cursor)
546 .map_err(|e| WebAuthnError::CborDecodeError(format!("COSE key: {e}")))?;
547 let cbor_len = cursor.position() as usize;
548
549 let public_key = parse_cose_key(&remaining[..cbor_len])?;
550
551 Ok((
552 AttestedCredentialData {
553 aaguid,
554 credential_id,
555 public_key,
556 },
557 offset + cbor_len,
558 ))
559}
560
561fn parse_extension_map(data: &[u8]) -> Result<HashMap<String, Value>> {
568 if data.is_empty() {
569 return Err(WebAuthnError::InvalidAuthenticatorData(
570 "ED flag is set but no extension bytes are present".to_string(),
571 ));
572 }
573
574 let value: Value = ciborium::from_reader(data)
575 .map_err(|e| WebAuthnError::CborDecodeError(format!("extension map: {e}")))?;
576
577 let map = match value {
578 Value::Map(m) => m,
579 _ => {
580 return Err(WebAuthnError::InvalidAuthenticatorData(
581 "extension data must be a CBOR map".to_string(),
582 ))
583 }
584 };
585
586 let mut extensions = HashMap::new();
587 for (k, v) in map {
588 if let Value::Text(key) = k {
590 extensions.insert(key, v);
591 }
592 }
593
594 Ok(extensions)
595}
596
597#[cfg(test)]
600mod tests {
601 use super::*;
602 use ciborium::value::Value;
603
604 pub fn make_auth_data(
606 rp_id_hash: &[u8; 32],
607 flags: u8,
608 sign_count: u32,
609 cred_data: Option<&[u8]>,
610 ) -> Vec<u8> {
611 let mut out = Vec::new();
612 out.extend_from_slice(rp_id_hash);
613 out.push(flags);
614 out.extend_from_slice(&sign_count.to_be_bytes());
615 if let Some(cd) = cred_data {
616 out.extend_from_slice(cd);
617 }
618 out
619 }
620
621 fn make_cose_key_cbor(x: &[u8], y: &[u8]) -> Vec<u8> {
622 make_cose_key_cbor_with_alg_crv(x, y, -7, 1)
623 }
624
625 fn make_cose_key_cbor_with_alg_crv(x: &[u8], y: &[u8], alg: i64, crv: i64) -> Vec<u8> {
626 let cose = Value::Map(vec![
627 (Value::Integer(1i64.into()), Value::Integer(2i64.into())),
628 (Value::Integer(3i64.into()), Value::Integer(alg.into())),
629 (Value::Integer((-1i64).into()), Value::Integer(crv.into())),
630 (Value::Integer((-2i64).into()), Value::Bytes(x.to_vec())),
631 (Value::Integer((-3i64).into()), Value::Bytes(y.to_vec())),
632 ]);
633 let mut buf = Vec::new();
634 ciborium::into_writer(&cose, &mut buf).expect("test setup");
635 buf
636 }
637
638 fn make_rsa_cose_key_cbor(n: &[u8], e: &[u8]) -> Vec<u8> {
639 let cose = Value::Map(vec![
640 (Value::Integer(1i64.into()), Value::Integer(3i64.into())), (
642 Value::Integer(3i64.into()),
643 Value::Integer((-257i64).into()),
644 ), (Value::Integer((-1i64).into()), Value::Bytes(n.to_vec())), (Value::Integer((-2i64).into()), Value::Bytes(e.to_vec())), ]);
648 let mut buf = Vec::new();
649 ciborium::into_writer(&cose, &mut buf).expect("test setup");
650 buf
651 }
652
653 fn make_attested_cred_data(cred_id: &[u8], pk_cbor: &[u8]) -> Vec<u8> {
654 let mut out = vec![0u8; 16]; out.extend_from_slice(&(cred_id.len() as u16).to_be_bytes());
656 out.extend_from_slice(cred_id);
657 out.extend_from_slice(pk_cbor);
658 out
659 }
660
661 #[test]
664 fn parses_be_flag() {
665 let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_BE, 0, None);
666 let parsed = parse_authenticator_data(&data).expect("test setup");
667 assert!(parsed.flags.backup_eligible);
668 assert!(!parsed.flags.backup_state);
669 }
670
671 #[test]
672 fn parses_be_and_bs_flags() {
673 let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_BE | FLAG_BS, 0, None);
674 let parsed = parse_authenticator_data(&data).expect("test setup");
675 assert!(parsed.flags.backup_eligible);
676 assert!(parsed.flags.backup_state);
677 }
678
679 #[test]
680 fn rejects_bs_without_be() {
681 let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_BS, 0, None);
683 let err = parse_authenticator_data(&data).expect_err("expected error");
684 assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
685 assert!(err.to_string().contains("BS"));
686 }
687
688 #[test]
691 fn rejects_too_short() {
692 let result = parse_authenticator_data(&[0u8; 10]);
693 assert!(matches!(
694 result,
695 Err(WebAuthnError::InvalidAuthenticatorData(_))
696 ));
697 }
698
699 #[test]
700 fn rejects_exactly_36_bytes() {
701 let result = parse_authenticator_data(&[0u8; 36]);
702 assert!(matches!(
703 result,
704 Err(WebAuthnError::InvalidAuthenticatorData(_))
705 ));
706 }
707
708 #[test]
709 fn rejects_empty() {
710 let result = parse_authenticator_data(&[]);
711 assert!(matches!(
712 result,
713 Err(WebAuthnError::InvalidAuthenticatorData(_))
714 ));
715 }
716
717 #[test]
718 fn parses_minimal_auth_data() {
719 let rp_hash = [0xABu8; 32];
720 let data = make_auth_data(&rp_hash, FLAG_UP, 42, None);
721 let parsed = parse_authenticator_data(&data).expect("test setup");
722
723 assert_eq!(parsed.rp_id_hash, rp_hash);
724 assert!(parsed.flags.user_present);
725 assert!(!parsed.flags.user_verified);
726 assert_eq!(parsed.sign_count, 42);
727 assert!(parsed.attested_credential_data.is_none());
728 assert_eq!(parsed.raw, data);
729 }
730
731 #[test]
732 fn parses_up_and_uv_flags() {
733 let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_UV, 0, None);
734 let parsed = parse_authenticator_data(&data).expect("test setup");
735 assert!(parsed.flags.user_present);
736 assert!(parsed.flags.user_verified);
737 }
738
739 #[test]
740 fn raw_field_equals_input_bytes() {
741 let data = make_auth_data(&[0u8; 32], FLAG_UP, 7, None);
742 let parsed = parse_authenticator_data(&data).expect("test setup");
743 assert_eq!(parsed.raw, data);
744 }
745
746 #[test]
747 fn all_flags_set_parses_without_panic() {
748 let data = make_auth_data(&[0u8; 32], 0xFF, 0, None);
751 let result = parse_authenticator_data(&data);
752 assert!(result.is_err());
753 }
754
755 #[test]
756 fn at_flag_set_but_no_data_returns_error() {
757 let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT, 0, None);
759 let err = parse_authenticator_data(&data).expect_err("expected error");
760 assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
761 }
762
763 #[test]
766 fn rejects_zero_length_credential_id() {
767 let pk = make_cose_key_cbor(&[0x01u8; 32], &[0x02u8; 32]);
768 let cred_data = make_attested_cred_data(&[], &pk); let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT, 0, Some(&cred_data));
770 let err = parse_authenticator_data(&data).expect_err("expected error");
771 assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
772 assert!(err.to_string().contains("0"));
774 }
775
776 #[test]
777 fn rejects_credential_id_too_large() {
778 let mut cred_data = vec![0u8; 16]; let oversized_len: u16 = 1024;
781 cred_data.extend_from_slice(&oversized_len.to_be_bytes());
782 let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT, 0, Some(&cred_data));
784 let err = parse_authenticator_data(&data).expect_err("expected error");
785 assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
786 assert!(err.to_string().contains("1024"));
787 }
788
789 #[test]
790 fn rejects_credential_id_extends_past_buffer() {
791 let mut cred_data = vec![0u8; 16]; cred_data.extend_from_slice(&50u16.to_be_bytes()); cred_data.extend_from_slice(&[0xABu8; 10]); let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT, 0, Some(&cred_data));
795 let err = parse_authenticator_data(&data).expect_err("expected error");
796 assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
797 }
798
799 #[test]
800 fn rejects_empty_cbor_after_credential_id() {
801 let mut cred_data = vec![0u8; 16]; cred_data.extend_from_slice(&1u16.to_be_bytes());
803 cred_data.push(0xAB); let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT, 0, Some(&cred_data));
806 let err = parse_authenticator_data(&data).expect_err("expected error");
807 assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
808 }
809
810 fn make_extension_cbor(entries: &[(&str, Value)]) -> Vec<u8> {
813 let map = Value::Map(
814 entries
815 .iter()
816 .map(|(k, v)| (Value::Text(k.to_string()), v.clone()))
817 .collect(),
818 );
819 let mut buf = Vec::new();
820 ciborium::into_writer(&map, &mut buf).expect("test setup");
821 buf
822 }
823
824 #[test]
825 fn ed_flag_not_set_produces_none_extensions() {
826 let data = make_auth_data(&[0u8; 32], FLAG_UP, 1, None);
827 let parsed = parse_authenticator_data(&data).expect("test setup");
828 assert!(parsed.extensions.is_none());
829 }
830
831 #[test]
832 fn ed_flag_set_with_valid_map_populates_extensions() {
833 let ext_cbor = make_extension_cbor(&[("appid", Value::Bool(true))]);
834 let mut data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_ED, 0, None);
835 data.extend_from_slice(&ext_cbor);
836 let parsed = parse_authenticator_data(&data).expect("test setup");
837 let exts = parsed.extensions.expect("test setup");
838 assert_eq!(exts.get("appid"), Some(&Value::Bool(true)));
839 }
840
841 #[test]
842 fn ed_flag_set_with_multiple_extensions() {
843 let cred_props = Value::Map(vec![(Value::Text("rk".to_string()), Value::Bool(true))]);
844 let ext_cbor = make_extension_cbor(&[
845 ("credProps", cred_props.clone()),
846 ("appid", Value::Bool(false)),
847 ]);
848 let mut data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_ED, 0, None);
849 data.extend_from_slice(&ext_cbor);
850 let parsed = parse_authenticator_data(&data).expect("test setup");
851 let exts = parsed.extensions.expect("test setup");
852 assert_eq!(exts.get("credProps"), Some(&cred_props));
853 assert_eq!(exts.get("appid"), Some(&Value::Bool(false)));
854 }
855
856 #[test]
857 fn ed_flag_set_with_at_flag_parses_extensions_after_cose_key() {
858 let pk_cbor = make_cose_key_cbor(&[0x01u8; 32], &[0x02u8; 32]);
860 let cred_data = make_attested_cred_data(&[0xAAu8; 8], &pk_cbor);
861 let ext_cbor = make_extension_cbor(&[("appid", Value::Bool(true))]);
862 let mut combined = cred_data;
863 combined.extend_from_slice(&ext_cbor);
864 let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT | FLAG_ED, 0, Some(&combined));
865 let parsed = parse_authenticator_data(&data).expect("test setup");
866 assert!(parsed.attested_credential_data.is_some());
867 let exts = parsed.extensions.expect("test setup");
868 assert_eq!(exts.get("appid"), Some(&Value::Bool(true)));
869 }
870
871 #[test]
872 fn ed_flag_set_with_no_bytes_returns_error() {
873 let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_ED, 0, None);
875 let err = parse_authenticator_data(&data).expect_err("expected error");
876 assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
877 assert!(err.to_string().contains("ED"));
878 }
879
880 #[test]
881 fn ed_flag_set_with_malformed_cbor_returns_error() {
882 let mut data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_ED, 0, None);
883 data.extend_from_slice(&[0xFF, 0xFF]); let err = parse_authenticator_data(&data).expect_err("expected error");
885 assert!(matches!(err, WebAuthnError::CborDecodeError(_)));
886 }
887
888 #[test]
889 fn ed_flag_set_with_non_map_cbor_returns_error() {
890 let mut data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_ED, 0, None);
891 let not_a_map = Value::Integer(42i64.into());
893 let mut cbor_buf = Vec::new();
894 ciborium::into_writer(¬_a_map, &mut cbor_buf).expect("test setup");
895 data.extend_from_slice(&cbor_buf);
896 let err = parse_authenticator_data(&data).expect_err("expected error");
897 assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
898 assert!(err.to_string().contains("map"));
899 }
900
901 #[test]
904 fn parse_cose_key_valid_es256() {
905 let x = vec![0x01u8; 32];
906 let y = vec![0x02u8; 32];
907 let cbor = make_cose_key_cbor(&x, &y);
908 let key = parse_cose_key(&cbor).expect("test setup");
909 match key {
910 CoseKey::EC2 {
911 alg,
912 crv,
913 x: kx,
914 y: ky,
915 } => {
916 assert_eq!(alg, -7);
917 assert_eq!(crv, 1);
918 assert_eq!(kx, x);
919 assert_eq!(ky, y);
920 }
921 _ => panic!("expected EC2 key"),
922 }
923 }
924
925 #[test]
926 fn parse_cose_key_rejects_missing_kty() {
927 let cose = Value::Map(vec![
928 (Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
929 (Value::Integer((-1i64).into()), Value::Integer(1i64.into())),
930 (Value::Integer((-2i64).into()), Value::Bytes(vec![0u8; 32])),
931 (Value::Integer((-3i64).into()), Value::Bytes(vec![0u8; 32])),
932 ]);
933 let mut buf = Vec::new();
934 ciborium::into_writer(&cose, &mut buf).expect("test setup");
935 let err = parse_cose_key(&buf).expect_err("expected error");
936 assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
937 assert!(err.to_string().contains("kty"));
938 }
939
940 #[test]
941 fn parse_cose_key_rejects_missing_alg() {
942 let cose = Value::Map(vec![
943 (Value::Integer(1i64.into()), Value::Integer(2i64.into())),
944 (Value::Integer((-1i64).into()), Value::Integer(1i64.into())),
945 (Value::Integer((-2i64).into()), Value::Bytes(vec![0u8; 32])),
946 (Value::Integer((-3i64).into()), Value::Bytes(vec![0u8; 32])),
947 ]);
948 let mut buf = Vec::new();
949 ciborium::into_writer(&cose, &mut buf).expect("test setup");
950 let err = parse_cose_key(&buf).expect_err("expected error");
951 assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
952 assert!(err.to_string().contains("alg"));
953 }
954
955 #[test]
956 fn parse_cose_key_rejects_missing_crv() {
957 let cose = Value::Map(vec![
958 (Value::Integer(1i64.into()), Value::Integer(2i64.into())),
959 (Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
960 (Value::Integer((-2i64).into()), Value::Bytes(vec![0u8; 32])),
961 (Value::Integer((-3i64).into()), Value::Bytes(vec![0u8; 32])),
962 ]);
963 let mut buf = Vec::new();
964 ciborium::into_writer(&cose, &mut buf).expect("test setup");
965 let err = parse_cose_key(&buf).expect_err("expected error");
966 assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
967 assert!(err.to_string().contains("crv"));
968 }
969
970 #[test]
971 fn parse_cose_key_rejects_missing_x() {
972 let cose = Value::Map(vec![
973 (Value::Integer(1i64.into()), Value::Integer(2i64.into())),
974 (Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
975 (Value::Integer((-1i64).into()), Value::Integer(1i64.into())),
976 (Value::Integer((-3i64).into()), Value::Bytes(vec![0u8; 32])),
977 ]);
978 let mut buf = Vec::new();
979 ciborium::into_writer(&cose, &mut buf).expect("test setup");
980 let err = parse_cose_key(&buf).expect_err("expected error");
981 assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
982 assert!(err.to_string().contains("x"));
983 }
984
985 #[test]
986 fn parse_cose_key_rejects_missing_y() {
987 let cose = Value::Map(vec![
988 (Value::Integer(1i64.into()), Value::Integer(2i64.into())),
989 (Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
990 (Value::Integer((-1i64).into()), Value::Integer(1i64.into())),
991 (Value::Integer((-2i64).into()), Value::Bytes(vec![0u8; 32])),
992 ]);
993 let mut buf = Vec::new();
994 ciborium::into_writer(&cose, &mut buf).expect("test setup");
995 let err = parse_cose_key(&buf).expect_err("expected error");
996 assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
997 assert!(err.to_string().contains("y"));
998 }
999
1000 #[test]
1001 fn parse_cose_key_rejects_unsupported_kty() {
1002 let cose = Value::Map(vec![
1004 (Value::Integer(1i64.into()), Value::Integer(4i64.into())),
1005 (Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
1006 ]);
1007 let mut buf = Vec::new();
1008 ciborium::into_writer(&cose, &mut buf).expect("test setup");
1009 let err = parse_cose_key(&buf).expect_err("expected error");
1010 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("4")));
1011 }
1012
1013 #[test]
1014 fn parse_cose_key_rejects_unsupported_crv() {
1015 let cbor = make_cose_key_cbor_with_alg_crv(&[0x01u8; 32], &[0x02u8; 32], -7, 2);
1017 let err = parse_cose_key(&cbor).expect_err("expected error");
1018 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("2")));
1019 }
1020
1021 #[test]
1022 fn parse_cose_key_valid_es384() {
1023 let x = vec![0x01u8; 48];
1024 let y = vec![0x02u8; 48];
1025 let cbor = make_cose_key_cbor_with_alg_crv(&x, &y, -35, 2);
1026 let key = parse_cose_key(&cbor).expect("test setup");
1027 match key {
1028 CoseKey::EC2 {
1029 alg,
1030 crv,
1031 x: kx,
1032 y: ky,
1033 } => {
1034 assert_eq!(alg, -35);
1035 assert_eq!(crv, 2);
1036 assert_eq!(kx, x);
1037 assert_eq!(ky, y);
1038 }
1039 _ => panic!("expected EC2 key"),
1040 }
1041 }
1042
1043 #[test]
1044 fn parse_cose_key_es384_rejects_short_x_coordinate() {
1045 let cbor = make_cose_key_cbor_with_alg_crv(&[0x01u8; 32], &[0x02u8; 48], -35, 2);
1046 let err = parse_cose_key(&cbor).expect_err("expected error");
1047 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("32")));
1048 }
1049
1050 #[test]
1051 fn parse_cose_key_es384_rejects_short_y_coordinate() {
1052 let cbor = make_cose_key_cbor_with_alg_crv(&[0x01u8; 48], &[0x02u8; 32], -35, 2);
1053 let err = parse_cose_key(&cbor).expect_err("expected error");
1054 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("32")));
1055 }
1056
1057 #[test]
1058 fn parse_cose_key_es384_rejects_wrong_crv() {
1059 let cbor = make_cose_key_cbor_with_alg_crv(&[0x01u8; 48], &[0x02u8; 48], -35, 1);
1061 let err = parse_cose_key(&cbor).expect_err("expected error");
1062 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("1")));
1063 }
1064
1065 #[test]
1066 fn parse_cose_key_rejects_short_x_coordinate() {
1067 let cbor = make_cose_key_cbor(&[0x01u8; 31], &[0x02u8; 32]);
1068 let err = parse_cose_key(&cbor).expect_err("expected error");
1069 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("31")));
1070 }
1071
1072 #[test]
1073 fn parse_cose_key_rejects_short_y_coordinate() {
1074 let cbor = make_cose_key_cbor(&[0x01u8; 32], &[0x02u8; 10]);
1075 let err = parse_cose_key(&cbor).expect_err("expected error");
1076 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("10")));
1077 }
1078
1079 #[test]
1080 fn parse_cose_key_rejects_long_x_coordinate() {
1081 let cbor = make_cose_key_cbor(&[0x01u8; 33], &[0x02u8; 32]);
1082 let err = parse_cose_key(&cbor).expect_err("expected error");
1083 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("33")));
1084 }
1085
1086 #[test]
1087 fn parse_cose_key_rejects_duplicate_key() {
1088 let cose = Value::Map(vec![
1090 (Value::Integer(1i64.into()), Value::Integer(2i64.into())),
1091 (Value::Integer(1i64.into()), Value::Integer(3i64.into())), (Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
1093 (Value::Integer((-1i64).into()), Value::Integer(1i64.into())),
1094 (Value::Integer((-2i64).into()), Value::Bytes(vec![0u8; 32])),
1095 (Value::Integer((-3i64).into()), Value::Bytes(vec![0u8; 32])),
1096 ]);
1097 let mut buf = Vec::new();
1098 ciborium::into_writer(&cose, &mut buf).expect("test setup");
1099 let err = parse_cose_key(&buf).expect_err("expected error");
1100 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("duplicate")));
1101 }
1102
1103 #[test]
1104 fn parse_cose_key_rejects_not_a_map() {
1105 let cose = Value::Integer(42i64.into());
1107 let mut buf = Vec::new();
1108 ciborium::into_writer(&cose, &mut buf).expect("test setup");
1109 let err = parse_cose_key(&buf).expect_err("expected error");
1110 assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
1111 }
1112
1113 #[test]
1114 fn parse_cose_key_rejects_empty_input() {
1115 let err = parse_cose_key(&[]).expect_err("expected error");
1116 assert!(matches!(err, WebAuthnError::CborDecodeError(_)));
1117 }
1118
1119 #[test]
1122 fn parse_cose_key_valid_rs256() {
1123 let n = vec![0x01u8; 256]; let e = vec![0x01u8, 0x00, 0x01];
1125 let cbor = make_rsa_cose_key_cbor(&n, &e);
1126 let key = parse_cose_key(&cbor).expect("test setup");
1127 match key {
1128 CoseKey::RSA { alg, n: kn, e: ke } => {
1129 assert_eq!(alg, -257);
1130 assert_eq!(kn, n);
1131 assert_eq!(ke, e);
1132 }
1133 _ => panic!("expected RSA key"),
1134 }
1135 }
1136
1137 #[test]
1138 fn parse_cose_key_rs256_rejects_short_modulus() {
1139 let n = vec![0x01u8; 255];
1141 let e = vec![0x01u8, 0x00, 0x01];
1142 let cbor = make_rsa_cose_key_cbor(&n, &e);
1143 let err = parse_cose_key(&cbor).expect_err("expected error");
1144 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("255")));
1145 }
1146
1147 #[test]
1148 fn parse_cose_key_rs256_rejects_missing_n() {
1149 let cose = Value::Map(vec![
1150 (Value::Integer(1i64.into()), Value::Integer(3i64.into())),
1151 (
1152 Value::Integer(3i64.into()),
1153 Value::Integer((-257i64).into()),
1154 ),
1155 (
1157 Value::Integer((-2i64).into()),
1158 Value::Bytes(vec![0x01, 0x00, 0x01]),
1159 ),
1160 ]);
1161 let mut buf = Vec::new();
1162 ciborium::into_writer(&cose, &mut buf).expect("test setup");
1163 let err = parse_cose_key(&buf).expect_err("expected error");
1164 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("n")));
1165 }
1166
1167 #[test]
1168 fn parse_cose_key_rs256_rejects_missing_e() {
1169 let cose = Value::Map(vec![
1170 (Value::Integer(1i64.into()), Value::Integer(3i64.into())),
1171 (
1172 Value::Integer(3i64.into()),
1173 Value::Integer((-257i64).into()),
1174 ),
1175 (
1176 Value::Integer((-1i64).into()),
1177 Value::Bytes(vec![0x01u8; 256]),
1178 ),
1179 ]);
1181 let mut buf = Vec::new();
1182 ciborium::into_writer(&cose, &mut buf).expect("test setup");
1183 let err = parse_cose_key(&buf).expect_err("expected error");
1184 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("e")));
1185 }
1186
1187 #[test]
1188 fn parse_cose_key_rsa_with_wrong_alg_returns_unsupported() {
1189 let cose = Value::Map(vec![
1191 (Value::Integer(1i64.into()), Value::Integer(3i64.into())),
1192 (Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
1193 (
1194 Value::Integer((-1i64).into()),
1195 Value::Bytes(vec![0x01u8; 256]),
1196 ),
1197 (
1198 Value::Integer((-2i64).into()),
1199 Value::Bytes(vec![0x01, 0x00, 0x01]),
1200 ),
1201 ]);
1202 let mut buf = Vec::new();
1203 ciborium::into_writer(&cose, &mut buf).expect("test setup");
1204 let err = parse_cose_key(&buf).expect_err("expected error");
1205 assert!(matches!(err, WebAuthnError::UnsupportedAlgorithm(-7)));
1206 }
1207}