use crate::{Error, RevocationChecker};
use der::{Decode as _, Encode as _};
use pkix_path::{names_match, SignatureVerifier, TrustAnchor};
use spki::der::referenced::OwnedToRef as _;
use x509_cert::{
crl::{CertificateList, RevokedCert},
ext::pkix::crl::CrlReason,
Certificate,
};
const OID_CRL_REASONS: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.21");
const OID_CRL_NUMBER: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.20");
const OID_DELTA_CRL_INDICATOR: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.27");
const OID_ISSUING_DISTRIBUTION_POINT: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.28");
const OID_CRL_DISTRIBUTION_POINTS: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.31");
const OID_KEY_USAGE_CRL: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.15");
const OID_CERTIFICATE_ISSUER: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.29");
#[derive(Clone, Debug)]
pub struct CrlChecker<V> {
crl: CertificateList,
delta_crl: Option<CertificateList>,
crl_issuer_cert: Option<Certificate>,
signer_discovered: bool,
now_unix: u64,
verifier: V,
}
impl<V: SignatureVerifier> CrlChecker<V> {
pub fn new(crl_der: impl AsRef<[u8]>, now_unix: u64, verifier: V) -> crate::Result<Self> {
let crl = CertificateList::from_der(crl_der.as_ref())
.map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
Ok(Self {
crl,
delta_crl: None,
crl_issuer_cert: None,
signer_discovered: false,
now_unix,
verifier,
})
}
pub fn new_with_crl_issuer(
crl_der: impl AsRef<[u8]>,
crl_issuer_cert: Certificate,
now_unix: u64,
verifier: V,
) -> crate::Result<Self> {
let crl = CertificateList::from_der(crl_der.as_ref())
.map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
Ok(Self {
crl,
delta_crl: None,
crl_issuer_cert: Some(crl_issuer_cert),
signer_discovered: false,
now_unix,
verifier,
})
}
pub fn new_with_signer_discovery(
crl_der: impl AsRef<[u8]>,
bundle: &[Certificate],
_cert_to_check: &Certificate,
now_unix: u64,
verifier: V,
) -> crate::Result<Self> {
let crl = CertificateList::from_der(crl_der.as_ref())
.map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
let signer = crate::signer_discovery::discover_crl_signer(bundle, &crl)
.ok_or(Error::CrlSignerNotFound)?;
check_crl_sign(signer)?;
if !crate::signer_discovery::reaches_self_signed(bundle, signer) {
return Err(Error::CrlSignerNotTrusted);
}
Ok(Self {
crl,
delta_crl: None,
crl_issuer_cert: Some(signer.clone()),
signer_discovered: true,
now_unix,
verifier,
})
}
pub fn with_delta(
base_der: impl AsRef<[u8]>,
delta_der: impl AsRef<[u8]>,
now_unix: u64,
verifier: V,
) -> crate::Result<Self> {
let base_crl = CertificateList::from_der(base_der.as_ref())
.map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
let delta_crl = CertificateList::from_der(delta_der.as_ref())
.map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
if has_delta_crl_indicator(&base_crl) {
return Err(Error::DeltaCrlBaseMismatch);
}
if !has_delta_crl_indicator(&delta_crl) {
return Err(Error::DeltaCrlBaseMismatch);
}
let delta_base_num = base_crl_number(&delta_crl)
.ok_or(Error::DeltaCrlBaseMismatch)? .map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
if !names_match(
&base_crl.tbs_cert_list.issuer,
&delta_crl.tbs_cert_list.issuer,
) {
return Err(Error::DeltaCrlBaseMismatch);
}
if let Some(base_num_result) = crl_number(&base_crl) {
let base_num = base_num_result.map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
if delta_base_num > base_num {
return Err(Error::CrlNumberMismatch);
}
}
Ok(Self {
crl: base_crl,
delta_crl: Some(delta_crl),
crl_issuer_cert: None,
signer_discovered: false,
now_unix,
verifier,
})
}
pub fn with_delta_and_crl_issuer(
base_der: impl AsRef<[u8]>,
delta_der: impl AsRef<[u8]>,
crl_issuer_cert: Certificate,
now_unix: u64,
verifier: V,
) -> crate::Result<Self> {
let mut checker = Self::with_delta(base_der, delta_der, now_unix, verifier)?;
checker.crl_issuer_cert = Some(crl_issuer_cert);
Ok(checker)
}
}
impl<V: SignatureVerifier> RevocationChecker for CrlChecker<V> {
fn check_revocation(&self, cert: &Certificate, issuer: &Certificate) -> crate::Result<()> {
let crl = &self.crl;
let parsed_idp = parse_issuing_dp(crl)?;
let crl_declares_indirect = parsed_idp
.as_ref()
.map(|idp| idp.indirect_crl)
.unwrap_or(false);
let signer_subject: &x509_cert::name::Name;
let signer_spki: spki::SubjectPublicKeyInfoRef<'_>;
let signer_for_crlsign_check: Option<&Certificate>;
match (&self.crl_issuer_cert, crl_declares_indirect) {
(Some(crl_issuer), true) => {
signer_subject = &crl_issuer.tbs_certificate.subject;
signer_spki = crl_issuer
.tbs_certificate
.subject_public_key_info
.owned_to_ref();
signer_for_crlsign_check = Some(crl_issuer);
}
(Some(discovered), false) if self.signer_discovered => {
signer_subject = &discovered.tbs_certificate.subject;
signer_spki = discovered
.tbs_certificate
.subject_public_key_info
.owned_to_ref();
signer_for_crlsign_check = Some(discovered);
}
(Some(_), false) => {
return Err(Error::IndirectCrlIssuerUnexpected);
}
(None, true) => {
return Err(Error::IndirectCrlIssuerMissing);
}
(None, false) => {
signer_subject = &issuer.tbs_certificate.subject;
signer_spki = issuer
.tbs_certificate
.subject_public_key_info
.owned_to_ref();
signer_for_crlsign_check = Some(issuer);
}
}
if !names_match(&crl.tbs_cert_list.issuer, signer_subject) {
return Err(Error::CrlIssuerMismatch);
}
if !names_match(
&issuer.tbs_certificate.subject,
&cert.tbs_certificate.issuer,
) {
return Err(Error::CrlIssuerMismatch);
}
if self.crl_issuer_cert.is_none()
&& !names_match(&cert.tbs_certificate.issuer, &crl.tbs_cert_list.issuer)
{
return Err(Error::CrlIssuerMismatch);
}
if let Some(signer_cert) = signer_for_crlsign_check {
check_crl_sign(signer_cert)?;
}
let this_update = crl.tbs_cert_list.this_update.to_unix_duration().as_secs();
if self.now_unix < this_update {
return Err(Error::CrlExpired);
}
let next_update = crl
.tbs_cert_list
.next_update
.as_ref()
.ok_or(Error::CrlExpired)?;
if self.now_unix > next_update.to_unix_duration().as_secs() {
return Err(Error::CrlExpired);
}
let tbs_bytes = crl
.tbs_cert_list
.to_der()
.map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
self.verifier
.verify_signature(
crl.signature_algorithm.owned_to_ref(),
signer_spki.clone(),
&tbs_bytes,
crl.signature.raw_bytes(),
)
.map_err(|_| Error::CrlSignatureInvalid)?;
if let Some(idp) = &parsed_idp {
if idp.only_contains_attribute_certs {
return Err(Error::OutOfScope(
crate::OutOfScopeReason::CrlOnlyAttributeCerts,
));
}
let cert_is_ca = cert_is_ca_cert(cert)?;
if idp.only_contains_user_certs && cert_is_ca {
return Err(Error::OutOfScope(crate::OutOfScopeReason::CrlOnlyUserCerts));
}
if idp.only_contains_ca_certs && !cert_is_ca {
return Err(Error::OutOfScope(crate::OutOfScopeReason::CrlOnlyCaCerts));
}
check_idp_dp_scope(cert, idp, &cert.tbs_certificate.issuer, signer_subject)?;
}
let delta_entries: &[RevokedCert] = if let Some(delta_crl) = &self.delta_crl {
if !names_match(&delta_crl.tbs_cert_list.issuer, &crl.tbs_cert_list.issuer) {
return Err(Error::CrlIssuerMismatch);
}
verify_delta_crl_and_collect(
delta_crl,
&self.verifier,
signer_spki,
signer_subject,
self.now_unix,
)?
} else {
&[]
};
let cert_issuer = &cert.tbs_certificate.issuer;
let cert_serial = &cert.tbs_certificate.serial_number;
let crl_default_issuer = &crl.tbs_cert_list.issuer;
let is_indirect = self.crl_issuer_cert.is_some();
check_revocation_status_indirect(
cert_issuer,
cert_serial,
delta_entries,
crl,
crl_default_issuer,
is_indirect,
)
}
fn check_revocation_against_anchor(
&self,
cert: &Certificate,
anchor: &TrustAnchor,
) -> crate::Result<()> {
let crl = &self.crl;
let parsed_idp = parse_issuing_dp(crl)?;
let crl_declares_indirect = parsed_idp
.as_ref()
.map(|idp| idp.indirect_crl)
.unwrap_or(false);
let signer_subject: &x509_cert::name::Name;
let signer_spki: spki::SubjectPublicKeyInfoRef<'_>;
let signer_for_crlsign_check: Option<&Certificate>;
match (&self.crl_issuer_cert, crl_declares_indirect) {
(Some(crl_issuer), true) => {
signer_subject = &crl_issuer.tbs_certificate.subject;
signer_spki = crl_issuer
.tbs_certificate
.subject_public_key_info
.owned_to_ref();
signer_for_crlsign_check = Some(crl_issuer);
}
(Some(discovered), false) if self.signer_discovered => {
signer_subject = &discovered.tbs_certificate.subject;
signer_spki = discovered
.tbs_certificate
.subject_public_key_info
.owned_to_ref();
signer_for_crlsign_check = Some(discovered);
}
(Some(_), false) => return Err(Error::IndirectCrlIssuerUnexpected),
(None, true) => return Err(Error::IndirectCrlIssuerMissing),
(None, false) => {
signer_subject = &anchor.subject;
signer_spki = anchor.subject_public_key_info.owned_to_ref();
signer_for_crlsign_check = None;
}
}
if !names_match(&crl.tbs_cert_list.issuer, signer_subject) {
return Err(Error::CrlIssuerMismatch);
}
if !names_match(&cert.tbs_certificate.issuer, &anchor.subject) {
return Err(Error::CrlIssuerMismatch);
}
if let Some(signer_cert) = signer_for_crlsign_check {
check_crl_sign(signer_cert)?;
}
let this_update = crl.tbs_cert_list.this_update.to_unix_duration().as_secs();
if self.now_unix < this_update {
return Err(Error::CrlExpired);
}
let next_update = crl
.tbs_cert_list
.next_update
.as_ref()
.ok_or(Error::CrlExpired)?;
if self.now_unix > next_update.to_unix_duration().as_secs() {
return Err(Error::CrlExpired);
}
let tbs_bytes = crl
.tbs_cert_list
.to_der()
.map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
self.verifier
.verify_signature(
crl.signature_algorithm.owned_to_ref(),
signer_spki.clone(),
&tbs_bytes,
crl.signature.raw_bytes(),
)
.map_err(|_| Error::CrlSignatureInvalid)?;
if let Some(idp) = &parsed_idp {
if idp.only_contains_attribute_certs {
return Err(Error::OutOfScope(
crate::OutOfScopeReason::CrlOnlyAttributeCerts,
));
}
let cert_is_ca = cert_is_ca_cert(cert)?;
if idp.only_contains_user_certs && cert_is_ca {
return Err(Error::OutOfScope(crate::OutOfScopeReason::CrlOnlyUserCerts));
}
if idp.only_contains_ca_certs && !cert_is_ca {
return Err(Error::OutOfScope(crate::OutOfScopeReason::CrlOnlyCaCerts));
}
check_idp_dp_scope(cert, idp, &cert.tbs_certificate.issuer, signer_subject)?;
}
let delta_entries: &[RevokedCert] = if let Some(delta_crl) = &self.delta_crl {
if !names_match(&delta_crl.tbs_cert_list.issuer, &crl.tbs_cert_list.issuer) {
return Err(Error::CrlIssuerMismatch);
}
verify_delta_crl_and_collect(
delta_crl,
&self.verifier,
signer_spki,
signer_subject,
self.now_unix,
)?
} else {
&[]
};
let cert_issuer = &cert.tbs_certificate.issuer;
let cert_serial = &cert.tbs_certificate.serial_number;
let crl_default_issuer = &crl.tbs_cert_list.issuer;
let is_indirect = self.crl_issuer_cert.is_some();
check_revocation_status_indirect(
cert_issuer,
cert_serial,
delta_entries,
crl,
crl_default_issuer,
is_indirect,
)
}
}
fn check_revocation_status_indirect(
cert_issuer: &x509_cert::name::Name,
cert_serial: &x509_cert::serial_number::SerialNumber,
delta_entries: &[RevokedCert],
crl: &CertificateList,
crl_default_issuer: &x509_cert::name::Name,
is_indirect: bool,
) -> crate::Result<()> {
if let Some(entry) = find_matching_entry(
cert_issuer,
cert_serial,
delta_entries,
crl_default_issuer,
is_indirect,
)? {
let reason = extract_reason_code(entry);
if reason == Some(CrlReason::RemoveFromCRL) {
return Ok(());
}
return Err(Error::Revoked {
serial: cert_serial.clone(),
reason_code: reason,
});
}
let base_entries: &[RevokedCert] = crl
.tbs_cert_list
.revoked_certificates
.as_deref()
.unwrap_or(&[]);
if let Some(entry) = find_matching_entry(
cert_issuer,
cert_serial,
base_entries,
crl_default_issuer,
is_indirect,
)? {
return Err(Error::Revoked {
serial: cert_serial.clone(),
reason_code: extract_reason_code(entry),
});
}
Ok(())
}
fn find_matching_entry<'a>(
cert_issuer: &x509_cert::name::Name,
cert_serial: &x509_cert::serial_number::SerialNumber,
entries: &'a [RevokedCert],
crl_default_issuer: &x509_cert::name::Name,
is_indirect: bool,
) -> crate::Result<Option<&'a RevokedCert>> {
use std::borrow::Cow;
let mut effective: Cow<'_, x509_cert::name::Name> = Cow::Borrowed(crl_default_issuer);
for entry in entries {
if is_indirect {
if let Some(exts) = entry.crl_entry_extensions.as_deref() {
if let Some(ce_ext) = exts.iter().find(|e| e.extn_id == OID_CERTIFICATE_ISSUER) {
effective = Cow::Owned(parse_certificate_issuer_dn(ce_ext.extn_value.as_bytes())?);
}
}
}
if &entry.serial_number == cert_serial && names_match(&effective, cert_issuer) {
return Ok(Some(entry));
}
}
Ok(None)
}
fn parse_certificate_issuer_dn(ext_value_der: &[u8]) -> crate::Result<x509_cert::name::Name> {
use x509_cert::ext::pkix::name::{GeneralName, GeneralNames};
let general_names = GeneralNames::from_der(ext_value_der)
.map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
for gn in general_names {
if let GeneralName::DirectoryName(name) = gn {
return Ok(name);
}
}
Err(Error::CrlParseError(crate::DerError::new(der::Error::new(
der::ErrorKind::Value { tag: der::Tag::Sequence },
der::Length::ZERO,
))))
}
fn verify_delta_crl_and_collect<'a, V: SignatureVerifier>(
delta_crl: &'a CertificateList,
verifier: &V,
issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
expected_issuer_name: &x509_cert::name::Name,
now_unix: u64,
) -> crate::Result<&'a [RevokedCert]> {
if !names_match(&delta_crl.tbs_cert_list.issuer, expected_issuer_name) {
return Err(Error::CrlIssuerMismatch);
}
let delta_this_update = delta_crl
.tbs_cert_list
.this_update
.to_unix_duration()
.as_secs();
if now_unix < delta_this_update {
return Err(Error::CrlExpired);
}
let delta_next_update = delta_crl
.tbs_cert_list
.next_update
.as_ref()
.ok_or(Error::CrlExpired)?;
if now_unix > delta_next_update.to_unix_duration().as_secs() {
return Err(Error::CrlExpired);
}
let delta_tbs_bytes = delta_crl
.tbs_cert_list
.to_der()
.map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
verifier
.verify_signature(
delta_crl.signature_algorithm.owned_to_ref(),
issuer_spki,
&delta_tbs_bytes,
delta_crl.signature.raw_bytes(),
)
.map_err(|_| Error::CrlSignatureInvalid)?;
Ok(delta_crl
.tbs_cert_list
.revoked_certificates
.as_deref()
.unwrap_or(&[]))
}
fn uint_to_u64(n: &der::asn1::Uint) -> Option<u64> {
let b = n.as_bytes();
if b.len() > 8 {
return None; }
let mut arr = [0u8; 8];
arr[8 - b.len()..].copy_from_slice(b);
Some(u64::from_be_bytes(arr))
}
fn crl_number(crl: &CertificateList) -> Option<Result<u64, der::Error>> {
let ext = crl
.tbs_cert_list
.crl_extensions
.as_deref()
.unwrap_or(&[])
.iter()
.find(|e| e.extn_id == OID_CRL_NUMBER)?;
let result = der::asn1::Uint::from_der(ext.extn_value.as_bytes())
.and_then(|n| uint_to_u64(&n).ok_or_else(|| der::Error::from(der::ErrorKind::Overflow)));
Some(result)
}
fn has_delta_crl_indicator(crl: &CertificateList) -> bool {
crl.tbs_cert_list
.crl_extensions
.as_deref()
.unwrap_or(&[])
.iter()
.any(|e| e.extn_id == OID_DELTA_CRL_INDICATOR)
}
fn base_crl_number(crl: &CertificateList) -> Option<Result<u64, der::Error>> {
let ext = crl
.tbs_cert_list
.crl_extensions
.as_deref()
.unwrap_or(&[])
.iter()
.find(|e| e.extn_id == OID_DELTA_CRL_INDICATOR)?;
let result = der::asn1::Uint::from_der(ext.extn_value.as_bytes())
.and_then(|n| uint_to_u64(&n).ok_or_else(|| der::Error::from(der::ErrorKind::Overflow)));
Some(result)
}
fn check_crl_sign(cert: &Certificate) -> crate::Result<()> {
use x509_cert::ext::pkix::KeyUsage;
let Some(ku_ext) = cert
.tbs_certificate
.extensions
.as_deref()
.unwrap_or(&[])
.iter()
.find(|e| e.extn_id == OID_KEY_USAGE_CRL)
else {
return Ok(()); };
let ku = KeyUsage::from_der(ku_ext.extn_value.as_bytes())
.map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
if ku.crl_sign() {
Ok(())
} else {
Err(Error::CrlSignMissing)
}
}
fn extract_reason_code(entry: &RevokedCert) -> Option<CrlReason> {
let exts = entry.crl_entry_extensions.as_ref()?;
exts.iter()
.find(|ext| ext.extn_id == OID_CRL_REASONS)
.and_then(|ext| CrlReason::from_der(ext.extn_value.as_bytes()).ok())
}
fn parse_issuing_dp(
crl: &CertificateList,
) -> crate::Result<Option<x509_cert::ext::pkix::crl::IssuingDistributionPoint>> {
use x509_cert::ext::pkix::crl::IssuingDistributionPoint;
crl.tbs_cert_list
.crl_extensions
.as_deref()
.unwrap_or(&[])
.iter()
.find(|e| e.extn_id == OID_ISSUING_DISTRIBUTION_POINT)
.map(|e| {
IssuingDistributionPoint::from_der(e.extn_value.as_bytes())
.map_err(|err| Error::CrlParseError(crate::DerError::new(err)))
})
.transpose()
}
fn cert_crl_distribution_points(
cert: &Certificate,
) -> crate::Result<Option<x509_cert::ext::pkix::CrlDistributionPoints>> {
use x509_cert::ext::pkix::CrlDistributionPoints;
cert.tbs_certificate
.extensions
.as_deref()
.unwrap_or(&[])
.iter()
.find(|e| e.extn_id == OID_CRL_DISTRIBUTION_POINTS)
.map(|e| {
CrlDistributionPoints::from_der(e.extn_value.as_bytes())
.map_err(|err| Error::CrlParseError(crate::DerError::new(err)))
})
.transpose()
}
fn dpn_to_general_names<'a>(
dpn: &'a x509_cert::ext::pkix::name::DistributionPointName,
base_dn: &x509_cert::name::Name,
) -> crate::Result<std::borrow::Cow<'a, [x509_cert::ext::pkix::name::GeneralName]>> {
use std::borrow::Cow;
use x509_cert::ext::pkix::name::{DistributionPointName, GeneralName};
match dpn {
DistributionPointName::FullName(names) => Ok(Cow::Borrowed(names.as_slice())),
DistributionPointName::NameRelativeToCRLIssuer(rdn) => {
let mut full_name = base_dn.clone();
full_name.0.push(rdn.clone());
Ok(Cow::Owned(vec![GeneralName::DirectoryName(full_name)]))
}
}
}
fn general_name_matches(
a: &x509_cert::ext::pkix::name::GeneralName,
b: &x509_cert::ext::pkix::name::GeneralName,
) -> bool {
use x509_cert::ext::pkix::name::GeneralName as GN;
match (a, b) {
(GN::DirectoryName(a_dn), GN::DirectoryName(b_dn)) => names_match(a_dn, b_dn),
_ => a == b,
}
}
fn general_names_intersect(
a: &[x509_cert::ext::pkix::name::GeneralName],
b: &[x509_cert::ext::pkix::name::GeneralName],
) -> bool {
a.iter()
.any(|ga| b.iter().any(|gb| general_name_matches(ga, gb)))
}
fn check_idp_dp_scope(
cert: &Certificate,
idp: &x509_cert::ext::pkix::crl::IssuingDistributionPoint,
cert_issuer: &x509_cert::name::Name,
crl_signer_subject: &x509_cert::name::Name,
) -> crate::Result<()> {
let cert_cdps = cert_crl_distribution_points(cert)?;
match (cert_cdps.as_ref(), idp.distribution_point.as_ref()) {
(None, None) | (Some(_), None) => Ok(()),
(None, Some(_)) => Err(Error::OutOfScope(
crate::OutOfScopeReason::CrlIdpDistributionPointMismatch,
)),
(Some(cert_cdps), Some(idp_dpn)) => {
let idp_names = dpn_to_general_names(idp_dpn, crl_signer_subject)?;
let matched = cert_cdps.0.iter().any(|dp| {
let Some(cdp_dpn) = &dp.distribution_point else {
return false;
};
let cdp_names = match dpn_to_general_names(cdp_dpn, cert_issuer) {
Ok(n) => n,
Err(_) => return false,
};
general_names_intersect(&cdp_names, &idp_names)
});
if matched {
Ok(())
} else {
Err(Error::OutOfScope(
crate::OutOfScopeReason::CrlIdpDistributionPointMismatch,
))
}
}
}
}
fn cert_is_ca_cert(cert: &Certificate) -> crate::Result<bool> {
pkix_path::cert_is_ca(cert).map_err(|_| Error::MalformedCertificate)
}