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(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}