use super::{
Check, DigestedObject, IssuerForm, Reason, charge, check_name, check_names, directory, proxy,
refused, target,
};
use crate::attribute_certificate::AttributeCertificate;
use crate::{Error, Result};
use structured_asn1::memory::{MemoryError, Work};
use structured_asn1::{Oid, Tag, universal};
use structured_pkix::access::{DistributionPoints, InfoAccess};
use structured_pkix::general_name::GeneralName;
use structured_pkix::identifier::AuthorityKeyIdentifier;
use structured_pkix::{Extension, oid};
pub const AUDIT_IDENTITY: Oid<'static> = Oid::from_bytes(&[43, 6, 1, 5, 5, 7, 1, 4]);
pub const NO_REV_AVAILABLE: Oid<'static> = Oid::from_bytes(&[85, 29, 56]);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Revocation {
NoRevAvailable,
Required,
}
#[derive(Debug)]
pub struct Profile<'r, 'a> {
certificate: &'r AttributeCertificate<'a>,
audit: Option<&'a [u8]>,
targets: Option<target::TargetInformation<'a>>,
proxy: Option<proxy::ProxyInfo<'a>>,
authority_key: Option<AuthorityKeyIdentifier<'a>>,
access: Option<InfoAccess<'a>>,
distribution: Option<DistributionPoints<'a>>,
revocation: Revocation,
}
impl<'r, 'a> Profile<'r, 'a> {
fn new(certificate: &'r AttributeCertificate<'a>) -> Self {
Self {
certificate,
audit: None,
targets: None,
proxy: None,
authority_key: None,
access: None,
distribution: None,
revocation: Revocation::Required,
}
}
#[must_use]
pub fn certificate(&self) -> &AttributeCertificate<'a> {
self.certificate
}
#[must_use]
pub fn audit_identity(&self) -> Option<&'a [u8]> {
self.audit
}
#[must_use]
pub fn targeting(&self) -> Option<&target::TargetInformation<'a>> {
self.targets.as_ref()
}
#[must_use]
pub fn proxying(&self) -> Option<&proxy::ProxyInfo<'a>> {
self.proxy.as_ref()
}
#[must_use]
pub fn authority_key(&self) -> Option<&AuthorityKeyIdentifier<'a>> {
self.authority_key.as_ref()
}
#[must_use]
pub fn access(&self) -> Option<&InfoAccess<'a>> {
self.access.as_ref()
}
#[must_use]
pub fn distribution(&self) -> Option<&DistributionPoints<'a>> {
self.distribution.as_ref()
}
#[must_use]
pub fn revocation(&self) -> Revocation {
self.revocation
}
#[must_use]
pub fn holder_alternatives(&self) -> u8 {
let holder = &self.certificate.information().holder;
u8::from(holder.base_certificate.is_some())
+ u8::from(holder.entity_names.is_some())
+ u8::from(holder.object_digest.is_some())
}
}
fn url(text: &str, schemes: &[&[u8]]) -> bool {
let bytes = text.as_bytes();
for scheme in schemes {
if bytes.len() > scheme.len()
&& bytes[..scheme.len()].eq_ignore_ascii_case(scheme)
&& (*scheme == b"ldap://" || !matches!(bytes[scheme.len()], b'/' | b'?' | b'#'))
{
return true;
}
}
false
}
fn critical<T>(extension: &Extension<'_>, expected: bool) -> Option<Check<T>> {
(extension.is_critical() != expected).then(|| refused(Reason::Criticality, extension.span()))
}
fn check_fields<W: Work>(ac: &AttributeCertificate<'_>, work: &W) -> Result<Check<()>> {
let at = ac.signed_element().span();
let info = ac.information();
charge(work, info.serial.as_bytes().len() as u64 + 8, at.start)?;
if info.issuer.form != IssuerForm::V2
|| info.issuer.base_certificate.is_some()
|| info.issuer.object_digest.is_some()
|| info.issuer.names.as_deref().and_then(directory).is_none()
{
return Ok(refused(Reason::IssuerForm, at));
}
if info.serial.is_negative()
|| info.serial.as_bytes().len() > 20
|| info.serial.as_bytes().iter().all(|b| *b == 0)
{
return Ok(refused(Reason::Serial, at));
}
if info.not_before > info.not_after {
return Ok(refused(Reason::Validity, at));
}
if let Some(base) = &info.holder.base_certificate
&& directory(&base.issuer).is_none()
{
return Ok(refused(Reason::HolderIssuer, at));
}
if let Some(names) = &info.holder.entity_names
&& let rejected @ Check::Rejected(_) = check_names(names, work, at)?
{
return Ok(rejected);
}
if let Some(digest) = &info.holder.object_digest
&& (digest.object_type == DigestedObject::Other || digest.other_type.is_some())
{
return Ok(refused(Reason::ObjectDigest, at));
}
Ok(Check::Accepted(()))
}
fn read_audit<'a>(extension: &Extension<'a>, work: &impl Work) -> Result<Check<&'a [u8]>> {
let mut reader = extension
.reader_scoped(work)
.map_err(|e| Error::encoding(e, super::FIELD))?;
let value = reader
.expect(Tag::primitive(universal::OCTET_STRING))
.map_err(|e| Error::encoding(e, super::FIELD))?;
super::super::end(&reader, super::FIELD)?;
if value.contents().is_empty() || value.contents().len() > 20 {
return Ok(refused(Reason::AuditIdentity, value.span()));
}
Ok(Check::Accepted(value.contents()))
}
fn read_access<'a>(
extension: &Extension<'a>,
work: &impl Work,
) -> Result<Check<(InfoAccess<'a>, bool)>> {
let at = extension.span();
let access = InfoAccess::read(extension, work).map_err(Error::pkix)?;
let mut pointer = false;
for entry in access.descriptions() {
charge(work, entry.method().as_bytes().len() as u64 + 1, at.start)?;
if let Check::Rejected(reason) = check_name(entry.location(), work, at)? {
return Ok(Check::Rejected(reason));
}
if entry.method() == oid::OCSP {
let GeneralName::Uri(uri) = entry.location() else {
return Ok(refused(Reason::OcspLocation, at));
};
charge(work, 8, at.start)?;
if !url(uri, &[b"http://"]) {
return Ok(refused(Reason::OcspLocation, at));
}
pointer = true;
}
}
Ok(Check::Accepted((access, pointer)))
}
fn read_distribution<'a>(
extension: &Extension<'a>,
work: &impl Work,
) -> Result<Check<DistributionPoints<'a>>> {
let at = extension.span();
let points = DistributionPoints::read(extension, work).map_err(Error::pkix)?;
let [point] = points.points() else {
return Ok(refused(Reason::DistributionPoint, at));
};
let valid = match point.full_name() {
[GeneralName::Directory(name)] => !name.is_empty(),
[GeneralName::Uri(uri)] => {
charge(work, 16, at.start)?;
url(uri, &[b"http://", b"ldap://"])
}
_ => false,
};
if !valid || point.name_relative_to_issuer().is_some() {
return Ok(refused(Reason::DistributionPoint, at));
}
Ok(Check::Accepted(points))
}
fn read_no_revocation(extension: &Extension<'_>, work: &impl Work) -> Result<()> {
let mut reader = extension
.reader_scoped(work)
.map_err(|e| Error::encoding(e, super::FIELD))?;
let value = reader
.expect(Tag::primitive(universal::NULL))
.map_err(|e| Error::encoding(e, super::FIELD))?;
reader
.null(&value)
.map_err(|e| Error::encoding(e, super::FIELD))?;
super::super::end(&reader, super::FIELD)
}
fn check_targets(
targets: &[target::Target<'_>],
certificates: &[target::TargetCertificate<'_>],
work: &impl Work,
) -> Result<Check<()>> {
if let Some(forbidden) = certificates.first() {
return Ok(refused(Reason::TargetCertificate, forbidden.element.span()));
}
if let Some(first) = targets.first() {
charge(work, targets.len() as u64, first.element.span().start)?;
}
for target in targets {
match &target.value {
target::Value::Name(name) | target::Value::Group(name) => {
if let rejected @ Check::Rejected(_) =
check_name(name, work, target.element.span())?
{
return Ok(rejected);
}
}
target::Value::Certificate(_) => {
return Ok(refused(Reason::TargetCertificate, target.element.span()));
}
}
}
Ok(Check::Accepted(()))
}
fn read_targeting<'a>(
extension: &Extension<'a>,
work: &impl Work,
) -> Result<Check<target::TargetInformation<'a>>> {
let targets = target::TargetInformation::read(extension, work)?;
if let Check::Rejected(reason) = check_targets(targets.targets(), targets.certificates(), work)?
{
return Ok(Check::Rejected(reason));
}
Ok(Check::Accepted(targets))
}
fn read_proxying<'a>(
extension: &Extension<'a>,
work: &impl Work,
) -> Result<Check<proxy::ProxyInfo<'a>>> {
let proxy = proxy::ProxyInfo::read(extension, work)?;
if let Check::Rejected(reason) = check_targets(proxy.targets(), proxy.certificates(), work)? {
return Ok(Check::Rejected(reason));
}
Ok(Check::Accepted(proxy))
}
fn read_authority_key<'a>(
extension: &Extension<'a>,
work: &impl Work,
) -> Result<Check<AuthorityKeyIdentifier<'a>>> {
let key = AuthorityKeyIdentifier::read(extension, work).map_err(Error::pkix)?;
if let Check::Rejected(reason) = check_names(key.issuer(), work, extension.span())? {
return Ok(Check::Rejected(reason));
}
if key.key_id().is_none() {
return Ok(refused(Reason::AuthorityKeyIdentifier, extension.span()));
}
Ok(Check::Accepted(key))
}
pub(super) fn check<'r, 'a, W: Work>(
ac: &'r AttributeCertificate<'a>,
work: &W,
) -> Result<Check<Profile<'r, 'a>>> {
let execution = work.execution_for(ac.parser);
if let Check::Rejected(reason) = check_fields(ac, &execution)? {
return Ok(Check::Rejected(reason));
}
let info = ac.information();
let at = ac.signed_element().span();
let mut facts = Profile::new(ac);
let Some(extensions) = &info.extensions else {
return Ok(Check::Accepted(facts));
};
let mut pointer = false;
for extension in extensions.iter(&execution) {
let extension = extension.map_err(Error::pkix)?;
let at = extension.span();
let dispatch_work = (extension.kind().as_bytes().len() as u64)
.checked_mul(8)
.and_then(|n| n.checked_add(8))
.ok_or_else(|| Error::resource(MemoryError::WorkExceeded, super::FIELD, at.start))?;
charge(&execution, dispatch_work, at.start)?;
match extension.kind() {
kind if kind == AUDIT_IDENTITY => {
if let Some(failure) = critical(extension, true) {
return Ok(failure);
}
facts.audit = Some(match read_audit(extension, &execution)? {
Check::Accepted(value) => value,
Check::Rejected(reason) => return Ok(Check::Rejected(reason)),
});
}
kind if kind == target::IDENTIFIER => {
if let Some(failure) = critical(extension, true) {
return Ok(failure);
}
facts.targets = Some(match read_targeting(extension, &execution)? {
Check::Accepted(value) => value,
Check::Rejected(reason) => return Ok(Check::Rejected(reason)),
});
}
kind if kind == proxy::IDENTIFIER => {
if let Some(failure) = critical(extension, true) {
return Ok(failure);
}
facts.proxy = Some(match read_proxying(extension, &execution)? {
Check::Accepted(value) => value,
Check::Rejected(reason) => return Ok(Check::Rejected(reason)),
});
}
kind if kind == oid::AUTHORITY_KEY_IDENTIFIER => {
if let Some(failure) = critical(extension, false) {
return Ok(failure);
}
facts.authority_key = Some(match read_authority_key(extension, &execution)? {
Check::Accepted(value) => value,
Check::Rejected(reason) => return Ok(Check::Rejected(reason)),
});
}
kind if kind == oid::AUTHORITY_INFO_ACCESS => {
if let Some(failure) = critical(extension, false) {
return Ok(failure);
}
let (access, has_pointer) = match read_access(extension, &execution)? {
Check::Accepted(value) => value,
Check::Rejected(reason) => return Ok(Check::Rejected(reason)),
};
pointer |= has_pointer;
facts.access = Some(access);
}
kind if kind == oid::CRL_DISTRIBUTION_POINTS => {
if let Some(failure) = critical(extension, false) {
return Ok(failure);
}
facts.distribution = Some(match read_distribution(extension, &execution)? {
Check::Accepted(value) => value,
Check::Rejected(reason) => return Ok(Check::Rejected(reason)),
});
pointer = true;
}
kind if kind == NO_REV_AVAILABLE => {
if let Some(failure) = critical(extension, false) {
return Ok(failure);
}
read_no_revocation(extension, &execution)?;
facts.revocation = Revocation::NoRevAvailable;
}
_ if extension.is_critical() => {
return Ok(refused(Reason::UnknownCritical, at));
}
_ => {}
}
}
if pointer && facts.revocation == Revocation::NoRevAvailable {
return Ok(refused(Reason::RevocationConflict, at));
}
Ok(Check::Accepted(facts))
}