use x509_parser::prelude::{FromDer, X509Certificate};
use crate::attestation::error::AttestationError;
use crate::attestation::extensions::{self, ProductId, VendorId};
#[derive(Debug, Clone)]
pub struct Dac {
der: Vec<u8>,
subject_vid: VendorId,
subject_pid: ProductId,
public_key: Vec<u8>,
}
impl Dac {
pub fn from_der(bytes: &[u8]) -> Result<Self, AttestationError> {
let (_, cert) = X509Certificate::from_der(bytes)
.map_err(|e| AttestationError::Parse(Box::new(e.clone())))?;
let subject = cert.subject();
let vid = extensions::extract_vid(subject)
.map_err(|e| AttestationError::Parse(Box::new(e)))?
.ok_or_else(|| {
AttestationError::Parse(Box::new(MissingRequired("DAC subject VendorId")))
})?;
let pid = extensions::extract_pid(subject)
.map_err(|e| AttestationError::Parse(Box::new(e)))?
.ok_or_else(|| {
AttestationError::Parse(Box::new(MissingRequired("DAC subject ProductId")))
})?;
let public_key = cert.public_key().subject_public_key.data.as_ref().to_vec();
Ok(Self {
der: bytes.to_vec(),
subject_vid: vid,
subject_pid: pid,
public_key,
})
}
pub fn der(&self) -> &[u8] {
&self.der
}
pub fn subject_vid(&self) -> VendorId {
self.subject_vid
}
pub fn subject_pid(&self) -> ProductId {
self.subject_pid
}
pub fn public_key(&self) -> &[u8] {
&self.public_key
}
}
#[derive(Debug, Clone)]
pub struct Pai {
der: Vec<u8>,
subject_vid: VendorId,
subject_pid: Option<ProductId>,
public_key: Vec<u8>,
issuer_raw: Vec<u8>,
}
impl Pai {
pub fn from_der(bytes: &[u8]) -> Result<Self, AttestationError> {
let (_, cert) = X509Certificate::from_der(bytes)
.map_err(|e| AttestationError::Parse(Box::new(e.clone())))?;
let subject = cert.subject();
let vid = extensions::extract_vid(subject)
.map_err(|e| AttestationError::Parse(Box::new(e)))?
.ok_or_else(|| {
AttestationError::Parse(Box::new(MissingRequired("PAI subject VendorId")))
})?;
let pid =
extensions::extract_pid(subject).map_err(|e| AttestationError::Parse(Box::new(e)))?;
let public_key = cert.public_key().subject_public_key.data.as_ref().to_vec();
let issuer_raw = cert.tbs_certificate.issuer.as_raw().to_vec();
Ok(Self {
der: bytes.to_vec(),
subject_vid: vid,
subject_pid: pid,
public_key,
issuer_raw,
})
}
pub fn der(&self) -> &[u8] {
&self.der
}
pub fn issuer_raw(&self) -> &[u8] {
&self.issuer_raw
}
pub fn subject_vid(&self) -> VendorId {
self.subject_vid
}
pub fn subject_pid(&self) -> Option<ProductId> {
self.subject_pid
}
pub fn public_key(&self) -> &[u8] {
&self.public_key
}
}
#[derive(Debug, Clone)]
pub struct Paa {
der: Vec<u8>,
subject_vid: Option<VendorId>,
public_key: Vec<u8>,
}
impl Paa {
pub fn from_der(bytes: &[u8]) -> Result<Self, AttestationError> {
let (_, cert) = X509Certificate::from_der(bytes)
.map_err(|e| AttestationError::Parse(Box::new(e.clone())))?;
let subject = cert.subject();
let vid =
extensions::extract_vid(subject).map_err(|e| AttestationError::Parse(Box::new(e)))?;
if extensions::extract_pid(subject)
.map_err(|e| AttestationError::Parse(Box::new(e)))?
.is_some()
{
return Err(AttestationError::Parse(Box::new(ForbiddenField(
"PAA subject must not contain a ProductId",
))));
}
let public_key = cert.public_key().subject_public_key.data.as_ref().to_vec();
Ok(Self {
der: bytes.to_vec(),
subject_vid: vid,
public_key,
})
}
pub fn der(&self) -> &[u8] {
&self.der
}
pub fn subject_vid(&self) -> Option<VendorId> {
self.subject_vid
}
pub fn public_key(&self) -> &[u8] {
&self.public_key
}
#[must_use]
pub fn subject_key_identifier(&self) -> Option<Vec<u8>> {
use x509_parser::extensions::ParsedExtension;
use x509_parser::prelude::{FromDer, X509Certificate};
let (_, cert) = X509Certificate::from_der(&self.der).ok()?;
cert.extensions()
.iter()
.find_map(|ext| match ext.parsed_extension() {
ParsedExtension::SubjectKeyIdentifier(kid) => Some(kid.0.to_vec()),
_ => None,
})
}
}
#[derive(Debug, thiserror::Error)]
#[error("required field absent: {0}")]
struct MissingRequired(&'static str);
#[derive(Debug, thiserror::Error)]
#[error("forbidden field present: {0}")]
struct ForbiddenField(&'static str);
#[cfg(test)]
mod tests {
use super::*;
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 dac_from_der_parses_happy_path() {
let dac = Dac::from_der(DAC_DER).expect("happy-path DAC parses");
assert_eq!(dac.subject_vid(), VendorId::new(0xFFF1));
assert_eq!(dac.subject_pid(), ProductId::new(0x8000));
}
#[test]
#[allow(clippy::unwrap_used)] fn dac_round_trips_der_bytes() {
let dac = Dac::from_der(DAC_DER).unwrap();
assert_eq!(dac.der(), DAC_DER);
}
#[test]
#[allow(clippy::unwrap_used)] fn dac_public_key_is_sec1_uncompressed_p256() {
let dac = Dac::from_der(DAC_DER).unwrap();
let pk = dac.public_key();
assert_eq!(pk.len(), 65, "P-256 uncompressed SPKI must be 65 bytes");
assert_eq!(pk[0], 0x04, "leading byte must be 0x04 (uncompressed)");
}
#[test]
#[allow(clippy::expect_used)] fn dac_from_der_rejects_empty_input() {
let err = Dac::from_der(&[]).expect_err("empty bytes must error");
assert!(matches!(err, AttestationError::Parse(_)));
}
#[test]
#[allow(clippy::expect_used)] fn dac_from_der_rejects_truncated_input() {
let err = Dac::from_der(&DAC_DER[..10]).expect_err("truncated must error");
assert!(matches!(err, AttestationError::Parse(_)));
}
#[test]
#[allow(clippy::expect_used)] fn dac_from_der_rejects_random_bytes() {
let garbage = vec![0xAA; 256];
let err = Dac::from_der(&garbage).expect_err("garbage must error");
assert!(matches!(err, AttestationError::Parse(_)));
}
const PAI_DER: &[u8] = include_bytes!(
"../../../../test-vectors/certs/attestation/happy-path/Chip-Test-PAI-FFF1-8000-Cert.der"
);
#[test]
#[allow(clippy::expect_used)] fn pai_from_der_parses_happy_path() {
let pai = Pai::from_der(PAI_DER).expect("happy-path PAI parses");
assert_eq!(pai.subject_vid(), VendorId::new(0xFFF1));
assert_eq!(pai.subject_pid(), Some(ProductId::new(0x8000)));
}
#[test]
#[allow(clippy::unwrap_used)] fn pai_round_trips_der_bytes() {
let pai = Pai::from_der(PAI_DER).unwrap();
assert_eq!(pai.der(), PAI_DER);
}
#[test]
#[allow(clippy::unwrap_used)] fn pai_public_key_is_sec1_uncompressed_p256() {
let pai = Pai::from_der(PAI_DER).unwrap();
let pk = pai.public_key();
assert_eq!(pk.len(), 65);
assert_eq!(pk[0], 0x04);
}
#[test]
#[allow(clippy::expect_used)] fn pai_from_der_rejects_garbage() {
let err = Pai::from_der(&vec![0xAA; 256]).expect_err("garbage must error");
assert!(matches!(err, AttestationError::Parse(_)));
}
const PAA_FFF1_DER: &[u8] = include_bytes!("csa_test_roots/Chip-Test-PAA-FFF1-Cert.der");
const PAA_NOVID_DER: &[u8] = include_bytes!("csa_test_roots/Chip-Test-PAA-NoVID-Cert.der");
#[test]
#[allow(clippy::expect_used)] fn paa_from_der_parses_vid_scoped_root() {
let paa = Paa::from_der(PAA_FFF1_DER).expect("VID-scoped PAA parses");
assert_eq!(paa.subject_vid(), Some(VendorId::new(0xFFF1)));
}
#[test]
#[allow(clippy::expect_used)] fn paa_from_der_parses_unscoped_root() {
let paa = Paa::from_der(PAA_NOVID_DER).expect("non-VID-scoped PAA parses");
assert_eq!(paa.subject_vid(), None);
}
#[test]
#[allow(clippy::unwrap_used)] fn paa_round_trips_der_bytes() {
let paa = Paa::from_der(PAA_FFF1_DER).unwrap();
assert_eq!(paa.der(), PAA_FFF1_DER);
}
#[test]
#[allow(clippy::unwrap_used)] fn paa_public_key_is_sec1_uncompressed_p256() {
let paa = Paa::from_der(PAA_FFF1_DER).unwrap();
assert_eq!(paa.public_key().len(), 65);
assert_eq!(paa.public_key()[0], 0x04);
}
#[test]
#[allow(clippy::expect_used)] fn paa_from_der_rejects_garbage() {
let err = Paa::from_der(&vec![0xAA; 256]).expect_err("garbage must error");
assert!(matches!(err, AttestationError::Parse(_)));
}
}