Skip to main content

structured_xades/validation/
ref_presence.rs

1//! Conditional Annex A incorporation from complete original digest/source facts.
2
3use super::{ReferenceDigests, ReferenceSearch};
4use crate::{
5    Xades,
6    certificate::Agreement,
7    pki::CounterPaths,
8    unsigned,
9    xml::{Access, AccessError, Xml},
10};
11use structured_asn1::memory::{MemoryError, OwnedVec, Work};
12use structured_ltv::timestamp::{TimestampRequirements, TimestampScope};
13
14/// Why a referenced value's required incorporation was not established.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum ReferencePresenceFailure {
17    /// Complete known-digest search found no equal incorporated original value.
18    Missing,
19    /// An OCSP identifier without a digest still needs authenticated identity
20    /// association; empty bytes are not successful digest evidence.
21    IdentifierOnly,
22    /// Open reference semantics require the owning format's interpretation.
23    OtherFormat,
24    /// A method/output was not available for a completed exact comparison.
25    Unavailable(Agreement),
26}
27
28/// One original reference row's conditional obligation and completed outcome.
29pub struct ReferencePresenceRow {
30    required: bool,
31    failure: Option<ReferencePresenceFailure>,
32}
33impl ReferencePresenceRow {
34    /// Whether original container/archive presence activates Annex A inclusion.
35    #[must_use]
36    pub fn required(&self) -> bool {
37        self.required
38    }
39    /// None only establishes this necessary presence condition, not trust,
40    /// successful identity hints, actual actor use or a baseline level.
41    #[must_use]
42    pub fn failure(&self) -> Option<ReferencePresenceFailure> {
43        self.failure
44    }
45}
46
47/// Conditional referenced-material facts bound to the same complete report.
48pub struct ReferencePresence<'report, 'references, 'source, N, D, G> {
49    digests: &'report ReferenceDigests<'references, 'source, N, D, G>,
50    rows: OwnedVec<ReferencePresenceRow>,
51    all_present: bool,
52}
53impl<'report, 'references, 'source, N, D, G>
54    ReferencePresence<'report, 'references, 'source, N, D, G>
55{
56    /// Original reference order and all exact source comparison outcomes.
57    #[must_use]
58    pub fn digests(&self) -> &'report ReferenceDigests<'references, 'source, N, D, G> {
59        self.digests
60    }
61    /// Exactly one result per original reference row, with no successful subset.
62    #[must_use]
63    pub fn rows(&self) -> &[ReferencePresenceRow] {
64        &self.rows
65    }
66    /// Only the activated inclusion requirements, not artifact validity or grade.
67    #[must_use]
68    pub fn all_required_present(&self) -> bool {
69        self.all_present
70    }
71}
72
73/// Invalid scope/census and processing refusal are not absent material.
74#[derive(Debug)]
75pub enum ReferencePresenceError<E, N> {
76    /// Current original source/generation refused, including the complete
77    /// timestamp/counter source verification.
78    Access(AccessError<E, N>),
79    /// Requirements, counter receipt or source pool does not cover the same
80    /// whole selected signature; a preceding-archive subset is insufficient.
81    Scope,
82    /// Current metadata/storage admission failed before traversal or growth.
83    Resource(MemoryError),
84}
85
86/// Conditional incorporation result in this original XML identity domain.
87pub type ReferencePresenceResult<'report, 'references, 'source, X> = Result<
88    ReferencePresence<
89        'report,
90        'references,
91        'source,
92        <X as Xml>::Id,
93        <X as Xml>::Document,
94        <X as Xml>::Generation,
95    >,
96    ReferencePresenceError<<X as Xml>::Error, <X as Xml>::Id>,
97>;
98
99/// Evaluate Annex A.1.1–A.1.4's conditional "present elsewhere" requirement
100/// from the SAME completed original digest search. Certificate/status values,
101/// qualifying nonempty `AnyValidationData` and `ArchiveTimeStamp` activate their
102/// respective obligations. Their URI and Number hints never replace presence.
103/// The exact token source census is checked against whole-scope requirements
104/// and the complete counter receipt before any output allocation. This prevents
105/// a root-only/failed-token subset from reporting another pool's values missing
106/// or accepting a fabricated empty source set. No parsing/hashing is repeated.
107/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
108///
109/// Identifier-only OCSP/open formats remain explicit unproven obligations.
110/// Completed inclusion is not authentication, complete/delta applicability,
111/// actor-role/reference completeness, freshness, trust or a baseline grade.
112///
113/// # Panics
114/// Only if a privately constructed digest report contains a row outside its
115/// own immutable comparison catalogue.
116pub fn reference_presence<'report, 'references, 'source, X: Xml, W: Work>(
117    digests: &'report ReferenceDigests<'references, 'source, X::Id, X::Document, X::Generation>,
118    required: &TimestampRequirements<X::Id, X::Document, X::Generation, Xades>,
119    counters: &CounterPaths<'_, '_, X::Id, X::Document, X::Generation>,
120    access: &Access<'_, X, W>,
121) -> ReferencePresenceResult<'report, 'references, 'source, X> {
122    require_sources(digests, required, counters, access)?;
123    let prepared = digests.prepared();
124    let mut certificates = false;
125    let mut revocations = false;
126    for entry in prepared.unsigned().entries() {
127        access
128            .execution()
129            .charge_work(1)
130            .map_err(ReferencePresenceError::Resource)?;
131        match entry.role {
132            unsigned::Role::Certificates | unsigned::Role::AttributeCertificates => {
133                certificates = true;
134            }
135            unsigned::Role::Revocation | unsigned::Role::AttributeRevocation => revocations = true,
136            unsigned::Role::Archive => {
137                certificates = true;
138                revocations = true;
139            }
140            _ => (),
141        }
142    }
143    for data in prepared.validation() {
144        access
145            .execution()
146            .charge_work(1)
147            .map_err(ReferencePresenceError::Resource)?;
148        if data.role() == super::Role::Any {
149            certificates |= data
150                .certificates()
151                .is_some_and(|values| !values.entries().is_empty());
152            revocations |= data
153                .revocations()
154                .is_some_and(|values| !values.entries().is_empty());
155        }
156    }
157    let mut rows = access.execution().vector();
158    rows.try_reserve_exact(digests.rows().len())
159        .map_err(ReferencePresenceError::Resource)?;
160    let mut all_present = true;
161    for (ordinal, row) in digests.rows().iter().enumerate() {
162        access
163            .execution()
164            .charge_work(1)
165            .map_err(ReferencePresenceError::Resource)?;
166        let property = &prepared.validation_references()[row.property()];
167        let required = match property.role() {
168            unsigned::Role::CertificateReferences
169            | unsigned::Role::AttributeCertificateReferences => certificates,
170            unsigned::Role::RevocationReferences
171            | unsigned::Role::AttributeRevocationReferences => revocations,
172            _ => unreachable!("privately constructed reference property role"),
173        };
174        let failure = if required {
175            presence_failure(
176                row.search(),
177                digests
178                    .comparisons(ordinal)
179                    .expect("original comparison row"),
180                access.execution(),
181            )
182            .map_err(ReferencePresenceError::Resource)?
183        } else {
184            None
185        };
186        if failure.is_some() {
187            all_present = false;
188        }
189        rows.try_push(ReferencePresenceRow { required, failure })
190            .map_err(ReferencePresenceError::Resource)?;
191    }
192    access
193        .query(required.signature(), |_, _, _| Ok(()))
194        .map_err(ReferencePresenceError::Access)?;
195    Ok(ReferencePresence {
196        digests,
197        rows: rows.into_owned(),
198        all_present,
199    })
200}
201
202fn presence_failure<W: Work>(
203    search: ReferenceSearch,
204    values: &[super::MaterialDigest],
205    work: &W,
206) -> Result<Option<ReferencePresenceFailure>, MemoryError> {
207    let mut unavailable = None;
208    match search {
209        ReferenceSearch::IdentifierOnly => {
210            return Ok(Some(ReferencePresenceFailure::IdentifierOnly));
211        }
212        ReferenceSearch::OtherFormat => return Ok(Some(ReferencePresenceFailure::OtherFormat)),
213        ReferenceSearch::Unavailable(cause) => {
214            return Ok(Some(ReferencePresenceFailure::Unavailable(cause)));
215        }
216        ReferenceSearch::Compared => (),
217    }
218    for value in values {
219        work.charge_work(1)?;
220        match value.agreement() {
221            Agreement::Matches => return Ok(None),
222            Agreement::Differs => (),
223            cause => {
224                if unavailable.is_none() {
225                    unavailable = Some(cause);
226                }
227            }
228        }
229    }
230    Ok(Some(unavailable.map_or(
231        ReferencePresenceFailure::Missing,
232        ReferencePresenceFailure::Unavailable,
233    )))
234}
235
236fn require_sources<X: Xml, W: Work>(
237    digests: &ReferenceDigests<'_, '_, X::Id, X::Document, X::Generation>,
238    required: &TimestampRequirements<X::Id, X::Document, X::Generation, Xades>,
239    counters: &CounterPaths<'_, '_, X::Id, X::Document, X::Generation>,
240    access: &Access<'_, X, W>,
241) -> Result<(), ReferencePresenceError<X::Error, X::Id>> {
242    let original = digests.prepared().bound().containers();
243    let TimestampScope::All(scope) = required.scope() else {
244        return Err(ReferencePresenceError::Scope);
245    };
246    if original.snapshot() != access.snapshot()
247        || scope.snapshot != original.snapshot()
248        || counters.snapshot() != original.snapshot()
249    {
250        return Err(ReferencePresenceError::Scope);
251    }
252    access
253        .query(original.signature(), |_, _, _| Ok(()))
254        .map_err(ReferencePresenceError::Access)?;
255    if !access
256        .equal_id(scope.node, original.signature())
257        .map_err(ReferencePresenceError::Resource)?
258        || !access
259            .equal_id(required.signature(), original.signature())
260            .map_err(ReferencePresenceError::Resource)?
261        || !access
262            .equal_id(counters.signature(), original.signature())
263            .map_err(ReferencePresenceError::Resource)?
264        || !required
265            .includes_counters(counters, access)
266            .map_err(|cause| ReferencePresenceError::Access(cause.into()))?
267    {
268        return Err(ReferencePresenceError::Scope);
269    }
270    let Some(sources) = digests.tokens() else {
271        if required.entries().is_empty() {
272            return Ok(());
273        }
274        return Err(ReferencePresenceError::Scope);
275    };
276    if sources.len() != required.entries().len() {
277        return Err(ReferencePresenceError::Scope);
278    }
279    for (source, entry) in sources.iter().zip(required.entries()) {
280        access
281            .execution()
282            .charge_work(1)
283            .map_err(ReferencePresenceError::Resource)?;
284        if source.location().snapshot != original.snapshot()
285            || source.role().purpose() != entry.purpose
286            || !access
287                .equal_id(source.signature(), entry.signature)
288                .map_err(ReferencePresenceError::Resource)?
289            || !access
290                .equal_id(source.property(), entry.property)
291                .map_err(ReferencePresenceError::Resource)?
292            || !access
293                .equal_id(source.location().node, entry.token)
294                .map_err(ReferencePresenceError::Resource)?
295        {
296            return Err(ReferencePresenceError::Scope);
297        }
298    }
299    Ok(())
300}