Skip to main content

matter_cert/
extensions.rs

1//! Matter certificate extensions.
2//!
3//! The spec (§6.5.4) defines exactly five extensions: basic-constraints,
4//! key-usage, extended-key-usage, subject-key-identifier, authority-
5//! key-identifier. Each appears at most once in the extensions list;
6//! absent extensions deserialise to `None`. Wire-format unknown
7//! extension tags are rejected as `Error::WrongFieldType` because the
8//! spec does not permit additions today.
9
10use bitflags::bitflags;
11use matter_codec::{Element, Tag, TlvReader, TlvWriter, Value};
12
13use crate::error::{Error, Result};
14use crate::tlv_tags as tags;
15
16/// Decoded certificate extensions.
17///
18/// `#[non_exhaustive]`: the spec (§6.5.4) defines exactly five extensions
19/// today, but marking this prevents a future spec-driven addition from being
20/// a semver-breaking change for downstream crates. Outside `matter-cert`,
21/// construct via [`Extensions::builder`] (or [`Extensions::default`]) rather
22/// than a struct literal.
23#[derive(Debug, Clone, PartialEq, Eq, Default)]
24#[non_exhaustive]
25pub struct Extensions {
26    /// Basic constraints extension, if present.
27    pub basic_constraints: Option<BasicConstraints>,
28    /// Key usage flags, if present.
29    pub key_usage: Option<KeyUsage>,
30    /// Extended key usage OID list, if present.
31    pub extended_key_usage: Option<Vec<u32>>,
32    /// Subject key identifier (20-byte SHA-1 of public key), if present.
33    pub subject_key_identifier: Option<KeyIdentifier>,
34    /// Authority key identifier (20-byte SHA-1 of issuer public key), if present.
35    pub authority_key_identifier: Option<KeyIdentifier>,
36}
37
38/// Builder for [`Extensions`] (the supported construction path for downstream
39/// crates, since [`Extensions`] is `#[non_exhaustive]`).
40///
41/// Each setter takes the already-wrapped `Option`, mirroring the struct
42/// fields one-to-one; unset fields remain `None`.
43#[derive(Debug, Clone, Default)]
44pub struct ExtensionsBuilder(Extensions);
45
46impl ExtensionsBuilder {
47    /// Set the basic-constraints extension.
48    #[must_use]
49    pub fn basic_constraints(mut self, v: Option<BasicConstraints>) -> Self {
50        self.0.basic_constraints = v;
51        self
52    }
53
54    /// Set the key-usage extension.
55    #[must_use]
56    pub fn key_usage(mut self, v: Option<KeyUsage>) -> Self {
57        self.0.key_usage = v;
58        self
59    }
60
61    /// Set the extended-key-usage OID list.
62    #[must_use]
63    pub fn extended_key_usage(mut self, v: Option<Vec<u32>>) -> Self {
64        self.0.extended_key_usage = v;
65        self
66    }
67
68    /// Set the subject-key-identifier extension.
69    #[must_use]
70    pub fn subject_key_identifier(mut self, v: Option<KeyIdentifier>) -> Self {
71        self.0.subject_key_identifier = v;
72        self
73    }
74
75    /// Set the authority-key-identifier extension.
76    #[must_use]
77    pub fn authority_key_identifier(mut self, v: Option<KeyIdentifier>) -> Self {
78        self.0.authority_key_identifier = v;
79        self
80    }
81
82    /// Finish building.
83    #[must_use]
84    pub fn build(self) -> Extensions {
85        self.0
86    }
87}
88
89/// Basic constraints extension.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91#[non_exhaustive]
92pub struct BasicConstraints {
93    /// Whether this certificate's subject may sign other certificates.
94    pub is_ca: bool,
95    /// Maximum number of intermediate certificates that may follow.
96    /// `None` means unbounded (or no constraint set).
97    pub path_len_constraint: Option<u8>,
98}
99
100impl BasicConstraints {
101    /// Construct a [`BasicConstraints`] extension.
102    ///
103    /// Provided because the struct is `#[non_exhaustive]`: callers in other
104    /// crates cannot use a struct literal, so this constructor is the stable
105    /// way to build one. Any future spec-driven field will gain a default
106    /// here without breaking existing callers.
107    #[must_use]
108    pub const fn new(is_ca: bool, path_len_constraint: Option<u8>) -> Self {
109        Self {
110            is_ca,
111            path_len_constraint,
112        }
113    }
114}
115
116bitflags! {
117    /// Matter spec §6.5.4 key-usage bits. Identical layout to X.509
118    /// KeyUsage but only the bits the spec defines are surfaced.
119    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
120    pub struct KeyUsage: u16 {
121        /// Subject's public key is used for verifying digital signatures.
122        const DIGITAL_SIGNATURE  = 0x0001;
123        /// Subject's public key is used only for verifying signatures on
124        /// certificates and revocation information.
125        const CONTENT_COMMITMENT = 0x0002;
126        /// Subject's public key is used for enciphering private or secret keys.
127        const KEY_ENCIPHERMENT   = 0x0004;
128        /// Subject's public key is used for directly enciphering raw user data.
129        const DATA_ENCIPHERMENT  = 0x0008;
130        /// Subject's public key is used for key agreement.
131        const KEY_AGREEMENT      = 0x0010;
132        /// Subject's public key is used for verifying signatures on public key
133        /// certificates.
134        const KEY_CERT_SIGN      = 0x0020;
135        /// Subject's public key is used for verifying signatures on certificate
136        /// revocation lists.
137        const CRL_SIGN           = 0x0040;
138        /// When used with `KEY_AGREEMENT`, the subject's public key may only be
139        /// used for enciphering data during key agreement.
140        const ENCIPHER_ONLY      = 0x0080;
141        /// When used with `KEY_AGREEMENT`, the subject's public key may only be
142        /// used for deciphering data during key agreement.
143        const DECIPHER_ONLY      = 0x0100;
144    }
145}
146
147/// 20-byte key identifier (Subject Key Identifier or Authority Key
148/// Identifier per spec §6.5.4).
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
150pub struct KeyIdentifier(pub [u8; 20]);
151
152impl KeyIdentifier {
153    /// Construct from a byte slice; rejects wrong-length input.
154    ///
155    /// # Errors
156    ///
157    /// Returns [`Error::WrongKeyIdentifierLength`] if `slice` is not exactly
158    /// 20 bytes.
159    pub fn from_slice(slice: &[u8]) -> Result<Self> {
160        let bytes: [u8; 20] = slice
161            .try_into()
162            .map_err(|_| Error::WrongKeyIdentifierLength(slice.len()))?;
163        Ok(Self(bytes))
164    }
165}
166
167impl Extensions {
168    /// Start building an [`Extensions`] value.
169    ///
170    /// Because [`Extensions`] is `#[non_exhaustive]`, downstream crates cannot
171    /// build it with a struct literal; this builder is the supported path.
172    /// Unset fields default to `None`.
173    #[must_use]
174    pub fn builder() -> ExtensionsBuilder {
175        ExtensionsBuilder(Self::default())
176    }
177
178    /// Read the extensions list (positioned BEFORE its `ContainerStart`).
179    ///
180    /// # Errors
181    ///
182    /// Returns an error if the TLV is malformed, a required type is wrong,
183    /// a field is duplicated, or an unknown extension tag appears.
184    // Used only in tests — suppress dead_code lint.
185    #[allow(dead_code)]
186    pub(crate) fn read(reader: &mut TlvReader<'_>) -> Result<Self> {
187        match reader.next()? {
188            Some(Element::ContainerStart {
189                kind: matter_codec::ContainerKind::List,
190                ..
191            }) => {}
192            _ => return Err(Error::WrongFieldType(tags::CERT_EXTENSIONS)),
193        }
194        Self::read_from_open_list(reader)
195    }
196
197    /// Read extensions where the `ContainerStart` is already consumed.
198    /// Used by `MatterCertificate::from_tlv` which has dispatched on
199    /// the extensions context tag via its own `next()` call.
200    ///
201    /// # Errors
202    ///
203    /// Returns an error if the TLV is malformed, a required type is wrong,
204    /// a field is duplicated, or an unknown extension tag appears.
205    pub(crate) fn read_from_open_list(reader: &mut TlvReader<'_>) -> Result<Self> {
206        let mut out = Self::default();
207        loop {
208            match reader.next()? {
209                None => return Err(matter_codec::Error::UnclosedContainer.into()),
210                Some(Element::ContainerEnd) => break,
211                Some(Element::ContainerStart {
212                    tag: Tag::Context(t),
213                    kind: matter_codec::ContainerKind::Structure,
214                }) if t == tags::EXT_BASIC_CONSTRAINTS => {
215                    if out.basic_constraints.is_some() {
216                        return Err(Error::DuplicateField(t));
217                    }
218                    out.basic_constraints = Some(BasicConstraints::read_body(reader)?);
219                }
220                Some(Element::Scalar {
221                    tag: Tag::Context(t),
222                    value: Value::Uint(bits),
223                }) if t == tags::EXT_KEY_USAGE => {
224                    if out.key_usage.is_some() {
225                        return Err(Error::DuplicateField(t));
226                    }
227                    let bits16 =
228                        u16::try_from(bits).map_err(|_| Error::FieldValueOutOfRange { tag: t })?;
229                    out.key_usage = Some(KeyUsage::from_bits_truncate(bits16));
230                }
231                Some(Element::ContainerStart {
232                    tag: Tag::Context(t),
233                    kind: matter_codec::ContainerKind::Array,
234                }) if t == tags::EXT_EXTENDED_KEY_USAGE => {
235                    if out.extended_key_usage.is_some() {
236                        return Err(Error::DuplicateField(t));
237                    }
238                    out.extended_key_usage = Some(read_uint_array(reader)?);
239                }
240                Some(Element::Scalar {
241                    tag: Tag::Context(t),
242                    value: Value::Bytes(b),
243                }) if t == tags::EXT_SUBJECT_KEY_IDENTIFIER => {
244                    if out.subject_key_identifier.is_some() {
245                        return Err(Error::DuplicateField(t));
246                    }
247                    out.subject_key_identifier = Some(KeyIdentifier::from_slice(&b)?);
248                }
249                Some(Element::Scalar {
250                    tag: Tag::Context(t),
251                    value: Value::Bytes(b),
252                }) if t == tags::EXT_AUTHORITY_KEY_IDENTIFIER => {
253                    if out.authority_key_identifier.is_some() {
254                        return Err(Error::DuplicateField(t));
255                    }
256                    out.authority_key_identifier = Some(KeyIdentifier::from_slice(&b)?);
257                }
258                Some(elem) => {
259                    let tag_num = element_tag_number(&elem);
260                    return Err(Error::WrongFieldType(tag_num));
261                }
262            }
263        }
264        Ok(out)
265    }
266
267    /// Write the extensions list under `outer_tag`.
268    ///
269    /// # Errors
270    ///
271    /// Propagates any [`matter_codec::Error`] from the underlying writer.
272    pub(crate) fn write(&self, writer: &mut TlvWriter<'_>, outer_tag: Tag) -> Result<()> {
273        writer.start_list(outer_tag)?;
274        if let Some(bc) = self.basic_constraints {
275            bc.write_body(writer, Tag::Context(tags::EXT_BASIC_CONSTRAINTS))?;
276        }
277        if let Some(ku) = self.key_usage {
278            writer.put_uint(Tag::Context(tags::EXT_KEY_USAGE), u64::from(ku.bits()))?;
279        }
280        if let Some(eku) = &self.extended_key_usage {
281            writer.start_array(Tag::Context(tags::EXT_EXTENDED_KEY_USAGE))?;
282            for oid in eku {
283                writer.put_uint(Tag::Anonymous, u64::from(*oid))?;
284            }
285            writer.end_container()?;
286        }
287        if let Some(ski) = &self.subject_key_identifier {
288            writer.put_bytes(Tag::Context(tags::EXT_SUBJECT_KEY_IDENTIFIER), &ski.0)?;
289        }
290        if let Some(aki) = &self.authority_key_identifier {
291            writer.put_bytes(Tag::Context(tags::EXT_AUTHORITY_KEY_IDENTIFIER), &aki.0)?;
292        }
293        writer.end_container()?;
294        Ok(())
295    }
296}
297
298impl BasicConstraints {
299    fn read_body(reader: &mut TlvReader<'_>) -> Result<Self> {
300        let mut is_ca = false;
301        let mut path_len = None;
302        loop {
303            match reader.next()? {
304                None => return Err(matter_codec::Error::UnclosedContainer.into()),
305                Some(Element::ContainerEnd) => break,
306                Some(Element::Scalar {
307                    tag: Tag::Context(t),
308                    value: Value::Bool(b),
309                }) if t == tags::BC_IS_CA => {
310                    is_ca = b;
311                }
312                Some(Element::Scalar {
313                    tag: Tag::Context(t),
314                    value: Value::Uint(v),
315                }) if t == tags::BC_PATH_LEN_CONSTRAINT => {
316                    let v8 = u8::try_from(v).map_err(|_| Error::FieldValueOutOfRange { tag: t })?;
317                    path_len = Some(v8);
318                }
319                Some(_) => {
320                    return Err(Error::WrongFieldType(tags::EXT_BASIC_CONSTRAINTS));
321                }
322            }
323        }
324        Ok(Self {
325            is_ca,
326            path_len_constraint: path_len,
327        })
328    }
329
330    // `BasicConstraints` is `Copy` (bool + Option<u8>); take by value.
331    fn write_body(self, writer: &mut TlvWriter<'_>, outer_tag: Tag) -> Result<()> {
332        writer.start_structure(outer_tag)?;
333        writer.put_bool(Tag::Context(tags::BC_IS_CA), self.is_ca)?;
334        if let Some(plc) = self.path_len_constraint {
335            writer.put_uint(Tag::Context(tags::BC_PATH_LEN_CONSTRAINT), u64::from(plc))?;
336        }
337        writer.end_container()?;
338        Ok(())
339    }
340}
341
342// Used by Extensions::read_from_open_list.
343fn read_uint_array(reader: &mut TlvReader<'_>) -> Result<Vec<u32>> {
344    let mut out = Vec::new();
345    loop {
346        match reader.next()? {
347            None => return Err(matter_codec::Error::UnclosedContainer.into()),
348            Some(Element::ContainerEnd) => break,
349            Some(Element::Scalar {
350                value: Value::Uint(v),
351                ..
352            }) => {
353                let v32 = u32::try_from(v).map_err(|_| Error::FieldValueOutOfRange {
354                    tag: tags::EXT_EXTENDED_KEY_USAGE,
355                })?;
356                out.push(v32);
357            }
358            Some(_) => return Err(Error::WrongFieldType(tags::EXT_EXTENDED_KEY_USAGE)),
359        }
360    }
361    Ok(out)
362}
363
364// Used by Extensions::read_from_open_list.
365fn element_tag_number(elem: &Element) -> u8 {
366    match elem {
367        Element::Scalar { tag, .. } | Element::ContainerStart { tag, .. } => match tag {
368            Tag::Context(n) => *n,
369            _ => 0,
370        },
371        // ContainerEnd and any future non_exhaustive variants: return 0.
372        _ => 0,
373    }
374}
375
376#[cfg(test)]
377#[allow(clippy::unwrap_used)] // Test-code carve-out.
378mod tests {
379    use super::*;
380
381    fn round_trip(ext: &Extensions) {
382        let mut buf = Vec::new();
383        {
384            let mut w = TlvWriter::new(&mut buf);
385            ext.write(&mut w, Tag::Anonymous).unwrap();
386        }
387        let mut r = TlvReader::new(&buf);
388        let parsed = Extensions::read(&mut r).unwrap();
389        assert_eq!(parsed, *ext);
390    }
391
392    #[test]
393    fn round_trip_empty() {
394        round_trip(&Extensions::default());
395    }
396
397    #[test]
398    fn round_trip_basic_constraints_only() {
399        round_trip(&Extensions {
400            basic_constraints: Some(BasicConstraints {
401                is_ca: true,
402                path_len_constraint: Some(3),
403            }),
404            ..Default::default()
405        });
406    }
407
408    #[test]
409    fn round_trip_key_usage_only() {
410        round_trip(&Extensions {
411            key_usage: Some(KeyUsage::DIGITAL_SIGNATURE | KeyUsage::KEY_CERT_SIGN),
412            ..Default::default()
413        });
414    }
415
416    #[test]
417    fn round_trip_extended_key_usage_only() {
418        round_trip(&Extensions {
419            extended_key_usage: Some(vec![1, 2, 3]),
420            ..Default::default()
421        });
422    }
423
424    #[test]
425    fn round_trip_key_identifiers() {
426        round_trip(&Extensions {
427            subject_key_identifier: Some(KeyIdentifier([0xAB; 20])),
428            authority_key_identifier: Some(KeyIdentifier([0xCD; 20])),
429            ..Default::default()
430        });
431    }
432
433    #[test]
434    fn round_trip_all_extensions() {
435        round_trip(&Extensions {
436            basic_constraints: Some(BasicConstraints {
437                is_ca: true,
438                path_len_constraint: None,
439            }),
440            key_usage: Some(KeyUsage::DIGITAL_SIGNATURE),
441            extended_key_usage: Some(vec![42]),
442            subject_key_identifier: Some(KeyIdentifier([0x11; 20])),
443            authority_key_identifier: Some(KeyIdentifier([0x22; 20])),
444        });
445    }
446}