Skip to main content

structured_ltv/material/
status.rs

1//! Original status artifacts derived from common authenticated evidence.
2
3use super::{ArchiveError, Archived, ArtifactRef, Coverage, Index, IndexError, PathCertificates};
4use crate::pki::{AuthorityClosure, CrlReading, MemberStatus, PathStatuses};
5use crate::source::{Location, Query, ScopeError};
6use crate::timestamp::Verified;
7use structured_asn1::memory::{HashWork, MemoryError, OwnedVec, Work};
8use structured_pkix::{crl, ocsp};
9
10/// Archive-material census for every path actually reached through the selected
11/// actor/status-authority inputs. This proves artifact availability only; it
12/// preserves every original status/time/authority fact without accepting it.
13pub struct AuthorityMaterial<'closure, 'view, 'certificate, 'document, N, D, G, P> {
14    inputs: &'closure AuthorityClosure<'view, 'certificate, 'document>,
15    paths: OwnedVec<PathMaterial<'view, 'certificate, 'document, N, D, G, P>>,
16    all_covered: bool,
17}
18
19impl<'closure, 'view, 'certificate, 'document, N, D, G, P>
20    AuthorityMaterial<'closure, 'view, 'certificate, 'document, N, D, G, P>
21{
22    /// Complete original root/authority input closure, not a selected subset.
23    #[must_use]
24    pub fn inputs(&self) -> &'closure AuthorityClosure<'view, 'certificate, 'document> {
25        self.inputs
26    }
27    /// Material for every reached path, preserving the same closure order.
28    #[must_use]
29    pub fn paths(&self) -> &[PathMaterial<'view, 'certificate, 'document, N, D, G, P>] {
30        &self.paths
31    }
32    /// Whether all actually required original artifacts precede this archive.
33    /// Status acceptance, all actor roots and historical trust remain separate.
34    #[must_use]
35    pub fn all_artifacts_covered(&self) -> bool {
36        self.all_covered
37    }
38}
39
40/// Census every path/status/authority artifact actually required by this input
41/// closure under the SAME original archive input. Reuses `archived_path`, its
42/// source index and complete/delta/format-container rules. No provider is repeated and
43/// an allocation/work/source refusal drops every partial material report.
44pub fn archived_authorities<'closure, 'view, 'certificate, 'document, N, D, G, P, O, W, Q, C, I>(
45    inputs: &'closure AuthorityClosure<'view, 'certificate, 'document>,
46    index: &Index<'_, N, D, G, P, O, W, I>,
47    coverage: &Coverage<N, D, G, C, I>,
48    evidence: &Verified<N, D, G, I>,
49    query: &Q,
50) -> Result<AuthorityMaterial<'closure, 'view, 'certificate, 'document, N, D, G, P>, ArchiveError<N>>
51where
52    N: Copy + Eq + HashWork,
53    D: Copy + Eq,
54    G: Copy + Eq,
55    P: Copy,
56    W: Work,
57    C: Work,
58    Q: Query<Id = N, Document = D, Generation = G>,
59{
60    let signature = evidence.signature();
61    let resource = |cause| ArchiveError::Index(IndexError::Resource { signature, cause });
62    let scope = |cause| ArchiveError::Index(IndexError::Scope(cause));
63    query.admit(signature, 1).map_err(scope)?;
64    let mut paths = query.execution().vector();
65    paths
66        .try_reserve_exact(inputs.nodes().len())
67        .map_err(resource)?;
68    let mut all_covered = true;
69    for node in inputs.nodes() {
70        query.admit(signature, 2).map_err(scope)?;
71        let material = archived_path(node.statuses, index, coverage, evidence, query)?;
72        if !material.all_artifacts_covered() {
73            all_covered = false;
74        }
75        paths.try_push(material).map_err(resource)?;
76    }
77    Ok(AuthorityMaterial {
78        inputs,
79        paths: paths.into_owned(),
80        all_covered,
81    })
82}
83
84/// Complete certificate/status artifact census for the SAME actual path and
85/// original evaluation inputs. This is one actor's contribution to an archive;
86/// other actors, status acceptance and historical trust remain separate.
87pub struct PathMaterial<'view, 'certificate, 'document, N, D, G, P> {
88    inputs: &'view PathStatuses<'view, 'certificate, 'document>,
89    certificates: PathCertificates<'view, 'document, N, D, G, P>,
90    statuses: OwnedVec<StatusMaterial<'view, 'certificate, 'document, N, D, G, P>>,
91    all_covered: bool,
92}
93
94impl<'view, 'certificate, 'document, N, D, G, P>
95    PathMaterial<'view, 'certificate, 'document, N, D, G, P>
96{
97    /// Original complete per-member input census; no caller-written actor count.
98    #[must_use]
99    pub fn inputs(&self) -> &'view PathStatuses<'view, 'certificate, 'document> {
100        self.inputs
101    }
102    /// Same actual checked-path certificate set, including its existing anchor.
103    #[must_use]
104    pub fn certificates(&self) -> &PathCertificates<'view, 'document, N, D, G, P> {
105        &self.certificates
106    }
107    /// Original contributing status sources/signers for members requiring a
108    /// status lookup. The original input census separately retains a validated
109    /// responder noCheck exemption; it never manufactures an OCSP Good result.
110    #[must_use]
111    pub fn statuses(&self) -> &[StatusMaterial<'view, 'certificate, 'document, N, D, G, P>] {
112        &self.statuses
113    }
114    /// Whether all these exact inputs have covered original copies. This is
115    /// neither a Good assertion nor completeness for counters/credentials/TSAs.
116    #[must_use]
117    pub fn all_artifacts_covered(&self) -> bool {
118        self.all_covered
119    }
120}
121
122/// Derive archive obligations from a complete original path-status census.
123/// Reuses the same exact-source index and archive coverage for certificates and
124/// all complete/delta/OCSP contributors. No signature/hash check is repeated,
125/// and refusal drops the accumulated reports rather than returning a subset.
126pub fn archived_path<'view, 'certificate, 'document, N, D, G, P, O, W, Q, C, I>(
127    inputs: &'view PathStatuses<'view, 'certificate, 'document>,
128    index: &Index<'_, N, D, G, P, O, W, I>,
129    coverage: &Coverage<N, D, G, C, I>,
130    evidence: &Verified<N, D, G, I>,
131    query: &Q,
132) -> Result<PathMaterial<'view, 'certificate, 'document, N, D, G, P>, ArchiveError<N>>
133where
134    N: Copy + Eq + HashWork,
135    D: Copy + Eq,
136    G: Copy + Eq,
137    P: Copy,
138    W: Work,
139    C: Work,
140    Q: Query<Id = N, Document = D, Generation = G>,
141{
142    let certificates = index.archived_path(inputs.path(), coverage, evidence, query)?;
143    let mut all_covered = certificates.all_certificates_covered();
144    let signature = evidence.signature();
145    let resource = |cause| ArchiveError::Index(IndexError::Resource { signature, cause });
146    let scope = |cause| ArchiveError::Index(IndexError::Scope(cause));
147    query.admit(signature, 1).map_err(scope)?;
148    let mut statuses = query.execution().vector();
149    let mut count = 0;
150    for input in inputs.entries() {
151        query.admit(signature, 2).map_err(scope)?;
152        if !matches!(input, MemberStatus::ResponderNoCheck(_)) {
153            count += 1;
154        }
155    }
156    statuses.try_reserve_exact(count).map_err(resource)?;
157    for input in inputs.entries() {
158        query.admit(signature, 2).map_err(scope)?;
159        let Some(source) = member_source(input) else {
160            continue;
161        };
162        let status = archived(source, index, coverage, evidence, query)?;
163        if !status.all_artifacts_covered() {
164            all_covered = false;
165        }
166        statuses.try_push(status).map_err(resource)?;
167    }
168    Ok(PathMaterial {
169        inputs,
170        certificates,
171        statuses: statuses.into_owned(),
172        all_covered,
173    })
174}
175
176/// Actual common status evidence, not a caller-written Good assertion.
177#[derive(Clone, Copy)]
178pub enum StatusSource<'view, 'certificate, 'document> {
179    /// Qualified reason union. Each contributing complete/delta source stays
180    /// visible; partial reasons alone still do not establish all-reason absence.
181    Crl(&'view crl::Coverage<'certificate, 'document>),
182    /// Original complete/delta facts whose entry/window may be negative. This
183    /// retains actual sources without constructing an absence assertion.
184    CrlReading(CrlReading<'view, 'certificate, 'document>),
185    /// Common issuer/request/time result. Its original status may be Good,
186    /// Unknown or Revoked; incorporation does not change that status.
187    Ocsp(&'view ocsp::BoundEvidence<'certificate, 'document, 'certificate, 'document>),
188}
189
190/// The status source of one member's status input; `None` for a validated
191/// responder noCheck exemption, whose responder certificate stays in the path.
192/// One mapping serves archive material and every format's placement checks.
193#[must_use]
194pub fn member_source<'view, 'certificate, 'document>(
195    input: &'view MemberStatus<'view, 'certificate, 'document>,
196) -> Option<StatusSource<'view, 'certificate, 'document>> {
197    match input {
198        MemberStatus::Crl(bound) => Some(StatusSource::Crl(bound.coverage())),
199        MemberStatus::CrlReading(bound) => Some(StatusSource::CrlReading(bound.reading())),
200        MemberStatus::Ocsp(bound) => Some(StatusSource::Ocsp(bound.evidence())),
201        MemberStatus::ResponderNoCheck(_) => None,
202    }
203}
204
205impl<'certificate, 'document> StatusSource<'_, 'certificate, 'document> {
206    /// Actual original target processed by this same common status result.
207    #[must_use]
208    pub fn target(self) -> &'certificate structured_pkix::Certificate<'document> {
209        match self {
210            Self::Crl(qualified) => qualified.target(),
211            Self::CrlReading(reading) => reading.target(),
212            Self::Ocsp(qualified) => qualified.target(),
213        }
214    }
215    /// Exact label used by the original common status processing.
216    #[must_use]
217    pub fn evaluated_at(self) -> structured_pkix::temporal::Reading {
218        match self {
219            Self::Crl(qualified) => qualified.evaluated_at(),
220            Self::CrlReading(reading) => reading.evaluated_at(),
221            Self::Ocsp(qualified) => qualified.evaluated_at(),
222        }
223    }
224}
225
226/// Role of one original artifact in common status evidence.
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228pub enum StatusPart {
229    /// Authenticated whole complete CRL, also required for a delta source.
230    CompleteCrl,
231    /// Authenticated whole updating delta CRL; never a replacement for the base.
232    DeltaCrl,
233    /// Format-issued original OCSP container carrying the exact basic response.
234    /// RFC5940 requires its whole envelope; legacy `CAdES` Annex A.1.4 specifies
235    /// a bare `BasicOCSPResponse`. Neither permits an invented envelope.
236    OcspResponse,
237    /// Actual CRL/OCSP signer certificate from the same authentication receipt.
238    SigningCertificate,
239}
240
241/// One required source artifact; ordinals address the retained evidence below.
242#[derive(Debug)]
243pub struct ArchivedStatus<N, D, G, P> {
244    /// Contributing CRL reason-source ordinal, or zero for one OCSP response.
245    pub source: usize,
246    /// Exact artifact role, not inferred from how many values were carried.
247    pub part: StatusPart,
248    /// Mathematical availability under this particular archive input.
249    pub availability: Archived<N, D, G, P>,
250}
251
252/// Complete artifact census for exactly one borrowed common status result.
253/// Status usability, actor/path association and additional delegated-responder
254/// checking remain explicit; this is not an attained baseline-level receipt.
255pub struct StatusMaterial<'view, 'certificate, 'document, N, D, G, P> {
256    source: StatusSource<'view, 'certificate, 'document>,
257    archive: Location<N, D, G>,
258    entries: OwnedVec<ArchivedStatus<N, D, G, P>>,
259    all_covered: bool,
260}
261
262impl<'view, 'certificate, 'document, N: Copy, D: Copy, G: Copy, P>
263    StatusMaterial<'view, 'certificate, 'document, N, D, G, P>
264{
265    /// Same original common result; another target/status cannot replace it.
266    #[must_use]
267    pub fn source(&self) -> StatusSource<'view, 'certificate, 'document> {
268        self.source
269    }
270    /// Original archive token whose mathematical input was checked.
271    #[must_use]
272    pub fn archive(&self) -> Location<N, D, G> {
273        self.archive
274    }
275    /// Every original status source and actual signer, including missing copies.
276    #[must_use]
277    pub fn entries(&self) -> &[ArchivedStatus<N, D, G, P>] {
278        &self.entries
279    }
280    /// Whether all these exact artifacts have covered copies. This does not
281    /// mean every path member has status, nor that an OCSP status is Good.
282    #[must_use]
283    pub fn all_artifacts_covered(&self) -> bool {
284        self.all_covered
285    }
286}
287
288/// Checks original status artifacts without reconstructing a CRL or OCSP value.
289///
290/// EN319132-1 V1.3.1 §§5.4.3/5.5.2.2/6.3(aa) require the full set used for
291/// validation before each archive. For a delta this includes both whole lists;
292/// RFC5940 §3 requires the whole `OCSPResponse` envelope in that format;
293/// historical `CAdES` Annex A.1.4 carries the original bare basic response.
294/// The format-issued index retains that boundary. Actual source/key
295/// authentication, applicability and temporal facts come from the shared PKIX
296/// result, never from a count or a fresh private status parser here.
297/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
298/// <https://www.rfc-editor.org/rfc/rfc5940#section-3>
299pub fn archived<'view, 'certificate, 'document, N, D, G, P, O, W, Q, C, I>(
300    source: StatusSource<'view, 'certificate, 'document>,
301    index: &Index<'_, N, D, G, P, O, W, I>,
302    coverage: &Coverage<N, D, G, C, I>,
303    evidence: &Verified<N, D, G, I>,
304    query: &Q,
305) -> Result<StatusMaterial<'view, 'certificate, 'document, N, D, G, P>, ArchiveError<N>>
306where
307    N: Copy + Eq + HashWork,
308    D: Copy + Eq,
309    G: Copy + Eq,
310    P: Copy,
311    W: Work,
312    C: Work,
313    Q: Query<Id = N, Document = D, Generation = G>,
314{
315    let signature = evidence.signature();
316    query
317        .admit(signature, 1)
318        .map_err(|cause| ArchiveError::Index(IndexError::Scope(cause)))?;
319    let mut entries = query.execution().vector();
320    let mut all_covered = true;
321    let mut record = |artifact, ordinal, part| {
322        let availability = index.archived(artifact, coverage, evidence, query)?;
323        if !matches!(availability, Archived::Present(_)) {
324            all_covered = false;
325        }
326        entries
327            .try_push(ArchivedStatus {
328                source: ordinal,
329                part,
330                availability,
331            })
332            .map_err(|cause| ArchiveError::Index(IndexError::Resource { signature, cause }))
333    };
334    visit_artifacts(source, signature, query, &mut record).map_err(|cause| match cause {
335        ArtifactVisitError::Scope(cause) => ArchiveError::Index(IndexError::Scope(cause)),
336        ArtifactVisitError::Visitor(cause) => cause,
337    })?;
338    Ok(StatusMaterial {
339        source,
340        archive: evidence.token(),
341        entries: entries.into_owned(),
342        all_covered,
343    })
344}
345
346fn record_crl<'certificate, 'document, E>(
347    source: &crl::EvidenceSource<'certificate, 'document>,
348    ordinal: usize,
349    mut record: impl FnMut(ArtifactRef<'certificate, 'document>, usize, StatusPart) -> Result<(), E>,
350) -> Result<(), E> {
351    match source {
352        crl::EvidenceSource::Complete(complete) => {
353            record(
354                ArtifactRef::Crl(complete.crl()),
355                ordinal,
356                StatusPart::CompleteCrl,
357            )?;
358            record(
359                ArtifactRef::Certificate(complete.issuer()),
360                ordinal,
361                StatusPart::SigningCertificate,
362            )?;
363        }
364        crl::EvidenceSource::Delta { base, delta } => {
365            record(
366                ArtifactRef::Crl(base.crl()),
367                ordinal,
368                StatusPart::CompleteCrl,
369            )?;
370            record(ArtifactRef::Crl(delta.crl()), ordinal, StatusPart::DeltaCrl)?;
371            record(
372                ArtifactRef::Certificate(base.issuer()),
373                ordinal,
374                StatusPart::SigningCertificate,
375            )?;
376            record(
377                ArtifactRef::Certificate(delta.issuer()),
378                ordinal,
379                StatusPart::SigningCertificate,
380            )?;
381        }
382    }
383    Ok(())
384}
385
386/// A source refusal while enumerating status artifacts, or the visitor's own.
387#[derive(Debug)]
388pub enum ArtifactVisitError<V, N> {
389    /// Source generation or current operation refused.
390    Scope(ScopeError<N>),
391    /// The visitor's unchanged cause.
392    Visitor(V),
393}
394
395/// Enumerates every original complete/delta/OCSP source and actual
396/// authentication signer of one status result, each step admitted under the
397/// current query. Archive availability and every format's placement check
398/// share this one walk; a visitor failure keeps its cause.
399pub fn visit_artifacts<'certificate, 'document, Q: Query, V>(
400    source: StatusSource<'_, 'certificate, 'document>,
401    signature: Q::Id,
402    query: &Q,
403    mut record: impl FnMut(ArtifactRef<'certificate, 'document>, usize, StatusPart) -> Result<(), V>,
404) -> Result<(), ArtifactVisitError<V, Q::Id>> {
405    let pay = || query.admit(signature, 2).map_err(ArtifactVisitError::Scope);
406    let record = |artifact, ordinal, part| {
407        record(artifact, ordinal, part).map_err(ArtifactVisitError::Visitor)
408    };
409    walk_artifacts(source, pay, record)
410}
411
412/// The same walk when the enclosing operation has already checked the source
413/// and needs only current admission.
414pub fn visit_artifacts_in<'c, 'd, W: Work>(
415    source: StatusSource<'_, 'c, 'd>,
416    work: &W,
417    record: impl FnMut(ArtifactRef<'c, 'd>, usize, StatusPart) -> Result<(), MemoryError>,
418) -> Result<(), MemoryError> {
419    walk_artifacts(source, || work.charge_work(1), record)
420}
421
422fn walk_artifacts<'c, 'd, E>(
423    source: StatusSource<'_, 'c, 'd>,
424    mut pay: impl FnMut() -> Result<(), E>,
425    mut record: impl FnMut(ArtifactRef<'c, 'd>, usize, StatusPart) -> Result<(), E>,
426) -> Result<(), E> {
427    match source {
428        StatusSource::Crl(qualified) => {
429            for (ordinal, entry) in qualified.sources().iter().enumerate() {
430                pay()?;
431                record_crl(&entry.source(), ordinal, &mut record)?;
432            }
433        }
434        StatusSource::CrlReading(reading) => {
435            pay()?;
436            record_crl(&reading.source(), 0, &mut record)?;
437        }
438        StatusSource::Ocsp(qualified) => {
439            pay()?;
440            let signed = qualified.evidence().source().signed();
441            record(
442                ArtifactRef::Ocsp(signed.response()),
443                0,
444                StatusPart::OcspResponse,
445            )?;
446            record(
447                ArtifactRef::Certificate(signed.signer()),
448                0,
449                StatusPart::SigningCertificate,
450            )?;
451        }
452    }
453    Ok(())
454}