#![forbid(unsafe_code)]
use crate::certificate::MatterCertificate;
use crate::error::{Error, Result};
use crate::extensions::Extensions;
use crate::name::DistinguishedName;
use crate::public_key::PublicKey;
use crate::signature::Signature;
use crate::time::MatterTime;
#[derive(Debug, Default)]
pub struct Builder {
serial: Option<Vec<u8>>,
issuer: Option<DistinguishedName>,
not_before: Option<MatterTime>,
not_after: Option<MatterTime>,
subject: Option<DistinguishedName>,
public_key: Option<PublicKey>,
extensions: Option<Extensions>,
}
#[derive(Debug, Clone)]
pub struct UnsignedCertificate {
serial: Vec<u8>,
issuer: DistinguishedName,
not_before: MatterTime,
not_after: MatterTime,
subject: DistinguishedName,
public_key: PublicKey,
extensions: Extensions,
}
impl Builder {
#[must_use]
pub fn serial(mut self, serial: Vec<u8>) -> Self {
self.serial = Some(serial);
self
}
#[must_use]
pub fn issuer(mut self, dn: DistinguishedName) -> Self {
self.issuer = Some(dn);
self
}
#[must_use]
pub fn subject(mut self, dn: DistinguishedName) -> Self {
self.subject = Some(dn);
self
}
#[must_use]
pub fn validity(mut self, not_before: MatterTime, not_after: MatterTime) -> Self {
self.not_before = Some(not_before);
self.not_after = Some(not_after);
self
}
#[must_use]
pub fn public_key(mut self, pk: PublicKey) -> Self {
self.public_key = Some(pk);
self
}
#[must_use]
pub fn extensions(mut self, ext: Extensions) -> Self {
self.extensions = Some(ext);
self
}
pub fn build_unsigned(self) -> Result<UnsignedCertificate> {
let serial = self.serial.ok_or(Error::MissingBuilderField("serial"))?;
if serial.is_empty() || serial.len() > 20 {
return Err(Error::FieldValueOutOfRange {
tag: crate::tlv_tags::CERT_SERIAL_NUMBER,
});
}
Ok(UnsignedCertificate {
serial,
issuer: self.issuer.ok_or(Error::MissingBuilderField("issuer"))?,
not_before: self
.not_before
.ok_or(Error::MissingBuilderField("not_before"))?,
not_after: self
.not_after
.ok_or(Error::MissingBuilderField("not_after"))?,
subject: self.subject.ok_or(Error::MissingBuilderField("subject"))?,
public_key: self
.public_key
.ok_or(Error::MissingBuilderField("public_key"))?,
extensions: self
.extensions
.ok_or(Error::MissingBuilderField("extensions"))?,
})
}
}
impl UnsignedCertificate {
#[must_use]
pub fn extensions(&self) -> &Extensions {
&self.extensions
}
#[must_use]
pub fn subject(&self) -> &DistinguishedName {
&self.subject
}
#[must_use]
pub fn issuer(&self) -> &DistinguishedName {
&self.issuer
}
pub fn tbs_der(&self) -> Result<Vec<u8>> {
let placeholder = MatterCertificate::from_fields(
self.serial.clone(),
self.issuer.clone(),
self.not_before,
self.not_after,
self.subject.clone(),
self.public_key.clone(),
self.extensions.clone(),
Signature::new([0u8; 64]),
);
placeholder.to_x509_tbs_der()
}
#[must_use]
pub fn assemble(self, signature: [u8; 64]) -> MatterCertificate {
MatterCertificate::from_fields(
self.serial,
self.issuer,
self.not_before,
self.not_after,
self.subject,
self.public_key,
self.extensions,
Signature::new(signature),
)
}
}
impl MatterCertificate {
#[must_use]
pub fn builder() -> Builder {
Builder::default()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::cast_possible_truncation)] mod tests {
use super::*;
use crate::extensions::{BasicConstraints, Extensions};
use crate::name::DnAttribute;
fn sample_public_key() -> PublicKey {
let mut key_bytes = [0u8; 65];
key_bytes[0] = 0x04;
for (i, b) in key_bytes.iter_mut().enumerate().skip(1) {
*b = i as u8;
}
PublicKey::new(key_bytes).unwrap()
}
#[test]
fn build_unsigned_then_assemble_roundtrips() {
let pk = sample_public_key();
let unsigned = MatterCertificate::builder()
.serial(vec![1, 2, 3])
.issuer(DistinguishedName::new(vec![DnAttribute::RcacId(1)]))
.subject(DistinguishedName::new(vec![
DnAttribute::FabricId(7),
DnAttribute::NodeId(42),
]))
.validity(MatterTime(1_000), MatterTime::NO_EXPIRY)
.public_key(pk.clone())
.extensions(Extensions {
basic_constraints: Some(BasicConstraints {
is_ca: false,
path_len_constraint: None,
}),
..Default::default()
})
.build_unsigned()
.unwrap();
let tbs = unsigned.tbs_der().unwrap();
assert!(!tbs.is_empty(), "TBS DER must be non-empty");
let cert = unsigned.assemble([0xAB; 64]);
let tlv = cert.to_tlv().unwrap();
let parsed = MatterCertificate::from_tlv(&tlv).unwrap();
assert_eq!(parsed, cert);
assert_eq!(
tbs,
cert.to_x509_tbs_der().unwrap(),
"TBS from unsigned must match TBS from assembled cert"
);
}
#[test]
fn build_unsigned_fails_on_missing_serial() {
let err = MatterCertificate::builder()
.issuer(DistinguishedName::new(vec![DnAttribute::RcacId(1)]))
.subject(DistinguishedName::new(vec![DnAttribute::NodeId(42)]))
.validity(MatterTime(1_000), MatterTime::NO_EXPIRY)
.public_key(sample_public_key())
.extensions(Extensions::default())
.build_unsigned()
.unwrap_err();
assert!(
matches!(err, Error::MissingBuilderField("serial")),
"got: {err:?}"
);
}
#[test]
fn build_unsigned_fails_on_missing_subject() {
let err = MatterCertificate::builder()
.serial(vec![1])
.issuer(DistinguishedName::new(vec![DnAttribute::RcacId(1)]))
.validity(MatterTime(1_000), MatterTime::NO_EXPIRY)
.public_key(sample_public_key())
.extensions(Extensions::default())
.build_unsigned()
.unwrap_err();
assert!(
matches!(err, Error::MissingBuilderField("subject")),
"got: {err:?}"
);
}
#[test]
fn build_unsigned_rejects_oversized_serial() {
let err = MatterCertificate::builder()
.serial(vec![0u8; 21])
.issuer(DistinguishedName::new(vec![DnAttribute::RcacId(1)]))
.subject(DistinguishedName::new(vec![DnAttribute::NodeId(42)]))
.validity(MatterTime(1_000), MatterTime::NO_EXPIRY)
.public_key(sample_public_key())
.extensions(Extensions::default())
.build_unsigned()
.unwrap_err();
assert!(
matches!(err, Error::FieldValueOutOfRange { .. }),
"got: {err:?}"
);
}
#[test]
fn build_unsigned_rejects_empty_serial() {
let err = MatterCertificate::builder()
.serial(vec![])
.issuer(DistinguishedName::new(vec![DnAttribute::RcacId(1)]))
.subject(DistinguishedName::new(vec![DnAttribute::NodeId(42)]))
.validity(MatterTime(1_000), MatterTime::NO_EXPIRY)
.public_key(sample_public_key())
.extensions(Extensions::default())
.build_unsigned()
.unwrap_err();
assert!(
matches!(err, Error::FieldValueOutOfRange { .. }),
"got: {err:?}"
);
}
}