Skip to main content

structured_pades/material/vri/
sources.rs

1//! Complete historical VRI-name associations, without success filtering.
2
3use super::{Name, NameError, status::status_name};
4use crate::{
5    container::{Container, ErrorOf},
6    material::{HistoryError, ParsedHistory, Value},
7    model::admit,
8    query::Query,
9    range::RangeSource,
10    source::Location,
11    timestamp::{
12        Node,
13        sources::{Models, Slot},
14    },
15};
16use structured_asn1::memory::{OwnedMap, OwnedVec, Work};
17use structured_ltv::source::{Query as _, ScopeError};
18use structured_pkix::{algorithm::digest::AsnDigest, signature::SignatureValue};
19
20/// One original VRI occurrence's normative nominal association. SHA-1 names
21/// identify inputs; they do not authenticate a signature or certificate status.
22pub struct Association {
23    revision: usize,
24    ordinal: usize,
25    location: Location,
26    named: bool,
27    timely: bool,
28}
29impl Association {
30    /// Original historical interpretation window.
31    #[must_use]
32    pub fn revision(&self) -> usize {
33        self.revision
34    }
35    /// Original VRI ordinal in that window.
36    #[must_use]
37    pub fn ordinal(&self) -> usize {
38        self.ordinal
39    }
40    /// Original VRI key, retaining physical/logical source coordinates.
41    #[must_use]
42    pub fn location(&self) -> Location {
43        self.location
44    }
45    /// At least one prescribed original signature input has this name.
46    #[must_use]
47    pub fn has_named_input(&self) -> bool {
48        self.named
49    }
50    /// The named signature exists in the permitted historical window.
51    #[must_use]
52    pub fn all_placement_requirements_met(&self) -> bool {
53        self.timely
54    }
55}
56/// Every original VRI occurrence, including negative nominal associations.
57pub struct Associations {
58    values: OwnedVec<Association>,
59    complete: bool,
60}
61impl Associations {
62    /// Source order is historical revision followed by original VRI ordinal.
63    #[must_use]
64    pub fn entries(&self) -> &[Association] {
65        &self.values
66    }
67    /// All original VRI names and incremental placement requirements hold.
68    /// Imprint mathematics, generation time and trust are separate facts.
69    #[must_use]
70    pub fn all_requirements_met(&self) -> bool {
71        self.complete
72    }
73}
74
75/// Original source/metadata/hash cause, never an unassociated-name finding.
76#[derive(Debug)]
77pub enum SourceError<E, P> {
78    /// Original historical material or index refusal.
79    History(HistoryError),
80    /// Original PDF source scope refused.
81    Scope(ScopeError<Node>),
82    /// Original PDF input/container processing refused.
83    Contents(ErrorOf<E>),
84    /// Prescribed name hashing refused with its original provider cause.
85    Name(NameError<P>),
86    /// Actual common material metadata query refused.
87    Cms(structured_cades::Error),
88    /// The selected provider cannot name SHA-1 inputs.
89    UnsupportedName,
90}
91impl<E, P> From<HistoryError> for SourceError<E, P> {
92    fn from(cause: HistoryError) -> Self {
93        Self::History(cause)
94    }
95}
96
97struct Row {
98    value: Association,
99    name: Name,
100}
101struct Available {
102    dictionary: Option<usize>,
103    status: Option<usize>,
104}
105struct Table<W: Work> {
106    heads: OwnedMap<Name, Available, W>,
107    rows: OwnedVec<Row, W>,
108}
109enum StatusNameError<P> {
110    Unsupported,
111    Name(NameError<P>),
112}
113enum NamingError<P> {
114    Scope(ScopeError<Node>),
115    Name(NameError<P>),
116    Unsupported,
117}
118impl<P> From<StatusNameError<P>> for NamingError<P> {
119    fn from(value: StatusNameError<P>) -> Self {
120        match value {
121            StatusNameError::Unsupported => Self::Unsupported,
122            StatusNameError::Name(cause) => Self::Name(cause),
123        }
124    }
125}
126impl<E, P> From<NamingError<P>> for SourceError<E, P> {
127    fn from(value: NamingError<P>) -> Self {
128        match value {
129            NamingError::Scope(cause) => Self::Scope(cause),
130            NamingError::Name(cause) => Self::Name(cause),
131            NamingError::Unsupported => Self::UnsupportedName,
132        }
133    }
134}
135impl<E, P> From<StatusNameError<P>> for SourceError<E, P> {
136    fn from(cause: StatusNameError<P>) -> Self {
137        match cause {
138            StatusNameError::Unsupported => Self::UnsupportedName,
139            StatusNameError::Name(cause) => Self::Name(cause),
140        }
141    }
142}
143impl<E, P> From<crate::Error> for SourceError<E, P> {
144    fn from(cause: crate::Error) -> Self {
145        Self::Name(NameError::Pdf(cause))
146    }
147}
148impl<W: Work> Table<W> {
149    fn mark(
150        &mut self,
151        name: Name,
152        available: usize,
153        dictionary: bool,
154        work: &W,
155        location: Location,
156    ) -> crate::Result<()> {
157        let resource = |cause| crate::Error {
158            kind: crate::ErrorKind::Resource(cause),
159            location,
160        };
161        if let Some(value) = self.heads.get_mut(&name, work).map_err(resource)? {
162            admit(work, location, 1)?;
163            let earliest = if dictionary {
164                &mut value.dictionary
165            } else {
166                &mut value.status
167            };
168            if earliest.is_none_or(|previous| available < previous) {
169                *earliest = Some(available);
170            }
171        }
172        Ok(())
173    }
174    fn status<'p, P: AsnDigest<'p>>(
175        &mut self,
176        signature: SignatureValue<'_>,
177        available: usize,
178        provider: &'p P,
179        work: &W,
180        location: Location,
181    ) -> Result<(), StatusNameError<P::Error>> {
182        let name = match status_name(signature, provider, work, location) {
183            Ok(Some(name)) => name,
184            Ok(None) => return Err(StatusNameError::Unsupported),
185            Err(NameError::PaddedSignature) => return Ok(()),
186            Err(cause) => return Err(StatusNameError::Name(cause)),
187        };
188        self.mark(name, available, false, work, location)
189            .map_err(|cause| StatusNameError::Name(NameError::Pdf(cause)))
190    }
191}
192
193fn model_names<'q, 'p, P: AsnDigest<'p>, S: RangeSource>(
194    table: &mut Table<structured_asn1::memory::Execution<'q>>,
195    models: &Models<'_, '_, '_, '_, S::Document, S::Generation>,
196    provider: &'p P,
197    query: &Query<'q, '_, S>,
198    location: Location,
199) -> Result<(), NamingError<P::Error>> {
200    let work = query.execution();
201    for (ordinal, model) in models.values.iter().enumerate() {
202        query
203            .admit(Node::of(location), 1)
204            .map_err(NamingError::Scope)?;
205        let Some(Slot::Value(container)) = models
206            .sources
207            .get(ordinal, work)
208            .map_err(|cause| NamingError::Name(NameError::Pdf(cause)))?
209        else {
210            continue;
211        };
212        let signature = container.signature();
213        let name = container
214            .vri_name(provider, work)
215            .map_err(NamingError::Name)?
216            .ok_or(NamingError::Unsupported)?;
217        table
218            .mark(
219                name,
220                signature.defined_revision(),
221                true,
222                work,
223                signature.location(),
224            )
225            .map_err(|cause| NamingError::Name(NameError::Pdf(cause)))?;
226        let crate::timestamp::sources::Model::Known { revocations, .. } = model else {
227            continue;
228        };
229        for value in revocations {
230            query
231                .admit(Node::of(location), 1)
232                .map_err(NamingError::Scope)?;
233            match &value.value {
234                Ok(structured_cms::signed::revocation::Reading::Crl(value)) => table.status(
235                    value.signature_value(),
236                    signature.defined_revision(),
237                    provider,
238                    work,
239                    signature.location(),
240                )?,
241                Ok(structured_cms::signed::revocation::Reading::Ocsp(value)) => table.status(
242                    value.signature_value(),
243                    signature.defined_revision(),
244                    provider,
245                    work,
246                    signature.location(),
247                )?,
248                _ => {}
249            }
250        }
251    }
252    Ok(())
253}
254
255fn historical_names<'q, 'p, P: AsnDigest<'p>, S: RangeSource>(
256    table: &mut Table<structured_asn1::memory::Execution<'q>>,
257    history: &ParsedHistory<'_, '_, '_, S::Document, S::Generation>,
258    revision: usize,
259    provider: &'p P,
260    query: &Query<'q, '_, S>,
261    location: Location,
262) -> Result<(), NamingError<P::Error>> {
263    let work = query.execution();
264    for (revision, parsed) in history.revisions()[..=revision].iter().enumerate() {
265        for (ordinal, value) in parsed.values().iter().enumerate() {
266            query
267                .admit(Node::of(location), 1)
268                .map_err(NamingError::Scope)?;
269            let location = parsed.owner.items()[ordinal].location();
270            match value {
271                Value::Crl(value) => {
272                    table.status(value.signature_value(), revision, provider, work, location)?;
273                }
274                Value::Ocsp {
275                    basic: Ok(Some(value)),
276                    ..
277                } => table.status(value.signature_value(), revision, provider, work, location)?,
278                Value::Timestamp(value) => {
279                    for value in &value.revocations {
280                        query
281                            .admit(Node::of(location), 1)
282                            .map_err(NamingError::Scope)?;
283                        match &value.value {
284                            Ok(structured_cms::signed::revocation::Reading::Crl(value)) => table
285                                .status(
286                                    value.signature_value(),
287                                    revision,
288                                    provider,
289                                    work,
290                                    location,
291                                )?,
292                            Ok(structured_cms::signed::revocation::Reading::Ocsp(value)) => table
293                                .status(
294                                value.signature_value(),
295                                revision,
296                                provider,
297                                work,
298                                location,
299                            )?,
300                            _ => {}
301                        }
302                    }
303                }
304                _ => {}
305            }
306        }
307    }
308    Ok(())
309}
310
311fn finish_table<'q, S: RangeSource, E>(
312    table: Table<structured_asn1::memory::Execution<'q>>,
313    query: &Query<'q, '_, S>,
314    location: Location,
315) -> Result<Associations, NamingError<E>> {
316    let work = query.execution();
317    let resource = |cause| {
318        NamingError::Name(NameError::Pdf(crate::Error {
319            kind: crate::ErrorKind::Resource(cause),
320            location,
321        }))
322    };
323    let mut values = work.vector();
324    values
325        .try_reserve_exact(table.rows.len())
326        .map_err(resource)?;
327    let mut complete = true;
328    for mut row in table.rows {
329        query
330            .admit(Node::of(location), 1)
331            .map_err(NamingError::Scope)?;
332        let available = table
333            .heads
334            .get(&row.name, work)
335            .map_err(resource)?
336            .expect("original VRI name");
337        row.value.named = available.dictionary.is_some() || available.status.is_some();
338        // EN319142-1 ยง5.4.2.2: VRI is incremental, and a signature
339        // dictionary in an update precedes its associated VRI update.
340        row.value.timely = row.value.revision > 0
341            && (available
342                .dictionary
343                .is_some_and(|revision| row.value.revision > revision)
344                || available
345                    .status
346                    .is_some_and(|revision| row.value.revision >= revision));
347        if !row.value.timely {
348            complete = false;
349        }
350        values.try_push(row.value).map_err(resource)?;
351    }
352    Ok(Associations {
353        values: values.into_owned(),
354        complete,
355    })
356}
357
358fn seed_table<'q, S: RangeSource, E>(
359    history: &ParsedHistory<'_, '_, '_, S::Document, S::Generation>,
360    revision: usize,
361    location: Location,
362    query: &Query<'q, '_, S>,
363) -> Result<Table<structured_asn1::memory::Execution<'q>>, NamingError<E>> {
364    let work = query.execution();
365    let resource = |cause| {
366        NamingError::Name(NameError::Pdf(crate::Error {
367            kind: crate::ErrorKind::Resource(cause),
368            location,
369        }))
370    };
371    let mut table = Table {
372        heads: work.map(),
373        rows: work.vector(),
374    };
375    for (revision, parsed) in history.revisions()[..=revision].iter().enumerate() {
376        for (ordinal, vri) in parsed.owner.vri().iter().enumerate() {
377            query
378                .admit(Node::dss(vri.location(), revision), 41)
379                .map_err(NamingError::Scope)?;
380            table
381                .heads
382                .try_insert(
383                    Name(*vri.name()),
384                    Available {
385                        dictionary: None,
386                        status: None,
387                    },
388                )
389                .map_err(resource)?;
390            table
391                .rows
392                .try_push(Row {
393                    value: Association {
394                        revision,
395                        ordinal,
396                        location: vri.location(),
397                        named: false,
398                        timely: false,
399                    },
400                    name: Name(*vri.name()),
401                })
402                .map_err(resource)?;
403        }
404    }
405    Ok(table)
406}
407
408#[expect(
409    clippy::result_large_err,
410    reason = "historical association failures preserve original item and nested format causes without allocating a refusal envelope"
411)]
412pub(in crate::material) async fn associate<'p, P, S>(
413    history: &ParsedHistory<'_, '_, '_, S::Document, S::Generation>,
414    root: &Container<'_, '_, S::Document, S::Generation>,
415    pool: &structured_cades::material::Pool<'_, '_, '_>,
416    models: &Models<'_, '_, '_, '_, S::Document, S::Generation>,
417    provider: &'p P,
418    query: &Query<'_, '_, S>,
419    revision: usize,
420) -> Result<Associations, SourceError<S::Error, P::Error>>
421where
422    S: RangeSource,
423    P: AsnDigest<'p>,
424{
425    let work = query.execution();
426    let location = root.signature().location();
427    let mut table = seed_table(history, revision, location, query).map_err(SourceError::from)?;
428    if table.rows.is_empty() {
429        return Ok(Associations {
430            values: work.vector().into_owned(),
431            complete: true,
432        });
433    }
434    let name = root
435        .vri_name(provider, work)
436        .map_err(SourceError::Name)?
437        .ok_or(SourceError::UnsupportedName)?;
438    table.mark(
439        name,
440        root.signature().defined_revision(),
441        true,
442        work,
443        location,
444    )?;
445    let catalogue = root.signature().catalogue();
446    for ordinal in 0..catalogue.len() {
447        query
448            .admit(Node::of(location), 1)
449            .map_err(SourceError::Scope)?;
450        let signature = catalogue.get(ordinal).expect("bounded original signature");
451        if signature.location() == location
452            || signature.is_document_timestamp()
453            || signature.defined_revision() > revision
454        {
455            continue;
456        }
457        if !matches!(
458            signature.sub_filter(),
459            Some(
460                crate::signature::SubFilter::CadesDetached
461                    | crate::signature::SubFilter::Pkcs7Detached
462            )
463        ) {
464            continue;
465        }
466        let container = match query.document().container(signature, work).await {
467            Ok(value) => value,
468            Err(cause) if cause.is_terminal() => return Err(SourceError::Contents(cause)),
469            Err(_) => continue,
470        };
471        let name = container
472            .vri_name(provider, work)
473            .map_err(SourceError::Name)?
474            .ok_or(SourceError::UnsupportedName)?;
475        table.mark(
476            name,
477            signature.defined_revision(),
478            true,
479            work,
480            signature.location(),
481        )?;
482    }
483    let available = root.signature().defined_revision();
484    for ordinal in 0..pool.crls() {
485        let crl = pool
486            .crl(ordinal, work)
487            .map_err(SourceError::Cms)?
488            .expect("bounded actual CRL");
489        table.status(crl.signature_value(), available, provider, work, location)?;
490    }
491    for ordinal in 0..pool.ocsp_responses() {
492        let ocsp = pool
493            .ocsp(ordinal, work)
494            .map_err(SourceError::Cms)?
495            .expect("bounded actual basic response");
496        table.status(ocsp.signature_value(), available, provider, work, location)?;
497    }
498    model_names(&mut table, models, provider, query, location).map_err(SourceError::from)?;
499    historical_names(&mut table, history, revision, provider, query, location)
500        .map_err(SourceError::from)?;
501    finish_table(table, query, location).map_err(SourceError::from)
502}