use x509_parser::extensions::ParsedExtension;
use x509_parser::prelude::{FromDer, X509Certificate};
use x509_parser::x509::X509Version;
use crate::attestation::error::AttestationError;
#[rustfmt::skip]
const OID_ECDSA_WITH_SHA256: x509_parser::der_parser::oid::Oid<'static> =
x509_parser::der_parser::oid!(1.2.840.10045.4.3.2);
const KEY_ID_LEN: usize = 20;
const KU_DIGITAL_SIGNATURE: u16 = 1 << 0;
const KU_KEY_CERT_SIGN: u16 = 1 << 5;
const KU_CRL_SIGN: u16 = 1 << 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CertRole {
Dac,
Pai,
#[allow(dead_code)]
Paa,
}
pub(crate) fn verify_attestation_cert_format(
der: &[u8],
role: CertRole,
) -> Result<(), AttestationError> {
let (_, cert) =
X509Certificate::from_der(der).map_err(|e| AttestationError::Parse(Box::new(e.clone())))?;
if cert.version() != X509Version::V3 {
return Err(fmt("certificate is not X.509 v3"));
}
if cert.signature_algorithm.algorithm != OID_ECDSA_WITH_SHA256 {
return Err(fmt("signature algorithm is not ecdsa-with-SHA256"));
}
let mut basic = false;
let mut key_usage = false;
let mut skid = false;
let mut akid = false;
for ext in cert.extensions() {
match ext.parsed_extension() {
ParsedExtension::BasicConstraints(bc) => {
if basic || !ext.critical {
return Err(AttestationError::BasicConstraintsViolation);
}
basic = true;
let ok = match role {
CertRole::Dac => !bc.ca && bc.path_len_constraint.is_none(),
CertRole::Pai => bc.ca && bc.path_len_constraint == Some(0),
CertRole::Paa => bc.ca && matches!(bc.path_len_constraint, None | Some(1)),
};
if !ok {
return Err(AttestationError::BasicConstraintsViolation);
}
}
ParsedExtension::KeyUsage(ku) => {
if key_usage || !ext.critical {
return Err(fmt("KeyUsage absent, non-critical, or duplicated"));
}
key_usage = true;
let ok = match role {
CertRole::Dac => ku.flags == KU_DIGITAL_SIGNATURE,
CertRole::Pai | CertRole::Paa => {
ku.flags & KU_KEY_CERT_SIGN != 0
&& ku.flags & KU_CRL_SIGN != 0
&& ku.flags & !(KU_DIGITAL_SIGNATURE | KU_KEY_CERT_SIGN | KU_CRL_SIGN)
== 0
}
};
if !ok {
return Err(fmt("KeyUsage bits are wrong for the certificate role"));
}
}
ParsedExtension::SubjectKeyIdentifier(kid) => {
if skid || ext.critical {
return Err(fmt("SubjectKeyIdentifier duplicated or critical"));
}
skid = true;
if kid.0.len() != KEY_ID_LEN {
return Err(fmt("SubjectKeyIdentifier is not 20 bytes"));
}
}
ParsedExtension::AuthorityKeyIdentifier(a) => {
if akid || ext.critical {
return Err(fmt("AuthorityKeyIdentifier duplicated or critical"));
}
akid = true;
match a.key_identifier.as_ref() {
Some(k) if k.0.len() == KEY_ID_LEN => {}
_ => {
return Err(fmt(
"AuthorityKeyIdentifier keyIdentifier absent or not 20 bytes",
))
}
}
}
_ => {}
}
}
if !(basic && key_usage && skid) {
return Err(fmt(
"missing a mandatory extension (BasicConstraints / KeyUsage / SubjectKeyIdentifier)",
));
}
if matches!(role, CertRole::Dac | CertRole::Pai) && !akid {
return Err(fmt("DAC/PAI missing the mandatory AuthorityKeyIdentifier"));
}
Ok(())
}
#[inline]
fn fmt(reason: &'static str) -> AttestationError {
AttestationError::CertFormatViolation { reason }
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
use super::*;
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"
);
macro_rules! fx {
($n:literal) => {
include_bytes!(concat!(
"../../../../test-vectors/certs/attestation/format/",
$n,
".der"
))
};
}
#[test]
fn real_happy_path_certs_pass() {
verify_attestation_cert_format(HAPPY_DAC, CertRole::Dac).unwrap();
verify_attestation_cert_format(HAPPY_PAI, CertRole::Pai).unwrap();
}
#[test]
fn synthetic_well_formed_certs_pass() {
verify_attestation_cert_format(fx!("dac-valid"), CertRole::Dac).unwrap();
verify_attestation_cert_format(fx!("pai-valid"), CertRole::Pai).unwrap();
}
#[test]
fn dac_with_keycertsign_bit_is_rejected() {
assert!(matches!(
verify_attestation_cert_format(fx!("dac-keycertsign"), CertRole::Dac),
Err(AttestationError::CertFormatViolation { .. })
));
}
#[test]
fn dac_missing_skid_is_rejected() {
assert!(matches!(
verify_attestation_cert_format(fx!("dac-missing-skid"), CertRole::Dac),
Err(AttestationError::CertFormatViolation { .. })
));
}
#[test]
fn dac_missing_akid_is_rejected() {
assert!(matches!(
verify_attestation_cert_format(fx!("dac-missing-akid"), CertRole::Dac),
Err(AttestationError::CertFormatViolation { .. })
));
}
#[test]
fn dac_keyusage_not_critical_is_rejected() {
assert!(matches!(
verify_attestation_cert_format(fx!("dac-ku-not-critical"), CertRole::Dac),
Err(AttestationError::CertFormatViolation { .. })
));
}
#[test]
fn dac_marked_as_ca_is_basic_constraints_violation() {
assert!(matches!(
verify_attestation_cert_format(fx!("dac-is-ca"), CertRole::Dac),
Err(AttestationError::BasicConstraintsViolation)
));
}
#[test]
fn pai_pathlen_nonzero_is_basic_constraints_violation() {
assert!(matches!(
verify_attestation_cert_format(fx!("pai-pathlen-nonzero"), CertRole::Pai),
Err(AttestationError::BasicConstraintsViolation)
));
}
#[test]
fn pai_not_ca_is_basic_constraints_violation() {
assert!(matches!(
verify_attestation_cert_format(fx!("pai-not-ca"), CertRole::Pai),
Err(AttestationError::BasicConstraintsViolation)
));
}
#[test]
fn pai_without_crlsign_is_rejected() {
assert!(matches!(
verify_attestation_cert_format(fx!("pai-missing-crlsign"), CertRole::Pai),
Err(AttestationError::CertFormatViolation { .. })
));
}
#[test]
fn real_pai_checked_under_dac_role_is_rejected() {
assert!(verify_attestation_cert_format(HAPPY_PAI, CertRole::Dac).is_err());
}
}