Skip to main content

xml_sec/xmldsig/
builder.rs

1//! Builders for deterministic XMLDSig signature templates.
2
3use crate::xml_input as xml_sec_xml_input;
4use std::{collections::HashSet, io::Write};
5
6use base64::Engine;
7use xml_sec_xml_input::lexical::Writer;
8
9use crate::c14n::{C14nAlgorithm, C14nMode, canonicalize_bounded_with_xml_base_budget};
10use crate::policy::{PolicyViolation, SigningPolicy};
11use crate::xml::{is_xml_1_0_character, is_xml_ncname};
12
13use super::mutation::{
14    fill_signature_value_with_budget, fill_signed_info_digest_values_with_budget,
15    padded_base64_len_for_xml, projected_signature_value_output_len_at_index_with_budget,
16    zero_base64_placeholder,
17};
18use super::transforms::{
19    ENVELOPED_SIGNATURE_XPATH_EXPR, ENVELOPED_SIGNATURE_XPATH_PREFIX, TransformExecutionBudget,
20    XPathSignatureParseBudget, map_c14n_resource_policy_violation, transform_chain_produces_binary,
21    validate_signing_transform_policy, validate_xpath_namespace_budget_with_resources,
22};
23use super::uri::validate_signing_reference_uri;
24use super::{
25    BASE64_TRANSFORM_URI, DigestAlgorithm, ENVELOPED_SIGNATURE_URI, SignatureAlgorithm,
26    SignatureContext, Transform, XPATH_FILTER2_TRANSFORM_URI, XPATH_TRANSFORM_URI, XPathExpression,
27};
28
29const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
30const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
31const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/";
32const EXCLUSIVE_C14N_NS: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
33
34/// Errors produced while validating or serializing an XMLDSig template.
35#[derive(Debug, thiserror::Error)]
36pub enum SignatureBuilderError {
37    /// A namespace prefix was not a supported XML NCName.
38    #[error("invalid XML namespace prefix: {0}")]
39    InvalidNamespacePrefix(String),
40    /// A namespace URI could not be represented in an XML 1.0 declaration.
41    #[error("XML namespace URI contains a character forbidden by XML 1.0: {0:?}")]
42    InvalidNamespaceUri(String),
43    /// An XPath binding would rebind the prefix used by XMLDSig elements.
44    #[error("XPath namespace binding conflicts with XMLDSig prefix: {0}")]
45    NamespacePrefixConflict(String),
46    /// An XMLDSig Id attribute was not a valid XML NCName.
47    #[error("invalid {element} Id: {value}")]
48    InvalidId {
49        /// XMLDSig element carrying the Id attribute.
50        element: &'static str,
51        /// Rejected attribute value.
52        value: String,
53    },
54    /// XMLDSig requires at least one reference in SignedInfo.
55    #[error("a signature template requires at least one Reference")]
56    MissingReference,
57    /// The immutable signing policy rejected the template.
58    #[error("signing policy violation: {0}")]
59    Policy(#[from] PolicyViolation),
60    /// An XPath parameter cannot be parsed or exceeds its resource bounds.
61    #[error("invalid XPath expression: {0}")]
62    InvalidXPath(String),
63    /// The selected algorithm is disabled by the immutable signing policy.
64    #[error("algorithm is not allowed for signing: {0}")]
65    SigningAlgorithmDisabled(&'static str),
66    /// The selected signature method cannot carry a cryptographic context.
67    #[error("signature context is not applicable to algorithm: {0}")]
68    SignatureContextNotApplicable(&'static str),
69    /// The XML writer failed.
70    #[error("XML serialization error: {0}")]
71    Serialization(#[from] std::io::Error),
72    /// The writer unexpectedly emitted bytes that are not UTF-8.
73    #[error("XML writer emitted invalid UTF-8: {0}")]
74    InvalidUtf8(#[from] std::string::FromUtf8Error),
75    /// The generated template could not be parsed under the selected policy.
76    #[error("generated XML template is invalid: {0}")]
77    GeneratedXml(#[from] crate::xml::dom::ParseError),
78    /// The generated SignedInfo could not be canonicalized.
79    #[error("generated SignedInfo canonicalization failed: {0}")]
80    Canonicalization(#[from] crate::c14n::C14nError),
81    /// The generated template did not contain its required SignedInfo child.
82    #[error("generated XML template is missing SignedInfo")]
83    MissingGeneratedSignedInfo,
84    /// The generated template could not be prepared for policy validation.
85    #[error("generated XML template validation failed: {0}")]
86    GeneratedMutation(#[source] super::mutation::XmlMutationError),
87}
88
89/// Builder for a single XMLDSig `<Reference>` template.
90#[derive(Debug, Clone)]
91pub struct ReferenceBuilder {
92    uri: 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: String::new(),
105            id: None,
106            ref_type: None,
107            transforms: Vec::new(),
108            digest_method,
109        }
110    }
111
112    /// Set the reference URI.
113    ///
114    /// References default to the empty same-document URI because the signing
115    /// pipeline requires an explicit `URI` attribute.
116    #[must_use]
117    pub fn uri(mut self, uri: impl Into<String>) -> Self {
118        self.uri = uri.into();
119        self
120    }
121
122    /// Set the optional reference Id.
123    #[must_use]
124    pub fn id(mut self, id: impl Into<String>) -> Self {
125        self.id = Some(id.into());
126        self
127    }
128
129    /// Set the optional reference Type URI.
130    #[must_use]
131    pub fn ref_type(mut self, ref_type: impl Into<String>) -> Self {
132        self.ref_type = Some(ref_type.into());
133        self
134    }
135
136    /// Append a transform, preserving insertion order.
137    #[must_use]
138    pub fn transform(mut self, transform: Transform) -> Self {
139        self.transforms.push(transform);
140        self
141    }
142}
143
144/// Builder for a complete XMLDSig `<Signature>` template.
145#[derive(Debug, Clone)]
146pub struct SignatureBuilder {
147    c14n_method: C14nAlgorithm,
148    sign_method: SignatureAlgorithm,
149    signature_context: SignatureContext,
150    ns_prefix: Option<String>,
151    signature_id: Option<String>,
152    references: Vec<ReferenceBuilder>,
153    include_key_info: bool,
154}
155
156impl SignatureBuilder {
157    /// Create a signature template using the required algorithms.
158    #[must_use]
159    pub fn new(c14n_method: C14nAlgorithm, sign_method: SignatureAlgorithm) -> Self {
160        Self {
161            c14n_method,
162            sign_method,
163            signature_context: SignatureContext::default(),
164            ns_prefix: None,
165            signature_id: None,
166            references: Vec::new(),
167            include_key_info: false,
168        }
169    }
170
171    /// Use a namespace prefix such as `ds`; the default is an unprefixed namespace.
172    #[must_use]
173    pub fn ns_prefix(mut self, prefix: impl Into<String>) -> Self {
174        self.ns_prefix = Some(prefix.into());
175        self
176    }
177
178    /// Set the optional Signature Id.
179    #[must_use]
180    pub fn signature_id(mut self, id: impl Into<String>) -> Self {
181        self.signature_id = Some(id.into());
182        self
183    }
184
185    /// Set the authenticated cryptographic context for a context-capable method.
186    /// Nonempty contexts on other methods are rejected, never silently ignored.
187    #[must_use]
188    pub fn signature_context(mut self, context: SignatureContext) -> Self {
189        self.signature_context = context;
190        self
191    }
192
193    /// Append a reference, preserving insertion order.
194    #[must_use]
195    pub fn add_reference(mut self, reference: ReferenceBuilder) -> Self {
196        self.references.push(reference);
197        self
198    }
199
200    /// Control whether an empty KeyInfo placeholder is emitted.
201    #[must_use]
202    pub fn key_info(mut self, include: bool) -> Self {
203        self.include_key_info = include;
204        self
205    }
206
207    /// Build a namespace-correct XMLDSig template with empty digest and signature values.
208    pub fn build_template(&self) -> Result<String, SignatureBuilderError> {
209        self.build_template_with_policy(&SigningPolicy::default())
210    }
211
212    /// Build a template after enforcing the same immutable policy snapshot used
213    /// by the signing operation that will consume it. This key-independent
214    /// method validates generated digest widths; `SignContext::sign_with_builder`
215    /// additionally validates the exact key-dependent `SignatureValue` width.
216    pub fn build_template_with_policy(
217        &self,
218        policy: &SigningPolicy,
219    ) -> Result<String, SignatureBuilderError> {
220        let budget = TransformExecutionBudget::from_resources(&policy.resources);
221        let mut xpath_parse_budget = XPathSignatureParseBudget::from_resources(&policy.resources);
222        self.build_template_with_policy_and_signature_output_len(
223            policy,
224            None,
225            &budget,
226            &mut xpath_parse_budget,
227        )
228    }
229
230    pub(super) fn build_template_with_policy_for_signature_output(
231        &self,
232        policy: &SigningPolicy,
233        signature_output_len: usize,
234        budget: &TransformExecutionBudget,
235        xpath_parse_budget: &mut XPathSignatureParseBudget,
236    ) -> Result<String, SignatureBuilderError> {
237        self.build_template_with_policy_and_signature_output_len(
238            policy,
239            Some(signature_output_len),
240            budget,
241            xpath_parse_budget,
242        )
243    }
244
245    fn build_template_with_policy_and_signature_output_len(
246        &self,
247        policy: &SigningPolicy,
248        signature_output_len: Option<usize>,
249        budget: &TransformExecutionBudget,
250        xpath_parse_budget: &mut XPathSignatureParseBudget,
251    ) -> Result<String, SignatureBuilderError> {
252        policy.validate()?;
253        self.validate(policy, xpath_parse_budget)?;
254
255        let prefix = self.ns_prefix.as_deref();
256        let mut writer = Writer::new(Vec::new());
257        let signature_name = qualified_name(prefix, "Signature");
258        let namespace_attr = prefix.map_or_else(|| "xmlns".to_owned(), |p| format!("xmlns:{p}"));
259        let mut attributes = vec![(namespace_attr.as_str(), XMLDSIG_NS)];
260        if let Some(id) = &self.signature_id {
261            attributes.push(("Id", id.as_str()));
262        }
263        writer.start(&signature_name, attributes)?;
264
265        write_start(&mut writer, prefix, "SignedInfo")?;
266        write_algorithm(
267            &mut writer,
268            prefix,
269            "CanonicalizationMethod",
270            self.c14n_method.uri(),
271        )?;
272        if self.signature_context.as_bytes().is_empty() {
273            write_algorithm(
274                &mut writer,
275                prefix,
276                "SignatureMethod",
277                self.sign_method.uri(),
278            )?;
279        } else {
280            let method_name = qualified_name(prefix, "SignatureMethod");
281            writer.start(&method_name, [("Algorithm", self.sign_method.uri())])?;
282            // RFC 8032 ยง5 defines the octet context; this XML parameter is an
283            // explicit libxmlsec1 extension, not part of RFC 9231's URI contract.
284            // https://www.rfc-editor.org/rfc/rfc8032.html#section-5
285            // Use a local default namespace to avoid collisions with caller prefixes.
286            let context_name = self.sign_method.context_element().ok_or(
287                SignatureBuilderError::SignatureContextNotApplicable(self.sign_method.uri()),
288            )?;
289            writer.start(
290                context_name,
291                [("xmlns", super::parse::EXPERIMENTAL_SIGNATURE_NS)],
292            )?;
293            let mut encoded = [0_u8; 340];
294            let length = base64::engine::general_purpose::STANDARD
295                .encode_slice(self.signature_context.as_bytes(), &mut encoded)
296                .map_err(std::io::Error::other)?;
297            writer.text(std::str::from_utf8(&encoded[..length]).map_err(std::io::Error::other)?)?;
298            writer.end(context_name)?;
299            writer.end(&method_name)?;
300        }
301        for reference in &self.references {
302            write_reference(&mut writer, prefix, reference)?;
303        }
304        write_end(&mut writer, prefix, "SignedInfo")?;
305        write_empty(&mut writer, prefix, "SignatureValue")?;
306        if self.include_key_info {
307            write_empty(&mut writer, prefix, "KeyInfo")?;
308        }
309        writer.end(&signature_name)?;
310
311        let template = String::from_utf8(writer.into_inner())?;
312        // Field-level checks bound each input class; these checks cover the
313        // completed artifact exactly as the signing operation will consume it.
314        policy.resources.validate_xml_document_len(template.len())?;
315        let digest_placeholders = self.references.iter().map(|reference| {
316            base64::engine::general_purpose::STANDARD.encode(vec![
317                0_u8;
318                reference
319                    .digest_method
320                    .output_len()
321            ])
322        });
323        let digest_validation_template = fill_signed_info_digest_values_with_budget(
324            &template,
325            digest_placeholders,
326            Some(policy),
327            Some(budget.xml_parse_work()),
328        )
329        .map_err(map_generated_mutation_error)?;
330        let validation_template = if let Some(signature_output_len) = signature_output_len {
331            let encoded_signature_len = padded_base64_len_for_xml(signature_output_len, policy)
332                .map_err(map_generated_mutation_error)?;
333            let projected_document_len = projected_signature_value_output_len_at_index_with_budget(
334                &digest_validation_template,
335                encoded_signature_len,
336                0,
337                Some(policy),
338                Some(budget.xml_parse_work()),
339            )
340            .map_err(map_generated_mutation_error)?;
341            policy
342                .resources
343                .validate_xml_document_len(projected_document_len)?;
344            let signature_placeholder =
345                zero_base64_placeholder(signature_output_len, encoded_signature_len);
346            fill_signature_value_with_budget(
347                &digest_validation_template,
348                &signature_placeholder,
349                Some(policy),
350                Some(budget.xml_parse_work()),
351            )
352            .map_err(map_generated_mutation_error)?
353        } else {
354            digest_validation_template
355        };
356        policy
357            .resources
358            .validate_xml_document_len(validation_template.len())?;
359        let settings =
360            crate::document::DocumentParseSettings::from_policy(&policy.xml, &policy.resources);
361        let document = super::mutation::parse_with_options_and_budget(
362            &validation_template,
363            settings,
364            Some(budget.xml_parse_work()),
365        )
366        .map_err(|error| match error.into_policy_violation(settings) {
367            Ok(error) => SignatureBuilderError::Policy(error),
368            Err(crate::document::XmlDocumentError::Parse(error)) => {
369                SignatureBuilderError::GeneratedXml(error)
370            }
371            Err(error) => SignatureBuilderError::GeneratedMutation(
372                super::mutation::XmlMutationError::Document(error),
373            ),
374        })?;
375        let signed_info = document
376            .root_element()
377            .children()
378            .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
379            .ok_or(SignatureBuilderError::MissingGeneratedSignedInfo)?;
380        let signed_info_subtree: HashSet<_> =
381            signed_info.descendants().map(|node| node.id()).collect();
382        let mut canonical_signed_info = Vec::new();
383        canonicalize_bounded_with_xml_base_budget(
384            &document,
385            Some(&|node| signed_info_subtree.contains(&node.id())),
386            &self.c14n_method,
387            budget.remaining_c14n_output(),
388            budget.xml_base_resolution(),
389            &mut canonical_signed_info,
390        )
391        .map_err(|error| {
392            map_c14n_resource_policy_violation(
393                &error,
394                crate::policy::resource_name::CANONICALIZED_BYTES,
395                budget.c14n_output_limit(),
396            )
397            .map_or_else(
398                || SignatureBuilderError::Canonicalization(error),
399                SignatureBuilderError::Policy,
400            )
401        })?;
402        budget.charge_c14n_output_policy(canonical_signed_info.len())?;
403        Ok(template)
404    }
405
406    pub(super) fn signature_method(&self) -> SignatureAlgorithm {
407        self.sign_method
408    }
409
410    fn validate(
411        &self,
412        policy: &SigningPolicy,
413        xpath_signature_budget: &mut XPathSignatureParseBudget,
414    ) -> Result<(), SignatureBuilderError> {
415        if !self.signature_context.as_bytes().is_empty()
416            && self.sign_method.context_element().is_none()
417        {
418            return Err(SignatureBuilderError::SignatureContextNotApplicable(
419                self.sign_method.uri(),
420            ));
421        }
422        if let Some(prefix) = &self.ns_prefix
423            && !is_namespace_prefix(prefix)
424        {
425            return Err(SignatureBuilderError::InvalidNamespacePrefix(
426                prefix.clone(),
427            ));
428        }
429        if let Some(id) = &self.signature_id
430            && !is_xml_ncname(id)
431        {
432            return Err(SignatureBuilderError::InvalidId {
433                element: "Signature",
434                value: id.clone(),
435            });
436        }
437        if self.references.is_empty() {
438            return Err(SignatureBuilderError::MissingReference);
439        }
440        if self.references.len() > policy.resources.max_references {
441            return Err(PolicyViolation::ResourceLimit {
442                resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
443                maximum: policy.resources.max_references,
444                actual: self.references.len(),
445            }
446            .into());
447        }
448        for reference in &self.references {
449            validate_signing_reference_uri(&reference.uri, policy)?;
450            let generated_transforms = reference_transforms_for_generation(reference);
451            if generated_transforms.len() > policy.resources.max_transforms_per_reference {
452                return Err(PolicyViolation::ResourceLimit {
453                    resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
454                    maximum: policy.resources.max_transforms_per_reference,
455                    actual: generated_transforms.len(),
456                }
457                .into());
458            }
459            let initial_binary = !reference.uri.is_empty() && !reference.uri.starts_with('#');
460            validate_signing_transform_policy(
461                initial_binary,
462                &generated_transforms,
463                policy.transforms.allowed_algorithms.as_ref(),
464            )?;
465            for transform in &reference.transforms {
466                match transform {
467                    Transform::XpathExcludeAllSignatures => {
468                        validate_xpath_source(
469                            ENVELOPED_SIGNATURE_XPATH_EXPR,
470                            xpath_signature_budget,
471                        )?;
472                    }
473                    Transform::XPath(xpath) => {
474                        validate_xpath_source(xpath.expression(), xpath_signature_budget)?;
475                    }
476                    Transform::XPathFilter2(filters) => {
477                        if filters.is_empty() {
478                            return Err(SignatureBuilderError::InvalidXPath(
479                                "XPath Filter 2.0 requires at least one expression".into(),
480                            ));
481                        }
482                        if filters.len() > policy.resources.max_xpath_filters {
483                            return Err(PolicyViolation::ResourceLimit {
484                                resource: crate::policy::resource_name::XPATH_FILTERS,
485                                maximum: policy.resources.max_xpath_filters,
486                                actual: filters.len(),
487                            }
488                            .into());
489                        }
490                        for filter in filters {
491                            validate_xpath_source(
492                                filter.xpath().expression(),
493                                xpath_signature_budget,
494                            )?;
495                        }
496                    }
497                    _ => {}
498                }
499            }
500            validate_xpath_namespace_budget_with_resources(
501                &reference.transforms,
502                self.ns_prefix.as_deref().map(|prefix| (prefix, XMLDSIG_NS)),
503                &policy.resources,
504            )
505            .map_err(map_transform_validation_error)?;
506        }
507        for (prefix, uri, shares_signature_namespace) in
508            self.references.iter().flat_map(|reference| {
509                reference
510                    .transforms
511                    .iter()
512                    .flat_map(|transform| match transform {
513                        Transform::XPath(xpath) => xpath
514                            .namespaces()
515                            .iter()
516                            .map(|(prefix, uri)| (prefix, uri, true))
517                            .collect::<Vec<_>>(),
518                        Transform::XPathFilter2(filters) => filters
519                            .iter()
520                            .flat_map(|filter| {
521                                filter
522                                    .xpath()
523                                    .namespaces()
524                                    .iter()
525                                    .map(|(prefix, uri)| (prefix, uri, false))
526                            })
527                            .collect(),
528                        _ => Vec::new(),
529                    })
530            })
531        {
532            // Namespaces in XML reserves the declaration namespace and both
533            // sides of the `xml` binding; prefixed declarations cannot be empty.
534            if uri.is_empty() || uri == XMLNS_NS || !uri.chars().all(is_xml_1_0_character) {
535                return Err(SignatureBuilderError::InvalidNamespaceUri(uri.clone()));
536            }
537            if prefix == "xmlns"
538                || (prefix == "xml") != (uri == XML_NS)
539                || (prefix != "xml" && !is_namespace_prefix(prefix))
540            {
541                return Err(SignatureBuilderError::InvalidNamespacePrefix(
542                    prefix.clone(),
543                ));
544            }
545            // Ordinary XPath parameters share the Signature namespace prefix,
546            // while Filter2 parameters are unprefixed in their own namespace.
547            if shares_signature_namespace
548                && self.ns_prefix.as_ref() == Some(prefix)
549                && uri != XMLDSIG_NS
550            {
551                return Err(SignatureBuilderError::NamespacePrefixConflict(
552                    prefix.clone(),
553                ));
554            }
555        }
556        policy.check_signature_algorithm(self.sign_method)?;
557        if policy
558            .transforms
559            .allowed_algorithms
560            .as_ref()
561            .is_some_and(|allowed| !allowed.contains(self.c14n_method.uri()))
562        {
563            return Err(PolicyViolation::Algorithm {
564                operation: "signing transform",
565                algorithm: self.c14n_method.uri().to_owned(),
566            }
567            .into());
568        }
569        for reference in &self.references {
570            if let Some(id) = &reference.id
571                && !is_xml_ncname(id)
572            {
573                return Err(SignatureBuilderError::InvalidId {
574                    element: "Reference",
575                    value: id.clone(),
576                });
577            }
578            policy.check_digest_algorithm(reference.digest_method)?;
579        }
580        Ok(())
581    }
582}
583
584fn validate_xpath_source(
585    source: &str,
586    budget: &mut XPathSignatureParseBudget,
587) -> Result<(), SignatureBuilderError> {
588    if let Some(character) = source
589        .chars()
590        .find(|character| !is_xml_1_0_character(*character))
591    {
592        return Err(SignatureBuilderError::InvalidXPath(format!(
593            "XPath expression contains a character forbidden by XML 1.0: {character:?}"
594        )));
595    }
596    budget
597        .validate_expression(source)
598        .map_err(map_transform_validation_error)
599}
600
601fn map_transform_validation_error(error: super::TransformError) -> SignatureBuilderError {
602    match error {
603        super::TransformError::Policy(error) => SignatureBuilderError::Policy(error),
604        error => SignatureBuilderError::InvalidXPath(error.to_string()),
605    }
606}
607
608fn map_generated_mutation_error(error: super::mutation::XmlMutationError) -> SignatureBuilderError {
609    match error {
610        super::mutation::XmlMutationError::Policy(violation) => {
611            SignatureBuilderError::Policy(violation)
612        }
613        other => SignatureBuilderError::GeneratedMutation(other),
614    }
615}
616
617fn write_reference<W: Write>(
618    writer: &mut Writer<W>,
619    prefix: Option<&str>,
620    reference: &ReferenceBuilder,
621) -> Result<(), std::io::Error> {
622    let name = qualified_name(prefix, "Reference");
623    let mut attributes = Vec::new();
624    if let Some(id) = &reference.id {
625        attributes.push(("Id", id.as_str()));
626    }
627    if let Some(ref_type) = &reference.ref_type {
628        attributes.push(("Type", ref_type.as_str()));
629    }
630    attributes.push(("URI", reference.uri.as_str()));
631    writer.start(&name, attributes)?;
632
633    let generated_transforms = reference_transforms_for_generation(reference);
634    if !generated_transforms.is_empty() {
635        write_start(writer, prefix, "Transforms")?;
636        for transform in &generated_transforms {
637            write_transform(writer, prefix, transform)?;
638        }
639        write_end(writer, prefix, "Transforms")?;
640    }
641    write_algorithm(
642        writer,
643        prefix,
644        "DigestMethod",
645        reference.digest_method.uri(),
646    )?;
647    write_empty(writer, prefix, "DigestValue")?;
648    writer.end(&name)?;
649    Ok(())
650}
651
652fn reference_transforms_for_generation(reference: &ReferenceBuilder) -> Vec<Transform> {
653    let mut transforms = reference.transforms.clone();
654    let initial_binary = !reference.uri.is_empty() && !reference.uri.starts_with('#');
655    if !transform_chain_produces_binary(initial_binary, &transforms) {
656        transforms.push(Transform::C14n(C14nAlgorithm::new(
657            C14nMode::Inclusive1_1,
658            false,
659        )));
660    }
661    transforms
662}
663
664fn write_transform<W: Write>(
665    writer: &mut Writer<W>,
666    prefix: Option<&str>,
667    transform: &Transform,
668) -> Result<(), std::io::Error> {
669    match transform {
670        Transform::Enveloped => {
671            write_algorithm(writer, prefix, "Transform", ENVELOPED_SIGNATURE_URI)
672        }
673        Transform::XpathExcludeAllSignatures => {
674            let name = qualified_name(prefix, "Transform");
675            writer.start(&name, [("Algorithm", XPATH_TRANSFORM_URI)])?;
676            let xpath_name = qualified_name(prefix, "XPath");
677            let namespace = format!("xmlns:{ENVELOPED_SIGNATURE_XPATH_PREFIX}");
678            writer.start(&xpath_name, [(namespace.as_str(), XMLDSIG_NS)])?;
679            writer.text(ENVELOPED_SIGNATURE_XPATH_EXPR)?;
680            writer.end(&xpath_name)?;
681            writer.end(&name)?;
682            Ok(())
683        }
684        Transform::XPath(xpath) => {
685            let transform_name = qualified_name(prefix, "Transform");
686            writer.start(&transform_name, [("Algorithm", XPATH_TRANSFORM_URI)])?;
687            write_xpath_expression(writer, prefix, "XPath", None, xpath)?;
688            writer.end(&transform_name)?;
689            Ok(())
690        }
691        Transform::XPathFilter2(filters) => {
692            let transform_name = qualified_name(prefix, "Transform");
693            writer.start(
694                &transform_name,
695                [("Algorithm", XPATH_FILTER2_TRANSFORM_URI)],
696            )?;
697            for filter in filters {
698                write_xpath_expression(
699                    writer,
700                    None,
701                    "XPath",
702                    Some(filter.operation().as_str()),
703                    filter.xpath(),
704                )?;
705            }
706            writer.end(&transform_name)?;
707            Ok(())
708        }
709        Transform::Base64Decode => {
710            write_algorithm(writer, prefix, "Transform", BASE64_TRANSFORM_URI)
711        }
712        Transform::C14n(algorithm) if algorithm.inclusive_prefixes().is_empty() => {
713            write_algorithm(writer, prefix, "Transform", algorithm.uri())
714        }
715        Transform::C14n(algorithm) => {
716            let name = qualified_name(prefix, "Transform");
717            writer.start(&name, [("Algorithm", algorithm.uri())])?;
718
719            if algorithm.mode() == C14nMode::Exclusive1_0 {
720                let mut prefixes: Vec<&str> = algorithm
721                    .inclusive_prefixes()
722                    .iter()
723                    .map(String::as_str)
724                    .collect();
725                prefixes.sort_unstable();
726                let prefix_list = prefixes
727                    .into_iter()
728                    .map(|p| if p.is_empty() { "#default" } else { p })
729                    .collect::<Vec<_>>()
730                    .join(" ");
731                writer.empty(
732                    "ec:InclusiveNamespaces",
733                    [
734                        ("xmlns:ec", EXCLUSIVE_C14N_NS),
735                        ("PrefixList", prefix_list.as_str()),
736                    ],
737                )?;
738            }
739            writer.end(&name)?;
740            Ok(())
741        }
742    }
743}
744
745fn write_xpath_expression<W: Write>(
746    writer: &mut Writer<W>,
747    prefix: Option<&str>,
748    local_name: &str,
749    filter: Option<&str>,
750    xpath: &XPathExpression,
751) -> Result<(), std::io::Error> {
752    let name = qualified_name(prefix, local_name);
753    let namespace_attributes = xpath
754        .namespaces()
755        .iter()
756        .filter(|(namespace_prefix, _)| namespace_prefix.as_str() != "xml")
757        .map(|(namespace_prefix, uri)| (format!("xmlns:{namespace_prefix}"), uri))
758        .collect::<Vec<_>>();
759    let mut attributes = Vec::new();
760    if prefix.is_none() && filter.is_some() {
761        attributes.push(("xmlns", XPATH_FILTER2_TRANSFORM_URI));
762    }
763    if let Some(filter) = filter {
764        attributes.push(("Filter", filter));
765    }
766    for (attribute, uri) in &namespace_attributes {
767        attributes.push((attribute.as_str(), uri.as_str()));
768    }
769    writer.start(&name, attributes)?;
770    writer.text(xpath.expression())?;
771    writer.end(&name)?;
772    Ok(())
773}
774
775fn write_algorithm<W: Write>(
776    writer: &mut Writer<W>,
777    prefix: Option<&str>,
778    local_name: &str,
779    algorithm: &str,
780) -> Result<(), std::io::Error> {
781    let name = qualified_name(prefix, local_name);
782    writer.empty(&name, [("Algorithm", algorithm)])?;
783    Ok(())
784}
785
786fn write_start<W: Write>(
787    writer: &mut Writer<W>,
788    prefix: Option<&str>,
789    local_name: &str,
790) -> Result<(), std::io::Error> {
791    writer.start(&qualified_name(prefix, local_name), [])?;
792    Ok(())
793}
794
795fn write_end<W: Write>(
796    writer: &mut Writer<W>,
797    prefix: Option<&str>,
798    local_name: &str,
799) -> Result<(), std::io::Error> {
800    writer.end(&qualified_name(prefix, local_name))?;
801    Ok(())
802}
803
804fn write_empty<W: Write>(
805    writer: &mut Writer<W>,
806    prefix: Option<&str>,
807    local_name: &str,
808) -> Result<(), std::io::Error> {
809    writer.empty(&qualified_name(prefix, local_name), [])?;
810    Ok(())
811}
812
813fn qualified_name(prefix: Option<&str>, local_name: &str) -> String {
814    prefix.map_or_else(
815        || local_name.to_owned(),
816        |prefix| format!("{prefix}:{local_name}"),
817    )
818}
819
820fn is_namespace_prefix(value: &str) -> bool {
821    // Namespaces in XML reserves these names regardless of the URI being bound.
822    !matches!(value, "xml" | "xmlns") && is_xml_ncname(value)
823}