Skip to main content

xml_sec/xmldsig/
mutation.rs

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