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