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}