use der::asn1::ObjectIdentifier;
use crate::certificate::MatterCertificate;
use crate::error::{Error, Result};
const OID_MATTER_NODE_ID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.4.1.37244.1.1");
#[allow(dead_code)]
const OID_MATTER_FIRMWARE_SIGNING_ID: ObjectIdentifier =
ObjectIdentifier::new_unwrap("1.3.6.1.4.1.37244.1.2");
const OID_MATTER_ICAC_ID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.4.1.37244.1.3");
const OID_MATTER_RCAC_ID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.4.1.37244.1.4");
const OID_MATTER_FABRIC_ID: ObjectIdentifier =
ObjectIdentifier::new_unwrap("1.3.6.1.4.1.37244.1.5");
const OID_MATTER_NOC_CAT: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.4.1.37244.1.6");
#[allow(dead_code)]
const OID_MATTER_VVS_ID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.4.1.37244.1.7");
const OID_MATTER_VENDOR_ID: ObjectIdentifier =
ObjectIdentifier::new_unwrap("1.3.6.1.4.1.37244.2.1");
const OID_MATTER_PRODUCT_ID: ObjectIdentifier =
ObjectIdentifier::new_unwrap("1.3.6.1.4.1.37244.2.2");
const OID_X520_COMMON_NAME: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.3");
const OID_X520_SURNAME: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.4");
const OID_X520_SERIAL_NUMBER: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.5");
const OID_X520_COUNTRY_NAME: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.6");
const OID_X520_LOCALITY_NAME: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.7");
const OID_X520_STATE_OR_PROVINCE: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.8");
const OID_X520_ORG: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.10");
const OID_X520_ORG_UNIT: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.11");
const OID_X520_TITLE: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.12");
const OID_X520_NAME: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.41");
const OID_X520_GIVEN_NAME: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.42");
const OID_X520_INITIALS: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.43");
const OID_X520_GENERATION_QUALIFIER: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.44");
const OID_X520_DN_QUALIFIER: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.46");
const OID_X520_PSEUDONYM: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.65");
const OID_DOMAIN_COMPONENT: ObjectIdentifier =
ObjectIdentifier::new_unwrap("0.9.2342.19200300.100.1.25");
const OID_EXT_BASIC_CONSTRAINTS: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.19");
const OID_EXT_KEY_USAGE: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.15");
const OID_EXT_EXTENDED_KEY_USAGE: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.37");
const OID_EXT_SUBJECT_KEY_IDENTIFIER: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.14");
const OID_EXT_AUTHORITY_KEY_IDENTIFIER: ObjectIdentifier =
ObjectIdentifier::new_unwrap("2.5.29.35");
const OID_EC_PUBLIC_KEY: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1");
const OID_EC_CURVE_P256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7");
const OID_ECDSA_WITH_SHA256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
pub(crate) fn matter_cert_to_x509_tbs_der(cert: &MatterCertificate) -> Result<Vec<u8>> {
let mut out = Vec::with_capacity(512);
let mut field_err: Option<Error> = None;
wrap_sequence_into(&mut out, |tbs| {
tbs.extend_from_slice(&[0xA0, 0x03, 0x02, 0x01, 0x02]);
if let Err(e) = encode_serial_number_into(tbs, cert.serial()) {
field_err = Some(e);
return;
}
encode_algorithm_identifier_ecdsa_sha256_into(tbs);
if let Err(e) = encode_dn_into(tbs, cert.issuer()) {
field_err = Some(e);
return;
}
encode_validity_into(tbs, cert.not_before(), cert.not_after());
if let Err(e) = encode_dn_into(tbs, cert.subject()) {
field_err = Some(e);
return;
}
encode_subject_public_key_info_into(tbs, cert.public_key());
encode_extensions_block_into(tbs, cert.extensions());
});
if let Some(e) = field_err {
return Err(e);
}
Ok(out)
}
const X509_UTCTIME_CUTOFF_UNIX_SECS: u64 = 2_524_608_000;
fn encode_validity_into(
out: &mut Vec<u8>,
not_before: crate::time::MatterTime,
not_after: crate::time::MatterTime,
) {
wrap_sequence_into(out, |inner| {
encode_one_time_into(inner, not_before, false);
encode_one_time_into(inner, not_after, true);
});
}
#[cfg(test)]
fn encode_validity(
not_before: crate::time::MatterTime,
not_after: crate::time::MatterTime,
) -> Vec<u8> {
let mut out = Vec::new();
encode_validity_into(&mut out, not_before, not_after);
out
}
fn encode_one_time_into(out: &mut Vec<u8>, t: crate::time::MatterTime, is_not_after: bool) {
if t == crate::time::MatterTime::NO_EXPIRY && is_not_after {
encode_generalized_time_literal_into(out, b"99991231235959Z");
return;
}
let unix = t.to_unix_secs();
let (year, month, day, hour, minute, second) = unix_to_ymdhms(unix);
if unix < X509_UTCTIME_CUTOFF_UNIX_SECS {
#[allow(clippy::cast_possible_truncation)]
let yy = (year % 100) as u8;
let s = format!("{yy:02}{month:02}{day:02}{hour:02}{minute:02}{second:02}Z");
encode_utc_time_literal_into(out, s.as_bytes());
} else {
let s = format!("{year:04}{month:02}{day:02}{hour:02}{minute:02}{second:02}Z");
encode_generalized_time_literal_into(out, s.as_bytes());
}
}
fn encode_utc_time_literal_into(out: &mut Vec<u8>, s: &[u8]) {
debug_assert_eq!(s.len(), 13);
#[allow(clippy::cast_possible_truncation)]
let len_byte = s.len() as u8;
out.push(0x17);
out.push(len_byte);
out.extend_from_slice(s);
}
fn encode_generalized_time_literal_into(out: &mut Vec<u8>, s: &[u8]) {
debug_assert_eq!(s.len(), 15);
#[allow(clippy::cast_possible_truncation)]
let len_byte = s.len() as u8;
out.push(0x18);
out.push(len_byte);
out.extend_from_slice(s);
}
fn encode_serial_number_into(out: &mut Vec<u8>, serial_bytes: &[u8]) -> Result<()> {
if serial_bytes.is_empty() {
return Err(Error::FieldValueOutOfRange {
tag: crate::tlv_tags::CERT_SERIAL_NUMBER,
});
}
let needs_leading_zero = (serial_bytes[0] & 0x80) != 0;
out.push(0x02); let content_len = serial_bytes.len() + usize::from(needs_leading_zero);
encode_definite_length(out, content_len);
if needs_leading_zero {
out.push(0x00);
}
out.extend_from_slice(serial_bytes);
Ok(())
}
#[cfg(test)]
fn encode_serial_number(serial_bytes: &[u8]) -> Result<Vec<u8>> {
let mut out = Vec::new();
encode_serial_number_into(&mut out, serial_bytes)?;
Ok(out)
}
pub(crate) fn encode_algorithm_identifier_ecdsa_sha256() -> Vec<u8> {
let mut out = Vec::new();
encode_algorithm_identifier_ecdsa_sha256_into(&mut out);
out
}
fn encode_algorithm_identifier_ecdsa_sha256_into(out: &mut Vec<u8>) {
wrap_sequence_into(out, |inner| encode_oid_into(inner, &OID_ECDSA_WITH_SHA256));
}
fn encode_subject_public_key_info_into(out: &mut Vec<u8>, key: &crate::PublicKey) {
wrap_sequence_into(out, |inner| {
wrap_sequence_into(inner, |alg| {
encode_oid_into(alg, &OID_EC_PUBLIC_KEY);
encode_oid_into(alg, &OID_EC_CURVE_P256);
});
let point = key.as_bytes();
inner.push(0x03);
encode_definite_length(inner, point.len() + 1);
inner.push(0x00); inner.extend_from_slice(point);
});
}
#[cfg(test)]
fn encode_subject_public_key_info(key: &crate::PublicKey) -> Vec<u8> {
let mut out = Vec::new();
encode_subject_public_key_info_into(&mut out, key);
out
}
#[allow(clippy::expect_used)] fn encode_oid_into(out: &mut Vec<u8>, oid: &ObjectIdentifier) {
use der::Encode;
oid.encode_to_vec(out)
.expect("internal: der OID encoder rejected a validated ObjectIdentifier");
}
use crate::name::{DistinguishedName, DnAttribute};
const TAG_UTF8_STRING: u8 = 0x0C;
const TAG_PRINTABLE_STRING: u8 = 0x13;
const TAG_IA5_STRING: u8 = 0x16;
#[cfg(test)]
fn encode_dn_attribute(attr: &DnAttribute) -> Result<Vec<u8>> {
let mut out = Vec::new();
encode_dn_attribute_into(&mut out, attr)?;
Ok(out)
}
#[allow(clippy::too_many_lines)]
fn encode_dn_attribute_into(out: &mut Vec<u8>, attr: &DnAttribute) -> Result<()> {
let (oid, string_tag, value_bytes) = match attr {
DnAttribute::NodeId(v) => (
&OID_MATTER_NODE_ID,
TAG_UTF8_STRING,
format!("{v:016X}").into_bytes(),
),
DnAttribute::FabricId(v) => (
&OID_MATTER_FABRIC_ID,
TAG_UTF8_STRING,
format!("{v:016X}").into_bytes(),
),
DnAttribute::RcacId(v) => (
&OID_MATTER_RCAC_ID,
TAG_UTF8_STRING,
format!("{v:016X}").into_bytes(),
),
DnAttribute::IcacId(v) => (
&OID_MATTER_ICAC_ID,
TAG_UTF8_STRING,
format!("{v:016X}").into_bytes(),
),
DnAttribute::CaseAuthenticatedTag(v) => (
&OID_MATTER_NOC_CAT,
TAG_UTF8_STRING,
format!("{v:08X}").into_bytes(),
),
DnAttribute::VendorId(v) => (
&OID_MATTER_VENDOR_ID,
TAG_PRINTABLE_STRING,
format!("{v:04X}").into_bytes(),
),
DnAttribute::ProductId(v) => (
&OID_MATTER_PRODUCT_ID,
TAG_PRINTABLE_STRING,
format!("{v:04X}").into_bytes(),
),
DnAttribute::CommonName(s) => (
&OID_X520_COMMON_NAME,
TAG_UTF8_STRING,
s.as_bytes().to_vec(),
),
DnAttribute::Surname(s) => (&OID_X520_SURNAME, TAG_UTF8_STRING, s.as_bytes().to_vec()),
DnAttribute::SerialNumber(s) => {
verify_printable(s.as_bytes(), "SerialNumber")?;
(
&OID_X520_SERIAL_NUMBER,
TAG_PRINTABLE_STRING,
s.as_bytes().to_vec(),
)
}
DnAttribute::CountryName(s) => {
verify_printable(s.as_bytes(), "CountryName")?;
if s.len() != 2 {
return Err(Error::InvalidDnAttributeForX509 {
asn1_type: "PrintableString",
reason: "CountryName must be exactly 2 characters",
});
}
(
&OID_X520_COUNTRY_NAME,
TAG_PRINTABLE_STRING,
s.as_bytes().to_vec(),
)
}
DnAttribute::LocalityName(s) => (
&OID_X520_LOCALITY_NAME,
TAG_UTF8_STRING,
s.as_bytes().to_vec(),
),
DnAttribute::StateOrProvinceName(s) => (
&OID_X520_STATE_OR_PROVINCE,
TAG_UTF8_STRING,
s.as_bytes().to_vec(),
),
DnAttribute::OrganizationName(s) => (&OID_X520_ORG, TAG_UTF8_STRING, s.as_bytes().to_vec()),
DnAttribute::OrganizationalUnitName(s) => {
(&OID_X520_ORG_UNIT, TAG_UTF8_STRING, s.as_bytes().to_vec())
}
DnAttribute::Title(s) => (&OID_X520_TITLE, TAG_UTF8_STRING, s.as_bytes().to_vec()),
DnAttribute::Name(s) => (&OID_X520_NAME, TAG_UTF8_STRING, s.as_bytes().to_vec()),
DnAttribute::GivenName(s) => (&OID_X520_GIVEN_NAME, TAG_UTF8_STRING, s.as_bytes().to_vec()),
DnAttribute::Initials(s) => (&OID_X520_INITIALS, TAG_UTF8_STRING, s.as_bytes().to_vec()),
DnAttribute::GenerationQualifier(s) => (
&OID_X520_GENERATION_QUALIFIER,
TAG_UTF8_STRING,
s.as_bytes().to_vec(),
),
DnAttribute::DnQualifier(s) => {
verify_printable(s.as_bytes(), "DnQualifier")?;
(
&OID_X520_DN_QUALIFIER,
TAG_PRINTABLE_STRING,
s.as_bytes().to_vec(),
)
}
DnAttribute::Pseudonym(s) => (&OID_X520_PSEUDONYM, TAG_UTF8_STRING, s.as_bytes().to_vec()),
DnAttribute::DomainComponent(s) => {
verify_ia5(s.as_bytes(), "DomainComponent")?;
(&OID_DOMAIN_COMPONENT, TAG_IA5_STRING, s.as_bytes().to_vec())
}
DnAttribute::Other { tag, .. } => return Err(Error::DnAttributeHasNoX509Oid(*tag)),
};
wrap_sequence_into(out, |inner| {
encode_oid_into(inner, oid);
wrap_primitive_into(inner, string_tag, &value_bytes);
});
Ok(())
}
fn verify_printable(s: &[u8], asn1_type: &'static str) -> Result<()> {
for &b in s {
let ok = b.is_ascii_alphanumeric()
|| matches!(
b,
b' ' | b'\'' | b'(' | b')' | b'+' | b',' | b'-' | b'.' | b'/' | b':' | b'=' | b'?'
);
if !ok {
return Err(Error::InvalidDnAttributeForX509 {
asn1_type,
reason: "value contains a non-PrintableString character",
});
}
}
Ok(())
}
fn verify_ia5(s: &[u8], asn1_type: &'static str) -> Result<()> {
if s.iter().any(|&b| b > 0x7F) {
return Err(Error::InvalidDnAttributeForX509 {
asn1_type,
reason: "value contains a non-ASCII character",
});
}
Ok(())
}
fn wrap_primitive_into(out: &mut Vec<u8>, tag: u8, value: &[u8]) {
out.push(tag);
encode_definite_length(out, value.len());
out.extend_from_slice(value);
}
#[cfg(test)]
fn encode_dn(dn: &DistinguishedName) -> Result<Vec<u8>> {
let mut out = Vec::new();
encode_dn_into(&mut out, dn)?;
Ok(out)
}
fn encode_dn_into(out: &mut Vec<u8>, dn: &DistinguishedName) -> Result<()> {
let mut attr_err: Option<Error> = None;
wrap_sequence_into(out, |seq| {
for attr in dn {
if let Err(e) = wrap_set_into(seq, |set| encode_dn_attribute_into(set, attr)) {
attr_err = Some(e);
return;
}
}
});
match attr_err {
Some(e) => Err(e),
None => Ok(()),
}
}
fn wrap_set_into<F>(out: &mut Vec<u8>, build: F) -> Result<()>
where
F: FnOnce(&mut Vec<u8>) -> Result<()>,
{
let mut build_err: Option<Error> = None;
wrap_tagged_into(out, 0x31, |set| {
if let Err(e) = build(set) {
build_err = Some(e);
}
});
match build_err {
Some(e) => Err(e),
None => Ok(()),
}
}
pub(crate) fn wrap_sequence(content: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(content.len() + 4);
out.push(0x30);
encode_definite_length(&mut out, content.len());
out.extend_from_slice(content);
out
}
fn definite_length_prefix(len: usize) -> ([u8; 9], usize) {
let mut buf = [0u8; 9];
if len < 0x80 {
#[allow(clippy::cast_possible_truncation)]
{
buf[0] = len as u8;
}
(buf, 1)
} else {
let bytes = len.to_be_bytes();
let leading_zeros = bytes.iter().take_while(|&&b| b == 0).count();
let used = &bytes[leading_zeros..];
#[allow(clippy::cast_possible_truncation)]
{
buf[0] = 0x80 | (used.len() as u8);
}
buf[1..=used.len()].copy_from_slice(used);
(buf, 1 + used.len())
}
}
fn wrap_tagged_into<F>(out: &mut Vec<u8>, tag: u8, build: F)
where
F: FnOnce(&mut Vec<u8>),
{
let content_start = out.len();
build(out);
let content_len = out.len() - content_start;
let (prefix, prefix_len) = definite_length_prefix(content_len);
let header = core::iter::once(tag).chain(prefix[..prefix_len].iter().copied());
out.splice(content_start..content_start, header);
}
fn wrap_sequence_into<F>(out: &mut Vec<u8>, build: F)
where
F: FnOnce(&mut Vec<u8>),
{
wrap_tagged_into(out, 0x30, build);
}
pub(crate) fn encode_definite_length(out: &mut Vec<u8>, len: usize) {
if len < 0x80 {
#[allow(clippy::cast_possible_truncation)]
out.push(len as u8);
} else {
let bytes = len.to_be_bytes();
let leading_zeros = bytes.iter().take_while(|&&b| b == 0).count();
let used = &bytes[leading_zeros..];
#[allow(clippy::cast_possible_truncation)]
out.push(0x80 | (used.len() as u8));
out.extend_from_slice(used);
}
}
#[allow(clippy::similar_names)]
fn unix_to_ymdhms(unix_secs: u64) -> (u32, u32, u32, u32, u32, u32) {
#[allow(clippy::cast_possible_wrap)]
let day = (unix_secs / 86_400) as i64;
#[allow(clippy::cast_possible_truncation)]
let seconds_in_day = (unix_secs % 86_400) as u32;
let hour = seconds_in_day / 3600;
let minute = (seconds_in_day / 60) % 60;
let second = seconds_in_day % 60;
let z = day + 719_468;
let era = z.div_euclid(146_097);
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let doe = (z - era * 146_097) as u32;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = i64::from(yoe) + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let year = if m <= 2 { y + 1 } else { y };
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let year_u32 = year as u32;
(year_u32, m, d, hour, minute, second)
}
use crate::extensions::{BasicConstraints, Extensions, KeyIdentifier, KeyUsage};
fn encode_extension_into(
out: &mut Vec<u8>,
oid: &ObjectIdentifier,
critical: bool,
extn_value: &[u8],
) {
wrap_sequence_into(out, |inner| {
encode_oid_into(inner, oid);
if critical {
inner.extend_from_slice(&[0x01, 0x01, 0xFF]);
}
wrap_primitive_into(inner, 0x04, extn_value);
});
}
fn encode_basic_constraints_into(out: &mut Vec<u8>, bc: BasicConstraints) {
let mut value = Vec::new();
wrap_sequence_into(&mut value, |inner| {
if bc.is_ca {
inner.extend_from_slice(&[0x01, 0x01, 0xFF]); if let Some(path_len) = bc.path_len_constraint {
inner.push(0x02);
inner.push(0x01);
inner.push(path_len);
}
}
});
encode_extension_into(
out,
&OID_EXT_BASIC_CONSTRAINTS,
true,
&value,
);
}
#[cfg(test)]
fn encode_basic_constraints(bc: BasicConstraints) -> Vec<u8> {
let mut out = Vec::new();
encode_basic_constraints_into(&mut out, bc);
out
}
fn encode_key_usage_into(out: &mut Vec<u8>, ku: KeyUsage) {
let raw = ku.bits();
#[allow(clippy::cast_possible_truncation)]
let byte0_rev = (raw as u8).reverse_bits();
#[allow(clippy::cast_possible_truncation)]
let byte1_rev = ((raw >> 8) as u8).reverse_bits();
let bit_string_content: Vec<u8> = if byte1_rev != 0 {
#[allow(clippy::cast_possible_truncation)]
let ub = byte1_rev.trailing_zeros() as u8;
vec![ub, byte0_rev, byte1_rev]
} else if byte0_rev != 0 {
#[allow(clippy::cast_possible_truncation)]
let ub = byte0_rev.trailing_zeros() as u8;
vec![ub, byte0_rev]
} else {
vec![0x00, 0x00]
};
let mut bit_string = Vec::with_capacity(2 + bit_string_content.len());
bit_string.push(0x03); encode_definite_length(&mut bit_string, bit_string_content.len());
bit_string.extend_from_slice(&bit_string_content);
encode_extension_into(
out,
&OID_EXT_KEY_USAGE,
true,
&bit_string,
);
}
#[cfg(test)]
fn encode_key_usage(ku: KeyUsage) -> Vec<u8> {
let mut out = Vec::new();
encode_key_usage_into(&mut out, ku);
out
}
const OID_KP_SERVER_AUTH: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.1");
const OID_KP_CLIENT_AUTH: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.2");
const OID_KP_CODE_SIGNING: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.3");
const OID_KP_EMAIL_PROTECTION: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.4");
const OID_KP_TIME_STAMPING: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.8");
const OID_KP_OCSP_SIGNING: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.9");
fn encode_extended_key_usage_into(out: &mut Vec<u8>, eku: &[u32], critical: bool) {
let mut value = Vec::new();
wrap_sequence_into(&mut value, |inner| {
for &val in eku {
let oid = match val {
1 => &OID_KP_SERVER_AUTH,
2 => &OID_KP_CLIENT_AUTH,
3 => &OID_KP_CODE_SIGNING,
4 => &OID_KP_EMAIL_PROTECTION,
5 => &OID_KP_TIME_STAMPING,
6 => &OID_KP_OCSP_SIGNING,
_ => continue,
};
encode_oid_into(inner, oid);
}
});
encode_extension_into(out, &OID_EXT_EXTENDED_KEY_USAGE, critical, &value);
}
fn encode_subject_key_identifier_into(out: &mut Vec<u8>, ski: &KeyIdentifier) {
let mut inner = Vec::new();
wrap_primitive_into(&mut inner, 0x04, &ski.0);
encode_extension_into(
out,
&OID_EXT_SUBJECT_KEY_IDENTIFIER,
false,
&inner,
);
}
#[cfg(test)]
fn encode_subject_key_identifier(ski: &KeyIdentifier) -> Vec<u8> {
let mut out = Vec::new();
encode_subject_key_identifier_into(&mut out, ski);
out
}
fn encode_authority_key_identifier_into(out: &mut Vec<u8>, aki: &KeyIdentifier) {
let mut value = Vec::new();
wrap_sequence_into(&mut value, |inner| {
inner.push(0x80);
encode_definite_length(inner, 20);
inner.extend_from_slice(&aki.0);
});
encode_extension_into(
out,
&OID_EXT_AUTHORITY_KEY_IDENTIFIER,
false,
&value,
);
}
fn encode_extensions_block_into(out: &mut Vec<u8>, ext: &Extensions) {
wrap_tagged_into(out, 0xA3, |explicit| {
wrap_sequence_into(explicit, |inner| {
if let Some(bc) = &ext.basic_constraints {
encode_basic_constraints_into(inner, *bc);
}
if let Some(ku) = ext.key_usage {
encode_key_usage_into(inner, ku);
}
if let Some(eku) = &ext.extended_key_usage {
encode_extended_key_usage_into(inner, eku, true);
}
if let Some(ski) = &ext.subject_key_identifier {
encode_subject_key_identifier_into(inner, ski);
}
if let Some(aki) = &ext.authority_key_identifier {
encode_authority_key_identifier_into(inner, aki);
}
});
});
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
use super::*;
use crate::extensions::{BasicConstraints, KeyIdentifier, KeyUsage};
use crate::time::MatterTime;
#[test]
fn validity_pre_2050_uses_utctime() {
let not_before = MatterTime::from_unix_secs(1_704_067_200);
let not_after = MatterTime::from_unix_secs(2_524_607_999);
let bytes = encode_validity(not_before, not_after);
assert_eq!(bytes[0], 0x30); assert_eq!(bytes[2], 0x17); assert_eq!(bytes[3], 0x0D); assert_eq!(&bytes[4..17], b"240101000000Z");
assert_eq!(bytes[17], 0x17); assert_eq!(bytes[18], 0x0D);
assert_eq!(&bytes[19..32], b"491231235959Z");
}
#[test]
fn validity_post_2050_uses_generalizedtime() {
let not_before = MatterTime::from_unix_secs(2_524_608_000);
let not_after = MatterTime::from_unix_secs(4_102_444_800);
let bytes = encode_validity(not_before, not_after);
assert_eq!(bytes[0], 0x30);
assert_eq!(bytes[2], 0x18); assert_eq!(bytes[3], 0x0F); assert_eq!(&bytes[4..19], b"20500101000000Z");
assert_eq!(bytes[19], 0x18);
assert_eq!(bytes[20], 0x0F);
assert_eq!(&bytes[21..36], b"21000101000000Z");
}
#[test]
fn validity_no_expiry_maps_to_sentinel() {
let not_before = MatterTime::from_unix_secs(1_704_067_200);
let bytes = encode_validity(not_before, MatterTime::NO_EXPIRY);
assert_eq!(bytes[17], 0x18); assert_eq!(bytes[18], 0x0F);
assert_eq!(&bytes[19..34], b"99991231235959Z");
}
#[test]
fn serial_high_bit_clear_no_leading_zero() {
let bytes = encode_serial_number(&[0x01, 0x02, 0x03]).unwrap();
assert_eq!(bytes, vec![0x02, 0x03, 0x01, 0x02, 0x03]);
}
#[test]
fn serial_high_bit_set_prepends_leading_zero() {
let bytes = encode_serial_number(&[0x80, 0x12, 0x34]).unwrap();
assert_eq!(bytes, vec![0x02, 0x04, 0x00, 0x80, 0x12, 0x34]);
}
#[test]
fn serial_single_byte_high_bit_set() {
let bytes = encode_serial_number(&[0xFF]).unwrap();
assert_eq!(bytes, vec![0x02, 0x02, 0x00, 0xFF]);
}
#[test]
fn serial_empty_is_rejected() {
assert!(encode_serial_number(&[]).is_err());
}
#[test]
fn alg_id_ecdsa_sha256_has_no_parameters() {
let bytes = encode_algorithm_identifier_ecdsa_sha256();
assert_eq!(bytes[0], 0x30);
assert_eq!(bytes[1], 0x0A);
assert_eq!(bytes[2], 0x06);
assert_eq!(bytes[3], 0x08);
assert_eq!(
&bytes[4..12],
&[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02]
);
}
#[test]
fn dn_attribute_node_id_uses_uppercase_hex_utf8string() {
let attr = crate::DnAttribute::NodeId(0x10001);
let bytes = encode_dn_attribute(&attr).unwrap();
let utf8_start = bytes
.iter()
.position(|&b| b == 0x0C)
.expect("expected UTF8String tag 0x0C");
let len = bytes[utf8_start + 1] as usize;
assert_eq!(len, 16, "Matter-attribute hex is 16 chars for u64");
assert_eq!(
&bytes[utf8_start + 2..utf8_start + 2 + len],
b"0000000000010001"
);
}
#[test]
fn dn_attribute_common_name_uses_utf8string() {
let attr = crate::DnAttribute::CommonName("matter-test".to_string());
let bytes = encode_dn_attribute(&attr).unwrap();
let utf8_start = bytes.iter().position(|&b| b == 0x0C).unwrap();
let len = bytes[utf8_start + 1] as usize;
assert_eq!(&bytes[utf8_start + 2..utf8_start + 2 + len], b"matter-test");
}
#[test]
fn dn_attribute_country_name_uses_printablestring() {
let attr = crate::DnAttribute::CountryName("US".to_string());
let bytes = encode_dn_attribute(&attr).unwrap();
let ps_start = bytes.iter().position(|&b| b == 0x13).unwrap();
assert_eq!(bytes[ps_start + 1], 0x02);
assert_eq!(&bytes[ps_start + 2..ps_start + 4], b"US");
}
#[test]
fn dn_attribute_domain_component_uses_ia5string() {
let attr = crate::DnAttribute::DomainComponent("example".to_string());
let bytes = encode_dn_attribute(&attr).unwrap();
let ia5_start = bytes.iter().position(|&b| b == 0x16).unwrap();
let len = bytes[ia5_start + 1] as usize;
assert_eq!(&bytes[ia5_start + 2..ia5_start + 2 + len], b"example");
}
#[test]
fn dn_attribute_other_is_rejected() {
use crate::DnAttributeValue;
let attr = crate::DnAttribute::Other {
tag: 99,
value: DnAttributeValue::Utf8("ignored".to_string()),
};
let err = encode_dn_attribute(&attr).unwrap_err();
assert!(matches!(err, Error::DnAttributeHasNoX509Oid(99)));
}
#[test]
fn dn_attribute_country_name_non_printable_rejected() {
let attr = crate::DnAttribute::CountryName("Я ".to_string());
let err = encode_dn_attribute(&attr).unwrap_err();
assert!(matches!(err, Error::InvalidDnAttributeForX509 { .. }));
}
#[test]
fn dn_with_one_attribute_wraps_in_sequence_of_set() {
let dn =
crate::DistinguishedName::new(vec![crate::DnAttribute::CommonName("test".to_string())]);
let bytes = encode_dn(&dn).unwrap();
assert_eq!(bytes[0], 0x30); assert_eq!(bytes[2], 0x31);
}
#[test]
fn basic_constraints_ca_true_is_critical_and_has_boolean_true() {
let bc = BasicConstraints {
is_ca: true,
path_len_constraint: None,
};
let bytes = encode_basic_constraints(bc);
assert!(
bytes.windows(3).any(|w| w == [0x01, 0x01, 0xFF]),
"BOOLEAN(true) for critical flag must be present"
);
assert!(
bytes
.windows(5)
.any(|w| w == [0x30, 0x03, 0x01, 0x01, 0xFF]),
"Inner BasicConstraints must contain BOOLEAN(true)"
);
}
#[test]
fn basic_constraints_ca_false_omits_boolean() {
let bc = BasicConstraints {
is_ca: false,
path_len_constraint: None,
};
let bytes = encode_basic_constraints(bc);
assert!(
!bytes.windows(3).any(|w| w == [0x01, 0x01, 0x00]),
"BOOLEAN(false) must NOT be encoded (DER default rule)"
);
}
#[test]
fn key_usage_packs_bits_correctly() {
let ku = KeyUsage::DIGITAL_SIGNATURE | KeyUsage::KEY_CERT_SIGN;
let bytes = encode_key_usage(ku);
assert!(bytes.windows(3).any(|w| w == [0x01, 0x01, 0xFF]));
}
#[test]
fn ski_wraps_20_bytes_in_octet_string() {
let ski = KeyIdentifier([0xABu8; 20]);
let bytes = encode_subject_key_identifier(&ski);
let needle: Vec<u8> = std::iter::once(0x04u8)
.chain(std::iter::once(20u8))
.chain(std::iter::repeat_n(0xABu8, 20))
.collect();
assert!(
bytes.windows(needle.len()).any(|w| w == needle.as_slice()),
"inner OCTET STRING with 20 bytes of 0xAB must be present"
);
}
#[test]
fn top_level_produces_outer_sequence() {
let bytes = std::fs::read("../../test-vectors/certs/rcac.bin").unwrap();
let cert = crate::MatterCertificate::from_tlv(&bytes).unwrap();
let tbs = matter_cert_to_x509_tbs_der(&cert).unwrap();
assert_eq!(tbs[0], 0x30, "TBS must start with a SEQUENCE tag");
assert!(
tbs.len() > 80,
"TBS must be substantial — at least 80 bytes"
);
}
#[test]
fn spki_has_correct_curve_oid_and_bitstring_prefix() {
let mut point = [0u8; 65];
point[0] = 0x04;
for (i, slot) in point.iter_mut().enumerate().skip(1) {
*slot = u8::try_from(i).unwrap();
}
let key = crate::PublicKey::new(point).unwrap();
let bytes = encode_subject_public_key_info(&key);
assert_eq!(bytes[0], 0x30); let bit_string_idx = 23;
assert_eq!(bytes[bit_string_idx], 0x03); assert_eq!(bytes[bit_string_idx + 1], 66);
assert_eq!(bytes[bit_string_idx + 2], 0x00); assert_eq!(&bytes[bit_string_idx + 3..bit_string_idx + 68], &point);
}
}