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    /// Return the X.509 `TBSCertificate` DER bytes that an external signer
136    /// must sign. Byte-identical to matter.js's `Certificate.asUnsignedDer()`.
137    ///
138    /// # Errors
139    ///
140    /// Returns any [`Error`] [`crate::MatterCertificate::to_x509_tbs_der`] would
141    /// return on conversion failure (DN attribute with no defined X.509 OID
142    /// mapping, etc.).
143    pub fn tbs_der(&self) -> Result<Vec<u8>> {
144        // The signature field is not part of the TBS by definition; use a
145        // zero placeholder so we can reuse the existing certificate -> X.509
146        // conversion path without refactoring it for two field shapes.
147        let placeholder = MatterCertificate::from_fields(
148            self.serial.clone(),
149            self.issuer.clone(),
150            self.not_before,
151            self.not_after,
152            self.subject.clone(),
153            self.public_key.clone(),
154            self.extensions.clone(),
155            Signature::new([0u8; 64]),
156        );
157        placeholder.to_x509_tbs_der()
158    }
159
160    /// Combine the unsigned fields with a 64-byte raw ECDSA signature
161    /// (the bytes the signer produced for `self.tbs_der()`).
162    /// Infallible: all fields were validated at `build_unsigned()`.
163    #[must_use]
164    pub fn assemble(self, signature: [u8; 64]) -> MatterCertificate {
165        MatterCertificate::from_fields(
166            self.serial,
167            self.issuer,
168            self.not_before,
169            self.not_after,
170            self.subject,
171            self.public_key,
172            self.extensions,
173            Signature::new(signature),
174        )
175    }
176}
177
178impl MatterCertificate {
179    /// Begin constructing a new certificate.
180    #[must_use]
181    pub fn builder() -> Builder {
182        Builder::default()
183    }
184}
185
186#[cfg(test)]
187#[allow(clippy::unwrap_used, clippy::cast_possible_truncation)] // Test-code carve-out: see CLAUDE.md.
188mod tests {
189    use super::*;
190    use crate::extensions::{BasicConstraints, Extensions};
191    use crate::name::DnAttribute;
192
193    fn sample_public_key() -> PublicKey {
194        let mut key_bytes = [0u8; 65];
195        key_bytes[0] = 0x04;
196        // Body bytes don't matter for builder roundtrips (no signature math here).
197        for (i, b) in key_bytes.iter_mut().enumerate().skip(1) {
198            *b = i as u8;
199        }
200        PublicKey::new(key_bytes).unwrap()
201    }
202
203    #[test]
204    fn build_unsigned_then_assemble_roundtrips() {
205        let pk = sample_public_key();
206        let unsigned = MatterCertificate::builder()
207            .serial(vec![1, 2, 3])
208            .issuer(DistinguishedName::new(vec![DnAttribute::RcacId(1)]))
209            .subject(DistinguishedName::new(vec![
210                DnAttribute::FabricId(7),
211                DnAttribute::NodeId(42),
212            ]))
213            .validity(MatterTime(1_000), MatterTime::NO_EXPIRY)
214            .public_key(pk.clone())
215            .extensions(Extensions {
216                basic_constraints: Some(BasicConstraints {
217                    is_ca: false,
218                    path_len_constraint: None,
219                }),
220                ..Default::default()
221            })
222            .build_unsigned()
223            .unwrap();
224
225        // tbs_der must succeed (DN attrs are all in the typed range).
226        let tbs = unsigned.tbs_der().unwrap();
227        assert!(!tbs.is_empty(), "TBS DER must be non-empty");
228
229        let cert = unsigned.assemble([0xAB; 64]);
230        // The assembled cert round-trips through TLV.
231        let tlv = cert.to_tlv().unwrap();
232        let parsed = MatterCertificate::from_tlv(&tlv).unwrap();
233        assert_eq!(parsed, cert);
234        // TBS produced by the unsigned helper must match what the assembled
235        // cert produces — catches a future regression where the two paths diverge.
236        assert_eq!(
237            tbs,
238            cert.to_x509_tbs_der().unwrap(),
239            "TBS from unsigned must match TBS from assembled cert"
240        );
241    }
242
243    #[test]
244    fn build_unsigned_fails_on_missing_serial() {
245        let err = MatterCertificate::builder()
246            .issuer(DistinguishedName::new(vec![DnAttribute::RcacId(1)]))
247            .subject(DistinguishedName::new(vec![DnAttribute::NodeId(42)]))
248            .validity(MatterTime(1_000), MatterTime::NO_EXPIRY)
249            .public_key(sample_public_key())
250            .extensions(Extensions::default())
251            .build_unsigned()
252            .unwrap_err();
253        assert!(
254            matches!(err, Error::MissingBuilderField("serial")),
255            "got: {err:?}"
256        );
257    }
258
259    #[test]
260    fn build_unsigned_fails_on_missing_subject() {
261        let err = MatterCertificate::builder()
262            .serial(vec![1])
263            .issuer(DistinguishedName::new(vec![DnAttribute::RcacId(1)]))
264            .validity(MatterTime(1_000), MatterTime::NO_EXPIRY)
265            .public_key(sample_public_key())
266            .extensions(Extensions::default())
267            .build_unsigned()
268            .unwrap_err();
269        assert!(
270            matches!(err, Error::MissingBuilderField("subject")),
271            "got: {err:?}"
272        );
273    }
274
275    #[test]
276    fn build_unsigned_rejects_oversized_serial() {
277        let err = MatterCertificate::builder()
278            .serial(vec![0u8; 21])
279            .issuer(DistinguishedName::new(vec![DnAttribute::RcacId(1)]))
280            .subject(DistinguishedName::new(vec![DnAttribute::NodeId(42)]))
281            .validity(MatterTime(1_000), MatterTime::NO_EXPIRY)
282            .public_key(sample_public_key())
283            .extensions(Extensions::default())
284            .build_unsigned()
285            .unwrap_err();
286        assert!(
287            matches!(err, Error::FieldValueOutOfRange { .. }),
288            "got: {err:?}"
289        );
290    }
291
292    #[test]
293    fn build_unsigned_rejects_empty_serial() {
294        let err = MatterCertificate::builder()
295            .serial(vec![])
296            .issuer(DistinguishedName::new(vec![DnAttribute::RcacId(1)]))
297            .subject(DistinguishedName::new(vec![DnAttribute::NodeId(42)]))
298            .validity(MatterTime(1_000), MatterTime::NO_EXPIRY)
299            .public_key(sample_public_key())
300            .extensions(Extensions::default())
301            .build_unsigned()
302            .unwrap_err();
303        assert!(
304            matches!(err, Error::FieldValueOutOfRange { .. }),
305            "got: {err:?}"
306        );
307    }
308}