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