Skip to main content

structured_cades/material/
credentials.rs

1//! Format-owner dependencies of every authenticated original credential.
2//!
3//! Reuses the original-artifact/coverage LTV core. Authentication receipts are
4//! borrowed, never reconstructed from a public boolean or replayed signature.
5
6use super::{Index, Open};
7use crate::{
8    Cades,
9    counters::Paths,
10    credential::{Checked, Complete},
11    source::{DocumentId, Node, Original},
12    timestamp::ArchiveView,
13};
14use structured_asn1::{
15    Reader, ScopedReader,
16    memory::{MemoryError, OwnedMap, OwnedVec, Work},
17};
18pub use structured_ltv::material::{Completeness, Finding, Missing, VisitError};
19use structured_ltv::{
20    baseline::CredentialMaterial,
21    material::{ArchiveError, Archived, IndexError},
22    pki::{CounterReceipts, CredentialReceipts},
23    source::{Query, Snapshot},
24    timestamp::{ObligationError, TimestampRequirements, TimestampScope},
25};
26
27/// One dependency established by the credential's actual format owner.
28pub type Need<'value, 'source, 'model, 'document> =
29    structured_ltv::material::Dependency<'value, 'source, Node, DocumentId<'model, 'document>, ()>;
30
31/// Same format owner as authentication, using its actual retained evidence.
32/// It discovers ALL credential-specific validation material, including the
33/// credential's own status, using current Work before decoding/copy/allocation
34/// or arithmetic. Certificate-free authentication does not waive this step.
35/// EN319122-1 ยงยง5.2.6.1, 5.4 and 6.3 require credential validation data in
36/// addition to the issuer's X.509 path/status.
37/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31912201/01.03.01_60/en_31912201v010301p.pdf>
38pub trait Provider<E> {
39    /// Original processing/resource cause, not a negative completeness result.
40    type Error;
41    /// Visit dependencies once without reauthenticating or acquiring sources.
42    fn visit<'m, 'd, V>(
43        &self,
44        credential: &Checked<'m, 'd, '_, E>,
45        open: &[Open<'d>],
46        operation: &ScopedReader<'d, '_>,
47        visitor: impl for<'value, 'source> FnMut(Need<'value, 'source, 'm, 'd>) -> Result<(), V>,
48    ) -> Result<Completeness, VisitError<Self::Error, V>>;
49}
50
51/// Actual authenticated credential, including all negative material facts.
52pub struct Entry<'model, 'document> {
53    source: Original<'model, 'document>,
54    completeness: Completeness,
55    findings: OwnedVec<Finding>,
56}
57impl<'m, 'd> Entry<'m, 'd> {
58    /// Original protected credential enclosure.
59    #[must_use]
60    pub fn source(&self) -> Original<'m, 'd> {
61        self.source
62    }
63    /// Actual format interpretation rather than successful copy lookup.
64    #[must_use]
65    pub fn completeness(&self) -> Completeness {
66        self.completeness
67    }
68    /// All missing dependencies, preserving original visitation order.
69    #[must_use]
70    pub fn findings(&self) -> &[Finding] {
71        &self.findings
72    }
73}
74
75/// Same complete credential receipt under one whole/archive source scope.
76pub struct Report<'auth, 'receipt, 'model, 'document, 'path, E> {
77    credentials: &'auth Complete<'receipt, 'model, 'document, 'path, E>,
78    scope: TimestampScope<Node, DocumentId<'model, 'document>, ()>,
79    entries: OwnedVec<Entry<'model, 'document>>,
80    complete: bool,
81}
82impl<'m, 'd, E> Report<'_, '_, 'm, 'd, '_, E> {
83    /// Every original credential required by this exact source scope.
84    #[must_use]
85    pub fn entries(&self) -> &[Entry<'m, 'd>] {
86        &self.entries
87    }
88    /// Complete interpretation AND original incorporation/coverage.
89    #[must_use]
90    pub fn all_requirements_met(&self) -> bool {
91        self.complete
92    }
93}
94impl<'r, 'm, 'd, 'p, E> CredentialMaterial for Report<'_, 'r, 'm, 'd, 'p, E> {
95    type Id = Node;
96    type Document = DocumentId<'m, 'd>;
97    type Generation = ();
98    type Credentials = Complete<'r, 'm, 'd, 'p, E>;
99    fn scope(&self) -> TimestampScope<Node, Self::Document, ()> {
100        self.scope
101    }
102    fn snapshot(&self) -> Snapshot<Self::Document, ()> {
103        self.credentials.snapshot()
104    }
105    fn credentials(&self) -> &Self::Credentials {
106        self.credentials
107    }
108    fn all_requirements_met(&self) -> bool {
109        self.complete
110    }
111}
112
113/// A source/handler/resource failure never returns a successful partial report.
114#[derive(Debug)]
115pub enum Error<E> {
116    /// Another document, signature, incomplete counter set or archive context.
117    Foreign {
118        /// Actual selecting enclosure.
119        node: Node,
120    },
121    /// Original parser/current-operation refusal.
122    Processing(crate::Error),
123    /// Original full counter/scope membership refusal.
124    Obligation(ObligationError<Node>),
125    /// Exact original indexed lookup refused.
126    Index(IndexError<Node>),
127    /// Archive evidence or covered lookup refused.
128    Archive(ArchiveError<Node>),
129    /// Actual credential handler refused with its original cause.
130    Provider {
131        /// Original protected credential.
132        node: Node,
133        /// Unchanged opaque cause.
134        cause: E,
135    },
136    /// Current work or retained report capacity refused before use.
137    Resource {
138        /// Original selecting enclosure.
139        node: Node,
140        /// Actual admission failure.
141        cause: MemoryError,
142    },
143    /// Handler returned a visitor refusal without an actual refused callback.
144    CallbackProtocol {
145        /// Original credential enclosure.
146        node: Node,
147    },
148}
149
150/// Optional archive proof. Whole mode cannot impersonate a preceding archive;
151/// construction retains the actual token's private mathematical receipt.
152pub struct Scope<'a, 'm, 'd, 'p> {
153    archive: Option<ArchiveView<'a, 'm, 'd, 'p>>,
154}
155impl<'a, 'm, 'd, 'p> Scope<'a, 'm, 'd, 'p> {
156    /// Whole original incorporation without archive implications.
157    #[must_use]
158    pub fn whole() -> Self {
159        Self { archive: None }
160    }
161    /// Actual modern or historical archive whose exact input must cover the
162    /// dependencies. Both use the SAME retained format-material mechanism.
163    #[must_use]
164    pub fn before_archive(archive: impl Into<ArchiveView<'a, 'm, 'd, 'p>>) -> Self {
165        Self {
166            archive: Some(archive.into()),
167        }
168    }
169    fn require<E, Q: Query<Id = Node, Document = DocumentId<'m, 'd>, Generation = ()>, W: Work>(
170        &self,
171        required: &TimestampRequirements<Node, DocumentId<'m, 'd>, (), Cades>,
172        index: &Index<'_, 'm, 'd, W>,
173        query: &Q,
174    ) -> Result<(), Error<E>> {
175        let location = required.scope().location();
176        if location.snapshot != query.snapshot() {
177            return Err(Error::Foreign {
178                node: location.node,
179            });
180        }
181        index
182            .require_signature(required.signature(), query)
183            .map_err(Error::Index)?;
184        match (required.scope(), self.archive) {
185            (TimestampScope::All(_), None) => Ok(()),
186            (TimestampScope::BeforeArchive(_), Some(archive)) => {
187                let actor = archive.timestamp().actor().ok_or(Error::Foreign {
188                    node: location.node,
189                })?;
190                let evidence = actor.verified();
191                query
192                    .admit(location.node, 3)
193                    .map_err(|cause| Error::Index(IndexError::Scope(cause)))?;
194                if evidence.signature() != required.signature()
195                    || archive.coverage().subject() != location.node
196                    || evidence.token().snapshot != location.snapshot
197                {
198                    return Err(Error::Foreign {
199                        node: location.node,
200                    });
201                }
202                archive
203                    .coverage()
204                    .require_evidence(evidence, location.node, query)
205                    .map_err(|cause| Error::Archive(ArchiveError::Coverage(cause)))
206            }
207            _ => Err(Error::Foreign {
208                node: location.node,
209            }),
210        }
211    }
212    fn missing<E, Q: Query<Id = Node, Document = DocumentId<'m, 'd>, Generation = ()>, W: Work>(
213        &self,
214        need: Need<'_, '_, 'm, 'd>,
215        index: &Index<'_, 'm, 'd, W>,
216        open: &OwnedMap<Node, (), Q::Work>,
217        query: &Q,
218    ) -> Result<Option<Missing>, Error<E>> {
219        match need {
220            Need::Missing => Ok(Some(Missing::Absent)),
221            Need::Known(artifact) => {
222                if let Some(archive) = self.archive {
223                    let actor = archive.timestamp().actor().ok_or(Error::Foreign {
224                        node: index.signature(),
225                    })?;
226                    return index
227                        .archived(artifact, archive.coverage(), actor.verified(), query)
228                        .map(|answer| match answer {
229                            Archived::Present(_) => None,
230                            Archived::Missing => Some(Missing::Absent),
231                            Archived::Uncovered => Some(Missing::Uncovered),
232                        })
233                        .map_err(Error::Archive);
234                }
235                let mut copies = index.artifact(artifact, query).map_err(Error::Index)?;
236                copies
237                    .next()
238                    .transpose()
239                    .map(|value| {
240                        if value.is_some() {
241                            None
242                        } else {
243                            Some(Missing::Absent)
244                        }
245                    })
246                    .map_err(Error::Index)
247            }
248            Need::Open(location) => {
249                if location.snapshot != query.snapshot() {
250                    return Err(Error::Foreign {
251                        node: location.node,
252                    });
253                }
254                if open
255                    .get(&location.node, query.execution())
256                    .map_err(|cause| Error::Resource {
257                        node: location.node,
258                        cause,
259                    })?
260                    .is_none()
261                {
262                    return Ok(Some(Missing::Absent));
263                }
264                if let Some(archive) = self.archive {
265                    let actor = archive.timestamp().actor().ok_or(Error::Foreign {
266                        node: location.node,
267                    })?;
268                    return archive
269                        .coverage()
270                        .covers(actor.verified(), location.node, query)
271                        .map(|covered| {
272                            if covered {
273                                None
274                            } else {
275                                Some(Missing::Uncovered)
276                            }
277                        })
278                        .map_err(|cause| Error::Archive(ArchiveError::Coverage(cause)));
279                }
280                Ok(None)
281            }
282        }
283    }
284}
285
286/// Check the complete original credential scope, using actual format receipts
287/// and the shared exact-artifact index. A swallowed callback failure remains
288/// terminal; later provider success cannot turn it into an empty complete set.
289pub fn check<'a, 'r, 'm, 'd, 'p, E, P: Provider<E>, W: Work>(
290    credentials: &'a Complete<'r, 'm, 'd, 'p, E>,
291    counters: &Paths<'_, 'm, 'd, 'p>,
292    required: &TimestampRequirements<Node, DocumentId<'m, 'd>, (), Cades>,
293    index: &Index<'_, 'm, 'd, W>,
294    scope: &Scope<'_, 'm, 'd, '_>,
295    provider: &P,
296    operation: &Reader<'d>,
297) -> Result<Report<'a, 'r, 'm, 'd, 'p, E>, Error<P::Error>> {
298    let document = credentials.snapshot().document.source();
299    document
300        .message()
301        .require_session(operation, "cades.credential.material")
302        .map_err(|cause| Error::Processing(crate::Error::Cms(cause)))?;
303    let execution = operation.execution();
304    let query = &structured_ltv::source::Scope::new(&document, credentials.snapshot(), &execution);
305    scope.require(required, index, query)?;
306    let node = credentials.signature();
307    query
308        .admit(node, 3)
309        .map_err(|cause| Error::Index(IndexError::Scope(cause)))?;
310    if credentials.snapshot() != query.snapshot()
311        || counters.snapshot() != query.snapshot()
312        || node != required.signature()
313        || node != counters.signature()
314    {
315        return Err(Error::Foreign { node });
316    }
317    required
318        .includes_counters(counters, query)
319        .map_err(|cause| Error::Index(IndexError::Scope(cause)))?;
320    let work = query.execution();
321    let mut open = work.map();
322    for entry in index.open() {
323        work.charge_work(1).map_err(|cause| Error::Resource {
324            node: entry.node,
325            cause,
326        })?;
327        open.try_insert(entry.node, ())
328            .map_err(|cause| Error::Resource {
329                node: entry.node,
330                cause,
331            })?;
332    }
333    let mut selected = work.vector();
334    for entry in credentials.report.root() {
335        selected
336            .try_push(entry)
337            .map_err(|cause| Error::Resource { node, cause })?;
338    }
339    let mut group = 0;
340    for ordinal in 0..counters.len() {
341        work.charge_work(2)
342            .map_err(|cause| Error::Resource { node, cause })?;
343        let Some((signature, entries)) = credentials
344            .report
345            .counter(group, work)
346            .map_err(|cause| Error::Resource { node, cause })?
347        else {
348            break;
349        };
350        if signature != counters.counter(ordinal).signature {
351            continue;
352        }
353        if required
354            .counter_in_scope(ordinal, query)
355            .map_err(Error::Obligation)?
356        {
357            for entry in entries {
358                selected
359                    .try_push(entry)
360                    .map_err(|cause| Error::Resource { node, cause })?;
361            }
362        }
363        group += 1;
364    }
365    if group != credentials.report.counters() {
366        return Err(Error::Foreign { node });
367    }
368    let mut entries = work.vector();
369    entries
370        .try_reserve_exact(selected.len())
371        .map_err(|cause| Error::Resource { node, cause })?;
372    let mut complete = true;
373    for credential in selected {
374        let entry = one(credential, index, scope, provider, operation, query, &open)?;
375        if entry.completeness != Completeness::Complete || !entry.findings.is_empty() {
376            complete = false;
377        }
378        entries
379            .try_push(entry)
380            .map_err(|cause| Error::Resource { node, cause })?;
381    }
382    query
383        .admit(node, 1)
384        .map_err(|cause| Error::Index(IndexError::Scope(cause)))?;
385    Ok(Report {
386        credentials,
387        scope: required.scope(),
388        entries: entries.into_owned(),
389        complete,
390    })
391}
392
393fn one<'m, 'd, E, P: Provider<E>, Q, W: Work>(
394    credential: &Checked<'m, 'd, '_, E>,
395    index: &Index<'_, 'm, 'd, W>,
396    scope: &Scope<'_, 'm, 'd, '_>,
397    provider: &P,
398    operation: &Reader<'d>,
399    query: &Q,
400    open: &OwnedMap<Node, (), Q::Work>,
401) -> Result<Entry<'m, 'd>, Error<P::Error>>
402where
403    Q: Query<Id = Node, Document = DocumentId<'m, 'd>, Generation = ()>,
404{
405    let work = query.execution();
406    let node = credential.source().node;
407    let mut visit = structured_ltv::material::DependencyVisit::new(node, work, |need| {
408        scope.missing(need, index, open, query)
409    })
410    .map_err(|cause| Error::Resource { node, cause })?;
411    let answer = provider.visit(credential, index.open(), operation, |need| {
412        visit.observe(need)
413    });
414    let result = visit.finish(answer).map_err(|cause| match cause {
415        structured_ltv::material::DependencyError::Processing { cause, .. } => cause,
416        structured_ltv::material::DependencyError::Resource { node, cause } => {
417            Error::Resource { node, cause }
418        }
419        structured_ltv::material::DependencyError::Provider { node, cause } => {
420            Error::Provider { node, cause }
421        }
422        structured_ltv::material::DependencyError::CallbackProtocol { node } => {
423            Error::CallbackProtocol { node }
424        }
425    })?;
426    Ok(Entry {
427        source: credential.source(),
428        completeness: result.completeness,
429        findings: result.findings,
430    })
431}