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 #[test]
1767 fn packed_x5c_self_signed_ec_is_basic_att() {
1768 let att_key = ec_key();
1769 let cert_der = ec_self_cert("attestor", &att_key, &[]);
1770 let (auth_data, cdh, cred_key, _) = credential();
1771 let sig = att_key
1772 .sign(&rng(), &signed_data(&auth_data, &cdh))
1773 .unwrap();
1774 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![cert_der]);
1775
1776 let result = verify_packed(
1777 &statement,
1778 &cred_key,
1779 COSE_ALG_ES256,
1780 &AttestationPolicy::default(),
1781 )
1782 .unwrap();
1783 assert_eq!(result.trust_level, TrustLevel::BasicAtt);
1784 assert_eq!(result.warnings, vec![warnings::UNANCHORED_CHAIN]);
1785 assert_eq!(result.format, AttestationFormat::Packed);
1786 }
1787
1788 #[test]
1790 fn packed_x5c_rsa_attestation_ok() {
1791 let rsa = rsa_signing_key();
1792 let cert_der = rsa_self_cert(&rsa, &[]);
1793 let (auth_data, cdh, cred_key, _) = credential();
1794 let rng = rng();
1795 let mut stmt_sig = vec![0u8; 256];
1796 rsa.sign(
1797 &RSA_PKCS1_SHA256,
1798 &rng,
1799 &signed_data(&auth_data, &cdh),
1800 &mut stmt_sig,
1801 )
1802 .unwrap();
1803 let statement = packed_stmt(COSE_ALG_RS256, &stmt_sig, vec![cert_der]);
1804
1805 let result = verify_packed(
1806 &statement,
1807 &cred_key,
1808 COSE_ALG_ES256,
1809 &AttestationPolicy::default(),
1810 )
1811 .unwrap();
1812 assert_eq!(result.trust_level, TrustLevel::BasicAtt);
1813 }
1814
1815 fn rsa_self_cert(key: &RsaKeyPair, exts: &[Vec<u8>]) -> Vec<u8> {
1817 let name = name_cn("rsa-attestor");
1818 rsa_cert("rsa-attestor", &name, key, None, exts)
1819 }
1820
1821 #[test]
1822 fn packed_x5c_unsupported_statement_alg_rejected() {
1823 let att_key = ec_key();
1824 let cert_der = ec_self_cert("attestor", &att_key, &[]);
1825 let (_, _, cred_key, _) = credential();
1826 let statement = packed_stmt(-47, &[0u8; 64], vec![cert_der]);
1827 let err = verify_packed(
1828 &statement,
1829 &cred_key,
1830 COSE_ALG_ES256,
1831 &AttestationPolicy::default(),
1832 )
1833 .unwrap_err();
1834 assert!(matches!(err, WebauthnError::UnsupportedAlgorithm(-47)));
1835 }
1836
1837 #[test]
1838 fn packed_x5c_alg_cert_key_type_mismatch_rejected() {
1839 let att_key = ec_key();
1841 let cert_der = ec_self_cert("attestor", &att_key, &[]);
1842 let (_, _, cred_key, _) = credential();
1843 let statement = packed_stmt(COSE_ALG_RS256, &[0u8; 256], vec![cert_der]);
1844 let err = verify_packed(
1845 &statement,
1846 &cred_key,
1847 COSE_ALG_ES256,
1848 &AttestationPolicy::default(),
1849 )
1850 .unwrap_err();
1851 assert!(
1852 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("does not match the attestation certificate key type"))
1853 );
1854 }
1855
1856 #[test]
1857 fn packed_x5c_bad_statement_signature_rejected() {
1858 let att_key = ec_key();
1859 let cert_der = ec_self_cert("attestor", &att_key, &[]);
1860 let (_, _, cred_key, _) = credential();
1861 let statement = packed_stmt(COSE_ALG_ES256, &[0u8; 64], vec![cert_der]);
1862 let err = verify_packed(
1863 &statement,
1864 &cred_key,
1865 COSE_ALG_ES256,
1866 &AttestationPolicy::default(),
1867 )
1868 .unwrap_err();
1869 assert!(matches!(err, WebauthnError::SignatureVerificationFailed));
1870 }
1871
1872 fn x5c_statement(cert_der: Vec<u8>) -> Value {
1876 packed_stmt(COSE_ALG_ES256, &[0u8; 64], vec![cert_der])
1877 }
1878
1879 #[test]
1880 fn packed_x5c_garbage_certificate_rejected() {
1881 let statement = x5c_statement(vec![0xFF; 16]);
1882 let (_, _, cred_key, _) = credential();
1883 let err = verify_packed(
1884 &statement,
1885 &cred_key,
1886 COSE_ALG_ES256,
1887 &AttestationPolicy::default(),
1888 )
1889 .unwrap_err();
1890 assert!(matches!(err, WebauthnError::AttestationError(msg) if msg.contains("parse error")));
1891 }
1892
1893 #[test]
1894 fn packed_x5c_trailing_bytes_rejected() {
1895 let mut cert_der = ec_self_cert("attestor", &ec_key(), &[]);
1896 cert_der.push(0x00);
1897 let statement = x5c_statement(cert_der);
1898 let (_, _, cred_key, _) = credential();
1899 let err = verify_packed(
1900 &statement,
1901 &cred_key,
1902 COSE_ALG_ES256,
1903 &AttestationPolicy::default(),
1904 )
1905 .unwrap_err();
1906 assert!(
1907 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("trailing bytes"))
1908 );
1909 }
1910
1911 #[test]
1912 fn packed_x5c_v1_certificate_rejected() {
1913 let cert = hand_cert(
1914 OID_SIG_ECDSA_SHA256,
1915 &name_cn("self"),
1916 &name_cn("self"),
1917 &ec_spki_from(&ec_key()),
1918 true, );
1920 let statement = x5c_statement(cert);
1921 let (_, _, cred_key, _) = credential();
1922 let err = verify_packed(
1923 &statement,
1924 &cred_key,
1925 COSE_ALG_ES256,
1926 &AttestationPolicy::default(),
1927 )
1928 .unwrap_err();
1929 assert!(
1930 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("must be X.509 v3"))
1931 );
1932 }
1933
1934 #[test]
1935 fn packed_x5c_compressed_ec_point_rejected() {
1936 let compressed_spki = der_seq(&[
1937 &der_seq(&[&der_oid(OID_EC_PUBLIC_KEY), &der_oid(OID_PRIME256V1)]),
1938 &der_bit(&[0x02; 33]), ]);
1940 let cert = hand_cert(
1941 OID_SIG_ECDSA_SHA256,
1942 &name_cn("self"),
1943 &name_cn("self"),
1944 &compressed_spki,
1945 false,
1946 );
1947 let statement = x5c_statement(cert);
1948 let (_, _, cred_key, _) = credential();
1949 let err = verify_packed(
1950 &statement,
1951 &cred_key,
1952 COSE_ALG_ES256,
1953 &AttestationPolicy::default(),
1954 )
1955 .unwrap_err();
1956 assert!(
1957 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("uncompressed P-256 point"))
1958 );
1959 }
1960
1961 #[test]
1962 fn packed_x5c_unsupported_public_key_algorithm_rejected() {
1963 let cert = hand_cert(
1964 OID_SIG_ECDSA_SHA256,
1965 &name_cn("self"),
1966 &name_cn("self"),
1967 &ed25519_spki(),
1968 false,
1969 );
1970 let statement = x5c_statement(cert);
1971 let (_, _, cred_key, _) = credential();
1972 let err = verify_packed(
1973 &statement,
1974 &cred_key,
1975 COSE_ALG_ES256,
1976 &AttestationPolicy::default(),
1977 )
1978 .unwrap_err();
1979 assert!(
1980 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("unsupported public key algorithm"))
1981 );
1982 }
1983
1984 #[test]
1987 fn packed_x5c_chain_carrying_anchor_is_att_ca() {
1988 let anchor_key = ec_key();
1990 let anchor = ec_self_cert("anchor", &anchor_key, &[bc_ca_true()]);
1991 let leaf_key = ec_key();
1992 let leaf = ec_cert(
1993 "leaf",
1994 &name_cn("anchor"),
1995 &leaf_key,
1996 Some(&anchor_key),
1997 &[],
1998 &validity(),
1999 );
2000 let (auth_data, cdh, cred_key, _) = credential();
2001 let sig = leaf_key
2002 .sign(&rng(), &signed_data(&auth_data, &cdh))
2003 .unwrap();
2004 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![leaf, anchor.clone()]);
2005 let policy = AttestationPolicy {
2006 trust_anchors: vec![anchor],
2007 ..AttestationPolicy::default()
2008 };
2009 let result = verify_packed(&statement, &cred_key, COSE_ALG_ES256, &policy).unwrap();
2010 assert_eq!(result.trust_level, TrustLevel::AttCa);
2011 assert!(result.warnings.is_empty());
2012 }
2013
2014 #[test]
2015 fn packed_x5c_chain_issued_by_anchor_is_att_ca() {
2016 let anchor_key = ec_key();
2019 let anchor = ec_self_cert("anchor", &anchor_key, &[bc_ca_true()]);
2020 let issuer_key = ec_key();
2021 let issuer = ec_cert(
2022 "issuer",
2023 &name_cn("anchor"),
2024 &issuer_key,
2025 Some(&anchor_key),
2026 &[bc_ca_true()],
2027 &validity(),
2028 );
2029 let leaf_key = ec_key();
2030 let leaf = ec_cert(
2031 "leaf",
2032 &name_cn("issuer"),
2033 &leaf_key,
2034 Some(&issuer_key),
2035 &[],
2036 &validity(),
2037 );
2038
2039 let (auth_data, cdh, cred_key, _) = credential();
2040 let sig = leaf_key
2041 .sign(&rng(), &signed_data(&auth_data, &cdh))
2042 .unwrap();
2043 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![leaf, issuer]);
2044 let policy = AttestationPolicy {
2045 trust_anchors: vec![anchor],
2046 ..AttestationPolicy::default()
2047 };
2048 let result = verify_packed(&statement, &cred_key, COSE_ALG_ES256, &policy).unwrap();
2049 assert_eq!(result.trust_level, TrustLevel::AttCa);
2050 }
2051
2052 #[test]
2053 fn packed_x5c_anchors_configured_without_match_rejected() {
2054 let (leaf, issuer, _anchor, leaf_key) = ec_chain();
2055 let other = ec_self_cert("other", &ec_key(), &[bc_ca_true()]);
2057 let (auth_data, cdh, cred_key, _) = credential();
2058 let sig = leaf_key
2059 .sign(&rng(), &signed_data(&auth_data, &cdh))
2060 .unwrap();
2061 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![leaf, issuer]);
2062 let policy = AttestationPolicy {
2063 trust_anchors: vec![other],
2064 ..AttestationPolicy::default()
2065 };
2066 let err = verify_packed(&statement, &cred_key, COSE_ALG_ES256, &policy).unwrap_err();
2067 assert!(
2068 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("does not terminate at a configured trust anchor"))
2069 );
2070 }
2071
2072 #[test]
2073 fn packed_x5c_garbage_anchor_is_error() {
2074 let (leaf, issuer, _anchor, leaf_key) = ec_chain();
2075 let (auth_data, cdh, cred_key, _) = credential();
2076 let sig = leaf_key
2077 .sign(&rng(), &signed_data(&auth_data, &cdh))
2078 .unwrap();
2079 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![leaf, issuer]);
2080 let policy = AttestationPolicy {
2081 trust_anchors: vec![vec![0x01]],
2082 ..AttestationPolicy::default()
2083 };
2084 let err = verify_packed(&statement, &cred_key, COSE_ALG_ES256, &policy).unwrap_err();
2085 assert!(matches!(err, WebauthnError::AttestationError(msg) if msg.contains("parse error")));
2086 }
2087
2088 #[test]
2089 fn packed_x5c_two_level_chain_without_root_rejected() {
2090 let (leaf, issuer, _anchor, leaf_key) = ec_chain();
2092 let (auth_data, cdh, cred_key, _) = credential();
2093 let sig = leaf_key
2094 .sign(&rng(), &signed_data(&auth_data, &cdh))
2095 .unwrap();
2096 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![leaf, issuer]);
2097 let err = verify_packed(
2098 &statement,
2099 &cred_key,
2100 COSE_ALG_ES256,
2101 &AttestationPolicy::default(),
2102 )
2103 .unwrap_err();
2104 assert!(
2105 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("not self-signed"))
2106 );
2107 }
2108
2109 #[test]
2110 fn packed_x5c_expired_leaf_rejected() {
2111 let att_key = ec_key();
2112 let cert_der = ec_cert(
2113 "attestor",
2114 &name_cn("attestor"),
2115 &att_key,
2116 None,
2117 &[],
2118 &expired_validity(),
2119 );
2120 let (auth_data, cdh, cred_key, _) = credential();
2121 let sig = att_key
2122 .sign(&rng(), &signed_data(&auth_data, &cdh))
2123 .unwrap();
2124 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![cert_der]);
2125 let err = verify_packed(
2126 &statement,
2127 &cred_key,
2128 COSE_ALG_ES256,
2129 &AttestationPolicy::default(),
2130 )
2131 .unwrap_err();
2132 assert!(
2133 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("expired or not yet valid"))
2134 );
2135 }
2136
2137 #[test]
2138 fn packed_x5c_broken_name_link_rejected() {
2139 let (leaf, _issuer, _anchor, leaf_key) = ec_chain();
2141 let stranger = ec_self_cert("stranger", &ec_key(), &[bc_ca_true()]);
2142 let (auth_data, cdh, cred_key, _) = credential();
2143 let sig = leaf_key
2144 .sign(&rng(), &signed_data(&auth_data, &cdh))
2145 .unwrap();
2146 let statement = packed_stmt(COSE_ALG_ES256, sig.as_ref(), vec![leaf, stranger]);
2147 let err = verify_packed(
2148 &statement,
2149 &cred_key,
2150 COSE_ALG_ES256,
2151 &AttestationPolicy::default(),
2152 )
2153 .unwrap_err();
2154 assert!(matches!(err, WebauthnError::AttestationError(msg) if msg.contains("do not link")));
2155 }
2156
2157 #[test]
2158 fn packed_x5c_intermediate_without_ca_flag_rejected() {
2159 let int_key = ec_key();
2161 let int = ec_self_cert("int", &int_key, &[bc_ca_false()]);
2162 let leaf_key = ec_key();
2163 let leaf = ec_cert(
2164 "leaf",
2165 &name_cn("int"),
2166 &leaf_key,
2167 Some(&int_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, int]);
2177 let err = verify_packed(
2178 &statement,
2179 &cred_key,
2180 COSE_ALG_ES256,
2181 &AttestationPolicy::default(),
2182 )
2183 .unwrap_err();
2184 assert!(
2185 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("lacks basicConstraints cA=true"))
2186 );
2187 }
2188
2189 fn aaguid_ext_cert(ext_value: Vec<u8>) -> Cert {
2194 let key = ec_key();
2195 let exts = vec![extension(OID_FIDO_AAGUID, &ext_value)];
2196 Cert::from_der(&ec_self_cert("attestor", &key, &exts)).unwrap()
2197 }
2198
2199 fn aaguid_ext_value() -> Vec<u8> {
2200 let mut v = vec![0x04, 0x10];
2201 v.extend_from_slice(&AAGUID);
2202 v
2203 }
2204
2205 #[test]
2206 fn aaguid_extension_matching_is_ok() {
2207 let cert = aaguid_ext_cert(aaguid_ext_value());
2208 assert!(check_aaguid_extension(&cert, &AAGUID).is_ok());
2209 }
2210
2211 #[test]
2212 fn aaguid_extension_mismatch_rejected() {
2213 let mut aaguid = AAGUID;
2214 aaguid[0] ^= 0xFF;
2215 let cert = aaguid_ext_cert(aaguid_ext_value());
2216 let err = check_aaguid_extension(&cert, &aaguid).unwrap_err();
2217 assert!(
2218 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("does not match"))
2219 );
2220 }
2221
2222 #[test]
2223 fn aaguid_extension_malforms_rejected() {
2224 let mut wrong_len = vec![0x04, 0x0F];
2225 wrong_len.extend_from_slice(&[0u8; 15]);
2226 assert!(check_aaguid_extension(&aaguid_ext_cert(wrong_len), &AAGUID).is_err());
2227
2228 let mut wrong_tag = vec![0x05, 0x10];
2229 wrong_tag.extend_from_slice(&AAGUID);
2230 assert!(check_aaguid_extension(&aaguid_ext_cert(wrong_tag), &AAGUID).is_err());
2231
2232 let indefinite = vec![0x04, 0x80];
2233 assert!(check_aaguid_extension(&aaguid_ext_cert(indefinite), &AAGUID).is_err());
2234
2235 let plain = Cert::from_der(&ec_self_cert("attestor", &ec_key(), &[])).unwrap();
2237 assert!(check_aaguid_extension(&plain, &AAGUID).is_ok());
2238 }
2239
2240 #[test]
2245 fn parse_basic_constraints_variants() {
2246 assert_eq!(
2248 parse_basic_constraints(&[0x30, 0x03, 0x01, 0x01, 0xFF]),
2249 Some(true)
2250 );
2251 assert_eq!(
2252 parse_basic_constraints(&[0x30, 0x03, 0x01, 0x01, 0x00]),
2253 Some(false)
2254 );
2255 assert_eq!(parse_basic_constraints(&[0x30, 0x00]), Some(false));
2257 assert_eq!(parse_basic_constraints(&[0x30, 0x01, 0x01]), Some(false));
2259 assert_eq!(parse_basic_constraints(&[0x02, 0x00]), None);
2261 assert_eq!(parse_basic_constraints(&[]), None);
2262 assert_eq!(
2264 parse_basic_constraints(&[0x30, 0x81, 0x03, 0x01, 0x01, 0xFF]),
2265 Some(true)
2266 );
2267 assert_eq!(
2268 parse_basic_constraints(&[0x30, 0x82, 0x00, 0x03, 0x01, 0x01, 0xFF]),
2269 Some(true)
2270 );
2271 assert_eq!(parse_basic_constraints(&[0x30, 0x80]), None);
2274 assert_eq!(
2275 parse_basic_constraints(&[0x30, 0x84, 0, 0, 0, 0]),
2276 Some(false)
2277 );
2278 assert_eq!(parse_basic_constraints(&[0x30, 0x81, 0x05, 0x01]), None);
2280 }
2281
2282 #[test]
2283 fn is_ca_reflects_certificate_constraints() {
2284 let ca = Cert::from_der(&ec_self_cert("ca", &ec_key(), &[bc_ca_true()])).unwrap();
2285 assert_eq!(ca.is_ca(), Some(true));
2286
2287 let explicit_no_ca =
2288 Cert::from_der(&ec_self_cert("leaf", &ec_key(), &[bc_ca_false()])).unwrap();
2289 assert_eq!(explicit_no_ca.is_ca(), Some(false));
2290
2291 let no_ext = Cert::from_der(&ec_self_cert("leaf", &ec_key(), &[])).unwrap();
2292 assert_eq!(no_ext.is_ca(), None);
2293 }
2294
2295 #[test]
2300 fn verify_signed_by_ecdsa_ok_and_tampered() {
2301 let (leaf, issuer, _anchor, _leaf_key) = ec_chain();
2302 let child = Cert::from_der(&leaf).unwrap();
2303 let parent = Cert::from_der(&issuer).unwrap();
2304 assert!(child.verify_signed_by(&parent).is_ok());
2305
2306 let mut tampered = leaf.clone();
2309 let last = tampered.len() - 1;
2310 tampered[last] ^= 0x01;
2311 let child = Cert::from_der(&tampered).unwrap();
2312 assert!(matches!(
2313 child.verify_signed_by(&parent),
2314 Err(WebauthnError::SignatureVerificationFailed)
2315 ));
2316 }
2317
2318 #[test]
2319 fn verify_signed_by_rsa_ok_and_tampered() {
2320 let ca_key = rsa_signing_key();
2322 let ca = rsa_self_cert(&ca_key, &[bc_ca_true()]);
2323 let leaf_key = ec_key();
2324
2325 let subject = name_cn("rsa-leaf");
2326 let leaf_tbs = tbs_cert(
2327 OID_SIG_SHA256_RSA,
2328 &subject_raw(&ca),
2329 &subject,
2330 &ec_spki_from(&leaf_key),
2331 &[],
2332 &validity(),
2333 false,
2334 );
2335 let rng = rng();
2336 let mut leaf_sig = vec![0u8; 256];
2337 ca_key
2338 .sign(&RSA_PKCS1_SHA256, &rng, &leaf_tbs, &mut leaf_sig)
2339 .unwrap();
2340 let leaf = finish_cert(&leaf_tbs, OID_SIG_SHA256_RSA, &leaf_sig);
2341
2342 let child = Cert::from_der(&leaf).unwrap();
2343 let parent = Cert::from_der(&ca).unwrap();
2344 assert!(child.verify_signed_by(&parent).is_ok());
2345
2346 let mut tampered = leaf.clone();
2347 let last = tampered.len() - 1;
2348 tampered[last] ^= 0x01;
2349 let child = Cert::from_der(&tampered).unwrap();
2350 assert!(matches!(
2351 child.verify_signed_by(&parent),
2352 Err(WebauthnError::SignatureVerificationFailed)
2353 ));
2354 }
2355
2356 #[test]
2357 fn verify_signed_by_algorithm_issuer_key_type_mismatches() {
2358 let rsa_key_pair = rsa_signing_key();
2360 let rsa_parent = rsa_self_cert(&rsa_key_pair, &[bc_ca_true()]);
2361 let parent = Cert::from_der(&rsa_parent).unwrap();
2362 let child = Cert::from_der(&hand_cert(
2363 OID_SIG_ECDSA_SHA256,
2364 &subject_raw(&rsa_parent),
2365 &name_cn("child"),
2366 &ec_spki_from(&ec_key()),
2367 false,
2368 ))
2369 .unwrap();
2370 let err = child.verify_signed_by(&parent).unwrap_err();
2371 assert!(
2372 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("does not match issuer key type"))
2373 );
2374
2375 let ec_key_pair = ec_key();
2377 let ec_parent = ec_self_cert("ec-parent", &ec_key_pair, &[bc_ca_true()]);
2378 let parent = Cert::from_der(&ec_parent).unwrap();
2379 let child = Cert::from_der(&hand_cert(
2380 OID_SIG_SHA256_RSA,
2381 &subject_raw(&ec_parent),
2382 &name_cn("child"),
2383 &ec_spki_from(&ec_key()),
2384 false,
2385 ))
2386 .unwrap();
2387 let err = child.verify_signed_by(&parent).unwrap_err();
2388 assert!(
2389 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("does not match issuer key type"))
2390 );
2391
2392 let child = Cert::from_der(&hand_cert(
2394 OID_SIG_ECDSA_SHA512,
2395 &subject_raw(&ec_parent),
2396 &name_cn("child"),
2397 &ec_spki_from(&ec_key()),
2398 false,
2399 ))
2400 .unwrap();
2401 let err = child.verify_signed_by(&parent).unwrap_err();
2402 assert!(
2403 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("unsupported signature algorithm"))
2404 );
2405 }
2406
2407 #[test]
2412 fn u2f_attestation_ok() {
2413 let (auth_data, cdh, key, _) = credential();
2414 let att_key = ec_key();
2415 let cert_der = ec_self_cert("u2f", &att_key, &[]);
2416 let msg = u2f_msg(&auth_data, &cdh, &key);
2417 let sig = att_key.sign(&rng(), &msg).unwrap();
2418 let statement = cbor_map(vec![
2419 (8, cbor_bytes_val(sig.as_ref())),
2420 (33, Value::Array(vec![cbor_bytes_val(&cert_der)])),
2421 ]);
2422 let result = u2f_call(&statement, &key, COSE_ALG_ES256, &auth_data, &cdh).unwrap();
2423 assert_eq!(result.format, AttestationFormat::FidoU2f);
2424 assert_eq!(result.trust_level, TrustLevel::BasicAtt);
2425 assert_eq!(result.warnings, vec![warnings::UNANCHORED_CHAIN]);
2426 assert_eq!(result.aaguid, Some(AAGUID));
2427 }
2428
2429 #[test]
2430 fn u2f_rejects_non_es256_credential() {
2431 let (auth_data, cdh, key, _) = credential();
2432 let att_key = ec_key();
2433 let cert_der = ec_self_cert("u2f", &att_key, &[]);
2434 let msg = u2f_msg(&auth_data, &cdh, &key);
2435 let sig = att_key.sign(&rng(), &msg).unwrap();
2436 let statement = cbor_map(vec![
2437 (8, cbor_bytes_val(sig.as_ref())),
2438 (33, Value::Array(vec![cbor_bytes_val(&cert_der)])),
2439 ]);
2440 let err = u2f_call(&statement, &key, COSE_ALG_RS256, &auth_data, &cdh).unwrap_err();
2441 assert!(
2442 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("requires an ES256 credential key"))
2443 );
2444 }
2445
2446 #[test]
2447 fn u2f_rejects_non_ec2_credential_key() {
2448 let (auth_data, cdh, _, _) = credential();
2449 let rsa_cred = CosePublicKey::Rsa {
2450 n: vec![0xAA; 256],
2451 e: vec![0x01, 0x00, 0x01],
2452 };
2453 let cert_der = ec_self_cert("u2f", &ec_key(), &[]);
2454 let statement = cbor_map(vec![
2457 (8, cbor_bytes_val(&[0u8; 64])),
2458 (33, Value::Array(vec![cbor_bytes_val(&cert_der)])),
2459 ]);
2460 let err = u2f_call(&statement, &rsa_cred, COSE_ALG_ES256, &auth_data, &cdh).unwrap_err();
2461 assert!(
2462 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("requires an EC2 credential key"))
2463 );
2464 }
2465
2466 #[test]
2467 fn u2f_rejects_non_ec_attestation_certificate() {
2468 let (auth_data, cdh, key, _) = credential();
2469 let rsa = rsa_signing_key();
2471 let cert_der = rsa_self_cert(&rsa, &[]);
2472
2473 let msg = u2f_msg(&auth_data, &cdh, &key);
2474 let rng = rng();
2475 let mut sig = vec![0u8; 256];
2476 rsa.sign(&RSA_PKCS1_SHA256, &rng, &msg, &mut sig).unwrap();
2477 let statement = cbor_map(vec![
2478 (8, cbor_bytes_val(&sig)),
2479 (33, Value::Array(vec![cbor_bytes_val(&cert_der)])),
2480 ]);
2481 let err = u2f_call(&statement, &key, COSE_ALG_ES256, &auth_data, &cdh).unwrap_err();
2482 assert!(
2483 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("must carry an EC P-256 key"))
2484 );
2485 }
2486
2487 #[test]
2488 fn u2f_rejects_wrong_coordinate_length() {
2489 let (auth_data, cdh, _, _) = credential();
2490 let short_key = CosePublicKey::Ec2 {
2491 x: vec![0xAA; 31],
2492 y: vec![0xBB; 32],
2493 };
2494 let well_formed = CosePublicKey::Ec2 {
2495 x: vec![0xAA; 32],
2496 y: vec![0xBB; 32],
2497 };
2498 let att_key = ec_key();
2499 let cert_der = ec_self_cert("u2f", &att_key, &[]);
2500 let msg = u2f_msg(&auth_data, &cdh, &well_formed);
2501 let sig = att_key.sign(&rng(), &msg).unwrap();
2502 let statement = cbor_map(vec![
2503 (8, cbor_bytes_val(sig.as_ref())),
2504 (33, Value::Array(vec![cbor_bytes_val(&cert_der)])),
2505 ]);
2506 let err = u2f_call(&statement, &short_key, COSE_ALG_ES256, &auth_data, &cdh).unwrap_err();
2507 assert!(
2508 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("must be 32 bytes"))
2509 );
2510 }
2511
2512 #[test]
2513 fn u2f_rejects_short_auth_data() {
2514 let (auth_data, cdh, key, _) = credential();
2515 let att_key = ec_key();
2516 let cert_der = ec_self_cert("u2f", &att_key, &[]);
2517 let msg = u2f_msg(&auth_data, &cdh, &key);
2518 let sig = att_key.sign(&rng(), &msg).unwrap();
2519 let statement = cbor_map(vec![
2520 (8, cbor_bytes_val(sig.as_ref())),
2521 (33, Value::Array(vec![cbor_bytes_val(&cert_der)])),
2522 ]);
2523 let err = u2f_call(&statement, &key, COSE_ALG_ES256, &auth_data[..31], &cdh).unwrap_err();
2524 assert!(
2525 matches!(err, WebauthnError::AttestationError(msg) if msg.contains("too short for fido-u2f"))
2526 );
2527 }
2528}