Skip to main content

structured_xades/
header.rs

1//! Baseline `XMLDSig` header requirements, separate from selected-key/path trust.
2
3use crate::binary::{self, BinaryXml, Location};
4use crate::binding::Bound;
5use crate::container::DS;
6use crate::properties::{Error, PropertyResult};
7use crate::xml::{Access, AccessError, Name, Snapshot, Xml};
8use structured_asn1::memory::{MemoryError, OwnedVec, Work};
9use structured_pkix::Certificate;
10
11/// Original mandatory header roles; presence alone is not certificate binding.
12#[derive(Debug)]
13pub struct Header<N, D, G> {
14    snapshot: Snapshot<D, G>,
15    signature: N,
16    key_info: N,
17    x509_data: N,
18    method: N,
19    certificates: OwnedVec<N>,
20}
21
22impl<N: Copy, D: Copy, G: Copy> Header<N, D, G> {
23    /// Original XML identity and immutable generation.
24    #[must_use]
25    pub fn snapshot(&self) -> Snapshot<D, G> {
26        self.snapshot
27    }
28    /// Exact selected Signature.
29    #[must_use]
30    pub fn signature(&self) -> N {
31        self.signature
32    }
33    /// Original mandatory `KeyInfo`.
34    #[must_use]
35    pub fn key_info(&self) -> N {
36        self.key_info
37    }
38    /// The one baseline `X509Data` container.
39    #[must_use]
40    pub fn x509_data(&self) -> N {
41        self.x509_data
42    }
43    /// Original `SignedInfo` canonicalization method, retaining parameters.
44    #[must_use]
45    pub fn method(&self) -> N {
46        self.method
47    }
48    /// Original certificate value nodes in source order.
49    #[must_use]
50    pub fn certificates(&self) -> &[N] {
51        &self.certificates
52    }
53}
54
55/// Header using the XML owner's original identity types.
56pub type Reading<X> = Header<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
57
58/// The two native value fields in XMLDSig1.1 `X509Data` (not draft extensions).
59#[derive(Clone, Copy, PartialEq, Eq)]
60pub(crate) enum ValueKind {
61    Certificate,
62    Crl,
63}
64impl ValueKind {
65    pub(crate) fn local(self) -> &'static str {
66        match self {
67            Self::Certificate => "X509Certificate",
68            Self::Crl => "X509CRL",
69        }
70    }
71}
72
73// Shared with plain counters: discovery does not impose B-B KeyInfo/X509Data
74// cardinalities or claim that these untrusted values authenticate the signer.
75type ValueVisitResult<X, E> =
76    Result<(), crate::reference::VisitError<Error<<X as Xml>::Error, <X as Xml>::Id>, E>>;
77
78pub(crate) fn visit_values<X: Xml, W: Work, VE>(
79    access: &Access<'_, X, W>,
80    data: X::Id,
81    mut visitor: impl FnMut(X::Id, ValueKind) -> Result<(), VE>,
82) -> ValueVisitResult<X, VE> {
83    use crate::reference::VisitError;
84    for child in access.children(data) {
85        let node = child.map_err(|cause| VisitError::Processing(Error::Child(cause)))?;
86        for kind in [ValueKind::Certificate, ValueKind::Crl] {
87            if access
88                .has_name(
89                    node,
90                    Name {
91                        namespace: DS,
92                        local: kind.local(),
93                    },
94                )
95                .map_err(|cause| VisitError::Processing(Error::Access(cause)))?
96            {
97                visitor(node, kind).map_err(VisitError::Visitor)?;
98                break;
99            }
100        }
101    }
102    Ok(())
103}
104
105/// Reads mandatory header roles under the already selected authenticated source.
106/// EN319132-1 ยง6.3 table2 requires one KeyInfo/X509Data and its requirement(a)
107/// requires the signing certificate within `X509Certificate`; [`included`] checks
108/// the latter against the actual candidate, rather than inferring it from count.
109/// Requirement(d)'s six canonicalization identities are format constraints;
110/// parameter validation and stricter compiled algorithm policy stay XML-owned.
111/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
112pub fn read<X: Xml, W: Work>(
113    access: &Access<'_, X, W>,
114    bound: &Bound<X::Id, X::Document, X::Generation>,
115) -> PropertyResult<Reading<X>, X> {
116    let layout = bound.containers();
117    if access.snapshot() != layout.snapshot() {
118        return Err(Error::Access(AccessError::Changed {
119            node: layout.signature(),
120        }));
121    }
122    let signature = layout.signature();
123    let key_info = required(access, signature, "KeyInfo")?;
124    let x509_data = required(access, key_info, "X509Data")?;
125    let signed_info = required(access, signature, "SignedInfo")?;
126    let method = required(access, signed_info, "CanonicalizationMethod")?;
127    let allowed = access
128        .attribute(
129            method,
130            Name {
131                namespace: "",
132                local: "Algorithm",
133            },
134            |value| {
135                let Some(value) = value else {
136                    return Ok(false);
137                };
138                let value = access.trim_xml_whitespace(value)?;
139                for algorithm in [
140                    "http://www.w3.org/2006/12/xml-c14n11",
141                    "http://www.w3.org/2001/10/xml-exc-c14n#",
142                    "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
143                    "http://www.w3.org/2006/12/xml-c14n11#WithComments",
144                    "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
145                    "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments",
146                ] {
147                    if access.equal(value, algorithm)? {
148                        return Ok(true);
149                    }
150                }
151                Ok(false)
152            },
153        )
154        .map_err(Error::Access)?
155        .map_err(|cause| Error::Resource {
156            node: method,
157            cause,
158        })?;
159    if !allowed {
160        return Err(Error::Unexpected { node: method });
161    }
162    let mut certificates = access
163        .query(x509_data, |_, _, work| Ok(work.vector()))
164        .map_err(Error::Access)?;
165    visit_values(access, x509_data, |node, kind| {
166        if kind == ValueKind::Certificate {
167            certificates
168                .try_push(node)
169                .map_err(|cause| Error::Resource { node, cause })?;
170        }
171        Ok(())
172    })
173    .map_err(|cause| match cause {
174        crate::reference::VisitError::Processing(cause)
175        | crate::reference::VisitError::Visitor(cause) => cause,
176    })?;
177    if certificates.is_empty() {
178        return Err(Error::Missing {
179            parent: x509_data,
180            local: "X509Certificate",
181        });
182    }
183    Ok(Header {
184        snapshot: layout.snapshot(),
185        signature,
186        key_info,
187        x509_data,
188        method,
189        certificates: certificates.into_owned(),
190    })
191}
192
193fn required<X: Xml, W: Work>(
194    access: &Access<'_, X, W>,
195    parent: X::Id,
196    local: &'static str,
197) -> PropertyResult<X::Id, X> {
198    let mut found = None;
199    for child in access.children(parent) {
200        let node = child.map_err(Error::Child)?;
201        if access
202            .has_name(
203                node,
204                Name {
205                    namespace: DS,
206                    local,
207                },
208            )
209            .map_err(Error::Access)?
210        {
211            if found.is_some() {
212                return Err(Error::Duplicate { node });
213            }
214            found = Some(node);
215        }
216    }
217    found.ok_or(Error::Missing { parent, local })
218}
219
220/// Original equality-scan admission refusal; never a certificate mismatch.
221#[derive(Debug)]
222pub struct InclusionError(pub MemoryError);
223
224/// Original included-certificate location using the XML owner's identities.
225pub type Inclusion<X> =
226    Option<Location<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>>;
227
228/// Finds the actual candidate's complete original DER in the selected `KeyInfo`.
229/// No hash, re-encoding, key hint or certificate count substitutes for equality.
230/// XML base64 decoding stays with the bounded XML owner. This checks presence,
231/// not a path, revocation result, or association with the selected signature key.
232pub fn included<X: BinaryXml, W: Work>(
233    xml: &X,
234    header: &Reading<X>,
235    certificate: &Certificate<'_>,
236    execution: &W,
237) -> binary::BinaryResult<Inclusion<X>, X, InclusionError> {
238    let access = Access::new(xml, header.snapshot, execution);
239    for index in 0..header.certificates.len() {
240        let location = Location {
241            snapshot: header.snapshot,
242            node: header.key_info,
243        };
244        access
245            .query(header.key_info, |_, _, work| Ok(work.charge_work(1)))
246            .map_err(|cause| binary::Error::Access { location, cause })?
247            .map_err(|cause| binary::Error::Callback {
248                location,
249                cause: InclusionError(cause),
250            })?;
251        let location = Location {
252            snapshot: header.snapshot,
253            node: header.certificates[index],
254        };
255        let matches = binary::with_octets(xml, location, execution, |bytes| {
256            if bytes.len() != certificate.as_bytes().len() {
257                return Ok(false);
258            }
259            let length = u64::try_from(bytes.len())
260                .map_err(|_| InclusionError(MemoryError::WorkExceeded))?;
261            execution.charge_work(length).map_err(InclusionError)?;
262            Ok(bytes == certificate.as_bytes())
263        })?;
264        if matches {
265            return Ok(Some(location));
266        }
267    }
268    Ok(None)
269}