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}