Skip to main content

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}