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    pub fn validate(&self, roots: &TrustedRoots, at: MatterTime) -> Result<()> {
165        if self.certs.is_empty() {
166            return Err(Error::UntrustedRoot);
167        }
168
169        let len = self.certs.len();
170        for i in 0..len {
171            let cert = &self.certs[i];
172            let i_u8 = u8::try_from(i).unwrap_or(u8::MAX);
173
174            // ---- Time bounds (cheap; fail fast) ----
175            let nb = cert.not_before();
176            let na = cert.not_after();
177            if nb > at {
178                return Err(Error::NotYetValid {
179                    cert_index: i_u8,
180                    not_before: nb,
181                    at,
182                });
183            }
184            if na != MatterTime::NO_EXPIRY && na < at {
185                return Err(Error::Expired {
186                    cert_index: i_u8,
187                    not_after: na,
188                    at,
189                });
190            }
191
192            // ---- CA bit + keyCertSign (above the leaf) ----
193            if i > 0 {
194                let is_ca = cert
195                    .extensions()
196                    .basic_constraints
197                    .as_ref()
198                    .is_some_and(|bc| bc.is_ca);
199                if !is_ca {
200                    return Err(Error::NotACa { cert_index: i_u8 });
201                }
202                // RFC 5280 §4.2.1.3 / Matter §6.5.5: a cert that signs other
203                // certs MUST carry a KeyUsage extension asserting keyCertSign.
204                // An absent KeyUsage, or one without the bit, is not a valid
205                // signing CA.
206                let has_key_cert_sign = cert
207                    .extensions()
208                    .key_usage
209                    .is_some_and(|ku| ku.contains(crate::extensions::KeyUsage::KEY_CERT_SIGN));
210                if !has_key_cert_sign {
211                    return Err(Error::MissingKeyCertSign { cert_index: i_u8 });
212                }
213            } else {
214                // ---- Leaf (index 0): must NOT assert the CA bit ----
215                // RFC 5280 forbids an end-entity cert from asserting is_ca.
216                // An absent basic_constraints extension is permitted; only an
217                // explicit is_ca = true is a violation.
218                let leaf_is_ca = cert
219                    .extensions()
220                    .basic_constraints
221                    .as_ref()
222                    .is_some_and(|bc| bc.is_ca);
223                if leaf_is_ca {
224                    return Err(Error::LeafIsCa);
225                }
226            }
227
228            // ---- Path-length constraint ----
229            if i > 0 {
230                if let Some(plc) = cert
231                    .extensions()
232                    .basic_constraints
233                    .as_ref()
234                    .and_then(|bc| bc.path_len_constraint)
235                {
236                    // Intermediates strictly between this cert and the leaf
237                    // (exclude the leaf at index 0).
238                    let intermediates_below = u8::try_from(i.saturating_sub(1)).unwrap_or(u8::MAX);
239                    if intermediates_below > plc {
240                        return Err(Error::PathLengthExceeded { cert_index: i_u8 });
241                    }
242                }
243            }
244
245            // ---- Issuer / subject linkage + signature (intra-chain) ----
246            if i + 1 < len {
247                let next = &self.certs[i + 1];
248                if cert.issuer() != next.subject() {
249                    return Err(Error::IssuerSubjectMismatch { cert_index: i_u8 });
250                }
251                cert.verify_signed_by(next.public_key())?;
252            }
253        }
254
255        // ---- Anchor the top cert against TrustedRoots ----
256        let top = &self.certs[len - 1];
257        for anchor in roots.iter() {
258            if top.issuer() != anchor.subject() {
259                continue;
260            }
261            // Asymmetric SKI gate: only the anchor's SKI controls strictness.
262            // When anchor.SKI is Some(X), the cert MUST present a matching AKI.
263            // When anchor.SKI is None, the gate is skipped (DN-only match).
264            if let Some(anchor_ski) = anchor.subject_key_identifier() {
265                let top_aki = top.extensions().authority_key_identifier;
266                if top_aki != Some(*anchor_ski) {
267                    continue;
268                }
269            }
270            if top.verify_signed_by(anchor.public_key()).is_ok() {
271                return Ok(());
272            }
273        }
274
275        Err(Error::UntrustedRoot)
276    }
277}
278
279#[cfg(test)]
280#[allow(clippy::unwrap_used)] // Test-code carve-out: see CLAUDE.md.
281mod tests {
282    use super::*;
283
284    #[test]
285    fn trusted_roots_default_is_empty() {
286        let roots = TrustedRoots::default();
287        assert!(roots.is_empty());
288        assert_eq!(roots.len(), 0);
289        assert_eq!(roots.iter().count(), 0);
290    }
291
292    #[test]
293    fn certificate_chain_empty_reports_zero_length() {
294        let chain = CertificateChain::new(&[]);
295        assert!(chain.is_empty());
296        assert_eq!(chain.len(), 0);
297    }
298
299    #[test]
300    fn validate_returns_untrusted_root_for_empty_chain() {
301        let roots = TrustedRoots::new();
302        let chain = CertificateChain::new(&[]);
303        let err = chain
304            .validate(&roots, MatterTime::from_unix_secs(1_700_000_000))
305            .unwrap_err();
306        assert!(matches!(err, Error::UntrustedRoot));
307    }
308}