Skip to main content

structured_xades/renewed/
check.rs

1//! EN319132-1 §5.5.3 processing over original authenticated Manifest inputs.
2
3use super::{Catalogue, Pair};
4use crate::{
5    binary,
6    certificate::{self, Agreement, DigestProvider},
7    container::DS,
8    digest, properties,
9    reference::{
10        Completeness, ObjectLocation, Parent, ReferenceXml, Replay, ReplayOutcome, VisitError,
11    },
12    timestamp::TimestampXml,
13    xml::{Access, AccessError, Name, Snapshot, Xml},
14};
15use core::cmp::Ordering;
16use structured_asn1::digest::{Digest, Resolution};
17use structured_asn1::memory::{MemoryError, OwnedVec, Work};
18use structured_cms::stream::{Receiver, Source};
19
20/// Completed processing state, independent of archive coverage or policy trust.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum State {
23    /// Every pair was examined; inspect each outcome before accepting renewal.
24    Processed,
25    /// No authenticated Manifest instruction references a detached resource.
26    NoDetachedManifest,
27    /// Actual resource provenance cannot establish the required detached case.
28    UnclassifiedManifest,
29    /// The selected new digest method/capability could not be used.
30    Algorithm(Agreement),
31}
32
33/// Per-pair result; an unavailable explicit source is not a terminal refusal.
34#[derive(Debug)]
35pub enum PairOutcome<E> {
36    /// Actual new digest agrees with the original declared value.
37    Matches,
38    /// Actual new digest differs from the declared value.
39    Differs,
40    /// No canonical original instruction has the declared reference digest.
41    MissingReference,
42    /// More than one original instruction has the declared reference digest.
43    AmbiguousReference,
44    /// The matching instruction does not name a proven detached resource.
45    NotDetached,
46    /// Old/new resolved algorithm identities are equal, contrary to §5.5.3.
47    SameAlgorithm,
48    /// Original/new method syntax, capability or declared width is unusable.
49    Algorithm(Agreement),
50    /// Explicit detached bytes are absent; subsequent pairs were still examined.
51    Unavailable(E),
52}
53
54/// Original pair, uniquely selected reference when known, and completed result.
55#[derive(Debug)]
56pub struct PairCheck<N, E> {
57    /// Original property/pair digest nodes.
58    pub pair: Pair<N>,
59    /// Unique original Manifest Reference, never reconstructed from a URI.
60    pub reference: Option<N>,
61    /// Historical old-target digest outcome, separate from authenticated parent
62    /// instruction discovery and this new digest. None means no unique match.
63    pub old_target_status: Option<crate::binding::Status>,
64    /// Completed semantic result, separate from source/provider/resource errors.
65    pub outcome: PairOutcome<E>,
66}
67
68/// Independently retained diagnostic results; no baseline level is asserted.
69pub struct Report<N, D, G, E> {
70    snapshot: Snapshot<D, G>,
71    signature: N,
72    property: N,
73    state: State,
74    pairs: OwnedVec<PairCheck<N, E>>,
75    agrees: bool,
76}
77impl<N: Copy, D: Copy, G: Copy, E> Report<N, D, G, E> {
78    /// Original source generation.
79    #[must_use]
80    pub fn snapshot(&self) -> Snapshot<D, G> {
81        self.snapshot
82    }
83    /// Signature authenticating the Manifest instructions.
84    #[must_use]
85    pub fn signature(&self) -> N {
86        self.signature
87    }
88    /// Original renewal property.
89    #[must_use]
90    pub fn property(&self) -> N {
91        self.property
92    }
93    /// Completed property-level processing state.
94    #[must_use]
95    pub fn state(&self) -> State {
96        self.state
97    }
98    /// Every pair in original order, including unavailable/mismatching inputs.
99    #[must_use]
100    pub fn pairs(&self) -> &[PairCheck<N, E>] {
101        &self.pairs
102    }
103    /// O(1) summary of the new digest comparisons. This does not turn an
104    /// unchecked/failed old-target result into success or assert a B level.
105    #[must_use]
106    pub fn agrees(&self) -> bool {
107        self.agrees
108    }
109}
110
111/// Original input-source refusal, including explicit immutable request absence.
112#[derive(Debug)]
113pub enum SourceError<E, N> {
114    /// Original XML/current operation processing failed.
115    Access(AccessError<E, N>),
116    /// Explicit detached bytes were unavailable, never a generic URI failure.
117    Unavailable {
118        /// Original detached-object reference instruction.
119        node: N,
120        /// Exact immutable-request acquisition cause.
121        cause: E,
122    },
123}
124
125/// Terminal failures retain source/provider causes and real original nodes.
126#[derive(Debug)]
127pub enum CheckError<E, PE, N, D, G> {
128    /// Original XML/source/current-operation refusal.
129    Access(AccessError<E, N>),
130    /// Original instruction shape could not supply a unique digest method.
131    Property(properties::Error<E, N>),
132    /// Declared digest lexical decoding/copy failed before provider effects.
133    Binary(binary::Error<E, MemoryError, N, D, G>),
134    /// Original selected-provider failure.
135    Provider {
136        /// Original method or value associated with provider processing.
137        node: N,
138        /// Original selected-provider cause.
139        cause: PE,
140    },
141    /// Streaming source/provider/processing refusal at the real enclosing node.
142    Digest {
143        /// Original reference/value enclosure, not a logical stream offset.
144        node: N,
145        /// Exact original source/provider/driver cause.
146        cause: digest::Error<SourceError<E, N>, PE>,
147    },
148    /// Metadata/capacity refused before traversal/storage growth.
149    Resource {
150        /// Original metadata/storage enclosure.
151        node: N,
152        /// Exact current admission refusal.
153        cause: MemoryError,
154    },
155    /// Authenticated reference catalogue was incomplete or unchecked.
156    Incomplete(Completeness<N>),
157}
158
159/// Original identity-domain processing result.
160pub type CheckResult<T, X, PE> = Result<
161    T,
162    CheckError<<X as Xml>::Error, PE, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
163>;
164/// Report using the XML owner's source identity and original error type.
165pub type Checked<X> =
166    Report<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation, <X as Xml>::Error>;
167
168pub(super) struct Instruction<N> {
169    pub(super) node: N,
170    pub(super) location: ObjectLocation,
171    pub(super) status: crate::binding::Status,
172}
173struct Expected<N> {
174    pair: Pair<N>,
175    reference: OwnedVec<u8>,
176    object: OwnedVec<u8>,
177}
178struct Candidate<N, I, O> {
179    instruction: Instruction<N>,
180    old: certificate::Expected<I>,
181    output: O,
182}
183
184type Candidates<'q, X, P> =
185    OwnedVec<Candidate<<X as Xml>::Id, <P as Digest<'q>>::Identity, <P as Digest<'q>>::Output>>;
186
187fn memoize<'q, X: TimestampXml, P: DigestProvider<'q, X>, W: Work>(
188    xml: &X,
189    access: &Access<'_, X, W>,
190    reading: &Catalogue<X::Id, X::Document, X::Generation>,
191    instructions: &[Instruction<X::Id>],
192    identity: &certificate::FormatDigest<P::Identity>,
193    provider: &'q P,
194) -> CheckResult<Resolution<Candidates<'q, X, P>>, X, P::Error> {
195    let work = access.execution();
196    let mut candidates = work.vector();
197    let mut ordinal = 0;
198    while ordinal < instructions.len() {
199        work.charge_work(1).map_err(|cause| CheckError::Resource {
200            node: reading.node,
201            cause,
202        })?;
203        let instruction = &instructions[ordinal];
204        let method = original_method(access, instruction.node).map_err(CheckError::Property)?;
205        let old = resolve(access, method, provider)?;
206        let mut source = Input {
207            xml,
208            snapshot: reading.snapshot,
209            mode: Mode::Canonical {
210                node: instruction.node,
211                method: reading.canonicalization,
212            },
213        };
214        let output =
215            match digest::compute_resolved::<P, _, _>(identity, &mut source, provider, work)
216                .map_err(|cause| CheckError::Digest {
217                    node: instruction.node,
218                    cause,
219                })? {
220                Resolution::Supported(output) => output,
221                Resolution::Unsupported => return Ok(Resolution::Unsupported),
222                Resolution::Malformed => return Ok(Resolution::Malformed),
223            };
224        candidates
225            .try_push(Candidate {
226                instruction: Instruction {
227                    node: instruction.node,
228                    location: instruction.location,
229                    status: instruction.status,
230                },
231                old,
232                output,
233            })
234            .map_err(|cause| CheckError::Resource {
235                node: instruction.node,
236                cause,
237            })?;
238        ordinal += 1;
239    }
240    Ok(Resolution::Supported(candidates.into_owned()))
241}
242
243pub(super) enum Mode<N> {
244    Canonical { node: N, method: N },
245    Reference { signature: N, node: N },
246}
247pub(super) struct Input<'a, X: TimestampXml> {
248    pub(super) xml: &'a X,
249    pub(super) snapshot: Snapshot<X::Document, X::Generation>,
250    pub(super) mode: Mode<X::Id>,
251}
252impl<X: TimestampXml> Source for Input<'_, X> {
253    type Error = SourceError<X::Error, X::Id>;
254    fn stream<W: Work>(
255        &mut self,
256        receiver: &mut impl Receiver,
257        work: &W,
258    ) -> Result<(), Self::Error> {
259        let access = Access::new(self.xml, self.snapshot, work);
260        match self.mode {
261            Mode::Canonical { node, method } => access
262                .query(node, |xml, snapshot, work| {
263                    xml.canonicalize(snapshot, node, method, receiver, work)
264                })
265                .map_err(SourceError::Access),
266            Mode::Reference { signature, node } => {
267                match access
268                    .query(node, |xml, snapshot, work| {
269                        xml.replay_reference(
270                            snapshot,
271                            signature,
272                            node,
273                            Replay::ReferenceOutput,
274                            receiver,
275                            work,
276                        )
277                    })
278                    .map_err(SourceError::Access)?
279                {
280                    ReplayOutcome::Complete => Ok(()),
281                    ReplayOutcome::Unavailable(cause) => {
282                        Err(SourceError::Unavailable { node, cause })
283                    }
284                }
285            }
286        }
287    }
288}
289
290/// Executes §5.5.3 over one complete authenticated instruction inventory.
291/// Canonical instruction digests are computed once per reference, separately
292/// from ordinary `ReferenceOutput` for the detached object. Every comparison is
293/// provider/full-identity aware; there is no URI/width identity shortcut, second
294/// XML parser, implicit acquisition or whole-input collector. Source unavailability
295/// is retained per pair and continues; policy/resource/receiver errors terminate.
296/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
297pub fn check<'q, X: TimestampXml + binary::BinaryXml, P: DigestProvider<'q, X>, W: Work>(
298    xml: &X,
299    reading: &Catalogue<X::Id, X::Document, X::Generation>,
300    provider: &'q P,
301    work: &W,
302) -> CheckResult<Checked<X>, X, P::Error> {
303    let result = check_inner(xml, reading, provider, work)?;
304    Access::new(xml, reading.snapshot, work)
305        .query(reading.node, |_, _, _| Ok(()))
306        .map_err(CheckError::Access)?;
307    Ok(result)
308}
309
310fn check_inner<'q, X: TimestampXml + binary::BinaryXml, P: DigestProvider<'q, X>, W: Work>(
311    xml: &X,
312    reading: &Catalogue<X::Id, X::Document, X::Generation>,
313    provider: &'q P,
314    work: &W,
315) -> CheckResult<Checked<X>, X, P::Error> {
316    let access = Access::new(xml, reading.snapshot, work);
317    access
318        .query(reading.node, |_, _, _| Ok(()))
319        .map_err(CheckError::Access)?;
320    if let Some(answer) =
321        certificate::preflight_method(&access, reading.digest).map_err(CheckError::Access)?
322    {
323        return Ok(report(
324            reading,
325            State::Algorithm(answer),
326            work.vector().into_owned(),
327            false,
328        ));
329    }
330    let expected = read_expected(xml, reading, work)?;
331    let instructions = inventory(&access, reading.signature, reading.node)?;
332    if let Some(state) = manifest_state::<X, W, P::Error>(&instructions, reading.node, work)? {
333        return Ok(report(reading, state, work.vector().into_owned(), false));
334    }
335    let identity = match resolve(&access, reading.digest, provider)? {
336        certificate::Expected::Ready(identity) => identity,
337        certificate::Expected::Answer(answer) => {
338            return Ok(report(
339                reading,
340                State::Algorithm(answer),
341                work.vector().into_owned(),
342                false,
343            ));
344        }
345    };
346    let width = provider.width(identity.identity());
347    let mut usable = false;
348    // Inspect every declared width before canonicalization/hash side effects.
349    // Invalid pairs remain diagnostics; only other usable pairs justify work.
350    for ordinal in 0..expected.len() {
351        work.charge_work(1).map_err(|cause| CheckError::Resource {
352            node: reading.node,
353            cause,
354        })?;
355        let pair = &expected[ordinal];
356        if let Some(width) = width
357            && (pair.reference.len() != width || pair.object.len() != width)
358        {
359            continue;
360        }
361        usable = true;
362    }
363    let candidates = if usable {
364        match memoize(xml, &access, reading, &instructions, &identity, provider)? {
365            Resolution::Supported(candidates) => candidates,
366            Resolution::Unsupported => {
367                return Ok(report(
368                    reading,
369                    State::Algorithm(Agreement::Unsupported),
370                    work.vector().into_owned(),
371                    false,
372                ));
373            }
374            Resolution::Malformed => {
375                return Ok(report(
376                    reading,
377                    State::Algorithm(Agreement::MalformedMethod),
378                    work.vector().into_owned(),
379                    false,
380                ));
381            }
382        }
383    } else {
384        work.vector().into_owned()
385    };
386    finish_pairs(
387        xml,
388        reading,
389        &expected,
390        &candidates,
391        &identity,
392        provider,
393        work,
394    )
395}
396
397fn manifest_state<X: Xml, W: Work, PE>(
398    instructions: &[Instruction<X::Id>],
399    node: X::Id,
400    work: &W,
401) -> CheckResult<Option<State>, X, PE> {
402    let mut unknown = false;
403    let mut ordinal = 0;
404    while ordinal < instructions.len() {
405        work.charge_work(1)
406            .map_err(|cause| CheckError::Resource { node, cause })?;
407        let instruction = &instructions[ordinal];
408        if instruction.location == ObjectLocation::Detached {
409            return Ok(None);
410        }
411        if instruction.location == ObjectLocation::Unknown {
412            unknown = true;
413        }
414        ordinal += 1;
415    }
416    Ok(Some(if unknown {
417        State::UnclassifiedManifest
418    } else {
419        State::NoDetachedManifest
420    }))
421}
422
423fn finish_pairs<'q, X: TimestampXml, P: DigestProvider<'q, X>, W: Work>(
424    xml: &X,
425    reading: &Catalogue<X::Id, X::Document, X::Generation>,
426    expected: &[Expected<X::Id>],
427    candidates: &[Candidate<X::Id, P::Identity, P::Output>],
428    identity: &certificate::FormatDigest<P::Identity>,
429    provider: &'q P,
430    work: &W,
431) -> CheckResult<Checked<X>, X, P::Error> {
432    let mut results = work.vector();
433    let mut agrees = true;
434    let mut ordinal = 0;
435    while ordinal < expected.len() {
436        work.charge_work(1).map_err(|cause| CheckError::Resource {
437            node: reading.node,
438            cause,
439        })?;
440        let pair = &expected[ordinal];
441        let result = process_pair(xml, reading, pair, candidates, identity, provider, work)?;
442        agrees &= matches!(result.outcome, PairOutcome::Matches);
443        results
444            .try_push(result)
445            .map_err(|cause| CheckError::Resource {
446                node: pair.pair.node,
447                cause,
448            })?;
449        ordinal += 1;
450    }
451    Ok(report(
452        reading,
453        State::Processed,
454        results.into_owned(),
455        agrees,
456    ))
457}
458
459fn report<N: Copy, D: Copy, G: Copy, E>(
460    reading: &Catalogue<N, D, G>,
461    state: State,
462    pairs: OwnedVec<PairCheck<N, E>>,
463    agrees: bool,
464) -> Report<N, D, G, E> {
465    Report {
466        snapshot: reading.snapshot,
467        signature: reading.signature,
468        property: reading.node,
469        state,
470        pairs,
471        agrees,
472    }
473}
474
475pub(super) fn resolve<'q, X: Xml, P: DigestProvider<'q, X>, W: Work>(
476    access: &Access<'_, X, W>,
477    node: X::Id,
478    provider: &P,
479) -> CheckResult<certificate::Expected<P::Identity>, X, P::Error> {
480    let resolved = access
481        .query(node, |_, _, _| Ok(provider.resolve(access, node)))
482        .map_err(CheckError::Access)?
483        .map_err(|cause| CheckError::Provider { node, cause })?;
484    Ok(match resolved {
485        Resolution::Supported(identity) => certificate::baseline_digest::<X, P>(identity, provider),
486        Resolution::Unsupported => certificate::Expected::Answer(Agreement::Unsupported),
487        Resolution::Malformed => certificate::Expected::Answer(Agreement::MalformedMethod),
488    })
489}
490
491pub(super) fn original_method<X: Xml, W: Work>(
492    access: &Access<'_, X, W>,
493    node: X::Id,
494) -> properties::PropertyResult<X::Id, X> {
495    let mut method = None;
496    for child in access.children(node) {
497        let child = child.map_err(properties::Error::Child)?;
498        if access
499            .has_name(
500                child,
501                Name {
502                    namespace: DS,
503                    local: "DigestMethod",
504                },
505            )
506            .map_err(properties::Error::Access)?
507        {
508            if method.is_some() {
509                return Err(properties::Error::Duplicate { node: child });
510            }
511            method = Some(child);
512        }
513    }
514    method.ok_or(properties::Error::Missing {
515        parent: node,
516        local: "DigestMethod",
517    })
518}
519
520pub(super) fn inventory<X: ReferenceXml, W: Work, PE>(
521    access: &Access<'_, X, W>,
522    signature: X::Id,
523    context: X::Id,
524) -> CheckResult<OwnedVec<Instruction<X::Id>>, X, PE> {
525    let work = access.execution();
526    let mut entries = work.vector();
527    let complete = access
528        .query(context, |xml, snapshot, work| {
529            Ok(
530                xml.visit_authenticated_references(snapshot, signature, work, |descriptor| {
531                    if matches!(descriptor.parent, Parent::Manifest(_)) {
532                        entries
533                            .try_push(Instruction {
534                                node: descriptor.reference.node,
535                                location: descriptor.object_location,
536                                status: descriptor.reference.status,
537                            })
538                            .map_err(|cause| CheckError::Resource {
539                                node: descriptor.reference.node,
540                                cause,
541                            })?;
542                    }
543                    Ok::<_, CheckError<X::Error, PE, X::Id, X::Document, X::Generation>>(())
544                }),
545            )
546        })
547        .map_err(CheckError::Access)?;
548    let complete = match complete {
549        Ok(complete) => complete,
550        Err(VisitError::Processing(cause)) => {
551            return Err(CheckError::Access(AccessError::Xml(cause)));
552        }
553        Err(VisitError::Visitor(cause)) => return Err(cause),
554    };
555    if complete != Completeness::Complete {
556        return Err(CheckError::Incomplete(complete));
557    }
558    Ok(entries.into_owned())
559}
560
561fn read_expected<X: binary::BinaryXml, W: Work, PE>(
562    xml: &X,
563    reading: &Catalogue<X::Id, X::Document, X::Generation>,
564    work: &W,
565) -> CheckResult<OwnedVec<Expected<X::Id>>, X, PE> {
566    let mut expected = work.vector();
567    for ordinal in 0..reading.pairs.len() {
568        work.charge_work(1).map_err(|cause| CheckError::Resource {
569            node: reading.node,
570            cause,
571        })?;
572        let pair = reading.pairs[ordinal];
573        let copy = |node| {
574            binary::with_octets(
575                xml,
576                binary::Location {
577                    snapshot: reading.snapshot,
578                    node,
579                },
580                work,
581                |bytes| {
582                    let mut owned = work.vector();
583                    owned.try_extend_from_slice(bytes)?;
584                    Ok(owned.into_owned())
585                },
586            )
587            .map_err(CheckError::Binary)
588        };
589        let reference = copy(pair.reference)?;
590        let object = copy(pair.object)?;
591        expected
592            .try_push(Expected {
593                pair,
594                reference,
595                object,
596            })
597            .map_err(|cause| CheckError::Resource {
598                node: pair.node,
599                cause,
600            })?;
601    }
602    Ok(expected.into_owned())
603}
604
605fn process_pair<'q, X: TimestampXml, P: DigestProvider<'q, X>, W: Work>(
606    xml: &X,
607    reading: &Catalogue<X::Id, X::Document, X::Generation>,
608    pair: &Expected<X::Id>,
609    candidates: &[Candidate<X::Id, P::Identity, P::Output>],
610    identity: &certificate::FormatDigest<P::Identity>,
611    provider: &'q P,
612    work: &W,
613) -> CheckResult<PairCheck<X::Id, X::Error>, X, P::Error> {
614    let width = provider.width(identity.identity());
615    let mut result = PairCheck {
616        pair: pair.pair,
617        reference: None,
618        old_target_status: None,
619        outcome: PairOutcome::MissingReference,
620    };
621    if let Some(width) = width
622        && (pair.reference.len() != width || pair.object.len() != width)
623    {
624        result.outcome = PairOutcome::Algorithm(Agreement::MalformedValue);
625        return Ok(result);
626    }
627    let mut selected = None;
628    let mut ordinal = 0;
629    while ordinal < candidates.len() {
630        work.charge_work(1).map_err(|cause| CheckError::Resource {
631            node: pair.pair.node,
632            cause,
633        })?;
634        let candidate = &candidates[ordinal];
635        ordinal += 1;
636        if provider
637            .matches(
638                identity.identity(),
639                &candidate.output,
640                &pair.reference,
641                work,
642            )
643            .map_err(|cause| CheckError::Provider {
644                node: pair.pair.reference,
645                cause,
646            })?
647        {
648            if selected.is_some() {
649                result.outcome = PairOutcome::AmbiguousReference;
650                return Ok(result);
651            }
652            selected = Some(candidate);
653        }
654    }
655    let Some(candidate) = selected else {
656        return Ok(result);
657    };
658    let node = candidate.instruction.node;
659    result.reference = Some(node);
660    result.old_target_status = Some(candidate.instruction.status);
661    if candidate.instruction.location != ObjectLocation::Detached {
662        result.outcome = PairOutcome::NotDetached;
663        return Ok(result);
664    }
665    // EN319132-1 §5.5.3 step3/note2 processes the signed instruction WITHOUT
666    // computing its old target digest. In particular, unavailable old bytes
667    // must reach step3/6 even if their historical target outcome is Invalid.
668    // The authenticated Manifest parent, not that target outcome, establishes
669    // original instruction integrity; preserve both facts independently.
670    // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
671    let old = match &candidate.old {
672        certificate::Expected::Ready(old) => old,
673        certificate::Expected::Answer(answer) => {
674            result.outcome = PairOutcome::Algorithm(*answer);
675            return Ok(result);
676        }
677    };
678    if provider
679        .compare(identity.identity(), old.identity(), work)
680        .map_err(|cause| CheckError::Provider { node, cause })?
681        == Ordering::Equal
682    {
683        result.outcome = PairOutcome::SameAlgorithm;
684        return Ok(result);
685    }
686    let mut source = Input {
687        xml,
688        snapshot: reading.snapshot,
689        mode: Mode::Reference {
690            signature: reading.signature,
691            node,
692        },
693    };
694    result.outcome = match digest::run_resolved::<P, _, _>(
695        identity,
696        &pair.object,
697        &mut source,
698        provider,
699        work,
700    ) {
701        Ok(Agreement::Matches) => PairOutcome::Matches,
702        Ok(Agreement::Differs) => PairOutcome::Differs,
703        Ok(answer) => PairOutcome::Algorithm(answer),
704        Err(digest::Error::Source(SourceError::Unavailable { cause, .. })) => {
705            PairOutcome::Unavailable(cause)
706        }
707        Err(cause) => {
708            return Err(CheckError::Digest {
709                node: pair.pair.object,
710                cause,
711            });
712        }
713    };
714    Ok(result)
715}