Skip to main content

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}