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
171 rule(
172 oid::ARCHIVE_TIMESTAMP_V3,
173 Set::Unsigned,
174 true,
175 "archiveTimeStampV3",
176 ),
177];
178
179/// Reports every `CAdES` profile violation of one `SignerInfo`, in rule order,
180/// with original locations.
181///
182/// The signer's own ESS signing-certificate attributes are read here, so the
183/// `policies` field §§5.2.2.2-5.2.2.3 forbid is always judged on this signer,
184/// never on attributes read from another. An attribute this crate does not read is
185/// judged only by its placement, value count and, for the ESS content
186/// attributes, a repeated instance (RFC 2634 §1.3.4), plus the
187/// `signaturePolicyImplied` choice that §5.2.9.1 forbids. An empty result is
188/// not a level or validity verdict.
189///
190/// # Examples
191///
192/// ```
193/// use std::sync::Arc;
194/// use structured_asn1::Parser;
195/// use structured_asn1::memory::{Memory, MemoryAccount};
196/// use structured_cms::cades::{Role, profile_faults};
197/// use structured_cms::{ErrorCode, SignedData, oid};
198///
199/// fn tlv(tag: u8, contents: &[u8]) -> Vec<u8> {
200/// [&[tag, contents.len() as u8][..], contents].concat()
201/// }
202/// // One signer whose signed attributes hold a mime-type, which §5.2.4.2
203/// // forbids only within a countersignature.
204/// let mime = tlv(0x30, &[tlv(6, oid::MIME_TYPE.as_bytes()), tlv(0x31, &tlv(12, b"text/plain"))].concat());
205/// let signer = [
206/// &[2, 1, 3, 0x80, 1, 7][..],
207/// &tlv(0x30, &tlv(6, oid::SHA256.as_bytes())),
208/// &tlv(0xa0, &mime),
209/// &tlv(0x30, &tlv(6, &[0x2b, 0x65, 0x70])),
210/// &tlv(4, &[8]),
211/// ]
212/// .concat();
213/// let inside = [
214/// &[2, 1, 3, 0x31, 0][..],
215/// &tlv(0x30, &tlv(6, oid::DATA.as_bytes())),
216/// &tlv(0x31, &tlv(0x30, &signer)),
217/// ]
218/// .concat();
219/// let message = tlv(0x30, &[tlv(6, oid::SIGNED_DATA.as_bytes()), tlv(0xa0, &tlv(0x30, &inside))].concat());
220///
221/// let parser = Parser::ber(Memory::new(Arc::new(MemoryAccount::new(65536, 65536))));
222/// let value = SignedData::read(&parser, &message, 0).unwrap();
223/// let query = parser.read(&[], 0).unwrap();
224/// let info = &value.signers()[0];
225/// assert!(profile_faults(info, Role::Signature, &query).unwrap().is_empty());
226/// let faults = profile_faults(info, Role::Countersignature, &query).unwrap();
227/// assert_eq!(faults[0].code(), ErrorCode::AttributePlacement);
228/// ```
229///
230/// # Errors
231///
232/// A foreign reader, signing-certificate attributes the ESS reader refuses
233/// ([`SigningAttributes::read`]), or a spent work or memory ceiling; never a
234/// violation.
235pub fn profile_faults<'a>(
236 signer: &SignerInfo<'a>,
237 role: Role,
238 parent: &Reader<'a>,
239) -> Result<OwnedVec<Error>> {
240 // The producing parser's ceilings fund every scan below.
241 signer.require_session(parent, "signerInfo")?;
242 let signing = SigningAttributes::read(signer, parent)?;
243 let execution = parent.execution();
244 let mut faults = execution.vector();
245 for rule in &RULES {
246 let protected = signer.signed_attributes();
247 let unprotected = signer.unsigned_attributes();
248 let (allowed, forbidden) = match rule.set {
249 Set::Signed => (protected, [unprotected, None]),
250 Set::Unsigned => (unprotected, [protected, None]),
251 Set::Neither => (None, [protected, unprotected]),
252 };
253 for set in forbidden.into_iter().flatten() {
254 report(set, rule, parent, &mut faults, |_, _| {
255 Some(ErrorCode::AttributePlacement)
256 })?;
257 }
258 let Some(set) = allowed else {
259 continue;
260 };
261 report(set, rule, parent, &mut faults, |attribute, instance| {
262 if !rule.signers.admit(role) {
263 Some(ErrorCode::AttributePlacement)
264 } else if rule.unique && instance > 0 {
265 Some(ErrorCode::DuplicateAttribute)
266 } else if rule.single && attribute.values().len() != 1 {
267 Some(ErrorCode::AttributeCardinality)
268 } else {
269 None
270 }
271 })?;
272 }
273 // §5.2.9.1: SignaturePolicyIdentifier ::= CHOICE { signaturePolicyId
274 // SignaturePolicyId, signaturePolicyImplied NULL }; the NULL choice shall
275 // not be used.
276 if let Some(signed) = signer.signed_attributes() {
277 for result in signed.all_in(oid::SIGNATURE_POLICY_IDENTIFIER, parent) {
278 for value in result?.values() {
279 // Each value inspected is paid before its tag is read.
280 execution.charge_work(1).map_err(|reason| {
281 Error::resource(reason, "signaturePolicyIdentifier", value.span().start)
282 })?;
283 if value.tag() == Tag::primitive(universal::NULL) {
284 push(
285 &mut faults,
286 Error::schema(
287 ErrorCode::AttributeSyntax,
288 "signaturePolicyIdentifier.signaturePolicyImplied",
289 value.span().start,
290 ),
291 )?;
292 }
293 }
294 }
295 }
296 // §§5.2.2.2-5.2.2.3: "The policies field shall not be used."
297 for (certificate, field) in [
298 (signing.v1(), "signingCertificate.policies"),
299 (signing.v2(), "signingCertificateV2.policies"),
300 ] {
301 if let Some(policies) = certificate.and_then(|value| value.policies()) {
302 execution
303 .charge_work(1)
304 .map_err(|reason| Error::resource(reason, field, policies.span().start))?;
305 push(
306 &mut faults,
307 Error::schema(ErrorCode::AttributeSyntax, field, policies.span().start),
308 )?;
309 }
310 }
311 Ok(faults.into_owned())
312}
313
314fn report<W: Work>(
315 set: &Attributes<'_>,
316 rule: &Rule,
317 parent: &Reader<'_>,
318 faults: &mut OwnedVec<Error, W>,
319 judge: impl Fn(&crate::attribute::Attribute<'_>, usize) -> Option<ErrorCode>,
320) -> Result<()> {
321 for (instance, result) in set.all_in(rule.kind, parent).enumerate() {
322 let attribute = result?;
323 if let Some(code) = judge(attribute, instance) {
324 push(
325 faults,
326 Error::schema(code, rule.field, attribute.span().start),
327 )?;
328 }
329 }
330 Ok(())
331}
332
333fn push<W: Work>(faults: &mut OwnedVec<Error, W>, fault: Error) -> Result<()> {
334 let field = fault.field();
335 let at = fault.offset();
336 faults
337 .try_push(fault)
338 .map_err(|reason| Error::resource(reason, field, at))
339}