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/whole-OCSP 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    /// Whole successful OCSP envelope containing the exact basic response.
234    OcspResponse,
235    /// Actual CRL/OCSP signer certificate from the same authentication receipt.
236    SigningCertificate,
237}
238
239/// One required source artifact; ordinals address the retained evidence below.
240#[derive(Debug)]
241pub struct ArchivedStatus<N, D, G, P> {
242    /// Contributing CRL reason-source ordinal, or zero for one OCSP response.
243    pub source: usize,
244    /// Exact artifact role, not inferred from how many values were carried.
245    pub part: StatusPart,
246    /// Mathematical availability under this particular archive input.
247    pub availability: Archived<N, D, G, P>,
248}
249
250/// Complete artifact census for exactly one borrowed common status result.
251/// Status usability, actor/path association and additional delegated-responder
252/// checking remain explicit; this is not an attained baseline-level receipt.
253pub struct StatusMaterial<'view, 'certificate, 'document, N, D, G, P> {
254    source: StatusSource<'view, 'certificate, 'document>,
255    archive: Location<N, D, G>,
256    entries: OwnedVec<ArchivedStatus<N, D, G, P>>,
257    all_covered: bool,
258}
259
260impl<'view, 'certificate, 'document, N: Copy, D: Copy, G: Copy, P>
261    StatusMaterial<'view, 'certificate, 'document, N, D, G, P>
262{
263    /// Same original common result; another target/status cannot replace it.
264    #[must_use]
265    pub fn source(&self) -> StatusSource<'view, 'certificate, 'document> {
266        self.source
267    }
268    /// Original archive token whose mathematical input was checked.
269    #[must_use]
270    pub fn archive(&self) -> Location<N, D, G> {
271        self.archive
272    }
273    /// Every original status source and actual signer, including missing copies.
274    #[must_use]
275    pub fn entries(&self) -> &[ArchivedStatus<N, D, G, P>] {
276        &self.entries
277    }
278    /// Whether all these exact artifacts have covered copies. This does not
279    /// mean every path member has status, nor that an OCSP status is Good.
280    #[must_use]
281    pub fn all_artifacts_covered(&self) -> bool {
282        self.all_covered
283    }
284}
285
286/// Checks original status artifacts without reconstructing a CRL or OCSP value.
287///
288/// EN319132-1 V1.3.1 §§5.4.3/5.5.2.2/6.3(aa) require the full set used for
289/// validation before each archive. For a delta this includes both whole lists;
290/// RFC5940 §3 requires the whole `OCSPResponse` envelope. Actual source/key
291/// authentication, applicability and temporal facts come from the shared PKIX
292/// result, never from a count or a fresh private status parser here.
293/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
294/// <https://www.rfc-editor.org/rfc/rfc5940#section-3>
295pub fn archived<'view, 'certificate, 'document, N, D, G, P, O, W, Q, C, I>(
296    source: StatusSource<'view, 'certificate, 'document>,
297    index: &Index<'_, N, D, G, P, O, W, I>,
298    coverage: &Coverage<N, D, G, C, I>,
299    evidence: &Verified<N, D, G, I>,
300    query: &Q,
301) -> Result<StatusMaterial<'view, 'certificate, 'document, N, D, G, P>, ArchiveError<N>>
302where
303    N: Copy + Eq + HashWork,
304    D: Copy + Eq,
305    G: Copy + Eq,
306    P: Copy,
307    W: Work,
308    C: Work,
309    Q: Query<Id = N, Document = D, Generation = G>,
310{
311    let signature = evidence.signature();
312    query
313        .admit(signature, 1)
314        .map_err(|cause| ArchiveError::Index(IndexError::Scope(cause)))?;
315    let mut entries = query.execution().vector();
316    let mut all_covered = true;
317    let mut record = |artifact, ordinal, part| {
318        let availability = index.archived(artifact, coverage, evidence, query)?;
319        if !matches!(availability, Archived::Present(_)) {
320            all_covered = false;
321        }
322        entries
323            .try_push(ArchivedStatus {
324                source: ordinal,
325                part,
326                availability,
327            })
328            .map_err(|cause| ArchiveError::Index(IndexError::Resource { signature, cause }))
329    };
330    visit_artifacts(source, signature, query, &mut record).map_err(|cause| match cause {
331        ArtifactVisitError::Scope(cause) => ArchiveError::Index(IndexError::Scope(cause)),
332        ArtifactVisitError::Visitor(cause) => cause,
333    })?;
334    Ok(StatusMaterial {
335        source,
336        archive: evidence.token(),
337        entries: entries.into_owned(),
338        all_covered,
339    })
340}
341
342fn record_crl<'certificate, 'document, E>(
343    source: &crl::EvidenceSource<'certificate, 'document>,
344    ordinal: usize,
345    mut record: impl FnMut(ArtifactRef<'certificate, 'document>, usize, StatusPart) -> Result<(), E>,
346) -> Result<(), E> {
347    match source {
348        crl::EvidenceSource::Complete(complete) => {
349            record(
350                ArtifactRef::Crl(complete.crl()),
351                ordinal,
352                StatusPart::CompleteCrl,
353            )?;
354            record(
355                ArtifactRef::Certificate(complete.issuer()),
356                ordinal,
357                StatusPart::SigningCertificate,
358            )?;
359        }
360        crl::EvidenceSource::Delta { base, delta } => {
361            record(
362                ArtifactRef::Crl(base.crl()),
363                ordinal,
364                StatusPart::CompleteCrl,
365            )?;
366            record(ArtifactRef::Crl(delta.crl()), ordinal, StatusPart::DeltaCrl)?;
367            record(
368                ArtifactRef::Certificate(base.issuer()),
369                ordinal,
370                StatusPart::SigningCertificate,
371            )?;
372            record(
373                ArtifactRef::Certificate(delta.issuer()),
374                ordinal,
375                StatusPart::SigningCertificate,
376            )?;
377        }
378    }
379    Ok(())
380}
381
382/// A source refusal while enumerating status artifacts, or the visitor's own.
383#[derive(Debug)]
384pub enum ArtifactVisitError<V, N> {
385    /// Source generation or current operation refused.
386    Scope(ScopeError<N>),
387    /// The visitor's unchanged cause.
388    Visitor(V),
389}
390
391/// Enumerates every original complete/delta/OCSP source and actual
392/// authentication signer of one status result, each step admitted under the
393/// current query. Archive availability and every format's placement check
394/// share this one walk; a visitor failure keeps its cause.
395pub fn visit_artifacts<'certificate, 'document, Q: Query, V>(
396    source: StatusSource<'_, 'certificate, 'document>,
397    signature: Q::Id,
398    query: &Q,
399    mut record: impl FnMut(ArtifactRef<'certificate, 'document>, usize, StatusPart) -> Result<(), V>,
400) -> Result<(), ArtifactVisitError<V, Q::Id>> {
401    let pay = || query.admit(signature, 2).map_err(ArtifactVisitError::Scope);
402    let record = |artifact, ordinal, part| {
403        record(artifact, ordinal, part).map_err(ArtifactVisitError::Visitor)
404    };
405    walk_artifacts(source, pay, record)
406}
407
408/// The same walk when the enclosing operation has already checked the source
409/// and needs only current admission.
410pub fn visit_artifacts_in<'c, 'd, W: Work>(
411    source: StatusSource<'_, 'c, 'd>,
412    work: &W,
413    record: impl FnMut(ArtifactRef<'c, 'd>, usize, StatusPart) -> Result<(), MemoryError>,
414) -> Result<(), MemoryError> {
415    walk_artifacts(source, || work.charge_work(1), record)
416}
417
418fn walk_artifacts<'c, 'd, E>(
419    source: StatusSource<'_, 'c, 'd>,
420    mut pay: impl FnMut() -> Result<(), E>,
421    mut record: impl FnMut(ArtifactRef<'c, 'd>, usize, StatusPart) -> Result<(), E>,
422) -> Result<(), E> {
423    match source {
424        StatusSource::Crl(qualified) => {
425            for (ordinal, entry) in qualified.sources().iter().enumerate() {
426                pay()?;
427                record_crl(&entry.source(), ordinal, &mut record)?;
428            }
429        }
430        StatusSource::CrlReading(reading) => {
431            pay()?;
432            record_crl(&reading.source(), 0, &mut record)?;
433        }
434        StatusSource::Ocsp(qualified) => {
435            pay()?;
436            let signed = qualified.evidence().source().signed();
437            record(
438                ArtifactRef::Ocsp(signed.response()),
439                0,
440                StatusPart::OcspResponse,
441            )?;
442            record(
443                ArtifactRef::Certificate(signed.signer()),
444                0,
445                StatusPart::SigningCertificate,
446            )?;
447        }
448    }
449    Ok(())
450}