Skip to main content

rs_matter/attest/
cd.rs

1/*
2 *
3 *    Copyright (c) 2026 Project CHIP Authors
4 *
5 *    Licensed under the Apache License, Version 2.0 (the "License");
6 *    you may not use this file except in compliance with the License.
7 *    You may obtain a copy of the License at
8 *
9 *        http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *    Unless required by applicable law or agreed to in writing, software
12 *    distributed under the License is distributed on an "AS IS" BASIS,
13 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *    See the License for the specific language governing permissions and
15 *    limitations under the License.
16 */
17
18//! Certification Declaration (CD) parsing, verification, and validation.
19//!
20//! A Certification Declaration is a CMS (RFC 5652) SignedData structure containing
21//! a TLV-encoded payload that attests to a device's certification status.
22//! It is issued by the CSA (Connectivity Standards Alliance) and signed with
23//! one of the well-known CD signing keys.
24//!
25//! This module implements:
26//! - CMS SignedData envelope parsing (extracting signer KID, CD content, signature)
27//! - DER-encoded ECDSA signature to raw (r || s) conversion
28//! - CD TLV payload decoding into [`CertificationElements`]
29//! - Signature verification using the [`Crypto`] trait
30//! - CD content validation against device identity (Matter Spec)
31//!
32//! Reference: connectedhomeip `src/credentials/CertificationDeclaration.cpp`
33
34use crate::attest::cd_keys::{self, KEY_IDENTIFIER_LEN};
35use crate::cert::der_utils::ecdsa_der_to_raw;
36use crate::cert::x509::AlgorithmIdentifier;
37use crate::crypto::{CanonPkcPublicKeyRef, CanonPkcSignatureRef, Crypto, PublicKey};
38use crate::error::{Error, ErrorCode};
39use crate::tlv::{TLVElement, TLVSequence};
40
41use der::asn1::{AnyRef, ObjectIdentifier, OctetStringRef};
42use der::{
43    Decode, DecodeValue, EncodeValue, FixedTag, Header, Reader, Sequence, Tag, TagNumber, Tagged,
44};
45
46/// https://www.rfc-editor.org/rfc/rfc5652#section-12.1
47/// OID: 1.2.840.113549.1.7.2 (id-signedData)
48const OID_PKCS7_SIGNED_DATA: ObjectIdentifier =
49    ObjectIdentifier::new_unwrap("1.2.840.113549.1.7.2");
50/// OID: 1.2.840.113549.1.7.1 (id-data)
51const OID_PKCS7_DATA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.7.1");
52
53/// https://www.rfc-editor.org/rfc/rfc5758.html#section-2
54/// OID: 2.16.840.1.101.3.4.2.1 (id-sha256)
55const OID_SHA256: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.1");
56/// OID: 1.2.840.10045.4.3.2 (ecdsa-with-SHA256)
57const OID_ECDSA_WITH_SHA256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
58
59/// P-256 field element length in bytes (ECDSA signature r and s values).
60const P256_FE_LEN: usize = 32;
61
62/// Raw ECDSA signature length: r (32 bytes) || s (32 bytes).
63const RAW_SIGNATURE_LEN: usize = P256_FE_LEN * 2;
64
65/// ContentInfo ::= SEQUENCE {
66///   contentType OBJECT IDENTIFIER,
67///   content [0] EXPLICIT ANY DEFINED BY contentType
68/// }
69///
70/// https://www.rfc-editor.org/rfc/rfc5652#section-3
71#[allow(unused)]
72struct ContentInfo<'a> {
73    content_type: ObjectIdentifier,
74    /// The raw bytes of the SignedData SEQUENCE (after [0] EXPLICIT unwrapping)
75    signed_data_bytes: &'a [u8],
76}
77
78impl<'a> DecodeValue<'a> for ContentInfo<'a> {
79    fn decode_value<R: Reader<'a>>(reader: &mut R, header: Header) -> der::Result<Self> {
80        reader.read_nested(header.length, |reader| {
81            // contentType OBJECT IDENTIFIER
82            let content_type = ObjectIdentifier::decode(reader)?;
83
84            // Validate contentType is id-signedData
85            if content_type != OID_PKCS7_SIGNED_DATA {
86                return Err(der::ErrorKind::Failed.into());
87            }
88
89            // content [0] EXPLICIT - we need to unwrap and get the inner bytes
90            // Read the [0] context tag (should be context-specific, constructed, number 0)
91            let context_header = Header::decode(reader)?;
92            // Check for context-specific tag [0] constructed (0xA0)
93            if context_header.tag.number() != TagNumber::new(0)
94                || !context_header.tag.is_constructed()
95            {
96                return Err(der::ErrorKind::Failed.into());
97            }
98
99            // The content inside [0] is the SignedData SEQUENCE (the whole TLV)
100            let signed_data_bytes = reader.read_slice(context_header.length)?;
101
102            Ok(Self {
103                content_type,
104                signed_data_bytes,
105            })
106        })
107    }
108}
109
110impl<'a> FixedTag for ContentInfo<'a> {
111    const TAG: Tag = Tag::Sequence;
112}
113
114// TODO Remove when upgrading to der 0.8+ which separates Encode/Decode traits.
115impl<'a> EncodeValue for ContentInfo<'a> {
116    fn value_len(&self) -> der::Result<der::Length> {
117        unimplemented!("ContentInfo encoding is not supported")
118    }
119
120    fn encode_value(&self, _writer: &mut impl der::Writer) -> der::Result<()> {
121        unimplemented!("ContentInfo encoding is not supported")
122    }
123}
124
125/// EncapsulatedContentInfo ::= SEQUENCE {
126///   eContentType OBJECT IDENTIFIER,
127///   eContent [0] EXPLICIT OCTET STRING
128/// }
129///
130/// https://www.rfc-editor.org/rfc/rfc5652#section-5.2
131#[derive(Sequence)]
132struct EncapsulatedContentInfo<'a> {
133    econtent_type: ObjectIdentifier,
134    #[asn1(context_specific = "0", tag_mode = "EXPLICIT")]
135    econtent: OctetStringRef<'a>,
136}
137
138/// SignedData ::= SEQUENCE {
139///   version INTEGER,
140///   digestAlgorithms DigestAlgorithmIdentifiers,
141///   encapContentInfo EncapsulatedContentInfo,
142///   signerInfos SignerInfos
143/// }
144///
145/// https://www.rfc-editor.org/rfc/rfc5652#section-5.1
146#[allow(unused)]
147struct SignedData<'a> {
148    version: u8,
149    encap_content_info: EncapsulatedContentInfo<'a>,
150    /// SignerInfos is a SET OF, but store as AnyRef for manual parsing
151    /// to extract the single SignerInfo
152    signer_infos: AnyRef<'a>,
153}
154
155impl<'a> DecodeValue<'a> for SignedData<'a> {
156    fn decode_value<R: Reader<'a>>(reader: &mut R, header: Header) -> der::Result<Self> {
157        reader.read_nested(header.length, |reader| {
158            // version INTEGER (v3 = 3)
159            let version = u8::decode(reader)?;
160            if version != 3 {
161                return Err(der::ErrorKind::Failed.into());
162            }
163
164            // skip digestAlgorithms SET OF
165            let _digest_algorithms = AnyRef::decode(reader)?;
166            if _digest_algorithms.tag() != Tag::Set {
167                return Err(der::ErrorKind::Failed.into());
168            }
169
170            // encapContentInfo EncapsulatedContentInfo
171            let encap_content_info = EncapsulatedContentInfo::decode(reader)?;
172
173            // Validate eContentType is pkcs7-data
174            if encap_content_info.econtent_type != OID_PKCS7_DATA {
175                return Err(der::ErrorKind::Failed.into());
176            }
177
178            // signerInfos SET OF
179            let signer_infos = AnyRef::decode(reader)?;
180            if signer_infos.tag() != Tag::Set {
181                return Err(der::ErrorKind::Failed.into());
182            }
183
184            Ok(Self {
185                version,
186                encap_content_info,
187                signer_infos,
188            })
189        })
190    }
191}
192
193impl<'a> FixedTag for SignedData<'a> {
194    const TAG: Tag = Tag::Sequence;
195}
196
197// TODO Remove when upgrading to der 0.8+ which separates Encode/Decode traits.
198impl<'a> EncodeValue for SignedData<'a> {
199    fn value_len(&self) -> der::Result<der::Length> {
200        unimplemented!("SignedData encoding is not supported")
201    }
202
203    fn encode_value(&self, _writer: &mut impl der::Writer) -> der::Result<()> {
204        unimplemented!("SignedData encoding is not supported")
205    }
206}
207
208/// SignerInfo ::= SEQUENCE {
209///   version INTEGER,
210///   subjectKeyIdentifier [0] IMPLICIT OCTET STRING,
211///   digestAlgorithm AlgorithmIdentifier,
212///   signatureAlgorithm AlgorithmIdentifier,
213///   signature OCTET STRING
214/// }
215///
216/// Matter-specific SignerInfo with subjectKeyIdentifier instead of SignerIdentifier.
217///
218/// https://www.rfc-editor.org/rfc/rfc5652#section-5.3
219#[allow(unused)]
220struct SignerInfo<'a> {
221    version: u8,
222    subject_key_identifier: &'a [u8],
223    digest_algorithm: AlgorithmIdentifier<'a>,
224    signature_algorithm: AlgorithmIdentifier<'a>,
225    signature: OctetStringRef<'a>,
226}
227
228impl<'a> DecodeValue<'a> for SignerInfo<'a> {
229    fn decode_value<R: Reader<'a>>(reader: &mut R, header: Header) -> der::Result<Self> {
230        reader.read_nested(header.length, |reader| {
231            // version INTEGER (v3 = 3)
232            let version = u8::decode(reader)?;
233            if version != 3 {
234                return Err(der::ErrorKind::Failed.into());
235            }
236
237            // subjectKeyIdentifier [0] IMPLICIT OCTET STRING
238            let ski_header = Header::decode(reader)?;
239
240            // Check for context-specific tag [0] primitive
241            if ski_header.tag.number() != TagNumber::new(0) || ski_header.tag.is_constructed() {
242                return Err(der::ErrorKind::Failed.into());
243            }
244
245            let subject_key_identifier = reader.read_slice(ski_header.length)?;
246
247            // Validate SKI is exactly 20 bytes
248            if subject_key_identifier.len() != KEY_IDENTIFIER_LEN {
249                return Err(der::ErrorKind::Failed.into());
250            }
251
252            // digestAlgorithm AlgorithmIdentifier
253            let digest_algorithm = AlgorithmIdentifier::decode(reader)?;
254
255            // Validate digest algorithm is SHA256
256            if digest_algorithm.algorithm != OID_SHA256 {
257                return Err(der::ErrorKind::Failed.into());
258            }
259
260            // signatureAlgorithm AlgorithmIdentifier
261            let signature_algorithm = AlgorithmIdentifier::decode(reader)?;
262
263            // Validate signature algorithm is ECDSA with SHA256
264            if signature_algorithm.algorithm != OID_ECDSA_WITH_SHA256 {
265                return Err(der::ErrorKind::Failed.into());
266            }
267
268            // signature OCTET STRING (contains DER-encoded ECDSA signature)
269            let signature = OctetStringRef::decode(reader)?;
270
271            Ok(Self {
272                version,
273                subject_key_identifier,
274                digest_algorithm,
275                signature_algorithm,
276                signature,
277            })
278        })
279    }
280}
281
282impl<'a> FixedTag for SignerInfo<'a> {
283    const TAG: Tag = Tag::Sequence;
284}
285
286// Dummy EncodeValue implementation
287// Required by der version 0.7 for use with #[derive(Sequence)] on structs that contain this type.
288// TODO: Remove when upgrading to der 0.8+ which separates Encode/Decode traits.
289impl<'a> EncodeValue for SignerInfo<'a> {
290    fn value_len(&self) -> der::Result<der::Length> {
291        unimplemented!("SignerInfo encoding is not supported")
292    }
293
294    fn encode_value(&self, _writer: &mut impl der::Writer) -> der::Result<()> {
295        unimplemented!("SignerInfo encoding is not supported")
296    }
297}
298
299/// Parsed contents of a CMS SignedData envelope
300pub struct CmsSignedData<'a> {
301    /// SubjectKeyIdentifier from the SignerInfo (identifies the signing key)
302    pub signer_key_id: &'a [u8],
303    /// Raw TLV CD payload (the encapsulated content)
304    pub cd_content: &'a [u8],
305    /// ECDSA signature in raw (r || s) format, 64 bytes
306    pub signature_raw: [u8; RAW_SIGNATURE_LEN],
307}
308
309impl<'a> CmsSignedData<'a> {
310    /// Parse a CMS SignedData message, extracting the signer key ID,
311    /// encapsulated CD content, and ECDSA signature (converted from DER to raw).
312    ///
313    /// Expects the profiled CMS structure used by Matter CDs (Matter Spec):
314    /// https://www.rfc-editor.org/rfc/rfc5652#section-5.2
315    /// ```text
316    /// ContentInfo ::= SEQUENCE {
317    ///   contentType OBJECT IDENTIFIER id-signedData (1.2.840.113549.1.7.2),
318    ///   content [0] EXPLICIT SignedData
319    /// }
320    ///
321    /// SignedData ::= SEQUENCE {
322    ///   version INTEGER (v3(3)),
323    ///   digestAlgorithms SET { OBJECT IDENTIFIER sha256 (2.16.840.1.101.3.4.2.1) },
324    ///   encapContentInfo EncapsulatedContentInfo,
325    ///   signerInfos SET { SignerInfo }
326    /// }
327    ///
328    /// EncapsulatedContentInfo ::= SEQUENCE {
329    ///   eContentType OBJECT IDENTIFIER pkcs7-data (1.2.840.113549.1.7.1),
330    ///   eContent [0] EXPLICIT OCTET STRING cd_content
331    /// }
332    ///
333    /// SignerInfo ::= SEQUENCE {
334    ///   version INTEGER (v3(3)),
335    ///   subjectKeyIdentifier [0] IMPLICIT OCTET STRING,
336    ///   digestAlgorithm OBJECT IDENTIFIER sha256 (2.16.840.1.101.3.4.2.1),
337    ///   signatureAlgorithm OBJECT IDENTIFIER ecdsa-with-SHA256 (1.2.840.10045.4.3.2),
338    ///   signature OCTET STRING
339    /// }
340    /// ```
341    pub fn parse(cms_message: &'a [u8]) -> Result<Self, Error> {
342        // Parse ContentInfo
343        let content_info = ContentInfo::from_der(cms_message)
344            .map_err(|_| Error::from(ErrorCode::CdInvalidFormat))?;
345
346        // Parse SignedData from the raw bytes (includes SEQUENCE tag + length + value)
347        let signed_data = SignedData::from_der(content_info.signed_data_bytes)
348            .map_err(|_| Error::from(ErrorCode::CdInvalidFormat))?;
349
350        // Extract CD content (TLV payload)
351        let cd_content = signed_data.encap_content_info.econtent.as_bytes();
352
353        // Parse SignerInfo from signerInfos SET to extract key ID and signature
354        let signer_info = SignerInfo::from_der(signed_data.signer_infos.value())
355            .map_err(|_| Error::from(ErrorCode::CdInvalidFormat))?;
356
357        // Convert DER-encoded ECDSA signature to raw (r || s) format
358        let signature_raw = ecdsa_der_to_raw(signer_info.signature.as_bytes())?;
359
360        Ok(Self {
361            signer_key_id: signer_info.subject_key_identifier,
362            cd_content,
363            signature_raw,
364        })
365    }
366}
367
368/// Matter TLV context tags for CD elements (Matter Spec)
369const CD_TAG_FORMAT_VERSION: u8 = 0;
370const CD_TAG_VENDOR_ID: u8 = 1;
371const CD_TAG_PRODUCT_ID_ARRAY: u8 = 2;
372const CD_TAG_DEVICE_TYPE_ID: u8 = 3;
373const CD_TAG_CERTIFICATE_ID: u8 = 4;
374const CD_TAG_SECURITY_LEVEL: u8 = 5;
375const CD_TAG_SECURITY_INFORMATION: u8 = 6;
376const CD_TAG_VERSION_NUMBER: u8 = 7;
377const CD_TAG_CERTIFICATION_TYPE: u8 = 8;
378const CD_TAG_DAC_ORIGIN_VENDOR_ID: u8 = 9;
379const CD_TAG_DAC_ORIGIN_PRODUCT_ID: u8 = 10;
380const CD_TAG_AUTHORIZED_PAA_LIST: u8 = 11;
381
382/// Maximum number of product IDs in a CD
383pub const MAX_PRODUCT_IDS: usize = 100;
384
385/// Fixed length of the certificate_id string
386pub const CERTIFICATE_ID_LEN: usize = 19;
387
388/// Maximum number of authorized PAA entries in a CD
389pub const MAX_AUTHORIZED_PAA_LIST: usize = 10;
390
391/// Certification type (Matter Spec.)
392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
393#[cfg_attr(feature = "defmt", derive(defmt::Format))]
394#[repr(u8)]
395pub enum CertificationType {
396    /// Development and test devices
397    DevelopmentAndTest = 0,
398    /// Provisionally certified devices
399    Provisional = 1,
400    /// Officially certified devices
401    Official = 2,
402}
403
404impl CertificationType {
405    /// Try to parse a certification type from a raw u8 value.
406    pub fn from_u8(value: u8) -> Result<Self, Error> {
407        match value {
408            0 => Ok(Self::DevelopmentAndTest),
409            1 => Ok(Self::Provisional),
410            2 => Ok(Self::Official),
411            _ => Err(ErrorCode::CdInvalidFormat.into()),
412        }
413    }
414}
415
416/// Decoded Certification Declaration payload (Matter Spec.)
417#[derive(Debug, PartialEq, Eq)]
418#[cfg_attr(feature = "defmt", derive(defmt::Format))]
419pub struct CertificationElements {
420    pub format_version: u16,
421    pub vendor_id: u16,
422    pub product_ids: [u16; MAX_PRODUCT_IDS],
423    pub product_ids_count: usize,
424    pub device_type_id: u32,
425    pub certificate_id: [u8; CERTIFICATE_ID_LEN],
426    pub security_level: u8,
427    pub security_information: u16,
428    pub version_number: u16,
429    pub certification_type: CertificationType,
430    /// DAC origin vendor ID (present only if `dac_origin_vid_pid_present` is true).
431    pub dac_origin_vendor_id: u16,
432    /// DAC origin product ID (present only if `dac_origin_vid_pid_present` is true).
433    pub dac_origin_product_id: u16,
434    /// Whether `dac_origin_vendor_id` and `dac_origin_product_id` are present.
435    pub dac_origin_vid_pid_present: bool,
436    /// Authorized PAA Subject Key Identifiers.
437    pub authorized_paa_list: [[u8; KEY_IDENTIFIER_LEN]; MAX_AUTHORIZED_PAA_LIST],
438    /// Number of entries in `authorized_paa_list`.
439    pub authorized_paa_list_count: usize,
440}
441
442impl Default for CertificationElements {
443    fn default() -> Self {
444        Self {
445            format_version: 0,
446            vendor_id: 0,
447            product_ids: [0u16; MAX_PRODUCT_IDS],
448            product_ids_count: 0,
449            device_type_id: 0,
450            certificate_id: [0u8; CERTIFICATE_ID_LEN],
451            security_level: 0,
452            security_information: 0,
453            version_number: 0,
454            certification_type: CertificationType::DevelopmentAndTest,
455            dac_origin_vendor_id: 0,
456            dac_origin_product_id: 0,
457            dac_origin_vid_pid_present: false,
458            authorized_paa_list: [[0u8; KEY_IDENTIFIER_LEN]; MAX_AUTHORIZED_PAA_LIST],
459            authorized_paa_list_count: 0,
460        }
461    }
462}
463
464impl CertificationElements {
465    /// Decode a TLV-encoded CD payload into [`CertificationElements`].
466    ///
467    /// Validates the TLV structure, field types, and constraints per the Matter spec:
468    /// - Tags 0-8 are mandatory and must appear in order.
469    /// - Tags 9-10 (DAC origin) are optional but must appear together.
470    /// - Tag 11 (authorized PAA list) is optional.
471    /// - Product IDs array must have 1..=100 entries.
472    /// - Certificate ID must be exactly 19 bytes.
473    /// - Authorized PAA entries must each be exactly 20 bytes.
474    pub fn decode(cd_content: &[u8]) -> Result<Self, Error> {
475        let elem = TLVElement::new(cd_content);
476        let structure = elem.structure()?;
477
478        let format_version = structure.find_ctx(CD_TAG_FORMAT_VERSION)?.u16()?;
479        if format_version != 1 {
480            return Err(ErrorCode::CdInvalidFormat.into());
481        }
482
483        let (product_ids, product_ids_count) = Self::parse_product_ids(&structure)?;
484        let certificate_id = Self::parse_certificate_id(&structure)?;
485        let (dac_origin_vendor_id, dac_origin_product_id, dac_origin_vid_pid_present) =
486            Self::parse_dac_origin(&structure)?;
487        let (authorized_paa_list, authorized_paa_list_count) =
488            Self::parse_authorized_paa_list(&structure)?;
489
490        Ok(Self {
491            format_version,
492            vendor_id: structure.find_ctx(CD_TAG_VENDOR_ID)?.u16()?,
493            product_ids,
494            product_ids_count,
495            device_type_id: structure.find_ctx(CD_TAG_DEVICE_TYPE_ID)?.u32()?,
496            certificate_id,
497            security_level: structure.find_ctx(CD_TAG_SECURITY_LEVEL)?.u8()?,
498            security_information: structure.find_ctx(CD_TAG_SECURITY_INFORMATION)?.u16()?,
499            version_number: structure.find_ctx(CD_TAG_VERSION_NUMBER)?.u16()?,
500            certification_type: CertificationType::from_u8(
501                structure.find_ctx(CD_TAG_CERTIFICATION_TYPE)?.u8()?,
502            )?,
503            dac_origin_vendor_id,
504            dac_origin_product_id,
505            dac_origin_vid_pid_present,
506            authorized_paa_list,
507            authorized_paa_list_count,
508        })
509    }
510
511    /// Parse the product ID array from TLV structure.
512    /// Returns (product_ids array, count of valid entries).
513    fn parse_product_ids(
514        structure: &TLVSequence,
515    ) -> Result<([u16; MAX_PRODUCT_IDS], usize), Error> {
516        let pid_array = structure.find_ctx(CD_TAG_PRODUCT_ID_ARRAY)?;
517        let pid_seq = pid_array.array()?;
518
519        let mut product_ids = [0u16; MAX_PRODUCT_IDS];
520        let mut count = 0usize;
521
522        for pid_elem in pid_seq.iter() {
523            let pid_elem: TLVElement<'_> = pid_elem?;
524            if count >= MAX_PRODUCT_IDS {
525                return Err(ErrorCode::CdInvalidFormat.into());
526            }
527            product_ids[count] = pid_elem.u16()?;
528            count += 1;
529        }
530
531        if count == 0 {
532            return Err(ErrorCode::CdInvalidFormat.into());
533        }
534
535        Ok((product_ids, count))
536    }
537
538    /// Parse the certificate ID from TLV structure.
539    /// Returns a fixed-length array of exactly CERTIFICATE_ID_LEN bytes.
540    fn parse_certificate_id(structure: &TLVSequence) -> Result<[u8; CERTIFICATE_ID_LEN], Error> {
541        let cert_id_str = structure.find_ctx(CD_TAG_CERTIFICATE_ID)?.utf8()?;
542        if cert_id_str.len() != CERTIFICATE_ID_LEN {
543            return Err(ErrorCode::CdInvalidFormat.into());
544        }
545
546        let mut certificate_id = [0u8; CERTIFICATE_ID_LEN];
547        certificate_id.copy_from_slice(cert_id_str.as_bytes());
548        Ok(certificate_id)
549    }
550
551    /// Parse optional DAC origin vendor/product IDs from TLV structure.
552    /// Returns (dac_origin_vendor_id, dac_origin_product_id, is_present).
553    fn parse_dac_origin(structure: &TLVSequence) -> Result<(u16, u16, bool), Error> {
554        let vid_elem = structure.find_ctx(CD_TAG_DAC_ORIGIN_VENDOR_ID)?;
555        let pid_elem = structure.find_ctx(CD_TAG_DAC_ORIGIN_PRODUCT_ID)?;
556
557        // Both must be present or both must be absent
558        if vid_elem.is_empty() != pid_elem.is_empty() {
559            return Err(ErrorCode::CdInvalidFormat.into());
560        }
561
562        if !vid_elem.is_empty() {
563            Ok((vid_elem.u16()?, pid_elem.u16()?, true))
564        } else {
565            Ok((0, 0, false))
566        }
567    }
568
569    /// Parse optional authorized PAA list from TLV structure.
570    /// Returns (authorized_paa_list array, count of valid entries).
571    fn parse_authorized_paa_list(
572        structure: &TLVSequence,
573    ) -> Result<([[u8; KEY_IDENTIFIER_LEN]; MAX_AUTHORIZED_PAA_LIST], usize), Error> {
574        let paa_elem = structure.find_ctx(CD_TAG_AUTHORIZED_PAA_LIST)?;
575
576        let mut authorized_paa_list = [[0u8; KEY_IDENTIFIER_LEN]; MAX_AUTHORIZED_PAA_LIST];
577        let mut paa_count = 0usize;
578
579        if !paa_elem.is_empty() {
580            let paa_seq = paa_elem.array()?;
581            for paa_entry in paa_seq.iter() {
582                let paa_entry: TLVElement<'_> = paa_entry?;
583                if paa_count >= MAX_AUTHORIZED_PAA_LIST {
584                    return Err(ErrorCode::CdInvalidFormat.into());
585                }
586                let paa_bytes = paa_entry.str()?;
587                if paa_bytes.len() != KEY_IDENTIFIER_LEN {
588                    return Err(ErrorCode::CdInvalidFormat.into());
589                }
590                authorized_paa_list[paa_count].copy_from_slice(paa_bytes);
591                paa_count += 1;
592            }
593        }
594
595        Ok((authorized_paa_list, paa_count))
596    }
597
598    /// Verify a CMS-signed Certification Declaration.
599    ///
600    /// 1. Parses the CMS envelope
601    /// 2. Looks up the signing key by Key ID in the well-known trust store
602    /// 3. Enforces test key policy (test keys only for DevelopmentAndTest/Provisional)
603    /// 4. Verifies the ECDSA-SHA256 signature over the CD content
604    /// 5. Decodes the CD TLV payload
605    ///
606    /// # Arguments
607    /// - `crypto`: Cryptographic backend for ECDSA verification.
608    /// - `cms_message`: The complete CMS-signed CD message bytes.
609    /// - `allow_test_cd_signing_key`: If `false`, CDs signed with the test key are rejected.
610    ///
611    /// # Returns
612    /// The decoded [`CertificationElements`] on success.
613    pub fn verify<C: Crypto>(
614        crypto: C,
615        cms_message: &[u8],
616        allow_test_cd_signing_key: bool,
617    ) -> Result<Self, Error> {
618        // Parse CMS envelope
619        let cms = CmsSignedData::parse(cms_message)?;
620
621        // Look up signing key
622        let pubkey_bytes = cd_keys::lookup_cd_signing_key(cms.signer_key_id)
623            .ok_or(Error::new(ErrorCode::CdSigningKeyNotFound))?;
624
625        // Test key policy
626        let is_test_key = cd_keys::is_test_cd_key(cms.signer_key_id);
627        if is_test_key && !allow_test_cd_signing_key {
628            return Err(ErrorCode::CdSigningKeyNotFound.into());
629        }
630
631        // Verify ECDSA-SHA256 signature over the raw CD content
632        let pubkey_ref = CanonPkcPublicKeyRef::try_new(pubkey_bytes)?;
633        let pubkey = crypto.pub_key(pubkey_ref)?;
634
635        let sig_ref = CanonPkcSignatureRef::new(&cms.signature_raw);
636
637        let valid = pubkey.verify(cms.cd_content, sig_ref)?;
638        if !valid {
639            return Err(ErrorCode::CdInvalidSignature.into());
640        }
641
642        // Decode CD TLV payload
643        let cd = CertificationElements::decode(cms.cd_content)?;
644
645        // Post-signature test key policy enforcement
646        // Test key may only sign DevelopmentAndTest (and optionally Provisional) CDs
647        if is_test_key && cd.certification_type == CertificationType::Official {
648            return Err(ErrorCode::CdSigningKeyNotFound.into());
649        }
650
651        Ok(cd)
652    }
653
654    /// Validate CD content against device identity.
655    ///
656    /// Implements the CD validation rules (Matter Spec).
657    ///
658    /// # Validation rules
659    ///
660    /// 1. `format_version` must be 1.
661    /// 2. `certification_type` must be valid (0, 1, or 2) -- already enforced by decoding.
662    /// 3. CD `vendor_id` must match device's BasicInformation VendorID.
663    /// 4. Device's BasicInformation ProductID must be in CD's `product_id_array`.
664    /// 5. If `dac_origin_vid_pid_present`:
665    ///    - DAC VID must match `dac_origin_vendor_id`
666    ///    - PAI VID must match `dac_origin_vendor_id`
667    ///    - DAC PID must match `dac_origin_product_id`
668    ///    - If PAI has PID, it must match `dac_origin_product_id`
669    /// 6. If NOT `dac_origin_vid_pid_present`:
670    ///    - DAC VID must match CD `vendor_id`
671    ///    - PAI VID must match CD `vendor_id`
672    ///    - DAC PID must be in CD `product_id_array`
673    ///    - If PAI has PID, it must be in CD `product_id_array`
674    /// 7. If `authorized_paa_list` is present, PAA's SKID must be in the list.
675    ///
676    /// Note: `security_level`, `security_information`, and `version_number` are
677    /// explicitly ignored per the specification.
678    pub fn validate(&self, device_info: &DeviceInfoForAttestation) -> Result<(), Error> {
679        // Rule 1: format_version must be 1
680        if self.format_version != 1 {
681            return Err(ErrorCode::CdInvalidFormat.into());
682        }
683
684        // Rule 2: certification_type is already validated by decode
685
686        // Rule 3: CD vendor_id must match device's BasicInformation VendorID
687        if self.vendor_id != device_info.vendor_id {
688            return Err(ErrorCode::CdInvalidVendorId.into());
689        }
690
691        // Rule 4: Device's ProductID must be in the CD's product_id_array
692        if !product_id_in_list(device_info.product_id, self) {
693            return Err(ErrorCode::CdInvalidProductId.into());
694        }
695
696        // Rules 5-6: VID/PID matching depends on dac_origin_vid_pid_present
697        if self.dac_origin_vid_pid_present {
698            // Rule 5: dacOriginVIDandPID present
699            if device_info.dac_vendor_id != self.dac_origin_vendor_id {
700                return Err(ErrorCode::CdInvalidVendorId.into());
701            }
702            if device_info.pai_vendor_id != self.dac_origin_vendor_id {
703                return Err(ErrorCode::CdInvalidVendorId.into());
704            }
705            if device_info.dac_product_id != self.dac_origin_product_id {
706                return Err(ErrorCode::CdInvalidProductId.into());
707            }
708            if device_info.pai_product_id != 0
709                && device_info.pai_product_id != self.dac_origin_product_id
710            {
711                return Err(ErrorCode::CdInvalidProductId.into());
712            }
713        } else {
714            // Rule 6: dacOriginVIDandPID NOT present
715            if device_info.dac_vendor_id != self.vendor_id {
716                return Err(ErrorCode::CdInvalidVendorId.into());
717            }
718            if device_info.pai_vendor_id != self.vendor_id {
719                return Err(ErrorCode::CdInvalidVendorId.into());
720            }
721            if !product_id_in_list(device_info.dac_product_id, self) {
722                return Err(ErrorCode::CdInvalidProductId.into());
723            }
724            if device_info.pai_product_id != 0
725                && !product_id_in_list(device_info.pai_product_id, self)
726            {
727                return Err(ErrorCode::CdInvalidProductId.into());
728            }
729        }
730
731        // Rule 7: Authorized PAA list check
732        if self.authorized_paa_list_count > 0 {
733            let found = self.authorized_paa_list[..self.authorized_paa_list_count]
734                .contains(&device_info.paa_skid);
735            if !found {
736                return Err(ErrorCode::CdInvalidPaa.into());
737            }
738        }
739
740        Ok(())
741    }
742}
743
744/// Device identity information for CD validation.
745///
746/// Carries the identity data extracted from the device's BasicInformation cluster
747/// and its certificate chain (DAC, PAI, PAA).
748pub struct DeviceInfoForAttestation {
749    /// Vendor ID from the BasicInformation cluster.
750    pub vendor_id: u16,
751    /// Product ID from the BasicInformation cluster.
752    pub product_id: u16,
753    /// Vendor ID extracted from the DAC certificate.
754    pub dac_vendor_id: u16,
755    /// Product ID extracted from the DAC certificate.
756    pub dac_product_id: u16,
757    /// Vendor ID extracted from the PAI certificate.
758    pub pai_vendor_id: u16,
759    /// Product ID extracted from the PAI certificate (0 if not present).
760    pub pai_product_id: u16,
761    /// Subject Key Identifier of the PAA certificate.
762    pub paa_skid: [u8; KEY_IDENTIFIER_LEN],
763}
764
765/// Check if a product ID is present in the CD's product_id_array.
766fn product_id_in_list(pid: u16, cd: &CertificationElements) -> bool {
767    cd.product_ids[..cd.product_ids_count].contains(&pid)
768}
769
770#[cfg(test)]
771mod tests {
772    use super::*;
773    use crate::crypto::test_only_crypto;
774
775    // ---- Test vector 1: single product ID, no DAC origin ----
776    // Signed with the "Matter Test CD Signing Authority" key.
777    // -> format_version = 1
778    // -> vendor_id = 0xFFF1
779    // -> product_id_array = [ 0x8000 ]
780    // -> device_type_id = 0x1234
781    // -> certificate_id = "ZIG20141ZB330001-24"
782    // -> security_level = 0
783    // -> security_information = 0
784    // -> version_number = 0x2694
785    // -> certification_type = 0
786    // -> dac_origin_vendor_id is not present
787    // -> dac_origin_product_id is not present
788
789    fn expected_cd_01() -> CertificationElements {
790        let mut cd = CertificationElements {
791            format_version: 1,
792            vendor_id: 0xFFF1,
793            product_ids_count: 1,
794            device_type_id: 0x1234,
795            version_number: 0x2694,
796            ..CertificationElements::default()
797        };
798        cd.product_ids[0] = 0x8000;
799        cd.certificate_id.copy_from_slice(b"ZIG20141ZB330001-24");
800        cd
801    }
802
803    const TEST_CMS_CD_CONTENT_01: &[u8] = &[
804        0x15, 0x24, 0x00, 0x01, 0x25, 0x01, 0xf1, 0xff, 0x36, 0x02, 0x05, 0x00, 0x80, 0x18, 0x25,
805        0x03, 0x34, 0x12, 0x2c, 0x04, 0x13, 0x5a, 0x49, 0x47, 0x32, 0x30, 0x31, 0x34, 0x31, 0x5a,
806        0x42, 0x33, 0x33, 0x30, 0x30, 0x30, 0x31, 0x2d, 0x32, 0x34, 0x24, 0x05, 0x00, 0x24, 0x06,
807        0x00, 0x25, 0x07, 0x94, 0x26, 0x24, 0x08, 0x00, 0x18,
808    ];
809
810    const TEST_CMS_SIGNED_MESSAGE_01: &[u8] = &[
811        0x30, 0x81, 0xe8, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02, 0xa0,
812        0x81, 0xda, 0x30, 0x81, 0xd7, 0x02, 0x01, 0x03, 0x31, 0x0d, 0x30, 0x0b, 0x06, 0x09, 0x60,
813        0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x30, 0x45, 0x06, 0x09, 0x2a, 0x86, 0x48,
814        0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01, 0xa0, 0x38, 0x04, 0x36, 0x15, 0x24, 0x00, 0x01, 0x25,
815        0x01, 0xf1, 0xff, 0x36, 0x02, 0x05, 0x00, 0x80, 0x18, 0x25, 0x03, 0x34, 0x12, 0x2c, 0x04,
816        0x13, 0x5a, 0x49, 0x47, 0x32, 0x30, 0x31, 0x34, 0x31, 0x5a, 0x42, 0x33, 0x33, 0x30, 0x30,
817        0x30, 0x31, 0x2d, 0x32, 0x34, 0x24, 0x05, 0x00, 0x24, 0x06, 0x00, 0x25, 0x07, 0x94, 0x26,
818        0x24, 0x08, 0x00, 0x18, 0x31, 0x7c, 0x30, 0x7a, 0x02, 0x01, 0x03, 0x80, 0x14, 0x62, 0xfa,
819        0x82, 0x33, 0x59, 0xac, 0xfa, 0xa9, 0x96, 0x3e, 0x1c, 0xfa, 0x14, 0x0a, 0xdd, 0xf5, 0x04,
820        0xf3, 0x71, 0x60, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02,
821        0x01, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x04, 0x46,
822        0x30, 0x44, 0x02, 0x20, 0x43, 0xa6, 0x3f, 0x2b, 0x94, 0x3d, 0xf3, 0x3c, 0x38, 0xb3, 0xe0,
823        0x2f, 0xca, 0xa7, 0x5f, 0xe3, 0x53, 0x2a, 0xeb, 0xbf, 0x5e, 0x63, 0xf5, 0xbb, 0xdb, 0xc0,
824        0xb1, 0xf0, 0x1d, 0x3c, 0x4f, 0x60, 0x02, 0x20, 0x4c, 0x1a, 0xbf, 0x5f, 0x18, 0x07, 0xb8,
825        0x18, 0x94, 0xb1, 0x57, 0x6c, 0x47, 0xe4, 0x72, 0x4e, 0x4d, 0x96, 0x6c, 0x61, 0x2e, 0xd3,
826        0xfa, 0x25, 0xc1, 0x18, 0xc3, 0xf2, 0xb3, 0xf9, 0x03, 0x69,
827    ];
828
829    // ---- Test vector 2: two product IDs, with DAC origin ----
830    // -> format_version = 1
831    // -> vendor_id = 0xFFF2
832    // -> product_id_array = [ 0x8001, 0x8002 ]
833    // -> device_type_id = 0x1234
834    // -> certificate_id = "ZIG20142ZB330002-24"
835    // -> security_level = 0
836    // -> security_information = 0
837    // -> version_number = 0x2694
838    // -> certification_type = 0
839    // -> dac_origin_vendor_id = 0xFFF1
840    // -> dac_origin_product_id = 0x8000
841
842    fn expected_cd_02() -> CertificationElements {
843        let mut cd = CertificationElements {
844            format_version: 1,
845            vendor_id: 0xFFF2,
846            product_ids_count: 2,
847            device_type_id: 0x1234,
848            version_number: 0x2694,
849            dac_origin_vendor_id: 0xFFF1,
850            dac_origin_product_id: 0x8000,
851            dac_origin_vid_pid_present: true,
852            ..CertificationElements::default()
853        };
854        cd.product_ids[0] = 0x8001;
855        cd.product_ids[1] = 0x8002;
856        cd.certificate_id.copy_from_slice(b"ZIG20142ZB330002-24");
857        cd
858    }
859
860    const TEST_CMS_CD_CONTENT_02: &[u8] = &[
861        0x15, 0x24, 0x00, 0x01, 0x25, 0x01, 0xf2, 0xff, 0x36, 0x02, 0x05, 0x01, 0x80, 0x05, 0x02,
862        0x80, 0x18, 0x25, 0x03, 0x34, 0x12, 0x2c, 0x04, 0x13, 0x5a, 0x49, 0x47, 0x32, 0x30, 0x31,
863        0x34, 0x32, 0x5a, 0x42, 0x33, 0x33, 0x30, 0x30, 0x30, 0x32, 0x2d, 0x32, 0x34, 0x24, 0x05,
864        0x00, 0x24, 0x06, 0x00, 0x25, 0x07, 0x94, 0x26, 0x24, 0x08, 0x00, 0x25, 0x09, 0xf1, 0xff,
865        0x25, 0x0a, 0x00, 0x80, 0x18,
866    ];
867
868    const TEST_CMS_SIGNED_MESSAGE_02: &[u8] = &[
869        0x30, 0x81, 0xf5, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02, 0xa0,
870        0x81, 0xe7, 0x30, 0x81, 0xe4, 0x02, 0x01, 0x03, 0x31, 0x0d, 0x30, 0x0b, 0x06, 0x09, 0x60,
871        0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x30, 0x50, 0x06, 0x09, 0x2a, 0x86, 0x48,
872        0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01, 0xa0, 0x43, 0x04, 0x41, 0x15, 0x24, 0x00, 0x01, 0x25,
873        0x01, 0xf2, 0xff, 0x36, 0x02, 0x05, 0x01, 0x80, 0x05, 0x02, 0x80, 0x18, 0x25, 0x03, 0x34,
874        0x12, 0x2c, 0x04, 0x13, 0x5a, 0x49, 0x47, 0x32, 0x30, 0x31, 0x34, 0x32, 0x5a, 0x42, 0x33,
875        0x33, 0x30, 0x30, 0x30, 0x32, 0x2d, 0x32, 0x34, 0x24, 0x05, 0x00, 0x24, 0x06, 0x00, 0x25,
876        0x07, 0x94, 0x26, 0x24, 0x08, 0x00, 0x25, 0x09, 0xf1, 0xff, 0x25, 0x0a, 0x00, 0x80, 0x18,
877        0x31, 0x7e, 0x30, 0x7c, 0x02, 0x01, 0x03, 0x80, 0x14, 0x62, 0xfa, 0x82, 0x33, 0x59, 0xac,
878        0xfa, 0xa9, 0x96, 0x3e, 0x1c, 0xfa, 0x14, 0x0a, 0xdd, 0xf5, 0x04, 0xf3, 0x71, 0x60, 0x30,
879        0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x30, 0x0a, 0x06,
880        0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x04, 0x48, 0x30, 0x46, 0x02, 0x21,
881        0x00, 0x92, 0x62, 0x96, 0xf7, 0x57, 0x81, 0x58, 0xbe, 0x7c, 0x45, 0x93, 0x88, 0x33, 0x6c,
882        0xa7, 0x38, 0x37, 0x66, 0xc9, 0xee, 0xdd, 0x98, 0x55, 0xcb, 0xda, 0x6f, 0x4c, 0xf6, 0xbd,
883        0xf4, 0x32, 0x11, 0x02, 0x21, 0x00, 0xe0, 0xdb, 0xf4, 0xa2, 0xbc, 0xec, 0x4e, 0xa2, 0x74,
884        0xba, 0xf0, 0xde, 0xa2, 0x08, 0xb3, 0x36, 0x5c, 0x6e, 0xd5, 0x44, 0x08, 0x6d, 0x10, 0x1a,
885        0xfd, 0xaf, 0x07, 0x9a, 0x2c, 0x23, 0xe0, 0xde,
886    ];
887
888    // ---- CMS parsing tests ----
889
890    #[test]
891    fn test_parse_cms_signed_data_01() {
892        let cms = unwrap!(CmsSignedData::parse(TEST_CMS_SIGNED_MESSAGE_01));
893
894        // Verify signer KID is the test key
895        assert_eq!(cms.signer_key_id, &cd_keys::TEST_CD_KID);
896
897        // Verify extracted CD content matches raw content
898        assert_eq!(cms.cd_content, TEST_CMS_CD_CONTENT_01);
899
900        // Verify signature is 64 bytes (non-zero)
901        assert!(cms.signature_raw.iter().any(|&b| b != 0));
902    }
903
904    #[test]
905    fn test_parse_cms_signed_data_02() {
906        let cms = unwrap!(CmsSignedData::parse(TEST_CMS_SIGNED_MESSAGE_02));
907
908        assert_eq!(cms.signer_key_id, &cd_keys::TEST_CD_KID);
909        assert_eq!(cms.cd_content, TEST_CMS_CD_CONTENT_02);
910    }
911
912    #[test]
913    fn test_cms_extract_cd_content() {
914        let cms = unwrap!(CmsSignedData::parse(TEST_CMS_SIGNED_MESSAGE_01));
915        assert_eq!(cms.cd_content, TEST_CMS_CD_CONTENT_01);
916    }
917
918    #[test]
919    fn test_cms_extract_key_id() {
920        let cms = unwrap!(CmsSignedData::parse(TEST_CMS_SIGNED_MESSAGE_01));
921        assert_eq!(cms.signer_key_id, &cd_keys::TEST_CD_KID);
922    }
923
924    #[test]
925    fn test_parse_cms_invalid_data() {
926        // Empty
927        assert!(CmsSignedData::parse(&[]).is_err());
928
929        // Random garbage
930        assert!(CmsSignedData::parse(&[0x01, 0x02, 0x03]).is_err());
931
932        // Valid SEQUENCE but wrong OID
933        assert!(CmsSignedData::parse(&[0x30, 0x06, 0x06, 0x02, 0x55, 0x04, 0x00, 0x00]).is_err());
934    }
935
936    // ---- TLV decoding tests ----
937
938    #[test]
939    fn test_decode_cd_content_01() {
940        let cd = unwrap!(CertificationElements::decode(TEST_CMS_CD_CONTENT_01));
941        assert_eq!(cd, expected_cd_01());
942    }
943
944    #[test]
945    fn test_decode_cd_content_02() {
946        let cd = unwrap!(CertificationElements::decode(TEST_CMS_CD_CONTENT_02));
947        assert_eq!(cd, expected_cd_02());
948    }
949
950    // ---- Signature verification tests ----
951
952    #[test]
953    fn test_verify_cd_01_with_test_key_allowed() {
954        let crypto = test_only_crypto();
955        let cd = unwrap!(CertificationElements::verify(
956            &crypto,
957            TEST_CMS_SIGNED_MESSAGE_01,
958            true,
959        ));
960
961        assert_eq!(cd, expected_cd_01());
962    }
963
964    #[test]
965    fn test_verify_cd_02_with_test_key_allowed() {
966        let crypto = test_only_crypto();
967        let cd = unwrap!(CertificationElements::verify(
968            &crypto,
969            TEST_CMS_SIGNED_MESSAGE_02,
970            true,
971        ));
972
973        assert_eq!(cd, expected_cd_02());
974    }
975
976    #[test]
977    fn test_verify_cd_test_key_not_allowed() {
978        let crypto = test_only_crypto();
979        let result = CertificationElements::verify(
980            &crypto,
981            TEST_CMS_SIGNED_MESSAGE_01,
982            false, // test key NOT allowed
983        );
984
985        assert_eq!(
986            result.map_err(|e| e.code()),
987            Err(ErrorCode::CdSigningKeyNotFound)
988        );
989    }
990
991    // ---- Content validation tests ----
992
993    #[test]
994    fn test_validate_cd_success_basic() {
995        let cd = unwrap!(CertificationElements::decode(TEST_CMS_CD_CONTENT_01));
996        let device_info = DeviceInfoForAttestation {
997            vendor_id: 0xFFF1,
998            product_id: 0x8000,
999            dac_vendor_id: 0xFFF1,
1000            dac_product_id: 0x8000,
1001            pai_vendor_id: 0xFFF1,
1002            pai_product_id: 0, // PAI without PID
1003            paa_skid: [0u8; KEY_IDENTIFIER_LEN],
1004        };
1005
1006        unwrap!(cd.validate(&device_info));
1007    }
1008
1009    #[test]
1010    fn test_validate_cd_wrong_vendor_id() {
1011        let cd = unwrap!(CertificationElements::decode(TEST_CMS_CD_CONTENT_01));
1012        let device_info = DeviceInfoForAttestation {
1013            vendor_id: 0xFFF2, // Wrong: CD has 0xFFF1
1014            product_id: 0x8000,
1015            dac_vendor_id: 0xFFF1,
1016            dac_product_id: 0x8000,
1017            pai_vendor_id: 0xFFF1,
1018            pai_product_id: 0,
1019            paa_skid: [0u8; KEY_IDENTIFIER_LEN],
1020        };
1021
1022        assert_eq!(
1023            cd.validate(&device_info).map_err(|e| e.code()),
1024            Err(ErrorCode::CdInvalidVendorId)
1025        );
1026    }
1027
1028    #[test]
1029    fn test_validate_cd_wrong_product_id() {
1030        let cd = unwrap!(CertificationElements::decode(TEST_CMS_CD_CONTENT_01));
1031        let device_info = DeviceInfoForAttestation {
1032            vendor_id: 0xFFF1,
1033            product_id: 0x9999, // Wrong: not in [0x8000]
1034            dac_vendor_id: 0xFFF1,
1035            dac_product_id: 0x8000,
1036            pai_vendor_id: 0xFFF1,
1037            pai_product_id: 0,
1038            paa_skid: [0u8; KEY_IDENTIFIER_LEN],
1039        };
1040
1041        assert_eq!(
1042            cd.validate(&device_info).map_err(|e| e.code()),
1043            Err(ErrorCode::CdInvalidProductId)
1044        );
1045    }
1046
1047    #[test]
1048    fn test_validate_cd_wrong_dac_vendor_id() {
1049        // CD01 has no dac_origin, so DAC VID must match CD vendor_id
1050        let cd = unwrap!(CertificationElements::decode(TEST_CMS_CD_CONTENT_01));
1051        let device_info = DeviceInfoForAttestation {
1052            vendor_id: 0xFFF1,
1053            product_id: 0x8000,
1054            dac_vendor_id: 0xFFF2, // Wrong: must match CD vendor_id (0xFFF1)
1055            dac_product_id: 0x8000,
1056            pai_vendor_id: 0xFFF1,
1057            pai_product_id: 0,
1058            paa_skid: [0u8; KEY_IDENTIFIER_LEN],
1059        };
1060
1061        assert_eq!(
1062            cd.validate(&device_info).map_err(|e| e.code()),
1063            Err(ErrorCode::CdInvalidVendorId)
1064        );
1065    }
1066
1067    #[test]
1068    fn test_validate_cd_with_dac_origin() {
1069        // CD02 has dac_origin_vid=0xFFF1, dac_origin_pid=0x8000
1070        let cd = unwrap!(CertificationElements::decode(TEST_CMS_CD_CONTENT_02));
1071        let device_info = DeviceInfoForAttestation {
1072            vendor_id: 0xFFF2,
1073            product_id: 0x8001,     // Must be in [0x8001, 0x8002]
1074            dac_vendor_id: 0xFFF1,  // Must match dac_origin_vendor_id
1075            dac_product_id: 0x8000, // Must match dac_origin_product_id
1076            pai_vendor_id: 0xFFF1,  // Must match dac_origin_vendor_id
1077            pai_product_id: 0,
1078            paa_skid: [0u8; KEY_IDENTIFIER_LEN],
1079        };
1080
1081        unwrap!(cd.validate(&device_info));
1082    }
1083
1084    #[test]
1085    fn test_validate_cd_dac_origin_wrong_dac_vid() {
1086        let cd = unwrap!(CertificationElements::decode(TEST_CMS_CD_CONTENT_02));
1087        let device_info = DeviceInfoForAttestation {
1088            vendor_id: 0xFFF2,
1089            product_id: 0x8001,
1090            dac_vendor_id: 0xFFF2, // Wrong: must match dac_origin_vid (0xFFF1)
1091            dac_product_id: 0x8000,
1092            pai_vendor_id: 0xFFF1,
1093            pai_product_id: 0,
1094            paa_skid: [0u8; KEY_IDENTIFIER_LEN],
1095        };
1096
1097        assert_eq!(
1098            cd.validate(&device_info).map_err(|e| e.code()),
1099            Err(ErrorCode::CdInvalidVendorId)
1100        );
1101    }
1102
1103    #[test]
1104    fn test_validate_cd_wrong_format_version() {
1105        // Manually construct a CD with format_version = 2
1106        let mut cd = unwrap!(CertificationElements::decode(TEST_CMS_CD_CONTENT_01));
1107        cd.format_version = 2;
1108        let device_info = DeviceInfoForAttestation {
1109            vendor_id: 0xFFF1,
1110            product_id: 0x8000,
1111            dac_vendor_id: 0xFFF1,
1112            dac_product_id: 0x8000,
1113            pai_vendor_id: 0xFFF1,
1114            pai_product_id: 0,
1115            paa_skid: [0u8; KEY_IDENTIFIER_LEN],
1116        };
1117
1118        assert_eq!(
1119            cd.validate(&device_info).map_err(|e| e.code()),
1120            Err(ErrorCode::CdInvalidFormat)
1121        );
1122    }
1123
1124    // ---- CertificationType tests ----
1125
1126    #[test]
1127    fn test_certification_type_from_u8() {
1128        assert_eq!(
1129            unwrap!(CertificationType::from_u8(0)),
1130            CertificationType::DevelopmentAndTest
1131        );
1132        assert_eq!(
1133            unwrap!(CertificationType::from_u8(1)),
1134            CertificationType::Provisional
1135        );
1136        assert_eq!(
1137            unwrap!(CertificationType::from_u8(2)),
1138            CertificationType::Official
1139        );
1140        assert!(CertificationType::from_u8(3).is_err());
1141        assert!(CertificationType::from_u8(255).is_err());
1142    }
1143}