Skip to main content

matter_cert/
builder.rs

1//! Public builder API for [`MatterCertificate`].
2//!
3//! Introduced in M6.3. Splits certificate construction into two stages so
4//! callers can sign with any backend (sync, HSM, OS keychain, offline
5//! ceremony) without the builder depending on a signer trait.
6//!
7//! ```text
8//! MatterCertificate::builder()
9//!     .serial(...).issuer(...).subject(...).validity(...)
10//!     .public_key(...).extensions(...)
11//!     .build_unsigned()?            // Result<UnsignedCertificate, Error>
12//!     .tbs_der()?                   // Result<Vec<u8>, Error>  — bytes to sign
13//!     // ... caller invokes its own ECDSA-P256-SHA256 signer ...
14//!     unsigned.assemble(sig);       // MatterCertificate, infallible
15//! ```
16
17#![forbid(unsafe_code)]
18
19use crate::certificate::MatterCertificate;
20use crate::error::{Error, Result};
21use crate::extensions::Extensions;
22use crate::name::DistinguishedName;
23use crate::public_key::PublicKey;
24use crate::signature::Signature;
25use crate::time::MatterTime;
26
27/// Builder for [`MatterCertificate`]. Construct via
28/// [`MatterCertificate::builder()`].
29#[derive(Debug, Default)]
30pub struct Builder {
31    serial: Option<Vec<u8>>,
32    issuer: Option<DistinguishedName>,
33    not_before: Option<MatterTime>,
34    not_after: Option<MatterTime>,
35    subject: Option<DistinguishedName>,
36    public_key: Option<PublicKey>,
37    extensions: Option<Extensions>,
38}
39
40/// A certificate whose fields are set but whose signature has not yet
41/// been computed. Convert to a signed [`MatterCertificate`] via
42/// [`Self::assemble`] once an external signer produces the 64-byte raw
43/// ECDSA signature over [`Self::tbs_der`].
44#[derive(Debug, Clone)]
45pub struct UnsignedCertificate {
46    serial: Vec<u8>,
47    issuer: DistinguishedName,
48    not_before: MatterTime,
49    not_after: MatterTime,
50    subject: DistinguishedName,
51    public_key: PublicKey,
52    extensions: Extensions,
53}
54
55impl Builder {
56    /// Set the certificate serial number (1..=20 raw bytes per spec §6.5.1).
57    #[must_use]
58    pub fn serial(mut self, serial: Vec<u8>) -> Self {
59        self.serial = Some(serial);
60        self
61    }
62
63    /// Set the issuer DN.
64    #[must_use]
65    pub fn issuer(mut self, dn: DistinguishedName) -> Self {
66        self.issuer = Some(dn);
67        self
68    }
69
70    /// Set the subject DN.
71    #[must_use]
72    pub fn subject(mut self, dn: DistinguishedName) -> Self {
73        self.subject = Some(dn);
74        self
75    }
76
77    /// Set the validity window.
78    #[must_use]
79    pub fn validity(mut self, not_before: MatterTime, not_after: MatterTime) -> Self {
80        self.not_before = Some(not_before);
81        self.not_after = Some(not_after);
82        self
83    }
84
85    /// Set the subject's EC P-256 public key.
86    #[must_use]
87    pub fn public_key(mut self, pk: PublicKey) -> Self {
88        self.public_key = Some(pk);
89        self
90    }
91
92    /// Set the extensions.
93    #[must_use]
94    pub fn extensions(mut self, ext: Extensions) -> Self {
95        self.extensions = Some(ext);
96        self
97    }
98
99    /// Validate that every required field is set and return an
100    /// [`UnsignedCertificate`] ready to be hashed/signed.
101    ///
102    /// # Errors
103    ///
104    /// Returns [`Error::MissingBuilderField`] naming the first missing field, or
105    /// [`Error::FieldValueOutOfRange`] if the serial number is empty or longer
106    /// than the 20-byte maximum required by Matter spec §6.5.1.
107    pub fn build_unsigned(self) -> Result<UnsignedCertificate> {
108        let serial = self.serial.ok_or(Error::MissingBuilderField("serial"))?;
109        if serial.is_empty() || serial.len() > 20 {
110            return Err(Error::FieldValueOutOfRange {
111                tag: crate::tlv_tags::CERT_SERIAL_NUMBER,
112            });
113        }
114        Ok(UnsignedCertificate {
115            serial,
116            issuer: self.issuer.ok_or(Error::MissingBuilderField("issuer"))?,
117            not_before: self
118                .not_before
119                .ok_or(Error::MissingBuilderField("not_before"))?,
120            not_after: self
121                .not_after
122                .ok_or(Error::MissingBuilderField("not_after"))?,
123            subject: self.subject.ok_or(Error::MissingBuilderField("subject"))?,
124            public_key: self
125                .public_key
126                .ok_or(Error::MissingBuilderField("public_key"))?,
127            extensions: self
128                .extensions
129                .ok_or(Error::MissingBuilderField("extensions"))?,
130        })
131    }
132}
133
134impl UnsignedCertificate {
135    /// The certificate's extensions, as set on the builder.
136    #[must_use]
137    pub fn extensions(&self) -> &Extensions {
138        &self.extensions
139    }
140
141    /// The certificate's subject distinguished name, as set on the builder.
142    #[must_use]
143    pub fn subject(&self) -> &DistinguishedName {
144        &self.subject
145    }
146
147    /// The certificate's issuer distinguished name, as set on the builder.
148    #[must_use]
149    pub fn issuer(&self) -> &DistinguishedName {
150        &self.issuer
151    }
152
153    /// Return the X.509 `TBSCertificate` DER bytes that an external signer
154    /// must sign. Byte-identical to matter.js's `Certificate.asUnsignedDer()`.
155    ///
156    /// # Errors
157    ///
158    /// Returns any [`Error`] [`crate::MatterCertificate::to_x509_tbs_der`] would
159    /// return on conversion failure (DN attribute with no defined X.509 OID
160    /// mapping, etc.).
161    pub fn tbs_der(&self) -> Result<Vec<u8>> {
162        // The signature field is not part of the TBS by definition; use a
163        // zero placeholder so we can reuse the existing certificate -> X.509
164        // conversion path without refactoring it for two field shapes.
165        let placeholder = MatterCertificate::from_fields(
166            self.serial.clone(),
167            self.issuer.clone(),
168            self.not_before,
169            self.not_after,
170            self.subject.clone(),
171            self.public_key.clone(),
172            self.extensions.clone(),
173            Signature::new([0u8; 64]),
174        );
175        placeholder.to_x509_tbs_der()
176    }
177
178    /// Combine the unsigned fields with a 64-byte raw ECDSA signature
179    /// (the bytes the signer produced for `self.tbs_der()`).
180    /// Infallible: all fields were validated at `build_unsigned()`.
181    #[must_use]
182    pub fn assemble(self, signature: [u8; 64]) -> MatterCertificate {
183        MatterCertificate::from_fields(
184            self.serial,
185            self.issuer,
186            self.not_before,
187            self.not_after,
188            self.subject,
189            self.public_key,
190            self.extensions,
191            Signature::new(signature),
192        )
193    }
194}
195
196impl MatterCertificate {
197    /// Begin constructing a new certificate.
198    #[must_use]
199    pub fn builder() -> Builder {
200        Builder::default()
201    }
202}
203
204#[cfg(test)]
205#[allow(clippy::unwrap_used, clippy::cast_possible_truncation)] // Test-code carve-out: see CLAUDE.md.
206mod tests {
207    use super::*;
208    use crate::extensions::{BasicConstraints, Extensions};
209    use crate::name::DnAttribute;
210
211    fn sample_public_key() -> PublicKey {
212        let mut key_bytes = [0u8; 65];
213        key_bytes[0] = 0x04;
214        // Body bytes don't matter for builder roundtrips (no signature math here).
215        for (i, b) in key_bytes.iter_mut().enumerate().skip(1) {
216            *b = i as u8;
217        }
218        PublicKey::new(key_bytes).unwrap()
219    }
220
221    #[test]
222    fn build_unsigned_then_assemble_roundtrips() {
223        let pk = sample_public_key();
224        let unsigned = MatterCertificate::builder()
225            .serial(vec![1, 2, 3])
226            .issuer(DistinguishedName::new(vec![DnAttribute::RcacId(1)]))
227            .subject(DistinguishedName::new(vec![
228                DnAttribute::FabricId(7),
229                DnAttribute::NodeId(42),
230            ]))
231            .validity(MatterTime(1_000), MatterTime::NO_EXPIRY)
232            .public_key(pk.clone())
233            .extensions(Extensions {
234                basic_constraints: Some(BasicConstraints {
235                    is_ca: false,
236                    path_len_constraint: None,
237                }),
238                ..Default::default()
239            })
240            .build_unsigned()
241            .unwrap();
242
243        // tbs_der must succeed (DN attrs are all in the typed range).
244        let tbs = unsigned.tbs_der().unwrap();
245        assert!(!tbs.is_empty(), "TBS DER must be non-empty");
246
247        let cert = unsigned.assemble([0xAB; 64]);
248        // The assembled cert round-trips through TLV.
249        let tlv = cert.to_tlv().unwrap();
250        let parsed = MatterCertificate::from_tlv(&tlv).unwrap();
251        assert_eq!(parsed, cert);
252        // TBS produced by the unsigned helper must match what the assembled
253        // cert produces — catches a future regression where the two paths diverge.
254        assert_eq!(
255            tbs,
256            cert.to_x509_tbs_der().unwrap(),
257            "TBS from unsigned must match TBS from assembled cert"
258        );
259    }
260
261    #[test]
262    fn build_unsigned_fails_on_missing_serial() {
263        let err = MatterCertificate::builder()
264            .issuer(DistinguishedName::new(vec![DnAttribute::RcacId(1)]))
265            .subject(DistinguishedName::new(vec![DnAttribute::NodeId(42)]))
266            .validity(MatterTime(1_000), MatterTime::NO_EXPIRY)
267            .public_key(sample_public_key())
268            .extensions(Extensions::default())
269            .build_unsigned()
270            .unwrap_err();
271        assert!(
272            matches!(err, Error::MissingBuilderField("serial")),
273            "got: {err:?}"
274        );
275    }
276
277    #[test]
278    fn build_unsigned_fails_on_missing_subject() {
279        let err = MatterCertificate::builder()
280            .serial(vec![1])
281            .issuer(DistinguishedName::new(vec![DnAttribute::RcacId(1)]))
282            .validity(MatterTime(1_000), MatterTime::NO_EXPIRY)
283            .public_key(sample_public_key())
284            .extensions(Extensions::default())
285            .build_unsigned()
286            .unwrap_err();
287        assert!(
288            matches!(err, Error::MissingBuilderField("subject")),
289            "got: {err:?}"
290        );
291    }
292
293    #[test]
294    fn build_unsigned_rejects_oversized_serial() {
295        let err = MatterCertificate::builder()
296            .serial(vec![0u8; 21])
297            .issuer(DistinguishedName::new(vec![DnAttribute::RcacId(1)]))
298            .subject(DistinguishedName::new(vec![DnAttribute::NodeId(42)]))
299            .validity(MatterTime(1_000), MatterTime::NO_EXPIRY)
300            .public_key(sample_public_key())
301            .extensions(Extensions::default())
302            .build_unsigned()
303            .unwrap_err();
304        assert!(
305            matches!(err, Error::FieldValueOutOfRange { .. }),
306            "got: {err:?}"
307        );
308    }
309
310    #[test]
311    fn build_unsigned_rejects_empty_serial() {
312        let err = MatterCertificate::builder()
313            .serial(vec![])
314            .issuer(DistinguishedName::new(vec![DnAttribute::RcacId(1)]))
315            .subject(DistinguishedName::new(vec![DnAttribute::NodeId(42)]))
316            .validity(MatterTime(1_000), MatterTime::NO_EXPIRY)
317            .public_key(sample_public_key())
318            .extensions(Extensions::default())
319            .build_unsigned()
320            .unwrap_err();
321        assert!(
322            matches!(err, Error::FieldValueOutOfRange { .. }),
323            "got: {err:?}"
324        );
325    }
326}