Skip to main content

structured_xades/
commitment.rs

1//! Original signed commitments and their authenticated Reference associations.
2
3use crate::binding::{Bound, Status};
4use crate::container::XADES;
5use crate::identifier;
6use crate::properties::{self, DataObjectRole, Error, PropertyResult};
7use crate::reference::{
8    Completeness, Descriptor, LexicalSource, Parent, ReferenceXml, Resolution, VisitError,
9};
10use crate::xml::{Access, AccessError, Name, Snapshot, Xml};
11use structured_asn1::memory::{OwnedVec, Work};
12
13/// Schema choice, preserving explicit references and `AllSignedDataObjects`.
14#[derive(Debug)]
15pub enum Objects<N> {
16    /// Original empty marker, not a caller-supplied all-objects assertion.
17    All(N),
18    /// Nonempty original `ObjectReference` simple-content elements in order.
19    References(OwnedVec<N>),
20}
21
22/// Structural signed commitment; URI syntax and policy meaning remain separate.
23pub struct Reading<N, D, G> {
24    snapshot: Snapshot<D, G>,
25    signature: N,
26    node: N,
27    kind: identifier::Reading<N>,
28    objects: Objects<N>,
29    qualifiers: Option<N>,
30}
31
32impl<N: Copy, D: Copy, G: Copy> Reading<N, D, G> {
33    /// Original commitment property.
34    #[must_use]
35    pub fn node(&self) -> N {
36        self.node
37    }
38    /// Original identifier. Its URI meaning is defined by registration/policy,
39    /// not by a hard-coded common list of commitment types.
40    #[must_use]
41    pub fn kind(&self) -> &identifier::Reading<N> {
42        &self.kind
43    }
44    /// Original schema choice.
45    #[must_use]
46    pub fn objects(&self) -> &Objects<N> {
47        &self.objects
48    }
49    /// Original optional qualifiers container; each child was checked for the
50    /// schema's `CommitmentTypeQualifier` role, whose `AnyType` payload stays open.
51    #[must_use]
52    pub fn qualifiers(&self) -> Option<N> {
53        self.qualifiers
54    }
55}
56
57/// Original commitment using this XML owner's identities.
58pub type Original<X> = Reading<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
59
60/// Reads the complete ยง5.2.3 schema choice from covered original properties.
61/// A commitment Identifier must not carry Qualifier, including an empty one.
62/// Current Work funds even skipped metadata and every retained reference.
63/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
64pub fn read<X: Xml, W: Work>(
65    access: &Access<'_, X, W>,
66    signed: &properties::Reading<X>,
67    index: usize,
68) -> PropertyResult<Original<X>, X> {
69    let node = selected(access, signed, index)?;
70    let mut kind = None;
71    let mut all = None;
72    let mut references = access
73        .query(node, |_, _, work| Ok(work.vector()))
74        .map_err(Error::Access)?;
75    let mut qualifiers = None;
76    for child in access.children(node) {
77        let child = child.map_err(Error::Child)?;
78        if named(access, child, "CommitmentTypeId")? {
79            if kind.is_some() {
80                return Err(Error::Duplicate { node: child });
81            }
82            if all.is_some() || !references.is_empty() || qualifiers.is_some() {
83                return Err(Error::Order { node: child });
84            }
85            let original = identifier::read(access, child)?;
86            if original.qualifier.is_some() {
87                return Err(Error::Forbidden {
88                    node: original.identifier,
89                });
90            }
91            kind = Some(original);
92        } else if named(access, child, "ObjectReference")? {
93            if kind.is_none() || qualifiers.is_some() {
94                return Err(Error::Order { node: child });
95            }
96            if all.is_some() {
97                return Err(Error::Unexpected { node: child });
98            }
99            properties::simple_content(access, child)?;
100            references
101                .try_push(child)
102                .map_err(|cause| Error::Resource { node: child, cause })?;
103        } else if named(access, child, "AllSignedDataObjects")? {
104            if kind.is_none() || qualifiers.is_some() {
105                return Err(Error::Order { node: child });
106            }
107            if all.is_some() {
108                return Err(Error::Duplicate { node: child });
109            }
110            if !references.is_empty() {
111                return Err(Error::Unexpected { node: child });
112            }
113            if let Some(nested) = access.children(child).next() {
114                return Err(Error::Unexpected {
115                    node: nested.map_err(Error::Child)?,
116                });
117            }
118            all = Some(child);
119        } else if named(access, child, "CommitmentTypeQualifiers")? {
120            if qualifiers.is_some() {
121                return Err(Error::Duplicate { node: child });
122            }
123            if kind.is_none() || (all.is_none() && references.is_empty()) {
124                return Err(Error::Order { node: child });
125            }
126            for qualifier in access.children(child) {
127                let qualifier = qualifier.map_err(Error::Child)?;
128                if !named(access, qualifier, "CommitmentTypeQualifier")? {
129                    return Err(Error::Unexpected { node: qualifier });
130                }
131            }
132            qualifiers = Some(child);
133        } else {
134            return Err(Error::Unexpected { node: child });
135        }
136    }
137    let kind = kind.ok_or(Error::Missing {
138        parent: node,
139        local: "CommitmentTypeId",
140    })?;
141    let objects = if let Some(all) = all {
142        Objects::All(all)
143    } else {
144        if references.is_empty() {
145            return Err(Error::Missing {
146                parent: node,
147                local: "ObjectReference or AllSignedDataObjects",
148            });
149        }
150        Objects::References(references.into_owned())
151    };
152    Ok(Reading {
153        snapshot: signed.snapshot(),
154        signature: signed.signature(),
155        node,
156        kind,
157        objects,
158        qualifiers,
159    })
160}
161
162/// Original instruction eligible for this commitment, not application acceptance.
163#[derive(Debug, Clone, Copy)]
164pub struct Object<N> {
165    /// Original authenticated Reference node.
166    pub reference: N,
167    /// Its original `SignedInfo` or authenticated Manifest parent.
168    pub parent: Parent<N>,
169}
170
171/// Original commitment and completed reference associations. This preserves
172/// source facts, not URI lexical validity or acceptance of the claimed meaning.
173pub struct Association<N, D, G> {
174    original: Reading<N, D, G>,
175    objects: OwnedVec<Object<N>>,
176}
177
178impl<N, D, G> Association<N, D, G> {
179    /// Original covered commitment structure.
180    #[must_use]
181    pub fn original(&self) -> &Reading<N, D, G> {
182        &self.original
183    }
184    /// Actual original eligible instructions in declared/catalogue order.
185    #[must_use]
186    pub fn objects(&self) -> &[Object<N>] {
187        &self.objects
188    }
189}
190
191/// Associated commitment using only this owner's stable identity domain; it
192/// does not retain the nominal XML adapter or a temporary document-view borrow.
193pub type Prepared<X> = Association<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
194
195/// Reads and associates a commitment through the same two kernels used by
196/// individual queries. Partial reference results are never returned on refusal.
197pub fn prepare<X: ReferenceXml, W: Work>(
198    access: &Access<'_, X, W>,
199    bound: &Bound<X::Id, X::Document, X::Generation>,
200    signed: &properties::Reading<X>,
201    index: usize,
202) -> Result<Prepared<X>, AssociationError<X::Error, X::Id>> {
203    let original = read(access, signed, index).map_err(AssociationError::Property)?;
204    let objects = associate(access, bound, &original)?;
205    Ok(Association { original, objects })
206}
207
208/// Association refusal preserving resource, processing and lookup distinctions.
209#[derive(Debug)]
210pub enum AssociationError<E, N> {
211    /// Original common source/schema/admission cause.
212    Property(Error<E, N>),
213    /// XML resolver/catalogue processing cause.
214    Processing(E),
215    /// Explicit original URI did not resolve to an authenticated instruction.
216    Lookup {
217        /// Original `ObjectReference` element.
218        node: N,
219        /// Exact lookup outcome.
220        resolution: Resolution<()>,
221    },
222    /// Referenced data was not checked, or it is `SignedProperties`.
223    Ineligible {
224        /// Original Reference node.
225        node: N,
226    },
227    /// Original catalogue was not completely processed.
228    Incomplete {
229        /// Original unprocessed instruction/container.
230        node: N,
231    },
232}
233
234/// Resolves explicit references or expands the original all-objects marker via
235/// the same authenticated catalogue. No hash, implicit acquisition, fabricated
236/// URI, re-verification or application-policy acceptance occurs here.
237pub fn associate<X: ReferenceXml, W: Work>(
238    access: &Access<'_, X, W>,
239    bound: &Bound<X::Id, X::Document, X::Generation>,
240    original: &Original<X>,
241) -> AssociationResult<OwnedVec<Object<X::Id>>, X> {
242    let property = AssociationError::Property;
243    bind_original(access, bound, original)?;
244    let mut output = access
245        .query(original.node, |_, _, work| Ok(work.vector()))
246        .map_err(|cause| property(Error::Access(cause)))?;
247    let is_properties = |r: crate::binding::Reference<'_, X::Id>| match r.target {
248        Some(target) => access
249            .equal_id(target, bound.containers().signed())
250            .map_err(|cause| {
251                property(Error::Resource {
252                    node: r.node,
253                    cause,
254                })
255            }),
256        None => Ok(false),
257    };
258    let mut retain = |descriptor: Descriptor<'_, X::Id>| {
259        let r = descriptor.reference;
260        if r.status != Status::Valid {
261            return Err(AssociationError::Ineligible { node: r.node });
262        }
263        output
264            .try_push(Object {
265                reference: r.node,
266                parent: descriptor.parent,
267            })
268            .map_err(|cause| {
269                property(Error::Resource {
270                    node: r.node,
271                    cause,
272                })
273            })
274    };
275    match &original.objects {
276        Objects::References(nodes) => {
277            for index in 0..nodes.len() {
278                admit(access, original.node).map_err(property)?;
279                let node = nodes[index];
280                let resolved = access
281                    .query(node, |xml, snapshot, work| {
282                        xml.resolve_reference(
283                            snapshot,
284                            original.signature,
285                            node,
286                            LexicalSource::SimpleContent,
287                            work,
288                            |descriptor| {
289                                let r = descriptor.reference;
290                                if r.status != Status::Valid || is_properties(r)? {
291                                    return Err(AssociationError::Ineligible { node: r.node });
292                                }
293                                retain(descriptor)
294                            },
295                        )
296                    })
297                    .map_err(|cause| property(Error::Access(cause)))?;
298                match resolved {
299                    Resolution::Resolved(value) => value?,
300                    other => {
301                        return Err(AssociationError::Lookup {
302                            node,
303                            resolution: other.map(|_| ()),
304                        });
305                    }
306                }
307            }
308        }
309        Objects::All(_) => {
310            let visited = access
311                .query(original.node, |xml, snapshot, work| {
312                    Ok(xml.visit_authenticated_references(
313                        snapshot,
314                        original.signature,
315                        work,
316                        |descriptor| {
317                            if is_properties(descriptor.reference)? {
318                                return Ok(());
319                            }
320                            retain(descriptor)
321                        },
322                    ))
323                })
324                .map_err(|cause| property(Error::Access(cause)))?;
325            let complete = match visited {
326                Ok(value) => value,
327                Err(VisitError::Processing(cause)) => {
328                    return Err(AssociationError::Processing(cause));
329                }
330                Err(VisitError::Visitor(cause)) => return Err(cause),
331            };
332            match complete {
333                Completeness::Complete => {}
334                Completeness::Unchecked(node) | Completeness::Incomplete(node) => {
335                    return Err(AssociationError::Incomplete { node });
336                }
337            }
338        }
339    }
340    Ok(output.into_owned())
341}
342
343fn bind_original<X: Xml, W: Work>(
344    access: &Access<'_, X, W>,
345    bound: &Bound<X::Id, X::Document, X::Generation>,
346    original: &Original<X>,
347) -> AssociationResult<(), X> {
348    let property = AssociationError::Property;
349    if access.snapshot() != original.snapshot || bound.containers().snapshot() != original.snapshot
350    {
351        return Err(property(Error::Access(AccessError::Changed {
352            node: original.node,
353        })));
354    }
355    if !access
356        .equal_id(bound.containers().signature(), original.signature)
357        .map_err(|cause| {
358            property(Error::Resource {
359                node: original.node,
360                cause,
361            })
362        })?
363    {
364        return Err(property(Error::Access(AccessError::Changed {
365            node: original.node,
366        })));
367    }
368    Ok(())
369}
370
371/// Association result preserving this owner's processing and original node types.
372pub type AssociationResult<T, X> = Result<T, AssociationError<<X as Xml>::Error, <X as Xml>::Id>>;
373
374fn selected<X: Xml, W: Work>(
375    access: &Access<'_, X, W>,
376    signed: &properties::Reading<X>,
377    index: usize,
378) -> PropertyResult<X::Id, X> {
379    if access.snapshot() != signed.snapshot() {
380        return Err(Error::Access(AccessError::Changed {
381            node: signed.parent(),
382        }));
383    }
384    admit(access, signed.parent())?;
385    let selection = || Error::Selection {
386        parent: signed.parent(),
387        index,
388    };
389    let entry = signed
390        .data_objects()
391        .and_then(|objects| objects.entries().get(index))
392        .ok_or_else(selection)?;
393    if entry.role != DataObjectRole::Commitment {
394        return Err(selection());
395    }
396    Ok(entry.node)
397}
398
399fn admit<X: Xml, W: Work>(access: &Access<'_, X, W>, node: X::Id) -> PropertyResult<(), X> {
400    access
401        .query(node, |_, _, work| Ok(work.charge_work(1)))
402        .map_err(Error::Access)?
403        .map_err(|cause| Error::Resource { node, cause })
404}
405
406fn named<X: Xml, W: Work>(
407    access: &Access<'_, X, W>,
408    node: X::Id,
409    local: &str,
410) -> PropertyResult<bool, X> {
411    access
412        .has_name(
413            node,
414            Name {
415                namespace: XADES,
416                local,
417            },
418        )
419        .map_err(Error::Access)
420}