Skip to main content

structured_xades/
binding.rs

1//! Selected-signature binding, separate from property validation and path trust.
2
3use crate::container::{Containers, Layout};
4use crate::xml::{Access, AccessError, Snapshot, Xml};
5use structured_asn1::memory::Work;
6
7/// Required Type of the `SignedInfo` reference protecting `SignedProperties`.
8pub const SIGNED_PROPERTIES: &str = "http://uri.etsi.org/01903#SignedProperties";
9
10/// `XMLDSig` outcome for this exact source generation and selected signature.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Status {
13    /// Complete `XMLDSig` processing and required reference checks succeeded.
14    Valid,
15    /// Completed processing rejected the signature or reference.
16    Invalid,
17    /// This signature or reference was not checked.
18    Unchecked,
19}
20
21/// Conservative original-element coverage from the XML owner's transform engine.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Coverage {
24    /// Expanded names, attributes and element content are authenticated.
25    ElementContent,
26    /// Arbitrary transformed bytes do not prove complete original-element coverage.
27    Transformed,
28}
29
30/// One callback-local verified `SignedInfo` reference from the selected signature.
31#[derive(Debug, Clone, Copy)]
32pub struct Reference<'a, N> {
33    /// Original Reference node identity, distinct from its resolved target.
34    pub node: N,
35    /// Same-document original target, absent for external or unresolved resources.
36    pub target: Option<N>,
37    /// Original normalized Type attribute, not inferred from the target name.
38    pub kind: Option<&'a str>,
39    /// Completed reference digest outcome for this exact source generation.
40    pub status: Status,
41    /// Coverage certified by the actual transform chain.
42    pub coverage: Coverage,
43}
44
45/// Resolution of the enveloped `QualifyingProperties` Target attribute.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Target<N> {
48    /// Empty non-fragment part and a bare-name fragment resolving to this node.
49    LocalBareName(N),
50    /// Required Target attribute is absent.
51    Missing,
52    /// Another URI form, external target or unresolved identifier.
53    Other,
54}
55
56/// Read-only access to actual `XMLDSig` evidence, never an untrusted XML assertion.
57///
58/// The adapter checks original identity/generation and maps only the chosen
59/// signature's evidence. It must not substitute another valid signature, a
60/// Manifest reference or a URI string for authenticated original-node coverage.
61/// Every data-dependent lookup is admitted through current Work and XML policy.
62/// These queries do not repeat signature verification or perform implicit I/O.
63pub trait SignatureXml: Xml {
64    /// Returns the exact selected signature's completed `XMLDSig` outcome.
65    /// Path trust, trusted lists and application acceptance are separate facts.
66    fn signature_status<W: Work>(
67        &self,
68        snapshot: Snapshot<Self::Document, Self::Generation>,
69        signature: Self::Id,
70        execution: &W,
71    ) -> Result<Status, Self::Error>;
72
73    /// Counts only the chosen signature's direct `SignedInfo` references.
74    fn reference_count<W: Work>(
75        &self,
76        snapshot: Snapshot<Self::Document, Self::Generation>,
77        signature: Self::Id,
78        execution: &W,
79    ) -> Result<usize, Self::Error>;
80
81    /// Inspects one original `SignedInfo` reference in ordinal order.
82    fn reference<R, W: Work>(
83        &self,
84        snapshot: Snapshot<Self::Document, Self::Generation>,
85        signature: Self::Id,
86        ordinal: usize,
87        execution: &W,
88        visitor: impl for<'view> FnOnce(Reference<'view, Self::Id>) -> R,
89    ) -> Result<R, Self::Error>;
90
91    /// Resolves Target using the XML owner's URI/ID kernel, without acquisition.
92    /// URI syntax, namespace-independent attribute access and ID uniqueness are
93    /// checked by the owner; resource/processing errors must not become Other.
94    fn qualifying_target<W: Work>(
95        &self,
96        snapshot: Snapshot<Self::Document, Self::Generation>,
97        qualifying: Self::Id,
98        execution: &W,
99    ) -> Result<Target<Self::Id>, Self::Error>;
100}
101
102/// Binding failed independently of a baseline property or path-trust decision.
103#[derive(Debug)]
104pub enum BindingError<E, N> {
105    /// Original XML/resource/generation failure, never a signature mismatch.
106    Access(AccessError<E, N>),
107    /// The exact selected signature has no successful `XMLDSig` outcome.
108    Signature {
109        /// Original selected signature.
110        node: N,
111        /// Completed or unchecked outcome.
112        status: Status,
113    },
114    /// Target does not identify the selected Signature by local bare-name URI.
115    Target {
116        /// Original `QualifyingProperties` container.
117        node: N,
118    },
119    /// No authenticated reference protects the original `SignedProperties`.
120    Missing {
121        /// Original `SignedProperties`.
122        node: N,
123    },
124    /// Matching target has wrong Type, digest outcome or incomplete coverage.
125    Reference {
126        /// Original Reference node.
127        node: N,
128    },
129    /// Baseline signature has fewer than two `SignedInfo` references.
130    References {
131        /// Original Signature.
132        node: N,
133    },
134}
135
136/// Authenticated original `SignedProperties` for one selected signature.
137///
138/// This proves only `XMLDSig` binding. It does not certify property semantics,
139/// signing-certificate identity, timestamps, attained baseline level or trust.
140#[derive(Debug)]
141pub struct Bound<N, D, G> {
142    containers: Containers<N, D, G>,
143    reference: N,
144    ordinal: usize,
145}
146
147impl<N: Copy, D: Copy, G: Copy> Bound<N, D, G> {
148    /// Original, generation-bound container identities.
149    #[must_use]
150    pub fn containers(&self) -> &Containers<N, D, G> {
151        &self.containers
152    }
153    /// Authenticated original Reference, never another signature's reference.
154    #[must_use]
155    pub fn reference(&self) -> N {
156        self.reference
157    }
158    /// Its original ordinal in the selected `SignedInfo`.
159    #[must_use]
160    pub fn ordinal(&self) -> usize {
161        self.ordinal
162    }
163}
164
165/// A selected binding preserving the XML adapter's original identity types.
166pub type Selected<X> = Bound<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
167/// A binding result with the adapter's opaque original error and node provenance.
168pub type BindingResult<T, X> = Result<T, BindingError<<X as Xml>::Error, <X as Xml>::Id>>;
169
170#[derive(Clone, Copy)]
171pub(crate) enum Purpose<N> {
172    Properties(N),
173    Counter(N),
174}
175
176pub(crate) fn covering_reference<X: SignatureXml, W: Work>(
177    access: &Access<'_, X, W>,
178    signature: X::Id,
179    purpose: Purpose<X::Id>,
180) -> BindingResult<(X::Id, usize), X> {
181    let status = access
182        .query(signature, |xml, snapshot, work| {
183            xml.signature_status(snapshot, signature, work)
184        })
185        .map_err(BindingError::Access)?;
186    if status != Status::Valid {
187        return Err(BindingError::Signature {
188            node: signature,
189            status,
190        });
191    }
192    let count = access
193        .query(signature, |xml, snapshot, work| {
194            xml.reference_count(snapshot, signature, work)
195        })
196        .map_err(BindingError::Access)?;
197    let (wanted, minimum) = match purpose {
198        Purpose::Properties(target) => (target, 2),
199        Purpose::Counter(target) => (target, 1),
200    };
201    if count < minimum {
202        return Err(BindingError::References { node: signature });
203    }
204    let mut selected = None;
205    let mut uncovered = None;
206    for ordinal in 0..count {
207        let (node, target, status, coverage, eligible) = access
208            .query(signature, |xml, snapshot, work| {
209                xml.reference(snapshot, signature, ordinal, work, |r| {
210                    let eligible = match purpose {
211                        Purpose::Properties(_) => match r.kind {
212                            Some(kind) => access.equal(kind, SIGNED_PROPERTIES),
213                            None => Ok(false),
214                        },
215                        // EN319132-1 §5.2.7.2 requires coverage of the original
216                        // SignatureValue. It does not mandate a Type attribute for
217                        // this enveloped form or turn it into SignedProperties.
218                        // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
219                        Purpose::Counter(_) => Ok(true),
220                    };
221                    (r.node, r.target, r.status, r.coverage, eligible)
222                })
223            })
224            .map_err(BindingError::Access)?;
225        let eligible = eligible
226            .map_err(|cause| BindingError::Access(AccessError::Resource { node, cause }))?;
227        if !eligible {
228            continue;
229        }
230        let matches = match target {
231            Some(target) => access
232                .equal_id(target, wanted)
233                .map_err(|cause| BindingError::Access(AccessError::Resource { node, cause }))?,
234            None => false,
235        };
236        if !matches {
237            if let Purpose::Counter(_) = purpose {
238                continue;
239            }
240            return Err(BindingError::Reference { node });
241        }
242        if status != Status::Valid {
243            return Err(BindingError::Reference { node });
244        }
245        if coverage != Coverage::ElementContent {
246            uncovered = Some(node);
247            continue;
248        }
249        if selected.is_none() {
250            selected = Some((node, ordinal));
251        }
252    }
253    if let Some(value) = selected {
254        return Ok(value);
255    }
256    if let Some(node) = uncovered {
257        return Err(BindingError::Reference { node });
258    }
259    Err(BindingError::Missing { node: wanted })
260}
261
262/// Verifies binding to the original selected signature before property execution.
263pub fn bind<X: SignatureXml, W: Work>(
264    access: &Access<'_, X, W>,
265    layout: &Layout<X>,
266) -> BindingResult<Selected<X>, X> {
267    check_target(access, layout)?;
268    let (reference, ordinal) = covering_reference(
269        access,
270        layout.signature(),
271        Purpose::Properties(layout.signed()),
272    )?;
273    Ok(Bound {
274        containers: *layout,
275        reference,
276        ordinal,
277    })
278}
279
280// EN319132-1 V1.3.1 4.3.1 applies to unsigned as well as signed qualifying
281// properties. A checked counter does not associate an unrelated Target with
282// itself. Share this source/URI guard without requiring a plain XMLDSig child
283// to supply the root baseline header or a SignedProperties reference.
284// https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
285pub(crate) fn check_target<X: SignatureXml, W: Work>(
286    access: &Access<'_, X, W>,
287    layout: &Layout<X>,
288) -> BindingResult<(), X> {
289    let signature = layout.signature();
290    if access.snapshot() != layout.snapshot() {
291        return Err(BindingError::Access(AccessError::Changed {
292            node: signature,
293        }));
294    }
295    // ETSI EN 319 132-1 V1.3.1 §§4.3.1/4.4.2 and §6.3 table 2.
296    // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
297    let target = access
298        .query(layout.qualifying(), |xml, snapshot, work| {
299            xml.qualifying_target(snapshot, layout.qualifying(), work)
300        })
301        .map_err(BindingError::Access)?;
302    let matched = match target {
303        Target::LocalBareName(target) => access.equal_id(target, signature).map_err(|cause| {
304            BindingError::Access(AccessError::Resource {
305                node: layout.qualifying(),
306                cause,
307            })
308        })?,
309        Target::Missing | Target::Other => false,
310    };
311    if !matched {
312        return Err(BindingError::Target {
313            node: layout.qualifying(),
314        });
315    }
316    Ok(())
317}