1use std::time::{SystemTime, UNIX_EPOCH};
49
50use ciborium::Value;
51use x509_parser::prelude::*;
52
53use crate::crypto::{
54 cbor_bytes, cbor_map_entries, verify_cose_signature, CosePublicKey, COSE_ALG_ES256,
55 COSE_ALG_RS256,
56};
57use crate::error::WebauthnError;
58
59#[derive(Debug, Clone, PartialEq, Eq)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize))]
71pub enum AttestationFormat {
72 None,
74 Packed,
76 FidoU2f,
78 Unknown,
81}
82
83impl AttestationFormat {
84 pub fn from_fmt(fmt: &str) -> Self {
86 match fmt {
87 "none" => AttestationFormat::None,
88 "packed" => AttestationFormat::Packed,
89 "fido-u2f" => AttestationFormat::FidoU2f,
90 _ => AttestationFormat::Unknown,
91 }
92 }
93
94 pub fn as_fmt(&self) -> &'static str {
97 match self {
98 AttestationFormat::None => "none",
99 AttestationFormat::Packed => "packed",
100 AttestationFormat::FidoU2f => "fido-u2f",
101 AttestationFormat::Unknown => "unknown",
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize))]
114pub enum TrustLevel {
115 None,
117 SelfAttested,
120 BasicAtt,
125 AttCa,
128}
129
130pub mod warnings {
134 pub const UNANCHORED_CHAIN: &str =
137 "attestation chain accepted without a configured trust anchor; \
138 treat as self-attested";
139 pub const UNVERIFIED_FORMAT: &str =
142 "unknown attestation format accepted via allow_unknown_formats; \
143 statement was not verified";
144}
145
146#[derive(Debug, Clone)]
148#[cfg_attr(feature = "serde", derive(serde::Serialize))]
149pub struct AttestationResult {
150 pub format: AttestationFormat,
152 pub aaguid: Option<[u8; 16]>,
155 pub trust_level: TrustLevel,
158 pub warnings: Vec<&'static str>,
160}
161
162#[derive(Debug, Clone, Default)]
177pub struct AttestationPolicy {
178 pub trust_anchors: Vec<Vec<u8>>,
181 pub allow_unknown_formats: bool,
184}
185
186impl AttestationPolicy {
187 pub fn strict() -> Self {
193 Self::default()
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
199pub(crate) enum AttestationStatement {
200 None,
202 Packed {
204 alg: i32,
206 sig: Vec<u8>,
208 x5c: Vec<Vec<u8>>,
210 },
211 FidoU2f {
213 sig: Vec<u8>,
215 cert: Vec<u8>,
217 },
218}
219
220const OID_SIG_SHA256_WITH_RSA: &[u8] = &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B];
226const OID_SIG_ECDSA_WITH_SHA256: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02];
228const OID_ALG_RSA_ENCRYPTION: &[u8] = &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01];
230const OID_ALG_EC_PUBLIC_KEY: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01];
232const OID_EXT_BASIC_CONSTRAINTS: &[u8] = &[0x55, 0x1D, 0x13];
234const OID_EXT_FIDO_GEN_CE_AAGUID: &[u8] =
236 &[0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xE5, 0x14, 0x01, 0x04];
237
238const STMT_LABEL_ALG: i64 = 3;
244const STMT_LABEL_SIG: i64 = 8;
246const STMT_LABEL_X5C: i64 = 33;
248
249pub(crate) fn parse_statement(
257 format: &AttestationFormat,
258 att_stmt: Option<&Value>,
259) -> Result<AttestationStatement, WebauthnError> {
260 match format {
261 AttestationFormat::None => {
262 match att_stmt {
264 None => Ok(AttestationStatement::None),
265 Some(Value::Map(entries)) if entries.is_empty() => Ok(AttestationStatement::None),
266 Some(_) => Err(WebauthnError::AttestationError(
267 "attStmt must be an empty map for the 'none' format".to_string(),
268 )),
269 }
270 }
271 AttestationFormat::Packed => {
272 let val = att_stmt.ok_or_else(|| {
273 WebauthnError::AttestationError(
274 "missing attStmt for the 'packed' format".to_string(),
275 )
276 })?;
277 let entries = cbor_map_entries(val).ok_or_else(|| {
278 WebauthnError::AttestationError(
279 "attStmt is not a CBOR map for the 'packed' format".to_string(),
280 )
281 })?;
282
283 let mut alg: Option<i32> = None;
284 let mut sig: Option<Vec<u8>> = None;
285 let mut x5c: Vec<Vec<u8>> = Vec::new();
286
287 for (label, v) in &entries {
288 match *label {
289 STMT_LABEL_ALG => alg = cbor_alg(v),
290 STMT_LABEL_SIG => sig = cbor_bytes(v),
291 STMT_LABEL_X5C => x5c = cbor_cert_array(v)?,
292 _ => {}
293 }
294 }
295
296 let alg = alg.ok_or_else(|| {
297 WebauthnError::AttestationError("packed attStmt missing 'alg'".to_string())
298 })?;
299 let sig = sig.ok_or_else(|| {
300 WebauthnError::AttestationError("packed attStmt missing 'sig'".to_string())
301 })?;
302
303 Ok(AttestationStatement::Packed { alg, sig, x5c })
304 }
305 AttestationFormat::FidoU2f => {
306 let val = att_stmt.ok_or_else(|| {
307 WebauthnError::AttestationError(
308 "missing attStmt for the 'fido-u2f' format".to_string(),
309 )
310 })?;
311 let entries = cbor_map_entries(val).ok_or_else(|| {
312 WebauthnError::AttestationError(
313 "attStmt is not a CBOR map for the 'fido-u2f' format".to_string(),
314 )
315 })?;
316
317 let mut sig: Option<Vec<u8>> = None;
318 let mut x5c: Vec<Vec<u8>> = Vec::new();
319
320 for (label, v) in &entries {
321 match *label {
322 STMT_LABEL_SIG => sig = cbor_bytes(v),
323 STMT_LABEL_X5C => x5c = cbor_cert_array(v)?,
324 _ => {}
325 }
326 }
327
328 let sig = sig.ok_or_else(|| {
329 WebauthnError::AttestationError("fido-u2f attStmt missing 'sig'".to_string())
330 })?;
331 let [cert] = x5c.as_slice() else {
333 return Err(WebauthnError::AttestationError(
334 "fido-u2f attStmt must carry exactly one x5c certificate".to_string(),
335 ));
336 };
337
338 Ok(AttestationStatement::FidoU2f {
339 sig,
340 cert: cert.clone(),
341 })
342 }
343 AttestationFormat::Unknown => Err(WebauthnError::AttestationError(
345 "unknown attestation format cannot be parsed".to_string(),
346 )),
347 }
348}
349
350fn cbor_alg(val: &Value) -> Option<i32> {
352 match val {
353 Value::Integer(i) => (*i).try_into().ok(),
354 _ => None,
355 }
356}
357
358fn cbor_cert_array(val: &Value) -> Result<Vec<Vec<u8>>, WebauthnError> {
360 match val {
361 Value::Array(items) => {
362 let mut certs = Vec::with_capacity(items.len());
363 for item in items {
364 certs.push(cbor_bytes(item).ok_or_else(|| {
365 WebauthnError::AttestationError(
366 "x5c contains a non-byte-string certificate".to_string(),
367 )
368 })?);
369 }
370 Ok(certs)
371 }
372 _ => Err(WebauthnError::AttestationError(
373 "x5c is not a CBOR array".to_string(),
374 )),
375 }
376}
377
378#[allow(clippy::too_many_arguments)]
403pub fn verify_attestation(
404 fmt: &str,
405 att_stmt: Option<&Value>,
406 auth_data: &[u8],
407 client_data_hash: &[u8],
408 credential_id: &[u8],
409 credential_alg: i32,
410 credential_key: &CosePublicKey,
411 aaguid: [u8; 16],
412 policy: &AttestationPolicy,
413) -> Result<AttestationResult, WebauthnError> {
414 let format = AttestationFormat::from_fmt(fmt);
415
416 if format == AttestationFormat::Unknown {
418 if policy.allow_unknown_formats {
419 return Ok(AttestationResult {
420 format,
421 aaguid: Some(aaguid),
422 trust_level: TrustLevel::None,
423 warnings: vec![warnings::UNVERIFIED_FORMAT],
424 });
425 }
426 return Err(WebauthnError::AttestationError(format!(
427 "unsupported attestation format: '{fmt}'"
428 )));
429 }
430
431 let statement = parse_statement(&format, att_stmt)?;
432
433 match statement {
434 AttestationStatement::None => verify_none_statement(aaguid),
435 AttestationStatement::Packed { alg, sig, x5c } => {
436 if x5c.is_empty() {
437 verify_packed_self(
438 &alg,
439 &sig,
440 auth_data,
441 client_data_hash,
442 credential_alg,
443 credential_key,
444 aaguid,
445 )
446 } else {
447 verify_packed_x5c(
448 &alg,
449 &sig,
450 auth_data,
451 client_data_hash,
452 &x5c,
453 aaguid,
454 policy,
455 )
456 }
457 }
458 AttestationStatement::FidoU2f { sig, cert } => verify_fido_u2f(
459 &sig,
460 &cert,
461 auth_data,
462 client_data_hash,
463 credential_id,
464 credential_alg,
465 credential_key,
466 aaguid,
467 policy,
468 ),
469 }
470}
471
472fn verify_none_statement(aaguid: [u8; 16]) -> Result<AttestationResult, WebauthnError> {
479 Ok(AttestationResult {
480 format: AttestationFormat::None,
481 aaguid: Some(aaguid),
482 trust_level: TrustLevel::None,
483 warnings: Vec::new(),
484 })
485}
486
487fn verify_packed_self(
494 alg: &i32,
495 sig: &[u8],
496 auth_data: &[u8],
497 client_data_hash: &[u8],
498 credential_alg: i32,
499 credential_key: &CosePublicKey,
500 aaguid: [u8; 16],
501) -> Result<AttestationResult, WebauthnError> {
502 if *alg != credential_alg {
503 return Err(WebauthnError::AttestationError(format!(
504 "packed self-attestation alg {alg} does not match credential key alg {credential_alg}"
505 )));
506 }
507
508 let signed = signed_data(auth_data, client_data_hash);
509 verify_cose_signature(*alg, credential_key, &signed, sig)?;
511
512 Ok(AttestationResult {
513 format: AttestationFormat::Packed,
514 aaguid: Some(aaguid),
515 trust_level: TrustLevel::SelfAttested,
516 warnings: Vec::new(),
517 })
518}
519
520fn verify_packed_x5c(
527 alg: &i32,
528 sig: &[u8],
529 auth_data: &[u8],
530 client_data_hash: &[u8],
531 x5c: &[Vec<u8>],
532 aaguid: [u8; 16],
533 policy: &AttestationPolicy,
534) -> Result<AttestationResult, WebauthnError> {
535 if *alg != COSE_ALG_ES256 && *alg != COSE_ALG_RS256 {
538 return Err(WebauthnError::UnsupportedAlgorithm(*alg));
539 }
540
541 let leaf = Cert::from_der(x5c.first().ok_or_else(|| {
542 WebauthnError::AttestationError("packed attStmt has an empty x5c chain".to_string())
543 })?)?;
544
545 let expected = match *alg {
548 COSE_ALG_ES256 => KeyKind::Ec2,
549 _ => KeyKind::Rsa,
550 };
551 if leaf.kind != expected {
552 return Err(WebauthnError::AttestationError(
553 "packed attestation alg does not match the attestation certificate key type"
554 .to_string(),
555 ));
556 }
557
558 let signed = signed_data(auth_data, client_data_hash);
559 verify_signature_with_cert(&leaf, &signed, sig)?;
560
561 check_aaguid_extension(&leaf, &aaguid)?;
564
565 let (trust_level, warnings) = verify_chain(x5c, policy)?;
566
567 Ok(AttestationResult {
568 format: AttestationFormat::Packed,
569 aaguid: Some(aaguid),
570 trust_level,
571 warnings,
572 })
573}
574
575#[allow(clippy::indexing_slicing)] #[allow(clippy::too_many_arguments)]
585fn verify_fido_u2f(
586 sig: &[u8],
587 cert_der: &[u8],
588 auth_data: &[u8],
589 client_data_hash: &[u8],
590 credential_id: &[u8],
591 credential_alg: i32,
592 credential_key: &CosePublicKey,
593 aaguid: [u8; 16],
594 policy: &AttestationPolicy,
595) -> Result<AttestationResult, WebauthnError> {
596 if credential_alg != COSE_ALG_ES256 {
599 return Err(WebauthnError::AttestationError(format!(
600 "fido-u2f registration requires an ES256 credential key, got alg {credential_alg}"
601 )));
602 }
603 let CosePublicKey::Ec2 { x, y } = credential_key else {
604 return Err(WebauthnError::AttestationError(
605 "fido-u2f registration requires an EC2 credential key".to_string(),
606 ));
607 };
608
609 let cert = Cert::from_der(cert_der)?;
610 if cert.kind != KeyKind::Ec2 {
611 return Err(WebauthnError::AttestationError(
612 "fido-u2f attestation certificate must carry an EC P-256 key".to_string(),
613 ));
614 }
615
616 if x.len() != 32 || y.len() != 32 {
618 return Err(WebauthnError::AttestationError(
619 "fido-u2f credential key coordinates must be 32 bytes (P-256)".to_string(),
620 ));
621 }
622 let mut public_key_u2f = Vec::with_capacity(65);
623 public_key_u2f.push(0x04);
624 public_key_u2f.extend_from_slice(x);
625 public_key_u2f.extend_from_slice(y);
626
627 if auth_data.len() < 32 {
628 return Err(WebauthnError::AttestationError(
629 "authenticator data too short for fido-u2f verification".to_string(),
630 ));
631 }
632 let mut verification_data = Vec::with_capacity(1 + 32 + 32 + credential_id.len() + 65);
633 verification_data.push(0x00);
634 verification_data.extend_from_slice(&auth_data[..32]); verification_data.extend_from_slice(client_data_hash);
637 verification_data.extend_from_slice(credential_id);
638 verification_data.extend_from_slice(&public_key_u2f);
639
640 verify_signature_with_cert(&cert, &verification_data, sig)?;
641
642 let (trust_level, warnings) = verify_chain(&[cert_der.to_vec()], policy)?;
643
644 Ok(AttestationResult {
645 format: AttestationFormat::FidoU2f,
646 aaguid: Some(aaguid),
647 trust_level,
648 warnings,
649 })
650}
651
652#[derive(Debug, Clone, Copy, PartialEq, Eq)]
658enum KeyKind {
659 Ec2,
661 Rsa,
663}
664
665enum CertPublicKey {
667 EcPoint(Vec<u8>),
669 RsaKey(Vec<u8>),
672}
673
674struct Cert {
676 tbs: Vec<u8>,
678 signature: Vec<u8>,
680 signature_alg_oid: Vec<u8>,
682 verifier_key: CertPublicKey,
684 issuer: Vec<u8>,
686 subject: Vec<u8>,
688 kind: KeyKind,
690 not_before: i64,
692 not_after: i64,
693 extensions: Vec<(Vec<u8>, Vec<u8>)>,
695}
696
697impl Cert {
698 fn from_der(der: &[u8]) -> Result<Self, WebauthnError> {
706 let (rem, parsed) = parse_x509_certificate(der).map_err(|e| {
707 WebauthnError::AttestationError(format!("attestation certificate parse error: {e}"))
708 })?;
709 if !rem.is_empty() {
710 return Err(WebauthnError::AttestationError(
711 "trailing bytes after attestation certificate".to_string(),
712 ));
713 }
714
715 let tbs = &parsed.tbs_certificate;
716 if tbs.version() != X509Version::V3 {
717 return Err(WebauthnError::AttestationError(
718 "attestation certificate must be X.509 v3".to_string(),
719 ));
720 }
721
722 let spki = tbs.public_key();
723 let spki_alg = spki.algorithm.algorithm.as_bytes();
724 let (verifier_key, kind) = if spki_alg == OID_ALG_EC_PUBLIC_KEY {
725 let point = spki.subject_public_key.data.to_vec();
726 if point.first() != Some(&0x04) || point.len() != 65 {
727 return Err(WebauthnError::AttestationError(
728 "attestation certificate EC key is not an uncompressed P-256 point".to_string(),
729 ));
730 }
731 (CertPublicKey::EcPoint(point), KeyKind::Ec2)
732 } else if spki_alg == OID_ALG_RSA_ENCRYPTION {
733 (
736 CertPublicKey::RsaKey(spki.subject_public_key.data.to_vec()),
737 KeyKind::Rsa,
738 )
739 } else {
740 return Err(WebauthnError::AttestationError(
741 "attestation certificate uses an unsupported public key algorithm".to_string(),
742 ));
743 };
744
745 let extensions = tbs
746 .extensions()
747 .iter()
748 .map(|ext| (ext.oid.as_bytes().to_vec(), ext.value.to_vec()))
749 .collect();
750
751 Ok(Self {
752 tbs: tbs.as_ref().to_vec(),
753 signature: parsed.signature_value.data.to_vec(),
754 signature_alg_oid: parsed.signature_algorithm.algorithm.as_bytes().to_vec(),
755 verifier_key,
756 issuer: tbs.issuer().as_raw().to_vec(),
757 subject: tbs.subject().as_raw().to_vec(),
758 kind,
759 not_before: tbs.validity().not_before.timestamp(),
760 not_after: tbs.validity().not_after.timestamp(),
761 extensions,
762 })
763 }
764
765 fn is_valid_at(&self, now: i64) -> bool {
767 self.not_before <= now && now <= self.not_after
768 }
769
770 fn verify_signed_by(&self, parent: &Cert) -> Result<(), WebauthnError> {
779 if self.signature_alg_oid == OID_SIG_ECDSA_WITH_SHA256 {
780 if parent.kind != KeyKind::Ec2 {
781 return Err(WebauthnError::AttestationError(
782 "certificate signature algorithm does not match issuer key type".to_string(),
783 ));
784 }
785 let CertPublicKey::EcPoint(point) = &parent.verifier_key else {
786 return Err(WebauthnError::AttestationError(
787 "issuer key is not an EC point".to_string(),
788 ));
789 };
790 let verifier = ring::signature::UnparsedPublicKey::new(
791 &ring::signature::ECDSA_P256_SHA256_FIXED,
792 point,
793 );
794 verifier
795 .verify(&self.tbs, &self.signature)
796 .map_err(|_| WebauthnError::SignatureVerificationFailed)
797 } else if self.signature_alg_oid == OID_SIG_SHA256_WITH_RSA {
798 if parent.kind != KeyKind::Rsa {
799 return Err(WebauthnError::AttestationError(
800 "certificate signature algorithm does not match issuer key type".to_string(),
801 ));
802 }
803 let CertPublicKey::RsaKey(rsa_key) = &parent.verifier_key else {
804 return Err(WebauthnError::AttestationError(
805 "issuer key is not an RSA key".to_string(),
806 ));
807 };
808 let verifier = ring::signature::UnparsedPublicKey::new(
809 &ring::signature::RSA_PKCS1_2048_8192_SHA256,
810 rsa_key,
811 );
812 verifier
813 .verify(&self.tbs, &self.signature)
814 .map_err(|_| WebauthnError::SignatureVerificationFailed)
815 } else {
816 Err(WebauthnError::AttestationError(
817 "certificate uses an unsupported signature algorithm (only ecdsa-with-SHA256 \
818 and sha256WithRSAEncryption are accepted)"
819 .to_string(),
820 ))
821 }
822 }
823
824 fn is_ca(&self) -> Option<bool> {
826 let (_, value) = self
827 .extensions
828 .iter()
829 .find(|(oid, _)| oid == OID_EXT_BASIC_CONSTRAINTS)?;
830 parse_basic_constraints(value)
831 }
832}
833
834fn parse_basic_constraints(value: &[u8]) -> Option<bool> {
840 let content = der_content(value, 0x30)?;
841 if content.first() == Some(&0x01) && content.len() >= 3 {
842 return Some(content.get(2).copied() == Some(0xFF));
844 }
845 Some(false)
846}
847
848fn der_content(bytes: &[u8], tag: u8) -> Option<&[u8]> {
851 if bytes.first() != Some(&tag) {
852 return None;
853 }
854 let first = *bytes.get(1)?;
855 if first < 0x80 {
856 bytes.get(2..2 + first as usize)
857 } else {
858 let n = (first & 0x7F) as usize;
859 if n == 0 || n > 4 {
860 return None;
861 }
862 let mut len = 0usize;
863 for b in bytes.get(2..2 + n)? {
864 len = (len << 8) | (*b as usize);
865 }
866 bytes.get(2 + n..2 + n + len)
867 }
868}
869
870fn check_aaguid_extension(cert: &Cert, aaguid: &[u8; 16]) -> Result<(), WebauthnError> {
877 let Some((_, value)) = cert
878 .extensions
879 .iter()
880 .find(|(oid, _)| oid == OID_EXT_FIDO_GEN_CE_AAGUID)
881 else {
882 return Ok(()); };
884
885 let bad = || {
888 WebauthnError::AttestationError("id-fido-gen-ce-aaguid extension is malformed".to_string())
889 };
890 let content = der_content(value, 0x04).ok_or_else(bad)?;
891 if content.len() != 16 {
892 return Err(bad());
893 }
894 if content != aaguid.as_slice() {
895 return Err(WebauthnError::AttestationError(
896 "id-fido-gen-ce-aaguid extension does not match the attested credential data AAGUID"
897 .to_string(),
898 ));
899 }
900 Ok(())
901}
902
903fn verify_chain(
919 x5c: &[Vec<u8>],
920 policy: &AttestationPolicy,
921) -> Result<(TrustLevel, Vec<&'static str>), WebauthnError> {
922 let now = SystemTime::now()
923 .duration_since(UNIX_EPOCH)
924 .map(|d| d.as_secs() as i64)
925 .map_err(|_| {
926 WebauthnError::AttestationError("system clock before unix epoch".to_string())
927 })?;
928
929 let mut certs = Vec::with_capacity(x5c.len());
930 for der in x5c {
931 let cert = Cert::from_der(der)?;
932 if !cert.is_valid_at(now) {
933 return Err(WebauthnError::AttestationError(
934 "attestation certificate is expired or not yet valid".to_string(),
935 ));
936 }
937 certs.push(cert);
938 }
939
940 #[allow(clippy::indexing_slicing)]
944 for pair in certs.windows(2) {
945 let (child, parent) = (&pair[0], &pair[1]);
946 if child.issuer != parent.subject {
947 return Err(WebauthnError::AttestationError(
948 "attestation chain issuer/subject names do not link".to_string(),
949 ));
950 }
951 child.verify_signed_by(parent)?;
952 }
953
954 for parent in certs.iter().skip(1) {
956 if parent.is_ca() != Some(true) {
957 return Err(WebauthnError::AttestationError(
958 "attestation chain intermediate lacks basicConstraints cA=true".to_string(),
959 ));
960 }
961 }
962
963 let root = certs
964 .last()
965 .ok_or_else(|| WebauthnError::AttestationError("attestation chain is empty".to_string()))?;
966 let root_der = x5c
967 .last()
968 .ok_or_else(|| WebauthnError::AttestationError("attestation chain is empty".to_string()))?;
969
970 for anchor in &policy.trust_anchors {
972 if root_der == anchor {
974 return Ok((TrustLevel::AttCa, Vec::new()));
975 }
976 let anchor_cert = Cert::from_der(anchor)?;
978 if root.issuer == anchor_cert.subject && root.verify_signed_by(&anchor_cert).is_ok() {
979 return Ok((TrustLevel::AttCa, Vec::new()));
980 }
981 }
982 if !policy.trust_anchors.is_empty() {
983 return Err(WebauthnError::AttestationError(
984 "attestation chain does not terminate at a configured trust anchor".to_string(),
985 ));
986 }
987
988 let self_signed = root.issuer == root.subject && root.verify_signed_by(root).is_ok();
991 if !self_signed {
992 return Err(WebauthnError::AttestationError(
993 "attestation chain root is not self-signed and no trust anchors are configured"
994 .to_string(),
995 ));
996 }
997
998 Ok((TrustLevel::BasicAtt, vec![warnings::UNANCHORED_CHAIN]))
999}
1000
1001fn signed_data(auth_data: &[u8], client_data_hash: &[u8]) -> Vec<u8> {
1009 let mut signed = Vec::with_capacity(auth_data.len() + client_data_hash.len());
1010 signed.extend_from_slice(auth_data);
1011 signed.extend_from_slice(client_data_hash);
1012 signed
1013}
1014
1015fn verify_signature_with_cert(cert: &Cert, data: &[u8], sig: &[u8]) -> Result<(), WebauthnError> {
1023 match &cert.verifier_key {
1024 CertPublicKey::EcPoint(point) => {
1025 let verifier = ring::signature::UnparsedPublicKey::new(
1026 &ring::signature::ECDSA_P256_SHA256_FIXED,
1027 point,
1028 );
1029 verifier
1030 .verify(data, sig)
1031 .map_err(|_| WebauthnError::SignatureVerificationFailed)
1032 }
1033 CertPublicKey::RsaKey(rsa_key) => {
1034 let verifier = ring::signature::UnparsedPublicKey::new(
1035 &ring::signature::RSA_PKCS1_2048_8192_SHA256,
1036 rsa_key,
1037 );
1038 verifier
1039 .verify(data, sig)
1040 .map_err(|_| WebauthnError::SignatureVerificationFailed)
1041 }
1042 }
1043}
1044
1045#[allow(
1049 clippy::unwrap_used,
1050 clippy::expect_used,
1051 clippy::indexing_slicing,
1052 clippy::panic
1053)]
1054#[cfg(test)]
1055mod tests {
1056 use super::*;
1057 use ring::signature::{
1058 EcdsaKeyPair, KeyPair as _, RsaKeyPair, ECDSA_P256_SHA256_FIXED_SIGNING, RSA_PKCS1_SHA256,
1059 };
1060 use sha2::Digest;
1061
1062 const OID_RSA_ENCRYPTION: &[u8] = &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01];
1072 const OID_SIG_ECDSA_SHA256: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02];
1073 const OID_SIG_SHA256_RSA: &[u8] = &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B];
1074 const OID_SIG_ECDSA_SHA512: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x04];
1075 const OID_EC_PUBLIC_KEY: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01];
1076 const OID_PRIME256V1: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07];
1077 const OID_ED25519: &[u8] = &[0x2B, 0x65, 0x70];
1078 const OID_CN: &[u8] = &[0x55, 0x04, 0x03];
1079 const OID_FIDO_AAGUID: &[u8] = &[0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xE5, 0x14, 0x01, 0x04];
1080 const OID_BASIC_CONSTRAINTS: &[u8] = &[0x55, 0x1D, 0x13];
1081
1082 fn rsa_signing_key() -> RsaKeyPair {
1083 let (_, _, pkcs8) = crate::crypto::tests::rsa_test_material();
1084 RsaKeyPair::from_pkcs8(&pkcs8).unwrap()
1085 }
1086
1087 fn rsa_spki() -> Vec<u8> {
1090 der_seq(&[&alg_id(OID_RSA_ENCRYPTION), &der_bit(&rsa_pubkey_der())])
1091 }
1092
1093 fn rsa_pubkey_der() -> Vec<u8> {
1095 crate::crypto::tests::rsa_test_spki()
1096 }
1097
1098 fn der_len(len: usize) -> Vec<u8> {
1099 if len < 0x80 {
1100 vec![len as u8]
1101 } else if len < 0x100 {
1102 vec![0x81, len as u8]
1103 } else {
1104 vec![0x82, (len >> 8) as u8, (len & 0xFF) as u8]
1105 }
1106 }
1107
1108 fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
1109 let mut out = vec![tag];
1110 out.extend(der_len(content.len()));
1111 out.extend_from_slice(content);
1112 out
1113 }
1114
1115 fn der_seq(parts: &[&[u8]]) -> Vec<u8> {
1116 let mut content = Vec::new();
1117 for part in parts {
1118 content.extend_from_slice(part);
1119 }
1120 tlv(0x30, &content)
1121 }
1122
1123 fn der_set(content: &[u8]) -> Vec<u8> {
1124 tlv(0x31, content)
1125 }
1126
1127 fn der_integer(bytes: &[u8]) -> Vec<u8> {
1128 tlv(0x02, bytes)
1129 }
1130
1131 fn der_oid(bytes: &[u8]) -> Vec<u8> {
1132 tlv(0x06, bytes)
1133 }
1134
1135 fn der_bit(bytes: &[u8]) -> Vec<u8> {
1136 let mut content = vec![0x00];
1137 content.extend_from_slice(bytes);
1138 tlv(0x03, &content)
1139 }
1140
1141 fn der_utf8(s: &str) -> Vec<u8> {
1142 tlv(0x0C, s.as_bytes())
1143 }
1144
1145 fn der_utc(s: &str) -> Vec<u8> {
1146 tlv(0x17, s.as_bytes())
1147 }
1148
1149 fn der_null() -> Vec<u8> {
1150 vec![0x05, 0x00]
1151 }
1152
1153 fn alg_id(oid: &[u8]) -> Vec<u8> {
1154 der_seq(&[&der_oid(oid), &der_null()])
1155 }
1156
1157 fn name_cn(cn: &str) -> Vec<u8> {
1158 der_seq(&[&der_set(&der_seq(&[&der_oid(OID_CN), &der_utf8(cn)]))])
1159 }
1160
1161 fn validity() -> Vec<u8> {
1162 der_seq(&[&der_utc("240101000000Z"), &der_utc("400101000000Z")])
1163 }
1164
1165 fn expired_validity() -> Vec<u8> {
1166 der_seq(&[&der_utc("200101000000Z"), &der_utc("210101000000Z")])
1167 }
1168
1169 fn ec_spki_from(key: &EcdsaKeyPair) -> Vec<u8> {
1170 let point = key.public_key().as_ref().to_vec();
1171 der_seq(&[
1172 &der_seq(&[&der_oid(OID_EC_PUBLIC_KEY), &der_oid(OID_PRIME256V1)]),
1173 &der_bit(&point),
1174 ])
1175 }
1176
1177 fn ed25519_spki() -> Vec<u8> {
1178 der_seq(&[&alg_id(OID_ED25519), &der_bit(&[0x42; 32])])
1179 }
1180
1181 fn extension(oid: &[u8], value: &[u8]) -> Vec<u8> {
1182 der_seq(&[&der_oid(oid), &tlv(0x04, value)])
1183 }
1184
1185 fn extensions_wrapper(exts: &[Vec<u8>]) -> Vec<u8> {
1186 let mut content = Vec::new();
1187 for ext in exts {
1188 content.extend_from_slice(ext);
1189 }
1190 tlv(0xA3, &der_seq(&[&content]))
1191 }
1192
1193 fn tbs_cert(
1195 sig_alg_oid: &[u8],
1196 issuer: &[u8],
1197 subject: &[u8],
1198 spki: &[u8],
1199 exts: &[Vec<u8>],
1200 validity_bytes: &[u8],
1201 v1: bool,
1202 ) -> Vec<u8> {
1203 let version = tlv(0xA0, &der_integer(&[2]));
1204 let serial = der_integer(&[1]);
1205 let sig_alg_tlv = alg_id(sig_alg_oid);
1206 let mut parts: Vec<&[u8]> = Vec::new();
1207 if !v1 {
1208 parts.push(&version);
1209 }
1210 parts.push(&serial);
1211 parts.push(&sig_alg_tlv);
1212 parts.push(issuer);
1213 parts.push(validity_bytes);
1214 parts.push(subject);
1215 parts.push(spki);
1216 let ext_wrapper = extensions_wrapper(exts);
1217 if !exts.is_empty() {
1218 parts.push(&ext_wrapper);
1219 }
1220 der_seq(&parts)
1221 }
1222
1223 fn finish_cert(tbs: &[u8], sig_alg_oid: &[u8], sig: &[u8]) -> Vec<u8> {
1225 der_seq(&[tbs, &alg_id(sig_alg_oid), &der_bit(sig)])
1226 }
1227
1228 fn ec_cert(
1231 subject_cn: &str,
1232 issuer_name: &[u8],
1233 key: &EcdsaKeyPair,
1234 signer: Option<&EcdsaKeyPair>,
1235 exts: &[Vec<u8>],
1236 validity_bytes: &[u8],
1237 ) -> Vec<u8> {
1238 let subject = name_cn(subject_cn);
1239 let spki = ec_spki_from(key);
1240 let tbs = tbs_cert(
1241 OID_SIG_ECDSA_SHA256,
1242 issuer_name,
1243 &subject,
1244 &spki,
1245 exts,
1246 validity_bytes,
1247 false,
1248 );
1249 let rng = ring::rand::SystemRandom::new();
1250 let sig = signer.unwrap_or(key).sign(&rng, &tbs).unwrap();
1251 finish_cert(&tbs, OID_SIG_ECDSA_SHA256, sig.as_ref())
1252 }
1253
1254 fn ec_self_cert(cn: &str, key: &EcdsaKeyPair, exts: &[Vec<u8>]) -> Vec<u8> {
1256 let name = name_cn(cn);
1257 ec_cert(cn, &name, key, None, exts, &validity())
1258 }
1259
1260 fn rsa_cert(
1263 subject_cn: &str,
1264 issuer_name: &[u8],
1265 key: &RsaKeyPair,
1266 signer: Option<&RsaKeyPair>,
1267 exts: &[Vec<u8>],
1268 ) -> Vec<u8> {
1269 let subject = name_cn(subject_cn);
1270 let spki = rsa_spki();
1271 let tbs = tbs_cert(
1272 OID_SIG_SHA256_RSA,
1273 issuer_name,
1274 &subject,
1275 &spki,
1276 exts,
1277 &validity(),
1278 false,
1279 );
1280 let rng = ring::rand::SystemRandom::new();
1281 let mut sig = vec![0u8; key.public().modulus_len()];
1282 signer
1283 .unwrap_or(key)
1284 .sign(&RSA_PKCS1_SHA256, &rng, &tbs, &mut sig)
1285 .unwrap();
1286 finish_cert(&tbs, OID_SIG_SHA256_RSA, &sig)
1287 }
1288
1289 fn hand_cert(sig_alg: &[u8], issuer: &[u8], subject: &[u8], spki: &[u8], v1: bool) -> Vec<u8> {
1292 let tbs = tbs_cert(sig_alg, issuer, subject, spki, &[], &validity(), v1);
1293 der_seq(&[&tbs, &alg_id(sig_alg), &der_bit(&[0xAA; 64])])
1294 }
1295
1296 fn bc_ca_true() -> Vec<u8> {
1298 extension(OID_BASIC_CONSTRAINTS, &[0x30, 0x03, 0x01, 0x01, 0xFF])
1299 }
1300
1301 fn bc_ca_false() -> Vec<u8> {
1303 extension(OID_BASIC_CONSTRAINTS, &[0x30, 0x03, 0x01, 0x01, 0x00])
1304 }
1305
1306 fn ec_key() -> EcdsaKeyPair {
1311 let rng = ring::rand::SystemRandom::new();
1312 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap();
1313 EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng).unwrap()
1314 }
1315
1316 fn ec_chain() -> (Vec<u8>, Vec<u8>, Vec<u8>, EcdsaKeyPair) {
1319 let anchor_key = ec_key();
1320 let issuer_key = ec_key();
1321 let leaf_key = ec_key();
1322 let anchor = ec_self_cert("anchor", &anchor_key, &[bc_ca_true()]);
1323 let issuer = ec_cert(
1324 "issuer",
1325 &name_cn("anchor"),
1326 &issuer_key,
1327 Some(&anchor_key),
1328 &[bc_ca_true()],
1329 &validity(),
1330 );
1331 let leaf = ec_cert(
1332 "leaf",
1333 &name_cn("issuer"),
1334 &leaf_key,
1335 Some(&issuer_key),
1336 &[],
1337 &validity(),
1338 );
1339 (leaf, issuer, anchor, leaf_key)
1340 }
1341
1342 fn subject_raw(cert_der: &[u8]) -> Vec<u8> {
1343 let (_, cert) = parse_x509_certificate(cert_der).unwrap();
1344 cert.tbs_certificate.subject().as_raw().to_vec()
1345 }
1346
1347 const AAGUID: [u8; 16] = [7u8; 16];
1352 const CRED_ID: &[u8] = &[0xC1, 0x02, 0x03, 0x04];
1353
1354 fn rng() -> ring::rand::SystemRandom {
1355 ring::rand::SystemRandom::new()
1356 }
1357
1358 fn credential() -> (Vec<u8>, Vec<u8>, CosePublicKey, EcdsaKeyPair) {
1360 let kp = ec_key();
1361 let pub_bytes = kp.public_key().as_ref().to_vec();
1362 let key = CosePublicKey::Ec2 {
1363 x: pub_bytes[1..33].to_vec(),
1364 y: pub_bytes[33..65].to_vec(),
1365 };
1366 let auth_data = vec![0x11u8; 42];
1367 let client_data_hash = sha2::Sha256::digest(b"client data").to_vec();
1368 (auth_data, client_data_hash, key, kp)
1369 }
1370
1371 fn signed_data(auth_data: &[u8], client_data_hash: &[u8]) -> Vec<u8> {
1372 let mut signed = auth_data.to_vec();
1373 signed.extend_from_slice(client_data_hash);
1374 signed
1375 }
1376
1377 fn cbor_int(v: i64) -> Value {
1378 Value::Integer(v.into())
1379 }
1380
1381 fn cbor_bytes_val(bytes: &[u8]) -> Value {
1382 Value::Bytes(bytes.to_vec())
1383 }
1384
1385 fn cbor_map(entries: Vec<(i64, Value)>) -> Value {
1386 Value::Map(entries.into_iter().map(|(k, v)| (cbor_int(k), v)).collect())
1387 }
1388
1389 fn packed_stmt(alg: i32, sig: &[u8], x5c: Vec<Vec<u8>>) -> Value {
1390 let mut entries = vec![(3i64, cbor_int(alg as i64)), (8, cbor_bytes_val(sig))];
1391 if !x5c.is_empty() {
1392 entries.push((
1393 33,
1394 Value::Array(x5c.iter().map(|c| cbor_bytes_val(c)).collect()),
1395 ));
1396 }
1397 cbor_map(entries)
1398 }
1399
1400 fn verify_packed(
1403 statement: &Value,
1404 key: &CosePublicKey,
1405 cred_alg: i32,
1406 policy: &AttestationPolicy,
1407 ) -> Result<AttestationResult, WebauthnError> {
1408 let (auth_data, cdh, _, _) = credential();
1409 verify_attestation(
1410 "packed",
1411 Some(statement),
1412 &auth_data,
1413 &cdh,
1414 CRED_ID,
1415 cred_alg,
1416 key,
1417 AAGUID,
1418 policy,
1419 )
1420 }
1421
1422 fn u2f_msg(auth_data: &[u8], cdh: &[u8], key: &CosePublicKey) -> Vec<u8> {
1423 let CosePublicKey::Ec2 { x, y } = key else {
1424 unreachable!("fixture key is EC2");
1425 };
1426 let mut msg = vec![0x00];
1427 msg.extend_from_slice(&auth_data[..32]);
1428 msg.extend_from_slice(cdh);
1429 msg.extend_from_slice(CRED_ID);
1430 msg.push(0x04);
1431 msg.extend_from_slice(x);
1432 msg.extend_from_slice(y);
1433 msg
1434 }
1435
1436 fn u2f_call(
1437 statement: &Value,
1438 key: &CosePublicKey,
1439 cred_alg: i32,
1440 auth_data: &[u8],
1441 cdh: &[u8],
1442 ) -> Result<AttestationResult, WebauthnError> {
1443 verify_attestation(
1444 "fido-u2f",
1445 Some(statement),
1446 auth_data,
1447 cdh,
1448 CRED_ID,
1449 cred_alg,
1450 key,
1451 AAGUID,
1452 &AttestationPolicy::default(),
1453 )
1454 }
1455
1456 #[test]
1461 fn format_names_roundtrip() {
1462 assert_eq!(AttestationFormat::from_fmt("none"), AttestationFormat::None);
1463 assert_eq!(
1464 AttestationFormat::from_fmt("packed"),
1465 AttestationFormat::Packed
1466 );
1467 assert_eq!(
1468 AttestationFormat::from_fmt("fido-u2f"),
1469 AttestationFormat::FidoU2f
1470 );
1471 assert_eq!(
1472 AttestationFormat::from_fmt("android-key"),
1473 AttestationFormat::Unknown
1474 );
1475 assert_eq!(AttestationFormat::None.as_fmt(), "none");
1476 assert_eq!(AttestationFormat::Packed.as_fmt(), "packed");
1477 assert_eq!(AttestationFormat::FidoU2f.as_fmt(), "fido-u2f");
1478 assert_eq!(AttestationFormat::Unknown.as_fmt(), "unknown");
1479 }
1480
1481 #[test]
1482 fn policy_strict_is_default() {
1483 let policy = AttestationPolicy::strict();
1484 assert!(policy.trust_anchors.is_empty());
1485 assert!(!policy.allow_unknown_formats);
1486 }
1487
1488 #[test]
1489 fn parse_none_statement() {
1490 assert_eq!(
1491 parse_statement(&AttestationFormat::None, None).unwrap(),
1492 AttestationStatement::None
1493 );
1494 assert_eq!(
1495 parse_statement(&AttestationFormat::None, Some(&cbor_map(vec![]))).unwrap(),
1496 AttestationStatement::None
1497 );
1498 let err = parse_statement(
1499 &AttestationFormat::None,
1500 Some(&cbor_map(vec![(1, cbor_int(0))])),
1501 )
1502 .unwrap_err();
1503 assert!(matches!(err, WebauthnError::AttestationError(msg) if msg.contains("empty map")));
1504 }
1505
1506 #[test]
1507 fn parse_packed_statement_happy_and_missing() {
1508 let stmt = packed_stmt(COSE_ALG_ES256, &[0xAA; 64], vec![]);
1509 match parse_statement(&AttestationFormat::Packed, Some(&stmt)).unwrap() {
1510 AttestationStatement::Packed { alg, sig, x5c } => {
1511 assert_eq!(alg, COSE_ALG_ES256);
1512 assert_eq!(sig, vec![0xAA; 64]);
1513 assert!(x5c.is_empty());
1514 }
1515 other => panic!("expected packed statement, got {other:?}"),
1516 }
1517
1518 let err = parse_statement(&AttestationFormat::Packed, None).unwrap_err();
1519 assert!(
1520 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("missing attStmt"))
1521 );
1522 }
1523
1524 #[test]
1525 fn parse_packed_statement_errors() {
1526 let err = parse_statement(&AttestationFormat::Packed, Some(&cbor_int(5))).unwrap_err();
1528 assert!(
1529 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("not a CBOR map"))
1530 );
1531
1532 let no_alg = cbor_map(vec![(8, cbor_bytes_val(&[0xAA; 64]))]);
1534 let err = parse_statement(&AttestationFormat::Packed, Some(&no_alg)).unwrap_err();
1535 assert!(
1536 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("missing 'alg'"))
1537 );
1538
1539 let huge_alg = cbor_map(vec![
1541 (3, cbor_int(i64::from(u32::MAX) + 1)),
1542 (8, cbor_bytes_val(&[0xAA; 64])),
1543 ]);
1544 let err = parse_statement(&AttestationFormat::Packed, Some(&huge_alg)).unwrap_err();
1545 assert!(
1546 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("missing 'alg'"))
1547 );
1548
1549 let no_sig = cbor_map(vec![(3, cbor_int(COSE_ALG_ES256 as i64))]);
1551 let err = parse_statement(&AttestationFormat::Packed, Some(&no_sig)).unwrap_err();
1552 assert!(
1553 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("missing 'sig'"))
1554 );
1555
1556 let bad_x5c = cbor_map(vec![
1558 (3, cbor_int(COSE_ALG_ES256 as i64)),
1559 (8, cbor_bytes_val(&[0xAA; 64])),
1560 (33, cbor_int(1)),
1561 ]);
1562 let err = parse_statement(&AttestationFormat::Packed, Some(&bad_x5c)).unwrap_err();
1563 assert!(
1564 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("not a CBOR array"))
1565 );
1566
1567 let bad_entry = cbor_map(vec![
1569 (3, cbor_int(COSE_ALG_ES256 as i64)),
1570 (8, cbor_bytes_val(&[0xAA; 64])),
1571 (33, Value::Array(vec![cbor_int(9)])),
1572 ]);
1573 let err = parse_statement(&AttestationFormat::Packed, Some(&bad_entry)).unwrap_err();
1574 assert!(
1575 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("non-byte-string"))
1576 );
1577 }
1578
1579 #[test]
1580 fn parse_fido_u2f_statement() {
1581 let cert = vec![0x30, 0x01, 0x00]; let stmt = cbor_map(vec![
1583 (8, cbor_bytes_val(&[0xBB; 70])),
1584 (33, Value::Array(vec![cbor_bytes_val(&cert)])),
1585 ]);
1586 match parse_statement(&AttestationFormat::FidoU2f, Some(&stmt)).unwrap() {
1587 AttestationStatement::FidoU2f { sig, cert: c } => {
1588 assert_eq!(sig, vec![0xBB; 70]);
1589 assert_eq!(c, cert);
1590 }
1591 other => panic!("expected fido-u2f statement, got {other:?}"),
1592 }
1593
1594 let cases: [(Option<Value>, &str); 3] = [
1596 (None, "missing attStmt"),
1597 (Some(cbor_int(1)), "not a CBOR map"),
1598 (
1599 Some(cbor_map(vec![(
1600 33,
1601 Value::Array(vec![cbor_bytes_val(&cert)]),
1602 )])),
1603 "missing 'sig'",
1604 ),
1605 ];
1606 for (stmt, fragment) in cases {
1607 let err = parse_statement(&AttestationFormat::FidoU2f, stmt.as_ref()).unwrap_err();
1608 assert!(
1609 matches!(err, WebauthnError::AttestationError(msg) if msg.contains(fragment)),
1610 "unexpected error for {fragment}"
1611 );
1612 }
1613
1614 let no_cert = cbor_map(vec![
1616 (8, cbor_bytes_val(&[0u8; 8])),
1617 (33, Value::Array(vec![])),
1618 ]);
1619 let err = parse_statement(&AttestationFormat::FidoU2f, Some(&no_cert)).unwrap_err();
1620 assert!(matches!(err, WebauthnError::AttestationError(msg) if msg.contains("exactly one")));
1621
1622 let two_certs = cbor_map(vec![
1623 (8, cbor_bytes_val(&[0u8; 8])),
1624 (
1625 33,
1626 Value::Array(vec![cbor_bytes_val(&cert), cbor_bytes_val(&cert)]),
1627 ),
1628 ]);
1629 let err = parse_statement(&AttestationFormat::FidoU2f, Some(&two_certs)).unwrap_err();
1630 assert!(matches!(err, WebauthnError::AttestationError(msg) if msg.contains("exactly one")));
1631 }
1632
1633 #[test]
1634 fn parse_unknown_statement_rejected() {
1635 let err = parse_statement(&AttestationFormat::Unknown, None).unwrap_err();
1636 assert!(
1637 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("cannot be parsed"))
1638 );
1639 }
1640
1641 #[test]
1646 fn unknown_format_strict_rejected_escape_allowed() {
1647 let (auth_data, cdh, key, _) = credential();
1648 let err = verify_attestation(
1649 "android-key",
1650 None,
1651 &auth_data,
1652 &cdh,
1653 CRED_ID,
1654 COSE_ALG_ES256,
1655 &key,
1656 AAGUID,
1657 &AttestationPolicy::default(),
1658 )
1659 .unwrap_err();
1660 assert!(
1661 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("unsupported attestation format"))
1662 );
1663
1664 let policy = AttestationPolicy {
1665 allow_unknown_formats: true,
1666 ..AttestationPolicy::default()
1667 };
1668 let result = verify_attestation(
1669 "android-key",
1670 None,
1671 &auth_data,
1672 &cdh,
1673 CRED_ID,
1674 COSE_ALG_ES256,
1675 &key,
1676 AAGUID,
1677 &policy,
1678 )
1679 .unwrap();
1680 assert_eq!(result.format, AttestationFormat::Unknown);
1681 assert_eq!(result.trust_level, TrustLevel::None);
1682 assert_eq!(result.warnings, vec![warnings::UNVERIFIED_FORMAT]);
1683 assert_eq!(result.aaguid, Some(AAGUID));
1684 }
1685
1686 #[test]
1687 fn none_format_verification() {
1688 let (auth_data, cdh, key, _) = credential();
1689 let result = verify_attestation(
1690 "none",
1691 Some(&cbor_map(vec![])),
1692 &auth_data,
1693 &cdh,
1694 CRED_ID,
1695 COSE_ALG_ES256,
1696 &key,
1697 AAGUID,
1698 &AttestationPolicy::default(),
1699 )
1700 .unwrap();
1701 assert_eq!(result.format, AttestationFormat::None);
1702 assert_eq!(result.trust_level, TrustLevel::None);
1703 assert!(result.warnings.is_empty());
1704 assert_eq!(result.aaguid, Some(AAGUID));
1705 }
1706
1707 #[test]
1712 fn packed_self_attestation_ok() {
1713 let (auth_data, cdh, key, kp) = credential();
1714 let sig = kp.sign(&rng(), &signed_data(&auth_data, &cdh)).unwrap();
1715 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), Vec::new());
1716 let result = verify_packed(
1717 &statement,
1718 &key,
1719 COSE_ALG_ES256,
1720 &AttestationPolicy::default(),
1721 )
1722 .unwrap();
1723 assert_eq!(result.format, AttestationFormat::Packed);
1724 assert_eq!(result.trust_level, TrustLevel::SelfAttested);
1725 assert!(result.warnings.is_empty());
1726 assert_eq!(result.aaguid, Some(AAGUID));
1727 }
1728
1729 #[test]
1730 fn packed_self_alg_mismatch_rejected() {
1731 let (_, _, key, _) = credential();
1733 let statement = packed_stmt(COSE_ALG_RS256, &[0u8; 256], Vec::new());
1734 let err = verify_packed(
1735 &statement,
1736 &key,
1737 COSE_ALG_ES256,
1738 &AttestationPolicy::default(),
1739 )
1740 .unwrap_err();
1741 assert!(
1742 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("does not match credential key alg"))
1743 );
1744 }
1745
1746 #[test]
1747 fn packed_self_bad_signature_rejected() {
1748 let (_, _, key, _) = credential();
1749 let statement = packed_stmt(COSE_ALG_ES256, &[0u8; 64], Vec::new());
1750 let err = verify_packed(
1751 &statement,
1752 &key,
1753 COSE_ALG_ES256,
1754 &AttestationPolicy::default(),
1755 )
1756 .unwrap_err();
1757 assert!(matches!(err, WebauthnError::SignatureVerificationFailed));
1758 }
1759
1760 use crate::crypto::{COSE_ALG_EDDSA, COSE_ALG_ES384};
1765
1766 fn credential_p384() -> (Vec<u8>, Vec<u8>, CosePublicKey, EcdsaKeyPair) {
1768 use ring::signature::{EcdsaKeyPair as P384Pair, ECDSA_P384_SHA384_FIXED_SIGNING};
1769 let rng = ring::rand::SystemRandom::new();
1770 let pkcs8 = P384Pair::generate_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, &rng).unwrap();
1771 let kp =
1772 P384Pair::from_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, pkcs8.as_ref(), &rng).unwrap();
1773 let pub_bytes = kp.public_key().as_ref();
1774 let key = CosePublicKey::Ec2 {
1775 x: pub_bytes[1..49].to_vec(),
1776 y: pub_bytes[49..97].to_vec(),
1777 };
1778 let auth_data = vec![0x11u8; 42];
1779 let client_data_hash = sha2::Sha256::digest(b"client data").to_vec();
1780 (auth_data, client_data_hash, key, kp)
1781 }
1782
1783 fn credential_ed25519() -> (
1785 Vec<u8>,
1786 Vec<u8>,
1787 CosePublicKey,
1788 ring::signature::Ed25519KeyPair,
1789 ) {
1790 use ring::signature::Ed25519KeyPair;
1791 let rng = ring::rand::SystemRandom::new();
1792 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1793 let kp = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1794 let key = CosePublicKey::Okp {
1795 id: kp.public_key().as_ref().to_vec(),
1796 };
1797 let auth_data = vec![0x11u8; 42];
1798 let client_data_hash = sha2::Sha256::digest(b"client data").to_vec();
1799 (auth_data, client_data_hash, key, kp)
1800 }
1801
1802 #[test]
1805 fn packed_self_es384_credential_ok() {
1806 let (auth_data, cdh, key, kp) = credential_p384();
1807 let sig = kp
1808 .sign(
1809 &ring::rand::SystemRandom::new(),
1810 &signed_data(&auth_data, &cdh),
1811 )
1812 .unwrap();
1813 let statement = packed_stmt(COSE_ALG_ES384, sig.as_ref(), Vec::new());
1814 let result = verify_attestation(
1815 "packed",
1816 Some(&statement),
1817 &auth_data,
1818 &cdh,
1819 CRED_ID,
1820 COSE_ALG_ES384,
1821 &key,
1822 AAGUID,
1823 &AttestationPolicy::default(),
1824 )
1825 .unwrap();
1826 assert_eq!(result.trust_level, TrustLevel::SelfAttested);
1827 }
1828
1829 #[test]
1831 fn packed_self_eddsa_credential_ok() {
1832 let (auth_data, cdh, key, kp) = credential_ed25519();
1833 let sig = kp.sign(&signed_data(&auth_data, &cdh));
1834 let statement = packed_stmt(COSE_ALG_EDDSA, sig.as_ref(), Vec::new());
1835 let result = verify_attestation(
1836 "packed",
1837 Some(&statement),
1838 &auth_data,
1839 &cdh,
1840 CRED_ID,
1841 COSE_ALG_EDDSA,
1842 &key,
1843 AAGUID,
1844 &AttestationPolicy::default(),
1845 )
1846 .unwrap();
1847 assert_eq!(result.trust_level, TrustLevel::SelfAttested);
1848 }
1849
1850 #[test]
1853 fn none_format_es384_credential() {
1854 let (auth_data, cdh, key, _) = credential_p384();
1855 let result = verify_attestation(
1856 "none",
1857 Some(&cbor_map(vec![])),
1858 &auth_data,
1859 &cdh,
1860 CRED_ID,
1861 COSE_ALG_ES384,
1862 &key,
1863 AAGUID,
1864 &AttestationPolicy::default(),
1865 )
1866 .unwrap();
1867 assert_eq!(result.format, AttestationFormat::None);
1868 assert_eq!(result.trust_level, TrustLevel::None);
1869 }
1870
1871 #[test]
1875 fn packed_x5c_es384_statement_alg_rejected() {
1876 let att_key = ec_key();
1877 let cert_der = ec_self_cert("attestor", &att_key, &[]);
1878 let (_, _, cred_key, _) = credential();
1879 let statement = packed_stmt(COSE_ALG_ES384, &[0u8; 96], vec![cert_der]);
1880 let err = verify_packed(
1881 &statement,
1882 &cred_key,
1883 COSE_ALG_ES384,
1884 &AttestationPolicy::default(),
1885 )
1886 .unwrap_err();
1887 assert!(matches!(
1888 err,
1889 WebauthnError::UnsupportedAlgorithm(COSE_ALG_ES384)
1890 ));
1891 }
1892
1893 #[test]
1900 fn packed_x5c_self_signed_ec_is_basic_att() {
1901 let att_key = ec_key();
1902 let cert_der = ec_self_cert("attestor", &att_key, &[]);
1903 let (auth_data, cdh, cred_key, _) = credential();
1904 let sig = att_key
1905 .sign(&rng(), &signed_data(&auth_data, &cdh))
1906 .unwrap();
1907 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![cert_der]);
1908
1909 let result = verify_packed(
1910 &statement,
1911 &cred_key,
1912 COSE_ALG_ES256,
1913 &AttestationPolicy::default(),
1914 )
1915 .unwrap();
1916 assert_eq!(result.trust_level, TrustLevel::BasicAtt);
1917 assert_eq!(result.warnings, vec![warnings::UNANCHORED_CHAIN]);
1918 assert_eq!(result.format, AttestationFormat::Packed);
1919 }
1920
1921 #[test]
1923 fn packed_x5c_rsa_attestation_ok() {
1924 let rsa = rsa_signing_key();
1925 let cert_der = rsa_self_cert(&rsa, &[]);
1926 let (auth_data, cdh, cred_key, _) = credential();
1927 let rng = rng();
1928 let mut stmt_sig = vec![0u8; 256];
1929 rsa.sign(
1930 &RSA_PKCS1_SHA256,
1931 &rng,
1932 &signed_data(&auth_data, &cdh),
1933 &mut stmt_sig,
1934 )
1935 .unwrap();
1936 let statement = packed_stmt(COSE_ALG_RS256, &stmt_sig, vec![cert_der]);
1937
1938 let result = verify_packed(
1939 &statement,
1940 &cred_key,
1941 COSE_ALG_ES256,
1942 &AttestationPolicy::default(),
1943 )
1944 .unwrap();
1945 assert_eq!(result.trust_level, TrustLevel::BasicAtt);
1946 }
1947
1948 fn rsa_self_cert(key: &RsaKeyPair, exts: &[Vec<u8>]) -> Vec<u8> {
1950 let name = name_cn("rsa-attestor");
1951 rsa_cert("rsa-attestor", &name, key, None, exts)
1952 }
1953
1954 #[test]
1955 fn packed_x5c_unsupported_statement_alg_rejected() {
1956 let att_key = ec_key();
1957 let cert_der = ec_self_cert("attestor", &att_key, &[]);
1958 let (_, _, cred_key, _) = credential();
1959 let statement = packed_stmt(-47, &[0u8; 64], vec![cert_der]);
1960 let err = verify_packed(
1961 &statement,
1962 &cred_key,
1963 COSE_ALG_ES256,
1964 &AttestationPolicy::default(),
1965 )
1966 .unwrap_err();
1967 assert!(matches!(err, WebauthnError::UnsupportedAlgorithm(-47)));
1968 }
1969
1970 #[test]
1971 fn packed_x5c_alg_cert_key_type_mismatch_rejected() {
1972 let att_key = ec_key();
1974 let cert_der = ec_self_cert("attestor", &att_key, &[]);
1975 let (_, _, cred_key, _) = credential();
1976 let statement = packed_stmt(COSE_ALG_RS256, &[0u8; 256], vec![cert_der]);
1977 let err = verify_packed(
1978 &statement,
1979 &cred_key,
1980 COSE_ALG_ES256,
1981 &AttestationPolicy::default(),
1982 )
1983 .unwrap_err();
1984 assert!(
1985 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("does not match the attestation certificate key type"))
1986 );
1987 }
1988
1989 #[test]
1990 fn packed_x5c_bad_statement_signature_rejected() {
1991 let att_key = ec_key();
1992 let cert_der = ec_self_cert("attestor", &att_key, &[]);
1993 let (_, _, cred_key, _) = credential();
1994 let statement = packed_stmt(COSE_ALG_ES256, &[0u8; 64], vec![cert_der]);
1995 let err = verify_packed(
1996 &statement,
1997 &cred_key,
1998 COSE_ALG_ES256,
1999 &AttestationPolicy::default(),
2000 )
2001 .unwrap_err();
2002 assert!(matches!(err, WebauthnError::SignatureVerificationFailed));
2003 }
2004
2005 fn x5c_statement(cert_der: Vec<u8>) -> Value {
2009 packed_stmt(COSE_ALG_ES256, &[0u8; 64], vec![cert_der])
2010 }
2011
2012 #[test]
2013 fn packed_x5c_garbage_certificate_rejected() {
2014 let statement = x5c_statement(vec![0xFF; 16]);
2015 let (_, _, cred_key, _) = credential();
2016 let err = verify_packed(
2017 &statement,
2018 &cred_key,
2019 COSE_ALG_ES256,
2020 &AttestationPolicy::default(),
2021 )
2022 .unwrap_err();
2023 assert!(matches!(err, WebauthnError::AttestationError(msg) if msg.contains("parse error")));
2024 }
2025
2026 #[test]
2027 fn packed_x5c_trailing_bytes_rejected() {
2028 let mut cert_der = ec_self_cert("attestor", &ec_key(), &[]);
2029 cert_der.push(0x00);
2030 let statement = x5c_statement(cert_der);
2031 let (_, _, cred_key, _) = credential();
2032 let err = verify_packed(
2033 &statement,
2034 &cred_key,
2035 COSE_ALG_ES256,
2036 &AttestationPolicy::default(),
2037 )
2038 .unwrap_err();
2039 assert!(
2040 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("trailing bytes"))
2041 );
2042 }
2043
2044 #[test]
2045 fn packed_x5c_v1_certificate_rejected() {
2046 let cert = hand_cert(
2047 OID_SIG_ECDSA_SHA256,
2048 &name_cn("self"),
2049 &name_cn("self"),
2050 &ec_spki_from(&ec_key()),
2051 true, );
2053 let statement = x5c_statement(cert);
2054 let (_, _, cred_key, _) = credential();
2055 let err = verify_packed(
2056 &statement,
2057 &cred_key,
2058 COSE_ALG_ES256,
2059 &AttestationPolicy::default(),
2060 )
2061 .unwrap_err();
2062 assert!(
2063 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("must be X.509 v3"))
2064 );
2065 }
2066
2067 #[test]
2068 fn packed_x5c_compressed_ec_point_rejected() {
2069 let compressed_spki = der_seq(&[
2070 &der_seq(&[&der_oid(OID_EC_PUBLIC_KEY), &der_oid(OID_PRIME256V1)]),
2071 &der_bit(&[0x02; 33]), ]);
2073 let cert = hand_cert(
2074 OID_SIG_ECDSA_SHA256,
2075 &name_cn("self"),
2076 &name_cn("self"),
2077 &compressed_spki,
2078 false,
2079 );
2080 let statement = x5c_statement(cert);
2081 let (_, _, cred_key, _) = credential();
2082 let err = verify_packed(
2083 &statement,
2084 &cred_key,
2085 COSE_ALG_ES256,
2086 &AttestationPolicy::default(),
2087 )
2088 .unwrap_err();
2089 assert!(
2090 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("uncompressed P-256 point"))
2091 );
2092 }
2093
2094 #[test]
2095 fn packed_x5c_unsupported_public_key_algorithm_rejected() {
2096 let cert = hand_cert(
2097 OID_SIG_ECDSA_SHA256,
2098 &name_cn("self"),
2099 &name_cn("self"),
2100 &ed25519_spki(),
2101 false,
2102 );
2103 let statement = x5c_statement(cert);
2104 let (_, _, cred_key, _) = credential();
2105 let err = verify_packed(
2106 &statement,
2107 &cred_key,
2108 COSE_ALG_ES256,
2109 &AttestationPolicy::default(),
2110 )
2111 .unwrap_err();
2112 assert!(
2113 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("unsupported public key algorithm"))
2114 );
2115 }
2116
2117 #[test]
2120 fn packed_x5c_chain_carrying_anchor_is_att_ca() {
2121 let anchor_key = ec_key();
2123 let anchor = ec_self_cert("anchor", &anchor_key, &[bc_ca_true()]);
2124 let leaf_key = ec_key();
2125 let leaf = ec_cert(
2126 "leaf",
2127 &name_cn("anchor"),
2128 &leaf_key,
2129 Some(&anchor_key),
2130 &[],
2131 &validity(),
2132 );
2133 let (auth_data, cdh, cred_key, _) = credential();
2134 let sig = leaf_key
2135 .sign(&rng(), &signed_data(&auth_data, &cdh))
2136 .unwrap();
2137 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![leaf, anchor.clone()]);
2138 let policy = AttestationPolicy {
2139 trust_anchors: vec![anchor],
2140 ..AttestationPolicy::default()
2141 };
2142 let result = verify_packed(&statement, &cred_key, COSE_ALG_ES256, &policy).unwrap();
2143 assert_eq!(result.trust_level, TrustLevel::AttCa);
2144 assert!(result.warnings.is_empty());
2145 }
2146
2147 #[test]
2148 fn packed_x5c_chain_issued_by_anchor_is_att_ca() {
2149 let anchor_key = ec_key();
2152 let anchor = ec_self_cert("anchor", &anchor_key, &[bc_ca_true()]);
2153 let issuer_key = ec_key();
2154 let issuer = ec_cert(
2155 "issuer",
2156 &name_cn("anchor"),
2157 &issuer_key,
2158 Some(&anchor_key),
2159 &[bc_ca_true()],
2160 &validity(),
2161 );
2162 let leaf_key = ec_key();
2163 let leaf = ec_cert(
2164 "leaf",
2165 &name_cn("issuer"),
2166 &leaf_key,
2167 Some(&issuer_key),
2168 &[],
2169 &validity(),
2170 );
2171
2172 let (auth_data, cdh, cred_key, _) = credential();
2173 let sig = leaf_key
2174 .sign(&rng(), &signed_data(&auth_data, &cdh))
2175 .unwrap();
2176 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![leaf, issuer]);
2177 let policy = AttestationPolicy {
2178 trust_anchors: vec![anchor],
2179 ..AttestationPolicy::default()
2180 };
2181 let result = verify_packed(&statement, &cred_key, COSE_ALG_ES256, &policy).unwrap();
2182 assert_eq!(result.trust_level, TrustLevel::AttCa);
2183 }
2184
2185 #[test]
2186 fn packed_x5c_anchors_configured_without_match_rejected() {
2187 let (leaf, issuer, _anchor, leaf_key) = ec_chain();
2188 let other = ec_self_cert("other", &ec_key(), &[bc_ca_true()]);
2190 let (auth_data, cdh, cred_key, _) = credential();
2191 let sig = leaf_key
2192 .sign(&rng(), &signed_data(&auth_data, &cdh))
2193 .unwrap();
2194 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![leaf, issuer]);
2195 let policy = AttestationPolicy {
2196 trust_anchors: vec![other],
2197 ..AttestationPolicy::default()
2198 };
2199 let err = verify_packed(&statement, &cred_key, COSE_ALG_ES256, &policy).unwrap_err();
2200 assert!(
2201 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("does not terminate at a configured trust anchor"))
2202 );
2203 }
2204
2205 #[test]
2206 fn packed_x5c_garbage_anchor_is_error() {
2207 let (leaf, issuer, _anchor, leaf_key) = ec_chain();
2208 let (auth_data, cdh, cred_key, _) = credential();
2209 let sig = leaf_key
2210 .sign(&rng(), &signed_data(&auth_data, &cdh))
2211 .unwrap();
2212 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![leaf, issuer]);
2213 let policy = AttestationPolicy {
2214 trust_anchors: vec![vec![0x01]],
2215 ..AttestationPolicy::default()
2216 };
2217 let err = verify_packed(&statement, &cred_key, COSE_ALG_ES256, &policy).unwrap_err();
2218 assert!(matches!(err, WebauthnError::AttestationError(msg) if msg.contains("parse error")));
2219 }
2220
2221 #[test]
2222 fn packed_x5c_two_level_chain_without_root_rejected() {
2223 let (leaf, issuer, _anchor, leaf_key) = ec_chain();
2225 let (auth_data, cdh, cred_key, _) = credential();
2226 let sig = leaf_key
2227 .sign(&rng(), &signed_data(&auth_data, &cdh))
2228 .unwrap();
2229 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![leaf, issuer]);
2230 let err = verify_packed(
2231 &statement,
2232 &cred_key,
2233 COSE_ALG_ES256,
2234 &AttestationPolicy::default(),
2235 )
2236 .unwrap_err();
2237 assert!(
2238 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("not self-signed"))
2239 );
2240 }
2241
2242 #[test]
2243 fn packed_x5c_expired_leaf_rejected() {
2244 let att_key = ec_key();
2245 let cert_der = ec_cert(
2246 "attestor",
2247 &name_cn("attestor"),
2248 &att_key,
2249 None,
2250 &[],
2251 &expired_validity(),
2252 );
2253 let (auth_data, cdh, cred_key, _) = credential();
2254 let sig = att_key
2255 .sign(&rng(), &signed_data(&auth_data, &cdh))
2256 .unwrap();
2257 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![cert_der]);
2258 let err = verify_packed(
2259 &statement,
2260 &cred_key,
2261 COSE_ALG_ES256,
2262 &AttestationPolicy::default(),
2263 )
2264 .unwrap_err();
2265 assert!(
2266 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("expired or not yet valid"))
2267 );
2268 }
2269
2270 #[test]
2271 fn packed_x5c_broken_name_link_rejected() {
2272 let (leaf, _issuer, _anchor, leaf_key) = ec_chain();
2274 let stranger = ec_self_cert("stranger", &ec_key(), &[bc_ca_true()]);
2275 let (auth_data, cdh, cred_key, _) = credential();
2276 let sig = leaf_key
2277 .sign(&rng(), &signed_data(&auth_data, &cdh))
2278 .unwrap();
2279 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![leaf, stranger]);
2280 let err = verify_packed(
2281 &statement,
2282 &cred_key,
2283 COSE_ALG_ES256,
2284 &AttestationPolicy::default(),
2285 )
2286 .unwrap_err();
2287 assert!(matches!(err, WebauthnError::AttestationError(msg) if msg.contains("do not link")));
2288 }
2289
2290 #[test]
2291 fn packed_x5c_intermediate_without_ca_flag_rejected() {
2292 let int_key = ec_key();
2294 let int = ec_self_cert("int", &int_key, &[bc_ca_false()]);
2295 let leaf_key = ec_key();
2296 let leaf = ec_cert(
2297 "leaf",
2298 &name_cn("int"),
2299 &leaf_key,
2300 Some(&int_key),
2301 &[],
2302 &validity(),
2303 );
2304
2305 let (auth_data, cdh, cred_key, _) = credential();
2306 let sig = leaf_key
2307 .sign(&rng(), &signed_data(&auth_data, &cdh))
2308 .unwrap();
2309 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![leaf, int]);
2310 let err = verify_packed(
2311 &statement,
2312 &cred_key,
2313 COSE_ALG_ES256,
2314 &AttestationPolicy::default(),
2315 )
2316 .unwrap_err();
2317 assert!(
2318 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("lacks basicConstraints cA=true"))
2319 );
2320 }
2321
2322 fn aaguid_ext_cert(ext_value: Vec<u8>) -> Cert {
2327 let key = ec_key();
2328 let exts = vec![extension(OID_FIDO_AAGUID, &ext_value)];
2329 Cert::from_der(&ec_self_cert("attestor", &key, &exts)).unwrap()
2330 }
2331
2332 fn aaguid_ext_value() -> Vec<u8> {
2333 let mut v = vec![0x04, 0x10];
2334 v.extend_from_slice(&AAGUID);
2335 v
2336 }
2337
2338 #[test]
2339 fn aaguid_extension_matching_is_ok() {
2340 let cert = aaguid_ext_cert(aaguid_ext_value());
2341 assert!(check_aaguid_extension(&cert, &AAGUID).is_ok());
2342 }
2343
2344 #[test]
2345 fn aaguid_extension_mismatch_rejected() {
2346 let mut aaguid = AAGUID;
2347 aaguid[0] ^= 0xFF;
2348 let cert = aaguid_ext_cert(aaguid_ext_value());
2349 let err = check_aaguid_extension(&cert, &aaguid).unwrap_err();
2350 assert!(
2351 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("does not match"))
2352 );
2353 }
2354
2355 #[test]
2356 fn aaguid_extension_malforms_rejected() {
2357 let mut wrong_len = vec![0x04, 0x0F];
2358 wrong_len.extend_from_slice(&[0u8; 15]);
2359 assert!(check_aaguid_extension(&aaguid_ext_cert(wrong_len), &AAGUID).is_err());
2360
2361 let mut wrong_tag = vec![0x05, 0x10];
2362 wrong_tag.extend_from_slice(&AAGUID);
2363 assert!(check_aaguid_extension(&aaguid_ext_cert(wrong_tag), &AAGUID).is_err());
2364
2365 let indefinite = vec![0x04, 0x80];
2366 assert!(check_aaguid_extension(&aaguid_ext_cert(indefinite), &AAGUID).is_err());
2367
2368 let plain = Cert::from_der(&ec_self_cert("attestor", &ec_key(), &[])).unwrap();
2370 assert!(check_aaguid_extension(&plain, &AAGUID).is_ok());
2371 }
2372
2373 #[test]
2378 fn parse_basic_constraints_variants() {
2379 assert_eq!(
2381 parse_basic_constraints(&[0x30, 0x03, 0x01, 0x01, 0xFF]),
2382 Some(true)
2383 );
2384 assert_eq!(
2385 parse_basic_constraints(&[0x30, 0x03, 0x01, 0x01, 0x00]),
2386 Some(false)
2387 );
2388 assert_eq!(parse_basic_constraints(&[0x30, 0x00]), Some(false));
2390 assert_eq!(parse_basic_constraints(&[0x30, 0x01, 0x01]), Some(false));
2392 assert_eq!(parse_basic_constraints(&[0x02, 0x00]), None);
2394 assert_eq!(parse_basic_constraints(&[]), None);
2395 assert_eq!(
2397 parse_basic_constraints(&[0x30, 0x81, 0x03, 0x01, 0x01, 0xFF]),
2398 Some(true)
2399 );
2400 assert_eq!(
2401 parse_basic_constraints(&[0x30, 0x82, 0x00, 0x03, 0x01, 0x01, 0xFF]),
2402 Some(true)
2403 );
2404 assert_eq!(parse_basic_constraints(&[0x30, 0x80]), None);
2407 assert_eq!(
2408 parse_basic_constraints(&[0x30, 0x84, 0, 0, 0, 0]),
2409 Some(false)
2410 );
2411 assert_eq!(parse_basic_constraints(&[0x30, 0x81, 0x05, 0x01]), None);
2413 }
2414
2415 #[test]
2416 fn is_ca_reflects_certificate_constraints() {
2417 let ca = Cert::from_der(&ec_self_cert("ca", &ec_key(), &[bc_ca_true()])).unwrap();
2418 assert_eq!(ca.is_ca(), Some(true));
2419
2420 let explicit_no_ca =
2421 Cert::from_der(&ec_self_cert("leaf", &ec_key(), &[bc_ca_false()])).unwrap();
2422 assert_eq!(explicit_no_ca.is_ca(), Some(false));
2423
2424 let no_ext = Cert::from_der(&ec_self_cert("leaf", &ec_key(), &[])).unwrap();
2425 assert_eq!(no_ext.is_ca(), None);
2426 }
2427
2428 #[test]
2433 fn verify_signed_by_ecdsa_ok_and_tampered() {
2434 let (leaf, issuer, _anchor, _leaf_key) = ec_chain();
2435 let child = Cert::from_der(&leaf).unwrap();
2436 let parent = Cert::from_der(&issuer).unwrap();
2437 assert!(child.verify_signed_by(&parent).is_ok());
2438
2439 let mut tampered = leaf.clone();
2442 let last = tampered.len() - 1;
2443 tampered[last] ^= 0x01;
2444 let child = Cert::from_der(&tampered).unwrap();
2445 assert!(matches!(
2446 child.verify_signed_by(&parent),
2447 Err(WebauthnError::SignatureVerificationFailed)
2448 ));
2449 }
2450
2451 #[test]
2452 fn verify_signed_by_rsa_ok_and_tampered() {
2453 let ca_key = rsa_signing_key();
2455 let ca = rsa_self_cert(&ca_key, &[bc_ca_true()]);
2456 let leaf_key = ec_key();
2457
2458 let subject = name_cn("rsa-leaf");
2459 let leaf_tbs = tbs_cert(
2460 OID_SIG_SHA256_RSA,
2461 &subject_raw(&ca),
2462 &subject,
2463 &ec_spki_from(&leaf_key),
2464 &[],
2465 &validity(),
2466 false,
2467 );
2468 let rng = rng();
2469 let mut leaf_sig = vec![0u8; 256];
2470 ca_key
2471 .sign(&RSA_PKCS1_SHA256, &rng, &leaf_tbs, &mut leaf_sig)
2472 .unwrap();
2473 let leaf = finish_cert(&leaf_tbs, OID_SIG_SHA256_RSA, &leaf_sig);
2474
2475 let child = Cert::from_der(&leaf).unwrap();
2476 let parent = Cert::from_der(&ca).unwrap();
2477 assert!(child.verify_signed_by(&parent).is_ok());
2478
2479 let mut tampered = leaf.clone();
2480 let last = tampered.len() - 1;
2481 tampered[last] ^= 0x01;
2482 let child = Cert::from_der(&tampered).unwrap();
2483 assert!(matches!(
2484 child.verify_signed_by(&parent),
2485 Err(WebauthnError::SignatureVerificationFailed)
2486 ));
2487 }
2488
2489 #[test]
2490 fn verify_signed_by_algorithm_issuer_key_type_mismatches() {
2491 let rsa_key_pair = rsa_signing_key();
2493 let rsa_parent = rsa_self_cert(&rsa_key_pair, &[bc_ca_true()]);
2494 let parent = Cert::from_der(&rsa_parent).unwrap();
2495 let child = Cert::from_der(&hand_cert(
2496 OID_SIG_ECDSA_SHA256,
2497 &subject_raw(&rsa_parent),
2498 &name_cn("child"),
2499 &ec_spki_from(&ec_key()),
2500 false,
2501 ))
2502 .unwrap();
2503 let err = child.verify_signed_by(&parent).unwrap_err();
2504 assert!(
2505 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("does not match issuer key type"))
2506 );
2507
2508 let ec_key_pair = ec_key();
2510 let ec_parent = ec_self_cert("ec-parent", &ec_key_pair, &[bc_ca_true()]);
2511 let parent = Cert::from_der(&ec_parent).unwrap();
2512 let child = Cert::from_der(&hand_cert(
2513 OID_SIG_SHA256_RSA,
2514 &subject_raw(&ec_parent),
2515 &name_cn("child"),
2516 &ec_spki_from(&ec_key()),
2517 false,
2518 ))
2519 .unwrap();
2520 let err = child.verify_signed_by(&parent).unwrap_err();
2521 assert!(
2522 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("does not match issuer key type"))
2523 );
2524
2525 let child = Cert::from_der(&hand_cert(
2527 OID_SIG_ECDSA_SHA512,
2528 &subject_raw(&ec_parent),
2529 &name_cn("child"),
2530 &ec_spki_from(&ec_key()),
2531 false,
2532 ))
2533 .unwrap();
2534 let err = child.verify_signed_by(&parent).unwrap_err();
2535 assert!(
2536 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("unsupported signature algorithm"))
2537 );
2538 }
2539
2540 #[test]
2545 fn u2f_attestation_ok() {
2546 let (auth_data, cdh, key, _) = credential();
2547 let att_key = ec_key();
2548 let cert_der = ec_self_cert("u2f", &att_key, &[]);
2549 let msg = u2f_msg(&auth_data, &cdh, &key);
2550 let sig = att_key.sign(&rng(), &msg).unwrap();
2551 let statement = cbor_map(vec![
2552 (8, cbor_bytes_val(sig.as_ref())),
2553 (33, Value::Array(vec![cbor_bytes_val(&cert_der)])),
2554 ]);
2555 let result = u2f_call(&statement, &key, COSE_ALG_ES256, &auth_data, &cdh).unwrap();
2556 assert_eq!(result.format, AttestationFormat::FidoU2f);
2557 assert_eq!(result.trust_level, TrustLevel::BasicAtt);
2558 assert_eq!(result.warnings, vec![warnings::UNANCHORED_CHAIN]);
2559 assert_eq!(result.aaguid, Some(AAGUID));
2560 }
2561
2562 #[test]
2563 fn u2f_rejects_non_es256_credential() {
2564 let (auth_data, cdh, key, _) = credential();
2565 let att_key = ec_key();
2566 let cert_der = ec_self_cert("u2f", &att_key, &[]);
2567 let msg = u2f_msg(&auth_data, &cdh, &key);
2568 let sig = att_key.sign(&rng(), &msg).unwrap();
2569 let statement = cbor_map(vec![
2570 (8, cbor_bytes_val(sig.as_ref())),
2571 (33, Value::Array(vec![cbor_bytes_val(&cert_der)])),
2572 ]);
2573 let err = u2f_call(&statement, &key, COSE_ALG_RS256, &auth_data, &cdh).unwrap_err();
2574 assert!(
2575 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("requires an ES256 credential key"))
2576 );
2577 }
2578
2579 #[test]
2580 fn u2f_rejects_non_ec2_credential_key() {
2581 let (auth_data, cdh, _, _) = credential();
2582 let rsa_cred = CosePublicKey::Rsa {
2583 n: vec![0xAA; 256],
2584 e: vec![0x01, 0x00, 0x01],
2585 };
2586 let cert_der = ec_self_cert("u2f", &ec_key(), &[]);
2587 let statement = cbor_map(vec![
2590 (8, cbor_bytes_val(&[0u8; 64])),
2591 (33, Value::Array(vec![cbor_bytes_val(&cert_der)])),
2592 ]);
2593 let err = u2f_call(&statement, &rsa_cred, COSE_ALG_ES256, &auth_data, &cdh).unwrap_err();
2594 assert!(
2595 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("requires an EC2 credential key"))
2596 );
2597 }
2598
2599 #[test]
2600 fn u2f_rejects_non_ec_attestation_certificate() {
2601 let (auth_data, cdh, key, _) = credential();
2602 let rsa = rsa_signing_key();
2604 let cert_der = rsa_self_cert(&rsa, &[]);
2605
2606 let msg = u2f_msg(&auth_data, &cdh, &key);
2607 let rng = rng();
2608 let mut sig = vec![0u8; 256];
2609 rsa.sign(&RSA_PKCS1_SHA256, &rng, &msg, &mut sig).unwrap();
2610 let statement = cbor_map(vec![
2611 (8, cbor_bytes_val(&sig)),
2612 (33, Value::Array(vec![cbor_bytes_val(&cert_der)])),
2613 ]);
2614 let err = u2f_call(&statement, &key, COSE_ALG_ES256, &auth_data, &cdh).unwrap_err();
2615 assert!(
2616 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("must carry an EC P-256 key"))
2617 );
2618 }
2619
2620 #[test]
2621 fn u2f_rejects_wrong_coordinate_length() {
2622 let (auth_data, cdh, _, _) = credential();
2623 let short_key = CosePublicKey::Ec2 {
2624 x: vec![0xAA; 31],
2625 y: vec![0xBB; 32],
2626 };
2627 let well_formed = CosePublicKey::Ec2 {
2628 x: vec![0xAA; 32],
2629 y: vec![0xBB; 32],
2630 };
2631 let att_key = ec_key();
2632 let cert_der = ec_self_cert("u2f", &att_key, &[]);
2633 let msg = u2f_msg(&auth_data, &cdh, &well_formed);
2634 let sig = att_key.sign(&rng(), &msg).unwrap();
2635 let statement = cbor_map(vec![
2636 (8, cbor_bytes_val(sig.as_ref())),
2637 (33, Value::Array(vec![cbor_bytes_val(&cert_der)])),
2638 ]);
2639 let err = u2f_call(&statement, &short_key, COSE_ALG_ES256, &auth_data, &cdh).unwrap_err();
2640 assert!(
2641 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("must be 32 bytes"))
2642 );
2643 }
2644
2645 #[test]
2646 fn u2f_rejects_short_auth_data() {
2647 let (auth_data, cdh, key, _) = credential();
2648 let att_key = ec_key();
2649 let cert_der = ec_self_cert("u2f", &att_key, &[]);
2650 let msg = u2f_msg(&auth_data, &cdh, &key);
2651 let sig = att_key.sign(&rng(), &msg).unwrap();
2652 let statement = cbor_map(vec![
2653 (8, cbor_bytes_val(sig.as_ref())),
2654 (33, Value::Array(vec![cbor_bytes_val(&cert_der)])),
2655 ]);
2656 let err = u2f_call(&statement, &key, COSE_ALG_ES256, &auth_data[..31], &cdh).unwrap_err();
2657 assert!(
2658 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("too short for fido-u2f"))
2659 );
2660 }
2661}