Skip to main content

structured_pades/
prepare.rs

1//! `PAdES` root obligations over the existing complete CMS/CAdES participant core.
2
3use crate::{
4    arena::Kind,
5    bytes::Form,
6    container::Container,
7    date::Date,
8    document::Document,
9    error::{Error, ErrorKind, ReadFailure},
10    model::{admit, get},
11    range::RangeSource,
12    signature::{Coverage, SubFilter},
13    source::Location,
14};
15use structured_asn1::{
16    Reader,
17    memory::{Memory, OwnedVec, Work},
18};
19use structured_cades::prepare::{Preparation as CmsPreparation, Rule as CmsRule, Severity};
20use structured_cms::oid;
21
22/// Explicitly requested `PAdES` level. Unsigned contents never select this input.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
24pub enum Level {
25    /// Mandatory PDF/CMS baseline fields and checked signer association.
26    Basic,
27    /// Basic plus trusted time of the original signature's existence.
28    Timestamped,
29    /// Timestamped plus complete incorporated validation material.
30    LongTerm,
31    /// Long-term plus source-bound trusted protection of validation data.
32    Archival,
33}
34
35/// A completed PDF/CMS profile finding, separate from source/resource refusal.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Rule {
38    /// Exact whole-revision coverage or the PDF signature declaration fails.
39    Signature,
40    /// Required PDF field is absent or has the wrong selected schema.
41    Field(&'static str),
42    /// A field or known `CAdES` attribute is forbidden by the `PAdES` table.
43    Forbidden,
44    /// CMS root, properties, nested counters or timestamps retain a fault.
45    Cms,
46    /// Reason conflicts with a protected commitment or signature policy.
47    Reason,
48    /// PDF dates must express the claimed UTC label.
49    ClaimedTime,
50    /// The original signature is not selected by one actual signature field.
51    SignaturePlacement,
52}
53
54/// One original finding, retaining its PDF enclosure for CMS coordinates.
55#[derive(Debug, Clone, Copy)]
56pub struct Finding {
57    /// Original physical/decoded PDF location, never an invented ASN offset.
58    pub location: Location,
59    /// Exact format requirement which failed.
60    pub rule: Rule,
61}
62
63/// Immutable `PAdES` format preparation. The complete nested CMS participant
64/// and token census is borrowed from its owning common preparation unchanged.
65/// This is not a signature, authority, chronology or trust verdict.
66pub struct Preparation<'p, 'm, 'd, 'a, 's, D, G> {
67    pub(crate) container: &'p Container<'a, 's, D, G>,
68    pub(crate) cms: &'p CmsPreparation<'m, 'd>,
69    requested: Level,
70    claimed: Option<Date>,
71    placement: Option<crate::permissions::Placement>,
72    findings: OwnedVec<Finding, Memory>,
73}
74
75/// Terminal PDF acquisition or existing CMS/CAdES processing refusal.
76#[derive(Debug)]
77pub enum PrepareError<E> {
78    /// Original PDF or carried-source refusal.
79    Pdf(ReadFailure<E>),
80    /// Original common participant/schema refusal.
81    Cms(structured_cades::Error),
82}
83
84impl<'p, 'm, 'd, 'a, 's, D: Copy + Eq, G: Copy + Eq> Preparation<'p, 'm, 'd, 'a, 's, D, G> {
85    async fn read_placement<S: RangeSource<Document = D, Generation = G>>(
86        document: &Document<'s, S>,
87        signature: crate::signature::Signature<'a, 's, D, G>,
88        work: &impl Work,
89        record: &mut impl FnMut(Rule, Location) -> Result<(), PrepareError<S::Error>>,
90    ) -> Result<Option<crate::permissions::Placement>, PrepareError<S::Error>> {
91        let pdf = PrepareError::Pdf;
92        let location = signature.location();
93        let placement = if let Coverage::Revision(revision) = signature.coverage() {
94            let graph = document.graph(revision, work).await.map_err(pdf)?;
95            let fields = document.fields(&graph, work).await.map_err(pdf)?;
96            let transforms = document
97                .transforms(signature, &graph, work)
98                .await
99                .map_err(pdf)?;
100            let placement = document
101                .signature_placement(signature, &graph, &fields, &transforms, work)
102                .map_err(pdf)?;
103            if !placement.all_requirements_met() {
104                record(Rule::SignaturePlacement, location)?;
105            }
106            for node in &graph.nodes {
107                admit(work, location, 1).map_err(|cause| pdf(cause.into()))?;
108                if let Some(reading) = &node.reading {
109                    for fault in reading.value.faults() {
110                        admit(work, fault.location, 1).map_err(|cause| pdf(cause.into()))?;
111                        record(Rule::Signature, fault.location)?;
112                    }
113                }
114            }
115            Some(placement)
116        } else {
117            None
118        };
119        Ok(placement)
120    }
121
122    async fn read_pdf_fields<S: RangeSource<Document = D, Generation = G>>(
123        document: &Document<'s, S>,
124        signature: crate::signature::Signature<'a, 's, D, G>,
125        work: &impl Work,
126        record: &mut impl FnMut(Rule, Location) -> Result<(), PrepareError<S::Error>>,
127    ) -> Result<Option<Date>, PrepareError<S::Error>> {
128        let pdf = PrepareError::Pdf;
129        let location = signature.location();
130        let reading = signature.owner();
131        let arena = &reading.value.arena;
132        let node = signature.node();
133        crate::direct::visit(arena, node, work, |location| {
134            record(Rule::Signature, location)
135        })
136        .map_err(|cause| match cause {
137            crate::direct::VisitError::Processing(cause) => pdf(cause.into()),
138            crate::direct::VisitError::Callback(cause) => cause,
139        })?;
140        for fault in &arena.faults {
141            admit(work, fault.location, 1).map_err(|cause| pdf(cause.into()))?;
142            record(Rule::Signature, fault.location)?;
143        }
144        if !matches!(
145            arena.nodes[get(arena, node, b"Contents", work)
146                .map_err(|cause| pdf(cause.into()))?
147                .expect("inspected Contents")
148                .0]
149                .kind,
150            Kind::String(Form::Hexadecimal)
151        ) {
152            record(Rule::Field("Contents"), signature.contents())?;
153        }
154        let filter = get(arena, node, b"Filter", work).map_err(|cause| pdf(cause.into()))?;
155        if filter.is_none_or(|node| !matches!(arena.nodes[node.0].kind, Kind::Name(_))) {
156            record(Rule::Field("Filter"), location)?;
157        }
158        if get(arena, node, b"Cert", work)
159            .map_err(|cause| pdf(cause.into()))?
160            .is_some()
161        {
162            record(Rule::Forbidden, location)?;
163        }
164        let date = get(arena, node, b"M", work).map_err(|cause| pdf(cause.into()))?;
165        let claimed = if let Some(date) = date {
166            let date_location = arena.nodes[date.0].location;
167            let bytes = document
168                .string(reading, date, 22, work)
169                .await
170                .map_err(pdf)?;
171            match Date::read(&bytes, date_location, work) {
172                Ok(date) => {
173                    if date.offset_minutes() != 0 {
174                        record(Rule::ClaimedTime, date_location)?;
175                    }
176                    Some(date)
177                }
178                Err(cause)
179                    if matches!(
180                        cause.kind,
181                        ErrorKind::Resource(_) | ErrorKind::Foreign | ErrorKind::Limit(_)
182                    ) =>
183                {
184                    return Err(pdf(cause.into()));
185                }
186                Err(_) => {
187                    record(Rule::ClaimedTime, arena.nodes[date.0].location)?;
188                    None
189                }
190            }
191        } else {
192            record(Rule::Field("M"), location)?;
193            None
194        };
195        for key in [b"Location".as_slice(), b"Reason", b"Name", b"ContactInfo"] {
196            if let Some(value) = get(arena, node, key, work).map_err(|cause| pdf(cause.into()))? {
197                let bytes = document
198                    .string(reading, value, usize::MAX, work)
199                    .await
200                    .map_err(pdf)?;
201                crate::text::read(&bytes, arena.nodes[value.0].location, work)
202                    .map_err(|cause| pdf(cause.into()))?;
203            }
204        }
205        Ok(claimed)
206    }
207
208    fn read_cms_fields<E>(
209        cms: &CmsPreparation<'m, 'd>,
210        signature: crate::signature::Signature<'a, 's, D, G>,
211        work: &impl Work,
212        record: &mut impl FnMut(Rule, Location) -> Result<(), PrepareError<E>>,
213    ) -> Result<(), PrepareError<E>> {
214        let pdf = PrepareError::Pdf;
215        let location = signature.location();
216        let reading = signature.owner();
217        let arena = &reading.value.arena;
218        let node = signature.node();
219        for finding in cms.findings() {
220            admit(work, signature.contents(), 1).map_err(|cause| pdf(cause.into()))?;
221            if finding.severity == Severity::Advisory {
222                continue;
223            }
224            if matches!(finding.rule, CmsRule::Missing(kind) if kind == oid::SIGNING_TIME)
225                || matches!(
226                    finding.rule,
227                    CmsRule::SignatureTimestamp | CmsRule::ArchiveTimestamp
228                )
229            {
230                continue;
231            }
232            record(Rule::Cms, signature.contents())?;
233        }
234        let reason = get(arena, node, b"Reason", work)
235            .map_err(|cause| pdf(cause.into()))?
236            .is_some();
237        for set in [
238            cms.signer().signed_attributes(),
239            cms.signer().unsigned_attributes(),
240        ]
241        .into_iter()
242        .flatten()
243        {
244            for attribute in set.items() {
245                admit(
246                    work,
247                    signature.contents(),
248                    attribute.kind().as_bytes().len() as u64 + 1,
249                )
250                .map_err(|cause| pdf(cause.into()))?;
251                let kind = attribute.kind();
252                if reason
253                    && (kind == oid::COMMITMENT_TYPE_INDICATION
254                        || kind == oid::SIGNATURE_POLICY_IDENTIFIER)
255                {
256                    record(Rule::Reason, location)?;
257                }
258                if structured_cades::properties::defines(kind, work).map_err(|cause| {
259                    pdf(Error {
260                        kind: ErrorKind::Resource(cause),
261                        location: signature.contents(),
262                    }
263                    .into())
264                })? && !allowed(kind)
265                {
266                    record(Rule::Forbidden, signature.contents())?;
267                }
268            }
269        }
270        Ok(())
271    }
272
273    /// Reads all root obligations before key resolution or payload acquisition.
274    /// EN319142-1 §§4.1/6.3 Table1 overrides `CAdES`'s root signing-time field;
275    /// the existing nested participant/token mechanisms are not reimplemented.
276    /// <https://www.etsi.org/deliver/etsi_EN/319100_319199/31914201/01.02.01_60/en_31914201v010201p.pdf>
277    ///
278    /// # Panics
279    /// Panics if the inspected signature loses its original `Contents` member.
280    pub async fn read<S: RangeSource<Document = D, Generation = G>, W: Work>(
281        document: &Document<'s, S>,
282        container: &'p Container<'a, 's, D, G>,
283        cms: &'p CmsPreparation<'m, 'd>,
284        requested: Level,
285        parent: &Reader<'d>,
286        operation: &W,
287    ) -> Result<Self, PrepareError<S::Error>> {
288        let pdf = PrepareError::Pdf;
289        let message = cms.document().message();
290        container
291            .require(message, parent)
292            .map_err(|cause| pdf(cause.into()))?;
293        message
294            .require_session(parent, "pades")
295            .map_err(|cause| PrepareError::Cms(cause.into()))?;
296        let signature = container.signature();
297        let location = signature.location();
298        let work = document.session.query(operation);
299        document
300            .check(&work, location)
301            .map_err(|cause| pdf(cause.into()))?;
302        let mut findings = work.vector();
303        let mut record = |rule, location| {
304            findings
305                .try_push(Finding { location, rule })
306                .map_err(|cause| {
307                    pdf(Error {
308                        kind: ErrorKind::Resource(cause),
309                        location,
310                    }
311                    .into())
312                })
313        };
314        if signature.sub_filter() != Some(SubFilter::CadesDetached)
315            || signature.is_document_timestamp()
316            || !matches!(signature.coverage(), Coverage::Revision(_))
317        {
318            record(Rule::Signature, location)?;
319        }
320        if message.signers().len() != 1 || !message.content().is_detached() {
321            record(Rule::Signature, signature.contents())?;
322        }
323        let placement = Self::read_placement(document, signature, &work, &mut record).await?;
324        let claimed = Self::read_pdf_fields(document, signature, &work, &mut record).await?;
325        Self::read_cms_fields(cms, signature, &work, &mut record)?;
326        document
327            .check(&work, location)
328            .map_err(|cause| pdf(cause.into()))?;
329        Ok(Self {
330            container,
331            cms,
332            requested,
333            claimed,
334            placement,
335            findings: findings.into_owned(),
336        })
337    }
338
339    /// Requested level, independent of optional timestamp presence.
340    #[must_use]
341    pub fn requested(&self) -> Level {
342        self.requested
343    }
344    /// Parsed claimed calendar label, without an occurrence proof.
345    #[must_use]
346    pub fn claimed_time(&self) -> Option<Date> {
347        self.claimed
348    }
349    /// Actual historical signature-field and certification placement facts.
350    #[must_use]
351    pub fn placement(&self) -> Option<&crate::permissions::Placement> {
352        self.placement.as_ref()
353    }
354    /// Every retained root preparation finding, without successful-subset filtering.
355    #[must_use]
356    pub fn findings(&self) -> &[Finding] {
357        &self.findings
358    }
359    /// Only original baseline format facts, before mathematical verification.
360    #[must_use]
361    pub fn all_basic_format_requirements_met(&self) -> bool {
362        self.findings.is_empty()
363    }
364    /// Existing whole CMS/counter/token preparation, not a copied catalogue.
365    #[must_use]
366    pub fn cms(&self) -> &'p CmsPreparation<'m, 'd> {
367        self.cms
368    }
369}
370
371fn allowed(kind: structured_asn1::Oid<'_>) -> bool {
372    // EN319142-1 §6.3 permits only its Table1 subset of EN319122 attributes.
373    [
374        oid::CONTENT_TYPE,
375        oid::MESSAGE_DIGEST,
376        oid::SIGNER_ATTRIBUTES_V2,
377        oid::CONTENT_TIMESTAMP,
378        oid::SIGNATURE_POLICY_IDENTIFIER,
379        oid::COMMITMENT_TYPE_INDICATION,
380        oid::SIGNING_CERTIFICATE,
381        oid::SIGNING_CERTIFICATE_V2,
382        oid::SIGNATURE_TIMESTAMP,
383    ]
384    .contains(&kind)
385}