Skip to main content

structured_cms/attribute_certificate/
validation.rs

1//! RFC5755 profile and supplied-certificate checks, without an authorization verdict.
2//!
3//! The component APIs keep signature, paths, holder identity, AA trust, time,
4//! targeting/proxy and status facts distinct (RFC5755 §5). [`usage`] composes
5//! them relative to explicit caller-authenticated protocol/trust inputs. A
6//! parsed critical extension is not, by itself, an enforced critical extension.
7//! <https://www.rfc-editor.org/rfc/rfc5755#section-5>
8
9use super::{AttributeCertificate, DigestedObject, IssuerForm, proxy, target};
10use crate::{Error, Result};
11use structured_asn1::memory::Work;
12use structured_asn1::{BitString, Span};
13use structured_pkix::general_name::{self, GeneralName};
14use structured_pkix::revocation::RevocationTarget;
15use structured_pkix::{Certificate, Name};
16
17pub mod aa_path;
18pub mod authentication;
19pub mod digest;
20mod profile;
21mod revocation;
22pub mod usage;
23pub use profile::{AUDIT_IDENTITY, NO_REV_AVAILABLE, Profile, Revocation};
24
25const FIELD: &str = "attributeCertificate.validation";
26
27/// A completed check or a located semantic refusal. Execution failures use Result.
28#[derive(Debug)]
29pub enum Check<T> {
30    /// The named check completed; other validation obligations are independent.
31    Accepted(T),
32    /// A completed comparison/profile check found a contradiction.
33    Rejected(Rejection),
34}
35
36/// Original source of a completed profile or binding refusal.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct Rejection {
39    /// Why this particular check refused.
40    pub reason: Reason,
41    /// The real enclosing original field; None for an empty caller path with
42    /// no document source. Never a fabricated parameter offset.
43    pub source: Option<Span>,
44}
45
46/// A semantic refusal, never a substitute for resource or decoding failure.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48#[non_exhaustive]
49pub enum Reason {
50    /// Issuer is not V2 with exactly one nonempty directoryName and no other forms.
51    IssuerForm,
52    /// Serial is zero, negative, or exceeds the twenty encoded octet limit.
53    Serial,
54    /// The inclusive validity interval is reversed.
55    Validity,
56    /// A holder baseCertificateID has an out-of-profile issuer name.
57    HolderIssuer,
58    /// RFC5755 §4.2 excludes X.400, EDI party and registered-ID name forms.
59    NameForm,
60    /// Optional objectDigestInfo names a forbidden object/type combination.
61    ObjectDigest,
62    /// Criticality contradicts the extension's profile.
63    Criticality,
64    /// An unrecognized critical extension cannot be accepted by this profile.
65    UnknownCritical,
66    /// Audit identity is outside the one-through-twenty octet range.
67    AuditIdentity,
68    /// A forbidden targetCert alternative was written.
69    TargetCertificate,
70    /// The issuer key identifier required by the PKIX profile is absent.
71    AuthorityKeyIdentifier,
72    /// An OCSP access location is not an HTTP URL.
73    OcspLocation,
74    /// CRL distribution points do not have the single fullName/profile form.
75    DistributionPoint,
76    /// `NoRevAvail` contradicts a written OCSP/CRL revocation pointer.
77    RevocationConflict,
78    /// Explicitly selected Holder alternative is absent.
79    HolderAbsent,
80    /// Supplied certificate's issuer or serial does not match the base identity.
81    HolderCertificate,
82    /// None of the written entity names identifies the supplied certificate.
83    HolderName,
84    /// A present holder issuerUID is absent/different in the supplied PKC.
85    HolderUniqueId,
86    /// AC issuer DN does not identify the supplied authority certificate.
87    AuthorityName,
88    /// AC issuer UID does not agree with the authority's subject UID.
89    AuthorityUniqueId,
90    /// An AC authority cannot also be a certificate authority.
91    AuthorityIsCa,
92    /// Present keyUsage permits neither digitalSignature nor contentCommitment.
93    AuthorityUsage,
94    /// A supplied AA path is empty.
95    EmptyPath,
96    /// A certificate below the explicitly selected AA CA lacks `AAControls`.
97    MissingControls,
98    /// `AAControls` pathLenConstraint is smaller than the actual distance.
99    ControlDistance,
100}
101
102fn refused<T>(reason: Reason, source: Span) -> Check<T> {
103    Check::Rejected(Rejection {
104        reason,
105        source: Some(source),
106    })
107}
108
109pub(super) fn charge(work: &impl Work, amount: u64, at: u64) -> Result<()> {
110    work.charge_work(amount)
111        .map_err(|e| Error::resource(e, FIELD, at))
112}
113
114fn same_bits(
115    left: BitString<'_>,
116    right: BitString<'_>,
117    left_work: &impl Work,
118    right_work: &impl Work,
119    at: u64,
120) -> Result<bool> {
121    // Switching to the PKC source replaces the origin, not the current
122    // operation. Fund both original UID scans explicitly before comparison;
123    // an independent third query must not erase the AC's source ceiling.
124    charge(left_work, 1, at)?;
125    charge(right_work, 1, at)?;
126    if left.len() != right.len() {
127        return Ok(false);
128    }
129    charge(left_work, left.as_bytes().len() as u64, at)?;
130    charge(right_work, right.as_bytes().len() as u64, at)?;
131    Ok(left.as_bytes() == right.as_bytes())
132}
133
134fn directory<'n, 'a>(names: &'n [GeneralName<'a>]) -> Option<&'n Name<'a>> {
135    match names {
136        [GeneralName::Directory(name)] if !name.is_empty() => Some(name),
137        _ => None,
138    }
139}
140
141fn check_name(name: &GeneralName<'_>, work: &impl Work, source: Span) -> Result<Check<()>> {
142    charge(work, 1, source.start)?;
143    // RFC5755 §4.2 forbids these GeneralName choices throughout its profile,
144    // even when their ASN.1 syntax is readable or a PKC SAN has equal bytes.
145    // The general PKIX decoder must still preserve them for inspection.
146    // <https://www.rfc-editor.org/rfc/rfc5755#section-4.2>
147    if matches!(
148        name,
149        GeneralName::X400(_) | GeneralName::EdiParty(_) | GeneralName::RegisteredId(_)
150    ) {
151        return Ok(refused(Reason::NameForm, source));
152    }
153    Ok(Check::Accepted(()))
154}
155
156fn check_names(names: &[GeneralName<'_>], work: &impl Work, source: Span) -> Result<Check<()>> {
157    charge(work, names.len() as u64, source.start)?;
158    for name in names {
159        if let rejected @ Check::Rejected(_) = check_name(name, work, source)? {
160            return Ok(rejected);
161        }
162    }
163    Ok(Check::Accepted(()))
164}
165
166/// A Holder alternative explicitly chosen by the authenticating protocol.
167/// No fallback or precedence is inferred when several alternatives are written.
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum HolderMethod {
170    /// RFC5755 §4.2.2: issuer DN, complete serial and optional issuer UID.
171    BaseCertificate,
172    /// RFC5755 §4.2.2: subject DN or a subjectAltName of the authenticated PKC.
173    EntityName,
174}
175
176impl<'a> AttributeCertificate<'a> {
177    /// Check static profile rules and decode all recognized extension values.
178    ///
179    /// All fields remain inspectable through `information()` on refusal. The
180    /// accepted facts retain targeting/proxy obligations; they are not evidence
181    /// that those effects, signatures, trust, time or revocation were checked.
182    pub fn profile<W: Work>(&self, work: &W) -> Result<Check<Profile<'_, 'a>>> {
183        profile::check(self, work)
184    }
185
186    /// Bind one explicitly chosen Holder method to a supplied authenticated PKC.
187    ///
188    /// This checks identity only: it does not authenticate the PKC or select
189    /// which Holder method the surrounding protocol trusts. Optional
190    /// objectDigestInfo is not silently treated as an entityName or a base ID.
191    /// RFC5755 §4.2.2: <https://www.rfc-editor.org/rfc/rfc5755#section-4.2.2>
192    pub fn bind_holder<W: Work>(
193        &self,
194        certificate: &Certificate<'_>,
195        method: HolderMethod,
196        work: &W,
197    ) -> Result<Check<()>> {
198        let execution = work.execution_for(self.parser);
199        let at = self.signed.span();
200        charge(&execution, 1, at.start)?;
201        let certificate_execution = certificate.execution_in(&execution);
202        charge(&certificate_execution, 1, certificate.span().start)?;
203        match method {
204            HolderMethod::BaseCertificate => {
205                let Some(base) = &self.information.holder.base_certificate else {
206                    return Ok(refused(Reason::HolderAbsent, at));
207                };
208                let Some(name) = directory(&base.issuer) else {
209                    return Ok(refused(Reason::HolderIssuer, at));
210                };
211                let left = base.serial.as_bytes();
212                let right = certificate.tbs().serial().as_bytes();
213                charge(&execution, left.len() as u64 + 1, at.start)?;
214                charge(
215                    &certificate_execution,
216                    right.len() as u64 + 1,
217                    certificate.span().start,
218                )?;
219                // Both objects came from DER readers, so minimal INTEGER
220                // encodings preserve arbitrary-width numeric identity.
221                if left != right
222                    || !name
223                        .matches_between(
224                            certificate.tbs().issuer(),
225                            (&execution, self.parser),
226                            (&certificate_execution, certificate.source_parser()),
227                        )
228                        .map_err(Error::pkix)?
229                {
230                    return Ok(refused(Reason::HolderCertificate, at));
231                }
232                if let Some(uid) = base.issuer_uid {
233                    let Some(other) = certificate.tbs().issuer_unique_id() else {
234                        return Ok(refused(Reason::HolderUniqueId, at));
235                    };
236                    if !same_bits(uid, other, &execution, &certificate_execution, at.start)? {
237                        return Ok(refused(Reason::HolderUniqueId, at));
238                    }
239                }
240            }
241            HolderMethod::EntityName => {
242                let Some(names) = &self.information.holder.entity_names else {
243                    return Ok(refused(Reason::HolderAbsent, at));
244                };
245                // Complete the normative type check before any positive
246                // subject/SAN shortcut can hide a later forbidden alternative.
247                if let rejected @ Check::Rejected(_) = check_names(names, &execution, at)? {
248                    return Ok(rejected);
249                }
250                // §4.2.2 permits subject OR SAN identity. A matching subject
251                // needs no SAN parsing/storage; certificate profile/path
252                // validation is independent of this identity-only operation.
253                for name in names {
254                    charge(&execution, 1, at.start)?;
255                    if let GeneralName::Directory(name) = name
256                        && !name.is_empty()
257                        && name
258                            .matches_between(
259                                certificate.tbs().subject(),
260                                (&execution, self.parser),
261                                (&certificate_execution, certificate.source_parser()),
262                            )
263                            .map_err(Error::pkix)?
264                    {
265                        return Ok(Check::Accepted(()));
266                    }
267                }
268                let Some(sans) = certificate
269                    .tbs()
270                    .extensions()
271                    .subject_alt_name(&certificate_execution)
272                    .map_err(Error::pkix)?
273                else {
274                    return Ok(refused(Reason::HolderName, at));
275                };
276                for name in names {
277                    charge(&execution, 1, at.start)?;
278                    for alternative in &sans {
279                        charge(&certificate_execution, 1, at.start)?;
280                        if general_name::matches_between(
281                            name,
282                            alternative,
283                            (&execution, self.parser),
284                            (&certificate_execution, certificate.source_parser()),
285                            at.start,
286                            FIELD,
287                        )
288                        .map_err(Error::pkix)?
289                        {
290                            return Ok(Check::Accepted(()));
291                        }
292                    }
293                }
294                return Ok(refused(Reason::HolderName, at));
295            }
296        }
297        Ok(Check::Accepted(()))
298    }
299
300    /// Bind issuer DN/UID and check the authority PKC's AA-specific purpose.
301    ///
302    /// RFC5755 §§4.2.3/4.2.8/4.5 require a non-CA authority permitted to sign
303    /// data (digitalSignature or contentCommitment). The AC issuerUID identifies
304    /// that authority itself, hence compares with its subjectUniqueID, whereas
305    /// Holder.baseCertificateID.issuerUID
306    /// identifies the holder PKC's issuer. The original authority's path,
307    /// effective key, algorithm agreement and AC signature still need checking.
308    /// <https://www.rfc-editor.org/rfc/rfc5755#section-4.2.8>
309    pub fn bind_authority<W: Work>(
310        &self,
311        authority: &Certificate<'_>,
312        work: &W,
313    ) -> Result<Check<()>> {
314        let execution = work.execution_for(self.parser);
315        let at = self.signed.span();
316        charge(&execution, 1, at.start)?;
317        let authority_execution = authority.execution_in(&execution);
318        charge(&authority_execution, 1, authority.span().start)?;
319        let issuer = &self.information.issuer;
320        if issuer.form != IssuerForm::V2
321            || issuer.base_certificate.is_some()
322            || issuer.object_digest.is_some()
323        {
324            return Ok(refused(Reason::IssuerForm, at));
325        }
326        let Some(name) = issuer.names.as_deref().and_then(directory) else {
327            return Ok(refused(Reason::IssuerForm, at));
328        };
329        if !name
330            .matches_between(
331                authority.tbs().subject(),
332                (&execution, self.parser),
333                (&authority_execution, authority.source_parser()),
334            )
335            .map_err(Error::pkix)?
336        {
337            return Ok(refused(Reason::AuthorityName, at));
338        }
339        match (
340            self.information.issuer_uid,
341            authority.tbs().subject_unique_id(),
342        ) {
343            (None, None) => {}
344            (Some(left), Some(right)) => {
345                if !same_bits(left, right, &execution, &authority_execution, at.start)? {
346                    return Ok(refused(Reason::AuthorityUniqueId, at));
347                }
348            }
349            _ => return Ok(refused(Reason::AuthorityUniqueId, at)),
350        }
351        let extensions = authority.tbs().extensions();
352        if extensions
353            .basic_constraints(&authority_execution)
354            .map_err(Error::pkix)?
355            .is_some_and(structured_pkix::constraints::BasicConstraints::is_authority)
356        {
357            return Ok(refused(Reason::AuthorityIsCa, authority.span()));
358        }
359        // RFC5755 §4.5 forbids a KU explicitly denying signature use, rather
360        // than mandating just bit 0. RFC5280 §4.2.1.3 assigns bit 1 to digital
361        // signatures as well, for content commitment. Both uses are accepted.
362        // <https://www.rfc-editor.org/rfc/rfc5755#section-4.5>
363        if extensions
364            .key_usage(&authority_execution)
365            .map_err(Error::pkix)?
366            .is_some_and(|value| !value.digital_signature() && !value.content_commitment())
367        {
368            return Ok(refused(Reason::AuthorityUsage, authority.span()));
369        }
370        Ok(Check::Accepted(()))
371    }
372}