Skip to main content

xml_sec/xmldsig/
mutation.rs

1//! Lexical XML mutation helpers for the XMLDSig signing pipeline.
2//!
3//! The selected semantic DOM is immutable. These helpers validate structure
4//! through the backend-neutral DOM contract, then splice validated source ranges.
5
6use crate::xml_input as xml_sec_xml_input;
7use std::ops::Range;
8
9use xml_sec_xml_input::lexical::{escape_attribute, escape_text};
10
11use super::parse::{XMLDSIG_NS, XMLDSIG11_NS};
12use super::whitespace::is_xml_whitespace_only;
13use crate::document::{
14    DocumentParseSettings, XmlDocumentError, XmlParseWorkBudget,
15    parse_borrowed_with_settings_and_budget,
16};
17
18pub(super) fn parse_with_options_and_budget<'a>(
19    xml: &'a str,
20    settings: DocumentParseSettings,
21    budget: Option<&XmlParseWorkBudget>,
22) -> Result<crate::xml::dom::Document<'a>, XmlDocumentError> {
23    parse_borrowed_with_settings_and_budget(xml, settings, budget)
24}
25
26fn map_mutation_parse_error(
27    error: XmlDocumentError,
28    settings: DocumentParseSettings,
29) -> XmlMutationError {
30    match error.into_policy_violation(settings) {
31        Ok(error) => XmlMutationError::Policy(error),
32        Err(XmlDocumentError::Parse(error)) => XmlMutationError::XmlParse(error),
33        Err(error) => XmlMutationError::Document(error),
34    }
35}
36
37fn parse_mutation_xml_with_options<'a>(
38    xml: &'a str,
39    policy: Option<&crate::policy::SigningPolicy>,
40) -> Result<crate::xml::dom::Document<'a>, XmlMutationError> {
41    parse_mutation_xml_with_budget(xml, policy, None)
42}
43
44fn parse_mutation_xml_with_budget<'a>(
45    xml: &'a str,
46    policy: Option<&crate::policy::SigningPolicy>,
47    budget: Option<&XmlParseWorkBudget>,
48) -> Result<crate::xml::dom::Document<'a>, XmlMutationError> {
49    let settings = policy
50        .map(|policy| DocumentParseSettings::from_policy(&policy.xml, &policy.resources))
51        .unwrap_or_default();
52    parse_with_options_and_budget(xml, settings, budget)
53        .map_err(|error| map_mutation_parse_error(error, settings))
54}
55
56fn actionable_node_range(
57    node: crate::xml::dom::Node<'_, '_>,
58) -> Result<Range<usize>, XmlMutationError> {
59    if !node.has_actionable_range() {
60        return Err(XmlDocumentError::EntityExpandedMutationTarget.into());
61    }
62    Ok(node.range())
63}
64
65fn order_disjoint_ranges_for_splicing(
66    ranges: &mut [Range<usize>],
67    source_len: usize,
68) -> Result<(), XmlMutationError> {
69    ranges.sort_by_key(|range| range.start);
70    if ranges
71        .iter()
72        .any(|range| range.start > range.end || range.end > source_len)
73        || ranges.windows(2).any(|pair| pair[0].end > pair[1].start)
74    {
75        return Err(XmlDocumentError::InvalidReplacement(
76            "mutation target ranges must be valid and disjoint".into(),
77        )
78        .into());
79    }
80    ranges.reverse();
81    Ok(())
82}
83
84/// Errors produced by XMLDSig XML mutation helpers.
85#[derive(Debug, thiserror::Error)]
86pub enum XmlMutationError {
87    /// The compiled signing policy rejected an intermediate XML document.
88    #[error("signing policy violation: {0}")]
89    Policy(#[from] crate::policy::PolicyViolation),
90    /// Input XML or generated template is not parseable XML.
91    #[error("XML parsing error: {0}")]
92    XmlParse(#[from] crate::xml::dom::ParseError),
93    /// The backend-neutral document boundary rejected generated XML.
94    #[error("XML document error: {0}")]
95    Document(#[from] XmlDocumentError),
96    /// The shared lexical scanner rejected generated XML.
97    #[error("XML lexical error: {0}")]
98    Lexical(#[from] xml_sec_xml_input::lexical::Error),
99    /// A template did not contain exactly one XMLDSig `<Signature>` root.
100    #[error("signature template root must be one XMLDSig Signature element")]
101    InvalidSignatureTemplate,
102    /// A replacement call supplied a different number of values than matching elements.
103    #[error("expected {expected} XMLDSig {element} values, got {actual}")]
104    ValueCountMismatch {
105        /// XMLDSig element local name.
106        element: &'static str,
107        /// Number of matching XMLDSig elements in the document.
108        expected: usize,
109        /// Number of values supplied by the caller.
110        actual: usize,
111    },
112    /// The source XML did not contain a root element that can receive a signature.
113    #[error("source XML must contain a root element")]
114    MissingRootElement,
115    /// The selected source element cannot receive an appended signature.
116    #[error("selected source element cannot receive a signature")]
117    InvalidAppendTarget,
118    /// A key-info writer emitted no element child to merge.
119    #[error("key-info writer emitted no element child")]
120    EmptyKeyInfoSource,
121    /// A reusable placeholder binds a generated namespace prefix differently.
122    #[error("key-info placeholder conflicts with generated namespace prefix {prefix}")]
123    ConflictingKeyInfoNamespace {
124        /// The prefix whose namespace URI differs.
125        prefix: String,
126    },
127    /// A reusable placeholder carries a different value for a generated attribute.
128    #[error("key-info placeholder conflicts with generated attribute {name}")]
129    ConflictingKeyInfoAttribute {
130        /// The expanded-name local component of the conflicting attribute.
131        name: String,
132    },
133}
134
135/// Append a generated XMLDSig `<Signature>` template as the last child of the
136/// source document root.
137pub fn append_signature_to_root(
138    xml: &str,
139    signature_template: &str,
140) -> Result<String, XmlMutationError> {
141    append_signature_to_root_with_options(xml, signature_template, None)
142}
143
144pub(super) fn append_signature_to_root_with_options(
145    xml: &str,
146    signature_template: &str,
147    policy: Option<&crate::policy::SigningPolicy>,
148) -> Result<String, XmlMutationError> {
149    validate_signature_template(signature_template, policy)?;
150    let source = parse_mutation_xml_with_options(xml, policy)?;
151    let root = source.root_element();
152    if !root.is_element() {
153        return Err(XmlMutationError::MissingRootElement);
154    }
155    let root_range = root.range();
156    let output = if xml[root_range.clone()].trim_end().ends_with("/>") {
157        replace_element_content(xml, root_range, signature_template, "", policy)?
158    } else {
159        append_element_content(xml, root_range, signature_template, "", &[], policy)?
160    };
161    parse_mutation_xml_with_options(&output, policy)?;
162    Ok(output)
163}
164
165/// Fill XMLDSig `<DigestValue>` elements in document order.
166pub fn fill_digest_values<I, S>(xml: &str, values: I) -> Result<String, XmlMutationError>
167where
168    I: IntoIterator<Item = S>,
169    S: AsRef<str>,
170{
171    fill_dsig_values(xml, "DigestValue", values)
172}
173
174/// Fill `<DigestValue>` elements for direct `<SignedInfo>/<Reference>` children.
175pub fn fill_signed_info_digest_values<I, S>(
176    xml: &str,
177    values: I,
178) -> Result<String, XmlMutationError>
179where
180    I: IntoIterator<Item = S>,
181    S: AsRef<str>,
182{
183    fill_signed_info_digest_values_with_options(xml, values, None)
184}
185
186pub(super) fn fill_signed_info_digest_values_with_options<I, S>(
187    xml: &str,
188    values: I,
189    policy: Option<&crate::policy::SigningPolicy>,
190) -> Result<String, XmlMutationError>
191where
192    I: IntoIterator<Item = S>,
193    S: AsRef<str>,
194{
195    fill_signed_info_digest_values_with_budget(xml, values, policy, None)
196}
197
198pub(super) fn fill_signed_info_digest_values_with_budget<I, S>(
199    xml: &str,
200    values: I,
201    policy: Option<&crate::policy::SigningPolicy>,
202    budget: Option<&XmlParseWorkBudget>,
203) -> Result<String, XmlMutationError>
204where
205    I: IntoIterator<Item = S>,
206    S: AsRef<str>,
207{
208    let target_signature = last_signature_index(xml, policy, budget)?;
209    fill_signed_info_digest_values_at_index_with_budget(
210        xml,
211        values,
212        target_signature,
213        policy,
214        budget,
215    )
216}
217
218#[cfg(test)]
219pub(super) fn fill_signed_info_digest_values_at_index_with_options<I, S>(
220    xml: &str,
221    values: I,
222    target_signature: usize,
223    policy: Option<&crate::policy::SigningPolicy>,
224) -> Result<String, XmlMutationError>
225where
226    I: IntoIterator<Item = S>,
227    S: AsRef<str>,
228{
229    fill_signed_info_digest_values_at_index_with_budget(xml, values, target_signature, policy, None)
230}
231
232pub(super) fn fill_signed_info_digest_values_at_index_with_budget<I, S>(
233    xml: &str,
234    values: I,
235    target_signature: usize,
236    policy: Option<&crate::policy::SigningPolicy>,
237    budget: Option<&XmlParseWorkBudget>,
238) -> Result<String, XmlMutationError>
239where
240    I: IntoIterator<Item = S>,
241    S: AsRef<str>,
242{
243    let values: Vec<String> = values
244        .into_iter()
245        .map(|value| value.as_ref().to_owned())
246        .collect();
247    fill_dsig_text_values_matching(
248        xml,
249        "DigestValue",
250        values,
251        policy,
252        budget,
253        DsigReplacementScope::SignedInfoReferenceDigest {
254            signature_index: target_signature,
255        },
256    )
257}
258
259/// Fill XMLDSig `<SignatureValue>` elements in document order.
260pub fn fill_signature_values<I, S>(xml: &str, values: I) -> Result<String, XmlMutationError>
261where
262    I: IntoIterator<Item = S>,
263    S: AsRef<str>,
264{
265    fill_dsig_values(xml, "SignatureValue", values)
266}
267
268/// Fill the direct `<Signature>/<SignatureValue>` child for a signing template.
269pub fn fill_signature_value(xml: &str, value: &str) -> Result<String, XmlMutationError> {
270    fill_signature_value_with_options(xml, value, None)
271}
272
273pub(super) fn fill_signature_value_with_options(
274    xml: &str,
275    value: &str,
276    policy: Option<&crate::policy::SigningPolicy>,
277) -> Result<String, XmlMutationError> {
278    fill_signature_value_with_budget(xml, value, policy, None)
279}
280
281pub(super) fn fill_signature_value_with_budget(
282    xml: &str,
283    value: &str,
284    policy: Option<&crate::policy::SigningPolicy>,
285    budget: Option<&XmlParseWorkBudget>,
286) -> Result<String, XmlMutationError> {
287    let target_signature = last_signature_index(xml, policy, budget)?;
288    fill_signature_value_at_index_with_budget(xml, value, target_signature, policy, budget)
289}
290
291#[cfg(test)]
292pub(super) fn fill_signature_value_at_index_with_options(
293    xml: &str,
294    value: &str,
295    target_signature: usize,
296    policy: Option<&crate::policy::SigningPolicy>,
297) -> Result<String, XmlMutationError> {
298    fill_signature_value_at_index_with_budget(xml, value, target_signature, policy, None)
299}
300
301pub(super) fn fill_signature_value_at_index_with_budget(
302    xml: &str,
303    value: &str,
304    target_signature: usize,
305    policy: Option<&crate::policy::SigningPolicy>,
306    budget: Option<&XmlParseWorkBudget>,
307) -> Result<String, XmlMutationError> {
308    fill_dsig_text_values_matching(
309        xml,
310        "SignatureValue",
311        vec![value.to_owned()],
312        policy,
313        budget,
314        DsigReplacementScope::DirectSignatureChild {
315            signature_index: target_signature,
316        },
317    )
318}
319
320#[cfg(test)]
321pub(super) fn projected_signature_value_output_len_at_index_with_options(
322    xml: &str,
323    value_len: usize,
324    target_signature: usize,
325    policy: Option<&crate::policy::SigningPolicy>,
326) -> Result<usize, XmlMutationError> {
327    projected_signature_value_output_len_at_index_with_budget(
328        xml,
329        value_len,
330        target_signature,
331        policy,
332        None,
333    )
334}
335
336pub(super) fn projected_signature_value_output_len_at_index_with_budget(
337    xml: &str,
338    value_len: usize,
339    target_signature: usize,
340    policy: Option<&crate::policy::SigningPolicy>,
341    budget: Option<&XmlParseWorkBudget>,
342) -> Result<usize, XmlMutationError> {
343    let document = parse_mutation_xml_with_budget(xml, policy, budget)?;
344    let Some(signature) = signature_node(&document, target_signature) else {
345        return Err(XmlMutationError::ValueCountMismatch {
346            element: "SignatureValue",
347            expected: 0,
348            actual: 1,
349        });
350    };
351    let mut signature_values = signature
352        .children()
353        .filter(|node| is_dsig_node(*node, "SignatureValue"));
354    let Some(signature_value) = signature_values.next() else {
355        return Err(XmlMutationError::ValueCountMismatch {
356            element: "SignatureValue",
357            expected: 0,
358            actual: 1,
359        });
360    };
361    let remaining = signature_values.count();
362    if remaining != 0 {
363        return Err(XmlMutationError::ValueCountMismatch {
364            element: "SignatureValue",
365            expected: remaining + 1,
366            actual: 1,
367        });
368    }
369
370    let range = signature_value.range();
371    let element = &xml[range.clone()];
372    let replacement_len = if element.trim_end().ends_with("/>") {
373        let name_end = element[1..]
374            .find(|character: char| {
375                character.is_ascii_whitespace() || character == '/' || character == '>'
376            })
377            .map(|offset| offset + 1)
378            .ok_or(XmlMutationError::InvalidAppendTarget)?;
379        let qualified_name_len = name_end - 1;
380        qualified_name_len
381            .checked_add(2)
382            .and_then(|closing_markup_len| {
383                element
384                    .len()
385                    .checked_add(value_len)
386                    .and_then(|length| length.checked_add(closing_markup_len))
387            })
388    } else {
389        let existing_content_len = element_inner_xml(xml, range.clone())?.len();
390        element
391            .len()
392            .checked_sub(existing_content_len)
393            .and_then(|length| length.checked_add(value_len))
394    };
395    let projected = replacement_len
396        .and_then(|replacement_len| {
397            xml.len()
398                .checked_sub(element.len())
399                .map(|base| (base, replacement_len))
400        })
401        .and_then(|(base, replacement_len)| base.checked_add(replacement_len));
402    projected.ok_or_else(|| projected_xml_length_overflow(policy))
403}
404
405pub(super) fn padded_base64_len_for_xml(
406    decoded_len: usize,
407    policy: &crate::policy::SigningPolicy,
408) -> Result<usize, XmlMutationError> {
409    base64::encoded_len(decoded_len, true)
410        .ok_or_else(|| projected_xml_length_overflow(Some(policy)))
411}
412
413pub(super) fn zero_base64_placeholder(decoded_len: usize, encoded_len: usize) -> String {
414    let padding_len = (3 - decoded_len % 3) % 3;
415    let mut placeholder = String::with_capacity(encoded_len);
416    placeholder.extend(std::iter::repeat_n('A', encoded_len - padding_len));
417    placeholder.extend(std::iter::repeat_n('=', padding_len));
418    placeholder
419}
420
421fn projected_xml_length_overflow(
422    policy: Option<&crate::policy::SigningPolicy>,
423) -> XmlMutationError {
424    policy.map_or(XmlMutationError::InvalidAppendTarget, |policy| {
425        XmlMutationError::Policy(crate::policy::PolicyViolation::ResourceLimit {
426            resource: crate::policy::resource_name::XML_DOCUMENT,
427            maximum: policy.resources.max_xml_document_bytes,
428            actual: usize::MAX,
429        })
430    })
431}
432
433/// Fill the direct `<Signature>/<KeyInfo>` child with XML child content.
434pub fn fill_key_info(xml: &str, key_info_content: &str) -> Result<String, XmlMutationError> {
435    fill_key_info_with_options(xml, key_info_content, None)
436}
437
438pub(super) fn fill_key_info_with_options(
439    xml: &str,
440    key_info_content: &str,
441    policy: Option<&crate::policy::SigningPolicy>,
442) -> Result<String, XmlMutationError> {
443    let target_signature = last_signature_index(xml, policy, None)?;
444    fill_key_info_at_index_with_options(xml, key_info_content, target_signature, policy)
445}
446
447pub(super) fn fill_key_info_at_index_with_options(
448    xml: &str,
449    key_info_content: &str,
450    target_signature: usize,
451    policy: Option<&crate::policy::SigningPolicy>,
452) -> Result<String, XmlMutationError> {
453    fill_dsig_raw_value_matching(
454        xml,
455        "KeyInfo",
456        key_info_content,
457        policy,
458        None,
459        DsigReplacementScope::DirectSignatureChild {
460            signature_index: target_signature,
461        },
462    )
463}
464
465#[cfg(test)]
466pub(super) fn merge_key_info_source_at_index_with_options(
467    xml: &str,
468    key_info_source: &str,
469    target_signature: usize,
470    policy: Option<&crate::policy::SigningPolicy>,
471) -> Result<String, XmlMutationError> {
472    merge_key_info_source_at_index_with_budget(xml, key_info_source, target_signature, policy, None)
473}
474
475pub(super) fn merge_key_info_source_at_index_with_budget(
476    xml: &str,
477    key_info_source: &str,
478    target_signature: usize,
479    policy: Option<&crate::policy::SigningPolicy>,
480    budget: Option<&XmlParseWorkBudget>,
481) -> Result<String, XmlMutationError> {
482    let document = parse_mutation_xml_with_budget(xml, policy, budget)?;
483    let Some(signature) = signature_node(&document, target_signature) else {
484        return Err(XmlMutationError::ValueCountMismatch {
485            element: "Signature",
486            expected: 1,
487            actual: 0,
488        });
489    };
490    let key_infos = signature
491        .children()
492        .filter(|node| is_dsig_node(*node, "KeyInfo"))
493        .collect::<Vec<_>>();
494    if key_infos.len() != 1 {
495        return Err(XmlMutationError::ValueCountMismatch {
496            element: "KeyInfo",
497            expected: 1,
498            actual: key_infos.len(),
499        });
500    }
501    let key_info = key_infos[0];
502
503    // The writer contract is XML child content, not a standalone document.
504    // Parse it under the template's namespace context so multiple siblings and
505    // inherited prefixes have exactly the semantics they will have in KeyInfo.
506    let wrapped_source = wrap_key_info_children(key_info_source, key_info, policy)?;
507    let source_document = parse_mutation_xml_with_budget(&wrapped_source, policy, budget)?;
508    let sources = source_document
509        .root_element()
510        .children()
511        .filter(|node| node.is_element())
512        .map(|node| {
513            Ok((
514                node.tag_name().namespace().map(str::to_owned),
515                node.tag_name().name().to_owned(),
516                standalone_element(&wrapped_source, node)?,
517            ))
518        })
519        .collect::<Result<Vec<_>, XmlMutationError>>()?;
520    if sources.is_empty() {
521        return Err(XmlMutationError::EmptyKeyInfoSource);
522    }
523
524    let generated_key_material_sources = sources
525        .iter()
526        .filter(|(namespace, name, _)| is_cryptographic_key_info_source(namespace.as_deref(), name))
527        .map(|(namespace, name, _)| (namespace.as_deref(), name.as_str()))
528        .collect::<Vec<_>>();
529    let generated_key_name = sources
530        .iter()
531        .any(|(namespace, name, _)| is_dsig_key_name(namespace.as_deref(), name));
532    let generated_x509_data = generated_key_material_sources
533        .iter()
534        .any(|(namespace, name)| is_dsig_x509_data(*namespace, name));
535    let mut output = xml.to_owned();
536    if !generated_key_material_sources.is_empty() || generated_key_name {
537        // Writer-provided identity is authoritative within its own group.
538        // Generated key material replaces stale material, while KeyName is
539        // replaced only by a generated KeyName; extension elements remain.
540        let mut stale_ranges = Vec::new();
541        for node in key_info.children().filter(|node| node.is_element()) {
542            let replaces_key_material = !generated_key_material_sources.is_empty()
543                && is_cryptographic_key_info_source(
544                    node.tag_name().namespace(),
545                    node.tag_name().name(),
546                );
547            let replaces_key_name = generated_key_name
548                && is_dsig_key_name(node.tag_name().namespace(), node.tag_name().name());
549            if !(replaces_key_material || replaces_key_name)
550                || !has_cryptographic_identity_content(node)
551            {
552                continue;
553            }
554            if generated_x509_data
555                && is_dsig_x509_data(node.tag_name().namespace(), node.tag_name().name())
556            {
557                for child in node
558                    .children()
559                    .filter(|child| child.is_element() && is_x509_identity_child(*child))
560                {
561                    stale_ranges.push(actionable_node_range(child)?);
562                }
563            } else {
564                stale_ranges.push(actionable_node_range(node)?);
565            }
566        }
567        order_disjoint_ranges_for_splicing(&mut stale_ranges, output.len())?;
568        for range in stale_ranges {
569            output.replace_range(range, "");
570        }
571    }
572
573    for (_, _, source) in sources {
574        output = merge_one_key_info_source_at_index_with_options(
575            &output,
576            &source,
577            target_signature,
578            policy,
579            budget,
580        )?;
581    }
582    Ok(output)
583}
584
585fn merge_one_key_info_source_at_index_with_options(
586    xml: &str,
587    key_info_source: &str,
588    target_signature: usize,
589    policy: Option<&crate::policy::SigningPolicy>,
590    budget: Option<&XmlParseWorkBudget>,
591) -> Result<String, XmlMutationError> {
592    let document = parse_mutation_xml_with_budget(xml, policy, budget)?;
593    let source_document = parse_mutation_xml_with_budget(key_info_source, policy, budget)?;
594    let source = source_document.root_element();
595    let source_content = element_inner_xml(key_info_source, source.range())?;
596    let Some(signature) = signature_node(&document, target_signature) else {
597        return Err(XmlMutationError::ValueCountMismatch {
598            element: "Signature",
599            expected: 1,
600            actual: 0,
601        });
602    };
603    let key_infos = signature
604        .children()
605        .filter(|node| is_dsig_node(*node, "KeyInfo"))
606        .collect::<Vec<_>>();
607    if key_infos.len() != 1 {
608        return Err(XmlMutationError::ValueCountMismatch {
609            element: "KeyInfo",
610            expected: 1,
611            actual: key_infos.len(),
612        });
613    }
614    let key_info = key_infos[0];
615
616    let source_is_x509_data =
617        is_dsig_x509_data(source.tag_name().namespace(), source.tag_name().name());
618    if let Some(placeholder) = key_info.children().find(|node| {
619        node.is_element()
620            && node.tag_name() == source.tag_name()
621            && (is_reusable_placeholder(*node)
622                || (source_is_x509_data && has_x509_mergeable_metadata(*node)))
623    }) {
624        let placeholder_fragment = &xml[placeholder.range()];
625        let placeholder_opening_end = element_opening_end(placeholder_fragment)
626            .ok_or(XmlMutationError::InvalidAppendTarget)?;
627        let placeholder_owned_namespaces =
628            owned_namespace_declarations(&placeholder_fragment[..placeholder_opening_end - 1])?;
629        let generated_namespace_attributes =
630            source
631                .namespaces()
632                .try_fold(String::new(), |mut attributes, namespace| {
633                    let prefix = namespace.name().unwrap_or_default();
634                    if placeholder_owned_namespaces.contains(prefix) {
635                        let declared = placeholder
636                            .namespaces()
637                            .find(|declared| declared.name() == namespace.name())
638                            .ok_or(XmlMutationError::InvalidAppendTarget)?;
639                        if declared.uri() != namespace.uri() {
640                            return Err(XmlMutationError::ConflictingKeyInfoNamespace {
641                                prefix: prefix.to_owned(),
642                            });
643                        }
644                        return Ok(attributes);
645                    }
646                    if placeholder
647                        .parent_element()
648                        .and_then(|parent| parent.lookup_namespace_uri(namespace.name()))
649                        == Some(namespace.uri())
650                    {
651                        return Ok(attributes);
652                    }
653                    let attribute = namespace
654                        .name()
655                        .map_or_else(|| "xmlns".to_owned(), |prefix| format!("xmlns:{prefix}"));
656                    attributes.push_str(&format!(
657                        " {attribute}=\"{}\"",
658                        escape_attribute(namespace.uri())
659                    ));
660                    Ok(attributes)
661                })?;
662        let generated_attributes =
663            source
664                .attributes()
665                .try_fold(String::new(), |mut attributes, attribute| {
666                    let existing = placeholder.attributes().find(|candidate| {
667                        candidate.namespace() == attribute.namespace()
668                            && candidate.name() == attribute.name()
669                    });
670                    if let Some(existing) = existing {
671                        if existing.value() != attribute.value() {
672                            return Err(XmlMutationError::ConflictingKeyInfoAttribute {
673                                name: attribute.name().to_owned(),
674                            });
675                        }
676                        return Ok(attributes);
677                    }
678                    let qualified_name = match attribute.namespace() {
679                        None => attribute.name().to_owned(),
680                        Some("http://www.w3.org/XML/1998/namespace") => {
681                            format!("xml:{}", attribute.name())
682                        }
683                        Some(namespace) => {
684                            let prefix = source
685                                .lookup_prefix(namespace)
686                                .ok_or(XmlMutationError::InvalidAppendTarget)?;
687                            format!("{prefix}:{}", attribute.name())
688                        }
689                    };
690                    attributes.push_str(&format!(
691                        " {qualified_name}=\"{}\"",
692                        escape_attribute(attribute.value())
693                    ));
694                    Ok(attributes)
695                })?;
696        let generated_attributes =
697            format!("{generated_namespace_attributes}{generated_attributes}");
698        let output = if is_reusable_placeholder(placeholder) {
699            replace_element_content(
700                xml,
701                placeholder.range(),
702                source_content,
703                &generated_attributes,
704                policy,
705            )?
706        } else {
707            let retained_bindings = source
708                .namespaces()
709                .any(|binding| {
710                    placeholder.lookup_namespace_uri(binding.name()) != Some(binding.uri())
711                })
712                .then(|| preserve_retained_child_namespaces(xml, placeholder, source))
713                .transpose()?
714                .unwrap_or_default();
715            append_element_content(
716                xml,
717                placeholder.range(),
718                source_content,
719                &generated_attributes,
720                &retained_bindings,
721                policy,
722            )?
723        };
724        parse_mutation_xml_with_budget(&output, policy, budget)?;
725        return Ok(output);
726    }
727
728    let range = key_info.range();
729    let raw_key_info = &xml[range.clone()];
730    let output = if raw_key_info.trim_end().ends_with("/>") {
731        let name_end = raw_key_info[1..]
732            .find(|character: char| {
733                character.is_ascii_whitespace() || character == '/' || character == '>'
734            })
735            .map(|offset| offset + 1)
736            .ok_or(XmlMutationError::InvalidAppendTarget)?;
737        let qualified_name = &raw_key_info[1..name_end];
738        let empty_end = raw_key_info
739            .rfind("/>")
740            .ok_or(XmlMutationError::InvalidAppendTarget)?;
741        let expanded_len = empty_end
742            .checked_add(1)
743            .and_then(|length| length.checked_add(key_info_source.len()))
744            .and_then(|length| length.checked_add(2))
745            .and_then(|length| length.checked_add(qualified_name.len()))
746            .and_then(|length| length.checked_add(1))
747            .ok_or_else(|| projected_xml_length_overflow(policy))?;
748        validate_projected_replacement_len(xml, range.len(), expanded_len, policy)?;
749        let expanded = format!(
750            "{}>{}</{}>",
751            &raw_key_info[..empty_end],
752            key_info_source,
753            qualified_name
754        );
755        let mut output = xml.to_owned();
756        output.replace_range(range, &expanded);
757        output
758    } else {
759        let closing = raw_key_info
760            .rfind("</")
761            .map(|offset| range.start + offset)
762            .ok_or(XmlMutationError::InvalidAppendTarget)?;
763        validate_projected_replacement_len(xml, 0, key_info_source.len(), policy)?;
764        let mut output = xml.to_owned();
765        output.insert_str(closing, key_info_source);
766        output
767    };
768    parse_mutation_xml_with_budget(&output, policy, budget)?;
769    Ok(output)
770}
771
772fn wrap_key_info_children(
773    source: &str,
774    key_info: crate::xml::dom::Node<'_, '_>,
775    policy: Option<&crate::policy::SigningPolicy>,
776) -> Result<String, XmlMutationError> {
777    const OPEN: &str = "<KeyInfoFragment";
778    const CLOSE: &str = "</KeyInfoFragment>";
779    let projected = key_info
780        .namespaces()
781        .try_fold(OPEN.len(), |length, namespace| {
782            let declaration_len = match namespace.name() {
783                Some(prefix) => "xmlns:"
784                    .len()
785                    .checked_add(prefix.len())
786                    .ok_or_else(|| projected_xml_length_overflow(policy))?,
787                None => "xmlns".len(),
788            };
789            let escaped_uri = escape_attribute(namespace.uri());
790            length
791                .checked_add(4)
792                .and_then(|length| length.checked_add(declaration_len))
793                .and_then(|length| length.checked_add(escaped_uri.len()))
794                .ok_or_else(|| projected_xml_length_overflow(policy))
795        })?;
796    let projected = projected
797        .checked_add(1)
798        .and_then(|length| length.checked_add(source.len()))
799        .and_then(|length| length.checked_add(CLOSE.len()))
800        .ok_or_else(|| projected_xml_length_overflow(policy))?;
801    if let Some(policy) = policy {
802        policy.resources.validate_xml_document_len(projected)?;
803    }
804
805    let mut wrapper = String::with_capacity(projected);
806    wrapper.push_str(OPEN);
807    for namespace in key_info.namespaces() {
808        let declaration = namespace
809            .name()
810            .map_or_else(|| "xmlns".to_owned(), |prefix| format!("xmlns:{prefix}"));
811        wrapper.push_str(&format!(
812            " {declaration}=\"{}\"",
813            escape_attribute(namespace.uri())
814        ));
815    }
816    wrapper.push('>');
817    wrapper.push_str(source);
818    wrapper.push_str(CLOSE);
819    Ok(wrapper)
820}
821
822fn standalone_element(
823    source: &str,
824    node: crate::xml::dom::Node<'_, '_>,
825) -> Result<String, XmlMutationError> {
826    let fragment = &source[node.range()];
827    let opening_end = element_opening_end(fragment).ok_or(XmlMutationError::InvalidAppendTarget)?;
828    let opening = &fragment[..opening_end - 1];
829    let namespace_insertion = opening.strip_suffix('/').map_or(opening.len(), str::len);
830    let mut output = opening[..namespace_insertion].to_owned();
831    let owned_namespaces = owned_namespace_declarations(opening)?;
832    for namespace in node.namespaces() {
833        let declaration = namespace
834            .name()
835            .map_or_else(|| "xmlns".to_owned(), |prefix| format!("xmlns:{prefix}"));
836        if !owned_namespaces.contains(namespace.name().unwrap_or_default()) {
837            output.push_str(&format!(
838                " {declaration}=\"{}\"",
839                escape_attribute(namespace.uri())
840            ));
841        }
842    }
843    output.push_str(&opening[namespace_insertion..]);
844    output.push_str(&fragment[opening_end - 1..]);
845    Ok(output)
846}
847
848fn owned_namespace_declarations(
849    opening: &str,
850) -> Result<xml_sec_xml_input::lexical::DeclaredNamespacePrefixes, XmlMutationError> {
851    xml_sec_xml_input::lexical::declared_namespace_prefixes(opening).map_err(Into::into)
852}
853
854fn is_reusable_placeholder(node: crate::xml::dom::Node<'_, '_>) -> bool {
855    node.children()
856        .all(|child| child.is_text() && child.text().is_some_and(is_xml_whitespace_only))
857}
858
859fn has_cryptographic_identity_content(node: crate::xml::dom::Node<'_, '_>) -> bool {
860    if is_dsig_x509_data(node.tag_name().namespace(), node.tag_name().name()) {
861        return node
862            .children()
863            .any(|child| child.is_element() && is_x509_identity_child(child));
864    }
865    if node.children().any(|child| child.is_element()) {
866        return true;
867    }
868    match (node.tag_name().namespace(), node.tag_name().name()) {
869        (Some(XMLDSIG_NS), "KeyName") => node
870            .children()
871            .filter_map(|child| child.text())
872            .any(|text| !is_xml_whitespace_only(text)),
873        (Some(XMLDSIG_NS), "RetrievalMethod") => node.attribute("URI").is_some(),
874        (Some(XMLDSIG11_NS), "DEREncodedKeyValue") => node
875            .children()
876            .filter_map(|child| child.text())
877            .any(|text| !is_xml_whitespace_only(text)),
878        (Some(XMLDSIG11_NS), "KeyInfoReference") => node.attribute("URI").is_some(),
879        _ => false,
880    }
881}
882
883fn is_dsig_x509_data(namespace: Option<&str>, name: &str) -> bool {
884    namespace == Some(XMLDSIG_NS) && name == "X509Data"
885}
886
887fn is_x509_identity_child(node: crate::xml::dom::Node<'_, '_>) -> bool {
888    matches!(
889        (node.tag_name().namespace(), node.tag_name().name()),
890        (
891            Some(XMLDSIG_NS),
892            "X509IssuerSerial" | "X509SKI" | "X509SubjectName" | "X509Certificate"
893        ) | (Some("http://www.w3.org/2009/xmldsig11#"), "X509Digest")
894    )
895}
896
897fn has_x509_mergeable_metadata(node: crate::xml::dom::Node<'_, '_>) -> bool {
898    node.children().any(|child| child.is_element()) && !has_cryptographic_identity_content(node)
899}
900
901fn is_cryptographic_key_info_source(namespace: Option<&str>, name: &str) -> bool {
902    matches!(
903        (namespace, name),
904        (
905            Some(XMLDSIG_NS),
906            "KeyValue" | "RetrievalMethod" | "X509Data" | "PGPData" | "SPKIData"
907        ) | (
908            Some(XMLDSIG11_NS),
909            "DEREncodedKeyValue" | "KeyInfoReference"
910        )
911    )
912}
913
914fn is_dsig_key_name(namespace: Option<&str>, name: &str) -> bool {
915    namespace == Some(XMLDSIG_NS) && name == "KeyName"
916}
917
918fn element_inner_xml(xml: &str, range: Range<usize>) -> Result<&str, XmlMutationError> {
919    let element = &xml[range];
920    if element.trim_end().ends_with("/>") {
921        return Ok("");
922    }
923    let content_start =
924        element_opening_end(element).ok_or(XmlMutationError::InvalidAppendTarget)?;
925    let content_end = element
926        .rfind("</")
927        .ok_or(XmlMutationError::InvalidAppendTarget)?;
928    Ok(&element[content_start..content_end])
929}
930
931fn replace_element_content(
932    xml: &str,
933    range: Range<usize>,
934    content: &str,
935    namespace_attributes: &str,
936    policy: Option<&crate::policy::SigningPolicy>,
937) -> Result<String, XmlMutationError> {
938    let element = xml
939        .get(range.clone())
940        .ok_or(XmlMutationError::InvalidAppendTarget)?;
941    let parts = element_content_parts(element, content, namespace_attributes)?;
942    let replacement_len = replacement_parts_len(&parts, policy)?;
943    let projected = validate_projected_replacement_len(xml, range.len(), replacement_len, policy)?;
944    let mut output = String::with_capacity(projected);
945    output.push_str(&xml[..range.start]);
946    for part in parts {
947        output.push_str(part);
948    }
949    output.push_str(&xml[range.end..]);
950    Ok(output)
951}
952
953// Describe the splice with borrowed segments. Sizing and emission share this representation,
954// so even a rejected large replacement never materializes an intermediate XML string.
955fn element_content_parts<'a>(
956    element: &'a str,
957    content: &'a str,
958    namespace_attributes: &'a str,
959) -> Result<[&'a str; 7], XmlMutationError> {
960    if element.trim_end().ends_with("/>") {
961        let name_end = element[1..]
962            .find(|character: char| {
963                character.is_ascii_whitespace() || character == '/' || character == '>'
964            })
965            .map(|offset| offset + 1)
966            .ok_or(XmlMutationError::InvalidAppendTarget)?;
967        let qualified_name = &element[1..name_end];
968        let empty_end = element
969            .rfind("/>")
970            .ok_or(XmlMutationError::InvalidAppendTarget)?;
971        Ok([
972            &element[..empty_end],
973            namespace_attributes,
974            ">",
975            content,
976            "</",
977            qualified_name,
978            ">",
979        ])
980    } else {
981        let content_start =
982            element_opening_end(element).ok_or(XmlMutationError::InvalidAppendTarget)?;
983        let content_end = element
984            .rfind("</")
985            .ok_or(XmlMutationError::InvalidAppendTarget)?;
986        Ok([
987            &element[..content_start - 1],
988            namespace_attributes,
989            ">",
990            content,
991            &element[content_end..],
992            "",
993            "",
994        ])
995    }
996}
997
998fn replacement_parts_len(
999    parts: &[&str],
1000    policy: Option<&crate::policy::SigningPolicy>,
1001) -> Result<usize, XmlMutationError> {
1002    parts.iter().try_fold(0usize, |length, part| {
1003        length
1004            .checked_add(part.len())
1005            .ok_or_else(|| projected_xml_length_overflow(policy))
1006    })
1007}
1008
1009fn replace_element_contents<'a>(
1010    xml: &str,
1011    replacements: impl IntoIterator<Item = (Range<usize>, &'a String)>,
1012    policy: Option<&crate::policy::SigningPolicy>,
1013) -> Result<String, XmlMutationError> {
1014    let mut planned = replacements
1015        .into_iter()
1016        .map(|(range, content)| {
1017            let element = xml
1018                .get(range.clone())
1019                .ok_or(XmlMutationError::InvalidAppendTarget)?;
1020            let parts = element_content_parts(element, content, "")?;
1021            Ok((range, parts))
1022        })
1023        .collect::<Result<Vec<_>, XmlMutationError>>()?;
1024    planned.sort_unstable_by_key(|(range, _)| range.start);
1025
1026    let mut projected = xml.len();
1027    let mut previous_end = 0usize;
1028    for (range, parts) in &planned {
1029        if range.start < previous_end || range.end > xml.len() {
1030            return Err(XmlMutationError::InvalidAppendTarget);
1031        }
1032        let replacement_len = replacement_parts_len(parts, policy)?;
1033        projected = projected
1034            .checked_sub(range.len())
1035            .and_then(|length| length.checked_add(replacement_len))
1036            .ok_or_else(|| projected_xml_length_overflow(policy))?;
1037        previous_end = range.end;
1038    }
1039    if let Some(policy) = policy {
1040        policy.resources.validate_xml_document_len(projected)?;
1041    }
1042
1043    let mut output = String::with_capacity(projected);
1044    let mut copied = 0usize;
1045    for (range, parts) in planned {
1046        output.push_str(&xml[copied..range.start]);
1047        for part in parts {
1048            output.push_str(part);
1049        }
1050        copied = range.end;
1051    }
1052    output.push_str(&xml[copied..]);
1053    Ok(output)
1054}
1055
1056fn append_element_content(
1057    xml: &str,
1058    range: Range<usize>,
1059    content: &str,
1060    namespace_attributes: &str,
1061    retained_bindings: &[(usize, String)],
1062    policy: Option<&crate::policy::SigningPolicy>,
1063) -> Result<String, XmlMutationError> {
1064    let element = &xml[range.clone()];
1065    let content_start =
1066        element_opening_end(element).ok_or(XmlMutationError::InvalidAppendTarget)?;
1067    let content_end = element
1068        .rfind("</")
1069        .ok_or(XmlMutationError::InvalidAppendTarget)?;
1070    let added_binding_bytes =
1071        retained_bindings
1072            .iter()
1073            .try_fold(0usize, |total, (_, binding)| {
1074                total
1075                    .checked_add(binding.len())
1076                    .ok_or_else(|| projected_xml_length_overflow(policy))
1077            })?;
1078    let replacement_len = (content_start - 1)
1079        .checked_add(namespace_attributes.len())
1080        .and_then(|length| length.checked_add(1))
1081        .and_then(|length| length.checked_add(content_end - content_start))
1082        .and_then(|length| length.checked_add(added_binding_bytes))
1083        .and_then(|length| length.checked_add(content.len()))
1084        .and_then(|length| length.checked_add(element.len() - content_end))
1085        .ok_or_else(|| projected_xml_length_overflow(policy))?;
1086    let projected_len =
1087        validate_projected_replacement_len(xml, range.len(), replacement_len, policy)?;
1088    let mut output = String::with_capacity(projected_len);
1089    output.push_str(&xml[..range.start]);
1090    output.push_str(&element[..content_start - 1]);
1091    output.push_str(namespace_attributes);
1092    output.push('>');
1093    let retained_start = range.start + content_start;
1094    let retained_end = range.start + content_end;
1095    let mut cursor = retained_start;
1096    for (insertion, binding) in retained_bindings {
1097        let segment = xml
1098            .get(cursor..*insertion)
1099            .ok_or(XmlMutationError::InvalidAppendTarget)?;
1100        output.push_str(segment);
1101        output.push_str(binding);
1102        cursor = *insertion;
1103    }
1104    output.push_str(
1105        xml.get(cursor..retained_end)
1106            .ok_or(XmlMutationError::InvalidAppendTarget)?,
1107    );
1108    output.push_str(content);
1109    output.push_str(&element[content_end..]);
1110    output.push_str(&xml[range.end..]);
1111    Ok(output)
1112}
1113
1114fn preserve_retained_child_namespaces(
1115    xml: &str,
1116    placeholder: crate::xml::dom::Node<'_, '_>,
1117    source: crate::xml::dom::Node<'_, '_>,
1118) -> Result<Vec<(usize, String)>, XmlMutationError> {
1119    let mut insertions = Vec::new();
1120    for child in placeholder.children().filter(|child| child.is_element()) {
1121        let child_range = child.range();
1122        let child_opening_end = child_range.start
1123            + element_opening_end(&xml[child_range.clone()])
1124                .ok_or(XmlMutationError::InvalidAppendTarget)?;
1125        let insertion = if xml[child_range.start..child_opening_end].ends_with("/>") {
1126            child_opening_end - 2
1127        } else {
1128            child_opening_end - 1
1129        };
1130        let owned = owned_namespace_declarations(&xml[child_range.start..insertion])?;
1131        let mut declarations = String::new();
1132        for binding in source.namespaces() {
1133            let name = binding.name();
1134            if placeholder.lookup_namespace_uri(name) == Some(binding.uri())
1135                || owned.contains(name.unwrap_or_default())
1136            {
1137                continue;
1138            }
1139            let original = child.lookup_namespace_uri(name);
1140            if original == Some(binding.uri()) {
1141                continue;
1142            }
1143            if let Some(prefix) = name {
1144                if let Some(uri) = original {
1145                    declarations
1146                        .push_str(&format!(" xmlns:{prefix}=\"{}\"", escape_attribute(uri)));
1147                }
1148            } else {
1149                declarations.push_str(&format!(
1150                    " xmlns=\"{}\"",
1151                    escape_attribute(original.unwrap_or_default())
1152                ));
1153            }
1154        }
1155        if !declarations.is_empty() {
1156            insertions.push((insertion, declarations));
1157        }
1158    }
1159    // Namespaces in XML 1.0 (Third Edition) §§6.1-6.2:
1160    // https://www.w3.org/TR/REC-xml-names/#scoping-defaulting
1161    // Rebinding X509Data must not change expanded names of retained descendants.
1162    Ok(insertions)
1163}
1164
1165fn validate_projected_replacement_len(
1166    xml: &str,
1167    removed_len: usize,
1168    added_len: usize,
1169    policy: Option<&crate::policy::SigningPolicy>,
1170) -> Result<usize, XmlMutationError> {
1171    let projected = xml
1172        .len()
1173        .checked_sub(removed_len)
1174        .and_then(|length| length.checked_add(added_len))
1175        .ok_or_else(|| projected_xml_length_overflow(policy))?;
1176    if let Some(policy) = policy {
1177        policy.resources.validate_xml_document_len(projected)?;
1178    }
1179    Ok(projected)
1180}
1181
1182fn element_opening_end(fragment: &str) -> Option<usize> {
1183    let mut quote = None;
1184    for (offset, character) in fragment.char_indices() {
1185        match (quote, character) {
1186            (None, '\'' | '"') => quote = Some(character),
1187            (Some(delimiter), current) if delimiter == current => quote = None,
1188            (None, '>') => return Some(offset + 1),
1189            _ => {}
1190        }
1191    }
1192    None
1193}
1194
1195fn fill_dsig_values<I, S>(
1196    xml: &str,
1197    local_name: &'static str,
1198    values: I,
1199) -> Result<String, XmlMutationError>
1200where
1201    I: IntoIterator<Item = S>,
1202    S: AsRef<str>,
1203{
1204    let values: Vec<String> = values
1205        .into_iter()
1206        .map(|value| value.as_ref().to_owned())
1207        .collect();
1208    fill_dsig_text_values_matching(
1209        xml,
1210        local_name,
1211        values,
1212        None,
1213        None,
1214        DsigReplacementScope::Any,
1215    )
1216}
1217
1218#[derive(Clone, Copy)]
1219enum DsigReplacementScope {
1220    Any,
1221    SignedInfoReferenceDigest { signature_index: usize },
1222    DirectSignatureChild { signature_index: usize },
1223}
1224
1225impl DsigReplacementScope {
1226    const fn signature_index(self) -> Option<usize> {
1227        match self {
1228            Self::Any => None,
1229            Self::SignedInfoReferenceDigest { signature_index }
1230            | Self::DirectSignatureChild { signature_index } => Some(signature_index),
1231        }
1232    }
1233
1234    fn includes(
1235        self,
1236        node: crate::xml::dom::Node<'_, '_>,
1237        signature: Option<crate::xml::dom::Node<'_, '_>>,
1238    ) -> bool {
1239        match self {
1240            Self::Any => true,
1241            Self::SignedInfoReferenceDigest { .. } => signature
1242                .is_some_and(|signature| is_direct_signed_info_reference_digest(node, signature)),
1243            Self::DirectSignatureChild { .. } => signature
1244                .is_some_and(|signature| node.parent().is_some_and(|parent| parent == signature)),
1245        }
1246    }
1247}
1248
1249fn fill_dsig_text_values_matching(
1250    xml: &str,
1251    local_name: &'static str,
1252    values: Vec<String>,
1253    policy: Option<&crate::policy::SigningPolicy>,
1254    budget: Option<&XmlParseWorkBudget>,
1255    scope: DsigReplacementScope,
1256) -> Result<String, XmlMutationError> {
1257    let escaped = values
1258        .iter()
1259        .map(|value| escape_text(value).into_owned())
1260        .collect::<Vec<_>>();
1261    fill_dsig_contents_matching(xml, local_name, &escaped, policy, budget, scope)
1262}
1263
1264fn fill_dsig_raw_value_matching(
1265    xml: &str,
1266    local_name: &'static str,
1267    content: &str,
1268    policy: Option<&crate::policy::SigningPolicy>,
1269    budget: Option<&XmlParseWorkBudget>,
1270    scope: DsigReplacementScope,
1271) -> Result<String, XmlMutationError> {
1272    fill_dsig_contents_matching(
1273        xml,
1274        local_name,
1275        &[content.to_owned()],
1276        policy,
1277        budget,
1278        scope,
1279    )
1280}
1281
1282fn fill_dsig_contents_matching(
1283    xml: &str,
1284    local_name: &'static str,
1285    contents: &[String],
1286    policy: Option<&crate::policy::SigningPolicy>,
1287    budget: Option<&XmlParseWorkBudget>,
1288    scope: DsigReplacementScope,
1289) -> Result<String, XmlMutationError> {
1290    let document = parse_mutation_xml_with_budget(xml, policy, budget)?;
1291    let signature = scope
1292        .signature_index()
1293        .and_then(|index| signature_node(&document, index));
1294    let ranges = document
1295        .descendants()
1296        .filter(|node| is_dsig_node(*node, local_name) && scope.includes(*node, signature))
1297        .map(|node| actionable_node_range(node))
1298        .collect::<Result<Vec<_>, XmlMutationError>>()?;
1299    if ranges.len() != contents.len() {
1300        return Err(XmlMutationError::ValueCountMismatch {
1301            element: local_name,
1302            expected: ranges.len(),
1303            actual: contents.len(),
1304        });
1305    }
1306    let mut ordered_ranges = ranges.iter().collect::<Vec<_>>();
1307    ordered_ranges.sort_by_key(|range| range.start);
1308    let independently_replaceable = ordered_ranges
1309        .iter()
1310        .fold((0usize, 0usize), |(count, covered_until), range| {
1311            if range.start < covered_until {
1312                (count, covered_until)
1313            } else {
1314                (count + 1, range.end)
1315            }
1316        })
1317        .0;
1318    if independently_replaceable != ranges.len() {
1319        return Err(XmlMutationError::ValueCountMismatch {
1320            element: local_name,
1321            expected: contents.len(),
1322            actual: independently_replaceable,
1323        });
1324    }
1325    if let Some(budget) = budget {
1326        budget.charge_policy(xml.len())?;
1327    }
1328    let output = replace_element_contents(xml, ranges.into_iter().zip(contents), policy)?;
1329    parse_mutation_xml_with_budget(&output, policy, budget)?;
1330    Ok(output)
1331}
1332
1333fn validate_signature_template(
1334    signature_template: &str,
1335    policy: Option<&crate::policy::SigningPolicy>,
1336) -> Result<(), XmlMutationError> {
1337    let document = parse_mutation_xml_with_options(signature_template, policy)?;
1338    let root = document.root_element();
1339    if root.tag_name().namespace() == Some(XMLDSIG_NS) && root.tag_name().name() == "Signature" {
1340        Ok(())
1341    } else {
1342        Err(XmlMutationError::InvalidSignatureTemplate)
1343    }
1344}
1345
1346fn signature_node<'a>(
1347    document: &'a crate::xml::dom::Document<'a>,
1348    target_signature: usize,
1349) -> Option<crate::xml::dom::Node<'a, 'a>> {
1350    document
1351        .descendants()
1352        .filter(|node| is_dsig_node(*node, "Signature"))
1353        .nth(target_signature)
1354}
1355
1356fn last_signature_index(
1357    xml: &str,
1358    policy: Option<&crate::policy::SigningPolicy>,
1359    budget: Option<&XmlParseWorkBudget>,
1360) -> Result<usize, XmlMutationError> {
1361    let document = parse_mutation_xml_with_budget(xml, policy, budget)?;
1362    document
1363        .descendants()
1364        .filter(|node| is_dsig_node(*node, "Signature"))
1365        .enumerate()
1366        .last()
1367        .map(|(index, _)| index)
1368        .ok_or(XmlMutationError::ValueCountMismatch {
1369            element: "Signature",
1370            expected: 1,
1371            actual: 0,
1372        })
1373}
1374
1375fn is_direct_signed_info_reference_digest(
1376    node: crate::xml::dom::Node<'_, '_>,
1377    signature: crate::xml::dom::Node<'_, '_>,
1378) -> bool {
1379    node.is_element()
1380        && node.tag_name().namespace() == Some(XMLDSIG_NS)
1381        && node.tag_name().name() == "DigestValue"
1382        && node
1383            .parent()
1384            .is_some_and(|parent| is_dsig_node(parent, "Reference"))
1385        && node
1386            .parent()
1387            .and_then(|parent| parent.parent())
1388            .is_some_and(|grandparent| is_dsig_node(grandparent, "SignedInfo"))
1389        && node
1390            .parent()
1391            .and_then(|parent| parent.parent())
1392            .and_then(|grandparent| grandparent.parent())
1393            .is_some_and(|parent| parent == signature)
1394}
1395
1396fn is_dsig_node(node: crate::xml::dom::Node<'_, '_>, expected_local: &str) -> bool {
1397    node.is_element()
1398        && node.tag_name().namespace() == Some(XMLDSIG_NS)
1399        && node.tag_name().name() == expected_local
1400}
1401
1402#[cfg(test)]
1403mod tests {
1404    use crate::c14n::{C14nAlgorithm, C14nMode};
1405    use crate::xml::dom;
1406    use crate::xmldsig::{
1407        DigestAlgorithm, ReferenceBuilder, SignatureAlgorithm, SignatureBuilder, Transform,
1408    };
1409
1410    use super::*;
1411
1412    #[test]
1413    fn replacement_splices_borrow_payload_and_render_exact_capacity() {
1414        // Sizing may inspect slices but must not copy the large replacement before the gate.
1415        let content = "value".repeat(1024);
1416        for element in ["<p:e/>", "<p:e old='x'>discarded</p:e>"] {
1417            let parts =
1418                element_content_parts(element, &content, " xmlns:p='urn:p'").expect("parts");
1419            assert_eq!(parts[3].as_ptr(), content.as_ptr());
1420            let expected = parts.concat();
1421            let output = replace_element_content(
1422                element,
1423                0..element.len(),
1424                &content,
1425                " xmlns:p='urn:p'",
1426                None,
1427            )
1428            .expect("splice");
1429            assert_eq!(output, expected);
1430            assert_eq!(output.capacity(), output.len());
1431        }
1432        let xml = "<r><a/><b>old</b></r>";
1433        let a = String::from("A");
1434        let b = String::from("B");
1435        let output = replace_element_contents(xml, [(7..17, &b), (3..7, &a)], None).expect("batch");
1436        assert_eq!(output, "<r><a>A</a><b>B</b></r>");
1437        assert_eq!(output.capacity(), output.len());
1438    }
1439
1440    fn template(reference_count: usize) -> String {
1441        let mut builder = SignatureBuilder::new(
1442            C14nAlgorithm::new(C14nMode::Exclusive1_0, false),
1443            SignatureAlgorithm::RsaSha256,
1444        )
1445        .ns_prefix("ds");
1446        for index in 0..reference_count {
1447            builder = builder.add_reference(
1448                ReferenceBuilder::new(DigestAlgorithm::Sha256)
1449                    .uri(format!("#ref-{index}"))
1450                    .transform(Transform::Enveloped),
1451            );
1452        }
1453        builder.build_template().expect("valid template")
1454    }
1455
1456    #[test]
1457    fn signature_value_projection_matches_streaming_mutation() {
1458        // Allocation preflight must predict the exact serializer output for
1459        // both XML spellings accepted as an empty SignatureValue placeholder.
1460        for placeholder in [
1461            "<ds:SignatureValue/>",
1462            "<ds:SignatureValue></ds:SignatureValue>",
1463        ] {
1464            let xml = format!(
1465                "<root><ds:Signature xmlns:ds=\"{XMLDSIG_NS}\"><ds:SignedInfo/>{placeholder}</ds:Signature></root>"
1466            );
1467            let value = "A".repeat(341);
1468            let projected = projected_signature_value_output_len_at_index_with_options(
1469                &xml,
1470                value.len(),
1471                0,
1472                Some(&crate::policy::SigningPolicy::default()),
1473            )
1474            .expect("project SignatureValue output length");
1475            let mutated = fill_signature_value_at_index_with_options(
1476                &xml,
1477                &value,
1478                0,
1479                Some(&crate::policy::SigningPolicy::default()),
1480            )
1481            .expect("fill SignatureValue");
1482
1483            assert_eq!(projected, mutated.len());
1484        }
1485    }
1486
1487    #[test]
1488    fn lexical_mutation_scan_consumes_the_shared_parse_budget() {
1489        // The lexical rewrite plus bounded preflight and every selected-mode
1490        // semantic parser consume one operation-wide allowance.
1491        let xml = format!(
1492            "<root><ds:Signature xmlns:ds=\"{XMLDSIG_NS}\"><ds:SignatureValue/></ds:Signature></root>"
1493        );
1494        let resources = crate::policy::ResourcePolicy::default();
1495        let budget = XmlParseWorkBudget::from_resources(&resources);
1496        let output = fill_signature_value_at_index_with_budget(
1497            &xml,
1498            "signature",
1499            0,
1500            Some(&crate::policy::SigningPolicy::default()),
1501            Some(&budget),
1502        )
1503        .expect("streaming mutation must succeed");
1504
1505        let dom_passes = crate::document::selected_parser_passes();
1506        assert_eq!(
1507            budget.consumed(),
1508            xml.len() * (dom_passes + 1) + output.len() * dom_passes
1509        );
1510    }
1511
1512    #[test]
1513    fn appends_signature_template_to_non_empty_root() {
1514        let signed = append_signature_to_root("<root><payload ID=\"ref-0\"/></root>", &template(1))
1515            .expect("append signature");
1516        let document = dom::Document::parse(&signed).expect("parse output");
1517        let root = document.root_element();
1518        let children: Vec<_> = root
1519            .children()
1520            .filter(dom::Node::is_element)
1521            .map(|node| node.tag_name().name())
1522            .collect();
1523        assert_eq!(children, ["payload", "Signature"]);
1524        assert_eq!(
1525            root.last_element_child()
1526                .expect("signature")
1527                .tag_name()
1528                .namespace(),
1529            Some(XMLDSIG_NS)
1530        );
1531    }
1532
1533    #[test]
1534    fn appends_signature_template_to_empty_root() {
1535        let signed = append_signature_to_root("<root/>", &template(1)).expect("append signature");
1536        let document = dom::Document::parse(&signed).expect("parse output");
1537        let root = document.root_element();
1538        assert_eq!(
1539            root.first_element_child()
1540                .expect("signature")
1541                .tag_name()
1542                .name(),
1543            "Signature"
1544        );
1545    }
1546
1547    #[test]
1548    fn appends_signature_template_to_selected_empty_element() {
1549        // Selected builder targets may be self-closing; insertion must expand
1550        // the element without dropping its qualified name or attributes.
1551        let source = r#"<root xmlns:s="urn:scope"><s:scope Id="urn:selected/item"/></root>"#;
1552        let mut document = crate::XmlDocument::parse(source).expect("source must parse");
1553        let registrations = [crate::IdAttributeRegistration::global("Id")];
1554        let scope = document.with_view(|view| {
1555            view.node_for_id("urn:selected/item", &registrations)
1556                .expect("selected scope")
1557        });
1558        document
1559            .append_child(scope, &template(1))
1560            .expect("selected empty element must accept a signature");
1561        let signed = document.into_xml();
1562        let output = dom::Document::parse(&signed).expect("output must parse");
1563        let scope = output
1564            .descendants()
1565            .find(|node| node.has_tag_name(("urn:scope", "scope")))
1566            .expect("qualified scope must remain");
1567
1568        assert!(
1569            scope
1570                .children()
1571                .any(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
1572        );
1573        assert_eq!(scope.attribute("Id"), Some("urn:selected/item"));
1574    }
1575
1576    #[test]
1577    fn rejects_non_signature_template() {
1578        let err = append_signature_to_root("<root/>", "<NotSignature/>")
1579            .expect_err("template must be a Signature");
1580        assert!(matches!(err, XmlMutationError::InvalidSignatureTemplate));
1581    }
1582
1583    #[test]
1584    fn signature_template_validation_applies_the_active_policy_first() {
1585        // The separately supplied template is an untrusted XML allocation
1586        // boundary. Reject it before parsing the source or constructing output.
1587        let template = format!(
1588            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo>{}</ds:SignedInfo><ds:SignatureValue/></ds:Signature>"#,
1589            "<part/>".repeat(16),
1590        );
1591
1592        let byte_policy = crate::policy::SigningPolicy {
1593            resources: crate::policy::ResourcePolicy {
1594                max_xml_document_bytes: template.len() - 1,
1595                ..crate::policy::ResourcePolicy::default()
1596            },
1597            ..crate::policy::SigningPolicy::default()
1598        };
1599        let byte_error =
1600            append_signature_to_root_with_options("not XML", &template, Some(&byte_policy))
1601                .expect_err("template byte policy must win before source parsing");
1602        assert!(matches!(
1603            byte_error,
1604            XmlMutationError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1605                resource: crate::policy::resource_name::XML_DOCUMENT,
1606                maximum,
1607                actual,
1608            }) if maximum == template.len() - 1 && actual == template.len()
1609        ));
1610
1611        let node_policy = crate::policy::SigningPolicy {
1612            resources: crate::policy::ResourcePolicy {
1613                max_xml_nodes: 2,
1614                ..crate::policy::ResourcePolicy::default()
1615            },
1616            ..crate::policy::SigningPolicy::default()
1617        };
1618        let node_error =
1619            append_signature_to_root_with_options("not XML", &template, Some(&node_policy))
1620                .expect_err("template node policy must win before source parsing");
1621        assert!(matches!(
1622            node_error,
1623            XmlMutationError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1624                resource: crate::policy::resource_name::XML_NODES,
1625                maximum: 2,
1626                actual: 3,
1627            })
1628        ));
1629    }
1630
1631    #[test]
1632    fn fills_digest_values_in_xml_dsig_document_order() {
1633        let signed = append_signature_to_root("<root/>", &template(2)).expect("append signature");
1634        let filled =
1635            fill_digest_values(&signed, ["digest-one", "digest-two"]).expect("fill digest values");
1636        let document = dom::Document::parse(&filled).expect("parse output");
1637        let values: Vec<_> = document
1638            .descendants()
1639            .filter(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
1640            .map(|node| node.text())
1641            .collect();
1642        assert_eq!(values, [Some("digest-one"), Some("digest-two")]);
1643    }
1644
1645    #[test]
1646    fn rejects_entity_expanded_dsig_mutation_targets() {
1647        // A general entity can project element nodes, but every expansion shares the entity
1648        // reference's lexical range. Mutating that range cannot uniquely target one semantic
1649        // element, so signing must fail instead of rewriting the declaration or reference token.
1650        let xml = format!(
1651            r#"<!DOCTYPE root [<!ENTITY digest '<ds:DigestValue/>'>]><root xmlns:ds="{XMLDSIG_NS}">&digest;</root>"#
1652        );
1653        let mut policy = crate::policy::SigningPolicy::default();
1654        policy.xml.allow_internal_dtd = true;
1655        let error = fill_dsig_contents_matching(
1656            &xml,
1657            "DigestValue",
1658            &["digest".to_owned()],
1659            Some(&policy),
1660            None,
1661            DsigReplacementScope::Any,
1662        )
1663        .expect_err("entity-expanded elements have no independently actionable range");
1664        assert!(
1665            matches!(
1666                error,
1667                XmlMutationError::Document(XmlDocumentError::EntityExpandedMutationTarget)
1668            ),
1669            "unexpected mutation error: {error:?}"
1670        );
1671    }
1672
1673    #[test]
1674    fn rejects_entity_expanded_key_info_replacement_ranges() {
1675        // One entity reference can project several semantic children onto the same lexical range.
1676        // KeyInfo replacement must reject those non-actionable targets before applying any splice.
1677        let xml = format!(
1678            r#"<!DOCTYPE ds:Signature [<!ENTITY identities '<ds:X509SubjectName>CN=old</ds:X509SubjectName><ds:X509SKI>b2xk</ds:X509SKI>'>]><ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:KeyInfo><ds:X509Data>&identities;</ds:X509Data></ds:KeyInfo></ds:Signature>"#
1679        );
1680        let generated = format!(
1681            r#"<ds:X509Data xmlns:ds="{XMLDSIG_NS}"><ds:X509Certificate>bmV3</ds:X509Certificate></ds:X509Data>"#
1682        );
1683        let mut policy = crate::policy::SigningPolicy::default();
1684        policy.xml.allow_internal_dtd = true;
1685        let error =
1686            merge_key_info_source_at_index_with_budget(&xml, &generated, 0, Some(&policy), None)
1687                .expect_err("entity-expanded KeyInfo ranges are not independently actionable");
1688        assert!(matches!(
1689            error,
1690            XmlMutationError::Document(XmlDocumentError::EntityExpandedMutationTarget)
1691        ));
1692    }
1693
1694    #[test]
1695    fn fills_signature_value_without_touching_digest_values() {
1696        let signed = append_signature_to_root("<root/>", &template(1)).expect("append signature");
1697        let filled =
1698            fill_signature_values(&signed, ["signature&bytes"]).expect("fill signature value");
1699        let document = dom::Document::parse(&filled).expect("parse output");
1700        let signature_value = document
1701            .descendants()
1702            .find(|node| node.has_tag_name((XMLDSIG_NS, "SignatureValue")))
1703            .expect("SignatureValue");
1704        assert_eq!(signature_value.text(), Some("signature&bytes"));
1705        let digest_value = document
1706            .descendants()
1707            .find(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
1708            .expect("DigestValue");
1709        assert_eq!(digest_value.text(), None);
1710    }
1711
1712    #[test]
1713    fn mutation_enforces_the_backend_neutral_namespace_ceiling() {
1714        // The shared preflight and semantic parser must enforce the same
1715        // active-binding ceiling rather than diverging during mutation.
1716        let declarations = (0..1_023)
1717            .map(|index| format!(r#" xmlns:n{index}="urn:namespace:{index}""#))
1718            .collect::<String>();
1719        let xml = format!(
1720            r#"<root{declarations}><ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo/><ds:SignatureValue/></ds:Signature></root>"#
1721        );
1722
1723        let filled = fill_signature_value(&xml, "signature")
1724            .expect("validated namespace bindings must remain mutable");
1725        let document = dom::Document::parse(&filled).expect("parse output");
1726        let signature_value = document
1727            .descendants()
1728            .find(|node| node.has_tag_name((XMLDSIG_NS, "SignatureValue")))
1729            .expect("SignatureValue");
1730        assert_eq!(signature_value.text(), Some("signature"));
1731
1732        let excessive_declarations =
1733            format!(r#"{declarations} xmlns:overflow="urn:namespace:overflow""#);
1734        let excessive = format!(
1735            r#"<root{excessive_declarations}><ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo/><ds:SignatureValue/></ds:Signature></root>"#
1736        );
1737        let error = fill_signature_value(&excessive, "signature")
1738            .expect_err("1025 active namespace bindings must fail during shared preflight");
1739        assert!(matches!(
1740            error,
1741            XmlMutationError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1742                resource: crate::policy::resource_name::XML_NAMESPACE_BINDINGS,
1743                maximum: 1_024,
1744                actual: 1_025,
1745            })
1746        ));
1747    }
1748
1749    #[test]
1750    fn replacement_count_must_match_dsig_elements() {
1751        let signed = append_signature_to_root("<root/>", &template(2)).expect("append signature");
1752        let err = fill_digest_values(&signed, ["only-one"]).expect_err("mismatch");
1753        assert!(matches!(
1754            err,
1755            XmlMutationError::ValueCountMismatch {
1756                element: "DigestValue",
1757                expected: 2,
1758                actual: 1
1759            }
1760        ));
1761    }
1762
1763    #[test]
1764    fn does_not_replace_foreign_same_local_name_elements() {
1765        let source = r#"<root xmlns:foreign="urn:test"><foreign:DigestValue>keep</foreign:DigestValue></root>"#;
1766        let signed = append_signature_to_root(source, &template(1)).expect("append signature");
1767        let filled = fill_digest_values(&signed, ["digest"]).expect("fill digest");
1768        let document = dom::Document::parse(&filled).expect("parse output");
1769        let foreign = document
1770            .descendants()
1771            .find(|node| node.has_tag_name(("urn:test", "DigestValue")))
1772            .expect("foreign DigestValue");
1773        assert_eq!(foreign.text(), Some("keep"));
1774        let dsig = document
1775            .descendants()
1776            .find(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
1777            .expect("dsig DigestValue");
1778        assert_eq!(dsig.text(), Some("digest"));
1779    }
1780
1781    #[test]
1782    fn replacement_preserves_target_end_after_self_closing_child() {
1783        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:Reference><ds:DigestValue><marker/></ds:DigestValue></ds:Reference></ds:SignedInfo></ds:Signature>"#;
1784        let filled = fill_digest_values(source, ["digest"]).expect("fill digest");
1785        let document = dom::Document::parse(&filled).expect("parse output");
1786        let digest_value = document
1787            .descendants()
1788            .find(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
1789            .expect("DigestValue");
1790        assert_eq!(digest_value.text(), Some("digest"));
1791        assert_eq!(
1792            digest_value
1793                .next_sibling_element()
1794                .map(|node| node.tag_name().name()),
1795            None
1796        );
1797    }
1798
1799    #[test]
1800    fn replacement_fails_when_nested_dsig_values_are_skipped() {
1801        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:Reference><ds:DigestValue><ds:DigestValue>nested</ds:DigestValue></ds:DigestValue></ds:Reference></ds:SignedInfo></ds:Signature>"#;
1802        let err =
1803            fill_digest_values(source, ["outer", "nested"]).expect_err("nested target skipped");
1804        assert!(matches!(
1805            err,
1806            XmlMutationError::ValueCountMismatch {
1807                element: "DigestValue",
1808                expected: 2,
1809                actual: 1
1810            }
1811        ));
1812    }
1813
1814    #[test]
1815    fn indexed_digest_replacement_ignores_nested_signatures() {
1816        // Digest counts and replacements must use the same nearest-Signature
1817        // boundary or a nested Object signature can exhaust the value list.
1818        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:Reference><ds:DigestValue>outer-old</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue/><ds:Object><ds:Signature><ds:SignedInfo><ds:Reference><ds:DigestValue>inner-keep</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue/></ds:Signature></ds:Object></ds:Signature>"#;
1819        let filled =
1820            fill_signed_info_digest_values_at_index_with_options(source, ["outer-new"], 0, None)
1821                .expect("outer signature replacement must ignore nested signatures");
1822        let document = dom::Document::parse(&filled).expect("filled XML must parse");
1823        let values = document
1824            .descendants()
1825            .filter(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
1826            .filter_map(|node| node.text())
1827            .collect::<Vec<_>>();
1828
1829        assert_eq!(values, ["outer-new", "inner-keep"]);
1830    }
1831
1832    #[test]
1833    fn key_info_source_merge_preserves_placeholder_attributes() {
1834        // Placeholder identity can be referenced from SignedInfo, so filling
1835        // its children must not replace the element that owns the ID.
1836        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo><ds:X509Data Id="key-info"/></ds:KeyInfo></ds:Signature>"#;
1837        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:example:key-info"><X509Certificate>Y2VydA==</X509Certificate><ext:Metadata/></X509Data>"#;
1838
1839        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1840            .expect("matching source must populate the placeholder");
1841        let document = dom::Document::parse(&merged).expect("merged XML must parse");
1842        let x509_data = document
1843            .descendants()
1844            .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
1845            .expect("X509Data");
1846
1847        assert_eq!(x509_data.attribute("Id"), Some("key-info"));
1848        assert_eq!(
1849            x509_data
1850                .children()
1851                .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Certificate")))
1852                .and_then(|node| node.text()),
1853            Some("Y2VydA==")
1854        );
1855        assert!(
1856            x509_data
1857                .children()
1858                .any(|node| node.has_tag_name(("urn:example:key-info", "Metadata")))
1859        );
1860    }
1861
1862    #[test]
1863    fn key_info_source_merge_uses_named_binding_for_namespaced_attributes() {
1864        // A default binding cannot qualify an attribute. Prefix lookup must
1865        // continue to the named binding when both map to the same URI.
1866        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo><ds:X509Data/></ds:KeyInfo></ds:Signature>"#;
1867        let generated = r#"<ds:X509Data xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns="urn:example:metadata" xmlns:ext="urn:example:metadata" ext:role="signer"><ds:X509Certificate>Y2VydA==</ds:X509Certificate></ds:X509Data>"#;
1868
1869        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1870            .expect("a named namespace binding must qualify the generated attribute");
1871        let document = dom::Document::parse(&merged).expect("merged XML must parse");
1872        let x509_data = document
1873            .descendants()
1874            .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
1875            .expect("X509Data");
1876
1877        assert_eq!(
1878            x509_data.attribute(("urn:example:metadata", "role")),
1879            Some("signer")
1880        );
1881    }
1882
1883    #[test]
1884    fn key_info_source_merge_preserves_comment_and_processing_instruction() {
1885        // Comments and processing instructions are caller-owned content, not an
1886        // empty placeholder that the generated identity may silently replace.
1887        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo><ds:X509Data><!--keep--><?audit preserve?></ds:X509Data></ds:KeyInfo></ds:Signature>"#;
1888        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#"><X509Certificate>Y2VydA==</X509Certificate></X509Data>"#;
1889
1890        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1891            .expect("generated identity must be appended without erasing caller content");
1892
1893        assert!(merged.contains("<!--keep-->"));
1894        assert!(merged.contains("<?audit preserve?>"));
1895        let document = dom::Document::parse(&merged).expect("merged XML must parse");
1896        let x509_sources = document
1897            .descendants()
1898            .filter(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
1899            .collect::<Vec<_>>();
1900        assert_eq!(x509_sources.len(), 2);
1901        assert!(x509_sources.iter().any(|source| {
1902            source
1903                .children()
1904                .any(|node| node.has_tag_name((XMLDSIG_NS, "X509Certificate")))
1905        }));
1906    }
1907
1908    #[test]
1909    fn key_info_source_merge_replaces_uri_reference_with_non_element_children() {
1910        // KeyInfoReference identity is carried by URI. Comments and processing
1911        // instructions make the element non-placeholder content, but must not
1912        // cause stale and generated references to coexist.
1913        let source = r##"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#"><ds:KeyInfo><dsig11:KeyInfoReference URI="#stale"><!--audit--><?trace keep?></dsig11:KeyInfoReference></ds:KeyInfo></ds:Signature>"##;
1914        let generated = r##"<dsig11:KeyInfoReference xmlns:dsig11="http://www.w3.org/2009/xmldsig11#" URI="#generated"/>"##;
1915
1916        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1917            .expect("generated reference must replace stale identity");
1918        let document = dom::Document::parse(&merged).expect("merged XML must parse");
1919        let references = document
1920            .descendants()
1921            .filter(|node| node.has_tag_name((XMLDSIG11_NS, "KeyInfoReference")))
1922            .collect::<Vec<_>>();
1923
1924        assert_eq!(references.len(), 1);
1925        assert_eq!(references[0].attribute("URI"), Some("#generated"));
1926        assert!(!merged.contains("#stale"));
1927    }
1928
1929    #[test]
1930    fn key_info_source_merge_replaces_self_closing_uri_reference() {
1931        // A URI is cryptographic identity even when the element has no child
1932        // nodes; the self-closing syntax must not turn it into a placeholder.
1933        let source = r##"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#"><ds:KeyInfo><dsig11:KeyInfoReference URI="#stale"/></ds:KeyInfo></ds:Signature>"##;
1934        let generated = r##"<dsig11:KeyInfoReference xmlns:dsig11="http://www.w3.org/2009/xmldsig11#" URI="#generated"/>"##;
1935
1936        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1937            .expect("generated reference must replace self-closing stale identity");
1938        let document = dom::Document::parse(&merged).expect("merged XML must parse");
1939        let references = document
1940            .descendants()
1941            .filter(|node| node.has_tag_name((XMLDSIG11_NS, "KeyInfoReference")))
1942            .collect::<Vec<_>>();
1943
1944        assert_eq!(references.len(), 1);
1945        assert_eq!(references[0].attribute("URI"), Some("#generated"));
1946        assert!(!merged.contains("#stale"));
1947    }
1948
1949    #[test]
1950    fn key_info_source_merge_preserves_non_xml_whitespace_text() {
1951        // XML only classifies space, tab, CR, and LF as whitespace. A non-breaking
1952        // space is caller-owned character data and must not turn X509Data into a
1953        // reusable placeholder that signing silently overwrites.
1954        let source = "<ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"><ds:KeyInfo><ds:X509Data>\u{00a0}</ds:X509Data></ds:KeyInfo></ds:Signature>";
1955        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#"><X509Certificate>Y2VydA==</X509Certificate></X509Data>"#;
1956
1957        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1958            .expect("generated identity must not replace non-whitespace character data");
1959        let document = dom::Document::parse(&merged).expect("merged XML must parse");
1960        let x509_sources = document
1961            .descendants()
1962            .filter(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
1963            .collect::<Vec<_>>();
1964
1965        assert_eq!(x509_sources.len(), 2);
1966        assert!(
1967            x509_sources
1968                .iter()
1969                .any(|source| source.text() == Some("\u{00a0}"))
1970        );
1971    }
1972
1973    #[test]
1974    fn key_info_source_merge_replaces_populated_key_name_identity() {
1975        // A generated key name is authoritative identity metadata. Retaining a
1976        // populated template value would let document-order resolvers select it.
1977        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo><ds:KeyName>stale</ds:KeyName></ds:KeyInfo></ds:Signature>"#;
1978        let generated =
1979            r#"<ds:KeyName xmlns:ds="http://www.w3.org/2000/09/xmldsig#">generated</ds:KeyName>"#;
1980
1981        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1982            .expect("generated KeyName must replace stale template identity");
1983        let document = dom::Document::parse(&merged).expect("merged XML must parse");
1984        let key_names = document
1985            .descendants()
1986            .filter(|node| node.has_tag_name((XMLDSIG_NS, "KeyName")))
1987            .filter_map(|node| node.text())
1988            .collect::<Vec<_>>();
1989
1990        assert_eq!(key_names, ["generated"]);
1991    }
1992
1993    #[test]
1994    fn key_info_source_merge_preserves_x509_revocation_metadata() {
1995        // A generated certificate replaces stale identity assertions, but the
1996        // caller's CRL and extension metadata still apply to that X509 source.
1997        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:example:x509"><ds:KeyInfo><ds:X509Data Id="caller"><ds:X509Certificate>c3RhbGU=</ds:X509Certificate><ds:X509SubjectName>CN=stale</ds:X509SubjectName><ds:X509CRL>Y3Js</ds:X509CRL><ext:Policy>keep</ext:Policy></ds:X509Data></ds:KeyInfo></ds:Signature>"#;
1998        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#"><X509Certificate>Z2VuZXJhdGVk</X509Certificate></X509Data>"#;
1999
2000        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2001            .expect("generated identity must preserve revocation metadata");
2002        let document = dom::Document::parse(&merged).expect("merged XML must parse");
2003        let x509_sources = document
2004            .descendants()
2005            .filter(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
2006            .collect::<Vec<_>>();
2007
2008        assert_eq!(x509_sources.len(), 1);
2009        let x509_data = x509_sources[0];
2010        assert_eq!(x509_data.attribute("Id"), Some("caller"));
2011        assert_eq!(
2012            x509_data
2013                .children()
2014                .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Certificate")))
2015                .and_then(|node| node.text()),
2016            Some("Z2VuZXJhdGVk")
2017        );
2018        assert!(!merged.contains("c3RhbGU="));
2019        assert!(!merged.contains("CN=stale"));
2020        assert_eq!(
2021            x509_data
2022                .children()
2023                .find(|node| node.has_tag_name((XMLDSIG_NS, "X509CRL")))
2024                .and_then(|node| node.text()),
2025            Some("Y3Js")
2026        );
2027        assert_eq!(
2028            x509_data
2029                .children()
2030                .find(|node| node.has_tag_name(("urn:example:x509", "Policy")))
2031                .and_then(|node| node.text()),
2032            Some("keep")
2033        );
2034    }
2035
2036    #[test]
2037    fn key_info_source_merge_preserves_retained_child_namespaces() {
2038        // Generated bindings must not re-interpret caller-owned extension metadata.
2039        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns="urn:caller" xmlns:ext="urn:caller-ext"><ds:KeyInfo><ds:X509Data><Metadata><ext:Child/></Metadata></ds:X509Data></ds:KeyInfo></ds:Signature>"#;
2040        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:generated-ext"><X509Certificate>Y2VydA==</X509Certificate></X509Data>"#;
2041
2042        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2043            .expect("generated identity must preserve caller namespace semantics");
2044        let document = dom::Document::parse(&merged).expect("merged XML must parse");
2045        assert!(
2046            document
2047                .descendants()
2048                .any(|node| node.has_tag_name(("urn:caller", "Metadata")))
2049        );
2050        assert!(
2051            document
2052                .descendants()
2053                .any(|node| node.has_tag_name(("urn:caller-ext", "Child")))
2054        );
2055        assert!(
2056            document
2057                .descendants()
2058                .any(|node| node.has_tag_name((XMLDSIG_NS, "X509Certificate")))
2059        );
2060    }
2061
2062    #[test]
2063    fn key_info_source_merge_reports_required_and_observed_counts() {
2064        // Mutation diagnostics are a structured API: expected is the required
2065        // singleton count and actual is the number observed in the template.
2066        for (source, actual) in [
2067            (
2068                r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/>"#,
2069                0,
2070            ),
2071            (
2072                r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo/><ds:KeyInfo/></ds:Signature>"#,
2073                2,
2074            ),
2075        ] {
2076            let error = merge_key_info_source_at_index_with_options(
2077                source,
2078                r#"<ds:KeyName xmlns:ds="http://www.w3.org/2000/09/xmldsig#">key</ds:KeyName>"#,
2079                0,
2080                None,
2081            )
2082            .expect_err("KeyInfo must be a singleton");
2083            assert!(matches!(
2084                error,
2085                XmlMutationError::ValueCountMismatch {
2086                    element: "KeyInfo",
2087                    expected: 1,
2088                    actual: observed,
2089                } if observed == actual
2090            ));
2091        }
2092    }
2093
2094    #[test]
2095    fn key_info_source_merge_rejects_conflicting_placeholder_namespaces() {
2096        // A generated child cannot reuse a prefix that the placeholder owns
2097        // with another URI; emitting both declarations would create invalid XML.
2098        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo><ds:X509Data xmlns:ext="urn:template"/></ds:KeyInfo></ds:Signature>"#;
2099        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:writer"><ext:Metadata/></X509Data>"#;
2100
2101        let error = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2102            .expect_err("conflicting namespace bindings must fail before serialization");
2103
2104        assert!(matches!(
2105            error,
2106            XmlMutationError::ConflictingKeyInfoNamespace { prefix } if prefix == "ext"
2107        ));
2108    }
2109
2110    #[test]
2111    fn key_info_source_merge_allows_shadowing_inherited_namespaces() {
2112        // An ancestor binding is context, not an attribute owned by the empty
2113        // placeholder. The generated source may validly shadow it locally.
2114        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:inherited"><ds:KeyInfo><ds:X509Data/></ds:KeyInfo></ds:Signature>"#;
2115        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:generated"><ext:Metadata/></X509Data>"#;
2116
2117        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2118            .expect("generated source may shadow an inherited namespace");
2119        let document = dom::Document::parse(&merged).expect("merged XML must parse");
2120        let x509_data = document
2121            .descendants()
2122            .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
2123            .expect("X509Data");
2124
2125        assert_eq!(
2126            x509_data.lookup_namespace_uri(Some("ext")),
2127            Some("urn:generated")
2128        );
2129        assert!(
2130            x509_data
2131                .children()
2132                .any(|node| node.has_tag_name(("urn:generated", "Metadata")))
2133        );
2134    }
2135
2136    #[test]
2137    fn key_info_source_merge_detects_redundant_owned_namespace_conflicts() {
2138        // A direct declaration remains owned by the placeholder even when it
2139        // repeats the parent binding; replacing it would duplicate xmlns:ext.
2140        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo xmlns:ext="urn:template"><ds:X509Data xmlns:ext="urn:template"/></ds:KeyInfo></ds:Signature>"#;
2141        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:writer"><ext:Metadata/></X509Data>"#;
2142
2143        let error = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2144            .expect_err("placeholder-owned namespace conflicts must be typed");
2145
2146        assert!(matches!(
2147            error,
2148            XmlMutationError::ConflictingKeyInfoNamespace { prefix } if prefix == "ext"
2149        ));
2150    }
2151
2152    #[test]
2153    fn key_info_source_merge_preserves_generated_attributes() {
2154        // Writer-owned identity must survive placeholder reuse so later
2155        // reference resolution observes the same element the writer emitted.
2156        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo><ds:X509Data/></ds:KeyInfo></ds:Signature>"#;
2157        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:key-info" Id="generated" ext:role="signing"><X509Certificate>Y2VydA==</X509Certificate></X509Data>"#;
2158
2159        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2160            .expect("generated attributes must populate the placeholder");
2161        let document = dom::Document::parse(&merged).expect("merged XML must parse");
2162        let x509_data = document
2163            .descendants()
2164            .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
2165            .expect("X509Data");
2166
2167        assert_eq!(x509_data.attribute("Id"), Some("generated"));
2168        assert_eq!(
2169            x509_data.attribute(("urn:key-info", "role")),
2170            Some("signing")
2171        );
2172    }
2173
2174    #[test]
2175    fn key_info_source_merge_accepts_whitespace_around_namespace_equals() {
2176        // XML permits whitespace around '='. Namespace ownership must come
2177        // from the parsed element rather than an exact lexical substring.
2178        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo/></ds:Signature>"#;
2179        let generated = r#"<ext:Metadata xmlns:ext = "urn:key-info">value</ext:Metadata>"#;
2180
2181        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2182            .expect("valid namespace declaration whitespace must be preserved");
2183        let document = dom::Document::parse(&merged).expect("merged XML must parse");
2184        assert!(
2185            document
2186                .descendants()
2187                .any(|node| node.has_tag_name(("urn:key-info", "Metadata")))
2188        );
2189    }
2190
2191    #[test]
2192    fn key_info_source_merge_rejects_conflicting_generated_attributes() {
2193        // Silently choosing template or writer identity would make signed
2194        // references ambiguous, so incompatible expanded attributes fail.
2195        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo><ds:X509Data Id="template"/></ds:KeyInfo></ds:Signature>"#;
2196        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" Id="generated"><X509Certificate>Y2VydA==</X509Certificate></X509Data>"#;
2197
2198        let error = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2199            .expect_err("conflicting attributes must fail before serialization");
2200
2201        assert!(matches!(
2202            error,
2203            XmlMutationError::ConflictingKeyInfoAttribute { name } if name == "Id"
2204        ));
2205    }
2206
2207    #[test]
2208    fn key_info_source_merge_rejects_empty_writer_output() {
2209        // An empty writer result is a writer-contract violation, not a malformed
2210        // signature append target, and callers need to distinguish the two.
2211        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo/></ds:Signature>"#;
2212
2213        let error = merge_key_info_source_at_index_with_options(source, "  ", 0, None)
2214            .expect_err("a key-info writer must emit an element child");
2215
2216        assert!(matches!(error, XmlMutationError::EmptyKeyInfoSource));
2217    }
2218
2219    #[test]
2220    fn key_info_source_merge_applies_policy_to_writer_fragments() {
2221        // A custom writer is an untrusted allocation boundary: its wrapper must
2222        // obey the same node ceiling as the caller's signing template.
2223        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo/></ds:Signature>"#;
2224        let children = (0..64).map(|_| "<part/>").collect::<String>();
2225        let generated = format!(
2226            r#"<ds:KeyName xmlns:ds="http://www.w3.org/2000/09/xmldsig#">{children}</ds:KeyName>"#
2227        );
2228        let policy = crate::policy::SigningPolicy {
2229            resources: crate::policy::ResourcePolicy {
2230                max_xml_nodes: 32,
2231                ..crate::policy::ResourcePolicy::default()
2232            },
2233            ..crate::policy::SigningPolicy::default()
2234        };
2235
2236        let error =
2237            merge_key_info_source_at_index_with_options(source, &generated, 0, Some(&policy))
2238                .expect_err("writer fragment must obey the signing node ceiling");
2239
2240        assert!(matches!(
2241            error,
2242            XmlMutationError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2243                resource: crate::policy::resource_name::XML_NODES,
2244                maximum: 32,
2245                actual: 33,
2246            })
2247        ));
2248    }
2249
2250    #[test]
2251    fn key_info_source_merge_bounds_synthesized_wrapper_before_parsing() {
2252        // The template and writer fragment can each fit while the namespace-
2253        // complete wrapper synthesized for fragment parsing crosses the byte
2254        // ceiling. Report that allocation boundary before parsing or merging.
2255        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:inherited"><ds:KeyInfo/></ds:Signature>"#;
2256        let generated =
2257            r#"<ds:KeyName xmlns:ds="http://www.w3.org/2000/09/xmldsig#">recipient</ds:KeyName>"#;
2258        let document = dom::Document::parse(source).expect("source must parse");
2259        let key_info = document
2260            .descendants()
2261            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
2262            .expect("KeyInfo");
2263        let wrapped =
2264            wrap_key_info_children(generated, key_info, None).expect("wrapper must serialize");
2265        let maximum = wrapped.len() - 1;
2266        assert!(source.len() <= maximum);
2267        assert!(generated.len() <= maximum);
2268        let policy = crate::policy::SigningPolicy {
2269            resources: crate::policy::ResourcePolicy {
2270                max_xml_document_bytes: maximum,
2271                ..crate::policy::ResourcePolicy::default()
2272            },
2273            ..crate::policy::SigningPolicy::default()
2274        };
2275
2276        let error =
2277            merge_key_info_source_at_index_with_options(source, generated, 0, Some(&policy))
2278                .expect_err("synthesized wrapper must be bounded before parsing");
2279
2280        assert!(matches!(
2281            error,
2282            XmlMutationError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2283                resource: crate::policy::resource_name::XML_DOCUMENT,
2284                maximum: observed_maximum,
2285                actual,
2286            }) if observed_maximum == maximum && actual == wrapped.len()
2287        ));
2288    }
2289}