structured_xades/container.rs
1//! Original direct-incorporation containers of a selected XML signature.
2
3use crate::xml::{Access, AccessError, ChildError, Name, Snapshot, Xml};
4use structured_asn1::memory::Work;
5
6/// `XMLDSig` namespace; the prefix used in the source is irrelevant.
7pub const DS: &str = "http://www.w3.org/2000/09/xmldsig#";
8/// Namespace containing the qualifying-property containers.
9pub const XADES: &str = "http://uri.etsi.org/01903/v1.3.2#";
10
11/// Why a selected signature's direct containers could not be located.
12#[derive(Debug)]
13pub enum ContainerError<E, N> {
14 /// Original XML access or execution failure.
15 Access(AccessError<E, N>),
16 /// Element-only container traversal failed, with its original location.
17 Child(ChildError<E, N>),
18 /// An element has no role in the selected container's schema.
19 Unexpected {
20 /// Original element identity.
21 node: N,
22 },
23 /// A required container is absent.
24 Missing {
25 /// Parent in whose direct child list the container was required.
26 parent: N,
27 /// Missing container's local name.
28 local: &'static str,
29 },
30 /// More than one original container occupies the same schema role.
31 Duplicate {
32 /// Repeated container's original identity.
33 node: N,
34 },
35 /// `SignedProperties` appears after `UnsignedProperties`.
36 Order {
37 /// Out-of-order original container.
38 node: N,
39 },
40 /// A required property container has no element children.
41 Empty {
42 /// Original empty container.
43 node: N,
44 },
45}
46
47/// Original container identities, before reference coverage or property checks.
48///
49/// This is structural inspection, not authenticated signature evidence or an
50/// attained baseline level. Construction is restricted to [`locate`]. Target,
51/// `SignedProperties` reference coverage, property syntax and mathematical checks
52/// are separate obligations; mere presence never proves them.
53#[derive(Debug, Clone, Copy)]
54pub struct Containers<N, D, G> {
55 snapshot: Snapshot<D, G>,
56 signature: N,
57 object: N,
58 qualifying: N,
59 signed: N,
60 unsigned: Option<N>,
61}
62
63impl<N: Copy, D: Copy, G: Copy> Containers<N, D, G> {
64 /// The exact original source under which these nodes were located.
65 #[must_use]
66 pub fn snapshot(&self) -> Snapshot<D, G> {
67 self.snapshot
68 }
69 /// The selected original Signature, not another signature in the document.
70 #[must_use]
71 pub fn signature(&self) -> N {
72 self.signature
73 }
74 /// The single ds:Object holding the directly incorporated properties.
75 #[must_use]
76 pub fn object(&self) -> N {
77 self.object
78 }
79 /// The original `QualifyingProperties` container.
80 #[must_use]
81 pub fn qualifying(&self) -> N {
82 self.qualifying
83 }
84 /// The original `SignedProperties` container, before coverage verification.
85 #[must_use]
86 pub fn signed(&self) -> N {
87 self.signed
88 }
89 /// The original optional `UnsignedProperties` container.
90 #[must_use]
91 pub fn unsigned(&self) -> Option<N> {
92 self.unsigned
93 }
94}
95
96/// Locates the direct containers under exactly the selected Signature.
97///
98/// Other ds:Object payloads may be mixed-content `XMLDSig` objects and are not
99/// reinterpreted as `XAdES` property containers. No recursive descendant search
100/// may substitute a nested or another signature's properties. All fan-out is
101/// iterative and admitted in the current execution before access.
102pub fn locate<X: Xml, W: Work>(
103 access: &Access<'_, X, W>,
104 signature: X::Id,
105) -> ContainerResult<Layout<X>, X> {
106 // EN 319 132-1 V1.3.1 §§4.4.1–4.4.2: direct incorporation within one Object,
107 // at most one QualifyingProperties and all signed properties in one container.
108 // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
109 if !named(access, signature, DS, "Signature")? {
110 return Err(ContainerError::Unexpected { node: signature });
111 }
112 let mut selected = None;
113 for child in access.children(signature) {
114 let object = child.map_err(ContainerError::Child)?;
115 if !named(access, object, DS, "Object")? {
116 continue;
117 }
118 // ds:Object is mixed content. Unlike property containers, arbitrary
119 // character data and other application payloads are permitted here.
120 let mut cursor = access.first_child(object).map_err(ContainerError::Access)?;
121 while let Some(node) = cursor {
122 // EN319132-1 §6.3 requires ONLY direct incorporation for all four
123 // current baseline levels (note1 excludes QualifyingPropertiesReference).
124 // A valid direct container cannot hide an indirect sibling in this
125 // or another Object. Arbitrary nested application payload is not
126 // searched or reinterpreted as a protocol property container.
127 // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
128 if named(access, node, XADES, "QualifyingPropertiesReference")? {
129 return Err(ContainerError::Unexpected { node });
130 }
131 if named(access, node, XADES, "QualifyingProperties")? {
132 if selected.is_some() {
133 return Err(ContainerError::Duplicate { node });
134 }
135 selected = Some((object, node));
136 }
137 cursor = access
138 .next_sibling(object, node)
139 .map_err(ContainerError::Access)?;
140 }
141 }
142 let Some((object, qualifying)) = selected else {
143 return Err(ContainerError::Missing {
144 parent: signature,
145 local: "QualifyingProperties",
146 });
147 };
148 let mut signed = None;
149 let mut unsigned = None;
150 for child in access.children(qualifying) {
151 let node = child.map_err(ContainerError::Child)?;
152 if named(access, node, XADES, "SignedProperties")? {
153 if signed.is_some() {
154 return Err(ContainerError::Duplicate { node });
155 }
156 if unsigned.is_some() {
157 return Err(ContainerError::Order { node });
158 }
159 signed = Some(node);
160 } else if named(access, node, XADES, "UnsignedProperties")? {
161 if unsigned.is_some() {
162 return Err(ContainerError::Duplicate { node });
163 }
164 unsigned = Some(node);
165 } else {
166 return Err(ContainerError::Unexpected { node });
167 }
168 }
169 let Some(signed) = signed else {
170 return Err(ContainerError::Missing {
171 parent: qualifying,
172 local: "SignedProperties",
173 });
174 };
175 nonempty(access, signed)?;
176 if let Some(unsigned) = unsigned {
177 nonempty(access, unsigned)?;
178 }
179 Ok(Containers {
180 snapshot: access.snapshot(),
181 signature,
182 object,
183 qualifying,
184 signed,
185 unsigned,
186 })
187}
188
189/// Original container layout carrying the selected adapter's source identities.
190pub type Layout<X> = Containers<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
191
192/// A structural result retaining the original XML cause and node location.
193pub type ContainerResult<T, X> = Result<T, ContainerError<<X as Xml>::Error, <X as Xml>::Id>>;
194
195fn named<X: Xml, W: Work>(
196 access: &Access<'_, X, W>,
197 node: X::Id,
198 namespace: &str,
199 local: &str,
200) -> ContainerResult<bool, X> {
201 access
202 .has_name(node, Name { namespace, local })
203 .map_err(ContainerError::Access)
204}
205
206fn nonempty<X: Xml, W: Work>(access: &Access<'_, X, W>, node: X::Id) -> ContainerResult<(), X> {
207 // EN 319 132-1 §§4.3.2–4.3.3 forbid empty property containers.
208 match access.children(node).next() {
209 Some(Ok(_)) => Ok(()),
210 Some(Err(error)) => Err(ContainerError::Child(error)),
211 None => Err(ContainerError::Empty { node }),
212 }
213}