Skip to main content

xml_sec/xmldsig/
builder.rs

1//! Builders for deterministic XMLDSig signature templates.
2
3use std::io::Write;
4
5use quick_xml::Writer;
6use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
7
8use crate::c14n::{C14nAlgorithm, C14nMode};
9
10use super::{
11    DigestAlgorithm, ENVELOPED_SIGNATURE_URI, SignatureAlgorithm, Transform, XPATH_TRANSFORM_URI,
12};
13
14const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
15const EXCLUSIVE_C14N_NS: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
16const XPATH_EXCLUDE_ALL_SIGNATURES: &str = "not(ancestor-or-self::dsig:Signature)";
17
18/// Errors produced while validating or serializing an XMLDSig template.
19#[derive(Debug, thiserror::Error)]
20pub enum SignatureBuilderError {
21    /// A namespace prefix was not a supported XML NCName.
22    #[error("invalid XML namespace prefix: {0}")]
23    InvalidNamespacePrefix(String),
24    /// An XMLDSig Id attribute was not a valid XML NCName.
25    #[error("invalid {element} Id: {value}")]
26    InvalidId {
27        /// XMLDSig element carrying the Id attribute.
28        element: &'static str,
29        /// Rejected attribute value.
30        value: String,
31    },
32    /// XMLDSig requires at least one reference in SignedInfo.
33    #[error("a signature template requires at least one Reference")]
34    MissingReference,
35    /// SHA-1 algorithms are available for verification but not new signatures.
36    #[error("algorithm is not allowed for signing: {0}")]
37    SigningAlgorithmDisabled(&'static str),
38    /// The XML writer failed.
39    #[error("XML serialization error: {0}")]
40    Serialization(#[from] std::io::Error),
41    /// The writer unexpectedly emitted bytes that are not UTF-8.
42    #[error("XML writer emitted invalid UTF-8: {0}")]
43    InvalidUtf8(#[from] std::string::FromUtf8Error),
44}
45
46/// Builder for a single XMLDSig `<Reference>` template.
47#[derive(Debug, Clone)]
48pub struct ReferenceBuilder {
49    uri: Option<String>,
50    id: Option<String>,
51    ref_type: Option<String>,
52    transforms: Vec<Transform>,
53    digest_method: DigestAlgorithm,
54}
55
56impl ReferenceBuilder {
57    /// Create a reference using the required digest algorithm.
58    #[must_use]
59    pub fn new(digest_method: DigestAlgorithm) -> Self {
60        Self {
61            uri: None,
62            id: None,
63            ref_type: None,
64            transforms: Vec::new(),
65            digest_method,
66        }
67    }
68
69    /// Set the optional reference URI.
70    #[must_use]
71    pub fn uri(mut self, uri: impl Into<String>) -> Self {
72        self.uri = Some(uri.into());
73        self
74    }
75
76    /// Set the optional reference Id.
77    #[must_use]
78    pub fn id(mut self, id: impl Into<String>) -> Self {
79        self.id = Some(id.into());
80        self
81    }
82
83    /// Set the optional reference Type URI.
84    #[must_use]
85    pub fn ref_type(mut self, ref_type: impl Into<String>) -> Self {
86        self.ref_type = Some(ref_type.into());
87        self
88    }
89
90    /// Append a transform, preserving insertion order.
91    #[must_use]
92    pub fn transform(mut self, transform: Transform) -> Self {
93        self.transforms.push(transform);
94        self
95    }
96}
97
98/// Builder for a complete XMLDSig `<Signature>` template.
99#[derive(Debug, Clone)]
100pub struct SignatureBuilder {
101    c14n_method: C14nAlgorithm,
102    sign_method: SignatureAlgorithm,
103    ns_prefix: Option<String>,
104    signature_id: Option<String>,
105    references: Vec<ReferenceBuilder>,
106    include_key_info: bool,
107}
108
109impl SignatureBuilder {
110    /// Create a signature template using the required algorithms.
111    #[must_use]
112    pub fn new(c14n_method: C14nAlgorithm, sign_method: SignatureAlgorithm) -> Self {
113        Self {
114            c14n_method,
115            sign_method,
116            ns_prefix: None,
117            signature_id: None,
118            references: Vec::new(),
119            include_key_info: false,
120        }
121    }
122
123    /// Use a namespace prefix such as `ds`; the default is an unprefixed namespace.
124    #[must_use]
125    pub fn ns_prefix(mut self, prefix: impl Into<String>) -> Self {
126        self.ns_prefix = Some(prefix.into());
127        self
128    }
129
130    /// Set the optional Signature Id.
131    #[must_use]
132    pub fn signature_id(mut self, id: impl Into<String>) -> Self {
133        self.signature_id = Some(id.into());
134        self
135    }
136
137    /// Append a reference, preserving insertion order.
138    #[must_use]
139    pub fn add_reference(mut self, reference: ReferenceBuilder) -> Self {
140        self.references.push(reference);
141        self
142    }
143
144    /// Control whether an empty KeyInfo placeholder is emitted.
145    #[must_use]
146    pub fn key_info(mut self, include: bool) -> Self {
147        self.include_key_info = include;
148        self
149    }
150
151    /// Build a namespace-correct XMLDSig template with empty digest and signature values.
152    pub fn build_template(&self) -> Result<String, SignatureBuilderError> {
153        self.validate()?;
154
155        let prefix = self.ns_prefix.as_deref();
156        let mut writer = Writer::new(Vec::new());
157        let signature_name = qualified_name(prefix, "Signature");
158        let mut signature = BytesStart::new(&signature_name);
159        let namespace_attr = prefix.map_or_else(|| "xmlns".to_owned(), |p| format!("xmlns:{p}"));
160        signature.push_attribute((namespace_attr.as_str(), XMLDSIG_NS));
161        if let Some(id) = &self.signature_id {
162            signature.push_attribute(("Id", id.as_str()));
163        }
164        writer.write_event(Event::Start(signature))?;
165
166        write_start(&mut writer, prefix, "SignedInfo")?;
167        write_algorithm(
168            &mut writer,
169            prefix,
170            "CanonicalizationMethod",
171            self.c14n_method.uri(),
172        )?;
173        write_algorithm(
174            &mut writer,
175            prefix,
176            "SignatureMethod",
177            self.sign_method.uri(),
178        )?;
179        for reference in &self.references {
180            write_reference(&mut writer, prefix, reference)?;
181        }
182        write_end(&mut writer, prefix, "SignedInfo")?;
183        write_empty(&mut writer, prefix, "SignatureValue")?;
184        if self.include_key_info {
185            write_empty(&mut writer, prefix, "KeyInfo")?;
186        }
187        writer.write_event(Event::End(BytesEnd::new(signature_name)))?;
188
189        Ok(String::from_utf8(writer.into_inner())?)
190    }
191
192    fn validate(&self) -> Result<(), SignatureBuilderError> {
193        if let Some(prefix) = &self.ns_prefix
194            && !is_namespace_prefix(prefix)
195        {
196            return Err(SignatureBuilderError::InvalidNamespacePrefix(
197                prefix.clone(),
198            ));
199        }
200        if let Some(id) = &self.signature_id
201            && !is_ncname(id)
202        {
203            return Err(SignatureBuilderError::InvalidId {
204                element: "Signature",
205                value: id.clone(),
206            });
207        }
208        if self.references.is_empty() {
209            return Err(SignatureBuilderError::MissingReference);
210        }
211        if !self.sign_method.signing_allowed() {
212            return Err(SignatureBuilderError::SigningAlgorithmDisabled(
213                self.sign_method.uri(),
214            ));
215        }
216        for reference in &self.references {
217            if let Some(id) = &reference.id
218                && !is_ncname(id)
219            {
220                return Err(SignatureBuilderError::InvalidId {
221                    element: "Reference",
222                    value: id.clone(),
223                });
224            }
225            if !reference.digest_method.signing_allowed() {
226                return Err(SignatureBuilderError::SigningAlgorithmDisabled(
227                    reference.digest_method.uri(),
228                ));
229            }
230        }
231        Ok(())
232    }
233}
234
235fn write_reference<W: Write>(
236    writer: &mut Writer<W>,
237    prefix: Option<&str>,
238    reference: &ReferenceBuilder,
239) -> Result<(), std::io::Error> {
240    let name = qualified_name(prefix, "Reference");
241    let mut element = BytesStart::new(&name);
242    if let Some(id) = &reference.id {
243        element.push_attribute(("Id", id.as_str()));
244    }
245    if let Some(ref_type) = &reference.ref_type {
246        element.push_attribute(("Type", ref_type.as_str()));
247    }
248    if let Some(uri) = &reference.uri {
249        element.push_attribute(("URI", uri.as_str()));
250    }
251    writer.write_event(Event::Start(element))?;
252
253    if !reference.transforms.is_empty() {
254        write_start(writer, prefix, "Transforms")?;
255        for transform in &reference.transforms {
256            write_transform(writer, prefix, transform)?;
257        }
258        write_end(writer, prefix, "Transforms")?;
259    }
260    write_algorithm(
261        writer,
262        prefix,
263        "DigestMethod",
264        reference.digest_method.uri(),
265    )?;
266    write_empty(writer, prefix, "DigestValue")?;
267    writer.write_event(Event::End(BytesEnd::new(name)))?;
268    Ok(())
269}
270
271fn write_transform<W: Write>(
272    writer: &mut Writer<W>,
273    prefix: Option<&str>,
274    transform: &Transform,
275) -> Result<(), std::io::Error> {
276    match transform {
277        Transform::Enveloped => {
278            write_algorithm(writer, prefix, "Transform", ENVELOPED_SIGNATURE_URI)
279        }
280        Transform::XpathExcludeAllSignatures => {
281            let name = qualified_name(prefix, "Transform");
282            let mut element = BytesStart::new(&name);
283            element.push_attribute(("Algorithm", XPATH_TRANSFORM_URI));
284            writer.write_event(Event::Start(element))?;
285            let xpath_name = qualified_name(prefix, "XPath");
286            let mut xpath = BytesStart::new(&xpath_name);
287            xpath.push_attribute(("xmlns:dsig", XMLDSIG_NS));
288            writer.write_event(Event::Start(xpath))?;
289            writer.write_event(Event::Text(BytesText::new(XPATH_EXCLUDE_ALL_SIGNATURES)))?;
290            writer.write_event(Event::End(BytesEnd::new(xpath_name)))?;
291            writer.write_event(Event::End(BytesEnd::new(name)))?;
292            Ok(())
293        }
294        Transform::C14n(algorithm) if algorithm.inclusive_prefixes().is_empty() => {
295            write_algorithm(writer, prefix, "Transform", algorithm.uri())
296        }
297        Transform::C14n(algorithm) => {
298            let name = qualified_name(prefix, "Transform");
299            let mut element = BytesStart::new(&name);
300            element.push_attribute(("Algorithm", algorithm.uri()));
301            writer.write_event(Event::Start(element))?;
302
303            if algorithm.mode() == C14nMode::Exclusive1_0 {
304                let mut prefixes: Vec<&str> = algorithm
305                    .inclusive_prefixes()
306                    .iter()
307                    .map(String::as_str)
308                    .collect();
309                prefixes.sort_unstable();
310                let prefix_list = prefixes
311                    .into_iter()
312                    .map(|p| if p.is_empty() { "#default" } else { p })
313                    .collect::<Vec<_>>()
314                    .join(" ");
315                let mut inclusive = BytesStart::new("ec:InclusiveNamespaces");
316                inclusive.push_attribute(("xmlns:ec", EXCLUSIVE_C14N_NS));
317                inclusive.push_attribute(("PrefixList", prefix_list.as_str()));
318                writer.write_event(Event::Empty(inclusive))?;
319            }
320            writer.write_event(Event::End(BytesEnd::new(name)))?;
321            Ok(())
322        }
323    }
324}
325
326fn write_algorithm<W: Write>(
327    writer: &mut Writer<W>,
328    prefix: Option<&str>,
329    local_name: &str,
330    algorithm: &str,
331) -> Result<(), std::io::Error> {
332    let name = qualified_name(prefix, local_name);
333    let mut element = BytesStart::new(name);
334    element.push_attribute(("Algorithm", algorithm));
335    writer.write_event(Event::Empty(element))?;
336    Ok(())
337}
338
339fn write_start<W: Write>(
340    writer: &mut Writer<W>,
341    prefix: Option<&str>,
342    local_name: &str,
343) -> Result<(), std::io::Error> {
344    writer.write_event(Event::Start(BytesStart::new(qualified_name(
345        prefix, local_name,
346    ))))?;
347    Ok(())
348}
349
350fn write_end<W: Write>(
351    writer: &mut Writer<W>,
352    prefix: Option<&str>,
353    local_name: &str,
354) -> Result<(), std::io::Error> {
355    writer.write_event(Event::End(BytesEnd::new(qualified_name(
356        prefix, local_name,
357    ))))?;
358    Ok(())
359}
360
361fn write_empty<W: Write>(
362    writer: &mut Writer<W>,
363    prefix: Option<&str>,
364    local_name: &str,
365) -> Result<(), std::io::Error> {
366    writer.write_event(Event::Empty(BytesStart::new(qualified_name(
367        prefix, local_name,
368    ))))?;
369    Ok(())
370}
371
372fn qualified_name(prefix: Option<&str>, local_name: &str) -> String {
373    prefix.map_or_else(
374        || local_name.to_owned(),
375        |prefix| format!("{prefix}:{local_name}"),
376    )
377}
378
379fn is_ncname(value: &str) -> bool {
380    if value.is_empty() || value.contains(':') {
381        return false;
382    }
383
384    roxmltree::Document::parse(&format!("<{value}/>"))
385        .is_ok_and(|document| document.root_element().tag_name().name() == value)
386}
387
388fn is_namespace_prefix(value: &str) -> bool {
389    if !is_ncname(value) {
390        return false;
391    }
392
393    // Parsing delegates the complete Unicode XML Name grammar and reserved-prefix
394    // rules to the same parser used by the rest of the crate.
395    roxmltree::Document::parse(&format!(
396        "<{value}:n xmlns:{value}=\"urn:xml-sec:prefix-validation\"/>"
397    ))
398    .is_ok()
399}