Skip to main content

structured_ltv/baseline/
archive.rs

1//! Join every participant's material under one original archive's coverage.
2
3use super::{ArchiveCensus, CredentialMaterial};
4use crate::material::ParticipantMaterial;
5use crate::pki::ParticipantError;
6use crate::source::{Query, ScopeError};
7use crate::timestamp::TimestampScope;
8use structured_asn1::memory::{MemoryError, Work};
9
10/// Original X.509 and credential-format material checked BEFORE the SAME
11/// archive. Construction requires the same authenticated credential census,
12/// not equivalent node IDs or separately selected authority contexts.
13///
14/// This proves only the combined incorporation/coverage predicates. Each
15/// archive's own TSA path/status, chronology, renewal, longevity and caller
16/// trust remain separate facts; this is not an unconditional B-LTA verdict.
17pub struct ArchiveMaterial<'a, M, C> {
18    x509: &'a M,
19    credentials: &'a C,
20    covered: bool,
21}
22
23impl<M, C> ArchiveMaterial<'_, M, C> {
24    /// Complete original actor/status-authority material and all coverage gaps.
25    #[must_use]
26    pub fn x509(&self) -> &M {
27        self.x509
28    }
29    /// Every actual credential's original format-material result and gaps.
30    #[must_use]
31    pub fn credentials(&self) -> &C {
32        self.credentials
33    }
34    /// All required material has an original copy inside THIS archive input.
35    /// A later copy or an unsupported format cannot satisfy this predicate.
36    #[must_use]
37    pub fn all_material_covered(&self) -> bool {
38        self.covered
39    }
40}
41
42type X509<'a, 'r, 'v, 'c, 'd, P, Q, I> = ParticipantMaterial<
43    'a,
44    'r,
45    'v,
46    'c,
47    'd,
48    <Q as Query>::Id,
49    <Q as Query>::Document,
50    <Q as Query>::Generation,
51    P,
52    I,
53>;
54
55/// Source, scope or input-receipt refusal, not a missing-material finding.
56pub type ArchiveMaterialResult<'a, M, C, N> =
57    Result<ArchiveMaterial<'a, M, C>, ParticipantError<N>>;
58
59/// Combine both original material reports for one exact preceding-archive
60/// scope. The X.509 report already establishes the original coverage and
61/// mathematical token binding; the format report must use that SAME scope and
62/// actual authenticated credential input. Neither source work nor provider
63/// interpretation is replayed; the fresh metadata query is still admitted.
64///
65/// EN319132-1 V1.3.1 ยง6.3(aa) requires all validation data before generation of
66/// each archive; final-document availability cannot repair an earlier input.
67/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
68pub fn archive_material<'s, 'a, 'r, 'v, 'c, 'd, P, C, Q, I>(
69    x509: &'s X509<'a, 'r, 'v, 'c, 'd, P, Q, I>,
70    credentials: &'s C,
71    query: &Q,
72) -> ArchiveMaterialResult<'s, X509<'a, 'r, 'v, 'c, 'd, P, Q, I>, C, Q::Id>
73where
74    C: CredentialMaterial<Id = Q::Id, Document = Q::Document, Generation = Q::Generation>,
75    Q: Query,
76{
77    let required = x509.inputs().requirements();
78    let scope = required.scope();
79    let original = scope.location();
80    let supplied = credentials.scope();
81    let foreign = || ParticipantError::Foreign {
82        signature: required.signature(),
83    };
84    if !matches!(scope, TimestampScope::BeforeArchive(_))
85        || !matches!(supplied, TimestampScope::BeforeArchive(_))
86        || original.snapshot != query.snapshot()
87        || supplied.location().snapshot != original.snapshot
88        || credentials.snapshot() != original.snapshot
89    {
90        return Err(foreign());
91    }
92    // One dispatch plus the paid identity metadata pass.
93    query.admit(original.node, 2)?;
94    if !required.same_credentials(credentials.credentials())
95        || !query
96            .equal(original.node, supplied.location().node)
97            .map_err(|cause| ParticipantError::Resource {
98                node: original.node,
99                cause,
100            })?
101    {
102        return Err(foreign());
103    }
104    Ok(ArchiveMaterial {
105        x509,
106        credentials,
107        covered: x509.all_x509_artifacts_covered() && credentials.all_requirements_met(),
108    })
109}
110
111/// Every original root archive's paired material facts in source order. The
112/// original timestamp census determines the set; a supplied successful subset
113/// cannot create it. Historical authority/path contexts remain on each report,
114/// since they need not be identical at different original archive times.
115pub struct Archives<'a, P, R> {
116    census: &'a P,
117    reports: &'a [&'a R],
118    covered: bool,
119}
120
121impl<P, R> Archives<'_, P, R> {
122    /// Same original timestamp census.
123    #[must_use]
124    pub fn census(&self) -> &P {
125        self.census
126    }
127    /// Every actual root archive once, preserving all its detailed gaps.
128    #[must_use]
129    pub fn reports(&self) -> &[&R] {
130        self.reports
131    }
132    /// At least one root archive exists and all its required material is
133    /// covered. This alone asserts neither complete B-LTA nor historical trust.
134    #[must_use]
135    pub fn all_archive_material_covered(&self) -> bool {
136        self.covered
137    }
138}
139
140/// Original processing/admission failure, never a failed document digest.
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum ArchiveCensusError<N> {
143    /// The supplied census/report belongs to a different source or root.
144    Foreign {
145        /// Original expected signature/archive.
146        node: N,
147    },
148    /// An actual original archive has no corresponding complete material report.
149    Missing {
150        /// Original unassessed archive property.
151        property: N,
152    },
153    /// Extra/duplicate supplied reports do not belong to this exact census.
154    Extra,
155    /// Original source/current query failure.
156    Scope(ScopeError<N>),
157    /// Admission refused before metadata traversal or identity comparison.
158    Resource {
159        /// Original selecting signature/archive.
160        node: N,
161        /// Actual work/storage refusal.
162        cause: MemoryError,
163    },
164}
165
166/// Complete original root-archive material census or a typed processing refusal.
167pub type ArchivesResult<'a, P, R, N> = Result<Archives<'a, P, R>, ArchiveCensusError<N>>;
168
169type Proof<'a, 'cl, 'r, 'v, 'c, 'd, P, C, Q, I> =
170    ArchiveMaterial<'a, X509<'cl, 'r, 'v, 'c, 'd, P, Q, I>, C>;
171
172/// Require one paired report for EACH original root archive, not only archives
173/// whose material happened to succeed. Counter timestamps retain their separate
174/// whole-signature TSA obligations; this does not impose root baseline properties
175/// on a countersignature. No crypto, source acquisition, artifact lookup,
176/// allocation or report copy occurs in this current-query census.
177pub fn archives<'s, 'a, 'cl, 'r, 'v, 'c, 'd, A, P, C, Q, I>(
178    census: &'s A,
179    reports: &'s [&'s Proof<'a, 'cl, 'r, 'v, 'c, 'd, P, C, Q, I>],
180    query: &Q,
181) -> ArchivesResult<'s, A, Proof<'a, 'cl, 'r, 'v, 'c, 'd, P, C, Q, I>, Q::Id>
182where
183    A: ArchiveCensus<Id = Q::Id, Document = Q::Document, Generation = Q::Generation>,
184    Q: Query,
185{
186    let selected = census.signature();
187    let signature = selected.node;
188    if selected.snapshot != query.snapshot() {
189        return Err(ArchiveCensusError::Foreign { node: signature });
190    }
191    query
192        .admit(signature, 1)
193        .map_err(ArchiveCensusError::Scope)?;
194    let mut cursor = 0;
195    let mut covered = true;
196    for ordinal in 0..census.timestamps() {
197        query
198            .execution()
199            .charge_work(1)
200            .map_err(|cause| ArchiveCensusError::Resource {
201                node: signature,
202                cause,
203            })?;
204        let Some(node) = census.archive(ordinal) else {
205            continue;
206        };
207        let resource = |cause| ArchiveCensusError::Resource { node, cause };
208        let Some(report) = reports.get(cursor) else {
209            return Err(ArchiveCensusError::Missing { property: node });
210        };
211        let required = report.x509.inputs().requirements();
212        let location = required.scope().location();
213        if location.snapshot != query.snapshot()
214            || !query
215                .equal(required.signature(), signature)
216                .map_err(resource)?
217            || !query.equal(location.node, node).map_err(resource)?
218        {
219            return Err(ArchiveCensusError::Foreign { node });
220        }
221        if !report.covered {
222            covered = false;
223        }
224        cursor += 1;
225    }
226    if cursor != reports.len() {
227        return Err(ArchiveCensusError::Extra);
228    }
229    if cursor == 0 {
230        covered = false;
231    }
232    query
233        .admit(signature, 1)
234        .map_err(ArchiveCensusError::Scope)?;
235    Ok(Archives {
236        census,
237        reports,
238        covered,
239    })
240}