Skip to main content

structured_cms/attribute_certificate/
validation.rs

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