structured_cms/cades.rs
1//! `CAdES` attribute rules of ETSI EN 319 122-1 V1.3.1 §§5.2-5.5 that narrow
2//! the generic CMS and ESS schemas.
3//!
4//! The generic readers accept what RFC 5652 and RFC 5035 accept; this module
5//! reports where a `CAdES` signature uses that freedom against the profile:
6//! an attribute in the wrong set, a single-valued attribute with another value
7//! count, a content-describing attribute inside a countersignature, ESS
8//! `policies` and an implied signature policy. Which attributes a level
9//! requires, and every verdict, stay with the caller.
10
11use crate::attribute::Attributes;
12use crate::ess::SigningAttributes;
13use crate::signer::SignerInfo;
14use crate::{Error, ErrorCode, Result, oid};
15use structured_asn1::memory::{OwnedVec, Work};
16use structured_asn1::{Oid, Reader, Tag, universal};
17
18/// The `SignerInfo` being checked, since rules differ by where it sits.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Role {
21 /// A signer of the root `SignedData`.
22 Signature,
23 /// A `SignerInfo` carried in a countersignature attribute (§5.2.7).
24 Countersignature,
25 /// The signer of the time-stamp token in an `archive-time-stamp-v3`
26 /// (§5.5.3), the only one that carries an `ats-hash-index-v3` (§5.5.2).
27 ArchiveTimestamp,
28}
29
30#[derive(Clone, Copy, PartialEq, Eq)]
31enum Set {
32 Signed,
33 Unsigned,
34 // Allowed in neither top-level set, only nested in another attribute.
35 Neither,
36}
37
38// Which signers may carry the attribute at all.
39#[derive(Clone, Copy, PartialEq, Eq)]
40enum Signers {
41 Any,
42 NotCountersignature,
43 ArchiveTimestampOnly,
44}
45
46impl Signers {
47 fn admit(self, role: Role) -> bool {
48 match self {
49 Self::Any => true,
50 Self::NotCountersignature => role != Role::Countersignature,
51 Self::ArchiveTimestampOnly => role == Role::ArchiveTimestamp,
52 }
53 }
54}
55
56struct Rule {
57 kind: Oid<'static>,
58 set: Set,
59 single: bool,
60 unique: bool,
61 signers: Signers,
62 field: &'static str,
63}
64
65const fn rule(kind: Oid<'static>, set: Set, single: bool, field: &'static str) -> Rule {
66 Rule {
67 kind,
68 set,
69 single,
70 unique: false,
71 signers: Signers::Any,
72 field,
73 }
74}
75
76// An attribute type defined in ESS: "A signerInfo MUST NOT include multiple
77// instances of any of the attribute types defined in ESS", each with "a single
78// instance of AttributeValue" (RFC 2634 §1.3.4).
79const fn ess(kind: Oid<'static>, field: &'static str) -> Rule {
80 Rule {
81 unique: true,
82 ..rule(kind, Set::Signed, true, field)
83 }
84}
85
86// The placement ("is/shall be a signed/unsigned attribute") and the value
87// count ("shall contain exactly one component of AttributeValue type") of each
88// §5.2-5.5 attribute. Rules the CMS and ESS readers already apply (content
89// type, message digest, signing time, algorithm protection, signing
90// certificates) are not repeated here.
91const RULES: [Rule; 14] = [
92 // §5.2.3
93 rule(
94 oid::COMMITMENT_TYPE_INDICATION,
95 Set::Signed,
96 true,
97 "commitmentTypeIndication",
98 ),
99 // §5.2.4.1: ESS, signed, and not within a countersignature.
100 Rule {
101 signers: Signers::NotCountersignature,
102 ..ess(oid::CONTENT_HINTS, "contentHints")
103 },
104 // §5.2.4.2: signed, one value, and not within a countersignature.
105 Rule {
106 signers: Signers::NotCountersignature,
107 ..rule(oid::MIME_TYPE, Set::Signed, true, "mimeType")
108 },
109 // §5.2.5
110 rule(oid::SIGNER_LOCATION, Set::Signed, true, "signerLocation"),
111 // §5.2.6.1
112 rule(
113 oid::SIGNER_ATTRIBUTES_V2,
114 Set::Signed,
115 true,
116 "signerAttributesV2",
117 ),
118 // §5.2.6.2: only in signer-attributes-v2 claimedAttributes; "It shall
119 // not be used anywhere else."
120 rule(
121 oid::CLAIMED_SAML_ASSERTION,
122 Set::Neither,
123 false,
124 "claimedSamlAssertion",
125 ),
126 // §5.2.8
127 rule(
128 oid::CONTENT_TIMESTAMP,
129 Set::Signed,
130 true,
131 "contentTimeStamp",
132 ),
133 // §5.2.9.1
134 rule(
135 oid::SIGNATURE_POLICY_IDENTIFIER,
136 Set::Signed,
137 true,
138 "signaturePolicyIdentifier",
139 ),
140 // §5.2.10
141 rule(
142 oid::SIGNATURE_POLICY_STORE,
143 Set::Unsigned,
144 true,
145 "signaturePolicyStore",
146 ),
147 // §5.2.11: ESS.
148 ess(oid::CONTENT_REFERENCE, "contentReference"),
149 // §5.2.12: ESS.
150 ess(oid::CONTENT_IDENTIFIER, "contentIdentifier"),
151 // §5.3
152 rule(
153 oid::SIGNATURE_TIMESTAMP,
154 Set::Unsigned,
155 true,
156 "signatureTimeStamp",
157 ),
158 // §5.5.2: an unsigned attribute of the archive time-stamp token's signer;
159 // §5.5.3: the token carries "a single ats-hash-index-v3 attribute".
160 Rule {
161 unique: true,
162 signers: Signers::ArchiveTimestampOnly,
163 ..rule(
164 oid::ATS_HASH_INDEX_V3,
165 Set::Unsigned,
166 true,
167 "atsHashIndexV3",
168 )
169 },
170 // §5.5.3(3) archives a SignedData.signerInfos item. Its nested counters
171 // are protected through the indexed unsigned values (§5.5.2 note6,
172 // §5.5.3 note4), not selected as another root of the archive input.
173 // https://www.etsi.org/deliver/etsi_en/319100_319199/31912201/01.03.01_60/en_31912201v010301p.pdf
174 Rule {
175 signers: Signers::NotCountersignature,
176 ..rule(
177 oid::ARCHIVE_TIMESTAMP_V3,
178 Set::Unsigned,
179 true,
180 "archiveTimeStampV3",
181 )
182 },
183];
184
185/// Reports every `CAdES` profile violation of one `SignerInfo`, in rule order,
186/// with original locations.
187///
188/// The signer's own ESS signing-certificate attributes are read here, so the
189/// `policies` field §§5.2.2.2-5.2.2.3 forbid is always judged on this signer,
190/// never on attributes read from another. An attribute this crate does not read is
191/// judged only by its placement, value count and, for the ESS content
192/// attributes, a repeated instance (RFC 2634 §1.3.4), plus the
193/// `signaturePolicyImplied` choice that §5.2.9.1 forbids. An empty result is
194/// not a level or validity verdict.
195///
196/// # Examples
197///
198/// ```
199/// use std::sync::Arc;
200/// use structured_asn1::Parser;
201/// use structured_asn1::memory::{Memory, MemoryAccount};
202/// use structured_cms::cades::{Role, profile_faults};
203/// use structured_cms::{ErrorCode, SignedData, oid};
204///
205/// fn tlv(tag: u8, contents: &[u8]) -> Vec<u8> {
206/// [&[tag, contents.len() as u8][..], contents].concat()
207/// }
208/// // One signer whose signed attributes hold a mime-type, which §5.2.4.2
209/// // forbids only within a countersignature.
210/// let mime = tlv(0x30, &[tlv(6, oid::MIME_TYPE.as_bytes()), tlv(0x31, &tlv(12, b"text/plain"))].concat());
211/// let signer = [
212/// &[2, 1, 3, 0x80, 1, 7][..],
213/// &tlv(0x30, &tlv(6, oid::SHA256.as_bytes())),
214/// &tlv(0xa0, &mime),
215/// &tlv(0x30, &tlv(6, &[0x2b, 0x65, 0x70])),
216/// &tlv(4, &[8]),
217/// ]
218/// .concat();
219/// let inside = [
220/// &[2, 1, 3, 0x31, 0][..],
221/// &tlv(0x30, &tlv(6, oid::DATA.as_bytes())),
222/// &tlv(0x31, &tlv(0x30, &signer)),
223/// ]
224/// .concat();
225/// let message = tlv(0x30, &[tlv(6, oid::SIGNED_DATA.as_bytes()), tlv(0xa0, &tlv(0x30, &inside))].concat());
226///
227/// let parser = Parser::ber(Memory::new(Arc::new(MemoryAccount::new(65536, 65536))));
228/// let value = SignedData::read(&parser, &message, 0).unwrap();
229/// let query = parser.read(&[], 0).unwrap();
230/// let info = &value.signers()[0];
231/// assert!(profile_faults(info, Role::Signature, &query).unwrap().is_empty());
232/// let faults = profile_faults(info, Role::Countersignature, &query).unwrap();
233/// assert_eq!(faults[0].code(), ErrorCode::AttributePlacement);
234/// ```
235///
236/// # Errors
237///
238/// A foreign reader, signing-certificate attributes the ESS reader refuses
239/// ([`SigningAttributes::read`]), or a spent work or memory ceiling; never a
240/// violation.
241pub fn profile_faults<'a>(
242 signer: &SignerInfo<'a>,
243 role: Role,
244 parent: &Reader<'a>,
245) -> Result<OwnedVec<Error>> {
246 Ok(profile(signer, role, parent)?.faults)
247}
248
249/// One original ESS reading and its CAdES-specific profile findings. Keeping
250/// the reading lets a baseline layer bind the certificate without parsing the
251/// same signed attribute a second time in that operation.
252#[derive(Debug)]
253pub struct Profile<'a> {
254 signing: SigningAttributes<'a>,
255 faults: OwnedVec<Error>,
256}
257
258impl<'a> Profile<'a> {
259 /// Original ESS signer/path restrictions, independently of hash agreement.
260 #[must_use]
261 pub fn signing(&self) -> &SigningAttributes<'a> {
262 &self.signing
263 }
264
265 /// `CAdES` placement/cardinality/profile violations from the same pass.
266 /// ESS reading diagnostics remain on [`Self::signing`].
267 #[must_use]
268 pub fn faults(&self) -> &[Error] {
269 &self.faults
270 }
271}
272
273/// Runs the same profile kernel as [`profile_faults`], retaining the already
274/// parsed ESS reading for subsequent baseline certificate binding. No result
275/// here supplies a baseline level, mathematical agreement or trust.
276pub fn profile<'a>(
277 signer: &SignerInfo<'a>,
278 role: Role,
279 parent: &Reader<'a>,
280) -> Result<Profile<'a>> {
281 // The producing parser's ceilings fund every scan below.
282 signer.require_session(parent, "signerInfo")?;
283 let signing = SigningAttributes::read(signer, parent)?;
284 let faults = findings_from_reading(signer, role, &signing, parent)?;
285 Ok(Profile { signing, faults })
286}
287
288/// Inspects the same profile while retaining nonterminal ESS selection/schema
289/// failures as located findings. A conflicting version has no selected binding;
290/// it cannot silently become the first assertion or an empty successful set.
291/// Source/resource refusal still returns an error without a successful prefix.
292/// This permits a format layer to inventory later timestamps and counters even
293/// when the original signer's ESS fields are nonconforming.
294pub fn inspect_profile<'a>(
295 signer: &SignerInfo<'a>,
296 role: Role,
297 parent: &Reader<'a>,
298) -> Result<Profile<'a>> {
299 signer.require_session(parent, "signerInfo")?;
300 let signing = SigningAttributes::inspect(signer, parent)?;
301 let faults = findings_from_reading(signer, role, &signing, parent)?;
302 Ok(Profile { signing, faults })
303}
304
305/// Applies the same profile kernel to the SAME already parsed ESS reading.
306/// Source identity is checked before traversal; no ESS value is reparsed and
307/// no signature, certificate binding or application trust is established.
308pub fn findings_from_reading(
309 signer: &SignerInfo<'_>,
310 role: Role,
311 signing: &SigningAttributes<'_>,
312 parent: &Reader<'_>,
313) -> Result<OwnedVec<Error>> {
314 signing.require_signer(signer, parent)?;
315 let execution = parent.execution();
316 let mut faults = execution.vector();
317 for rule in &RULES {
318 let protected = signer.signed_attributes();
319 let unprotected = signer.unsigned_attributes();
320 let (allowed, forbidden) = match rule.set {
321 Set::Signed => (protected, [unprotected, None]),
322 Set::Unsigned => (unprotected, [protected, None]),
323 Set::Neither => (None, [protected, unprotected]),
324 };
325 for set in forbidden.into_iter().flatten() {
326 report(set, rule, parent, &mut faults, |_, _| {
327 Some(ErrorCode::AttributePlacement)
328 })?;
329 }
330 let Some(set) = allowed else {
331 continue;
332 };
333 report(set, rule, parent, &mut faults, |attribute, instance| {
334 if !rule.signers.admit(role) {
335 Some(ErrorCode::AttributePlacement)
336 } else if rule.unique && instance > 0 {
337 Some(ErrorCode::DuplicateAttribute)
338 } else if rule.single && attribute.values().len() != 1 {
339 Some(ErrorCode::AttributeCardinality)
340 } else {
341 None
342 }
343 })?;
344 }
345 // §5.2.9.1: SignaturePolicyIdentifier ::= CHOICE { signaturePolicyId
346 // SignaturePolicyId, signaturePolicyImplied NULL }; the NULL choice shall
347 // not be used.
348 if let Some(signed) = signer.signed_attributes() {
349 for result in signed.all_in(oid::SIGNATURE_POLICY_IDENTIFIER, parent) {
350 for value in result?.values() {
351 // Each value inspected is paid before its tag is read.
352 execution.charge_work(1).map_err(|reason| {
353 Error::resource(reason, "signaturePolicyIdentifier", value.span().start)
354 })?;
355 if value.tag() == Tag::primitive(universal::NULL) {
356 push(
357 &mut faults,
358 Error::schema(
359 ErrorCode::AttributeSyntax,
360 "signaturePolicyIdentifier.signaturePolicyImplied",
361 value.span().start,
362 ),
363 )?;
364 }
365 }
366 }
367 }
368 // §§5.2.2.2-5.2.2.3: "The policies field shall not be used."
369 for (certificate, field) in [
370 (signing.v1(), "signingCertificate.policies"),
371 (signing.v2(), "signingCertificateV2.policies"),
372 ] {
373 if let Some(policies) = certificate.and_then(|value| value.policies()) {
374 execution
375 .charge_work(1)
376 .map_err(|reason| Error::resource(reason, field, policies.span().start))?;
377 push(
378 &mut faults,
379 Error::schema(ErrorCode::AttributeSyntax, field, policies.span().start),
380 )?;
381 }
382 }
383 Ok(faults.into_owned())
384}
385
386fn report<W: Work>(
387 set: &Attributes<'_>,
388 rule: &Rule,
389 parent: &Reader<'_>,
390 faults: &mut OwnedVec<Error, W>,
391 judge: impl Fn(&crate::attribute::Attribute<'_>, usize) -> Option<ErrorCode>,
392) -> Result<()> {
393 for (instance, result) in set.all_in(rule.kind, parent).enumerate() {
394 let attribute = result?;
395 if let Some(code) = judge(attribute, instance) {
396 push(
397 faults,
398 Error::schema(code, rule.field, attribute.span().start),
399 )?;
400 }
401 }
402 Ok(())
403}
404
405fn push<W: Work>(faults: &mut OwnedVec<Error, W>, fault: Error) -> Result<()> {
406 let field = fault.field();
407 let at = fault.offset();
408 faults
409 .try_push(fault)
410 .map_err(|reason| Error::resource(reason, field, at))
411}