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