Skip to main content

structured_xades/material/
pipeline.rs

1//! One scoped decode/PKIX pass over the complete prepared material inventory.
2
3use super::counters::collect_counters;
4use super::{Cause, Entry, RetainError, Source, Value};
5use crate::binary::{Located, Location, OwnedBinaryXml};
6use crate::prepare::Prepared;
7use crate::unsigned;
8use crate::xml::{Access, AccessError, Xml};
9use structured_asn1::{
10    Reader,
11    memory::{HashWork, MemoryError, OwnedVec, Work},
12};
13use structured_pkix::ocsp::{BasicResponse, Response};
14
15/// Original storage role, not authorization, freshness or a completeness claim.
16#[derive(Debug, Clone, Copy)]
17pub enum Placement<N> {
18    /// Original selected `KeyInfo` certificate/CRL. The identity is its owning
19    /// signature, matching archive coverage's original header component.
20    KeyInfo(N),
21    /// Ordinary signature/countersignature material property.
22    Signature(N),
23    /// Attribute-authority material property.
24    Attribute(N),
25    /// General `AnyValidationData` container.
26    General(N),
27    /// Timestamp-specific validation-data container; its URI is only a hint.
28    Timestamp(N),
29    /// Material carried in an actual CMS token, whose enclosing timestamp
30    /// property and input role remain separate from mathematical evidence.
31    TimestampToken {
32        /// Original timestamp property.
33        property: N,
34        /// Original `EncapsulatedTimeStamp` element.
35        token: N,
36        /// Original timestamp input role.
37        role: crate::timestamp::Role,
38    },
39}
40
41/// Original retained material and the property role which actually contains it.
42pub struct Retained<N, D, G> {
43    placement: Placement<N>,
44    source: Source<N, D, G>,
45}
46
47impl<N: Copy, D: Copy, G: Copy> Retained<N, D, G> {
48    /// Actual original source placement.
49    #[must_use]
50    pub fn placement(&self) -> Placement<N> {
51        self.placement
52    }
53    /// Retained original XML enclosure and decoded source, without another copy.
54    #[must_use]
55    pub fn source(&self) -> &Source<N, D, G> {
56        &self.source
57    }
58}
59
60/// Schema-open entry preserved separately from parsed X.509/CRL/OCSP values.
61#[derive(Debug, Clone, Copy)]
62pub struct Opaque<N, D, G> {
63    /// Actual original source placement, never inferred from a material count.
64    pub placement: Placement<N>,
65    /// Original open-format member and its XML snapshot.
66    pub location: Location<N, D, G>,
67    /// Original other-certificate or other-status role.
68    pub entry: Entry<N>,
69}
70
71/// Parsed basic response from one original whole OCSP envelope. Its signature,
72/// authorization, freshness and status still require the common PKIX checks.
73pub struct Basic<'source, N, D, G> {
74    source_index: usize,
75    location: Location<N, D, G>,
76    response: BasicResponse<'source>,
77}
78
79impl<'source, N: Copy, D: Copy, G: Copy> Basic<'source, N, D, G> {
80    /// Ordinal of the original whole response in the frozen sources/values set.
81    #[must_use]
82    pub fn source_index(&self) -> usize {
83        self.source_index
84    }
85    /// Original XML enclosure; basic-response offsets remain decoded coordinates.
86    #[must_use]
87    pub fn location(&self) -> Location<N, D, G> {
88        self.location
89    }
90    /// Shared original signed data, embedded certificates and status assertions.
91    #[must_use]
92    pub fn response(&self) -> &BasicResponse<'source> {
93        &self.response
94    }
95}
96
97/// Callback-local parsed set. All known values have been decoded and parsed
98/// before the visitor runs; source and parsed arrays have identical index order.
99/// Open formats are not silently counted as usable X.509/status material.
100pub struct View<'source, N, D, G> {
101    pub(super) snapshot: crate::xml::Snapshot<D, G>,
102    pub(super) signature: N,
103    pub(super) sources: &'source [Retained<N, D, G>],
104    pub(super) values: &'source [Located<Value<'source>, N, D, G>],
105    opaque: &'source [Opaque<N, D, G>],
106    pub(super) basic: &'source [Basic<'source, N, D, G>],
107}
108
109impl<'source, N, D, G> View<'source, N, D, G> {
110    /// Frozen original sources backing every parsed value.
111    #[must_use]
112    pub fn sources(&self) -> &'source [Retained<N, D, G>] {
113        self.sources
114    }
115    /// Parsed known values, without authentication or status promotion.
116    #[must_use]
117    pub fn values(&self) -> &'source [Located<Value<'source>, N, D, G>] {
118        self.values
119    }
120    /// Original schema-open formats requiring explicit recognition if needed.
121    #[must_use]
122    pub fn opaque(&self) -> &'source [Opaque<N, D, G>] {
123        self.opaque
124    }
125    /// Every known basic-response payload, decoded once under the same current
126    /// operation. Unknown types and protocol failures remain in `values()`;
127    /// they are never silently promoted to signed basic responses.
128    #[must_use]
129    pub fn basic_responses(&self) -> &'source [Basic<'source, N, D, G>] {
130        self.basic
131    }
132}
133
134impl<N: Copy, D: Copy, G: Copy> View<'_, N, D, G> {
135    /// Original immutable source generation of this complete parsed catalogue.
136    #[must_use]
137    pub fn snapshot(&self) -> crate::xml::Snapshot<D, G> {
138        self.snapshot
139    }
140
141    /// Exact selected root whose original material was collected.
142    #[must_use]
143    pub fn signature(&self) -> N {
144        self.signature
145    }
146}
147
148/// Terminal pipeline refusal; no successfully parsed subset is returned.
149#[derive(Debug)]
150pub enum Error<E, VE, N, D, G> {
151    /// XML source/generation/current-operation refusal.
152    Access(AccessError<E, N>),
153    /// Original header/material field grammar, source or resource cause.
154    Property(crate::properties::Error<E, N>),
155    /// Original general/timestamp validation-data field cause.
156    Validation(crate::validation::Error<E, N>),
157    /// Source retention failed before the exact value was available.
158    Retain {
159        /// Original XML enclosure.
160        location: Location<N, D, G>,
161        /// Unchanged source cause.
162        cause: RetainError<E, N>,
163    },
164    /// Located original PKIX/schema/resource cause in decoded coordinates.
165    Parse(Located<Cause, N, D, G>),
166    /// Prepared catalogue does not belong to the expected original scope.
167    Foreign {
168        /// Original catalogue parent.
169        node: N,
170    },
171    /// Metadata/source/parsed catalogue storage or traversal admission failed.
172    Resource {
173        /// Original enclosing node.
174        node: N,
175        /// Original resource cause.
176        cause: MemoryError,
177    },
178    /// Exact visitor refusal; external visitor side effects are not rolled back.
179    Visitor(VE),
180}
181
182/// Pipeline result with this owner's unchanged causes and source identity types.
183pub type PipelineResult<T, X, VE> = Result<
184    T,
185    Error<<X as Xml>::Error, VE, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
186>;
187
188pub(super) type Record<X> = Retained<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
189pub(super) type Open<X> = Opaque<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
190type Basics<'source, X, W> =
191    OwnedVec<Basic<'source, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>, W>;
192
193/// Retains and parses every known original value once, then invokes a scoped
194/// visitor over the finalized source/parsed catalogues. Partial preparation and
195/// results are dropped on refusal. The visitor may retain accounted owned facts
196/// or original XML identities, but cannot borrow these temporary source buffers.
197/// Material availability/authentication/path/status/chronology and attained
198/// levels remain separate: an OCSP failure envelope stays a failure value.
199///
200/// ```compile_fail
201/// use structured_asn1::Reader;
202/// use structured_xades::{binary::OwnedBinaryXml, material, prepare::Prepared};
203/// fn escape<'a, X: OwnedBinaryXml>(
204///     xml: &X, prepared: &Prepared<X>, operation: &Reader<'_>,
205/// ) -> material::PipelineResult<&'a [u8], X, ()> {
206///     material::with_values(xml, prepared, operation, |_, view| {
207///         Ok(view.sources()[0].source().bytes())
208///     })
209/// }
210/// ```
211pub fn with_values<X: OwnedBinaryXml, R, VE>(
212    xml: &X,
213    prepared: &Prepared<X>,
214    operation: &Reader<'_>,
215    visitor: impl for<'source> FnOnce(
216        &Reader<'source>,
217        View<'source, X::Id, X::Document, X::Generation>,
218    ) -> Result<R, VE>,
219) -> PipelineResult<R, X, VE> {
220    with_inputs(xml, prepared, None, operation, visitor)
221}
222
223/// Retain the root's prepared material AND every original counter's header and
224/// unsigned material in one frozen source pool. Membership comes from the
225/// complete actual counter/path receipt, not a caller-written child list.
226/// Plain `XMLDSig` counters need no B-level properties or mandatory `KeyInfo`.
227/// This does not process child timestamp tokens, authenticate material, or
228/// assert a counter/root baseline level. Token-carried material remains a
229/// separate source pool, consumed by the same original-artifact index.
230///
231/// ```compile_fail
232/// use structured_asn1::Reader;
233/// use structured_xades::{binary::OwnedBinaryXml, material, pki, prepare::Prepared};
234/// fn escape<'a, X: OwnedBinaryXml>(
235///     xml: &X, prepared: &Prepared<X>, operation: &Reader<'_>,
236///     counters: &pki::CounterPaths<'_, '_, X::Id, X::Document, X::Generation>,
237/// ) -> material::PipelineResult<&'a [u8], X, ()> {
238///     material::with_counter_values(xml, prepared, counters, operation, |_, view| {
239///         Ok(view.sources()[0].source().bytes())
240///     })
241/// }
242/// ```
243pub fn with_counter_values<X: OwnedBinaryXml, R, VE>(
244    xml: &X,
245    prepared: &Prepared<X>,
246    counters: &CounterSources<'_, '_, X>,
247    operation: &Reader<'_>,
248    visitor: impl for<'source> FnOnce(
249        &Reader<'source>,
250        View<'source, X::Id, X::Document, X::Generation>,
251    ) -> Result<R, VE>,
252) -> PipelineResult<R, X, VE> {
253    with_inputs(xml, prepared, Some(counters), operation, visitor)
254}
255
256pub(super) type CounterSources<'view, 'document, X> = crate::pki::CounterPaths<
257    'view,
258    'document,
259    <X as Xml>::Id,
260    <X as Xml>::Document,
261    <X as Xml>::Generation,
262>;
263
264fn with_inputs<X: OwnedBinaryXml, R, VE>(
265    xml: &X,
266    prepared: &Prepared<X>,
267    counters: Option<&CounterSources<'_, '_, X>>,
268    operation: &Reader<'_>,
269    visitor: impl for<'source> FnOnce(
270        &Reader<'source>,
271        View<'source, X::Id, X::Document, X::Generation>,
272    ) -> Result<R, VE>,
273) -> PipelineResult<R, X, VE> {
274    let layout = prepared.bound().containers();
275    let execution = operation.execution();
276    let access = Access::new(xml, layout.snapshot(), &execution);
277    admit(&access, layout.signature())?;
278    if let Some(counters) = counters
279        && (counters.snapshot() != layout.snapshot()
280            || !access
281                .equal_id(counters.signature(), layout.signature())
282                .map_err(|cause| Error::Resource {
283                    node: layout.signature(),
284                    cause,
285                })?)
286    {
287        return Err(Error::Foreign {
288            node: counters.signature(),
289        });
290    }
291    let mut sources = execution.vector();
292    let mut opaque = execution.vector();
293    collect_header(xml, &access, prepared.header(), operation, &mut sources)?;
294    let parents = material_parents(&access, prepared)?;
295    for index in 0..prepared.material().len() {
296        admit(&access, layout.signature())?;
297        let catalogue = &prepared.material()[index];
298        let node = catalogue.parent();
299        let placement = source_placement(parents, node, &execution)
300            .map_err(|cause| Error::Resource { node, cause })?
301            .ok_or(Error::Foreign { node })?;
302        collect(
303            xml,
304            &access,
305            catalogue,
306            placement,
307            operation,
308            &mut sources,
309            &mut opaque,
310        )?;
311    }
312    for index in 0..prepared.validation().len() {
313        admit(&access, layout.signature())?;
314        let data = &prepared.validation()[index];
315        if data.snapshot() != layout.snapshot()
316            || !access
317                .equal_id(data.signature(), layout.signature())
318                .map_err(|cause| Error::Resource {
319                    node: data.node(),
320                    cause,
321                })?
322        {
323            return Err(Error::Foreign { node: data.node() });
324        }
325        collect_validation(xml, &access, data, operation, &mut sources, &mut opaque)?;
326    }
327    if let Some(counters) = counters {
328        collect_counters(xml, &access, counters, operation, &mut sources, &mut opaque)?;
329    }
330    let mut values = execution.vector();
331    for index in 0..sources.len() {
332        admit(&access, layout.signature())?;
333        let source = &sources[index].source;
334        let value = source.read_in(operation).map_err(Error::Parse)?;
335        values.try_push(value).map_err(|cause| Error::Resource {
336            node: source.location().node,
337            cause,
338        })?;
339    }
340    let basic = read_basics(&access, layout.signature(), &values, operation, &execution)?;
341    admit(&access, layout.signature())?;
342    let result = visitor(
343        operation,
344        View {
345            snapshot: layout.snapshot(),
346            signature: layout.signature(),
347            sources: &sources,
348            values: &values,
349            opaque: &opaque,
350            basic: &basic,
351        },
352    )
353    .map_err(Error::Visitor)?;
354    // A successful visitor result must not escape after a source generation
355    // change. Returning Err drops that result and all temporary catalogues.
356    admit(&access, layout.signature())?;
357    Ok(result)
358}
359
360fn read_basics<'source, X: Xml, W: Work, VE>(
361    access: &Access<'_, X, W>,
362    signature: X::Id,
363    values: &[crate::binary::LocatedValue<Value<'source>, X>],
364    operation: &Reader<'source>,
365    execution: &W,
366) -> PipelineResult<Basics<'source, X, W>, X, VE> {
367    let mut basic = execution.vector();
368    let mut cursor = 0;
369    while cursor < values.len() {
370        admit(access, signature)?;
371        let index = cursor;
372        cursor += 1;
373        let value = &values[index];
374        let Value::Ocsp(Response::Successful(envelope)) = &value.value else {
375            continue;
376        };
377        if let Some(response) = envelope.basic_in(operation).map_err(|cause| {
378            Error::Parse(Located {
379                location: value.location,
380                value: Cause::Pki(cause),
381            })
382        })? {
383            basic
384                .try_push(Basic {
385                    source_index: index,
386                    location: value.location,
387                    response,
388                })
389                .map_err(|cause| Error::Resource {
390                    node: value.location.node,
391                    cause,
392                })?;
393        }
394    }
395    Ok(basic)
396}
397
398fn collect_header<X: OwnedBinaryXml, W: Work, VE>(
399    xml: &X,
400    access: &Access<'_, X, W>,
401    header: &crate::header::Reading<X>,
402    operation: &Reader<'_>,
403    sources: &mut OwnedVec<Record<X>, W>,
404) -> PipelineResult<(), X, VE> {
405    // XMLDSig1.1 §4.5.4 includes X509CRL in the SAME original X509Data. EN
406    // 319132-1 §5.4.3(1-3) expressly permits status data already in KeyInfo;
407    // losing these copies would incorrectly require a second incorporation.
408    // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-X509Data
409    collect_data(
410        xml,
411        access,
412        header.signature(),
413        header.x509_data(),
414        operation,
415        sources,
416    )
417}
418
419pub(super) fn collect_data<X: OwnedBinaryXml, W: Work, VE>(
420    xml: &X,
421    access: &Access<'_, X, W>,
422    signature: X::Id,
423    data: X::Id,
424    operation: &Reader<'_>,
425    sources: &mut OwnedVec<Record<X>, W>,
426) -> PipelineResult<(), X, VE> {
427    crate::header::visit_values(access, data, |node, kind| {
428        admit(access, signature)?;
429        let location = Location {
430            snapshot: access.snapshot(),
431            node,
432        };
433        let schema = match kind {
434            crate::header::ValueKind::Certificate => super::Kind::Certificate,
435            crate::header::ValueKind::Crl => super::Kind::Crl,
436        };
437        let source = super::retain_expected(
438            xml,
439            location,
440            schema,
441            crate::container::DS,
442            kind.local(),
443            operation,
444        )
445        .map_err(|cause| Error::Retain { location, cause })?;
446        sources
447            .try_push(Retained {
448                placement: Placement::KeyInfo(signature),
449                source,
450            })
451            .map_err(|cause| Error::Resource { node, cause })?;
452        Ok(())
453    })
454    .map_err(|cause| match cause {
455        crate::reference::VisitError::Processing(cause) => Error::Property(cause),
456        crate::reference::VisitError::Visitor(cause) => cause,
457    })
458}
459
460fn material_parents<X: Xml, W: Work, VE>(
461    access: &Access<'_, X, W>,
462    prepared: &Prepared<X>,
463) -> PipelineResult<[Option<X::Id>; 4], X, VE> {
464    let mut parents = [None; 4];
465    for index in 0..prepared.unsigned().entries().len() {
466        admit(access, prepared.unsigned().signature())?;
467        let entry = prepared.unsigned().entries()[index];
468        let slot = match entry.role {
469            unsigned::Role::Certificates => 0,
470            unsigned::Role::Revocation => 1,
471            unsigned::Role::AttributeCertificates => 2,
472            unsigned::Role::AttributeRevocation => 3,
473            _ => continue,
474        };
475        parents[slot] = Some(entry.node);
476    }
477    Ok(parents)
478}
479
480pub(super) fn collect<X: OwnedBinaryXml, W: Work, VE>(
481    xml: &X,
482    access: &Access<'_, X, W>,
483    catalogue: &super::Catalogue<X::Id, X::Document, X::Generation>,
484    placement: Placement<X::Id>,
485    operation: &Reader<'_>,
486    sources: &mut OwnedVec<Record<X>, W>,
487    opaque: &mut OwnedVec<Open<X>, W>,
488) -> PipelineResult<(), X, VE> {
489    if catalogue.snapshot() != access.snapshot() {
490        return Err(Error::Foreign {
491            node: catalogue.parent(),
492        });
493    }
494    for index in 0..catalogue.entries().len() {
495        admit(access, catalogue.parent())?;
496        let entry = catalogue.entries()[index];
497        match entry {
498            Entry::Encapsulated { node, kind } => {
499                let location = Location {
500                    snapshot: catalogue.snapshot(),
501                    node,
502                };
503                let source = super::retain(xml, location, kind, operation)
504                    .map_err(|cause| Error::Retain { location, cause })?;
505                sources
506                    .try_push(Retained { placement, source })
507                    .map_err(|cause| Error::Resource { node, cause })?;
508            }
509            Entry::OtherCertificate { node } | Entry::OtherStatus { node } => {
510                opaque
511                    .try_push(Opaque {
512                        placement,
513                        location: Location {
514                            snapshot: catalogue.snapshot(),
515                            node,
516                        },
517                        entry,
518                    })
519                    .map_err(|cause| Error::Resource { node, cause })?;
520            }
521        }
522    }
523    Ok(())
524}
525
526pub(super) fn collect_validation<X: OwnedBinaryXml, W: Work, VE>(
527    xml: &X,
528    access: &Access<'_, X, W>,
529    data: &crate::validation::Reading<X>,
530    operation: &Reader<'_>,
531    sources: &mut OwnedVec<Record<X>, W>,
532    opaque: &mut OwnedVec<Open<X>, W>,
533) -> PipelineResult<(), X, VE> {
534    if data.snapshot() != access.snapshot() {
535        return Err(Error::Foreign { node: data.node() });
536    }
537    let placement = match data.role() {
538        crate::validation::Role::Any => Placement::General(data.node()),
539        crate::validation::Role::Timestamp => Placement::Timestamp(data.node()),
540    };
541    for catalogue in [data.certificates(), data.revocations()]
542        .into_iter()
543        .flatten()
544    {
545        collect(
546            xml, access, catalogue, placement, operation, sources, opaque,
547        )?;
548    }
549    Ok(())
550}
551
552pub(super) fn admit<X: Xml, W: Work, VE>(
553    access: &Access<'_, X, W>,
554    node: X::Id,
555) -> PipelineResult<(), X, VE> {
556    access
557        .query(node, |_, _, work| Ok(work.charge_work(1)))
558        .map_err(Error::Access)?
559        .map_err(|cause| Error::Resource { node, cause })
560}
561
562fn source_placement<N: Copy + HashWork, W: Work>(
563    parents: [Option<N>; 4],
564    node: N,
565    work: &W,
566) -> Result<Option<Placement<N>>, MemoryError> {
567    for (index, parent) in parents.into_iter().enumerate() {
568        if let Some(parent) = parent
569            && crate::xml::equal_id(parent, node, work)?
570        {
571            return Ok(Some(if index < 2 {
572                Placement::Signature(node)
573            } else {
574                Placement::Attribute(node)
575            }));
576        }
577    }
578    Ok(None)
579}
580
581#[cfg(test)]
582mod placement_tests {
583    use super::{Placement, source_placement};
584    use alloc::sync::Arc;
585    use structured_asn1::memory::{HashWork, Memory, MemoryAccount, MemoryError};
586
587    #[derive(Clone, Copy, PartialEq, Eq, Hash)]
588    struct Identity(u8, u64);
589    impl HashWork for Identity {
590        fn hash_work(&self) -> Result<u64, MemoryError> {
591            Ok(self.1)
592        }
593    }
594
595    #[test]
596    fn placement_pays_the_retained_parent_operand_before_comparison() {
597        // The untrusted retained parent has a larger scan bound than the
598        // material node. Paying four node bounds cannot admit even one pair.
599        let node = Identity(1, 1);
600        let parent = Identity(2, 100);
601        let short = Memory::new(Arc::new(MemoryAccount::new(0, 4)));
602        assert!(matches!(
603            source_placement([Some(parent), None, None, None], node, &short),
604            Err(MemoryError::WorkExceeded)
605        ));
606        assert_eq!(short.work_remaining(), 4);
607        let exact = Memory::new(Arc::new(MemoryAccount::new(0, 2)));
608        assert!(matches!(
609            source_placement([None, None, Some(node), None], node, &exact),
610            Ok(Some(Placement::Attribute(_)))
611        ));
612        assert_eq!(exact.work_remaining(), 0);
613    }
614}