1use ciborium::value::Value;
25
26use crate::algorithm::{
27 COSE_CRV_ED25519, COSE_CRV_P256, COSE_CRV_P384, COSE_EDDSA, COSE_ES256, COSE_ES384,
28 COSE_KTY_EC2, COSE_KTY_OKP, COSE_KTY_RSA, COSE_RS256,
29};
30use crate::error::{Result, WebAuthnError};
31
32const FLAG_UP: u8 = 0x01;
36const FLAG_UV: u8 = 0x04;
38const FLAG_BE: u8 = 0x08;
40const FLAG_BS: u8 = 0x10;
42const FLAG_AT: u8 = 0x40;
44const FLAG_ED: u8 = 0x80;
46
47const MAX_CREDENTIAL_ID_LEN: usize = 1023;
49
50const MIN_RSA_N_LEN: usize = 256;
52
53#[derive(Debug, Clone, Copy)]
57pub struct AuthenticatorFlags {
58 pub user_present: bool,
60 pub user_verified: bool,
62 pub backup_eligible: bool,
65 pub backup_state: bool,
67 pub attested_credential_data: bool,
69 pub extension_data: bool,
71}
72
73#[derive(Debug, Clone)]
83pub enum CoseKey {
84 EC2 {
86 alg: i64,
88 crv: i64,
90 x: Vec<u8>,
92 y: Vec<u8>,
94 },
95 OKP {
97 alg: i64,
99 x: Vec<u8>,
101 },
102 RSA {
104 alg: i64,
106 n: Vec<u8>,
108 e: Vec<u8>,
110 },
111}
112
113impl CoseKey {
114 pub fn alg(&self) -> i64 {
116 match self {
117 CoseKey::EC2 { alg, .. } => *alg,
118 CoseKey::OKP { alg, .. } => *alg,
119 CoseKey::RSA { alg, .. } => *alg,
120 }
121 }
122}
123
124#[derive(Debug)]
126pub struct AttestedCredentialData {
127 pub aaguid: [u8; 16],
130
131 pub credential_id: Vec<u8>,
133
134 pub public_key: CoseKey,
136}
137
138#[derive(Debug)]
140pub struct AuthenticatorData {
141 pub rp_id_hash: [u8; 32],
143
144 pub flags: AuthenticatorFlags,
146
147 pub sign_count: u32,
149
150 pub attested_credential_data: Option<AttestedCredentialData>,
152
153 pub raw: Vec<u8>,
158}
159
160pub fn parse_authenticator_data(data: &[u8]) -> Result<AuthenticatorData> {
169 if data.len() < 37 {
171 return Err(WebAuthnError::InvalidAuthenticatorData(format!(
172 "too short: expected at least 37 bytes, got {}",
173 data.len()
174 )));
175 }
176
177 let rp_id_hash: [u8; 32] = data[0..32]
179 .try_into()
180 .expect("slice of exactly 32 bytes always converts");
181
182 let flags_byte = data[32];
184 let flags = AuthenticatorFlags {
185 user_present: flags_byte & FLAG_UP != 0,
186 user_verified: flags_byte & FLAG_UV != 0,
187 backup_eligible: flags_byte & FLAG_BE != 0,
188 backup_state: flags_byte & FLAG_BS != 0,
189 attested_credential_data: flags_byte & FLAG_AT != 0,
190 extension_data: flags_byte & FLAG_ED != 0,
191 };
192
193 if flags.backup_state && !flags.backup_eligible {
196 return Err(WebAuthnError::InvalidAuthenticatorData(
197 "BS flag (backup state) is set but BE flag (backup eligibility) is not — invalid per §6.1".to_string(),
198 ));
199 }
200
201 let sign_count = u32::from_be_bytes(
203 data[33..37]
204 .try_into()
205 .expect("slice of exactly 4 bytes always converts"),
206 );
207
208 let attested_credential_data = if flags.attested_credential_data {
210 Some(parse_attested_credential_data(&data[37..])?)
211 } else {
212 None
213 };
214
215 Ok(AuthenticatorData {
216 rp_id_hash,
217 flags,
218 sign_count,
219 attested_credential_data,
220 raw: data.to_vec(),
221 })
222}
223
224pub fn parse_cose_key(data: &[u8]) -> Result<CoseKey> {
237 let value: Value = ciborium::from_reader(data)
238 .map_err(|e| WebAuthnError::CborDecodeError(format!("COSE key: {e}")))?;
239
240 let map = match value {
241 Value::Map(m) => m,
242 _ => {
243 return Err(WebAuthnError::InvalidPublicKey(
244 "COSE key must be a CBOR map".to_string(),
245 ))
246 }
247 };
248
249 {
251 let mut seen: Vec<i64> = Vec::new();
252 for (k, _) in &map {
253 if let Value::Integer(ki) = k {
254 if let Ok(n) = i64::try_from(*ki) {
255 if seen.contains(&n) {
256 return Err(WebAuthnError::InvalidPublicKey(format!(
257 "duplicate COSE key: {n}"
258 )));
259 }
260 seen.push(n);
261 }
262 }
263 }
264 }
265
266 let get_int = |key: i64| -> Option<i64> {
267 map.iter().find_map(|(k, v)| {
268 if let (Value::Integer(ki), Value::Integer(vi)) = (k, v) {
269 if i64::try_from(*ki).ok()? == key {
270 return i64::try_from(*vi).ok();
271 }
272 }
273 None
274 })
275 };
276
277 let get_bytes = |key: i64| -> Option<Vec<u8>> {
278 map.iter().find_map(|(k, v)| {
279 if let Value::Integer(ki) = k {
280 if i64::try_from(*ki).ok()? == key {
281 if let Value::Bytes(b) = v {
282 return Some(b.clone());
283 }
284 }
285 }
286 None
287 })
288 };
289
290 let kty = get_int(1).ok_or_else(|| {
292 WebAuthnError::InvalidPublicKey("missing required field: kty".to_string())
293 })?;
294
295 if kty == COSE_KTY_OKP {
296 parse_okp_key(&get_int, &get_bytes)
297 } else if kty == COSE_KTY_EC2 {
298 parse_ec2_key(&get_int, &get_bytes)
299 } else if kty == COSE_KTY_RSA {
300 parse_rsa_key(&get_int, &get_bytes)
301 } else {
302 Err(WebAuthnError::InvalidPublicKey(format!(
303 "unsupported key type: {kty} (supported: OKP=1, EC2=2, RSA=3)"
304 )))
305 }
306}
307
308fn parse_okp_key(
311 get_int: &impl Fn(i64) -> Option<i64>,
312 get_bytes: &impl Fn(i64) -> Option<Vec<u8>>,
313) -> Result<CoseKey> {
314 let alg = get_int(3).ok_or_else(|| {
316 WebAuthnError::InvalidPublicKey("missing required field: alg".to_string())
317 })?;
318
319 if alg != COSE_EDDSA {
320 return Err(WebAuthnError::UnsupportedAlgorithm(alg));
321 }
322
323 let crv = get_int(-1).ok_or_else(|| {
325 WebAuthnError::InvalidPublicKey("missing required field: crv".to_string())
326 })?;
327
328 if crv != COSE_CRV_ED25519 {
329 return Err(WebAuthnError::InvalidPublicKey(format!(
330 "unsupported OKP curve: {crv} (only Ed25519 / crv=6 is supported)"
331 )));
332 }
333
334 let x = get_bytes(-2)
336 .ok_or_else(|| WebAuthnError::InvalidPublicKey("missing required field: x".to_string()))?;
337
338 if x.len() != 32 {
339 return Err(WebAuthnError::InvalidPublicKey(format!(
340 "Ed25519 public key must be 32 bytes, got {}",
341 x.len()
342 )));
343 }
344
345 Ok(CoseKey::OKP { alg, x })
346}
347
348fn parse_ec2_key(
349 get_int: &impl Fn(i64) -> Option<i64>,
350 get_bytes: &impl Fn(i64) -> Option<Vec<u8>>,
351) -> Result<CoseKey> {
352 let alg = get_int(3).ok_or_else(|| {
355 WebAuthnError::InvalidPublicKey("missing required field: alg".to_string())
356 })?;
357
358 let crv = get_int(-1).ok_or_else(|| {
360 WebAuthnError::InvalidPublicKey("missing required field: crv".to_string())
361 })?;
362
363 let coord_size: usize = match alg {
364 COSE_ES256 => {
365 if crv != COSE_CRV_P256 {
367 return Err(WebAuthnError::InvalidPublicKey(format!(
368 "ES256 requires curve P-256 (crv=1), got {crv}"
369 )));
370 }
371 32
372 }
373 COSE_ES384 => {
374 if crv != COSE_CRV_P384 {
376 return Err(WebAuthnError::InvalidPublicKey(format!(
377 "ES384 requires curve P-384 (crv=2), got {crv}"
378 )));
379 }
380 48
381 }
382 _ => return Err(WebAuthnError::UnsupportedAlgorithm(alg)),
383 };
384
385 let x = get_bytes(-2)
387 .ok_or_else(|| WebAuthnError::InvalidPublicKey("missing required field: x".to_string()))?;
388
389 let y = get_bytes(-3)
391 .ok_or_else(|| WebAuthnError::InvalidPublicKey("missing required field: y".to_string()))?;
392
393 if x.len() != coord_size {
394 return Err(WebAuthnError::InvalidPublicKey(format!(
395 "x coordinate must be {coord_size} bytes, got {}",
396 x.len()
397 )));
398 }
399 if y.len() != coord_size {
400 return Err(WebAuthnError::InvalidPublicKey(format!(
401 "y coordinate must be {coord_size} bytes, got {}",
402 y.len()
403 )));
404 }
405
406 Ok(CoseKey::EC2 { alg, crv, x, y })
407}
408
409fn parse_rsa_key(
410 get_int: &impl Fn(i64) -> Option<i64>,
411 get_bytes: &impl Fn(i64) -> Option<Vec<u8>>,
412) -> Result<CoseKey> {
413 let alg = get_int(3).ok_or_else(|| {
415 WebAuthnError::InvalidPublicKey("missing required field: alg".to_string())
416 })?;
417
418 if alg != COSE_RS256 {
419 return Err(WebAuthnError::UnsupportedAlgorithm(alg));
420 }
421
422 let n = get_bytes(-1)
424 .ok_or_else(|| WebAuthnError::InvalidPublicKey("missing required field: n".to_string()))?;
425
426 if n.len() < MIN_RSA_N_LEN {
427 return Err(WebAuthnError::InvalidPublicKey(format!(
428 "RSA modulus must be at least {} bytes (2048-bit), got {}",
429 MIN_RSA_N_LEN,
430 n.len()
431 )));
432 }
433
434 let e = get_bytes(-2)
436 .ok_or_else(|| WebAuthnError::InvalidPublicKey("missing required field: e".to_string()))?;
437
438 if e.is_empty() {
439 return Err(WebAuthnError::InvalidPublicKey(
440 "RSA exponent (e) must not be empty".to_string(),
441 ));
442 }
443
444 Ok(CoseKey::RSA { alg, n, e })
445}
446
447fn parse_attested_credential_data(data: &[u8]) -> Result<AttestedCredentialData> {
448 if data.len() < 18 {
450 return Err(WebAuthnError::InvalidAuthenticatorData(format!(
451 "attested credential data too short: expected at least 18 bytes after flags/counter, got {}",
452 data.len()
453 )));
454 }
455
456 let mut offset = 0usize;
457
458 let aaguid: [u8; 16] = data
460 .get(offset..offset + 16)
461 .ok_or_else(|| {
462 WebAuthnError::InvalidAuthenticatorData("truncated before aaguid".to_string())
463 })?
464 .try_into()
465 .expect("slice of exactly 16 bytes always converts");
466 offset += 16;
467
468 let cred_id_len = u16::from_be_bytes([
470 *data.get(offset).ok_or_else(|| {
471 WebAuthnError::InvalidAuthenticatorData(
472 "truncated before credentialIdLength high byte".to_string(),
473 )
474 })?,
475 *data.get(offset + 1).ok_or_else(|| {
476 WebAuthnError::InvalidAuthenticatorData(
477 "truncated before credentialIdLength low byte".to_string(),
478 )
479 })?,
480 ]) as usize;
481 offset += 2;
482
483 if cred_id_len == 0 {
484 return Err(WebAuthnError::InvalidAuthenticatorData(
485 "credentialIdLength is 0 — empty credential ID is not valid".to_string(),
486 ));
487 }
488
489 if cred_id_len > MAX_CREDENTIAL_ID_LEN {
490 return Err(WebAuthnError::InvalidAuthenticatorData(format!(
491 "credentialIdLength {cred_id_len} exceeds maximum {MAX_CREDENTIAL_ID_LEN} — likely corrupt data"
492 )));
493 }
494
495 let credential_id = data
496 .get(offset..offset + cred_id_len)
497 .ok_or_else(|| {
498 WebAuthnError::InvalidAuthenticatorData(format!(
499 "credentialIdLength ({cred_id_len}) extends past end of buffer"
500 ))
501 })?
502 .to_vec();
503 offset += cred_id_len;
504
505 let remaining = data.get(offset..).ok_or_else(|| {
509 WebAuthnError::InvalidAuthenticatorData("truncated before public key CBOR".to_string())
510 })?;
511
512 if remaining.is_empty() {
513 return Err(WebAuthnError::InvalidAuthenticatorData(
514 "no bytes remaining for credentialPublicKey CBOR".to_string(),
515 ));
516 }
517
518 let public_key = parse_cose_key(remaining)?;
519
520 Ok(AttestedCredentialData {
521 aaguid,
522 credential_id,
523 public_key,
524 })
525}
526
527#[cfg(test)]
530mod tests {
531 use super::*;
532 use ciborium::value::Value;
533
534 pub fn make_auth_data(
536 rp_id_hash: &[u8; 32],
537 flags: u8,
538 sign_count: u32,
539 cred_data: Option<&[u8]>,
540 ) -> Vec<u8> {
541 let mut out = Vec::new();
542 out.extend_from_slice(rp_id_hash);
543 out.push(flags);
544 out.extend_from_slice(&sign_count.to_be_bytes());
545 if let Some(cd) = cred_data {
546 out.extend_from_slice(cd);
547 }
548 out
549 }
550
551 fn make_cose_key_cbor(x: &[u8], y: &[u8]) -> Vec<u8> {
552 make_cose_key_cbor_with_alg_crv(x, y, -7, 1)
553 }
554
555 fn make_cose_key_cbor_with_alg_crv(x: &[u8], y: &[u8], alg: i64, crv: i64) -> Vec<u8> {
556 let cose = Value::Map(vec![
557 (Value::Integer(1i64.into()), Value::Integer(2i64.into())),
558 (Value::Integer(3i64.into()), Value::Integer(alg.into())),
559 (Value::Integer((-1i64).into()), Value::Integer(crv.into())),
560 (Value::Integer((-2i64).into()), Value::Bytes(x.to_vec())),
561 (Value::Integer((-3i64).into()), Value::Bytes(y.to_vec())),
562 ]);
563 let mut buf = Vec::new();
564 ciborium::into_writer(&cose, &mut buf).unwrap();
565 buf
566 }
567
568 fn make_rsa_cose_key_cbor(n: &[u8], e: &[u8]) -> Vec<u8> {
569 let cose = Value::Map(vec![
570 (Value::Integer(1i64.into()), Value::Integer(3i64.into())), (
572 Value::Integer(3i64.into()),
573 Value::Integer((-257i64).into()),
574 ), (Value::Integer((-1i64).into()), Value::Bytes(n.to_vec())), (Value::Integer((-2i64).into()), Value::Bytes(e.to_vec())), ]);
578 let mut buf = Vec::new();
579 ciborium::into_writer(&cose, &mut buf).unwrap();
580 buf
581 }
582
583 fn make_attested_cred_data(cred_id: &[u8], pk_cbor: &[u8]) -> Vec<u8> {
584 let mut out = vec![0u8; 16]; out.extend_from_slice(&(cred_id.len() as u16).to_be_bytes());
586 out.extend_from_slice(cred_id);
587 out.extend_from_slice(pk_cbor);
588 out
589 }
590
591 #[test]
594 fn parses_be_flag() {
595 let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_BE, 0, None);
596 let parsed = parse_authenticator_data(&data).unwrap();
597 assert!(parsed.flags.backup_eligible);
598 assert!(!parsed.flags.backup_state);
599 }
600
601 #[test]
602 fn parses_be_and_bs_flags() {
603 let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_BE | FLAG_BS, 0, None);
604 let parsed = parse_authenticator_data(&data).unwrap();
605 assert!(parsed.flags.backup_eligible);
606 assert!(parsed.flags.backup_state);
607 }
608
609 #[test]
610 fn rejects_bs_without_be() {
611 let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_BS, 0, None);
613 let err = parse_authenticator_data(&data).unwrap_err();
614 assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
615 assert!(err.to_string().contains("BS"));
616 }
617
618 #[test]
621 fn rejects_too_short() {
622 let result = parse_authenticator_data(&[0u8; 10]);
623 assert!(matches!(
624 result,
625 Err(WebAuthnError::InvalidAuthenticatorData(_))
626 ));
627 }
628
629 #[test]
630 fn rejects_exactly_36_bytes() {
631 let result = parse_authenticator_data(&[0u8; 36]);
632 assert!(matches!(
633 result,
634 Err(WebAuthnError::InvalidAuthenticatorData(_))
635 ));
636 }
637
638 #[test]
639 fn rejects_empty() {
640 let result = parse_authenticator_data(&[]);
641 assert!(matches!(
642 result,
643 Err(WebAuthnError::InvalidAuthenticatorData(_))
644 ));
645 }
646
647 #[test]
648 fn parses_minimal_auth_data() {
649 let rp_hash = [0xABu8; 32];
650 let data = make_auth_data(&rp_hash, FLAG_UP, 42, None);
651 let parsed = parse_authenticator_data(&data).unwrap();
652
653 assert_eq!(parsed.rp_id_hash, rp_hash);
654 assert!(parsed.flags.user_present);
655 assert!(!parsed.flags.user_verified);
656 assert_eq!(parsed.sign_count, 42);
657 assert!(parsed.attested_credential_data.is_none());
658 assert_eq!(parsed.raw, data);
659 }
660
661 #[test]
662 fn parses_up_and_uv_flags() {
663 let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_UV, 0, None);
664 let parsed = parse_authenticator_data(&data).unwrap();
665 assert!(parsed.flags.user_present);
666 assert!(parsed.flags.user_verified);
667 }
668
669 #[test]
670 fn raw_field_equals_input_bytes() {
671 let data = make_auth_data(&[0u8; 32], FLAG_UP, 7, None);
672 let parsed = parse_authenticator_data(&data).unwrap();
673 assert_eq!(parsed.raw, data);
674 }
675
676 #[test]
677 fn all_flags_set_parses_without_panic() {
678 let data = make_auth_data(&[0u8; 32], 0xFF, 0, None);
681 let result = parse_authenticator_data(&data);
682 assert!(result.is_err());
683 }
684
685 #[test]
686 fn at_flag_set_but_no_data_returns_error() {
687 let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT, 0, None);
689 let err = parse_authenticator_data(&data).unwrap_err();
690 assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
691 }
692
693 #[test]
696 fn rejects_zero_length_credential_id() {
697 let pk = make_cose_key_cbor(&[0x01u8; 32], &[0x02u8; 32]);
698 let cred_data = make_attested_cred_data(&[], &pk); let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT, 0, Some(&cred_data));
700 let err = parse_authenticator_data(&data).unwrap_err();
701 assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
702 assert!(err.to_string().contains("0"));
704 }
705
706 #[test]
707 fn rejects_credential_id_too_large() {
708 let mut cred_data = vec![0u8; 16]; let oversized_len: u16 = 1024;
711 cred_data.extend_from_slice(&oversized_len.to_be_bytes());
712 let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT, 0, Some(&cred_data));
714 let err = parse_authenticator_data(&data).unwrap_err();
715 assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
716 assert!(err.to_string().contains("1024"));
717 }
718
719 #[test]
720 fn rejects_credential_id_extends_past_buffer() {
721 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));
725 let err = parse_authenticator_data(&data).unwrap_err();
726 assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
727 }
728
729 #[test]
730 fn rejects_empty_cbor_after_credential_id() {
731 let mut cred_data = vec![0u8; 16]; cred_data.extend_from_slice(&1u16.to_be_bytes());
733 cred_data.push(0xAB); let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT, 0, Some(&cred_data));
736 let err = parse_authenticator_data(&data).unwrap_err();
737 assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
738 }
739
740 #[test]
743 fn parse_cose_key_valid_es256() {
744 let x = vec![0x01u8; 32];
745 let y = vec![0x02u8; 32];
746 let cbor = make_cose_key_cbor(&x, &y);
747 let key = parse_cose_key(&cbor).unwrap();
748 match key {
749 CoseKey::EC2 {
750 alg,
751 crv,
752 x: kx,
753 y: ky,
754 } => {
755 assert_eq!(alg, -7);
756 assert_eq!(crv, 1);
757 assert_eq!(kx, x);
758 assert_eq!(ky, y);
759 }
760 _ => panic!("expected EC2 key"),
761 }
762 }
763
764 #[test]
765 fn parse_cose_key_rejects_missing_kty() {
766 let cose = Value::Map(vec![
767 (Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
768 (Value::Integer((-1i64).into()), Value::Integer(1i64.into())),
769 (Value::Integer((-2i64).into()), Value::Bytes(vec![0u8; 32])),
770 (Value::Integer((-3i64).into()), Value::Bytes(vec![0u8; 32])),
771 ]);
772 let mut buf = Vec::new();
773 ciborium::into_writer(&cose, &mut buf).unwrap();
774 let err = parse_cose_key(&buf).unwrap_err();
775 assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
776 assert!(err.to_string().contains("kty"));
777 }
778
779 #[test]
780 fn parse_cose_key_rejects_missing_alg() {
781 let cose = Value::Map(vec![
782 (Value::Integer(1i64.into()), Value::Integer(2i64.into())),
783 (Value::Integer((-1i64).into()), Value::Integer(1i64.into())),
784 (Value::Integer((-2i64).into()), Value::Bytes(vec![0u8; 32])),
785 (Value::Integer((-3i64).into()), Value::Bytes(vec![0u8; 32])),
786 ]);
787 let mut buf = Vec::new();
788 ciborium::into_writer(&cose, &mut buf).unwrap();
789 let err = parse_cose_key(&buf).unwrap_err();
790 assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
791 assert!(err.to_string().contains("alg"));
792 }
793
794 #[test]
795 fn parse_cose_key_rejects_missing_crv() {
796 let cose = Value::Map(vec![
797 (Value::Integer(1i64.into()), Value::Integer(2i64.into())),
798 (Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
799 (Value::Integer((-2i64).into()), Value::Bytes(vec![0u8; 32])),
800 (Value::Integer((-3i64).into()), Value::Bytes(vec![0u8; 32])),
801 ]);
802 let mut buf = Vec::new();
803 ciborium::into_writer(&cose, &mut buf).unwrap();
804 let err = parse_cose_key(&buf).unwrap_err();
805 assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
806 assert!(err.to_string().contains("crv"));
807 }
808
809 #[test]
810 fn parse_cose_key_rejects_missing_x() {
811 let cose = Value::Map(vec![
812 (Value::Integer(1i64.into()), Value::Integer(2i64.into())),
813 (Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
814 (Value::Integer((-1i64).into()), Value::Integer(1i64.into())),
815 (Value::Integer((-3i64).into()), Value::Bytes(vec![0u8; 32])),
816 ]);
817 let mut buf = Vec::new();
818 ciborium::into_writer(&cose, &mut buf).unwrap();
819 let err = parse_cose_key(&buf).unwrap_err();
820 assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
821 assert!(err.to_string().contains("x"));
822 }
823
824 #[test]
825 fn parse_cose_key_rejects_missing_y() {
826 let cose = Value::Map(vec![
827 (Value::Integer(1i64.into()), Value::Integer(2i64.into())),
828 (Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
829 (Value::Integer((-1i64).into()), Value::Integer(1i64.into())),
830 (Value::Integer((-2i64).into()), Value::Bytes(vec![0u8; 32])),
831 ]);
832 let mut buf = Vec::new();
833 ciborium::into_writer(&cose, &mut buf).unwrap();
834 let err = parse_cose_key(&buf).unwrap_err();
835 assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
836 assert!(err.to_string().contains("y"));
837 }
838
839 #[test]
840 fn parse_cose_key_rejects_unsupported_kty() {
841 let cose = Value::Map(vec![
843 (Value::Integer(1i64.into()), Value::Integer(4i64.into())),
844 (Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
845 ]);
846 let mut buf = Vec::new();
847 ciborium::into_writer(&cose, &mut buf).unwrap();
848 let err = parse_cose_key(&buf).unwrap_err();
849 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("4")));
850 }
851
852 #[test]
853 fn parse_cose_key_rejects_unsupported_crv() {
854 let cbor = make_cose_key_cbor_with_alg_crv(&[0x01u8; 32], &[0x02u8; 32], -7, 2);
856 let err = parse_cose_key(&cbor).unwrap_err();
857 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("2")));
858 }
859
860 #[test]
861 fn parse_cose_key_valid_es384() {
862 let x = vec![0x01u8; 48];
863 let y = vec![0x02u8; 48];
864 let cbor = make_cose_key_cbor_with_alg_crv(&x, &y, -35, 2);
865 let key = parse_cose_key(&cbor).unwrap();
866 match key {
867 CoseKey::EC2 {
868 alg,
869 crv,
870 x: kx,
871 y: ky,
872 } => {
873 assert_eq!(alg, -35);
874 assert_eq!(crv, 2);
875 assert_eq!(kx, x);
876 assert_eq!(ky, y);
877 }
878 _ => panic!("expected EC2 key"),
879 }
880 }
881
882 #[test]
883 fn parse_cose_key_es384_rejects_short_x_coordinate() {
884 let cbor = make_cose_key_cbor_with_alg_crv(&[0x01u8; 32], &[0x02u8; 48], -35, 2);
885 let err = parse_cose_key(&cbor).unwrap_err();
886 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("32")));
887 }
888
889 #[test]
890 fn parse_cose_key_es384_rejects_short_y_coordinate() {
891 let cbor = make_cose_key_cbor_with_alg_crv(&[0x01u8; 48], &[0x02u8; 32], -35, 2);
892 let err = parse_cose_key(&cbor).unwrap_err();
893 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("32")));
894 }
895
896 #[test]
897 fn parse_cose_key_es384_rejects_wrong_crv() {
898 let cbor = make_cose_key_cbor_with_alg_crv(&[0x01u8; 48], &[0x02u8; 48], -35, 1);
900 let err = parse_cose_key(&cbor).unwrap_err();
901 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("1")));
902 }
903
904 #[test]
905 fn parse_cose_key_rejects_short_x_coordinate() {
906 let cbor = make_cose_key_cbor(&[0x01u8; 31], &[0x02u8; 32]);
907 let err = parse_cose_key(&cbor).unwrap_err();
908 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("31")));
909 }
910
911 #[test]
912 fn parse_cose_key_rejects_short_y_coordinate() {
913 let cbor = make_cose_key_cbor(&[0x01u8; 32], &[0x02u8; 10]);
914 let err = parse_cose_key(&cbor).unwrap_err();
915 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("10")));
916 }
917
918 #[test]
919 fn parse_cose_key_rejects_long_x_coordinate() {
920 let cbor = make_cose_key_cbor(&[0x01u8; 33], &[0x02u8; 32]);
921 let err = parse_cose_key(&cbor).unwrap_err();
922 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("33")));
923 }
924
925 #[test]
926 fn parse_cose_key_rejects_duplicate_key() {
927 let cose = Value::Map(vec![
929 (Value::Integer(1i64.into()), Value::Integer(2i64.into())),
930 (Value::Integer(1i64.into()), Value::Integer(3i64.into())), (Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
932 (Value::Integer((-1i64).into()), Value::Integer(1i64.into())),
933 (Value::Integer((-2i64).into()), Value::Bytes(vec![0u8; 32])),
934 (Value::Integer((-3i64).into()), Value::Bytes(vec![0u8; 32])),
935 ]);
936 let mut buf = Vec::new();
937 ciborium::into_writer(&cose, &mut buf).unwrap();
938 let err = parse_cose_key(&buf).unwrap_err();
939 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("duplicate")));
940 }
941
942 #[test]
943 fn parse_cose_key_rejects_not_a_map() {
944 let cose = Value::Integer(42i64.into());
946 let mut buf = Vec::new();
947 ciborium::into_writer(&cose, &mut buf).unwrap();
948 let err = parse_cose_key(&buf).unwrap_err();
949 assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
950 }
951
952 #[test]
953 fn parse_cose_key_rejects_empty_input() {
954 let err = parse_cose_key(&[]).unwrap_err();
955 assert!(matches!(err, WebAuthnError::CborDecodeError(_)));
956 }
957
958 #[test]
961 fn parse_cose_key_valid_rs256() {
962 let n = vec![0x01u8; 256]; let e = vec![0x01u8, 0x00, 0x01];
964 let cbor = make_rsa_cose_key_cbor(&n, &e);
965 let key = parse_cose_key(&cbor).unwrap();
966 match key {
967 CoseKey::RSA { alg, n: kn, e: ke } => {
968 assert_eq!(alg, -257);
969 assert_eq!(kn, n);
970 assert_eq!(ke, e);
971 }
972 _ => panic!("expected RSA key"),
973 }
974 }
975
976 #[test]
977 fn parse_cose_key_rs256_rejects_short_modulus() {
978 let n = vec![0x01u8; 255];
980 let e = vec![0x01u8, 0x00, 0x01];
981 let cbor = make_rsa_cose_key_cbor(&n, &e);
982 let err = parse_cose_key(&cbor).unwrap_err();
983 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("255")));
984 }
985
986 #[test]
987 fn parse_cose_key_rs256_rejects_missing_n() {
988 let cose = Value::Map(vec![
989 (Value::Integer(1i64.into()), Value::Integer(3i64.into())),
990 (
991 Value::Integer(3i64.into()),
992 Value::Integer((-257i64).into()),
993 ),
994 (
996 Value::Integer((-2i64).into()),
997 Value::Bytes(vec![0x01, 0x00, 0x01]),
998 ),
999 ]);
1000 let mut buf = Vec::new();
1001 ciborium::into_writer(&cose, &mut buf).unwrap();
1002 let err = parse_cose_key(&buf).unwrap_err();
1003 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("n")));
1004 }
1005
1006 #[test]
1007 fn parse_cose_key_rs256_rejects_missing_e() {
1008 let cose = Value::Map(vec![
1009 (Value::Integer(1i64.into()), Value::Integer(3i64.into())),
1010 (
1011 Value::Integer(3i64.into()),
1012 Value::Integer((-257i64).into()),
1013 ),
1014 (
1015 Value::Integer((-1i64).into()),
1016 Value::Bytes(vec![0x01u8; 256]),
1017 ),
1018 ]);
1020 let mut buf = Vec::new();
1021 ciborium::into_writer(&cose, &mut buf).unwrap();
1022 let err = parse_cose_key(&buf).unwrap_err();
1023 assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("e")));
1024 }
1025
1026 #[test]
1027 fn parse_cose_key_rsa_with_wrong_alg_returns_unsupported() {
1028 let cose = Value::Map(vec![
1030 (Value::Integer(1i64.into()), Value::Integer(3i64.into())),
1031 (Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
1032 (
1033 Value::Integer((-1i64).into()),
1034 Value::Bytes(vec![0x01u8; 256]),
1035 ),
1036 (
1037 Value::Integer((-2i64).into()),
1038 Value::Bytes(vec![0x01, 0x00, 0x01]),
1039 ),
1040 ]);
1041 let mut buf = Vec::new();
1042 ciborium::into_writer(&cose, &mut buf).unwrap();
1043 let err = parse_cose_key(&buf).unwrap_err();
1044 assert!(matches!(err, WebAuthnError::UnsupportedAlgorithm(-7)));
1045 }
1046}