Skip to main content

structured_cms/attribute_certificate/validation/
profile.rs

1use super::{
2    Check, DigestedObject, IssuerForm, Reason, charge, check_name, check_names, directory, proxy,
3    refused, target,
4};
5use crate::attribute_certificate::AttributeCertificate;
6use crate::{Error, Result};
7use structured_asn1::memory::{MemoryError, Work};
8use structured_asn1::{Oid, Tag, universal};
9use structured_pkix::access::{DistributionPoints, InfoAccess};
10use structured_pkix::general_name::GeneralName;
11use structured_pkix::identifier::AuthorityKeyIdentifier;
12use structured_pkix::{Extension, oid};
13
14/// `id-pe-ac-auditIdentity`, RFC5755 §4.3.1.
15pub const AUDIT_IDENTITY: Oid<'static> = Oid::from_bytes(&[43, 6, 1, 5, 5, 7, 1, 4]);
16/// `id-ce-noRevAvail`, RFC5755 §4.3.6.
17pub const NO_REV_AVAILABLE: Oid<'static> = Oid::from_bytes(&[85, 29, 56]);
18
19/// The issuer's written revocation scheme, not an authenticated status answer.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Revocation {
22    /// A well-formed `NoRevAvail` without any contradictory revocation pointer.
23    NoRevAvailable,
24    /// Revocation checking remains required; absence of a pointer is no exemption.
25    Required,
26}
27
28/// Static profile facts borrowing the original AC and owning decoded catalogues.
29/// A critical extension's effect must still be enforced before using the AC.
30#[derive(Debug)]
31pub struct Profile<'r, 'a> {
32    certificate: &'r AttributeCertificate<'a>,
33    audit: Option<&'a [u8]>,
34    targets: Option<target::TargetInformation<'a>>,
35    proxy: Option<proxy::ProxyInfo<'a>>,
36    authority_key: Option<AuthorityKeyIdentifier<'a>>,
37    access: Option<InfoAccess<'a>>,
38    distribution: Option<DistributionPoints<'a>>,
39    revocation: Revocation,
40}
41
42impl<'r, 'a> Profile<'r, 'a> {
43    fn new(certificate: &'r AttributeCertificate<'a>) -> Self {
44        Self {
45            certificate,
46            audit: None,
47            targets: None,
48            proxy: None,
49            authority_key: None,
50            access: None,
51            distribution: None,
52            revocation: Revocation::Required,
53        }
54    }
55    /// Original AC, including exact signed bytes and unchanged attributes.
56    #[must_use]
57    pub fn certificate(&self) -> &AttributeCertificate<'a> {
58        self.certificate
59    }
60    /// Opaque original audit identity, for the caller's audit handling (§4.3.1).
61    /// Parsing does not establish unlinkability or enforce an audit policy.
62    #[must_use]
63    pub fn audit_identity(&self) -> Option<&'a [u8]> {
64        self.audit
65    }
66    /// Present targeting is a mandatory effect check, not satisfied by parsing.
67    #[must_use]
68    pub fn targeting(&self) -> Option<&target::TargetInformation<'a>> {
69        self.targets.as_ref()
70    }
71    /// Present proxying requires the authenticated sender/recipient/hop check.
72    #[must_use]
73    pub fn proxying(&self) -> Option<&proxy::ProxyInfo<'a>> {
74        self.proxy.as_ref()
75    }
76    /// Original authority key assertion, not proof that a supplied key belongs to it.
77    #[must_use]
78    pub fn authority_key(&self) -> Option<&AuthorityKeyIdentifier<'a>> {
79        self.authority_key.as_ref()
80    }
81    /// Original unresolved access locations; no acquisition occurred.
82    #[must_use]
83    pub fn access(&self) -> Option<&InfoAccess<'a>> {
84        self.access.as_ref()
85    }
86    /// Original unresolved single distribution point, if present.
87    #[must_use]
88    pub fn distribution(&self) -> Option<&DistributionPoints<'a>> {
89        self.distribution.as_ref()
90    }
91    /// Written revocation scheme. Required never means a successful status check.
92    #[must_use]
93    pub fn revocation(&self) -> Revocation {
94        self.revocation
95    }
96    /// Number of written Holder alternatives, for explicit protocol selection.
97    /// Zero cannot authenticate a holder; several are not silently prioritized.
98    #[must_use]
99    pub fn holder_alternatives(&self) -> u8 {
100        let holder = &self.certificate.information().holder;
101        u8::from(holder.base_certificate.is_some())
102            + u8::from(holder.entity_names.is_some())
103            + u8::from(holder.object_digest.is_some())
104    }
105}
106
107fn url(text: &str, schemes: &[&[u8]]) -> bool {
108    // GeneralName has already applied the shared absolute URI grammar. This
109    // only enforces RFC5755 §§4.3.4/4.3.5's schemes. HTTP needs a host;
110    // LDAP may omit its server (RFC4516 §2), with DN/CRL attributes already
111    // enforced by the shared DistributionPoints location decoder.
112    // <https://www.rfc-editor.org/rfc/rfc4516#section-2>
113    let bytes = text.as_bytes();
114    for scheme in schemes {
115        if bytes.len() > scheme.len()
116            && bytes[..scheme.len()].eq_ignore_ascii_case(scheme)
117            && (*scheme == b"ldap://" || !matches!(bytes[scheme.len()], b'/' | b'?' | b'#'))
118        {
119            return true;
120        }
121    }
122    false
123}
124
125fn critical<T>(extension: &Extension<'_>, expected: bool) -> Option<Check<T>> {
126    (extension.is_critical() != expected).then(|| refused(Reason::Criticality, extension.span()))
127}
128
129fn check_fields<W: Work>(ac: &AttributeCertificate<'_>, work: &W) -> Result<Check<()>> {
130    let at = ac.signed_element().span();
131    let info = ac.information();
132    charge(work, info.serial.as_bytes().len() as u64 + 8, at.start)?;
133    // RFC5755 §§4.2.3/4.2.5/4.2.6. These are profile checks, while
134    // readability and exact signature inputs remain available on the AC.
135    // <https://www.rfc-editor.org/rfc/rfc5755#section-4.2>
136    if info.issuer.form != IssuerForm::V2
137        || info.issuer.base_certificate.is_some()
138        || info.issuer.object_digest.is_some()
139        || info.issuer.names.as_deref().and_then(directory).is_none()
140    {
141        return Ok(refused(Reason::IssuerForm, at));
142    }
143    if info.serial.is_negative()
144        || info.serial.as_bytes().len() > 20
145        || info.serial.as_bytes().iter().all(|b| *b == 0)
146    {
147        return Ok(refused(Reason::Serial, at));
148    }
149    if info.not_before > info.not_after {
150        return Ok(refused(Reason::Validity, at));
151    }
152    if let Some(base) = &info.holder.base_certificate
153        && directory(&base.issuer).is_none()
154    {
155        return Ok(refused(Reason::HolderIssuer, at));
156    }
157    if let Some(names) = &info.holder.entity_names
158        && let rejected @ Check::Rejected(_) = check_names(names, work, at)?
159    {
160        return Ok(rejected);
161    }
162    if let Some(digest) = &info.holder.object_digest
163        && (digest.object_type == DigestedObject::Other || digest.other_type.is_some())
164    {
165        return Ok(refused(Reason::ObjectDigest, at));
166    }
167    Ok(Check::Accepted(()))
168}
169
170fn read_audit<'a>(extension: &Extension<'a>, work: &impl Work) -> Result<Check<&'a [u8]>> {
171    let mut reader = extension
172        .reader_scoped(work)
173        .map_err(|e| Error::encoding(e, super::FIELD))?;
174    let value = reader
175        .expect(Tag::primitive(universal::OCTET_STRING))
176        .map_err(|e| Error::encoding(e, super::FIELD))?;
177    super::super::end(&reader, super::FIELD)?;
178    if value.contents().is_empty() || value.contents().len() > 20 {
179        return Ok(refused(Reason::AuditIdentity, value.span()));
180    }
181    Ok(Check::Accepted(value.contents()))
182}
183
184fn read_access<'a>(
185    extension: &Extension<'a>,
186    work: &impl Work,
187) -> Result<Check<(InfoAccess<'a>, bool)>> {
188    let at = extension.span();
189    let access = InfoAccess::read(extension, work).map_err(Error::pkix)?;
190    let mut pointer = false;
191    for entry in access.descriptions() {
192        charge(work, entry.method().as_bytes().len() as u64 + 1, at.start)?;
193        if let Check::Rejected(reason) = check_name(entry.location(), work, at)? {
194            return Ok(Check::Rejected(reason));
195        }
196        if entry.method() == oid::OCSP {
197            let GeneralName::Uri(uri) = entry.location() else {
198                return Ok(refused(Reason::OcspLocation, at));
199            };
200            charge(work, 8, at.start)?;
201            if !url(uri, &[b"http://"]) {
202                return Ok(refused(Reason::OcspLocation, at));
203            }
204            pointer = true;
205        }
206    }
207    Ok(Check::Accepted((access, pointer)))
208}
209
210fn read_distribution<'a>(
211    extension: &Extension<'a>,
212    work: &impl Work,
213) -> Result<Check<DistributionPoints<'a>>> {
214    let at = extension.span();
215    let points = DistributionPoints::read(extension, work).map_err(Error::pkix)?;
216    let [point] = points.points() else {
217        return Ok(refused(Reason::DistributionPoint, at));
218    };
219    let valid = match point.full_name() {
220        [GeneralName::Directory(name)] => !name.is_empty(),
221        [GeneralName::Uri(uri)] => {
222            charge(work, 16, at.start)?;
223            url(uri, &[b"http://", b"ldap://"])
224        }
225        _ => false,
226    };
227    if !valid || point.name_relative_to_issuer().is_some() {
228        return Ok(refused(Reason::DistributionPoint, at));
229    }
230    Ok(Check::Accepted(points))
231}
232
233fn read_no_revocation(extension: &Extension<'_>, work: &impl Work) -> Result<()> {
234    let mut reader = extension
235        .reader_scoped(work)
236        .map_err(|e| Error::encoding(e, super::FIELD))?;
237    let value = reader
238        .expect(Tag::primitive(universal::NULL))
239        .map_err(|e| Error::encoding(e, super::FIELD))?;
240    reader
241        .null(&value)
242        .map_err(|e| Error::encoding(e, super::FIELD))?;
243    super::super::end(&reader, super::FIELD)
244}
245
246fn check_targets(
247    targets: &[target::Target<'_>],
248    certificates: &[target::TargetCertificate<'_>],
249    work: &impl Work,
250) -> Result<Check<()>> {
251    if let Some(forbidden) = certificates.first() {
252        return Ok(refused(Reason::TargetCertificate, forbidden.element.span()));
253    }
254    if let Some(first) = targets.first() {
255        charge(work, targets.len() as u64, first.element.span().start)?;
256    }
257    for target in targets {
258        match &target.value {
259            target::Value::Name(name) | target::Value::Group(name) => {
260                if let rejected @ Check::Rejected(_) =
261                    check_name(name, work, target.element.span())?
262                {
263                    return Ok(rejected);
264                }
265            }
266            target::Value::Certificate(_) => {
267                return Ok(refused(Reason::TargetCertificate, target.element.span()));
268            }
269        }
270    }
271    Ok(Check::Accepted(()))
272}
273
274fn read_targeting<'a>(
275    extension: &Extension<'a>,
276    work: &impl Work,
277) -> Result<Check<target::TargetInformation<'a>>> {
278    let targets = target::TargetInformation::read(extension, work)?;
279    if let Check::Rejected(reason) = check_targets(targets.targets(), targets.certificates(), work)?
280    {
281        return Ok(Check::Rejected(reason));
282    }
283    Ok(Check::Accepted(targets))
284}
285
286fn read_proxying<'a>(
287    extension: &Extension<'a>,
288    work: &impl Work,
289) -> Result<Check<proxy::ProxyInfo<'a>>> {
290    let proxy = proxy::ProxyInfo::read(extension, work)?;
291    if let Check::Rejected(reason) = check_targets(proxy.targets(), proxy.certificates(), work)? {
292        return Ok(Check::Rejected(reason));
293    }
294    Ok(Check::Accepted(proxy))
295}
296
297fn read_authority_key<'a>(
298    extension: &Extension<'a>,
299    work: &impl Work,
300) -> Result<Check<AuthorityKeyIdentifier<'a>>> {
301    let key = AuthorityKeyIdentifier::read(extension, work).map_err(Error::pkix)?;
302    if let Check::Rejected(reason) = check_names(key.issuer(), work, extension.span())? {
303        return Ok(Check::Rejected(reason));
304    }
305    // RFC5280 §4.2.1.1, incorporated by RFC5755 §4.3.3.
306    if key.key_id().is_none() {
307        return Ok(refused(Reason::AuthorityKeyIdentifier, extension.span()));
308    }
309    Ok(Check::Accepted(key))
310}
311
312pub(super) fn check<'r, 'a, W: Work>(
313    ac: &'r AttributeCertificate<'a>,
314    work: &W,
315) -> Result<Check<Profile<'r, 'a>>> {
316    let execution = work.execution_for(ac.parser);
317    if let Check::Rejected(reason) = check_fields(ac, &execution)? {
318        return Ok(Check::Rejected(reason));
319    }
320    let info = ac.information();
321    let at = ac.signed_element().span();
322    let mut facts = Profile::new(ac);
323    let Some(extensions) = &info.extensions else {
324        return Ok(Check::Accepted(facts));
325    };
326    let mut pointer = false;
327    for extension in extensions.iter(&execution) {
328        let extension = extension.map_err(Error::pkix)?;
329        let at = extension.span();
330        // Bound the fixed dispatch's OID scans before any equality test.
331        let dispatch_work = (extension.kind().as_bytes().len() as u64)
332            .checked_mul(8)
333            .and_then(|n| n.checked_add(8))
334            .ok_or_else(|| Error::resource(MemoryError::WorkExceeded, super::FIELD, at.start))?;
335        charge(&execution, dispatch_work, at.start)?;
336        match extension.kind() {
337            kind if kind == AUDIT_IDENTITY => {
338                if let Some(failure) = critical(extension, true) {
339                    return Ok(failure);
340                }
341                facts.audit = Some(match read_audit(extension, &execution)? {
342                    Check::Accepted(value) => value,
343                    Check::Rejected(reason) => return Ok(Check::Rejected(reason)),
344                });
345            }
346            kind if kind == target::IDENTIFIER => {
347                if let Some(failure) = critical(extension, true) {
348                    return Ok(failure);
349                }
350                facts.targets = Some(match read_targeting(extension, &execution)? {
351                    Check::Accepted(value) => value,
352                    Check::Rejected(reason) => return Ok(Check::Rejected(reason)),
353                });
354            }
355            kind if kind == proxy::IDENTIFIER => {
356                if let Some(failure) = critical(extension, true) {
357                    return Ok(failure);
358                }
359                facts.proxy = Some(match read_proxying(extension, &execution)? {
360                    Check::Accepted(value) => value,
361                    Check::Rejected(reason) => return Ok(Check::Rejected(reason)),
362                });
363            }
364            kind if kind == oid::AUTHORITY_KEY_IDENTIFIER => {
365                if let Some(failure) = critical(extension, false) {
366                    return Ok(failure);
367                }
368                facts.authority_key = Some(match read_authority_key(extension, &execution)? {
369                    Check::Accepted(value) => value,
370                    Check::Rejected(reason) => return Ok(Check::Rejected(reason)),
371                });
372            }
373            kind if kind == oid::AUTHORITY_INFO_ACCESS => {
374                if let Some(failure) = critical(extension, false) {
375                    return Ok(failure);
376                }
377                let (access, has_pointer) = match read_access(extension, &execution)? {
378                    Check::Accepted(value) => value,
379                    Check::Rejected(reason) => return Ok(Check::Rejected(reason)),
380                };
381                pointer |= has_pointer;
382                facts.access = Some(access);
383            }
384            kind if kind == oid::CRL_DISTRIBUTION_POINTS => {
385                if let Some(failure) = critical(extension, false) {
386                    return Ok(failure);
387                }
388                facts.distribution = Some(match read_distribution(extension, &execution)? {
389                    Check::Accepted(value) => value,
390                    Check::Rejected(reason) => return Ok(Check::Rejected(reason)),
391                });
392                pointer = true;
393            }
394            kind if kind == NO_REV_AVAILABLE => {
395                if let Some(failure) = critical(extension, false) {
396                    return Ok(failure);
397                }
398                read_no_revocation(extension, &execution)?;
399                facts.revocation = Revocation::NoRevAvailable;
400            }
401            _ if extension.is_critical() => {
402                return Ok(refused(Reason::UnknownCritical, at));
403            }
404            _ => {}
405        }
406    }
407    // §6 forbids NoRevAvail together with a revocation pointer, in either order.
408    if pointer && facts.revocation == Revocation::NoRevAvailable {
409        return Ok(refused(Reason::RevocationConflict, at));
410    }
411    Ok(Check::Accepted(facts))
412}