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};
9use crate::xml::is_xml_1_0_character;
10
11use super::parse::MAX_REFERENCES_PER_SIGNATURE;
12use super::transforms::{
13    MAX_TRANSFORMS_PER_REFERENCE, MAX_XPATH_FILTERS, XPathSignatureParseBudget,
14    validate_xpath_namespace_budget,
15};
16use super::xpath::compile_xpath;
17use super::{
18    BASE64_TRANSFORM_URI, DigestAlgorithm, ENVELOPED_SIGNATURE_URI, SignatureAlgorithm, Transform,
19    XPATH_FILTER2_TRANSFORM_URI, XPATH_TRANSFORM_URI, XPathExpression,
20};
21
22const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
23const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
24const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/";
25const EXCLUSIVE_C14N_NS: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
26const XPATH_EXCLUDE_ALL_SIGNATURES: &str = "not(ancestor-or-self::dsig:Signature)";
27
28/// Errors produced while validating or serializing an XMLDSig template.
29#[derive(Debug, thiserror::Error)]
30pub enum SignatureBuilderError {
31    /// A namespace prefix was not a supported XML NCName.
32    #[error("invalid XML namespace prefix: {0}")]
33    InvalidNamespacePrefix(String),
34    /// A namespace URI could not be represented in an XML 1.0 declaration.
35    #[error("XML namespace URI contains a character forbidden by XML 1.0: {0:?}")]
36    InvalidNamespaceUri(String),
37    /// An XPath binding would rebind the prefix used by XMLDSig elements.
38    #[error("XPath namespace binding conflicts with XMLDSig prefix: {0}")]
39    NamespacePrefixConflict(String),
40    /// An XMLDSig Id attribute was not a valid XML NCName.
41    #[error("invalid {element} Id: {value}")]
42    InvalidId {
43        /// XMLDSig element carrying the Id attribute.
44        element: &'static str,
45        /// Rejected attribute value.
46        value: String,
47    },
48    /// XMLDSig requires at least one reference in SignedInfo.
49    #[error("a signature template requires at least one Reference")]
50    MissingReference,
51    /// A template declared more references than signing and verification accept.
52    #[error("signature template contains {count} references; maximum is {max}")]
53    TooManyReferences {
54        /// Number of references supplied by the caller.
55        count: usize,
56        /// Maximum references accepted for one signature.
57        max: usize,
58    },
59    /// A reference exceeded the transform-chain limit shared with execution.
60    #[error("transform chain contains {count} transforms; maximum is {max}")]
61    TooManyTransforms {
62        /// Number of transforms supplied by the caller.
63        count: usize,
64        /// Maximum transforms accepted by parsing and execution.
65        max: usize,
66    },
67    /// XPath Filter 2.0 requires a non-empty, bounded expression sequence.
68    #[error("XPath Filter 2.0 requires between 1 and {max} expressions, got {count}")]
69    InvalidXPathFilterCount {
70        /// Number of expressions supplied by the caller.
71        count: usize,
72        /// Maximum expression count accepted by parsing and execution.
73        max: usize,
74    },
75    /// An XPath parameter cannot be parsed or exceeds its resource bounds.
76    #[error("invalid XPath expression: {0}")]
77    InvalidXPath(String),
78    /// SHA-1 algorithms are available for verification but not new signatures.
79    #[error("algorithm is not allowed for signing: {0}")]
80    SigningAlgorithmDisabled(&'static str),
81    /// The XML writer failed.
82    #[error("XML serialization error: {0}")]
83    Serialization(#[from] std::io::Error),
84    /// The writer unexpectedly emitted bytes that are not UTF-8.
85    #[error("XML writer emitted invalid UTF-8: {0}")]
86    InvalidUtf8(#[from] std::string::FromUtf8Error),
87}
88
89/// Builder for a single XMLDSig `<Reference>` template.
90#[derive(Debug, Clone)]
91pub struct ReferenceBuilder {
92    uri: Option<String>,
93    id: Option<String>,
94    ref_type: Option<String>,
95    transforms: Vec<Transform>,
96    digest_method: DigestAlgorithm,
97}
98
99impl ReferenceBuilder {
100    /// Create a reference using the required digest algorithm.
101    #[must_use]
102    pub fn new(digest_method: DigestAlgorithm) -> Self {
103        Self {
104            uri: None,
105            id: None,
106            ref_type: None,
107            transforms: Vec::new(),
108            digest_method,
109        }
110    }
111
112    /// Set the optional reference URI.
113    #[must_use]
114    pub fn uri(mut self, uri: impl Into<String>) -> Self {
115        self.uri = Some(uri.into());
116        self
117    }
118
119    /// Set the optional reference Id.
120    #[must_use]
121    pub fn id(mut self, id: impl Into<String>) -> Self {
122        self.id = Some(id.into());
123        self
124    }
125
126    /// Set the optional reference Type URI.
127    #[must_use]
128    pub fn ref_type(mut self, ref_type: impl Into<String>) -> Self {
129        self.ref_type = Some(ref_type.into());
130        self
131    }
132
133    /// Append a transform, preserving insertion order.
134    #[must_use]
135    pub fn transform(mut self, transform: Transform) -> Self {
136        self.transforms.push(transform);
137        self
138    }
139}
140
141/// Builder for a complete XMLDSig `<Signature>` template.
142#[derive(Debug, Clone)]
143pub struct SignatureBuilder {
144    c14n_method: C14nAlgorithm,
145    sign_method: SignatureAlgorithm,
146    ns_prefix: Option<String>,
147    signature_id: Option<String>,
148    references: Vec<ReferenceBuilder>,
149    include_key_info: bool,
150}
151
152impl SignatureBuilder {
153    /// Create a signature template using the required algorithms.
154    #[must_use]
155    pub fn new(c14n_method: C14nAlgorithm, sign_method: SignatureAlgorithm) -> Self {
156        Self {
157            c14n_method,
158            sign_method,
159            ns_prefix: None,
160            signature_id: None,
161            references: Vec::new(),
162            include_key_info: false,
163        }
164    }
165
166    /// Use a namespace prefix such as `ds`; the default is an unprefixed namespace.
167    #[must_use]
168    pub fn ns_prefix(mut self, prefix: impl Into<String>) -> Self {
169        self.ns_prefix = Some(prefix.into());
170        self
171    }
172
173    /// Set the optional Signature Id.
174    #[must_use]
175    pub fn signature_id(mut self, id: impl Into<String>) -> Self {
176        self.signature_id = Some(id.into());
177        self
178    }
179
180    /// Append a reference, preserving insertion order.
181    #[must_use]
182    pub fn add_reference(mut self, reference: ReferenceBuilder) -> Self {
183        self.references.push(reference);
184        self
185    }
186
187    /// Control whether an empty KeyInfo placeholder is emitted.
188    #[must_use]
189    pub fn key_info(mut self, include: bool) -> Self {
190        self.include_key_info = include;
191        self
192    }
193
194    /// Build a namespace-correct XMLDSig template with empty digest and signature values.
195    pub fn build_template(&self) -> Result<String, SignatureBuilderError> {
196        self.validate()?;
197
198        let prefix = self.ns_prefix.as_deref();
199        let mut writer = Writer::new(Vec::new());
200        let signature_name = qualified_name(prefix, "Signature");
201        let mut signature = BytesStart::new(&signature_name);
202        let namespace_attr = prefix.map_or_else(|| "xmlns".to_owned(), |p| format!("xmlns:{p}"));
203        signature.push_attribute((namespace_attr.as_str(), XMLDSIG_NS));
204        if let Some(id) = &self.signature_id {
205            signature.push_attribute(("Id", id.as_str()));
206        }
207        writer.write_event(Event::Start(signature))?;
208
209        write_start(&mut writer, prefix, "SignedInfo")?;
210        write_algorithm(
211            &mut writer,
212            prefix,
213            "CanonicalizationMethod",
214            self.c14n_method.uri(),
215        )?;
216        write_algorithm(
217            &mut writer,
218            prefix,
219            "SignatureMethod",
220            self.sign_method.uri(),
221        )?;
222        for reference in &self.references {
223            write_reference(&mut writer, prefix, reference)?;
224        }
225        write_end(&mut writer, prefix, "SignedInfo")?;
226        write_empty(&mut writer, prefix, "SignatureValue")?;
227        if self.include_key_info {
228            write_empty(&mut writer, prefix, "KeyInfo")?;
229        }
230        writer.write_event(Event::End(BytesEnd::new(signature_name)))?;
231
232        Ok(String::from_utf8(writer.into_inner())?)
233    }
234
235    fn validate(&self) -> Result<(), SignatureBuilderError> {
236        if let Some(prefix) = &self.ns_prefix
237            && !is_namespace_prefix(prefix)
238        {
239            return Err(SignatureBuilderError::InvalidNamespacePrefix(
240                prefix.clone(),
241            ));
242        }
243        if let Some(id) = &self.signature_id
244            && !is_ncname(id)
245        {
246            return Err(SignatureBuilderError::InvalidId {
247                element: "Signature",
248                value: id.clone(),
249            });
250        }
251        if self.references.is_empty() {
252            return Err(SignatureBuilderError::MissingReference);
253        }
254        if self.references.len() > MAX_REFERENCES_PER_SIGNATURE {
255            return Err(SignatureBuilderError::TooManyReferences {
256                count: self.references.len(),
257                max: MAX_REFERENCES_PER_SIGNATURE,
258            });
259        }
260        let mut xpath_signature_budget = XPathSignatureParseBudget::default();
261        for reference in &self.references {
262            if reference.transforms.len() > MAX_TRANSFORMS_PER_REFERENCE {
263                return Err(SignatureBuilderError::TooManyTransforms {
264                    count: reference.transforms.len(),
265                    max: MAX_TRANSFORMS_PER_REFERENCE,
266                });
267            }
268            for transform in &reference.transforms {
269                match transform {
270                    Transform::XPath(xpath) => {
271                        validate_xpath_source(xpath.expression())?;
272                        xpath_signature_budget.charge().map_err(|error| {
273                            SignatureBuilderError::InvalidXPath(error.to_string())
274                        })?;
275                    }
276                    Transform::XPathFilter2(filters) => {
277                        if filters.is_empty() || filters.len() > MAX_XPATH_FILTERS {
278                            return Err(SignatureBuilderError::InvalidXPathFilterCount {
279                                count: filters.len(),
280                                max: MAX_XPATH_FILTERS,
281                            });
282                        }
283                        for filter in filters {
284                            validate_xpath_source(filter.xpath().expression())?;
285                            xpath_signature_budget.charge().map_err(|error| {
286                                SignatureBuilderError::InvalidXPath(error.to_string())
287                            })?;
288                        }
289                    }
290                    _ => {}
291                }
292            }
293            validate_xpath_namespace_budget(
294                &reference.transforms,
295                self.ns_prefix.as_deref().map(|prefix| (prefix, XMLDSIG_NS)),
296            )
297            .map_err(|error| SignatureBuilderError::InvalidXPath(error.to_string()))?;
298        }
299        for (prefix, uri, shares_signature_namespace) in
300            self.references.iter().flat_map(|reference| {
301                reference
302                    .transforms
303                    .iter()
304                    .flat_map(|transform| match transform {
305                        Transform::XPath(xpath) => xpath
306                            .namespaces()
307                            .iter()
308                            .map(|(prefix, uri)| (prefix, uri, true))
309                            .collect::<Vec<_>>(),
310                        Transform::XPathFilter2(filters) => filters
311                            .iter()
312                            .flat_map(|filter| {
313                                filter
314                                    .xpath()
315                                    .namespaces()
316                                    .iter()
317                                    .map(|(prefix, uri)| (prefix, uri, false))
318                            })
319                            .collect(),
320                        _ => Vec::new(),
321                    })
322            })
323        {
324            // Namespaces in XML reserves the declaration namespace and both
325            // sides of the `xml` binding; prefixed declarations cannot be empty.
326            if uri.is_empty() || uri == XMLNS_NS || !uri.chars().all(is_xml_1_0_character) {
327                return Err(SignatureBuilderError::InvalidNamespaceUri(uri.clone()));
328            }
329            if prefix == "xmlns"
330                || (prefix == "xml") != (uri == XML_NS)
331                || (prefix != "xml" && !is_namespace_prefix(prefix))
332            {
333                return Err(SignatureBuilderError::InvalidNamespacePrefix(
334                    prefix.clone(),
335                ));
336            }
337            // Ordinary XPath parameters share the Signature namespace prefix,
338            // while Filter2 parameters are unprefixed in their own namespace.
339            if shares_signature_namespace
340                && self.ns_prefix.as_ref() == Some(prefix)
341                && uri != XMLDSIG_NS
342            {
343                return Err(SignatureBuilderError::NamespacePrefixConflict(
344                    prefix.clone(),
345                ));
346            }
347        }
348        if !self.sign_method.signing_allowed() {
349            return Err(SignatureBuilderError::SigningAlgorithmDisabled(
350                self.sign_method.uri(),
351            ));
352        }
353        for reference in &self.references {
354            if let Some(id) = &reference.id
355                && !is_ncname(id)
356            {
357                return Err(SignatureBuilderError::InvalidId {
358                    element: "Reference",
359                    value: id.clone(),
360                });
361            }
362            if !reference.digest_method.signing_allowed() {
363                return Err(SignatureBuilderError::SigningAlgorithmDisabled(
364                    reference.digest_method.uri(),
365                ));
366            }
367        }
368        Ok(())
369    }
370}
371
372fn validate_xpath_source(source: &str) -> Result<(), SignatureBuilderError> {
373    if let Some(character) = source
374        .chars()
375        .find(|character| !is_xml_1_0_character(*character))
376    {
377        return Err(SignatureBuilderError::InvalidXPath(format!(
378            "XPath expression contains a character forbidden by XML 1.0: {character:?}"
379        )));
380    }
381    compile_xpath(source).map_err(SignatureBuilderError::InvalidXPath)?;
382    Ok(())
383}
384
385fn write_reference<W: Write>(
386    writer: &mut Writer<W>,
387    prefix: Option<&str>,
388    reference: &ReferenceBuilder,
389) -> Result<(), std::io::Error> {
390    let name = qualified_name(prefix, "Reference");
391    let mut element = BytesStart::new(&name);
392    if let Some(id) = &reference.id {
393        element.push_attribute(("Id", id.as_str()));
394    }
395    if let Some(ref_type) = &reference.ref_type {
396        element.push_attribute(("Type", ref_type.as_str()));
397    }
398    if let Some(uri) = &reference.uri {
399        element.push_attribute(("URI", uri.as_str()));
400    }
401    writer.write_event(Event::Start(element))?;
402
403    if !reference.transforms.is_empty() {
404        write_start(writer, prefix, "Transforms")?;
405        for transform in &reference.transforms {
406            write_transform(writer, prefix, transform)?;
407        }
408        write_end(writer, prefix, "Transforms")?;
409    }
410    write_algorithm(
411        writer,
412        prefix,
413        "DigestMethod",
414        reference.digest_method.uri(),
415    )?;
416    write_empty(writer, prefix, "DigestValue")?;
417    writer.write_event(Event::End(BytesEnd::new(name)))?;
418    Ok(())
419}
420
421fn write_transform<W: Write>(
422    writer: &mut Writer<W>,
423    prefix: Option<&str>,
424    transform: &Transform,
425) -> Result<(), std::io::Error> {
426    match transform {
427        Transform::Enveloped => {
428            write_algorithm(writer, prefix, "Transform", ENVELOPED_SIGNATURE_URI)
429        }
430        Transform::XpathExcludeAllSignatures => {
431            let name = qualified_name(prefix, "Transform");
432            let mut element = BytesStart::new(&name);
433            element.push_attribute(("Algorithm", XPATH_TRANSFORM_URI));
434            writer.write_event(Event::Start(element))?;
435            let xpath_name = qualified_name(prefix, "XPath");
436            let mut xpath = BytesStart::new(&xpath_name);
437            xpath.push_attribute(("xmlns:dsig", XMLDSIG_NS));
438            writer.write_event(Event::Start(xpath))?;
439            writer.write_event(Event::Text(BytesText::new(XPATH_EXCLUDE_ALL_SIGNATURES)))?;
440            writer.write_event(Event::End(BytesEnd::new(xpath_name)))?;
441            writer.write_event(Event::End(BytesEnd::new(name)))?;
442            Ok(())
443        }
444        Transform::XPath(xpath) => {
445            let transform_name = qualified_name(prefix, "Transform");
446            let mut transform_element = BytesStart::new(&transform_name);
447            transform_element.push_attribute(("Algorithm", XPATH_TRANSFORM_URI));
448            writer.write_event(Event::Start(transform_element))?;
449            write_xpath_expression(writer, prefix, "XPath", None, xpath)?;
450            writer.write_event(Event::End(BytesEnd::new(transform_name)))?;
451            Ok(())
452        }
453        Transform::XPathFilter2(filters) => {
454            let transform_name = qualified_name(prefix, "Transform");
455            let mut transform_element = BytesStart::new(&transform_name);
456            transform_element.push_attribute(("Algorithm", XPATH_FILTER2_TRANSFORM_URI));
457            writer.write_event(Event::Start(transform_element))?;
458            for filter in filters {
459                write_xpath_expression(
460                    writer,
461                    None,
462                    "XPath",
463                    Some(filter.operation().as_str()),
464                    filter.xpath(),
465                )?;
466            }
467            writer.write_event(Event::End(BytesEnd::new(transform_name)))?;
468            Ok(())
469        }
470        Transform::Base64Decode => {
471            write_algorithm(writer, prefix, "Transform", BASE64_TRANSFORM_URI)
472        }
473        Transform::C14n(algorithm) if algorithm.inclusive_prefixes().is_empty() => {
474            write_algorithm(writer, prefix, "Transform", algorithm.uri())
475        }
476        Transform::C14n(algorithm) => {
477            let name = qualified_name(prefix, "Transform");
478            let mut element = BytesStart::new(&name);
479            element.push_attribute(("Algorithm", algorithm.uri()));
480            writer.write_event(Event::Start(element))?;
481
482            if algorithm.mode() == C14nMode::Exclusive1_0 {
483                let mut prefixes: Vec<&str> = algorithm
484                    .inclusive_prefixes()
485                    .iter()
486                    .map(String::as_str)
487                    .collect();
488                prefixes.sort_unstable();
489                let prefix_list = prefixes
490                    .into_iter()
491                    .map(|p| if p.is_empty() { "#default" } else { p })
492                    .collect::<Vec<_>>()
493                    .join(" ");
494                let mut inclusive = BytesStart::new("ec:InclusiveNamespaces");
495                inclusive.push_attribute(("xmlns:ec", EXCLUSIVE_C14N_NS));
496                inclusive.push_attribute(("PrefixList", prefix_list.as_str()));
497                writer.write_event(Event::Empty(inclusive))?;
498            }
499            writer.write_event(Event::End(BytesEnd::new(name)))?;
500            Ok(())
501        }
502    }
503}
504
505fn write_xpath_expression<W: Write>(
506    writer: &mut Writer<W>,
507    prefix: Option<&str>,
508    local_name: &str,
509    filter: Option<&str>,
510    xpath: &XPathExpression,
511) -> Result<(), std::io::Error> {
512    let name = qualified_name(prefix, local_name);
513    let mut element = BytesStart::new(&name);
514    let namespace_attributes = xpath
515        .namespaces()
516        .iter()
517        .filter(|(namespace_prefix, _)| namespace_prefix.as_str() != "xml")
518        .map(|(namespace_prefix, uri)| (format!("xmlns:{namespace_prefix}"), uri))
519        .collect::<Vec<_>>();
520    if prefix.is_none() && filter.is_some() {
521        element.push_attribute(("xmlns", XPATH_FILTER2_TRANSFORM_URI));
522    }
523    if let Some(filter) = filter {
524        element.push_attribute(("Filter", filter));
525    }
526    for (attribute, uri) in &namespace_attributes {
527        element.push_attribute((attribute.as_str(), uri.as_str()));
528    }
529    writer.write_event(Event::Start(element))?;
530    writer.write_event(Event::Text(BytesText::new(xpath.expression())))?;
531    writer.write_event(Event::End(BytesEnd::new(name)))?;
532    Ok(())
533}
534
535fn write_algorithm<W: Write>(
536    writer: &mut Writer<W>,
537    prefix: Option<&str>,
538    local_name: &str,
539    algorithm: &str,
540) -> Result<(), std::io::Error> {
541    let name = qualified_name(prefix, local_name);
542    let mut element = BytesStart::new(name);
543    element.push_attribute(("Algorithm", algorithm));
544    writer.write_event(Event::Empty(element))?;
545    Ok(())
546}
547
548fn write_start<W: Write>(
549    writer: &mut Writer<W>,
550    prefix: Option<&str>,
551    local_name: &str,
552) -> Result<(), std::io::Error> {
553    writer.write_event(Event::Start(BytesStart::new(qualified_name(
554        prefix, local_name,
555    ))))?;
556    Ok(())
557}
558
559fn write_end<W: Write>(
560    writer: &mut Writer<W>,
561    prefix: Option<&str>,
562    local_name: &str,
563) -> Result<(), std::io::Error> {
564    writer.write_event(Event::End(BytesEnd::new(qualified_name(
565        prefix, local_name,
566    ))))?;
567    Ok(())
568}
569
570fn write_empty<W: Write>(
571    writer: &mut Writer<W>,
572    prefix: Option<&str>,
573    local_name: &str,
574) -> Result<(), std::io::Error> {
575    writer.write_event(Event::Empty(BytesStart::new(qualified_name(
576        prefix, local_name,
577    ))))?;
578    Ok(())
579}
580
581fn qualified_name(prefix: Option<&str>, local_name: &str) -> String {
582    prefix.map_or_else(
583        || local_name.to_owned(),
584        |prefix| format!("{prefix}:{local_name}"),
585    )
586}
587
588fn is_ncname(value: &str) -> bool {
589    if value.is_empty() || value.contains(':') {
590        return false;
591    }
592
593    roxmltree::Document::parse(&format!("<{value}/>"))
594        .is_ok_and(|document| document.root_element().tag_name().name() == value)
595}
596
597fn is_namespace_prefix(value: &str) -> bool {
598    // Namespaces in XML reserves these names regardless of the URI being bound.
599    // Keep the invariant explicit instead of depending on parser rejection of a
600    // synthetic declaration assembled below.
601    if matches!(value, "xml" | "xmlns") || !is_ncname(value) {
602        return false;
603    }
604
605    // Parsing delegates the complete Unicode XML Name grammar to the same parser
606    // used by the rest of the crate.
607    roxmltree::Document::parse(&format!(
608        "<{value}:n xmlns:{value}=\"urn:xml-sec:prefix-validation\"/>"
609    ))
610    .is_ok()
611}