Skip to main content

webauthn_kit/
attestation.rs

1//! Attestation statement parsing and verification (WebAuthn Level 2 §8).
2//!
3//! Tranche 1 implements the three formats that cover the overwhelming
4//! majority of real registrations:
5//!
6//! - **`none`** (§8.1): no attestation statement; the ceremony reduces to
7//!   proving possession of the credential private key. Enforced strictly:
8//!   the statement must be an empty CBOR map ([`AttestationFormat::None`]).
9//! - **`packed`** (§8.2): self-attestation (signature with the credential
10//!   key) or basic/AttCA attestation (signature with the first certificate's
11//!   key, optionally chained to a configured trust anchor).
12//! - **`fido-u2f`** (§8.6): the legacy FIDO U2F attestation, verified over
13//!   the U2F transport encoding with exactly one attestation certificate.
14//!
15//! # Trust levels
16//!
17//! [`TrustLevel`] reports what a *verified* attestation actually proves:
18//!
19//! - [`TrustLevel::None`]: key possession only (`none` format).
20//! - [`TrustLevel::SelfAttested`]: the credential key signed the statement
21//!   (packed self-attestation) — possession, not provenance.
22//! - [`TrustLevel::BasicAtt`]: a certificate chain internally consistent and
23//!   rooted at a self-signed certificate, but **not** anchored to any RP-
24//!   configured trust anchor. Anyone can mint such a chain; treat exactly
25//!   like self-attestation. A warning is always emitted (see
26//!   [`warnings::UNANCHORED_CHAIN`]).
27//! - [`TrustLevel::AttCa`]: the chain terminates at a caller-configured
28//!   trust anchor — the only level that attests device provenance.
29//!
30//! # Security notes / threat assumptions
31//!
32//! - Every statement field arrives from the hostile client. Parsing is total:
33//!   malformed input yields `Err`, never a panic (fuzz-tested).
34//! - Signature checks are fail-closed: any unexpected certificate property
35//!   (unsupported algorithm, non-v3, expired, non-CA intermediate, AAGUID
36//!   extension mismatch) aborts verification.
37//! - Trust-anchor matching is caller policy: `AttestationPolicy::trust_anchors`
38//!   empty accepts unanchored chains *flagged* as [`TrustLevel::BasicAtt`]
39//!   with [`warnings::UNANCHORED_CHAIN`] — callers enforcing provenance MUST
40//!   configure anchors and inspect [`AttestationResult::trust_level`].
41//! - Certificate revocation (OCSP/CRL) is out of scope; see THREAT-MODEL.md.
42//!
43//! # Requirements
44//! REQ-WA-119, REQ-WA-120, REQ-WA-121, REQ-WA-122, REQ-WA-123, REQ-WA-124,
45//! REQ-WA-125, REQ-WA-126, REQ-WA-127, REQ-WA-128, REQ-WA-129, REQ-WA-130,
46//! REQ-WA-132
47
48use 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// ---------------------------------------------------------------------------
60// Public API types
61// ---------------------------------------------------------------------------
62
63/// WebAuthn attestation statement format identifier (WebAuthn L2 §5.1.4).
64///
65/// Parsed from the `fmt` string of the attestation object. [`AttestationFormat::Unknown`]
66/// exists so a registration can be accepted under the documented
67/// `allow_unknown_formats` escape hatch while still reporting honestly that
68/// its statement was **not** verified.
69#[derive(Debug, Clone, PartialEq, Eq)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize))]
71pub enum AttestationFormat {
72    /// `fmt = "none"`: no attestation statement.
73    None,
74    /// `fmt = "packed"`: self-attestation or basic/AttCA attestation.
75    Packed,
76    /// `fmt = "fido-u2f"`: legacy FIDO U2F attestation.
77    FidoU2f,
78    /// Any other `fmt` value (accepted only via the `allow_unknown_formats`
79    /// escape hatch; the statement is never verified).
80    Unknown,
81}
82
83impl AttestationFormat {
84    /// Parse an attestation-object `fmt` string.
85    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    /// Canonical `fmt` string for this format (unknown formats report
95    /// `"unknown"`; see [`AttestationFormat::Unknown`]).
96    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/// What a verified attestation proves about the credential's provenance.
107///
108/// Ordered from weakest to strongest. See the module docs for the exact
109/// semantics — in particular [`TrustLevel::BasicAtt`] chains are *not*
110/// anchored to a configured trust anchor and must be treated as
111/// [`TrustLevel::SelfAttested`] by policy.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize))]
114pub enum TrustLevel {
115    /// No attestation claim (`none` format): key possession only.
116    None,
117    /// The credential key itself signed the statement (packed
118    /// self-attestation): key possession only.
119    SelfAttested,
120    /// A syntactically valid, internally consistent certificate chain rooted
121    /// at a self-signed certificate that is **not** an RP-configured trust
122    /// anchor. Equivalent in trust to [`TrustLevel::SelfAttested`]; always
123    /// accompanied by [`warnings::UNANCHORED_CHAIN`].
124    BasicAtt,
125    /// The chain terminates at an RP-configured trust anchor: real device
126    /// provenance (AttCA / basic attestation with a trusted root).
127    AttCa,
128}
129
130/// Warning strings emitted into [`AttestationResult::warnings`].
131///
132/// Warnings are stable string constants so callers can match on them.
133pub mod warnings {
134    /// A certificate chain was accepted without any configured trust anchor;
135    /// its provenance is equivalent to self-attestation.
136    pub const UNANCHORED_CHAIN: &str =
137        "attestation chain accepted without a configured trust anchor; \
138         treat as self-attested";
139    /// An unknown attestation format was accepted via the
140    /// `allow_unknown_formats` escape hatch; its statement was NOT verified.
141    pub const UNVERIFIED_FORMAT: &str =
142        "unknown attestation format accepted via allow_unknown_formats; \
143         statement was not verified";
144}
145
146/// Result of attestation verification for a registration.
147#[derive(Debug, Clone)]
148#[cfg_attr(feature = "serde", derive(serde::Serialize))]
149pub struct AttestationResult {
150    /// The attestation format that was used.
151    pub format: AttestationFormat,
152    /// AAGUID from attested credential data (always present during
153    /// registration; U2F authenticators report the all-zero AAGUID).
154    pub aaguid: Option<[u8; 16]>,
155    /// What the verified attestation proves. Callers enforcing device
156    /// provenance MUST require [`TrustLevel::AttCa`].
157    pub trust_level: TrustLevel,
158    /// Trust-relevant warnings (e.g. [`warnings::UNANCHORED_CHAIN`]).
159    pub warnings: Vec<&'static str>,
160}
161
162/// Caller policy for attestation verification.
163///
164/// # Security notes
165///
166/// - `trust_anchors` empty means *any* internally-consistent, self-rooted
167///   chain is accepted at [`TrustLevel::BasicAtt`] with
168///   [`warnings::UNANCHORED_CHAIN`] — a deliberate flag-not-block default so
169///   unmodified integrations keep working, documented as trust-weakening.
170///   Configure your attestation root certificates here to upgrade verified
171///   chains to [`TrustLevel::AttCa`].
172/// - `allow_unknown_formats = true` is an explicit trust-weakening escape
173///   hatch: registrations with unrecognized `fmt` values are accepted with
174///   [`warnings::UNVERIFIED_FORMAT`] instead of rejected. Leave it `false`
175///   unless you know you need it.
176#[derive(Debug, Clone, Default)]
177pub struct AttestationPolicy {
178    /// DER-encoded X.509 attestation root (trust anchor) certificates.
179    /// Empty accepts any self-rooted chain, flagged (see struct docs).
180    pub trust_anchors: Vec<Vec<u8>>,
181    /// Accept unknown attestation formats with a warning instead of
182    /// rejecting them (trust-weakening; see struct docs).
183    pub allow_unknown_formats: bool,
184}
185
186impl AttestationPolicy {
187    /// Strict policy: no trust anchors, unknown formats rejected.
188    ///
189    /// This still verifies `none`/`packed`/`fido-u2f` self-attestation and
190    /// unanchored chains (flagged); add `trust_anchors` to enforce
191    /// provenance ([`TrustLevel::AttCa`] only).
192    pub fn strict() -> Self {
193        Self::default()
194    }
195}
196
197/// Parsed attestation statement (CBOR `attStmt` map).
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub(crate) enum AttestationStatement {
200    /// Empty statement (`none` format).
201    None,
202    /// Packed statement: COSE algorithm, signature, optional cert chain.
203    Packed {
204        /// COSE algorithm identifier used for `sig`.
205        alg: i32,
206        /// Signature over `authData || clientDataHash`.
207        sig: Vec<u8>,
208        /// Optional X.509 certificate chain (DER), leaf first.
209        x5c: Vec<Vec<u8>>,
210    },
211    /// FIDO U2F statement: signature plus exactly one certificate.
212    FidoU2f {
213        /// Signature over the U2F registration message.
214        sig: Vec<u8>,
215        /// The single attestation certificate (DER).
216        cert: Vec<u8>,
217    },
218}
219
220// ---------------------------------------------------------------------------
221// OID constants (compared as DER-encoded bytes via `Oid::as_bytes`)
222// ---------------------------------------------------------------------------
223
224/// OID 1.2.840.113549.1.1.11 — sha256WithRSAEncryption.
225const OID_SIG_SHA256_WITH_RSA: &[u8] = &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B];
226/// OID 1.2.840.10045.4.3.2 — ecdsa-with-SHA256.
227const OID_SIG_ECDSA_WITH_SHA256: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02];
228/// OID 1.2.840.113549.1.1.1 — rsaEncryption.
229const OID_ALG_RSA_ENCRYPTION: &[u8] = &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01];
230/// OID 1.2.840.10045.2.1 — id-ecPublicKey.
231const OID_ALG_EC_PUBLIC_KEY: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01];
232/// OID 2.5.29.19 — basicConstraints.
233const OID_EXT_BASIC_CONSTRAINTS: &[u8] = &[0x55, 0x1D, 0x13];
234/// OID 1.3.6.1.4.1.45724.1.1.4 — id-fido-gen-ce-aaguid (FIDO 2.1 §8.2.1).
235const OID_EXT_FIDO_GEN_CE_AAGUID: &[u8] =
236    &[0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xE5, 0x14, 0x01, 0x04];
237
238// ---------------------------------------------------------------------------
239// Statement parsing
240// ---------------------------------------------------------------------------
241
242/// CBOR `attStmt` label: `alg` (COSE algorithm identifier).
243const STMT_LABEL_ALG: i64 = 3;
244/// CBOR `attStmt` label: `sig` (signature byte string).
245const STMT_LABEL_SIG: i64 = 8;
246/// CBOR `attStmt` label: `x5c` (X.509 certificate chain array).
247const STMT_LABEL_X5C: i64 = 33;
248
249/// Parse the raw `attStmt` CBOR value for the given format.
250///
251/// `att_stmt` is `None` when the attestation object carried no statement
252/// entry at all (only legal for the `none` format).
253///
254/// # Requirements
255/// REQ-WA-119, REQ-WA-128, REQ-WA-132
256pub(crate) fn parse_statement(
257    format: &AttestationFormat,
258    att_stmt: Option<&Value>,
259) -> Result<AttestationStatement, WebauthnError> {
260    match format {
261        AttestationFormat::None => {
262            // WebAuthn L2 §8.1: attStmt MUST be an empty CBOR map.
263            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            // WebAuthn L2 §8.6: x5c carries exactly one attestation certificate.
332            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        // Unknown formats are a policy decision, never parsed.
344        AttestationFormat::Unknown => Err(WebauthnError::AttestationError(
345            "unknown attestation format cannot be parsed".to_string(),
346        )),
347    }
348}
349
350/// Extract an `i32` COSE algorithm from a CBOR value.
351fn cbor_alg(val: &Value) -> Option<i32> {
352    match val {
353        Value::Integer(i) => (*i).try_into().ok(),
354        _ => None,
355    }
356}
357
358/// Extract the `x5c` certificate array: a CBOR array of byte strings.
359fn 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// ---------------------------------------------------------------------------
379// Verification entry point
380// ---------------------------------------------------------------------------
381
382/// Verify an attestation statement for a registration ceremony.
383///
384/// Dispatches on the parsed format (see the module docs for the per-format
385/// verification procedures) and returns the attestation result on success.
386///
387/// Parameters:
388///
389/// - `fmt`: the raw `fmt` string from the attestation object.
390/// - `att_stmt`: the raw CBOR `attStmt` value (`None` if absent).
391/// - `auth_data`: the raw authenticator data bytes.
392/// - `client_data_hash`: SHA-256 of the raw `clientDataJSON`.
393/// - `credential_id`: the raw credential ID from attested credential data.
394/// - `credential_alg` / `credential_key`: the COSE algorithm and parsed
395///   public key of the *new credential* (from attested credential data).
396/// - `aaguid`: the 16-byte AAGUID from attested credential data.
397/// - `policy`: the caller's trust-anchor / unknown-format policy.
398///
399/// # Requirements
400/// REQ-WA-119, REQ-WA-120, REQ-WA-121, REQ-WA-122, REQ-WA-123, REQ-WA-124,
401/// REQ-WA-126, REQ-WA-127, REQ-WA-128, REQ-WA-129, REQ-WA-130
402#[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    // Unknown format: reject unless the caller opted in (REQ-WA-126).
417    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
472/// `none` format: the statement itself was already checked to be an empty
473/// map by [`parse_statement`] — the registration ceremony establishes key
474/// possession and nothing more.
475///
476/// # Requirements
477/// REQ-WA-119, REQ-WA-128
478fn 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
487/// Packed self-attestation (no x5c): the signature over
488/// `authData || clientDataHash` MUST verify with the credential public key
489/// and the statement `alg` MUST match the credential key's algorithm.
490///
491/// # Requirements
492/// REQ-WA-120, REQ-WA-127
493fn 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    // Cross-checks key type vs alg and verifies with ring (constant-time).
510    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
520/// Packed basic/AttCA attestation (x5c present): the signature MUST verify
521/// with the first certificate's public key, the statement `alg` MUST match
522/// the certificate key type, and the chain MUST satisfy the policy.
523///
524/// # Requirements
525/// REQ-WA-121, REQ-WA-122, REQ-WA-123, REQ-WA-125, REQ-WA-127
526fn 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    // The statement signs with the attestation certificate's key; its alg
536    // must be one of the algorithms this crate verifies at all.
537    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    // Statement alg vs certificate key type cross-check (fail closed on
546    // mismatch, mirroring REQ-WA-107 semantics for COSE keys).
547    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    // AAGUID extension: if present, MUST match the attested credential data
562    // AAGUID (FIDO 2.1 §8.2.1).
563    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/// FIDO U2F attestation (WebAuthn L2 §8.6): exactly one certificate; the
576/// signature MUST verify over
577/// `0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F`
578/// with the certificate's ES256 public key, where `publicKeyU2F` is the
579/// 65-byte `0x04 || X || Y` transport form of the credential's EC2 key.
580///
581/// # Requirements
582/// REQ-WA-124, REQ-WA-127, REQ-WA-129
583#[allow(clippy::indexing_slicing)] // rpIdHash slice is length-guarded above
584#[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    // U2F credentials are always ES256 on P-256; anything else in the
597    // credential slot means this is not a U2F transport registration.
598    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    // publicKeyU2F: 0x04 || X || Y (FIDO U2F Raw Message Formats §4.3).
617    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    // rpIdHash is the first 32 bytes of authData; length pre-checked above.
635    verification_data.extend_from_slice(&auth_data[..32]); // rpIdHash
636    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// ---------------------------------------------------------------------------
653// X.509 certificate handling and chain verification
654// ---------------------------------------------------------------------------
655
656/// Key material kinds this crate can verify with (mirrors `CosePublicKey`).
657#[derive(Debug, Clone, Copy, PartialEq, Eq)]
658enum KeyKind {
659    /// EC P-256 (`id-ecPublicKey`, ES256).
660    Ec2,
661    /// RSA (`rsaEncryption`, RS256, ≥2048-bit enforced by ring).
662    Rsa,
663}
664
665/// Certificate public key in the form `ring` consumes.
666enum CertPublicKey {
667    /// Uncompressed EC point `0x04 || X || Y`.
668    EcPoint(Vec<u8>),
669    /// DER `RSAPublicKey` (PKCS#1) — the SubjectPublicKeyInfo BIT STRING
670    /// content, which is the encoding `ring`'s RSA verifiers parse.
671    RsaKey(Vec<u8>),
672}
673
674/// A parsed, structurally validated X.509 certificate.
675struct Cert {
676    /// Raw TBS bytes (the signed content).
677    tbs: Vec<u8>,
678    /// DER signature value (without the BIT STRING wrapper).
679    signature: Vec<u8>,
680    /// Signature algorithm OID (DER bytes).
681    signature_alg_oid: Vec<u8>,
682    /// Verifier input for this certificate's own public key.
683    verifier_key: CertPublicKey,
684    /// Issuer name (raw DER, for chain linking).
685    issuer: Vec<u8>,
686    /// Subject name (raw DER, for chain linking).
687    subject: Vec<u8>,
688    /// This certificate's key kind.
689    kind: KeyKind,
690    /// Not-before / not-after (unix seconds).
691    not_before: i64,
692    not_after: i64,
693    /// Raw `(oid, value)` extension list.
694    extensions: Vec<(Vec<u8>, Vec<u8>)>,
695}
696
697impl Cert {
698    /// Parse and structurally validate a DER certificate.
699    ///
700    /// Rejects: malformed DER, trailing bytes after the certificate,
701    /// non-v3 certificates, and unsupported public key algorithms.
702    ///
703    /// # Requirements
704    /// REQ-WA-122, REQ-WA-130, REQ-WA-132
705    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            // ring's RSA verifier parses PKCS#1 `RSAPublicKey`, i.e. the
734            // SPKI's subjectPublicKey BIT STRING content.
735            (
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    /// Validity window contains `now` (unix seconds).
766    fn is_valid_at(&self, now: i64) -> bool {
767        self.not_before <= now && now <= self.not_after
768    }
769
770    /// Verify this certificate's own signature with the parent's key.
771    ///
772    /// Only `ecdsa-with-SHA256` (EC P-256 parent) and
773    /// `sha256WithRSAEncryption` (RSA parent) are accepted — anything else
774    /// fails closed.
775    ///
776    /// # Requirements
777    /// REQ-WA-122
778    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    /// basicConstraints `cA` flag; `None` when the extension is absent.
825    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
834/// Parse a basicConstraints extension value:
835/// `SEQUENCE { BOOLEAN DEFAULT FALSE, INTEGER OPTIONAL }`.
836///
837/// Returns the `cA` flag — `false` when the BOOLEAN is omitted (DER
838/// defaulting) or when only a pathLenConstraint INTEGER is present.
839fn parse_basic_constraints(value: &[u8]) -> Option<bool> {
840    let content = der_content(value, 0x30)?;
841    if content.first() == Some(&0x01) && content.len() >= 3 {
842        // Explicit BOOLEAN: tag (0x01), length (0x01), value.
843        return Some(content.get(2).copied() == Some(0xFF));
844    }
845    Some(false)
846}
847
848/// Return the content octets of a DER TLV with the given tag byte, strictly
849/// validating that the declared length matches the available bytes.
850fn 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
870/// Verify the AAGUID extension (`id-fido-gen-ce-aaguid`) on the leaf, when
871/// present: its value is a DER OCTET STRING wrapping exactly 16 bytes that
872/// MUST equal the attested credential data AAGUID.
873///
874/// # Requirements
875/// REQ-WA-125
876fn 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(()); // extension is optional
883    };
884
885    // extnValue content: OCTET STRING (tag 0x04) of length 16 wrapping the
886    // AAGUID (FIDO 2.1 §8.2.1).
887    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
903/// Verify the x5c chain and return the resulting trust level.
904///
905/// Checks, in order (fail closed):
906///
907/// 1. Every certificate parses and is X.509 v3 ([`Cert::from_der`]).
908/// 2. Every certificate is within its validity window *now* (REQ-WA-130).
909/// 3. Each certificate is signed by the next with a supported algorithm;
910///    issuer/subject names link byte-exactly; intermediates declare
911///    basicConstraints `cA = true` (REQ-WA-122).
912/// 4. Anchor policy (REQ-WA-123):
913///    - anchors configured → the chain root must equal an anchor, or be
914///      signed by one, → [`TrustLevel::AttCa`]; otherwise reject;
915///    - no anchors → the chain root must be self-signed (self-signature
916///      verified) → [`TrustLevel::BasicAtt`] +
917///      [`warnings::UNANCHORED_CHAIN`].
918fn 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    // Signature chain: leaf → intermediate(s) → root. `windows(2)` always
941    // yields exactly-2 slices, so the destructuring below is infallible;
942    // allow the lint rather than an unreachable expect.
943    #[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    // Non-leaf certificates must declare CA: true (basicConstraints).
955    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    // Anchor policy (REQ-WA-123).
971    for anchor in &policy.trust_anchors {
972        // Case 1: the chain carries the anchor certificate itself.
973        if root_der == anchor {
974            return Ok((TrustLevel::AttCa, Vec::new()));
975        }
976        // Case 2: the chain root is issued directly by the anchor.
977        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    // No anchors configured: accept only a chain rooted at a self-signed
989    // certificate (self-signature verified), and flag it.
990    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
1001// ---------------------------------------------------------------------------
1002// Shared helpers
1003// ---------------------------------------------------------------------------
1004
1005/// The bytes covered by a WebAuthn registration attestation signature:
1006/// `authenticatorData || SHA-256(clientDataJSON)` (the caller supplies the
1007/// precomputed hash).
1008fn 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
1015/// Verify `data`/`sig` with an X.509 certificate's public key via `ring`.
1016///
1017/// Only ES256 (EC P-256) and RS256 (RSA PKCS#1 v1.5, ≥2048-bit) are
1018/// supported, matching this crate's COSE algorithm support.
1019///
1020/// # Requirements
1021/// REQ-WA-121, REQ-WA-124, REQ-WA-127
1022fn 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// Tests exercise failure paths and invariants directly; unwrap/expect,
1046// slicing, and panicking asserts are acceptable here — violations
1047// surface as test failures, not production panics.
1048#[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    // ------------------------------------------------------------------
1063    // Minimal DER builder for hand-crafted certificates. Signatures are
1064    // made with `ring` in exactly the encodings this crate verifies
1065    // (fixed-length ECDSA per COSE, PKCS#1 v1.5 for RSA), so chains built
1066    // here exercise the real verification paths. Parse-only variants
1067    // (junk signatures) cover error paths that abort before any
1068    // signature check.
1069    // ------------------------------------------------------------------
1070
1071    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    /// Full RFC 5280 SubjectPublicKeyInfo carrying the fixed test key's
1088    /// `RSAPublicKey` (what a certificate embeds).
1089    fn rsa_spki() -> Vec<u8> {
1090        der_seq(&[&alg_id(OID_RSA_ENCRYPTION), &der_bit(&rsa_pubkey_der())])
1091    }
1092
1093    /// PKCS#1 `RSAPublicKey` DER for the fixed test key (what ring parses).
1094    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    /// Build the TBSCertificate bytes (X.509 v3 unless `v1`).
1194    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    /// Wrap a signed TBS into a full certificate.
1224    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    /// EC P-256 certificate signed with the fixed-length (COSE) encoding
1229    /// this crate verifies. `signer = None` → self-signed with `key`.
1230    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    /// Self-signed EC certificate helper.
1255    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    /// RSA certificate signed with PKCS#1 v1.5 SHA-256 (matching SPKI).
1261    /// `signer = None` → self-signed with `key`.
1262    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    /// Parse-only certificate builder (junk signature): for error paths
1290    /// that abort before any signature check.
1291    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    /// basicConstraints extension (cA=true).
1297    fn bc_ca_true() -> Vec<u8> {
1298        extension(OID_BASIC_CONSTRAINTS, &[0x30, 0x03, 0x01, 0x01, 0xFF])
1299    }
1300
1301    /// basicConstraints extension (explicit cA=false).
1302    fn bc_ca_false() -> Vec<u8> {
1303        extension(OID_BASIC_CONSTRAINTS, &[0x30, 0x03, 0x01, 0x01, 0x00])
1304    }
1305
1306    // ------------------------------------------------------------------
1307    // Chain fixtures
1308    // ------------------------------------------------------------------
1309
1310    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    /// `anchor --signs--> issuer --signs--> leaf` (all P-256). Returns
1317    /// (leaf, issuer, anchor, leaf signing key).
1318    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    // ------------------------------------------------------------------
1348    // Ceremony fixtures
1349    // ------------------------------------------------------------------
1350
1351    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    /// (authData, clientDataHash, credential key, credential signing key).
1359    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    /// Drive `verify_attestation` with a packed statement and the shared
1401    /// fixture; returns the raw result.
1402    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    // ------------------------------------------------------------------
1457    // Format parsing and policy
1458    // ------------------------------------------------------------------
1459
1460    #[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        // attStmt not a map
1527        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        // missing alg (text-string alg is not an integer)
1533        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        // alg does not fit i32
1540        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        // missing sig
1550        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        // x5c not an array
1557        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        // x5c contains a non-byte-string entry
1568        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]; // structural bytes; not parsed here
1582        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        // missing attStmt / not a map / missing sig
1595        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        // x5c must carry exactly one certificate
1615        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    // ------------------------------------------------------------------
1642    // verify_attestation dispatch: unknown / none
1643    // ------------------------------------------------------------------
1644
1645    #[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    // ------------------------------------------------------------------
1708    // Packed self-attestation
1709    // ------------------------------------------------------------------
1710
1711    #[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        // Statement claims RS256, credential key is ES256.
1732        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    // ------------------------------------------------------------------
1761    // ES384 / EdDSA credentials through the attestation paths
1762    // ------------------------------------------------------------------
1763
1764    use crate::crypto::{COSE_ALG_EDDSA, COSE_ALG_ES384};
1765
1766    /// (authData, clientDataHash, P-384 credential key, signing key).
1767    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    /// (authData, clientDataHash, Ed25519 credential key, signing key).
1784    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    /// Packed self-attestation with an ES384 credential key: the signature
1803    /// over authData ‖ clientDataHash verifies via the ES384 arm.
1804    #[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    /// Packed self-attestation with an Ed25519 credential key.
1830    #[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    /// `none` attestation is algorithm-agnostic: an ES384 credential
1851    /// registers with TrustLevel::None and no warnings.
1852    #[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    /// Packed **x5c** attestation statements remain limited to ES256/RS256:
1872    /// the certificate chain machinery only parses P-256/RSA keys, so an
1873    /// ES384 statement alg fails closed rather than mis-verifying.
1874    #[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    // ------------------------------------------------------------------
1894    // Packed x5c attestation
1895    // ------------------------------------------------------------------
1896
1897    /// Self-signed EC attestation certificate, no trust anchors configured:
1898    /// verifies at BasicAtt with the unanchored-chain warning.
1899    #[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    /// RSA attestation certificate + real RSA statement signature (RS256).
1922    #[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    /// Self-signed RSA certificate helper.
1949    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        // RS256 statement with an EC attestation certificate: fail closed.
1973        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    // -- leaf certificate structural rejections (parsing happens before
2006    //    any signature check, so junk-signature fixtures suffice) --
2007
2008    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, // v1: no explicit version field
2052        );
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]), // compressed point: not a 0x04-prefixed 65B point
2072        ]);
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    // -- chain verification through the public entry point --
2118
2119    #[test]
2120    fn packed_x5c_chain_carrying_anchor_is_att_ca() {
2121        // Two-level chain whose root IS the anchor certificate.
2122        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        // The chain root is *issued by* the anchor, which is not itself in
2150        // the chain — the second anchor-matching case.
2151        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        // Unrelated (but well-formed) anchor.
2189        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        // [leaf, issuer] with no anchors: the chain root is not self-signed.
2224        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        // Leaf from one CA, "issuer" from an unrelated CA.
2273        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        // basicConstraints cA=false on the parent → must be rejected.
2293        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    // ------------------------------------------------------------------
2323    // AAGUID extension
2324    // ------------------------------------------------------------------
2325
2326    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        // Absent extension is fine (optional per FIDO 2.1 §8.2.1).
2369        let plain = Cert::from_der(&ec_self_cert("attestor", &ec_key(), &[])).unwrap();
2370        assert!(check_aaguid_extension(&plain, &AAGUID).is_ok());
2371    }
2372
2373    // ------------------------------------------------------------------
2374    // basicConstraints / DER content helpers
2375    // ------------------------------------------------------------------
2376
2377    #[test]
2378    fn parse_basic_constraints_variants() {
2379        // Explicit BOOLEAN.
2380        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        // Empty SEQUENCE → DER default (cA = false).
2389        assert_eq!(parse_basic_constraints(&[0x30, 0x00]), Some(false));
2390        // BOOLEAN truncated below 3 content bytes → default false.
2391        assert_eq!(parse_basic_constraints(&[0x30, 0x01, 0x01]), Some(false));
2392        // Not a SEQUENCE / empty input.
2393        assert_eq!(parse_basic_constraints(&[0x02, 0x00]), None);
2394        assert_eq!(parse_basic_constraints(&[]), None);
2395        // Long-form lengths.
2396        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        // Indefinite length → None; long-form length with empty content →
2405        // default false.
2406        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        // Declared length beyond available bytes → None.
2412        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    // ------------------------------------------------------------------
2429    // verify_signed_by (child, parent) algorithm handling
2430    // ------------------------------------------------------------------
2431
2432    #[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        // Flip the last signature byte (inside the final BIT STRING) — the
2440        // certificate still parses but the signature must fail.
2441        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        // RSA CA signs an EC-SPKI leaf with PKCS#1 v1.5 SHA-256.
2454        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        // ECDSA-signed child over an RSA parent.
2492        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        // RSA-signed child over an EC parent.
2509        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        // Unsupported signature algorithm fails closed.
2526        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    // ------------------------------------------------------------------
2541    // FIDO U2F attestation
2542    // ------------------------------------------------------------------
2543
2544    #[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        // Signature content is irrelevant: the credential key is rejected
2588        // before any signature check.
2589        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        // RSA attestation certificate (self-signed, real signature).
2603        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}