Skip to main content

structured_cades/evidence/
closure.rs

1//! Complete source-bound preservation, distinct from modern baseline promotion.
2use super::record::Checked;
3use crate::{
4    Error,
5    prepare::Preparation,
6    preservation::{Kind, Required},
7    source::Node,
8};
9use core::cmp::Ordering;
10use structured_asn1::{
11    Reader,
12    memory::{Execution, OwnedMap, OwnedVec, SortError, Work},
13};
14use structured_cms::{
15    evidence::verify::{self as ers, Decision, OrderCause},
16    timestamp::Evidence,
17};
18use structured_pkix::temporal::{Instant, LeapTable};
19const FIELD: &str = "cades.preservation.close";
20
21/// Full record history at its actual next authenticated proof, never `poeDate`.
22pub struct History<'view, 'prepared, 'm, 'd, 'path> {
23    /// All original successful TSA actors of this source-bound proof.
24    pub record: &'view Checked<'prepared, 'm, 'd, 'path>,
25    /// Earliest authenticated TSA of the next proof on this SAME signature.
26    /// Its complete input has already been checked; metadata is not evidence.
27    pub following: Option<&'view Evidence>,
28    /// Explicit current verification reference for the last proof.
29    pub verification_time: Instant,
30}
31/// Refusal cannot become a successful partial preservation set.
32#[derive(Debug)]
33pub enum ClosureError<E> {
34    /// Actual original source/query/work refusal.
35    Processing(Error),
36    /// Actual source node of a missing or incomplete proof.
37    Unproven {
38        /// Whole original proof attribute.
39        property: Node,
40    },
41    /// A receipt belongs to another preparation or was supplied twice.
42    Foreign {
43        /// Actual supplied/selecting property.
44        property: Node,
45    },
46    /// The original unsigned ordering dates cannot be ordered exactly.
47    Date(structured_pkix::temporal::CompareError),
48    /// Original proofs do not establish an acyclic preservation dependency.
49    Dependency {
50        /// First actual source proof.
51        first: Node,
52        /// Second actual source proof.
53        second: Node,
54    },
55    /// Earlier/later authenticated generations fail the normative order.
56    Chronology {
57        /// Earlier source proof.
58        earlier: Node,
59        /// Later source proof.
60        later: Node,
61        /// Actual uncertainty/reversal, never a boolean fallback.
62        cause: OrderCause,
63    },
64    /// Trusted historical validation completed adversely or remains unknown.
65    History {
66        /// Actual original proof.
67        property: Node,
68        /// Actual completed decision.
69        decision: Decision,
70    },
71    /// Original historical validator failed to execute.
72    Callback(E),
73}
74
75/// Every original ERS/PoE obligation, input, TSA and cross-proof history has
76/// been closed. Private construction prevents writable facts or a successful
77/// subset from bypassing the whole original census. This is not B-LTA status.
78pub struct Closure<'checks, 'prepared, 'm, 'd, 'path> {
79    prepared: &'prepared Preparation<'m, 'd>,
80    records: OwnedVec<&'checks Checked<'prepared, 'm, 'd, 'path>>,
81    index: OwnedMap<Node, usize>,
82}
83impl<'checks, 'prepared, 'm, 'd, 'path> Closure<'checks, 'prepared, 'm, 'd, 'path> {
84    /// Same immutable complete source, never a substituted parse of equal bytes.
85    #[must_use]
86    pub fn prepared(&self) -> &'prepared Preparation<'m, 'd> {
87        self.prepared
88    }
89    /// Exact original complete receipts in normative per-signature order.
90    #[must_use]
91    pub fn records(&self) -> &[&'checks Checked<'prepared, 'm, 'd, 'path>] {
92        &self.records
93    }
94    /// Paid lookup for baseline/participant composition. No parsing or status
95    /// callback repeats; this immutable closure already retains those receipts.
96    pub fn contains<W: Work>(
97        &self,
98        required: &Required<'_>,
99        work: &W,
100    ) -> Result<bool, structured_asn1::memory::MemoryError> {
101        let Some(index) = self.index.get(&required.property, work)? else {
102            return Ok(false);
103        };
104        work.charge_work(3)?;
105        let actual = self.records[*index].required;
106        Ok(actual.signature == required.signature
107            && actual.kind == required.kind
108            && actual.placement == required.placement)
109    }
110}
111
112/// Close a whole preparation. Before any historical callback, this admits the
113/// ENTIRE original census, unique source identities, exact metadata order and
114/// actual timestamp chronology. Historical checks then run latest-to-earliest
115/// with the actual following proof, as TS101733 Annex B.3A.1 requires.
116pub fn close_preservation<'checks, 'prepared, 'm, 'd, 'path, E>(
117    prepared: &'prepared Preparation<'m, 'd>,
118    records: &[&'checks Checked<'prepared, 'm, 'd, 'path>],
119    operation: &Reader<'d>,
120    utc: &LeapTable<'_>,
121    reference: Instant,
122    mut historical: impl FnMut(
123        History<'_, 'prepared, 'm, 'd, 'path>,
124        &Execution<'_>,
125    ) -> Result<Decision, E>,
126) -> Result<Closure<'checks, 'prepared, 'm, 'd, 'path>, ClosureError<E>> {
127    prepared
128        .document()
129        .message()
130        .require_session(operation, FIELD)
131        .map_err(|cause| ClosureError::Processing(Error::Cms(cause)))?;
132    let work = operation.execution();
133    let resource = |cause| {
134        ClosureError::Processing(Error::resource(
135            cause,
136            FIELD,
137            prepared.signer().span().start,
138        ))
139    };
140    let mut selected = work.vector();
141    let mut index = work.map();
142    selected
143        .try_reserve_exact(records.len())
144        .map_err(resource)?;
145    for record in records {
146        work.charge_work(1).map_err(resource)?;
147        let property = record.required.property;
148        if !core::ptr::eq(record.prepared, prepared)
149            || !record.required.kind.is_evidence()
150            || index
151                .try_insert(property, selected.len())
152                .map_err(resource)?
153                .is_some()
154        {
155            return Err(ClosureError::Foreign { property });
156        }
157        if record.verified().is_none() {
158            return Err(ClosureError::Unproven { property });
159        }
160        selected.try_push(*record).map_err(resource)?;
161    }
162    for required in prepared.preservation() {
163        work.charge_work(1).map_err(resource)?;
164        if !required.kind.is_evidence() {
165            continue;
166        }
167        if index
168            .get(&required.property, &work)
169            .map_err(resource)?
170            .is_none()
171        {
172            return Err(ClosureError::Unproven {
173                property: required.property,
174            });
175        }
176        if required.placement != structured_cms::attribute::Placement::Unsigned {
177            return Err(ClosureError::Unproven {
178                property: required.property,
179            });
180        }
181    }
182    // No unrelated readable proof or malformed source may disappear while
183    // closing a chosen successful subset.
184    if !prepared.evidence_faults().is_empty() {
185        return Err(ClosureError::Unproven {
186            property: prepared.evidence_faults()[0].0,
187        });
188    }
189    order_selected(prepared, &mut selected, &index, utc, &work)?;
190    check_chronology(&selected, utc, &work)?;
191    check_history(&selected, reference, &mut historical, &work)?;
192    // Freeze the sorted identity mapping AFTER all comparisons/callbacks.
193    let mut sorted = work.map();
194    for (ordinal, record) in selected.iter().enumerate() {
195        sorted
196            .try_insert(record.required.property, ordinal)
197            .map_err(resource)?;
198    }
199    Ok(Closure {
200        prepared,
201        records: selected.into_owned(),
202        index: sorted.into_owned(),
203    })
204}
205
206fn order_selected<E, W: Work>(
207    prepared: &Preparation<'_, '_>,
208    selected: &mut OwnedVec<&Checked<'_, '_, '_, '_>, W>,
209    index: &OwnedMap<Node, usize, W>,
210    utc: &LeapTable<'_>,
211    work: &W,
212) -> Result<(), ClosureError<E>> {
213    let resource = |cause| {
214        ClosureError::Processing(Error::resource(
215            cause,
216            FIELD,
217            prepared.signer().span().start,
218        ))
219    };
220    // Coverage, not a kind priority or claimed date, determines which proof
221    // came later. This also handles a qualified AnnexB ERS appended after PoE.
222    // Rank a fully related per-signature dependency graph before sorting, so
223    // an attacker-controlled cycle never reaches a non-total sort comparator.
224    let mut ranks = work.vector();
225    ranks.try_reserve_exact(selected.len()).map_err(resource)?;
226    for _ in selected.iter() {
227        ranks.try_push(0usize).map_err(resource)?;
228    }
229    for left in 0..selected.len() {
230        for right in left + 1..selected.len() {
231            work.charge_work(3).map_err(resource)?;
232            if selected[left].required.signature != selected[right].required.signature {
233                continue;
234            }
235            let order = precedes(prepared, selected[left], selected[right], utc, work)?;
236            let later = if order == Ordering::Less { right } else { left };
237            debug_assert!(ranks[later] < selected.len());
238            ranks[later] += 1;
239        }
240    }
241    selected
242        .try_sort_unstable_by(|a, b| {
243            work.charge_work(3).map_err(resource)?;
244            let owner = a
245                .required
246                .signature
247                .span()
248                .start
249                .cmp(&b.required.signature.span().start);
250            if owner != Ordering::Equal {
251                return Ok(owner);
252            }
253            let left = *index
254                .get(&a.required.property, work)
255                .map_err(resource)?
256                .ok_or(ClosureError::Foreign {
257                    property: a.required.property,
258                })?;
259            let right = *index
260                .get(&b.required.property, work)
261                .map_err(resource)?
262                .ok_or(ClosureError::Foreign {
263                    property: b.required.property,
264                })?;
265            Ok::<_, ClosureError<E>>(ranks[left].cmp(&ranks[right]).then_with(|| {
266                a.required
267                    .property
268                    .span()
269                    .start
270                    .cmp(&b.required.property.span().start)
271            }))
272        })
273        .map_err(|cause| match cause {
274            SortError::Resource(cause) => resource(cause),
275            SortError::Comparison(cause) => cause,
276        })?;
277    for left in 0..selected.len() {
278        for right in left + 1..selected.len() {
279            work.charge_work(3).map_err(resource)?;
280            if selected[left].required.signature != selected[right].required.signature {
281                continue;
282            }
283            if precedes(prepared, selected[left], selected[right], utc, work)? != Ordering::Less {
284                return Err(ClosureError::Dependency {
285                    first: selected[left].required.property,
286                    second: selected[right].required.property,
287                });
288            }
289        }
290    }
291    Ok(())
292}
293
294fn check_chronology<E, W: Work>(
295    selected: &[&Checked<'_, '_, '_, '_>],
296    utc: &LeapTable<'_>,
297    work: &W,
298) -> Result<(), ClosureError<E>> {
299    for pair in selected.windows(2) {
300        let resource = |cause| {
301            ClosureError::Processing(Error::resource(
302                cause,
303                FIELD,
304                pair[0].required.property.span().start,
305            ))
306        };
307        work.charge_work(1).map_err(resource)?;
308        if pair[0].required.signature != pair[1].required.signature {
309            continue;
310        }
311        let a = pair[0]
312            .checks()
313            .last()
314            .and_then(|check| check.actor())
315            .ok_or(ClosureError::Unproven {
316                property: pair[0].required.property,
317            })?;
318        let b = pair[1]
319            .checks()
320            .next()
321            .and_then(|check| check.actor())
322            .ok_or(ClosureError::Unproven {
323                property: pair[1].required.property,
324            })?;
325        if let Some(cause) = ers::order_evidence(
326            a.verified().evidence(),
327            a.certificate(),
328            b.verified().evidence(),
329            b.certificate(),
330            utc,
331            work,
332        )
333        .map_err(|cause| ClosureError::Processing(Error::Cms(cause)))?
334        {
335            return Err(ClosureError::Chronology {
336                earlier: pair[0].required.property,
337                later: pair[1].required.property,
338                cause,
339            });
340        }
341    }
342    Ok(())
343}
344
345fn check_history<'checks, 'prepared, 'm, 'd, 'path, E, W: Work>(
346    selected: &[&'checks Checked<'prepared, 'm, 'd, 'path>],
347    reference: Instant,
348    historical: &mut impl FnMut(History<'_, 'prepared, 'm, 'd, 'path>, &W) -> Result<Decision, E>,
349    work: &W,
350) -> Result<(), ClosureError<E>> {
351    for ordinal in (0..selected.len()).rev() {
352        let resource = |cause| {
353            ClosureError::Processing(Error::resource(
354                cause,
355                FIELD,
356                selected[ordinal].required.property.span().start,
357            ))
358        };
359        work.charge_work(1).map_err(resource)?;
360        let record = selected[ordinal];
361        let following = if let Some(next) = selected.get(ordinal + 1)
362            && next.required.signature == record.required.signature
363        {
364            Some(
365                next.checks()
366                    .next()
367                    .and_then(|check| check.actor())
368                    .ok_or(ClosureError::Unproven {
369                        property: next.required.property,
370                    })?
371                    .verified()
372                    .evidence(),
373            )
374        } else {
375            None
376        };
377        let decision = historical(
378            History {
379                record,
380                following,
381                verification_time: reference,
382            },
383            work,
384        )
385        .map_err(ClosureError::Callback)?;
386        if decision != Decision::Valid {
387            return Err(ClosureError::History {
388                property: record.required.property,
389                decision,
390            });
391        }
392    }
393    Ok(())
394}
395
396fn precedes<E, W: Work>(
397    prepared: &Preparation<'_, '_>,
398    a: &Checked<'_, '_, '_, '_>,
399    b: &Checked<'_, '_, '_, '_>,
400    utc: &LeapTable<'_>,
401    work: &W,
402) -> Result<Ordering, ClosureError<E>> {
403    let resource = |cause| {
404        ClosureError::Processing(Error::resource(
405            cause,
406            FIELD,
407            a.required.property.span().start,
408        ))
409    };
410    let ac = a.covered.as_ref().ok_or(ClosureError::Unproven {
411        property: a.required.property,
412    })?;
413    let bc = b.covered.as_ref().ok_or(ClosureError::Unproven {
414        property: b.required.property,
415    })?;
416    let ab = super::coverage::selects(ac, b.required.property.span(), work).map_err(resource)?;
417    let ba = super::coverage::selects(bc, a.required.property.span(), work).map_err(resource)?;
418    let order = match (ab, ba) {
419        (false, true) => Ordering::Less,
420        (true, false) => Ordering::Greater,
421        _ => {
422            return Err(ClosureError::Dependency {
423                first: a.required.property,
424                second: b.required.property,
425            });
426        }
427    };
428    if a.required.kind == Kind::LongTermValidation && b.required.kind == Kind::LongTermValidation {
429        work.charge_work(2).map_err(resource)?;
430        let ae = prepared
431            .parsed
432            .proofs
433            .entries
434            .get(a.proof)
435            .ok_or(ClosureError::Foreign {
436                property: a.required.property,
437            })?;
438        let be = prepared
439            .parsed
440            .proofs
441            .entries
442            .get(b.proof)
443            .ok_or(ClosureError::Foreign {
444                property: b.required.property,
445            })?;
446        let (crate::prepare::proofs::Body::Poe(a), crate::prepare::proofs::Body::Poe(b)) =
447            (&ae.body, &be.body)
448        else {
449            return Err(ClosureError::Dependency {
450                first: ae.required.property,
451                second: be.required.property,
452            });
453        };
454        if a.compare_date(b, utc, work).map_err(ClosureError::Date)? != order {
455            return Err(ClosureError::Dependency {
456                first: ae.required.property,
457                second: be.required.property,
458            });
459        }
460    }
461    Ok(order)
462}