Skip to main content

matter_cert/
chain.rs

1//! Matter certificate chain validation.
2//!
3//! Walks an ordered slice of [`MatterCertificate`]s (leaf to topmost
4//! intermediate) and verifies that the chain anchors against a known
5//! trusted root. Per-cert checks: time bounds, CA bit (above the leaf),
6//! issuer/subject linkage (structural DN equality), path-length
7//! constraint, and signature verification (via M2.3's
8//! [`crate::MatterCertificate::verify_signed_by`]).
9//!
10//! See `docs/superpowers/specs/2026-05-18-matter-cert-chain-validation-design.md`
11//! for the full design.
12
13use crate::certificate::MatterCertificate;
14use crate::error::{Error, Result};
15use crate::extensions::KeyIdentifier;
16use crate::name::DistinguishedName;
17use crate::public_key::PublicKey;
18use crate::time::MatterTime;
19
20/// A trust anchor — a known-good public key paired with the DN under
21/// which it was certified and, optionally, its subject-key-identifier
22/// for the X.509-style AKI/SKI link check.
23#[derive(Debug, Clone)]
24pub struct TrustAnchor {
25    subject: DistinguishedName,
26    public_key: PublicKey,
27    subject_key_identifier: Option<KeyIdentifier>,
28}
29
30impl TrustAnchor {
31    /// Build an anchor from a known-good root certificate.
32    ///
33    /// Extracts subject, public key, and (if present) SKI from the cert.
34    /// When the cert lacks a `SubjectKeyIdentifier` extension, the
35    /// anchor matches by DN only.
36    #[must_use]
37    pub fn from_root_cert(root: &MatterCertificate) -> Self {
38        Self {
39            subject: root.subject().clone(),
40            public_key: root.public_key().clone(),
41            subject_key_identifier: root.extensions().subject_key_identifier,
42        }
43    }
44
45    /// Build an anchor from raw fields.
46    ///
47    /// `subject_key_identifier` is optional — when `None`, this anchor
48    /// matches by DN only (the SKI gate is skipped for this anchor).
49    #[must_use]
50    pub fn from_raw(
51        subject: DistinguishedName,
52        public_key: PublicKey,
53        subject_key_identifier: Option<KeyIdentifier>,
54    ) -> Self {
55        Self {
56            subject,
57            public_key,
58            subject_key_identifier,
59        }
60    }
61
62    /// Returns the subject DN of this trust anchor.
63    #[must_use]
64    pub fn subject(&self) -> &DistinguishedName {
65        &self.subject
66    }
67
68    /// Returns the public key of this trust anchor.
69    #[must_use]
70    pub fn public_key(&self) -> &PublicKey {
71        &self.public_key
72    }
73
74    /// Returns the subject key identifier of this trust anchor, if present.
75    #[must_use]
76    pub fn subject_key_identifier(&self) -> Option<&KeyIdentifier> {
77        self.subject_key_identifier.as_ref()
78    }
79}
80
81/// A collection of trusted roots.
82///
83/// Validation succeeds only if the chain anchors against at least
84/// one entry here.
85#[derive(Debug, Clone, Default)]
86pub struct TrustedRoots {
87    anchors: Vec<TrustAnchor>,
88}
89
90impl TrustedRoots {
91    /// Create an empty set of trusted roots.
92    #[must_use]
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// Add a trust anchor to this set.
98    pub fn add(&mut self, anchor: TrustAnchor) {
99        self.anchors.push(anchor);
100    }
101
102    /// Iterate over all trust anchors in this set.
103    pub fn iter(&self) -> impl Iterator<Item = &TrustAnchor> {
104        self.anchors.iter()
105    }
106
107    /// Returns the number of trust anchors in this set.
108    #[must_use]
109    pub fn len(&self) -> usize {
110        self.anchors.len()
111    }
112
113    /// Returns `true` if this set contains no trust anchors.
114    #[must_use]
115    pub fn is_empty(&self) -> bool {
116        self.anchors.is_empty()
117    }
118}
119
120/// A chain of Matter certificates, ordered from leaf to topmost
121/// intermediate. The root itself is supplied separately via
122/// [`TrustedRoots`].
123#[derive(Debug, Clone, Copy)]
124pub struct CertificateChain<'a> {
125    certs: &'a [MatterCertificate],
126}
127
128impl<'a> CertificateChain<'a> {
129    /// Wrap a slice of certs as a chain.
130    ///
131    /// Empty slices are accepted here — [`Self::validate`] is what
132    /// rejects them (with [`Error::UntrustedRoot`]).
133    #[must_use]
134    pub fn new(certs: &'a [MatterCertificate]) -> Self {
135        Self { certs }
136    }
137
138    /// Returns the number of certificates in this chain.
139    #[must_use]
140    pub fn len(&self) -> usize {
141        self.certs.len()
142    }
143
144    /// Returns `true` if this chain contains no certificates.
145    #[must_use]
146    pub fn is_empty(&self) -> bool {
147        self.certs.is_empty()
148    }
149
150    /// Validate the chain against `roots` at the moment `at`.
151    ///
152    /// Returns `Ok(())` iff every per-cert check passes AND the topmost
153    /// cert anchors against at least one entry in `roots`.
154    ///
155    /// # Errors
156    ///
157    /// Returns the most-specific `Error` variant identifying which check
158    /// failed; for per-cert failures the variant carries `cert_index`
159    /// (0 = leaf). [`Error::UntrustedRoot`] is returned for empty chains,
160    /// no matching anchor, or anchor signature failure.
161    /// [`Error::MissingKeyCertSign`] is returned when a non-leaf CA cert
162    /// lacks the `keyCertSign` `KeyUsage` bit, and [`Error::LeafIsCa`] when
163    /// the end-entity leaf asserts `basic_constraints.is_ca = true`.
164    /// Returns any error [`MatterCertificate::to_x509_tbs_der`] returns for
165    /// the top certificate.
166    pub fn validate(&self, roots: &TrustedRoots, at: MatterTime) -> Result<()> {
167        if self.certs.is_empty() {
168            return Err(Error::UntrustedRoot);
169        }
170
171        let len = self.certs.len();
172        for i in 0..len {
173            let cert = &self.certs[i];
174            let i_u8 = u8::try_from(i).unwrap_or(u8::MAX);
175
176            // ---- Time bounds (cheap; fail fast) ----
177            let nb = cert.not_before();
178            let na = cert.not_after();
179            if nb > at {
180                return Err(Error::NotYetValid {
181                    cert_index: i_u8,
182                    not_before: nb,
183                    at,
184                });
185            }
186            if na != MatterTime::NO_EXPIRY && na < at {
187                return Err(Error::Expired {
188                    cert_index: i_u8,
189                    not_after: na,
190                    at,
191                });
192            }
193
194            // ---- CA bit + keyCertSign (above the leaf) ----
195            if i > 0 {
196                let is_ca = cert
197                    .extensions()
198                    .basic_constraints
199                    .as_ref()
200                    .is_some_and(|bc| bc.is_ca);
201                if !is_ca {
202                    return Err(Error::NotACa { cert_index: i_u8 });
203                }
204                // RFC 5280 §4.2.1.3 / Matter §6.5.5: a cert that signs other
205                // certs MUST carry a KeyUsage extension asserting keyCertSign.
206                // An absent KeyUsage, or one without the bit, is not a valid
207                // signing CA.
208                let has_key_cert_sign = cert
209                    .extensions()
210                    .key_usage
211                    .is_some_and(|ku| ku.contains(crate::extensions::KeyUsage::KEY_CERT_SIGN));
212                if !has_key_cert_sign {
213                    return Err(Error::MissingKeyCertSign { cert_index: i_u8 });
214                }
215            } else {
216                // ---- Leaf (index 0): must NOT assert the CA bit ----
217                // RFC 5280 forbids an end-entity cert from asserting is_ca.
218                // An absent basic_constraints extension is permitted; only an
219                // explicit is_ca = true is a violation.
220                let leaf_is_ca = cert
221                    .extensions()
222                    .basic_constraints
223                    .as_ref()
224                    .is_some_and(|bc| bc.is_ca);
225                if leaf_is_ca {
226                    return Err(Error::LeafIsCa);
227                }
228            }
229
230            // ---- Path-length constraint ----
231            if i > 0 {
232                if let Some(plc) = cert
233                    .extensions()
234                    .basic_constraints
235                    .as_ref()
236                    .and_then(|bc| bc.path_len_constraint)
237                {
238                    // Intermediates strictly between this cert and the leaf
239                    // (exclude the leaf at index 0).
240                    let intermediates_below = u8::try_from(i.saturating_sub(1)).unwrap_or(u8::MAX);
241                    if intermediates_below > plc {
242                        return Err(Error::PathLengthExceeded { cert_index: i_u8 });
243                    }
244                }
245            }
246
247            // ---- Issuer / subject linkage + signature (intra-chain) ----
248            if i + 1 < len {
249                let next = &self.certs[i + 1];
250                if cert.issuer() != next.subject() {
251                    return Err(Error::IssuerSubjectMismatch { cert_index: i_u8 });
252                }
253                cert.verify_signed_by(next.public_key())?;
254            }
255        }
256
257        // ---- Anchor the top cert against TrustedRoots ----
258        let top = &self.certs[len - 1];
259        // TBS depends only on `top` — compute once, not per anchor.
260        let top_tbs = top.to_x509_tbs_der()?;
261        for anchor in roots.iter() {
262            if top.issuer() != anchor.subject() {
263                continue;
264            }
265            // Asymmetric SKI gate: only the anchor's SKI controls strictness.
266            // When anchor.SKI is Some(X), the cert MUST present a matching AKI.
267            // When anchor.SKI is None, the gate is skipped (DN-only match).
268            if let Some(anchor_ski) = anchor.subject_key_identifier() {
269                let top_aki = top.extensions().authority_key_identifier;
270                if top_aki != Some(*anchor_ski) {
271                    continue;
272                }
273            }
274            if anchor
275                .public_key()
276                .verify(&top_tbs, top.signature())
277                .is_ok()
278            {
279                return Ok(());
280            }
281        }
282
283        Err(Error::UntrustedRoot)
284    }
285}
286
287#[cfg(test)]
288#[allow(clippy::unwrap_used)] // Test-code carve-out: see CLAUDE.md.
289mod tests {
290    use super::*;
291
292    #[test]
293    fn trusted_roots_default_is_empty() {
294        let roots = TrustedRoots::default();
295        assert!(roots.is_empty());
296        assert_eq!(roots.len(), 0);
297        assert_eq!(roots.iter().count(), 0);
298    }
299
300    #[test]
301    fn certificate_chain_empty_reports_zero_length() {
302        let chain = CertificateChain::new(&[]);
303        assert!(chain.is_empty());
304        assert_eq!(chain.len(), 0);
305    }
306
307    #[test]
308    fn validate_returns_untrusted_root_for_empty_chain() {
309        let roots = TrustedRoots::new();
310        let chain = CertificateChain::new(&[]);
311        let err = chain
312            .validate(&roots, MatterTime::from_unix_secs(1_700_000_000))
313            .unwrap_err();
314        assert!(matches!(err, Error::UntrustedRoot));
315    }
316}