#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct VendorId(u16);
impl VendorId {
pub const fn new(value: u16) -> Self {
Self(value)
}
pub const fn as_u16(self) -> u16 {
self.0
}
}
impl core::fmt::Display for VendorId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:04X}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ProductId(u16);
impl ProductId {
pub const fn new(value: u16) -> Self {
Self(value)
}
pub const fn as_u16(self) -> u16 {
self.0
}
}
impl core::fmt::Display for ProductId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:04X}", self.0)
}
}
use x509_parser::der_parser::oid::Oid;
use x509_parser::x509::X509Name;
#[rustfmt::skip]
pub(crate) const MATTER_VID_OID: Oid<'static> =
x509_parser::der_parser::oid!(1.3.6.1.4.1.37244.2.1);
#[rustfmt::skip]
pub(crate) const MATTER_PID_OID: Oid<'static> =
x509_parser::der_parser::oid!(1.3.6.1.4.1.37244.2.2);
pub(crate) fn extract_vid(name: &X509Name<'_>) -> Result<Option<VendorId>, MatterDnError> {
extract_matter_u16(name, &MATTER_VID_OID).map(|opt| opt.map(VendorId::new))
}
pub(crate) fn extract_pid(name: &X509Name<'_>) -> Result<Option<ProductId>, MatterDnError> {
extract_matter_u16(name, &MATTER_PID_OID).map(|opt| opt.map(ProductId::new))
}
fn extract_matter_u16(name: &X509Name<'_>, oid: &Oid<'_>) -> Result<Option<u16>, MatterDnError> {
let Some(attr) = name.iter_attributes().find(|a| a.attr_type() == oid) else {
return Ok(None);
};
let raw = attr.as_str().map_err(|_| MatterDnError::NonUtf8Value)?;
if raw.len() != 4 {
return Err(MatterDnError::WrongLength { actual: raw.len() });
}
let value = u16::from_str_radix(raw, 16).map_err(|_| MatterDnError::NotHex)?;
let canonical = format!("{value:04X}");
if raw != canonical {
return Err(MatterDnError::NotUppercase);
}
Ok(Some(value))
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum MatterDnError {
#[error("Matter VID/PID DN attribute is not valid UTF-8")]
NonUtf8Value,
#[error("Matter VID/PID DN attribute is {actual} chars, expected 4")]
WrongLength {
actual: usize,
},
#[error("Matter VID/PID DN attribute is not hex")]
NotHex,
#[error("Matter VID/PID DN attribute must be UPPERCASE hex")]
NotUppercase,
}
#[cfg(test)]
mod tests {
use super::{ProductId, VendorId};
#[test]
fn vendor_id_round_trips_through_u16() {
let v = VendorId::new(0xFFF1);
assert_eq!(v.as_u16(), 0xFFF1);
}
#[test]
fn vendor_id_display_is_4char_uppercase_hex() {
assert_eq!(VendorId::new(0x0001).to_string(), "0001");
assert_eq!(VendorId::new(0xFFF1).to_string(), "FFF1");
assert_eq!(VendorId::new(0x00AB).to_string(), "00AB");
}
#[test]
fn product_id_round_trips_through_u16() {
let p = ProductId::new(0x8000);
assert_eq!(p.as_u16(), 0x8000);
}
#[test]
fn product_id_display_is_4char_uppercase_hex() {
assert_eq!(ProductId::new(0x0001).to_string(), "0001");
assert_eq!(ProductId::new(0x8000).to_string(), "8000");
}
use x509_parser::prelude::{FromDer, X509Certificate};
const DAC_DER: &[u8] = include_bytes!(
"../../../../test-vectors/certs/attestation/happy-path/Chip-Test-DAC-FFF1-8000-0004-Cert.der"
);
#[test]
#[allow(clippy::expect_used)] fn extract_vid_finds_dac_vid() {
let (_, cert) = X509Certificate::from_der(DAC_DER).expect("happy-path DAC parses");
let vid = super::extract_vid(cert.subject())
.expect("VID well-formed")
.expect("VID present");
assert_eq!(vid, super::VendorId::new(0xFFF1));
}
#[test]
#[allow(clippy::expect_used)] fn extract_pid_finds_dac_pid() {
let (_, cert) = X509Certificate::from_der(DAC_DER).expect("happy-path DAC parses");
let pid = super::extract_pid(cert.subject())
.expect("PID well-formed")
.expect("PID present");
assert_eq!(pid, super::ProductId::new(0x8000));
}
#[test]
#[allow(clippy::expect_used)] fn matter_vid_oid_constant_matches_spec_arc() {
let parts: Vec<u64> = super::MATTER_VID_OID.iter().expect("iterable").collect();
assert_eq!(parts, vec![1, 3, 6, 1, 4, 1, 37244, 2, 1]);
}
}