Skip to main content

structured_pades/
signature.rs

1//! Source-bound signature dictionaries and exact ordered `ByteRange` coverage.
2
3use crate::arena::{Kind, NodeId};
4use crate::document::{Document, Reading};
5use crate::error::{Error, ErrorKind, ReadFailure, Syntax};
6use crate::model::{admit, get, named, unsigned};
7use crate::range::{RangeReader, RangeSource};
8use crate::source::{Location, Source};
9use structured_asn1::{
10    Span,
11    memory::{Memory, OwnedVec, Work},
12};
13
14/// The declared cryptographic container profile, independently of validity.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum SubFilter {
17    /// Detached CMS signature required by the `PAdES` baseline profile.
18    CadesDetached,
19    /// RFC 3161 document timestamp.
20    Rfc3161,
21    /// Legacy detached PKCS #7 container, not a `PAdES` baseline declaration.
22    Pkcs7Detached,
23    /// Another original subfilter value, retained without reinterpretation.
24    Other,
25}
26
27/// Coverage classification derived from original source spans, not supplied
28/// by a caller or inferred from successful cryptographic verification.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum Coverage {
31    /// Exactly one complete historical revision, excluding only Contents.
32    Revision(usize),
33    /// Readable ordered ranges that omit additional document bytes.
34    Partial,
35}
36
37pub(crate) struct Entry {
38    pub(crate) owner: usize,
39    pub(crate) node: NodeId,
40    pub(crate) contents: NodeId,
41    pub(crate) ranges: OwnedVec<Span, Memory>,
42    pub(crate) sub_filter: Option<SubFilter>,
43    pub(crate) coverage: Coverage,
44    pub(crate) timestamp: bool,
45    pub(crate) definition: (u64, u16),
46}
47
48/// One original signature-shaped dictionary whose required input could not
49/// be read. It remains in the complete census and cannot mint an actor.
50pub struct Failure {
51    /// Actual dictionary enclosure, distinct from the failed field's location.
52    pub location: Location,
53    /// Unchanged syntax, ambiguity or unsupported scalar-query cause.
54    pub cause: Error,
55    /// Actual containing indirect object identity.
56    pub definition: (u64, u16),
57    /// Original physical update, including compressed members.
58    pub revision: usize,
59    /// Original Type/SubFilter identified a document timestamp candidate.
60    pub document_timestamp: bool,
61    /// Actual Contents value enclosure when present, regardless of its type.
62    pub contents: Option<Location>,
63}
64
65pub(crate) enum Position {
66    Readable(usize),
67    Unreadable(usize),
68}
69
70/// Format role proven by the original index/container grammar.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum DefinitionKind {
73    /// Ordinary value, including definitions not reachable from the catalog.
74    Object,
75    /// A structurally decoded object-stream transport.
76    ObjectStream,
77    /// A source-bound cross-reference stream, not a dictionary name guess.
78    CrossReference,
79}
80/// Every actual indirect definition, independent of latest xref reachability.
81#[derive(Debug, Clone, Copy)]
82pub struct Definition {
83    /// Original object number and generation.
84    pub identity: (u64, u16),
85    /// Original value coordinates, with physical enclosure for decoded objects.
86    pub location: Location,
87    /// Whole physical indirect record or original decoded member value.
88    pub encoded: Location,
89    /// Actual update containing the definition.
90    pub revision: usize,
91    /// Proven transport role, separately from value semantics or permissions.
92    pub kind: DefinitionKind,
93}
94
95/// A complete-census member, without filtering by parse or math success.
96pub enum Candidate<'a, 's, D, G> {
97    /// Source-bound required fields were readable.
98    Readable(Signature<'a, 's, D, G>),
99    /// Original required fields were unreadable; the cause is retained.
100    Unreadable(&'a Failure),
101}
102
103/// Complete catalogue ownership. Each entry points into one retained original
104/// reading; neither a copied dictionary nor caller-provided coverage can mint
105/// a signature input association.
106pub struct Catalogue<'s, D, G> {
107    pub(crate) readings: OwnedVec<Reading<'s, D, G>, Memory>,
108    pub(crate) entries: OwnedVec<Entry, Memory>,
109    pub(crate) failures: OwnedVec<Failure, Memory>,
110    pub(crate) order: OwnedVec<Position, Memory>,
111    pub(crate) definitions: OwnedVec<Definition, Memory>,
112}
113
114impl<'s, D: Copy + Eq, G: Copy + Eq> Catalogue<'s, D, G> {
115    /// Number of readable signature dictionaries, including unindexed ones.
116    #[must_use]
117    pub fn len(&self) -> usize {
118        self.entries.len()
119    }
120
121    /// Whether the complete traversal found no readable signatures.
122    #[must_use]
123    pub fn is_empty(&self) -> bool {
124        self.entries.is_empty()
125    }
126
127    /// One source-bound signature handle, without cloning its source storage.
128    #[must_use]
129    pub fn get(&self, index: usize) -> Option<Signature<'_, 's, D, G>> {
130        self.entries.get(index).map(|entry| Signature {
131            catalogue: self,
132            entry,
133        })
134    }
135    /// Count of ALL actual candidates, including malformed/unavailable inputs.
136    #[must_use]
137    pub fn candidates(&self) -> usize {
138        self.order.len()
139    }
140    /// Source-order member of the complete physical/decoded census.
141    ///
142    /// # Panics
143    /// Panics if an internally recorded readable ordinal leaves its catalogue.
144    #[must_use]
145    pub fn candidate(&self, index: usize) -> Option<Candidate<'_, 's, D, G>> {
146        self.order.get(index).map(|position| match position {
147            Position::Readable(index) => {
148                Candidate::Readable(self.get(*index).expect("original readable ordinal"))
149            }
150            Position::Unreadable(index) => Candidate::Unreadable(&self.failures[*index]),
151        })
152    }
153    /// Every original unreadable input, without losing readable siblings.
154    #[must_use]
155    pub fn failures(&self) -> &[Failure] {
156        &self.failures
157    }
158    /// Full physical/member inventory, including unindexed non-signature data.
159    #[must_use]
160    pub fn definitions(&self) -> &[Definition] {
161        &self.definitions
162    }
163}
164
165/// A signature handle borrows the actual catalogue member that established its
166/// original dictionary, Contents and coverage association.
167#[derive(Clone, Copy)]
168pub struct Signature<'a, 's, D, G> {
169    catalogue: &'a Catalogue<'s, D, G>,
170    entry: &'a Entry,
171}
172
173impl<'a, 's, D: Copy + Eq, G: Copy + Eq> Signature<'a, 's, D, G> {
174    /// Exact original dictionary range and coordinate domain.
175    #[must_use]
176    pub fn location(self) -> Location {
177        self.owner().value.arena.nodes[self.entry.node.0].location
178    }
179
180    /// Entire original encoded Contents string, including its delimiters.
181    #[must_use]
182    pub fn contents(self) -> Location {
183        self.owner().value.arena.nodes[self.entry.contents.0].location
184    }
185
186    /// Original ordered byte ranges. Their sum is not a substitute for the
187    /// exact exclusion/coverage classification.
188    #[must_use]
189    pub fn ranges(self) -> &'a [Span] {
190        &self.entry.ranges
191    }
192
193    /// Whether original ranges cover a complete historical revision.
194    #[must_use]
195    pub fn coverage(self) -> Coverage {
196        self.entry.coverage
197    }
198
199    /// Original recognized `SubFilter` value.
200    #[must_use]
201    pub fn sub_filter(self) -> Option<SubFilter> {
202        self.entry.sub_filter
203    }
204
205    /// Original Type is `DocTimeStamp`, independently of the `SubFilter` value.
206    #[must_use]
207    pub fn is_document_timestamp(self) -> bool {
208        self.entry.timestamp
209    }
210
211    /// Original containing indirect object, including an object-stream member.
212    #[must_use]
213    pub fn definition(self) -> (u64, u16) {
214        self.entry.definition
215    }
216
217    /// Physical incremental update in which this dictionary was defined.
218    #[must_use]
219    pub fn defined_revision(self) -> usize {
220        self.owner().revision
221    }
222
223    pub(crate) fn catalogue(self) -> &'a Catalogue<'s, D, G> {
224        self.catalogue
225    }
226
227    pub(crate) fn owner(&self) -> &'a Reading<'s, D, G> {
228        &self.catalogue.readings[self.entry.owner]
229    }
230
231    pub(crate) fn node(&self) -> NodeId {
232        self.entry.node
233    }
234}
235
236/// Bounded replay of the original ordered `ByteRange` members. It cannot be
237/// constructed from arbitrary replacement spans or a different document.
238pub struct Covered<'q, S: RangeSource> {
239    reader: RangeReader<'q, S>,
240    ranges: &'q [Span],
241}
242
243/// Original range acquisition or sticky receiver refusal.
244#[derive(Debug)]
245pub enum CoveredError<E> {
246    /// Original file/source processing cause and location.
247    Input(crate::range::InputError<E>),
248    /// The CMS driver retains the original callback/processing cause.
249    Receiver(structured_cms::stream::Refused),
250}
251
252impl<S: RangeSource> structured_cms::stream::AsyncSource for Covered<'_, S> {
253    type Error = CoveredError<S::Error>;
254
255    async fn stream<'q, W: Work + 'q, R: structured_cms::stream::Receiver + 'q>(
256        &'q mut self,
257        receiver: &'q mut R,
258        execution: &'q W,
259    ) -> Result<(), Self::Error> {
260        for span in self.ranges {
261            execution.charge_work(1).map_err(|cause| {
262                CoveredError::Input(crate::range::InputError::Resource {
263                    location: self.reader.location(span.start, span.length),
264                    cause,
265                })
266            })?;
267            let mut at = span.start;
268            let end = span.start + span.length;
269            while at < end {
270                let bytes = self
271                    .reader
272                    .chunk_at(at, end)
273                    .await
274                    .map_err(CoveredError::Input)?;
275                receiver.write(bytes).map_err(CoveredError::Receiver)?;
276                at += bytes.len() as u64;
277            }
278        }
279        self.reader
280            .check(self.reader.location(0, self.reader.stamp().length))
281            .map_err(CoveredError::Input)
282    }
283}
284
285pub(crate) fn inspect<S: RangeSource, W: Work>(
286    document: &Document<'_, S>,
287    reading: &Reading<'_, S::Document, S::Generation>,
288    owner: usize,
289    entries: &mut OwnedVec<Entry, W>,
290    failures: &mut OwnedVec<Failure, W>,
291    order: &mut OwnedVec<Position, W>,
292    work: &impl Work,
293) -> crate::Result<()> {
294    inspect_part(
295        document,
296        reading,
297        owner,
298        0..reading.value.arena.nodes.len(),
299        (reading.number, reading.generation),
300        Inspection {
301            entries,
302            failures,
303            order,
304        },
305        work,
306    )
307}
308
309struct Header {
310    byte_range: Option<NodeId>,
311    contents: Option<NodeId>,
312    timestamp: bool,
313    signature: bool,
314    sub_filter: Option<SubFilter>,
315}
316
317fn header(
318    arena: &crate::arena::Arena<impl Work>,
319    node_id: NodeId,
320    work: &impl Work,
321) -> crate::Result<Header> {
322    let byte_range = get(arena, node_id, b"ByteRange", work)?;
323    let contents = get(arena, node_id, b"Contents", work)?;
324    let type_value = get(arena, node_id, b"Type", work)?;
325    let timestamp = type_value
326        .map(|node| named(arena, node, b"DocTimeStamp", work))
327        .transpose()?
328        .unwrap_or(false);
329    let signature = type_value
330        .map(|node| named(arena, node, b"Sig", work))
331        .transpose()?
332        .unwrap_or(false);
333    let sub_filter = if let Some(sub) = get(arena, node_id, b"SubFilter", work)? {
334        Some(if named(arena, sub, b"ETSI.CAdES.detached", work)? {
335            SubFilter::CadesDetached
336        } else if named(arena, sub, b"ETSI.RFC3161", work)? {
337            SubFilter::Rfc3161
338        } else if named(arena, sub, b"adbe.pkcs7.detached", work)? {
339            SubFilter::Pkcs7Detached
340        } else {
341            SubFilter::Other
342        })
343    } else {
344        None
345    };
346    Ok(Header {
347        byte_range,
348        contents,
349        timestamp,
350        signature,
351        sub_filter,
352    })
353}
354
355pub(crate) struct Inspection<'a, W: Work> {
356    pub(crate) entries: &'a mut OwnedVec<Entry, W>,
357    pub(crate) failures: &'a mut OwnedVec<Failure, W>,
358    pub(crate) order: &'a mut OwnedVec<Position, W>,
359}
360
361pub(crate) fn inspect_part<S: RangeSource, W: Work>(
362    document: &Document<'_, S>,
363    reading: &Reading<'_, S::Document, S::Generation>,
364    owner: usize,
365    nodes: core::ops::Range<usize>,
366    definition: (u64, u16),
367    output: Inspection<'_, W>,
368    work: &impl Work,
369) -> crate::Result<()> {
370    let Inspection {
371        entries,
372        failures,
373        order,
374    } = output;
375    let arena = &reading.value.arena;
376    for (index, node) in arena.nodes[nodes.clone()].iter().enumerate() {
377        admit(work, node.location, 1)?;
378        if !matches!(node.kind, Kind::Dictionary(_, _)) {
379            continue;
380        }
381        let node_id = NodeId(nodes.start + index);
382        let Header {
383            byte_range,
384            contents,
385            timestamp,
386            signature,
387            sub_filter,
388        } = header(arena, node_id, work)?;
389        if byte_range.is_none()
390            && !timestamp
391            && !signature
392            && sub_filter != Some(SubFilter::Rfc3161)
393        {
394            continue;
395        }
396        let read = || -> crate::Result<Entry> {
397            let byte_range = byte_range.ok_or(Error {
398                kind: ErrorKind::Syntax(Syntax::Dictionary),
399                location: node.location,
400            })?;
401            let contents = contents.ok_or(Error {
402                kind: ErrorKind::Syntax(Syntax::Dictionary),
403                location: node.location,
404            })?;
405            if !matches!(arena.nodes[contents.0].kind, Kind::String(_)) {
406                return Err(Error {
407                    kind: ErrorKind::Syntax(Syntax::String),
408                    location: arena.nodes[contents.0].location,
409                });
410            }
411            let original = arena.nodes[contents.0].location;
412            let (ranges, coverage) =
413                parsed_ranges(document, arena, byte_range, original, node.location, work)?;
414            Ok(Entry {
415                owner,
416                node: node_id,
417                contents,
418                ranges,
419                sub_filter,
420                coverage,
421                timestamp,
422                definition,
423            })
424        };
425        let position = match read() {
426            Ok(entry) => {
427                let ordinal = entries.len();
428                entries.try_push(entry).map_err(|cause| Error {
429                    kind: ErrorKind::Resource(cause),
430                    location: node.location,
431                })?;
432                Position::Readable(ordinal)
433            }
434            Err(cause)
435                if matches!(
436                    cause.kind,
437                    ErrorKind::Resource(_) | ErrorKind::Limit(_) | ErrorKind::Foreign
438                ) =>
439            {
440                return Err(cause);
441            }
442            Err(cause) => {
443                let ordinal = failures.len();
444                failures
445                    .try_push(Failure {
446                        location: node.location,
447                        cause,
448                        definition,
449                        revision: reading.revision,
450                        document_timestamp: timestamp || sub_filter == Some(SubFilter::Rfc3161),
451                        contents: contents.map(|node| arena.nodes[node.0].location),
452                    })
453                    .map_err(|cause| Error {
454                        kind: ErrorKind::Resource(cause),
455                        location: node.location,
456                    })?;
457                Position::Unreadable(ordinal)
458            }
459        };
460        order.try_push(position).map_err(|cause| Error {
461            kind: ErrorKind::Resource(cause),
462            location: node.location,
463        })?;
464    }
465    Ok(())
466}
467
468fn parsed_ranges<S: RangeSource, W: Work>(
469    document: &Document<'_, S>,
470    arena: &crate::arena::Arena<impl Work>,
471    byte_range: NodeId,
472    original: Location,
473    location: Location,
474    work: &W,
475) -> crate::Result<(OwnedVec<Span>, Coverage)> {
476    let Kind::Array(children) = arena.nodes[byte_range.0].kind else {
477        return Err(Error {
478            kind: ErrorKind::Syntax(Syntax::Number),
479            location: arena.nodes[byte_range.0].location,
480        });
481    };
482    if children.length == 0 || children.length % 2 != 0 {
483        return Err(Error {
484            kind: ErrorKind::Syntax(Syntax::Number),
485            location: arena.nodes[byte_range.0].location,
486        });
487    }
488    let mut next = children.first;
489    let mut ranges = work.vector();
490    let mut previous_end = 0;
491    while let Some(start_link) = next {
492        admit(work, location, 2)?;
493        let first = arena.links[start_link];
494        let second = arena.links[first.next.expect("even ByteRange array")];
495        next = second.next;
496        let start = unsigned(arena, first.value, work)?;
497        let length = unsigned(arena, second.value, work)?;
498        let end = start.checked_add(length).ok_or(Error {
499            kind: ErrorKind::Syntax(Syntax::Number),
500            location,
501        })?;
502        if start < previous_end || end > document.stamp.length {
503            return Err(Error {
504                kind: ErrorKind::Syntax(Syntax::Number),
505                location: arena.nodes[first.value.0].location,
506            });
507        }
508        ranges
509            .try_push(Span { start, length })
510            .map_err(|cause| Error {
511                kind: ErrorKind::Resource(cause),
512                location,
513            })?;
514        previous_end = end;
515    }
516    let mut coverage = Coverage::Partial;
517    if original.source() == Source::File
518        && ranges.len() == 2
519        && ranges[0].start == 0
520        && ranges[0].length == original.span().start
521        && ranges[1].start == original.end()
522    {
523        for revision in 0..document.revisions() {
524            admit(work, location, 1)?;
525            if document.revision(revision).expect("bounded revision").end == previous_end {
526                coverage = Coverage::Revision(revision);
527                break;
528            }
529        }
530    }
531    Ok((ranges.into_owned(), coverage))
532}
533
534impl<'s, S: RangeSource> Document<'s, S> {
535    /// Creates the original-range source for the common asynchronous CMS
536    /// digest/signature driver, without materializing the PDF or its ranges.
537    pub fn covered<'q, W: Work>(
538        &'q self,
539        signature: Signature<'q, 's, S::Document, S::Generation>,
540        operation: &'q W,
541    ) -> Result<Covered<'q, S>, ReadFailure<S::Error>> {
542        let owner = signature.owner();
543        if !core::ptr::eq(owner.value.session, self.session) || owner.value.stamp != self.stamp {
544            return Err(Error {
545                kind: ErrorKind::Foreign,
546                location: signature.location(),
547            }
548            .into());
549        }
550        let reader = RangeReader::new(self.source, self.session, operation)?;
551        if reader.stamp() != self.stamp {
552            return Err(crate::range::InputError::Changed {
553                location: signature.location(),
554            }
555            .into());
556        }
557        Ok(Covered {
558            reader,
559            ranges: &signature.entry.ranges,
560        })
561    }
562    /// Reads signature Contents as decoded bytes with independent actual
563    /// capacity ownership. This retains padding: VRI names hash the complete
564    /// decoded value, while CMS parsing separately identifies its DER extent.
565    pub async fn contents<W: Work>(
566        &self,
567        signature: Signature<'_, 's, S::Document, S::Generation>,
568        operation: &W,
569    ) -> Result<OwnedVec<u8, Memory>, ReadFailure<S::Error>> {
570        let owner = signature.owner();
571        self.string(owner, signature.entry.contents, usize::MAX, operation)
572            .await
573    }
574}