Skip to main content

matter_commissioning/attestation/cd/
verifier.rs

1//! Certification Declaration verifier implementation.
2//!
3//! The verifier performs five checks in order:
4//!
5//! 1. CMS `SignedData` DER parse via the `cms` crate.
6//! 2. Structural validation: a single `SignerInfo`, attached
7//!    encapsulated content.
8//! 3. ECDSA-P256 / SHA-256 signature verification against each
9//!    trusted root's public key; accept on first match.
10//! 4. Decode the inner CD TLV via `parse_inner_cd_tlv` (M6.4.3 T30
11//!    fills the helper).
12//! 5. Cross-check declared VID + PID against the expected pair
13//!    supplied by the caller (typically sourced from the verified DAC
14//!    subject).
15//!
16//! The trust store ([`CdSigningRoots`]) holds SEC1-uncompressed P-256
17//! public keys for each trusted CSA signing root. Production callers
18//! build the store via [`CdSigningRoots::from_pem`]; tests and
19//! examples use the bundled CSA-test root via
20//! [`CdSigningRoots::with_example_device_roots`].
21
22#![forbid(unsafe_code)]
23
24use crate::attestation::{AttestationError, ProductId, VendorId};
25
26/// Bundled PEM-encoded `SubjectPublicKeyInfo` for the synthetic
27/// CSA-test CD signing root generated by `xtask capture-cd`. The
28/// matching private key lives in `test-vectors/commissioning/cd/` and
29/// signs the fixture CDs the verifier tests consume.
30const CSA_TEST_CD_SIGNING_ROOT_PEM: &[u8] =
31    include_bytes!("./csa_cd_signing_roots/csa-test-cd-signing-root.pem");
32
33/// chip's real **test** CD signing authority (X.509 DER). Signs the CDs
34/// of chip's example/test devices that use the test signer (e.g.
35/// `Chip-Test-CD-FFF2-8001`). Vendored from connectedhomeip
36/// `credentials/test/certification-declaration/`.
37const CHIP_TEST_CD_SIGNING_CERT_DER: &[u8] =
38    include_bytes!("./csa_cd_signing_roots/Chip-Test-CD-Signing-Cert.der");
39
40/// CSA **production** "CD Signing Key 001" (X.509 DER). Signs the
41/// VID=0xFFF1 CD every `CONFIG_EXAMPLE_DAC_PROVIDER` device serves,
42/// including the esp-matter ESP32-C6. Vendored from connectedhomeip
43/// `credentials/production/cd-certs/`.
44const CSA_CD_SIGNING_KEY_001_DER: &[u8] =
45    include_bytes!("./csa_cd_signing_roots/CSA-CD-Signing-Key-001.der");
46
47/// Trusted CSA Certification Declaration signing roots.
48///
49/// Built from production roots via [`Self::from_cert_der`] (X.509 CD
50/// signing certificates, as published by the CSA DCL) or
51/// [`Self::from_pem`] (bare `SubjectPublicKeyInfo` PEMs), or seeded with
52/// the bundled synthetic CSA-test root via
53/// [`Self::with_example_device_roots`].
54///
55/// Internally stores each trusted root as a SEC1-uncompressed P-256
56/// public key (65 bytes: `0x04 || X || Y`) so signature verification
57/// can call `ring::signature::UnparsedPublicKey` directly without
58/// re-parsing.
59#[derive(Debug, Clone)]
60pub struct CdSigningRoots {
61    /// SEC1-uncompressed P-256 public keys (65 bytes each) for each
62    /// trusted root.
63    public_keys: Vec<Vec<u8>>,
64}
65
66impl CdSigningRoots {
67    /// Build a trust store seeded with the CD signing roots that verify
68    /// CSA **test / example** devices and the hermetic loopback:
69    ///
70    /// - the bundled **synthetic** root — its private half signs the
71    ///   loopback and fixture CDs the verifier tests consume;
72    /// - chip's real **test** CD signing authority; and
73    /// - CSA **production** "CD Signing Key 001" — the key that signs the
74    ///   VID=0xFFF1 CD every `CONFIG_EXAMPLE_DAC_PROVIDER` device serves,
75    ///   including the esp-matter ESP32-C6. (chip's own
76    ///   `DefaultDeviceAttestationVerifier` trusts the test *and*
77    ///   production keys; trusting only the test key rejects the C6, which
78    ///   cost a live commission to learn — see `chip_cd_vector.rs`.)
79    ///
80    /// This verifies test / dev / example devices — **not** the full set
81    /// of CSA production-certified products, which may present CDs signed
82    /// by other CSA production keys. A commissioner for arbitrary
83    /// certified devices loads the whole CSA root set via
84    /// [`Self::from_cert_der`] / [`Self::from_pem`] (e.g.
85    /// `matter_controller::AttestationTrust::from_dirs`).
86    ///
87    /// Each bundled root is a compile-time constant; one that fails to
88    /// parse is skipped rather than panicking, and the verifier then
89    /// rejects any CD that needed it with
90    /// [`AttestationError::CertificationDeclarationSignatureInvalid`].
91    #[must_use]
92    pub fn with_example_device_roots() -> Self {
93        let mut public_keys = Vec::with_capacity(3);
94        if let Ok(pk) = parse_pem_public_key(CSA_TEST_CD_SIGNING_ROOT_PEM) {
95            public_keys.push(pk);
96        }
97        for der in [CHIP_TEST_CD_SIGNING_CERT_DER, CSA_CD_SIGNING_KEY_001_DER] {
98            if let Some(pk) = cert_der_public_key(der) {
99                public_keys.push(pk);
100            }
101        }
102        Self { public_keys }
103    }
104
105    /// Build a trust store from PEM-encoded P-256
106    /// `SubjectPublicKeyInfo` blobs (one per trusted CSA signing
107    /// root).
108    ///
109    /// # Errors
110    ///
111    /// Returns [`AttestationError::CertificationDeclarationMalformed`]
112    /// if any input fails to parse. An empty slice returns an empty
113    /// trust store.
114    pub fn from_pem(pems: &[&[u8]]) -> Result<Self, AttestationError> {
115        let mut public_keys = Vec::with_capacity(pems.len());
116        for raw in pems {
117            let pk = parse_pem_public_key(raw)?;
118            public_keys.push(pk);
119        }
120        Ok(Self { public_keys })
121    }
122
123    /// Build a trust store from X.509 **certificate** DER blobs (one per
124    /// trusted CSA CD signing root), extracting each certificate's P-256
125    /// subject public key.
126    ///
127    /// This is the ingestion path for real-world CD signing roots: the CSA
128    /// Distributed Compliance Ledger — and the `connectedhomeip`
129    /// `credentials/production/cd-certs/` mirror of it — publish the roots as
130    /// X.509 certificates, not as the bare `SubjectPublicKeyInfo` PEMs that
131    /// [`Self::from_pem`] consumes. There are several distinct CSA CD signing
132    /// keys, so a real commissioner typically loads the whole directory.
133    ///
134    /// The certificate is treated purely as a trust anchor: only its subject
135    /// public key is extracted. No signature, validity-window, or chain checks
136    /// are performed — the operator vouches for the roots by supplying them
137    /// (exactly as [`Self::from_pem`] trusts the keys it is given).
138    ///
139    /// # Errors
140    ///
141    /// Returns [`AttestationError::CertificationDeclarationMalformed`] if any
142    /// input fails to parse as an X.509 certificate, or does not carry a
143    /// 65-byte SEC1-uncompressed P-256 public key.
144    pub fn from_cert_der(certs: &[&[u8]]) -> Result<Self, AttestationError> {
145        let mut public_keys = Vec::with_capacity(certs.len());
146        for der in certs {
147            // CD signatures are ECDSA-P256; the trust root must carry a
148            // SEC1-uncompressed P-256 point (`0x04` || X || Y, 65 bytes).
149            let pk = cert_der_public_key(der)
150                .ok_or(AttestationError::CertificationDeclarationMalformed)?;
151            public_keys.push(pk);
152        }
153        Ok(Self { public_keys })
154    }
155
156    /// Number of trusted roots in the store.
157    #[must_use]
158    pub fn len(&self) -> usize {
159        self.public_keys.len()
160    }
161
162    /// Returns `true` if no trusted roots have been loaded.
163    #[must_use]
164    pub fn is_empty(&self) -> bool {
165        self.public_keys.is_empty()
166    }
167
168    /// Internal accessor — borrow the raw 65-byte SEC1 uncompressed
169    /// public-key bytes for each trusted root, for the verifier's
170    /// signature-check loop.
171    fn keys(&self) -> &[Vec<u8>] {
172        &self.public_keys
173    }
174}
175
176/// Verify a Certification Declaration against a trust store and an
177/// expected VID / PID pair, without enforcing the CD's
178/// `authorized_paa_list`.
179///
180/// Equivalent to [`verify_certification_declaration_with_paa`] with no
181/// device PAA `SubjectKeyIdentifier`: a CD carrying an `authorized_paa_list`
182/// (tag 11) is **not** enforced. Prefer the `_with_paa` form during
183/// commissioning, where the anchoring PAA's SKID is known — otherwise a
184/// device may present a CD that restricts itself to PAAs it does not
185/// actually chain to.
186///
187/// # Errors
188///
189/// See [`verify_certification_declaration_with_paa`].
190#[allow(
191    clippy::similar_names,
192    reason = "expected_vid/expected_pid mirror the crate-wide VendorId/ProductId vocabulary"
193)]
194pub fn verify_certification_declaration(
195    cd_bytes: &[u8],
196    expected_vid: VendorId,
197    expected_pid: ProductId,
198    trust: &CdSigningRoots,
199) -> Result<(), AttestationError> {
200    verify_certification_declaration_with_paa(cd_bytes, expected_vid, expected_pid, trust, None)
201}
202
203/// Verify a Certification Declaration extracted from
204/// `attestation_elements` (Matter Core Spec §6.3.1) against a trust
205/// store, an expected VID / PID pair, and — when supplied — the
206/// `SubjectKeyIdentifier` of the PAA that anchored the device's DAC chain.
207///
208/// Performs six checks in order:
209///
210/// 1. CMS `SignedData` DER parse via the `cms` crate.
211/// 2. Structural validation: a single `SignerInfo`, attached
212///    encapsulated content.
213/// 3. ECDSA-P256 / SHA-256 signature verification against each
214///    trusted root in `trust`; accept on first match.
215/// 4. Decode the inner CD TLV.
216/// 5. Cross-check declared VID + PID against `expected_vid` /
217///    `expected_pid` (sourced from the verified DAC chain by the
218///    caller). Per Matter Core Spec §6.2.3, when the CD carries both
219///    `dac_origin_vendor_id` (tag 9) and `dac_origin_product_id`
220///    (tag 10), those override fields are compared instead of the CD's
221///    own `vendor_id` / `product_id_array`.
222/// 6. If the CD carries an `authorized_paa_list` (tag 11), require
223///    `device_paa_skid` to be one of its entries (Matter §6.2.3, chip
224///    `DefaultDeviceAttestationVerifier.cpp:738`). Passing `None` for
225///    `device_paa_skid` skips this check only when the CD omits tag 11;
226///    a CD that *does* carry tag 11 is rejected when no SKID is supplied.
227///
228/// # Errors
229///
230/// - [`AttestationError::CertificationDeclarationMalformed`] — the
231///   CMS DER failed to parse or did not match the expected shape.
232/// - [`AttestationError::CertificationDeclarationSignatureInvalid`] —
233///   no trusted root accepts the signature.
234/// - [`AttestationError::CertificationDeclarationTlvMalformed`] —
235///   inner CD TLV missing required fields or malformed.
236/// - [`AttestationError::CertificationDeclarationVidMismatch`] —
237///   declared VID does not equal `expected_vid`.
238/// - [`AttestationError::CertificationDeclarationPidMismatch`] —
239///   declared PID list does not contain `expected_pid`.
240/// - [`AttestationError::CertificationDeclarationPaaNotAuthorized`] —
241///   the CD's `authorized_paa_list` does not include the device's PAA
242///   `SubjectKeyIdentifier`.
243#[allow(
244    clippy::similar_names,
245    clippy::too_many_lines,
246    reason = "the `expected_vid`/`expected_pid` pair mirrors the public-API \
247     vocabulary used elsewhere in this crate (VendorId/ProductId); \
248     renaming would obscure intent at the call site. The function is a \
249     single linear six-step CMS+TLV verification, clearer inline than split."
250)]
251pub fn verify_certification_declaration_with_paa(
252    cd_bytes: &[u8],
253    expected_vid: VendorId,
254    expected_pid: ProductId,
255    trust: &CdSigningRoots,
256    device_paa_skid: Option<&[u8]>,
257) -> Result<(), AttestationError> {
258    use cms::content_info::ContentInfo;
259    use cms::signed_data::SignedData;
260    use der::asn1::OctetString;
261    use der::{Decode, DecodeValue, Encode, Header, SliceReader, Tag as DerTag};
262
263    // 1. Parse the outer ContentInfo.
264    let content_info = ContentInfo::from_der(cd_bytes)
265        .map_err(|_| AttestationError::CertificationDeclarationMalformed)?;
266    // ContentInfo.content is an Any wrapping SignedData; re-encode and
267    // decode to convert.
268    let signed_data_der = content_info
269        .content
270        .to_der()
271        .map_err(|_| AttestationError::CertificationDeclarationMalformed)?;
272    let signed_data = SignedData::from_der(&signed_data_der)
273        .map_err(|_| AttestationError::CertificationDeclarationMalformed)?;
274
275    // 2. Validate shape: exactly one SignerInfo and attached content.
276    if signed_data.signer_infos.0.len() != 1 {
277        return Err(AttestationError::CertificationDeclarationMalformed);
278    }
279    let signer = signed_data
280        .signer_infos
281        .0
282        .iter()
283        .next()
284        .ok_or(AttestationError::CertificationDeclarationMalformed)?;
285
286    // 3. Extract the signed content (the inner CD TLV bytes).
287    //
288    // `EncapsulatedContentInfo.econtent` is typed `Option<Any>` where
289    // the Any wraps an OCTET STRING. We decode the OCTET STRING from
290    // the Any's body and copy its bytes — this is the eContent value
291    // (the inner CD TLV) the signer signed.
292    let econtent = signed_data
293        .encap_content_info
294        .econtent
295        .as_ref()
296        .ok_or(AttestationError::CertificationDeclarationMalformed)?;
297    // `Any` does not expose its raw value bytes directly; re-encode it
298    // then peel off the OCTET STRING tag+length to read the inner
299    // bytes via `OctetString::decode_value`.
300    let econtent_der = econtent
301        .to_der()
302        .map_err(|_| AttestationError::CertificationDeclarationMalformed)?;
303    let mut reader = SliceReader::new(&econtent_der)
304        .map_err(|_| AttestationError::CertificationDeclarationMalformed)?;
305    let header = Header::decode(&mut reader)
306        .map_err(|_| AttestationError::CertificationDeclarationMalformed)?;
307    if header.tag != DerTag::OctetString {
308        return Err(AttestationError::CertificationDeclarationMalformed);
309    }
310    let octet_string = OctetString::decode_value(&mut reader, header)
311        .map_err(|_| AttestationError::CertificationDeclarationMalformed)?;
312    let content_bytes = octet_string.as_bytes().to_vec();
313
314    // 4. Extract the signature bytes.
315    let sig = signer.signature.as_bytes();
316
317    // 5. Verify ECDSA-P256 / SHA-256 against each trusted root.
318    let mut accepted = false;
319    for key in trust.keys() {
320        if verify_ecdsa_p256_sha256(key, &content_bytes, sig).is_ok() {
321            accepted = true;
322            break;
323        }
324    }
325    if !accepted {
326        return Err(AttestationError::CertificationDeclarationSignatureInvalid);
327    }
328
329    // 6. Decode the inner CD TLV and cross-check VID / PID.
330    //
331    // Matter Core Spec §6.2.3: when the CD carries the optional
332    // `dac_origin_vendor_id` (tag 9) AND `dac_origin_product_id`
333    // (tag 10), the commissioner MUST validate the DAC/PAI subject
334    // VID/PID against THOSE origin fields, not the CD's own
335    // `vendor_id` / `product_id_array`. This supports CDs issued for a
336    // PAA/PAI scoped to a different vendor than the device's own VID
337    // (e.g. white-label / contract-manufactured products).
338    //
339    // Decision on partial presence: the spec treats `dac_origin_*` as a
340    // both-or-neither pair. We trigger the override only when BOTH are
341    // present; if exactly one is present we ignore the override and fall
342    // back to the standard fields (the safe, conservative reading — a
343    // half-specified override is not a valid §6.2.3 override).
344    let parsed = parse_inner_cd_tlv(&content_bytes)?;
345    if let (Some(origin_vid), Some(origin_pid)) =
346        (parsed.dac_origin_vendor_id, parsed.dac_origin_product_id)
347    {
348        // Override path: bind the DAC against the dac_origin_* fields.
349        if origin_vid != expected_vid {
350            return Err(AttestationError::CertificationDeclarationVidMismatch {
351                declared: origin_vid,
352                expected: expected_vid,
353            });
354        }
355        if origin_pid != expected_pid {
356            return Err(AttestationError::CertificationDeclarationPidMismatch(
357                expected_pid,
358            ));
359        }
360    } else {
361        // Standard path: bind against vendor_id / product_id_array.
362        if parsed.vendor_id != expected_vid {
363            return Err(AttestationError::CertificationDeclarationVidMismatch {
364                declared: parsed.vendor_id,
365                expected: expected_vid,
366            });
367        }
368        if !parsed.product_ids.contains(&expected_pid) {
369            return Err(AttestationError::CertificationDeclarationPidMismatch(
370                expected_pid,
371            ));
372        }
373    }
374
375    // 7. authorized_paa_list (tag 11): if the CD scopes itself to specific
376    //    PAAs, the device's anchoring PAA SKID SHALL be one of them.
377    if !paa_is_authorized(parsed.authorized_paa_list.as_deref(), device_paa_skid) {
378        return Err(AttestationError::CertificationDeclarationPaaNotAuthorized);
379    }
380
381    Ok(())
382}
383
384/// Decoded view of the inner Certification Declaration TLV — the
385/// subset the verifier cross-checks today. T30 fleshes out the parser.
386#[derive(Debug)]
387struct ParsedCd {
388    /// Vendor ID (Matter Core Spec §6.3.1 tag 1).
389    vendor_id: VendorId,
390    /// Product ID list (tag 2 — at least one element required).
391    product_ids: Vec<ProductId>,
392    /// `dac_origin_vendor_id` (Matter Core Spec §6.3.1 tag 9, optional).
393    ///
394    /// When present (together with [`Self::dac_origin_product_id`]), the
395    /// commissioner MUST validate the DAC/PAI subject VID against THIS
396    /// value rather than [`Self::vendor_id`] (Matter Core Spec §6.2.3).
397    dac_origin_vendor_id: Option<VendorId>,
398    /// `dac_origin_product_id` (Matter Core Spec §6.3.1 tag 10, optional).
399    ///
400    /// The PID counterpart to [`Self::dac_origin_vendor_id`]; see its docs.
401    dac_origin_product_id: Option<ProductId>,
402    /// `authorized_paa_list` (Matter Core Spec §6.3.1 tag 11, optional):
403    /// the `SubjectKeyIdentifiers` of the PAAs permitted to anchor this
404    /// device's DAC chain. When present (`Some`, possibly empty), the
405    /// anchoring PAA's SKID MUST be one of these values (Matter §6.2.3);
406    /// each entry is exactly 20 bytes. `None` means the CD imposes no PAA
407    /// constraint.
408    authorized_paa_list: Option<Vec<[u8; 20]>>,
409}
410
411/// Decode the inner CD TLV per Matter Core Spec §6.3.1: an anonymous
412/// outer structure with context-tagged fields including tag 1
413/// (`vendor_id`, u16), tag 2 (`product_id_array`, array of u16), the
414/// optional override fields tag 9 (`dac_origin_vendor_id`, u16) and tag
415/// 10 (`dac_origin_product_id`, u16), and the optional tag 11
416/// (`authorized_paa_list`, array of 20-byte PAA `SubjectKeyIdentifiers`).
417///
418/// All other context-tagged fields (`format_version`, `device_type_id`,
419/// `certificate_id`, `security_level`, `security_information`,
420/// `version_number`, `certification_type`) and any future-extension
421/// fields are forward-compat ignored — the verifier only needs VID + PID
422/// (and the optional `dac_origin_*` overrides + `authorized_paa_list`) for
423/// cross-checking against the DAC subject.
424#[allow(
425    clippy::too_many_lines,
426    reason = "one linear tag-dispatch loop over the CD's context-tagged fields; \
427     splitting the per-tag arms into helpers would scatter the decode logic."
428)]
429fn parse_inner_cd_tlv(tlv: &[u8]) -> Result<ParsedCd, AttestationError> {
430    use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
431
432    let mut reader = TlvReader::new(tlv);
433    match reader
434        .next()
435        .map_err(|_| AttestationError::CertificationDeclarationTlvMalformed)?
436    {
437        Some(Element::ContainerStart {
438            tag: Tag::Anonymous,
439            kind: ContainerKind::Structure,
440        }) => {}
441        _ => return Err(AttestationError::CertificationDeclarationTlvMalformed),
442    }
443
444    let mut vid: Option<VendorId> = None;
445    let mut pids: Vec<ProductId> = Vec::new();
446    let mut origin_vendor: Option<VendorId> = None;
447    let mut origin_product: Option<ProductId> = None;
448    let mut authorized_paa_list: Option<Vec<[u8; 20]>> = None;
449
450    loop {
451        match reader
452            .next()
453            .map_err(|_| AttestationError::CertificationDeclarationTlvMalformed)?
454        {
455            None => return Err(AttestationError::CertificationDeclarationTlvMalformed),
456            Some(Element::ContainerEnd) => break,
457            Some(Element::Scalar {
458                tag: Tag::Context(1),
459                value: Value::Uint(v),
460            }) => {
461                if vid.is_some() {
462                    return Err(AttestationError::CertificationDeclarationTlvMalformed);
463                }
464                let v16 = u16::try_from(v)
465                    .map_err(|_| AttestationError::CertificationDeclarationTlvMalformed)?;
466                vid = Some(VendorId::new(v16));
467            }
468            Some(Element::ContainerStart {
469                tag: Tag::Context(2),
470                kind: ContainerKind::Array,
471            }) => {
472                if !pids.is_empty() {
473                    return Err(AttestationError::CertificationDeclarationTlvMalformed);
474                }
475                loop {
476                    match reader
477                        .next()
478                        .map_err(|_| AttestationError::CertificationDeclarationTlvMalformed)?
479                    {
480                        None => return Err(AttestationError::CertificationDeclarationTlvMalformed),
481                        Some(Element::ContainerEnd) => break,
482                        Some(Element::Scalar {
483                            tag: Tag::Anonymous,
484                            value: Value::Uint(p),
485                        }) => {
486                            let p16 = u16::try_from(p).map_err(|_| {
487                                AttestationError::CertificationDeclarationTlvMalformed
488                            })?;
489                            pids.push(ProductId::new(p16));
490                        }
491                        // Inside the array, unknown shapes are a structural error
492                        // (the spec says product IDs are u16). But be lenient on
493                        // tagged-not-anonymous in case future M6.x extensions land.
494                        Some(_) => {}
495                    }
496                }
497            }
498            // dac_origin_vendor_id (tag 9) — optional override; see §6.2.3.
499            Some(Element::Scalar {
500                tag: Tag::Context(9),
501                value: Value::Uint(v),
502            }) => {
503                if origin_vendor.is_some() {
504                    return Err(AttestationError::CertificationDeclarationTlvMalformed);
505                }
506                let v16 = u16::try_from(v)
507                    .map_err(|_| AttestationError::CertificationDeclarationTlvMalformed)?;
508                origin_vendor = Some(VendorId::new(v16));
509            }
510            // dac_origin_product_id (tag 10) — optional override; see §6.2.3.
511            Some(Element::Scalar {
512                tag: Tag::Context(10),
513                value: Value::Uint(p),
514            }) => {
515                if origin_product.is_some() {
516                    return Err(AttestationError::CertificationDeclarationTlvMalformed);
517                }
518                let p16 = u16::try_from(p)
519                    .map_err(|_| AttestationError::CertificationDeclarationTlvMalformed)?;
520                origin_product = Some(ProductId::new(p16));
521            }
522            // authorized_paa_list (tag 11) — optional; array of 20-byte PAA
523            // SubjectKeyIdentifiers (chip `CertificationDeclaration.cpp:285`).
524            Some(Element::ContainerStart {
525                tag: Tag::Context(11),
526                kind: ContainerKind::Array,
527            }) => {
528                if authorized_paa_list.is_some() {
529                    return Err(AttestationError::CertificationDeclarationTlvMalformed);
530                }
531                let mut list: Vec<[u8; 20]> = Vec::new();
532                loop {
533                    match reader
534                        .next()
535                        .map_err(|_| AttestationError::CertificationDeclarationTlvMalformed)?
536                    {
537                        None => return Err(AttestationError::CertificationDeclarationTlvMalformed),
538                        Some(Element::ContainerEnd) => break,
539                        Some(Element::Scalar {
540                            tag: Tag::Anonymous,
541                            value: Value::Bytes(b),
542                        }) => {
543                            // chip requires each entry to be exactly a 20-byte
544                            // key identifier; anything else is a structural error.
545                            let skid: [u8; 20] = b.as_slice().try_into().map_err(|_| {
546                                AttestationError::CertificationDeclarationTlvMalformed
547                            })?;
548                            list.push(skid);
549                        }
550                        Some(_) => {
551                            return Err(AttestationError::CertificationDeclarationTlvMalformed)
552                        }
553                    }
554                }
555                authorized_paa_list = Some(list);
556            }
557            // Forward-compat: ignore other context-tagged scalars / containers.
558            Some(_) => {}
559        }
560    }
561
562    let vendor_id = vid.ok_or(AttestationError::CertificationDeclarationTlvMalformed)?;
563    if pids.is_empty() {
564        // product_id_array required to be non-empty per spec §6.3.1.
565        return Err(AttestationError::CertificationDeclarationTlvMalformed);
566    }
567    Ok(ParsedCd {
568        vendor_id,
569        product_ids: pids,
570        dac_origin_vendor_id: origin_vendor,
571        dac_origin_product_id: origin_product,
572        authorized_paa_list,
573    })
574}
575
576/// Whether a device's anchoring-PAA `SubjectKeyIdentifier` is authorized by
577/// a CD's `authorized_paa_list` (Matter §6.2.3, chip
578/// `DefaultDeviceAttestationVerifier.cpp:738`).
579///
580/// - `None` list → the CD imposes no PAA constraint → authorized.
581/// - `Some(list)` → `device_paa_skid` must be present and byte-equal to
582///   one of the 20-byte entries. A missing device SKID, or a SKID not in
583///   the list, is unauthorized.
584fn paa_is_authorized(list: Option<&[[u8; 20]]>, device_paa_skid: Option<&[u8]>) -> bool {
585    match list {
586        None => true,
587        Some(entries) => match device_paa_skid {
588            Some(skid) => entries.iter().any(|e| e.as_slice() == skid),
589            None => false,
590        },
591    }
592}
593
594/// Parse a PEM-encoded `SubjectPublicKeyInfo` for a P-256 public key
595/// and return the SEC1 uncompressed point (65 bytes:
596/// `0x04 || X || Y`) suitable for `ring`'s
597/// `UnparsedPublicKey<ECDSA_P256_SHA256_FIXED>`.
598///
599/// Strips the `-----BEGIN PUBLIC KEY-----` / `-----END PUBLIC KEY-----`
600/// armor, base64-decodes the body, and slices the trailing 65 bytes of
601/// the DER (the SEC1 uncompressed point inside the SPKI's `BIT
602/// STRING`). The point's `0x04` marker byte is checked here; ring's
603/// `UnparsedPublicKey` rejects any malformed point at signature-verify
604/// time as a second line of defense.
605/// Extract the 65-byte SEC1-uncompressed P-256 subject public key from
606/// an X.509 certificate DER, or `None` if it does not parse as X.509 or
607/// does not carry a P-256 point. Used to ingest CD signing roots that
608/// are published as certificates (CSA DCL / `connectedhomeip`
609/// `credentials/`) rather than as bare `SubjectPublicKeyInfo` PEMs.
610fn cert_der_public_key(der: &[u8]) -> Option<Vec<u8>> {
611    use x509_parser::prelude::{FromDer, X509Certificate};
612
613    let (_, cert) = X509Certificate::from_der(der).ok()?;
614    let pk = cert.public_key().subject_public_key.data.as_ref().to_vec();
615    (pk.len() == 65 && pk[0] == 0x04).then_some(pk)
616}
617
618fn parse_pem_public_key(pem: &[u8]) -> Result<Vec<u8>, AttestationError> {
619    use base64::Engine;
620
621    const HEADER: &str = "-----BEGIN PUBLIC KEY-----";
622    const FOOTER: &str = "-----END PUBLIC KEY-----";
623
624    let pem_str = std::str::from_utf8(pem)
625        .map_err(|_| AttestationError::CertificationDeclarationMalformed)?;
626
627    let header_start = pem_str
628        .find(HEADER)
629        .ok_or(AttestationError::CertificationDeclarationMalformed)?;
630    let body_start = header_start + HEADER.len();
631    let footer_start = pem_str
632        .find(FOOTER)
633        .ok_or(AttestationError::CertificationDeclarationMalformed)?;
634    if footer_start <= body_start {
635        return Err(AttestationError::CertificationDeclarationMalformed);
636    }
637
638    // Strip all whitespace from the base64 body.
639    let body: String = pem_str[body_start..footer_start]
640        .chars()
641        .filter(|c| !c.is_whitespace())
642        .collect();
643
644    let der = base64::engine::general_purpose::STANDARD
645        .decode(body.as_bytes())
646        .map_err(|_| AttestationError::CertificationDeclarationMalformed)?;
647
648    // Extract the SEC1 uncompressed point from the SubjectPublicKeyInfo.
649    // The structure for a P-256 SPKI is exactly 91 bytes:
650    //   30 59                        SEQUENCE (89)
651    //   30 13                          SEQUENCE (19)
652    //     06 07 2A 86 48 CE 3D 02 01   OID ecPublicKey
653    //     06 08 2A 86 48 CE 3D 03 01 07 OID prime256v1
654    //   03 42 00                       BIT STRING (66 bytes, 0 unused bits)
655    //   04 XX...XX                     SEC1 uncompressed point (65 bytes)
656    //                                  starting with 0x04
657    //
658    // The SEC1 point is the last 65 bytes. We validate the marker byte
659    // and let `ring::UnparsedPublicKey::new` reject malformed bytes
660    // later if the prefix is corrupt.
661    if der.len() < 65 {
662        return Err(AttestationError::CertificationDeclarationMalformed);
663    }
664    let point = &der[der.len() - 65..];
665    if point[0] != 0x04 {
666        return Err(AttestationError::CertificationDeclarationMalformed);
667    }
668    Ok(point.to_vec())
669}
670
671/// Verify an ECDSA-P256 / SHA-256 signature against a SEC1-uncompressed
672/// P-256 public key.
673///
674/// CMS `SignerInfo.signature` carries ECDSA signatures as a DER
675/// `ECDSA-Sig-Value` (SEQUENCE of r, s) — confirmed against a real
676/// CSA-signed CD (Tapo P110M, M6.6.5 validation), and matching chip's
677/// `CMS_Sign`. A raw fixed-form (r||s, exactly 64 bytes) signature is
678/// also accepted for compatibility with the historical local test
679/// fixtures generated by `xtask capture-cd`.
680///
681/// Maps any verification failure to
682/// [`AttestationError::CertificationDeclarationSignatureInvalid`] —
683/// the caller (the per-root loop in
684/// [`verify_certification_declaration`]) only cares whether *some*
685/// trusted root accepted the signature, not which one rejected it.
686fn verify_ecdsa_p256_sha256(
687    public_key: &[u8],
688    msg: &[u8],
689    sig: &[u8],
690) -> Result<(), AttestationError> {
691    use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_ASN1, ECDSA_P256_SHA256_FIXED};
692    let asn1 = UnparsedPublicKey::new(&ECDSA_P256_SHA256_ASN1, public_key);
693    if asn1.verify(msg, sig).is_ok() {
694        return Ok(());
695    }
696    if sig.len() == 64 {
697        let fixed = UnparsedPublicKey::new(&ECDSA_P256_SHA256_FIXED, public_key);
698        if fixed.verify(msg, sig).is_ok() {
699            return Ok(());
700        }
701    }
702    Err(AttestationError::CertificationDeclarationSignatureInvalid)
703}
704
705#[cfg(test)]
706mod tests {
707    // The CD test helpers pair `dac_origin_vid`/`dac_origin_pid` and
708    // `origin_vid`/`origin_pid` (and VendorId/ProductId locals) by design —
709    // the near-identical names mirror the spec field pairs. Same carve-out
710    // as `tests/support/mod.rs`.
711    #![allow(clippy::similar_names)]
712
713    use super::*;
714
715    #[test]
716    #[allow(clippy::unwrap_used, clippy::expect_used)] // Test-code carve-out: see CLAUDE.md.
717    fn verify_accepts_der_encoded_ecdsa_signature() {
718        // CMS `SignerInfo.signature` carries ECDSA signatures as a DER
719        // `ECDSA-Sig-Value` (SEQUENCE of r, s) — confirmed on a real
720        // CSA-signed CD (Tapo P110M, M6.6.5 validation: 70-byte `0x30 44 …`).
721        // chip's `CMS_Sign` emits the same (`ConvertECDSASignatureRawToDER`).
722        use ring::rand::SystemRandom;
723        use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
724
725        let rng = SystemRandom::new();
726        let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
727        let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
728            .unwrap();
729        let msg = b"certification declaration content";
730        let sig = kp.sign(&rng, msg).unwrap(); // DER-encoded ECDSA-Sig-Value
731        let pk = kp.public_key().as_ref();
732
733        verify_ecdsa_p256_sha256(pk, msg, sig.as_ref())
734            .expect("DER-encoded CMS signature must verify");
735    }
736
737    #[test]
738    fn with_example_device_roots_loads_bundled_roots() {
739        // ATT-3: three roots — synthetic (loopback), chip test authority,
740        // CSA production key 001 (verifies the ESP32-C6). All three DERs
741        // parse, so none are silently skipped.
742        let trust = CdSigningRoots::with_example_device_roots();
743        assert_eq!(trust.len(), 3);
744        assert!(!trust.is_empty());
745    }
746
747    #[test]
748    #[allow(clippy::expect_used)] // Test-code carve-out: see CLAUDE.md.
749    fn parse_pem_public_key_extracts_65_byte_sec1_point() {
750        const PEM: &[u8] = include_bytes!("./csa_cd_signing_roots/csa-test-cd-signing-root.pem");
751        let key = parse_pem_public_key(PEM).expect("happy path parses");
752        assert_eq!(key.len(), 65, "SEC1 uncompressed P-256 point");
753        assert_eq!(key[0], 0x04, "uncompressed-point marker byte");
754    }
755
756    #[test]
757    #[allow(clippy::expect_used)] // Test-code carve-out: see CLAUDE.md.
758    fn parse_pem_public_key_rejects_garbage() {
759        let err = parse_pem_public_key(b"not a PEM").expect_err("garbage rejected");
760        assert!(matches!(
761            err,
762            AttestationError::CertificationDeclarationMalformed
763        ));
764    }
765
766    #[test]
767    #[allow(clippy::unwrap_used)] // Test-code carve-out: see CLAUDE.md.
768    fn from_pem_empty_input_yields_empty_trust_store() {
769        let trust = CdSigningRoots::from_pem(&[]).unwrap();
770        assert!(trust.is_empty());
771        assert_eq!(trust.len(), 0);
772    }
773
774    #[test]
775    #[allow(clippy::unwrap_used, clippy::expect_used)] // Test-code carve-out: see CLAUDE.md.
776    fn from_cert_der_extracts_p256_pubkey_from_x509_cert() {
777        // Real-world CD signing roots (CSA DCL / connectedhomeip
778        // `credentials/production/cd-certs/`) are X.509 certificates, not bare
779        // SubjectPublicKeyInfo PEMs. `from_cert_der` must extract the cert's
780        // P-256 subject public key. We synthesise a self-signed P-256 cert with
781        // a known key and assert the extracted SEC1 point matches it byte-for-byte.
782        use matter_cert::test_support::{build_x509_der, TestCertFields};
783        use matter_cert::{
784            DistinguishedName, DnAttribute, Extensions, MatterTime, PublicKey, Signature,
785        };
786        use ring::rand::SystemRandom;
787        use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
788
789        let rng = SystemRandom::new();
790        let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
791        let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
792            .unwrap();
793        let expected = kp.public_key().as_ref().to_vec(); // 65-byte SEC1 uncompressed
794        let pk = PublicKey::from_slice(&expected).unwrap();
795
796        let dn = DistinguishedName::new(vec![DnAttribute::CommonName(
797            "Test CD Signing Key (synthetic)".into(),
798        )]);
799        let der = build_x509_der(
800            TestCertFields {
801                serial: vec![0x01],
802                issuer: dn.clone(),
803                not_before: MatterTime::from_unix_secs(1_700_000_000),
804                not_after: MatterTime::NO_EXPIRY,
805                subject: dn,
806                public_key: pk,
807                extensions: Extensions::default(),
808                signature: Signature::new([0u8; 64]),
809            },
810            pkcs8.as_ref(), // self-signed
811        )
812        .expect("synthetic CD signing cert builds");
813
814        let trust = CdSigningRoots::from_cert_der(&[&der]).expect("cert parses");
815        assert_eq!(trust.len(), 1);
816        assert_eq!(
817            trust.public_keys[0], expected,
818            "extracted SEC1 public key must match the cert's subject key"
819        );
820    }
821
822    #[test]
823    #[allow(clippy::expect_used)] // Test-code carve-out: see CLAUDE.md.
824    fn from_cert_der_rejects_non_certificate_bytes() {
825        let err = CdSigningRoots::from_cert_der(&[b"not a certificate"])
826            .expect_err("garbage DER rejected");
827        assert!(matches!(
828            err,
829            AttestationError::CertificationDeclarationMalformed
830        ));
831    }
832
833    #[test]
834    fn parse_inner_cd_tlv_extracts_vendor_id_and_pid_list() {
835        // Test-code carve-out: see CLAUDE.md.
836        #![allow(clippy::unwrap_used, clippy::expect_used)]
837        use matter_codec::{Tag, TlvWriter};
838        let mut buf = Vec::new();
839        let mut w = TlvWriter::new(&mut buf);
840        w.start_structure(Tag::Anonymous).unwrap();
841        w.put_uint(Tag::Context(0), 1).unwrap(); // format_version
842        w.put_uint(Tag::Context(1), 0xFFF1).unwrap(); // vendor_id
843        w.start_array(Tag::Context(2)).unwrap();
844        w.put_uint(Tag::Anonymous, 0x8001).unwrap();
845        w.put_uint(Tag::Anonymous, 0x8002).unwrap();
846        w.end_container().unwrap(); // close array
847        w.end_container().unwrap(); // close struct
848
849        let parsed = parse_inner_cd_tlv(&buf).expect("happy path decodes");
850        assert_eq!(parsed.vendor_id, VendorId::new(0xFFF1));
851        assert_eq!(
852            parsed.product_ids,
853            vec![ProductId::new(0x8001), ProductId::new(0x8002)]
854        );
855        assert!(
856            parsed.authorized_paa_list.is_none(),
857            "no tag 11 → no PAA constraint"
858        );
859    }
860
861    /// Build a minimal valid CD inner TLV, optionally carrying an
862    /// `authorized_paa_list` (tag 11) of the given SKID entries.
863    #[cfg(test)]
864    fn cd_tlv_with_paa_list(entries: Option<&[&[u8]]>) -> Vec<u8> {
865        #![allow(clippy::unwrap_used)]
866        use matter_codec::{Tag, TlvWriter};
867        let mut buf = Vec::new();
868        let mut w = TlvWriter::new(&mut buf);
869        w.start_structure(Tag::Anonymous).unwrap();
870        w.put_uint(Tag::Context(1), 0xFFF1).unwrap();
871        w.start_array(Tag::Context(2)).unwrap();
872        w.put_uint(Tag::Anonymous, 0x8000).unwrap();
873        w.end_container().unwrap();
874        if let Some(entries) = entries {
875            w.start_array(Tag::Context(11)).unwrap();
876            for e in entries {
877                w.put_bytes(Tag::Anonymous, e).unwrap();
878            }
879            w.end_container().unwrap();
880        }
881        w.end_container().unwrap();
882        buf
883    }
884
885    #[test]
886    #[allow(clippy::expect_used)] // Test-code carve-out: see CLAUDE.md.
887    fn parse_inner_cd_tlv_extracts_authorized_paa_list() {
888        let a = [0xAAu8; 20];
889        let b = [0xBBu8; 20];
890        let buf = cd_tlv_with_paa_list(Some(&[&a, &b]));
891        let parsed = parse_inner_cd_tlv(&buf).expect("tag 11 decodes");
892        assert_eq!(parsed.authorized_paa_list, Some(vec![a, b]));
893    }
894
895    #[test]
896    fn parse_inner_cd_tlv_rejects_wrong_length_paa_entry() {
897        // chip requires each authorized_paa_list entry to be exactly 20
898        // bytes; a 19-byte entry is a structural error.
899        let short = [0xAAu8; 19];
900        let buf = cd_tlv_with_paa_list(Some(&[&short]));
901        assert!(matches!(
902            parse_inner_cd_tlv(&buf),
903            Err(AttestationError::CertificationDeclarationTlvMalformed)
904        ));
905    }
906
907    #[test]
908    fn paa_is_authorized_matrix() {
909        let a = [0xAAu8; 20];
910        let b = [0xBBu8; 20];
911        // No list → always authorized (CD imposes no constraint).
912        assert!(paa_is_authorized(None, Some(&a)));
913        assert!(paa_is_authorized(None, None));
914        // List present, SKID in it → authorized.
915        assert!(paa_is_authorized(Some(&[a, b]), Some(&a)));
916        // List present, SKID not in it → rejected.
917        let c = [0xCCu8; 20];
918        assert!(!paa_is_authorized(Some(&[a, b]), Some(&c)));
919        // List present, no device SKID → rejected.
920        assert!(!paa_is_authorized(Some(&[a, b]), None));
921        // Empty list → nothing authorized.
922        assert!(!paa_is_authorized(Some(&[]), Some(&a)));
923    }
924
925    #[test]
926    fn parse_inner_cd_tlv_ignores_forward_compat_fields() {
927        // Test-code carve-out: see CLAUDE.md.
928        #![allow(clippy::unwrap_used, clippy::expect_used)]
929        use matter_codec::{Tag, TlvWriter};
930        // Hand-roll a CD with tag 0 (format_version), tag 1 (vid), tag 2 (pids),
931        // tag 3 (device_type_id), tag 4 (certificate_id utf8), tag 5..8 (security_*,
932        // version_number, certification_type), AND a fake tag 99 (future field).
933        let mut buf = Vec::new();
934        let mut w = TlvWriter::new(&mut buf);
935        w.start_structure(Tag::Anonymous).unwrap();
936        w.put_uint(Tag::Context(0), 1).unwrap();
937        w.put_uint(Tag::Context(1), 0xFFF1).unwrap();
938        w.start_array(Tag::Context(2)).unwrap();
939        w.put_uint(Tag::Anonymous, 0x8001).unwrap();
940        w.end_container().unwrap();
941        w.put_uint(Tag::Context(3), 0x0100).unwrap();
942        w.put_utf8(Tag::Context(4), "CSA-ID").unwrap();
943        w.put_uint(Tag::Context(5), 0).unwrap();
944        w.put_uint(Tag::Context(6), 0).unwrap();
945        w.put_uint(Tag::Context(7), 1).unwrap();
946        w.put_uint(Tag::Context(8), 0).unwrap();
947        w.put_uint(Tag::Context(99), 0xDEAD).unwrap(); // unknown future field
948        w.end_container().unwrap();
949
950        let parsed = parse_inner_cd_tlv(&buf).expect("forward-compat decode");
951        assert_eq!(parsed.vendor_id, VendorId::new(0xFFF1));
952        assert_eq!(parsed.product_ids, vec![ProductId::new(0x8001)]);
953    }
954
955    #[test]
956    fn parse_inner_cd_tlv_rejects_missing_vid() {
957        // Test-code carve-out: see CLAUDE.md.
958        #![allow(clippy::unwrap_used, clippy::expect_used)]
959        use matter_codec::{Tag, TlvWriter};
960        let mut buf = Vec::new();
961        let mut w = TlvWriter::new(&mut buf);
962        w.start_structure(Tag::Anonymous).unwrap();
963        // No tag 1 (vendor_id).
964        w.start_array(Tag::Context(2)).unwrap();
965        w.put_uint(Tag::Anonymous, 0x8001).unwrap();
966        w.end_container().unwrap();
967        w.end_container().unwrap();
968
969        let err = parse_inner_cd_tlv(&buf).expect_err("missing vid rejected");
970        assert!(matches!(
971            err,
972            AttestationError::CertificationDeclarationTlvMalformed
973        ));
974    }
975
976    #[test]
977    fn parse_inner_cd_tlv_rejects_garbage() {
978        // Test-code carve-out: see CLAUDE.md.
979        #![allow(clippy::unwrap_used, clippy::expect_used)]
980        let err = parse_inner_cd_tlv(&[0xFF]).expect_err("garbage rejected");
981        assert!(matches!(
982            err,
983            AttestationError::CertificationDeclarationTlvMalformed
984        ));
985    }
986
987    #[test]
988    fn parse_inner_cd_tlv_captures_dac_origin_fields() {
989        // Test-code carve-out: see CLAUDE.md.
990        #![allow(clippy::unwrap_used, clippy::expect_used)]
991        let tlv = build_inner_cd_tlv(0xFFF1, 0x8001, Some(0x1234), Some(0x5678), &[0x8001]);
992        let parsed = parse_inner_cd_tlv(&tlv).expect("decodes with dac_origin");
993        assert_eq!(parsed.vendor_id, VendorId::new(0xFFF1));
994        assert_eq!(parsed.product_ids, vec![ProductId::new(0x8001)]);
995        assert_eq!(parsed.dac_origin_vendor_id, Some(VendorId::new(0x1234)));
996        assert_eq!(parsed.dac_origin_product_id, Some(ProductId::new(0x5678)));
997    }
998
999    // ── Fix B — CD dac_origin override binding (Matter §6.2.3) ──────────────
1000    //
1001    // These tests sign a synthetic CD with the bundled CSA-test signing key
1002    // (which `CdSigningRoots::with_example_device_roots()` trusts) and run it
1003    // through the public `verify_certification_declaration`, exercising the
1004    // dac_origin override path end-to-end:
1005    //
1006    //   - CD with dac_origin set + DAC matching the ORIGIN VID/PID (but NOT
1007    //     the CD's own vendor_id) → accepted.
1008    //   - CD with dac_origin set + DAC matching neither → rejected.
1009    //   - CD WITHOUT dac_origin → unchanged: compares vendor_id /
1010    //     product_id_array.
1011
1012    /// PKCS#8 private key for the bundled CSA-test CD signing root. The
1013    /// matching public key is bundled in
1014    /// `csa_cd_signing_roots/csa-test-cd-signing-root.pem` and trusted by
1015    /// `CdSigningRoots::with_example_device_roots()`.
1016    const CSA_TEST_CD_SIGNING_KEY_PKCS8: &[u8] = include_bytes!(
1017        "../../../../../test-vectors/commissioning/cd/csa-test-cd-signing-root.pkcs8.der"
1018    );
1019
1020    /// Build the inner CD TLV per Matter Core Spec §6.3.1, optionally
1021    /// including the `dac_origin_*` override fields (tags 9 / 10).
1022    ///
1023    /// Mirrors `xtask/src/capture_cd.rs::build_inner_cd_tlv` but adds the
1024    /// optional tags 9/10 so the Fix B override path can be exercised.
1025    #[allow(clippy::unwrap_used)] // Test-code carve-out: see CLAUDE.md.
1026    fn build_inner_cd_tlv(
1027        vendor_id: u16,
1028        product_id: u16,
1029        dac_origin_vid: Option<u16>,
1030        dac_origin_pid: Option<u16>,
1031        product_id_array: &[u16],
1032    ) -> Vec<u8> {
1033        use matter_codec::{Tag, TlvWriter};
1034        let mut buf = Vec::new();
1035        {
1036            let mut w = TlvWriter::new(&mut buf);
1037            w.start_structure(Tag::Anonymous).unwrap();
1038            w.put_uint(Tag::Context(0), 1).unwrap(); // format_version
1039            w.put_uint(Tag::Context(1), u64::from(vendor_id)).unwrap(); // vendor_id
1040            w.start_array(Tag::Context(2)).unwrap(); // product_id_array
1041            let pids: Vec<u16> = if product_id_array.is_empty() {
1042                vec![product_id]
1043            } else {
1044                product_id_array.to_vec()
1045            };
1046            for p in pids {
1047                w.put_uint(Tag::Anonymous, u64::from(p)).unwrap();
1048            }
1049            w.end_container().unwrap();
1050            w.put_uint(Tag::Context(3), 0x0100).unwrap(); // device_type_id
1051            w.put_utf8(Tag::Context(4), "CSA00000000000000").unwrap(); // certificate_id
1052            w.put_uint(Tag::Context(5), 0).unwrap(); // security_level
1053            w.put_uint(Tag::Context(6), 0).unwrap(); // security_information
1054            w.put_uint(Tag::Context(7), 1).unwrap(); // version_number
1055            w.put_uint(Tag::Context(8), 0).unwrap(); // certification_type
1056            if let Some(v) = dac_origin_vid {
1057                w.put_uint(Tag::Context(9), u64::from(v)).unwrap();
1058            }
1059            if let Some(p) = dac_origin_pid {
1060                w.put_uint(Tag::Context(10), u64::from(p)).unwrap();
1061            }
1062            w.end_container().unwrap();
1063        }
1064        buf
1065    }
1066
1067    /// Sign `content` (the inner CD TLV) into a CMS `SignedData`
1068    /// `ContentInfo` DER blob with the bundled CSA-test key, using the
1069    /// no-`signedAttrs` shape the verifier expects. Mirrors
1070    /// `xtask/src/capture_cd.rs::sign_into_cms`.
1071    #[allow(clippy::unwrap_used, clippy::expect_used)] // Test-code carve-out: see CLAUDE.md.
1072    fn sign_into_cms(content: &[u8]) -> Vec<u8> {
1073        use cms::cert::IssuerAndSerialNumber;
1074        use cms::content_info::{CmsVersion, ContentInfo};
1075        use cms::signed_data::{
1076            EncapsulatedContentInfo, SignedData, SignerIdentifier, SignerInfo, SignerInfos,
1077        };
1078        use const_oid::ObjectIdentifier;
1079        use der::asn1::{Any, AnyRef, OctetString, SetOfVec};
1080        use der::{Encode, Tag as DerTag};
1081        use ring::rand::SystemRandom;
1082        use ring::signature::{EcdsaKeyPair, ECDSA_P256_SHA256_FIXED_SIGNING};
1083        use spki::AlgorithmIdentifierOwned;
1084        use x509_cert::name::RdnSequence;
1085        use x509_cert::serial_number::SerialNumber;
1086
1087        const ID_DATA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.7.1");
1088        const ID_SIGNED_DATA: ObjectIdentifier =
1089            ObjectIdentifier::new_unwrap("1.2.840.113549.1.7.2");
1090        const ID_SHA_256: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.1");
1091        const ECDSA_WITH_SHA_256: ObjectIdentifier =
1092            ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
1093
1094        let rng = SystemRandom::new();
1095        let key = EcdsaKeyPair::from_pkcs8(
1096            &ECDSA_P256_SHA256_FIXED_SIGNING,
1097            CSA_TEST_CD_SIGNING_KEY_PKCS8,
1098            &rng,
1099        )
1100        .expect("bundled CSA-test CD signing key loads");
1101        let signature = key.sign(&rng, content).expect("sign eContent");
1102
1103        let econtent_any =
1104            Any::new(DerTag::OctetString, content.to_vec()).expect("Any(OctetString)");
1105        let encap = EncapsulatedContentInfo {
1106            econtent_type: ID_DATA,
1107            econtent: Some(econtent_any),
1108        };
1109        let sha256 = AlgorithmIdentifierOwned {
1110            oid: ID_SHA_256,
1111            parameters: None,
1112        };
1113        let digest_algorithms =
1114            SetOfVec::try_from(vec![sha256.clone()]).expect("digest_algorithms");
1115        let serial = SerialNumber::new(&[0x01]).expect("serial");
1116        let sid = SignerIdentifier::IssuerAndSerialNumber(IssuerAndSerialNumber {
1117            issuer: RdnSequence::default(),
1118            serial_number: serial,
1119        });
1120        let signature_octets =
1121            OctetString::new(signature.as_ref().to_vec()).expect("signature octets");
1122        let signer_info = SignerInfo {
1123            version: CmsVersion::V1,
1124            sid,
1125            digest_alg: sha256,
1126            signed_attrs: None,
1127            signature_algorithm: AlgorithmIdentifierOwned {
1128                oid: ECDSA_WITH_SHA_256,
1129                parameters: None,
1130            },
1131            signature: signature_octets,
1132            unsigned_attrs: None,
1133        };
1134        let signer_infos = SignerInfos(SetOfVec::try_from(vec![signer_info]).expect("signer set"));
1135        let signed_data = SignedData {
1136            version: CmsVersion::V1,
1137            digest_algorithms,
1138            encap_content_info: encap,
1139            certificates: None,
1140            crls: None,
1141            signer_infos,
1142        };
1143        let signed_data_der = signed_data.to_der().expect("SignedData der");
1144        let signed_data_any =
1145            Any::from(AnyRef::try_from(signed_data_der.as_slice()).expect("AnyRef"));
1146        let content_info = ContentInfo {
1147            content_type: ID_SIGNED_DATA,
1148            content: signed_data_any,
1149        };
1150        content_info.to_der().expect("ContentInfo der")
1151    }
1152
1153    #[test]
1154    #[allow(clippy::expect_used)] // Test-code carve-out: see CLAUDE.md.
1155    fn dac_origin_present_dac_matches_origin_is_accepted() {
1156        // CD's own vendor_id/product_id_array are 0xFFF1 / 0x8001, but
1157        // dac_origin says the DAC is scoped to 0x1234 / 0x5678. A DAC at
1158        // 0x1234 / 0x5678 must be accepted (bound to the origin fields),
1159        // even though it does NOT match the CD's own vendor_id.
1160        let tlv = build_inner_cd_tlv(0xFFF1, 0x8001, Some(0x1234), Some(0x5678), &[0x8001]);
1161        let cd = sign_into_cms(&tlv);
1162        let trust = CdSigningRoots::with_example_device_roots();
1163
1164        verify_certification_declaration(
1165            &cd,
1166            VendorId::new(0x1234),
1167            ProductId::new(0x5678),
1168            &trust,
1169        )
1170        .expect("DAC matching dac_origin VID/PID must be accepted");
1171    }
1172
1173    #[test]
1174    #[allow(clippy::expect_used)] // Test-code carve-out: see CLAUDE.md.
1175    fn dac_origin_present_dac_matches_neither_is_rejected() {
1176        // dac_origin = 0x1234/0x5678. A DAC at 0xFFF1/0x8001 (the CD's own
1177        // vendor_id/pid) must be REJECTED, because the override path binds
1178        // against dac_origin, not the CD's own fields.
1179        let tlv = build_inner_cd_tlv(0xFFF1, 0x8001, Some(0x1234), Some(0x5678), &[0x8001]);
1180        let cd = sign_into_cms(&tlv);
1181        let trust = CdSigningRoots::with_example_device_roots();
1182
1183        let err = verify_certification_declaration(
1184            &cd,
1185            VendorId::new(0xFFF1),
1186            ProductId::new(0x8001),
1187            &trust,
1188        )
1189        .expect_err("DAC not matching dac_origin must be rejected");
1190        assert!(
1191            matches!(
1192                err,
1193                AttestationError::CertificationDeclarationVidMismatch {
1194                    declared,
1195                    expected,
1196                } if declared == VendorId::new(0x1234) && expected == VendorId::new(0xFFF1)
1197            ),
1198            "expected VID mismatch against the dac_origin VID, got {err:?}"
1199        );
1200    }
1201
1202    #[test]
1203    #[allow(clippy::expect_used)] // Test-code carve-out: see CLAUDE.md.
1204    fn no_dac_origin_uses_vendor_id_and_pid_array() {
1205        // Without dac_origin, the standard path compares against the CD's
1206        // own vendor_id / product_id_array (unchanged behaviour).
1207        let tlv = build_inner_cd_tlv(0xFFF1, 0x8001, None, None, &[0x8001, 0x8002]);
1208        let cd = sign_into_cms(&tlv);
1209        let trust = CdSigningRoots::with_example_device_roots();
1210
1211        // A PID present in the array is accepted.
1212        verify_certification_declaration(
1213            &cd,
1214            VendorId::new(0xFFF1),
1215            ProductId::new(0x8002),
1216            &trust,
1217        )
1218        .expect("DAC matching vendor_id and a member of product_id_array accepted");
1219
1220        // A PID NOT in the array is rejected (still the standard path).
1221        let err = verify_certification_declaration(
1222            &cd,
1223            VendorId::new(0xFFF1),
1224            ProductId::new(0x9999),
1225            &trust,
1226        )
1227        .expect_err("PID outside product_id_array rejected");
1228        assert!(
1229            matches!(err, AttestationError::CertificationDeclarationPidMismatch(p)
1230                if p == ProductId::new(0x9999)),
1231            "expected PID mismatch, got {err:?}"
1232        );
1233    }
1234}