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}