Skip to main content

structured_pades/material/
vri.rs

1//! Normative VRI naming over the complete original decoded Contents value.
2
3use crate::{
4    container::Container,
5    error::{Error, ErrorKind},
6    model::admit,
7};
8use structured_asn1::memory::Work;
9use structured_cms::digest::{DigestBatch, DigestError, PassError};
10use structured_pkix::algorithm::digest::{Algorithm, AsnDigest};
11pub(super) mod sources;
12mod status;
13pub use sources::{Association, Associations, SourceError};
14pub use status::{StatusBinding, StatusError};
15
16/// One exact signature VRI selected by the normative whole-Contents name.
17/// The actual parsed historical owner and root container cannot be replaced
18/// with caller-supplied signature octets or another VRI's timestamp.
19pub struct Binding<'b, 'v, 'a, 's, D, G> {
20    parsed: &'b super::Parsed<'v, 's, D, G>,
21    root: &'b Container<'a, 's, D, G>,
22    vri: &'b super::Vri,
23}
24
25impl<'v, 's, D: Copy + Eq, G: Copy + Eq> super::Parsed<'v, 's, D, G> {
26    /// Selects this signature's actual optional VRI. A named VRI for another
27    /// signature remains in the source census but cannot bind to this root.
28    pub fn bind_signature_vri<'b, 'a, 'p, P: AsnDigest<'p>>(
29        &'b self,
30        root: &'b Container<'a, 's, D, G>,
31        provider: &'p P,
32        operation: &impl Work,
33    ) -> Result<Option<Binding<'b, 'v, 'a, 's, D, G>>, BindingError<P::Error>> {
34        let signature = root.signature();
35        let owner = signature.owner();
36        let location = signature.location();
37        let work = owner.value.session.query(operation);
38        admit(&work, location, 1).map_err(BindingError::Pdf)?;
39        if !core::ptr::eq(owner.value.session, self.owner.session)
40            || owner.value.stamp != self.owner.stamp
41        {
42            return Err(BindingError::Pdf(Error {
43                kind: ErrorKind::Foreign,
44                location,
45            }));
46        }
47        if self.owner.vri.is_empty() {
48            return Ok(None);
49        }
50        let name = root
51            .vri_name(provider, &work)
52            .map_err(BindingError::Name)?
53            .ok_or(BindingError::UnsupportedName)?;
54        let matched = self.owner.vri_names.get(&name, &work).map_err(|cause| {
55            BindingError::Pdf(Error {
56                kind: ErrorKind::Resource(cause),
57                location,
58            })
59        })?;
60        Ok(matched.map(|&ordinal| Binding {
61            parsed: self,
62            root,
63            vri: &self.owner.vri[ordinal],
64        }))
65    }
66}
67
68/// Source/refusal versus selected-provider inability to name an actual VRI.
69#[derive(Debug)]
70pub enum BindingError<E> {
71    /// Original producing/current PDF scope refused.
72    Pdf(Error),
73    /// Whole original Contents hashing failed with its unchanged cause.
74    Name(NameError<E>),
75    /// Selected provider does not expose the prescribed SHA-1 identity.
76    UnsupportedName,
77}
78
79/// Located original input/profile/provider failure of an actual VRI TS.
80#[derive(Debug)]
81pub enum TimestampError<'a, P, R> {
82    /// Original PDF association or original/current resource refusal.
83    Pdf(Error),
84    /// Original unreadable carried value; no second parse is attempted.
85    Material {
86        /// Physical stream enclosing the logical carried refusal.
87        location: crate::source::Location,
88        /// Exact first-read cause, including its logical source coordinates.
89        cause: &'a super::MaterialError,
90    },
91    /// Original carried reader refused before timestamp/provider execution.
92    Encoding(structured_asn1::Error),
93    /// Common timestamp check preserves the original opaque/source cause.
94    Check(
95        structured_cms::timestamp::ProfileCheckError<
96            structured_cms::octets::SourceError,
97            P,
98            R,
99            structured_cades::Error,
100        >,
101    ),
102}
103
104/// Actual signature-octet imprint, token mathematics and selected TSA facts.
105pub struct Time<'q, 'certificate, D, G> {
106    checks: structured_cms::timestamp::CheckedTimestamp<'q>,
107    certificate: &'q structured_pkix::Certificate<'certificate>,
108    key: structured_pkix::path::WorkingKey<'certificate>,
109    snapshot: structured_ltv::source::Snapshot<D, G>,
110    root: crate::timestamp::Node,
111    property: crate::timestamp::Node,
112    token: crate::timestamp::Node,
113    session: &'q crate::Session,
114}
115impl<'q, 'd, D: Copy, G: Copy> Time<'q, 'd, D, G> {
116    /// Complete original common facts, independently of path/trust acceptance.
117    #[must_use]
118    pub fn checks(&self) -> &structured_cms::timestamp::CheckedTimestamp<'q> {
119        &self.checks
120    }
121    /// Issues a validation-data actor only for actual successful common
122    /// evidence. This receipt asserts neither DSS coverage nor trusted time.
123    pub fn into_actor(
124        self,
125    ) -> Option<
126        structured_ltv::timestamp::CheckedActor<
127            'q,
128            'd,
129            crate::timestamp::Node,
130            D,
131            G,
132            crate::timestamp::Pades,
133        >,
134    > {
135        let root = self.root;
136        self.issue(root)
137    }
138    fn issue(
139        self,
140        root: crate::timestamp::Node,
141    ) -> Option<
142        structured_ltv::timestamp::CheckedActor<
143            'q,
144            'd,
145            crate::timestamp::Node,
146            D,
147            G,
148            crate::timestamp::Pades,
149        >,
150    > {
151        let evidence = self.checks.into_evidence()?;
152        let verified = structured_ltv::timestamp::Verified::new(
153            crate::timestamp::ISSUER,
154            structured_ltv::source::Location {
155                snapshot: self.snapshot,
156                node: self.token,
157            },
158            root,
159            self.property,
160            structured_ltv::timestamp::Purpose::ValidationData,
161            evidence,
162        );
163        Some(structured_ltv::timestamp::CheckedActor::new(
164            crate::timestamp::ISSUER,
165            verified,
166            self.certificate,
167            self.key,
168        ))
169    }
170}
171impl<'q, 'd, D: Copy + Eq, G: Copy + Eq> Time<'q, 'd, D, G> {
172    /// Retains an independently checked VRI TSA as a historical obligation of
173    /// another signature in the SAME original PDF. This changes only the LTV
174    /// census namespace; it does not assert coverage of that other signature.
175    pub fn into_historical_actor(
176        self,
177        root: crate::signature::Signature<'_, '_, D, G>,
178        operation: &impl Work,
179    ) -> crate::Result<
180        Option<
181            structured_ltv::timestamp::CheckedActor<
182                'q,
183                'd,
184                crate::timestamp::Node,
185                D,
186                G,
187                crate::timestamp::Pades,
188            >,
189        >,
190    > {
191        let work = self.session.query(operation);
192        let location = root.location();
193        admit(&work, location, 1)?;
194        if !core::ptr::eq(self.session, root.owner().value.session)
195            || self.snapshot != root.owner().value.stamp.snapshot
196        {
197            return Err(Error {
198                kind: ErrorKind::Foreign,
199                location,
200            });
201        }
202        Ok(self.issue(crate::timestamp::Node::of(location)))
203    }
204}
205
206impl<'v, D: Copy + Eq, G: Copy + Eq> Binding<'_, 'v, '_, '_, D, G> {
207    /// Actual matching original VRI, preserving its independent TU/TS method.
208    #[must_use]
209    pub fn vri(&self) -> &super::Vri {
210        self.vri
211    }
212
213    /// Checks the existing parsed TS over the root CMS encryptedDigest
214    /// content octets, not the complete CMS value, PDF bytes or VRI name.
215    /// EN319142-1 §5.4.2.3 note1 defines this exact input. A TU-only or
216    /// subsequent-DTS method returns None without invoking crypto callbacks.
217    /// <https://www.etsi.org/deliver/etsi_en/319100_319199/31914201/01.02.01_60/en_31914201v010201p.pdf>
218    #[expect(
219        clippy::result_large_err,
220        reason = "the common scoped timestamp error retains its original map, provider cause and enclosing material source without an error allocation"
221    )]
222    pub fn check_timestamp<'q, 'certificate, P, PE, RE>(
223        &'q self,
224        root: &'q structured_cms::SignedData<'_>,
225        parent: &'q structured_asn1::Reader<'q>,
226        provider: &'q P,
227        context: &'q structured_cms::timestamp::CheckContext<'certificate, 'q>,
228        recognizes: impl FnMut(
229            &structured_cms::timestamp::TokenExtension<'_>,
230            &structured_asn1::memory::Execution<'_>,
231        ) -> Result<bool, RE>,
232    ) -> Result<Option<Time<'q, 'certificate, D, G>>, TimestampError<'q, PE, RE>>
233    where
234        'v: 'q,
235        'certificate: 'q,
236        P: for<'scope> structured_cms::provider::SignatureProvider<'scope, Error = PE>,
237    {
238        let execution = parent.execution();
239        let work = self.parsed.owner.session.query(&execution);
240        let location = self.vri.location;
241        admit(&work, location, 1).map_err(TimestampError::Pdf)?;
242        self.root
243            .require(root, parent)
244            .map_err(TimestampError::Pdf)?;
245        if root.signers().len() != 1 {
246            return Err(TimestampError::Pdf(Error {
247                kind: ErrorKind::Syntax(crate::error::Syntax::Container),
248                location,
249            }));
250        }
251        let Some(ordinal) = self.vri.timestamp else {
252            return Ok(None);
253        };
254        let item = &self.parsed.owner.items[ordinal];
255        let token = match &self.parsed.values[ordinal] {
256            super::Value::Timestamp(value) => &value.token,
257            super::Value::Unreadable(cause) => {
258                return Err(TimestampError::Material {
259                    location: item.location,
260                    cause,
261                });
262            }
263            _ => {
264                return Err(TimestampError::Pdf(Error {
265                    kind: ErrorKind::Foreign,
266                    location,
267                }));
268            }
269        };
270        let reader = item
271            .session
272            .carried(item.as_bytes(), &parent.execution())
273            .map_err(TimestampError::Encoding)?;
274        let mut input = root.signers()[0].signature();
275        let checks = token
276            .check_historical_input_with_profile(
277                provider,
278                &reader,
279                context,
280                &mut structured_cms::timestamp::RawInput::new(&mut input),
281                recognizes,
282                |input| crate::timestamp::profile(&input),
283            )
284            .map_err(TimestampError::Check)?;
285        Ok(Some(Time {
286            checks,
287            certificate: context.certificate,
288            key: context.key,
289            snapshot: self.parsed.owner.stamp.snapshot,
290            root: crate::timestamp::Node::of(self.root.signature().location()),
291            property: crate::timestamp::Node::dss(self.vri.location, self.parsed.owner.revision),
292            token: crate::timestamp::Node::dss(item.location, self.parsed.owner.revision),
293            session: self.parsed.owner.session,
294        }))
295    }
296}
297
298/// Uppercase hexadecimal SHA-1 name over the prescribed original input.
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
300pub struct Name(pub(super) [u8; 40]);
301impl structured_asn1::memory::HashWork for Name {
302    fn hash_work(&self) -> Result<u64, structured_asn1::memory::MemoryError> {
303        Ok(41)
304    }
305}
306impl Name {
307    /// Exact decoded PDF name bytes, without a slash or alternative casing.
308    #[must_use]
309    pub fn as_bytes(&self) -> &[u8; 40] {
310        &self.0
311    }
312}
313
314/// Original resource, provider or digest-output contract failure.
315#[derive(Debug)]
316pub enum NameError<E> {
317    /// The original status signature is not a whole-octet BIT STRING.
318    PaddedSignature,
319    /// Original PDF source/current operation refused.
320    Pdf(Error),
321    /// Original digest preparation or memo query failed.
322    Digest(DigestError<E>),
323    /// Original complete Contents pass/provider failure.
324    Pass(PassError<structured_cms::stream::RangesError, E>),
325    /// Selected SHA-1 backend returned an inconsistent output width.
326    Width(usize),
327    /// The successful batch lost its single prepared request.
328    LostRequest,
329}
330
331impl<D: Copy + Eq, G: Copy + Eq> Container<'_, '_, D, G> {
332    /// EN319142-1 §5.4.2.2(a) hashes all decoded Contents bytes, INCLUDING its
333    /// unused padding, not just the parsed DER extent. SHA-1 here identifies
334    /// a VRI dictionary; it does not grant a signature-algorithm permission.
335    /// <https://www.etsi.org/deliver/etsi_EN/319100_319199/31914201/01.02.01_60/en_31914201v010201p.pdf>
336    pub fn vri_name<'p, P: AsnDigest<'p>, W: Work>(
337        &self,
338        provider: &'p P,
339        operation: &W,
340    ) -> Result<Option<Name>, NameError<P::Error>> {
341        let location = self.signature().contents();
342        let work = self.signature().owner().value.session.query(operation);
343        name_for_ranges(provider, &mut [self.contents()], &work, location)
344    }
345}
346
347fn name_for_ranges<'p, P: AsnDigest<'p>, const N: usize>(
348    provider: &'p P,
349    ranges: &mut [&[u8]; N],
350    work: &impl Work,
351    location: crate::source::Location,
352) -> Result<Option<Name>, NameError<P::Error>> {
353    admit(work, location, 1).map_err(NameError::Pdf)?;
354    let Some(identity) = provider.identity(Algorithm::Sha1) else {
355        return Ok(None);
356    };
357    let mut identities = work.vector();
358    identities.try_push(identity).map_err(|cause| {
359        NameError::Pdf(Error {
360            kind: ErrorKind::Resource(cause),
361            location,
362        })
363    })?;
364    let batch = DigestBatch::prepare(
365        provider,
366        identities.into_owned(),
367        work,
368        "pades.vri",
369        location.span().start,
370    )
371    .map_err(NameError::Digest)?;
372    let memo = batch.run(ranges, work).map_err(NameError::Pass)?;
373    let output = memo
374        .for_request(0, work)
375        .map_err(NameError::Digest)?
376        .ok_or(NameError::LostRequest)?;
377    let bytes = output.as_bytes();
378    if bytes.len() != 20 {
379        return Err(NameError::Width(bytes.len()));
380    }
381    admit(work, location, 40).map_err(NameError::Pdf)?;
382    let mut name = [0; 40];
383    for (pair, byte) in name.as_chunks_mut::<2>().0.iter_mut().zip(bytes.iter()) {
384        pair[0] = b"0123456789ABCDEF"[usize::from(byte >> 4)];
385        pair[1] = b"0123456789ABCDEF"[usize::from(byte & 15)];
386    }
387    Ok(Some(Name(name)))
388}