#![forbid(unsafe_code)]
use core::time::Duration;
use matter_cert::time::MatterTime;
use rustls_pki_types::{CertificateDer, SignatureVerificationAlgorithm, TrustAnchor, UnixTime};
use webpki::{EndEntityCert, KeyUsage};
use crate::attestation::error::{map_webpki_error, AttestationError};
use crate::attestation::extensions::{ProductId, VendorId};
use crate::attestation::trust_store::PaaTrustStore;
use crate::attestation::x509::{Dac, Paa, Pai};
static MATTER_SIG_ALGS: &[&dyn SignatureVerificationAlgorithm] = &[webpki::ring::ECDSA_P256_SHA256];
pub(crate) fn paa_to_trust_anchor(paa: &Paa) -> Result<TrustAnchor<'static>, AttestationError> {
let cert_der = CertificateDer::from(paa.der());
webpki::anchor_from_trusted_cert(&cert_der)
.map(|anchor| anchor.to_owned())
.map_err(|e| AttestationError::Parse(Box::new(e)))
}
#[derive(Debug, Clone)]
pub struct ChainVerification {
pub vendor_id: VendorId,
pub product_id: ProductId,
pub dac_public_key: Vec<u8>,
pub paa_subject: Vec<u8>,
}
pub fn verify_chain(
dac: &Dac,
pai: &Pai,
trust_store: &PaaTrustStore,
at: MatterTime,
) -> Result<ChainVerification, AttestationError> {
let anchors: Vec<TrustAnchor<'static>> = trust_store
.iter()
.map(paa_to_trust_anchor)
.collect::<Result<Vec<_>, _>>()?;
let dac_der = CertificateDer::from(dac.der());
let pai_der = CertificateDer::from(pai.der());
let intermediates = [pai_der];
let end_entity = EndEntityCert::try_from(&dac_der).map_err(map_webpki_error)?;
let now = UnixTime::since_unix_epoch(Duration::from_secs(at.to_unix_secs()));
end_entity
.verify_for_usage(
MATTER_SIG_ALGS,
&anchors,
&intermediates,
now,
KeyUsage::client_auth(),
None,
None,
)
.map_err(map_webpki_error)?;
let dac_vid = dac.subject_vid();
let pai_vid = pai.subject_vid();
if dac_vid != pai_vid {
return Err(AttestationError::VidMismatch {
dac: dac_vid,
pai: pai_vid,
});
}
if let Some(pai_pid) = pai.subject_pid() {
if pai_pid != dac.subject_pid() {
return Err(AttestationError::PaiVidNotAuthorized);
}
}
let pai_issuer_contents =
strip_sequence_wrapper(pai.issuer_raw()).ok_or(AttestationError::UntrustedRoot)?;
let (anchoring_paa, paa_subject) = trust_store
.iter()
.find_map(|paa| {
let anchor = paa_to_trust_anchor(paa).ok()?;
if anchor.subject.as_ref() == pai_issuer_contents {
Some((paa, anchor.subject.as_ref().to_vec()))
} else {
None
}
})
.ok_or(AttestationError::UntrustedRoot)?;
if let Some(paa_vid) = anchoring_paa.subject_vid() {
if paa_vid != dac_vid {
return Err(AttestationError::PaaVidScopeMismatch { paa_vid, dac_vid });
}
}
Ok(ChainVerification {
vendor_id: dac_vid,
product_id: dac.subject_pid(),
dac_public_key: dac.public_key().to_vec(),
paa_subject,
})
}
fn strip_sequence_wrapper(bytes: &[u8]) -> Option<&[u8]> {
let (&tag, rest) = bytes.split_first()?;
if tag != 0x30 {
return None;
}
let (&first_len_byte, after_first) = rest.split_first()?;
let (content_len, header_bytes) = match first_len_byte {
n if n < 0x80 => (n as usize, 0_usize),
0x81 => {
let (&len, _) = after_first.split_first()?;
(len as usize, 1)
}
0x82 => {
let len_bytes: &[u8; 2] = after_first.get(..2)?.try_into().ok()?;
(u16::from_be_bytes(*len_bytes) as usize, 2)
}
_ => return None,
};
let content_start = 2 + header_bytes;
let content_end = content_start.checked_add(content_len)?;
bytes.get(content_start..content_end)
}
#[cfg(test)]
mod tests {
#![allow(clippy::similar_names, clippy::struct_field_names)]
use super::*;
use crate::attestation::PaaTrustStore;
const HAPPY_DAC: &[u8] = include_bytes!(
"../../../../test-vectors/certs/attestation/happy-path/Chip-Test-DAC-FFF1-8000-0004-Cert.der"
);
const HAPPY_PAI: &[u8] = include_bytes!(
"../../../../test-vectors/certs/attestation/happy-path/Chip-Test-PAI-FFF1-8000-Cert.der"
);
#[test]
#[allow(clippy::unwrap_used)] fn paa_to_trust_anchor_works_on_bundled_csa_root() {
let store = PaaTrustStore::with_csa_test_roots();
let paa = store.iter().next().unwrap();
let _anchor = paa_to_trust_anchor(paa).unwrap();
}
#[test]
#[allow(clippy::expect_used, clippy::unwrap_used)] fn verify_chain_happy_path_on_csa_test_vectors() {
let dac = Dac::from_der(HAPPY_DAC).unwrap();
let pai = Pai::from_der(HAPPY_PAI).unwrap();
let store = PaaTrustStore::with_csa_test_roots();
let at = MatterTime::from_unix_secs(1_704_067_200);
let result = verify_chain(&dac, &pai, &store, at).expect("happy-path verify_chain");
assert_eq!(result.vendor_id, VendorId::new(0xFFF1));
assert_eq!(result.product_id, ProductId::new(0x8000));
assert_eq!(result.dac_public_key.len(), 65);
assert!(!result.paa_subject.is_empty());
}
use matter_cert::test_support::{build_x509_der, TestCertFields};
use matter_cert::{
BasicConstraints, DistinguishedName, DnAttribute, Extensions, KeyUsage, Signature,
};
use matter_crypto::{CaseSigner as _, RingSigner};
const EKU_CLIENT_AUTH: u32 = 2;
struct SyntheticChain {
dac_der: Vec<u8>,
pai_der: Vec<u8>,
paa_der: Vec<u8>,
}
#[allow(clippy::expect_used)] fn build_synthetic_chain(
at_unix: u64,
paa_vid: Option<u16>,
device_vid: u16,
device_pid: u16,
) -> SyntheticChain {
let (paa_signer, paa_pkcs8) = RingSigner::generate().expect("PAA key");
let mut paa_attrs = vec![DnAttribute::CommonName("Synthetic Test PAA".into())];
if let Some(v) = paa_vid {
paa_attrs.push(DnAttribute::VendorId(v));
}
let paa_dn = DistinguishedName::new(paa_attrs);
let paa_der = build_x509_der(
TestCertFields {
serial: vec![0x01],
issuer: paa_dn.clone(),
not_before: MatterTime::from_unix_secs(at_unix.saturating_sub(365 * 86_400)),
not_after: MatterTime::from_unix_secs(at_unix.saturating_add(3650 * 86_400)),
subject: paa_dn.clone(),
public_key: paa_signer.public_key().clone(),
extensions: Extensions::builder()
.basic_constraints(Some(BasicConstraints::new(true, Some(1))))
.key_usage(Some(KeyUsage::KEY_CERT_SIGN | KeyUsage::CRL_SIGN))
.build(),
signature: Signature::new([0u8; 64]),
},
&paa_pkcs8,
)
.expect("PAA DER");
let (pai_signer, pai_pkcs8) = RingSigner::generate().expect("PAI key");
let pai_dn = DistinguishedName::new(vec![
DnAttribute::CommonName("Synthetic Test PAI".into()),
DnAttribute::VendorId(device_vid),
]);
let pai_der = build_x509_der(
TestCertFields {
serial: vec![0x02],
issuer: paa_dn,
not_before: MatterTime::from_unix_secs(at_unix.saturating_sub(180 * 86_400)),
not_after: MatterTime::from_unix_secs(at_unix.saturating_add(1825 * 86_400)),
subject: pai_dn.clone(),
public_key: pai_signer.public_key().clone(),
extensions: Extensions::builder()
.basic_constraints(Some(BasicConstraints::new(true, Some(0))))
.key_usage(Some(KeyUsage::KEY_CERT_SIGN | KeyUsage::CRL_SIGN))
.build(),
signature: Signature::new([0u8; 64]),
},
&paa_pkcs8,
)
.expect("PAI DER");
let (dac_signer, _dac_pkcs8) = RingSigner::generate().expect("DAC key");
let dac_dn = DistinguishedName::new(vec![
DnAttribute::CommonName("Synthetic Test DAC".into()),
DnAttribute::VendorId(device_vid),
DnAttribute::ProductId(device_pid),
]);
let dac_der = build_x509_der(
TestCertFields {
serial: vec![0x03],
issuer: pai_dn,
not_before: MatterTime::from_unix_secs(at_unix.saturating_sub(30 * 86_400)),
not_after: MatterTime::from_unix_secs(at_unix.saturating_add(365 * 86_400)),
subject: dac_dn,
public_key: dac_signer.public_key().clone(),
extensions: Extensions::builder()
.basic_constraints(Some(BasicConstraints::new(false, None)))
.key_usage(Some(KeyUsage::DIGITAL_SIGNATURE))
.extended_key_usage(Some(vec![EKU_CLIENT_AUTH]))
.build(),
signature: Signature::new([0u8; 64]),
},
&pai_pkcs8,
)
.expect("DAC DER");
SyntheticChain {
dac_der,
pai_der,
paa_der,
}
}
#[allow(clippy::expect_used)] fn store_with(paa_der: &[u8]) -> PaaTrustStore {
let mut store = PaaTrustStore::empty();
store.add(Paa::from_der(paa_der).expect("synthetic PAA parses"));
store
}
const SYNTH_AT_UNIX: u64 = 1_800_000_000;
#[test]
#[allow(clippy::expect_used)] fn vid_scoped_paa_matching_device_vid_is_accepted() {
let chain = build_synthetic_chain(SYNTH_AT_UNIX, Some(0xFFF1), 0xFFF1, 0x8001);
let dac = Dac::from_der(&chain.dac_der).expect("DAC parses");
let pai = Pai::from_der(&chain.pai_der).expect("PAI parses");
let store = store_with(&chain.paa_der);
let at = MatterTime::from_unix_secs(SYNTH_AT_UNIX);
let result =
verify_chain(&dac, &pai, &store, at).expect("matching VID-scoped PAA accepted");
assert_eq!(result.vendor_id, VendorId::new(0xFFF1));
}
#[test]
#[allow(clippy::expect_used)] fn vid_scoped_paa_mismatched_device_vid_is_rejected() {
let chain = build_synthetic_chain(SYNTH_AT_UNIX, Some(0xFFF2), 0xFFF1, 0x8001);
let dac = Dac::from_der(&chain.dac_der).expect("DAC parses");
let pai = Pai::from_der(&chain.pai_der).expect("PAI parses");
let store = store_with(&chain.paa_der);
let at = MatterTime::from_unix_secs(SYNTH_AT_UNIX);
let err = verify_chain(&dac, &pai, &store, at)
.expect_err("VID-scoped PAA anchoring a different vendor must be rejected");
assert!(
matches!(
err,
AttestationError::PaaVidScopeMismatch {
paa_vid,
dac_vid,
} if paa_vid == VendorId::new(0xFFF2) && dac_vid == VendorId::new(0xFFF1)
),
"expected PaaVidScopeMismatch {{ paa_vid: FFF2, dac_vid: FFF1 }}, got {err:?}"
);
}
#[test]
#[allow(clippy::expect_used)] fn non_vid_scoped_paa_imposes_no_vid_constraint() {
let chain = build_synthetic_chain(SYNTH_AT_UNIX, None, 0xFFF1, 0x8001);
let dac = Dac::from_der(&chain.dac_der).expect("DAC parses");
let pai = Pai::from_der(&chain.pai_der).expect("PAI parses");
let store = store_with(&chain.paa_der);
let at = MatterTime::from_unix_secs(SYNTH_AT_UNIX);
let result =
verify_chain(&dac, &pai, &store, at).expect("non-VID-scoped PAA imposes no constraint");
assert_eq!(result.vendor_id, VendorId::new(0xFFF1));
}
}