Skip to main content

xml_sec/xmldsig/
mutation.rs

1//! Streaming XML mutation helpers for the XMLDSig signing pipeline.
2//!
3//! Signing cannot mutate `roxmltree`'s read-only DOM. These helpers validate
4//! structure with `roxmltree`, then rewrite the document 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;
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<roxmltree::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<roxmltree::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<roxmltree::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] roxmltree::Error),
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                    || is_matching_empty_placeholder(node, &generated_key_material_sources)
591                {
592                    return Vec::new();
593                }
594                if generated_x509_data
595                    && is_dsig_x509_data(node.tag_name().namespace(), node.tag_name().name())
596                {
597                    return node
598                        .children()
599                        .filter(|child| child.is_element() && is_x509_identity_child(*child))
600                        .map(|child| child.range())
601                        .collect();
602                }
603                vec![node.range()]
604            })
605            .collect::<Vec<_>>();
606        stale_ranges.sort_by_key(|range| std::cmp::Reverse(range.start));
607        for range in stale_ranges {
608            output.replace_range(range, "");
609        }
610    }
611
612    for (_, _, source) in sources {
613        output = merge_one_key_info_source_at_index_with_options(
614            &output,
615            &source,
616            target_signature,
617            policy,
618            budget,
619        )?;
620    }
621    Ok(output)
622}
623
624fn merge_one_key_info_source_at_index_with_options(
625    xml: &str,
626    key_info_source: &str,
627    target_signature: usize,
628    policy: Option<&crate::policy::SigningPolicy>,
629    budget: Option<&XmlParseWorkBudget>,
630) -> Result<String, XmlMutationError> {
631    let document = parse_mutation_xml_with_budget(xml, policy, budget)?;
632    let source_document = parse_mutation_xml_with_budget(key_info_source, policy, budget)?;
633    let source = source_document.root_element();
634    let source_content = element_inner_xml(key_info_source, source.range())?;
635    let Some(signature) = signature_node(&document, target_signature) else {
636        return Err(XmlMutationError::ValueCountMismatch {
637            element: "Signature",
638            expected: 1,
639            actual: 0,
640        });
641    };
642    let key_infos = signature
643        .children()
644        .filter(|node| is_dsig_node(*node, "KeyInfo"))
645        .collect::<Vec<_>>();
646    if key_infos.len() != 1 {
647        return Err(XmlMutationError::ValueCountMismatch {
648            element: "KeyInfo",
649            expected: 1,
650            actual: key_infos.len(),
651        });
652    }
653    let key_info = key_infos[0];
654
655    let source_is_x509_data =
656        is_dsig_x509_data(source.tag_name().namespace(), source.tag_name().name());
657    if let Some(placeholder) = key_info.children().find(|node| {
658        node.is_element()
659            && node.tag_name() == source.tag_name()
660            && (is_reusable_placeholder(*node)
661                || (source_is_x509_data && has_x509_mergeable_metadata(*node)))
662    }) {
663        let placeholder_fragment = &xml[placeholder.range()];
664        let placeholder_opening_end = element_opening_end(placeholder_fragment)
665            .ok_or(XmlMutationError::InvalidAppendTarget)?;
666        let placeholder_owned_namespaces =
667            owned_namespace_declarations(&placeholder_fragment[..placeholder_opening_end - 1])?;
668        let generated_namespace_attributes =
669            source
670                .namespaces()
671                .try_fold(String::new(), |mut attributes, namespace| {
672                    let prefix = namespace.name().unwrap_or_default();
673                    if placeholder_owned_namespaces.contains(prefix) {
674                        let declared = placeholder
675                            .namespaces()
676                            .find(|declared| declared.name() == namespace.name())
677                            .ok_or(XmlMutationError::InvalidAppendTarget)?;
678                        if declared.uri() != namespace.uri() {
679                            return Err(XmlMutationError::ConflictingKeyInfoNamespace {
680                                prefix: prefix.to_owned(),
681                            });
682                        }
683                        return Ok(attributes);
684                    }
685                    if placeholder
686                        .parent_element()
687                        .and_then(|parent| parent.lookup_namespace_uri(namespace.name()))
688                        == Some(namespace.uri())
689                    {
690                        return Ok(attributes);
691                    }
692                    let attribute = namespace
693                        .name()
694                        .map_or_else(|| "xmlns".to_owned(), |prefix| format!("xmlns:{prefix}"));
695                    attributes.push_str(&format!(
696                        " {attribute}=\"{}\"",
697                        quick_xml::escape::escape(namespace.uri())
698                    ));
699                    Ok(attributes)
700                })?;
701        let generated_attributes =
702            source
703                .attributes()
704                .try_fold(String::new(), |mut attributes, attribute| {
705                    let existing = placeholder.attributes().find(|candidate| {
706                        candidate.namespace() == attribute.namespace()
707                            && candidate.name() == attribute.name()
708                    });
709                    if let Some(existing) = existing {
710                        if existing.value() != attribute.value() {
711                            return Err(XmlMutationError::ConflictingKeyInfoAttribute {
712                                name: attribute.name().to_owned(),
713                            });
714                        }
715                        return Ok(attributes);
716                    }
717                    let qualified_name = match attribute.namespace() {
718                        None => attribute.name().to_owned(),
719                        Some("http://www.w3.org/XML/1998/namespace") => {
720                            format!("xml:{}", attribute.name())
721                        }
722                        Some(namespace) => {
723                            let prefix = source
724                                .lookup_prefix(namespace)
725                                .ok_or(XmlMutationError::InvalidAppendTarget)?;
726                            format!("{prefix}:{}", attribute.name())
727                        }
728                    };
729                    attributes.push_str(&format!(
730                        " {qualified_name}=\"{}\"",
731                        quick_xml::escape::escape(attribute.value())
732                    ));
733                    Ok(attributes)
734                })?;
735        let generated_attributes =
736            format!("{generated_namespace_attributes}{generated_attributes}");
737        let output = if is_reusable_placeholder(placeholder) {
738            replace_element_content(
739                xml,
740                placeholder.range(),
741                source_content,
742                &generated_attributes,
743                policy,
744            )?
745        } else {
746            append_element_content(
747                xml,
748                placeholder.range(),
749                source_content,
750                &generated_attributes,
751                policy,
752            )?
753        };
754        parse_mutation_xml_with_budget(&output, policy, budget)?;
755        return Ok(output);
756    }
757
758    let range = key_info.range();
759    let raw_key_info = &xml[range.clone()];
760    let output = if raw_key_info.trim_end().ends_with("/>") {
761        let name_end = raw_key_info[1..]
762            .find(|character: char| {
763                character.is_ascii_whitespace() || character == '/' || character == '>'
764            })
765            .map(|offset| offset + 1)
766            .ok_or(XmlMutationError::InvalidAppendTarget)?;
767        let qualified_name = &raw_key_info[1..name_end];
768        let empty_end = raw_key_info
769            .rfind("/>")
770            .ok_or(XmlMutationError::InvalidAppendTarget)?;
771        let expanded_len = empty_end
772            .checked_add(1)
773            .and_then(|length| length.checked_add(key_info_source.len()))
774            .and_then(|length| length.checked_add(2))
775            .and_then(|length| length.checked_add(qualified_name.len()))
776            .and_then(|length| length.checked_add(1))
777            .ok_or_else(|| projected_xml_length_overflow(policy))?;
778        validate_projected_replacement_len(xml, range.len(), expanded_len, policy)?;
779        let expanded = format!(
780            "{}>{}</{}>",
781            &raw_key_info[..empty_end],
782            key_info_source,
783            qualified_name
784        );
785        let mut output = xml.to_owned();
786        output.replace_range(range, &expanded);
787        output
788    } else {
789        let closing = raw_key_info
790            .rfind("</")
791            .map(|offset| range.start + offset)
792            .ok_or(XmlMutationError::InvalidAppendTarget)?;
793        validate_projected_replacement_len(xml, 0, key_info_source.len(), policy)?;
794        let mut output = xml.to_owned();
795        output.insert_str(closing, key_info_source);
796        output
797    };
798    parse_mutation_xml_with_budget(&output, policy, budget)?;
799    Ok(output)
800}
801
802fn wrap_key_info_children(
803    source: &str,
804    key_info: roxmltree::Node<'_, '_>,
805    policy: Option<&crate::policy::SigningPolicy>,
806) -> Result<String, XmlMutationError> {
807    const OPEN: &str = "<KeyInfoFragment";
808    const CLOSE: &str = "</KeyInfoFragment>";
809    let projected = key_info
810        .namespaces()
811        .try_fold(OPEN.len(), |length, namespace| {
812            let declaration_len = match namespace.name() {
813                Some(prefix) => "xmlns:"
814                    .len()
815                    .checked_add(prefix.len())
816                    .ok_or_else(|| projected_xml_length_overflow(policy))?,
817                None => "xmlns".len(),
818            };
819            let escaped_uri = quick_xml::escape::escape(namespace.uri());
820            length
821                .checked_add(4)
822                .and_then(|length| length.checked_add(declaration_len))
823                .and_then(|length| length.checked_add(escaped_uri.len()))
824                .ok_or_else(|| projected_xml_length_overflow(policy))
825        })?;
826    let projected = projected
827        .checked_add(1)
828        .and_then(|length| length.checked_add(source.len()))
829        .and_then(|length| length.checked_add(CLOSE.len()))
830        .ok_or_else(|| projected_xml_length_overflow(policy))?;
831    if let Some(policy) = policy {
832        policy.resources.validate_xml_document_len(projected)?;
833    }
834
835    let mut wrapper = String::with_capacity(projected);
836    wrapper.push_str(OPEN);
837    for namespace in key_info.namespaces() {
838        let declaration = namespace
839            .name()
840            .map_or_else(|| "xmlns".to_owned(), |prefix| format!("xmlns:{prefix}"));
841        wrapper.push_str(&format!(
842            " {declaration}=\"{}\"",
843            quick_xml::escape::escape(namespace.uri())
844        ));
845    }
846    wrapper.push('>');
847    wrapper.push_str(source);
848    wrapper.push_str(CLOSE);
849    Ok(wrapper)
850}
851
852fn standalone_element(
853    source: &str,
854    node: roxmltree::Node<'_, '_>,
855) -> Result<String, XmlMutationError> {
856    let fragment = &source[node.range()];
857    let opening_end = element_opening_end(fragment).ok_or(XmlMutationError::InvalidAppendTarget)?;
858    let opening = &fragment[..opening_end - 1];
859    let namespace_insertion = opening.strip_suffix('/').map_or(opening.len(), str::len);
860    let mut output = opening[..namespace_insertion].to_owned();
861    let owned_namespaces = owned_namespace_declarations(opening)?;
862    for namespace in node.namespaces() {
863        let declaration = namespace
864            .name()
865            .map_or_else(|| "xmlns".to_owned(), |prefix| format!("xmlns:{prefix}"));
866        if !owned_namespaces.contains(namespace.name().unwrap_or_default()) {
867            output.push_str(&format!(
868                " {declaration}=\"{}\"",
869                quick_xml::escape::escape(namespace.uri())
870            ));
871        }
872    }
873    output.push_str(&opening[namespace_insertion..]);
874    output.push_str(&fragment[opening_end - 1..]);
875    Ok(output)
876}
877
878fn owned_namespace_declarations(opening: &str) -> Result<HashSet<String>, XmlMutationError> {
879    let standalone = format!("{} />", opening.trim_end_matches('/'));
880    let mut reader = Reader::from_str(&standalone);
881    let event = reader.read_event()?;
882    let element = match event {
883        Event::Start(element) | Event::Empty(element) => element,
884        _ => return Err(XmlMutationError::InvalidAppendTarget),
885    };
886    element
887        .attributes()
888        .map(|attribute| {
889            let attribute = attribute.map_err(|_| XmlMutationError::InvalidAppendTarget)?;
890            let name = std::str::from_utf8(attribute.key.as_ref())
891                .map_err(|_| XmlMutationError::InvalidAppendTarget)?;
892            Ok(match name {
893                "xmlns" => Some(String::new()),
894                _ => name.strip_prefix("xmlns:").map(str::to_owned),
895            })
896        })
897        .filter_map(|result| result.transpose())
898        .collect()
899}
900
901fn is_matching_empty_placeholder(
902    node: roxmltree::Node<'_, '_>,
903    generated_sources: &[(Option<&str>, &str)],
904) -> bool {
905    generated_sources.iter().any(|(namespace, name)| {
906        node.tag_name().namespace() == *namespace
907            && node.tag_name().name() == *name
908            && is_reusable_placeholder(node)
909    })
910}
911
912fn is_reusable_placeholder(node: roxmltree::Node<'_, '_>) -> bool {
913    node.children()
914        .all(|child| child.is_text() && child.text().is_some_and(is_xml_whitespace_only))
915}
916
917fn has_cryptographic_identity_content(node: roxmltree::Node<'_, '_>) -> bool {
918    if is_dsig_x509_data(node.tag_name().namespace(), node.tag_name().name()) {
919        return node
920            .children()
921            .any(|child| child.is_element() && is_x509_identity_child(child));
922    }
923    if node.children().any(|child| child.is_element()) {
924        return true;
925    }
926    match (node.tag_name().namespace(), node.tag_name().name()) {
927        (Some(XMLDSIG_NS), "KeyName") => node
928            .children()
929            .filter_map(|child| child.text())
930            .any(|text| !is_xml_whitespace_only(text)),
931        (Some(XMLDSIG_NS), "RetrievalMethod") => node.attribute("URI").is_some(),
932        (Some("http://www.w3.org/2009/xmldsig11#"), "DEREncodedKeyValue") => node
933            .children()
934            .filter_map(|child| child.text())
935            .any(|text| !is_xml_whitespace_only(text)),
936        _ => false,
937    }
938}
939
940fn is_dsig_x509_data(namespace: Option<&str>, name: &str) -> bool {
941    namespace == Some(XMLDSIG_NS) && name == "X509Data"
942}
943
944fn is_x509_identity_child(node: roxmltree::Node<'_, '_>) -> bool {
945    matches!(
946        (node.tag_name().namespace(), node.tag_name().name()),
947        (
948            Some(XMLDSIG_NS),
949            "X509IssuerSerial" | "X509SKI" | "X509SubjectName" | "X509Certificate"
950        ) | (Some("http://www.w3.org/2009/xmldsig11#"), "X509Digest")
951    )
952}
953
954fn has_x509_mergeable_metadata(node: roxmltree::Node<'_, '_>) -> bool {
955    node.children().any(|child| child.is_element()) && !has_cryptographic_identity_content(node)
956}
957
958fn is_cryptographic_key_info_source(namespace: Option<&str>, name: &str) -> bool {
959    matches!(
960        (namespace, name),
961        (
962            Some(XMLDSIG_NS),
963            "KeyValue" | "RetrievalMethod" | "X509Data" | "PGPData" | "SPKIData"
964        ) | (
965            Some("http://www.w3.org/2009/xmldsig11#"),
966            "DEREncodedKeyValue" | "KeyInfoReference"
967        )
968    )
969}
970
971fn is_dsig_key_name(namespace: Option<&str>, name: &str) -> bool {
972    namespace == Some(XMLDSIG_NS) && name == "KeyName"
973}
974
975fn element_inner_xml(xml: &str, range: Range<usize>) -> Result<&str, XmlMutationError> {
976    let element = &xml[range];
977    if element.trim_end().ends_with("/>") {
978        return Ok("");
979    }
980    let content_start =
981        element_opening_end(element).ok_or(XmlMutationError::InvalidAppendTarget)?;
982    let content_end = element
983        .rfind("</")
984        .ok_or(XmlMutationError::InvalidAppendTarget)?;
985    Ok(&element[content_start..content_end])
986}
987
988fn replace_element_content(
989    xml: &str,
990    range: Range<usize>,
991    content: &str,
992    namespace_attributes: &str,
993    policy: Option<&crate::policy::SigningPolicy>,
994) -> Result<String, XmlMutationError> {
995    let element = &xml[range.clone()];
996    let replacement_len = if element.trim_end().ends_with("/>") {
997        let name_end = element[1..]
998            .find(|character: char| {
999                character.is_ascii_whitespace() || character == '/' || character == '>'
1000            })
1001            .map(|offset| offset + 1)
1002            .ok_or(XmlMutationError::InvalidAppendTarget)?;
1003        let qualified_name = &element[1..name_end];
1004        let empty_end = element
1005            .rfind("/>")
1006            .ok_or(XmlMutationError::InvalidAppendTarget)?;
1007        empty_end
1008            .checked_add(namespace_attributes.len())
1009            .and_then(|length| length.checked_add(1))
1010            .and_then(|length| length.checked_add(content.len()))
1011            .and_then(|length| length.checked_add(2))
1012            .and_then(|length| length.checked_add(qualified_name.len()))
1013            .and_then(|length| length.checked_add(1))
1014            .ok_or_else(|| projected_xml_length_overflow(policy))?
1015    } else {
1016        let content_start =
1017            element_opening_end(element).ok_or(XmlMutationError::InvalidAppendTarget)?;
1018        let content_end = element
1019            .rfind("</")
1020            .ok_or(XmlMutationError::InvalidAppendTarget)?;
1021        (content_start - 1)
1022            .checked_add(namespace_attributes.len())
1023            .and_then(|length| length.checked_add(1))
1024            .and_then(|length| length.checked_add(content.len()))
1025            .and_then(|length| length.checked_add(element.len() - content_end))
1026            .ok_or_else(|| projected_xml_length_overflow(policy))?
1027    };
1028    validate_projected_replacement_len(xml, range.len(), replacement_len, policy)?;
1029
1030    let mut output = xml.to_owned();
1031    if element.trim_end().ends_with("/>") {
1032        let name_end = element[1..]
1033            .find(|character: char| {
1034                character.is_ascii_whitespace() || character == '/' || character == '>'
1035            })
1036            .map(|offset| offset + 1)
1037            .ok_or(XmlMutationError::InvalidAppendTarget)?;
1038        let qualified_name = &element[1..name_end];
1039        let empty_end = element
1040            .rfind("/>")
1041            .ok_or(XmlMutationError::InvalidAppendTarget)?;
1042        output.replace_range(
1043            range,
1044            &format!(
1045                "{}{}>{}</{}>",
1046                &element[..empty_end],
1047                namespace_attributes,
1048                content,
1049                qualified_name
1050            ),
1051        );
1052    } else {
1053        let content_start =
1054            element_opening_end(element).ok_or(XmlMutationError::InvalidAppendTarget)?;
1055        let content_end = element
1056            .rfind("</")
1057            .ok_or(XmlMutationError::InvalidAppendTarget)?;
1058        let replacement = format!(
1059            "{}{}>{}{}",
1060            &element[..content_start - 1],
1061            namespace_attributes,
1062            content,
1063            &element[content_end..]
1064        );
1065        output.replace_range(range, &replacement);
1066    }
1067    Ok(output)
1068}
1069
1070fn append_element_content(
1071    xml: &str,
1072    range: Range<usize>,
1073    content: &str,
1074    namespace_attributes: &str,
1075    policy: Option<&crate::policy::SigningPolicy>,
1076) -> Result<String, XmlMutationError> {
1077    let element = &xml[range.clone()];
1078    let content_start =
1079        element_opening_end(element).ok_or(XmlMutationError::InvalidAppendTarget)?;
1080    let content_end = element
1081        .rfind("</")
1082        .ok_or(XmlMutationError::InvalidAppendTarget)?;
1083    let replacement_len = (content_start - 1)
1084        .checked_add(namespace_attributes.len())
1085        .and_then(|length| length.checked_add(1))
1086        .and_then(|length| length.checked_add(content_end - content_start))
1087        .and_then(|length| length.checked_add(content.len()))
1088        .and_then(|length| length.checked_add(element.len() - content_end))
1089        .ok_or_else(|| projected_xml_length_overflow(policy))?;
1090    validate_projected_replacement_len(xml, range.len(), replacement_len, policy)?;
1091    let replacement = format!(
1092        "{}{}>{}{}{}",
1093        &element[..content_start - 1],
1094        namespace_attributes,
1095        &element[content_start..content_end],
1096        content,
1097        &element[content_end..]
1098    );
1099    let mut output = xml.to_owned();
1100    output.replace_range(range, &replacement);
1101    Ok(output)
1102}
1103
1104fn validate_projected_replacement_len(
1105    xml: &str,
1106    removed_len: usize,
1107    added_len: usize,
1108    policy: Option<&crate::policy::SigningPolicy>,
1109) -> Result<usize, XmlMutationError> {
1110    let projected = xml
1111        .len()
1112        .checked_sub(removed_len)
1113        .and_then(|length| length.checked_add(added_len))
1114        .ok_or_else(|| projected_xml_length_overflow(policy))?;
1115    if let Some(policy) = policy {
1116        policy.resources.validate_xml_document_len(projected)?;
1117    }
1118    Ok(projected)
1119}
1120
1121fn element_opening_end(fragment: &str) -> Option<usize> {
1122    let mut quote = None;
1123    for (offset, character) in fragment.char_indices() {
1124        match (quote, character) {
1125            (None, '\'' | '"') => quote = Some(character),
1126            (Some(delimiter), current) if delimiter == current => quote = None,
1127            (None, '>') => return Some(offset + 1),
1128            _ => {}
1129        }
1130    }
1131    None
1132}
1133
1134fn fill_dsig_values<I, S>(
1135    xml: &str,
1136    local_name: &'static str,
1137    values: I,
1138) -> Result<String, XmlMutationError>
1139where
1140    I: IntoIterator<Item = S>,
1141    S: AsRef<str>,
1142{
1143    let values: Vec<String> = values
1144        .into_iter()
1145        .map(|value| value.as_ref().to_owned())
1146        .collect();
1147    let expected = count_dsig_elements(xml, local_name)?;
1148    if expected != values.len() {
1149        return Err(XmlMutationError::ValueCountMismatch {
1150            element: local_name,
1151            expected,
1152            actual: values.len(),
1153        });
1154    }
1155
1156    fill_dsig_values_matching(xml, local_name, values, None, None, |_, _| true)
1157}
1158
1159fn fill_dsig_values_matching(
1160    xml: &str,
1161    local_name: &'static str,
1162    values: Vec<String>,
1163    policy: Option<&crate::policy::SigningPolicy>,
1164    budget: Option<&XmlParseWorkBudget>,
1165    mut should_replace: impl FnMut(&[(bool, Vec<u8>, Option<usize>)], &ResolveResult<'_>) -> bool,
1166) -> Result<String, XmlMutationError> {
1167    if let Some(budget) = budget {
1168        budget.charge_policy(xml.len())?;
1169    }
1170    let mut reader = NsReader::from_str(xml);
1171    let mut writer = Writer::new(Vec::new());
1172    let mut buf = Vec::new();
1173    let mut value_index = 0usize;
1174    let mut replacing_depth: Option<usize> = None;
1175    let mut element_stack: Vec<(bool, Vec<u8>, Option<usize>)> = Vec::new();
1176    let mut signature_index = 0usize;
1177
1178    loop {
1179        let (namespace, event) = reader.read_resolved_event_into(&mut buf)?;
1180        if let Some(depth) = replacing_depth.as_mut() {
1181            match event {
1182                Event::Start(_) => *depth += 1,
1183                Event::End(end) if *depth == 0 => {
1184                    writer.write_event(Event::End(end))?;
1185                    replacing_depth = None;
1186                    element_stack.pop();
1187                }
1188                Event::End(_) => *depth -= 1,
1189                Event::Eof => break,
1190                _ => {}
1191            }
1192            buf.clear();
1193            continue;
1194        }
1195
1196        match event {
1197            Event::Start(element)
1198                if is_dsig_element(&namespace, element.local_name().as_ref(), local_name)
1199                    && should_replace(&element_stack, &namespace) =>
1200            {
1201                let signature = signature_stack_index(
1202                    &namespace,
1203                    element.local_name().as_ref(),
1204                    &mut signature_index,
1205                );
1206                element_stack.push((
1207                    is_dsig_namespace(&namespace),
1208                    element.local_name().as_ref().to_vec(),
1209                    signature,
1210                ));
1211                writer.write_event(Event::Start(element))?;
1212                writer.write_event(Event::Text(BytesText::new(&values[value_index])))?;
1213                value_index += 1;
1214                replacing_depth = Some(0);
1215            }
1216            Event::Empty(element)
1217                if is_dsig_element(&namespace, element.local_name().as_ref(), local_name)
1218                    && should_replace(&element_stack, &namespace) =>
1219            {
1220                let _signature = signature_stack_index(
1221                    &namespace,
1222                    element.local_name().as_ref(),
1223                    &mut signature_index,
1224                );
1225                writer.write_event(Event::Start(element.borrow()))?;
1226                writer.write_event(Event::Text(BytesText::new(&values[value_index])))?;
1227                value_index += 1;
1228                writer.write_event(Event::End(element.to_end()))?;
1229            }
1230            Event::Start(element) => {
1231                let signature = signature_stack_index(
1232                    &namespace,
1233                    element.local_name().as_ref(),
1234                    &mut signature_index,
1235                );
1236                element_stack.push((
1237                    is_dsig_namespace(&namespace),
1238                    element.local_name().as_ref().to_vec(),
1239                    signature,
1240                ));
1241                writer.write_event(Event::Start(element))?;
1242            }
1243            Event::Empty(element) => {
1244                let _signature = signature_stack_index(
1245                    &namespace,
1246                    element.local_name().as_ref(),
1247                    &mut signature_index,
1248                );
1249                writer.write_event(Event::Empty(element))?
1250            }
1251            Event::End(element) => {
1252                element_stack.pop();
1253                writer.write_event(Event::End(element))?;
1254            }
1255            Event::Eof => break,
1256            event => writer.write_event(event)?,
1257        }
1258        buf.clear();
1259    }
1260
1261    if value_index != values.len() {
1262        return Err(XmlMutationError::ValueCountMismatch {
1263            element: local_name,
1264            expected: values.len(),
1265            actual: value_index,
1266        });
1267    }
1268
1269    let output = String::from_utf8(writer.into_inner())?;
1270    parse_mutation_xml_with_budget(&output, policy, budget)?;
1271    Ok(output)
1272}
1273
1274fn fill_dsig_element_raw_matching(
1275    xml: &str,
1276    local_name: &'static str,
1277    content: &str,
1278    policy: Option<&crate::policy::SigningPolicy>,
1279    mut should_replace: impl FnMut(&[(bool, Vec<u8>, Option<usize>)], &ResolveResult<'_>) -> bool,
1280) -> Result<String, XmlMutationError> {
1281    let mut reader = NsReader::from_str(xml);
1282    let mut writer = Writer::new(Vec::new());
1283    let mut buf = Vec::new();
1284    let mut replacing_depth: Option<usize> = None;
1285    let mut element_stack: Vec<(bool, Vec<u8>, Option<usize>)> = Vec::new();
1286    let mut signature_index = 0usize;
1287
1288    loop {
1289        let (namespace, event) = reader.read_resolved_event_into(&mut buf)?;
1290        if let Some(depth) = replacing_depth.as_mut() {
1291            match event {
1292                Event::Start(_) => *depth += 1,
1293                Event::End(end) if *depth == 0 => {
1294                    writer.write_event(Event::End(end))?;
1295                    replacing_depth = None;
1296                    element_stack.pop();
1297                }
1298                Event::End(_) => *depth -= 1,
1299                Event::Eof => break,
1300                _ => {}
1301            }
1302            buf.clear();
1303            continue;
1304        }
1305
1306        match event {
1307            Event::Start(element)
1308                if is_dsig_element(&namespace, element.local_name().as_ref(), local_name)
1309                    && should_replace(&element_stack, &namespace) =>
1310            {
1311                let signature = signature_stack_index(
1312                    &namespace,
1313                    element.local_name().as_ref(),
1314                    &mut signature_index,
1315                );
1316                element_stack.push((
1317                    is_dsig_namespace(&namespace),
1318                    element.local_name().as_ref().to_vec(),
1319                    signature,
1320                ));
1321                writer.write_event(Event::Start(element))?;
1322                writer.get_mut().write_all(content.as_bytes())?;
1323                replacing_depth = Some(0);
1324            }
1325            Event::Empty(element)
1326                if is_dsig_element(&namespace, element.local_name().as_ref(), local_name)
1327                    && should_replace(&element_stack, &namespace) =>
1328            {
1329                let _signature = signature_stack_index(
1330                    &namespace,
1331                    element.local_name().as_ref(),
1332                    &mut signature_index,
1333                );
1334                writer.write_event(Event::Start(element.borrow()))?;
1335                writer.get_mut().write_all(content.as_bytes())?;
1336                writer.write_event(Event::End(element.to_end()))?;
1337            }
1338            Event::Start(element) => {
1339                let signature = signature_stack_index(
1340                    &namespace,
1341                    element.local_name().as_ref(),
1342                    &mut signature_index,
1343                );
1344                element_stack.push((
1345                    is_dsig_namespace(&namespace),
1346                    element.local_name().as_ref().to_vec(),
1347                    signature,
1348                ));
1349                writer.write_event(Event::Start(element))?;
1350            }
1351            Event::Empty(element) => {
1352                let _signature = signature_stack_index(
1353                    &namespace,
1354                    element.local_name().as_ref(),
1355                    &mut signature_index,
1356                );
1357                writer.write_event(Event::Empty(element))?
1358            }
1359            Event::End(element) => {
1360                element_stack.pop();
1361                writer.write_event(Event::End(element))?;
1362            }
1363            Event::Eof => break,
1364            event => writer.write_event(event)?,
1365        }
1366        buf.clear();
1367    }
1368
1369    let output = String::from_utf8(writer.into_inner())?;
1370    parse_mutation_xml_with_options(&output, policy)?;
1371    Ok(output)
1372}
1373
1374fn validate_signature_template(
1375    signature_template: &str,
1376    policy: Option<&crate::policy::SigningPolicy>,
1377) -> Result<(), XmlMutationError> {
1378    let document = parse_mutation_xml_with_options(signature_template, policy)?;
1379    let root = document.root_element();
1380    if root.tag_name().namespace() == Some(XMLDSIG_NS) && root.tag_name().name() == "Signature" {
1381        Ok(())
1382    } else {
1383        Err(XmlMutationError::InvalidSignatureTemplate)
1384    }
1385}
1386
1387fn count_dsig_elements(xml: &str, local_name: &str) -> Result<usize, XmlMutationError> {
1388    let document = parse_mutation_xml_with_options(xml, None)?;
1389    Ok(document
1390        .descendants()
1391        .filter(|node| {
1392            node.is_element()
1393                && node.tag_name().namespace() == Some(XMLDSIG_NS)
1394                && node.tag_name().name() == local_name
1395        })
1396        .count())
1397}
1398
1399fn count_signed_info_digest_values(
1400    xml: &str,
1401    target_signature: usize,
1402    policy: Option<&crate::policy::SigningPolicy>,
1403    budget: Option<&XmlParseWorkBudget>,
1404) -> Result<usize, XmlMutationError> {
1405    let document = parse_mutation_xml_with_budget(xml, policy, budget)?;
1406    let Some(signature) = signature_node(&document, target_signature) else {
1407        return Ok(0);
1408    };
1409    Ok(document
1410        .descendants()
1411        .filter(|node| is_direct_signed_info_reference_digest(*node, signature))
1412        .count())
1413}
1414
1415fn count_direct_signature_values(
1416    xml: &str,
1417    target_signature: usize,
1418    policy: Option<&crate::policy::SigningPolicy>,
1419    budget: Option<&XmlParseWorkBudget>,
1420) -> Result<usize, XmlMutationError> {
1421    let document = parse_mutation_xml_with_budget(xml, policy, budget)?;
1422    let Some(signature) = signature_node(&document, target_signature) else {
1423        return Ok(0);
1424    };
1425    Ok(document
1426        .descendants()
1427        .filter(|node| {
1428            node.is_element()
1429                && node.tag_name().namespace() == Some(XMLDSIG_NS)
1430                && node.tag_name().name() == "SignatureValue"
1431                && node.parent().is_some_and(|parent| parent == signature)
1432        })
1433        .count())
1434}
1435
1436fn count_direct_key_infos(
1437    xml: &str,
1438    target_signature: usize,
1439    policy: Option<&crate::policy::SigningPolicy>,
1440) -> Result<usize, XmlMutationError> {
1441    let document = parse_mutation_xml_with_options(xml, policy)?;
1442    let Some(signature) = signature_node(&document, target_signature) else {
1443        return Ok(0);
1444    };
1445    Ok(document
1446        .descendants()
1447        .filter(|node| {
1448            node.is_element()
1449                && node.tag_name().namespace() == Some(XMLDSIG_NS)
1450                && node.tag_name().name() == "KeyInfo"
1451                && node.parent().is_some_and(|parent| parent == signature)
1452        })
1453        .count())
1454}
1455
1456fn signature_node<'a>(
1457    document: &'a roxmltree::Document<'a>,
1458    target_signature: usize,
1459) -> Option<roxmltree::Node<'a, 'a>> {
1460    document
1461        .descendants()
1462        .filter(|node| is_dsig_node(*node, "Signature"))
1463        .nth(target_signature)
1464}
1465
1466fn last_signature_index(
1467    xml: &str,
1468    policy: Option<&crate::policy::SigningPolicy>,
1469    budget: Option<&XmlParseWorkBudget>,
1470) -> Result<usize, XmlMutationError> {
1471    let document = parse_mutation_xml_with_budget(xml, policy, budget)?;
1472    document
1473        .descendants()
1474        .filter(|node| is_dsig_node(*node, "Signature"))
1475        .enumerate()
1476        .last()
1477        .map(|(index, _)| index)
1478        .ok_or(XmlMutationError::ValueCountMismatch {
1479            element: "Signature",
1480            expected: 1,
1481            actual: 0,
1482        })
1483}
1484
1485fn is_direct_signed_info_reference_digest(
1486    node: roxmltree::Node<'_, '_>,
1487    signature: roxmltree::Node<'_, '_>,
1488) -> bool {
1489    node.is_element()
1490        && node.tag_name().namespace() == Some(XMLDSIG_NS)
1491        && node.tag_name().name() == "DigestValue"
1492        && node
1493            .parent()
1494            .is_some_and(|parent| is_dsig_node(parent, "Reference"))
1495        && node
1496            .parent()
1497            .and_then(|parent| parent.parent())
1498            .is_some_and(|grandparent| is_dsig_node(grandparent, "SignedInfo"))
1499        && node
1500            .parent()
1501            .and_then(|parent| parent.parent())
1502            .and_then(|grandparent| grandparent.parent())
1503            .is_some_and(|parent| parent == signature)
1504}
1505
1506fn is_dsig_node(node: roxmltree::Node<'_, '_>, expected_local: &str) -> bool {
1507    node.is_element()
1508        && node.tag_name().namespace() == Some(XMLDSIG_NS)
1509        && node.tag_name().name() == expected_local
1510}
1511
1512fn is_signed_info_reference_context(
1513    element_stack: &[(bool, Vec<u8>, Option<usize>)],
1514    namespace: &ResolveResult<'_>,
1515    target_signature: usize,
1516) -> bool {
1517    is_dsig_namespace(namespace)
1518        && is_in_target_signature(element_stack, target_signature)
1519        && matches!(
1520            element_stack,
1521            [.., (true, signed_info, _), (true, reference, _)]
1522                if signed_info.as_slice() == b"SignedInfo"
1523                    && reference.as_slice() == b"Reference"
1524        )
1525}
1526
1527fn is_direct_signature_context(
1528    element_stack: &[(bool, Vec<u8>, Option<usize>)],
1529    namespace: &ResolveResult<'_>,
1530    target_signature: usize,
1531) -> bool {
1532    is_dsig_namespace(namespace)
1533        && is_in_target_signature(element_stack, target_signature)
1534        && matches!(
1535            element_stack,
1536            [.., (true, signature, Some(index))]
1537                if signature.as_slice() == b"Signature" && *index == target_signature
1538        )
1539}
1540
1541fn is_in_target_signature(
1542    element_stack: &[(bool, Vec<u8>, Option<usize>)],
1543    target_signature: usize,
1544) -> bool {
1545    element_stack
1546        .iter()
1547        .rev()
1548        .find(|(is_dsig, local_name, _)| *is_dsig && local_name.as_slice() == b"Signature")
1549        .is_some_and(|(_, _, signature)| *signature == Some(target_signature))
1550}
1551
1552fn is_dsig_element(namespace: &ResolveResult<'_>, local: &[u8], expected_local: &str) -> bool {
1553    is_dsig_namespace(namespace) && local == expected_local.as_bytes()
1554}
1555
1556fn is_dsig_namespace(namespace: &ResolveResult<'_>) -> bool {
1557    matches!(namespace, ResolveResult::Bound(Namespace(ns)) if *ns == XMLDSIG_NS.as_bytes())
1558}
1559
1560fn signature_stack_index(
1561    namespace: &ResolveResult<'_>,
1562    local_name: &[u8],
1563    next_signature_index: &mut usize,
1564) -> Option<usize> {
1565    if is_dsig_namespace(namespace) && local_name == b"Signature" {
1566        let index = *next_signature_index;
1567        *next_signature_index += 1;
1568        Some(index)
1569    } else {
1570        None
1571    }
1572}
1573
1574#[cfg(test)]
1575mod tests {
1576    use crate::c14n::{C14nAlgorithm, C14nMode};
1577    use crate::xmldsig::{
1578        DigestAlgorithm, ReferenceBuilder, SignatureAlgorithm, SignatureBuilder, Transform,
1579    };
1580
1581    use super::*;
1582
1583    fn template(reference_count: usize) -> String {
1584        let mut builder = SignatureBuilder::new(
1585            C14nAlgorithm::new(C14nMode::Exclusive1_0, false),
1586            SignatureAlgorithm::RsaSha256,
1587        )
1588        .ns_prefix("ds");
1589        for index in 0..reference_count {
1590            builder = builder.add_reference(
1591                ReferenceBuilder::new(DigestAlgorithm::Sha256)
1592                    .uri(format!("#ref-{index}"))
1593                    .transform(Transform::Enveloped),
1594            );
1595        }
1596        builder.build_template().expect("valid template")
1597    }
1598
1599    #[test]
1600    fn signature_value_projection_matches_streaming_mutation() {
1601        // Allocation preflight must predict the exact serializer output for
1602        // both XML spellings accepted as an empty SignatureValue placeholder.
1603        for placeholder in [
1604            "<ds:SignatureValue/>",
1605            "<ds:SignatureValue></ds:SignatureValue>",
1606        ] {
1607            let xml = format!(
1608                "<root><ds:Signature xmlns:ds=\"{XMLDSIG_NS}\"><ds:SignedInfo/>{placeholder}</ds:Signature></root>"
1609            );
1610            let value = "A".repeat(341);
1611            let projected = projected_signature_value_output_len_at_index_with_options(
1612                &xml,
1613                value.len(),
1614                0,
1615                Some(&crate::policy::SigningPolicy::default()),
1616            )
1617            .expect("project SignatureValue output length");
1618            let mutated = fill_signature_value_at_index_with_options(
1619                &xml,
1620                &value,
1621                0,
1622                Some(&crate::policy::SigningPolicy::default()),
1623            )
1624            .expect("fill SignatureValue");
1625
1626            assert_eq!(projected, mutated.len());
1627        }
1628    }
1629
1630    #[test]
1631    fn streaming_mutation_scan_consumes_the_shared_parse_budget() {
1632        // The quick-xml rewrite and every bounded backend pass before and after
1633        // it consume one operation-wide allowance.
1634        let xml = format!(
1635            "<root><ds:Signature xmlns:ds=\"{XMLDSIG_NS}\"><ds:SignatureValue/></ds:Signature></root>"
1636        );
1637        let resources = crate::policy::ResourcePolicy::default();
1638        let budget = XmlParseWorkBudget::from_resources(&resources);
1639        let output = fill_signature_value_at_index_with_budget(
1640            &xml,
1641            "signature",
1642            0,
1643            Some(&crate::policy::SigningPolicy::default()),
1644            Some(&budget),
1645        )
1646        .expect("streaming mutation must succeed");
1647
1648        let dom_passes = 2 + usize::from(cfg!(feature = "xml-backend-xmloxide"));
1649        assert_eq!(
1650            budget.consumed(),
1651            xml.len() * (dom_passes + 1) + output.len() * dom_passes
1652        );
1653    }
1654
1655    #[test]
1656    fn appends_signature_template_to_non_empty_root() {
1657        let signed = append_signature_to_root("<root><payload ID=\"ref-0\"/></root>", &template(1))
1658            .expect("append signature");
1659        let document = roxmltree::Document::parse(&signed).expect("parse output");
1660        let root = document.root_element();
1661        let children: Vec<_> = root
1662            .children()
1663            .filter(roxmltree::Node::is_element)
1664            .map(|node| node.tag_name().name())
1665            .collect();
1666        assert_eq!(children, ["payload", "Signature"]);
1667        assert_eq!(
1668            root.last_element_child()
1669                .expect("signature")
1670                .tag_name()
1671                .namespace(),
1672            Some(XMLDSIG_NS)
1673        );
1674    }
1675
1676    #[test]
1677    fn appends_signature_template_to_empty_root() {
1678        let signed = append_signature_to_root("<root/>", &template(1)).expect("append signature");
1679        let document = roxmltree::Document::parse(&signed).expect("parse output");
1680        let root = document.root_element();
1681        assert_eq!(
1682            root.first_element_child()
1683                .expect("signature")
1684                .tag_name()
1685                .name(),
1686            "Signature"
1687        );
1688    }
1689
1690    #[test]
1691    fn appends_signature_template_to_selected_empty_element() {
1692        // Selected builder targets may be self-closing; insertion must expand
1693        // the element without dropping its qualified name or attributes.
1694        let source = r#"<root xmlns:s="urn:scope"><s:scope Id="urn:selected/item"/></root>"#;
1695        let mut document = crate::XmlDocument::parse(source).expect("source must parse");
1696        let registrations = [crate::IdAttributeRegistration::global("Id")];
1697        let scope = document.with_view(|view| {
1698            view.node_for_id("urn:selected/item", &registrations)
1699                .expect("selected scope")
1700        });
1701        document
1702            .append_child(scope, &template(1))
1703            .expect("selected empty element must accept a signature");
1704        let signed = document.into_xml();
1705        let output = roxmltree::Document::parse(&signed).expect("output must parse");
1706        let scope = output
1707            .descendants()
1708            .find(|node| node.has_tag_name(("urn:scope", "scope")))
1709            .expect("qualified scope must remain");
1710
1711        assert!(
1712            scope
1713                .children()
1714                .any(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
1715        );
1716        assert_eq!(scope.attribute("Id"), Some("urn:selected/item"));
1717    }
1718
1719    #[test]
1720    fn rejects_non_signature_template() {
1721        let err = append_signature_to_root("<root/>", "<NotSignature/>")
1722            .expect_err("template must be a Signature");
1723        assert!(matches!(err, XmlMutationError::InvalidSignatureTemplate));
1724    }
1725
1726    #[test]
1727    fn signature_template_validation_applies_the_active_policy_first() {
1728        // The separately supplied template is an untrusted XML allocation
1729        // boundary. Reject it before parsing the source or constructing output.
1730        let template = format!(
1731            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo>{}</ds:SignedInfo><ds:SignatureValue/></ds:Signature>"#,
1732            "<part/>".repeat(16),
1733        );
1734
1735        let byte_policy = crate::policy::SigningPolicy {
1736            resources: crate::policy::ResourcePolicy {
1737                max_xml_document_bytes: template.len() - 1,
1738                ..crate::policy::ResourcePolicy::default()
1739            },
1740            ..crate::policy::SigningPolicy::default()
1741        };
1742        let byte_error =
1743            append_signature_to_root_with_options("not XML", &template, Some(&byte_policy))
1744                .expect_err("template byte policy must win before source parsing");
1745        assert!(matches!(
1746            byte_error,
1747            XmlMutationError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1748                resource: crate::policy::resource_name::XML_DOCUMENT,
1749                maximum,
1750                actual,
1751            }) if maximum == template.len() - 1 && actual == template.len()
1752        ));
1753
1754        let node_policy = crate::policy::SigningPolicy {
1755            resources: crate::policy::ResourcePolicy {
1756                max_xml_nodes: 2,
1757                ..crate::policy::ResourcePolicy::default()
1758            },
1759            ..crate::policy::SigningPolicy::default()
1760        };
1761        let node_error =
1762            append_signature_to_root_with_options("not XML", &template, Some(&node_policy))
1763                .expect_err("template node policy must win before source parsing");
1764        assert!(matches!(
1765            node_error,
1766            XmlMutationError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1767                resource: crate::policy::resource_name::XML_NODES,
1768                maximum: 2,
1769                actual: 3,
1770            })
1771        ));
1772    }
1773
1774    #[test]
1775    fn fills_digest_values_in_xml_dsig_document_order() {
1776        let signed = append_signature_to_root("<root/>", &template(2)).expect("append signature");
1777        let filled =
1778            fill_digest_values(&signed, ["digest-one", "digest-two"]).expect("fill digest values");
1779        let document = roxmltree::Document::parse(&filled).expect("parse output");
1780        let values: Vec<_> = document
1781            .descendants()
1782            .filter(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
1783            .map(|node| node.text())
1784            .collect();
1785        assert_eq!(values, [Some("digest-one"), Some("digest-two")]);
1786    }
1787
1788    #[test]
1789    fn fills_signature_value_without_touching_digest_values() {
1790        let signed = append_signature_to_root("<root/>", &template(1)).expect("append signature");
1791        let filled =
1792            fill_signature_values(&signed, ["signature&bytes"]).expect("fill signature value");
1793        let document = roxmltree::Document::parse(&filled).expect("parse output");
1794        let signature_value = document
1795            .descendants()
1796            .find(|node| node.has_tag_name((XMLDSIG_NS, "SignatureValue")))
1797            .expect("SignatureValue");
1798        assert_eq!(signature_value.text(), Some("signature&bytes"));
1799        let digest_value = document
1800            .descendants()
1801            .find(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
1802            .expect("DigestValue");
1803        assert_eq!(digest_value.text(), None);
1804    }
1805
1806    #[test]
1807    fn replacement_count_must_match_dsig_elements() {
1808        let signed = append_signature_to_root("<root/>", &template(2)).expect("append signature");
1809        let err = fill_digest_values(&signed, ["only-one"]).expect_err("mismatch");
1810        assert!(matches!(
1811            err,
1812            XmlMutationError::ValueCountMismatch {
1813                element: "DigestValue",
1814                expected: 2,
1815                actual: 1
1816            }
1817        ));
1818    }
1819
1820    #[test]
1821    fn does_not_replace_foreign_same_local_name_elements() {
1822        let source = r#"<root xmlns:foreign="urn:test"><foreign:DigestValue>keep</foreign:DigestValue></root>"#;
1823        let signed = append_signature_to_root(source, &template(1)).expect("append signature");
1824        let filled = fill_digest_values(&signed, ["digest"]).expect("fill digest");
1825        let document = roxmltree::Document::parse(&filled).expect("parse output");
1826        let foreign = document
1827            .descendants()
1828            .find(|node| node.has_tag_name(("urn:test", "DigestValue")))
1829            .expect("foreign DigestValue");
1830        assert_eq!(foreign.text(), Some("keep"));
1831        let dsig = document
1832            .descendants()
1833            .find(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
1834            .expect("dsig DigestValue");
1835        assert_eq!(dsig.text(), Some("digest"));
1836    }
1837
1838    #[test]
1839    fn replacement_preserves_target_end_after_self_closing_child() {
1840        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>"#;
1841        let filled = fill_digest_values(source, ["digest"]).expect("fill digest");
1842        let document = roxmltree::Document::parse(&filled).expect("parse output");
1843        let digest_value = document
1844            .descendants()
1845            .find(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
1846            .expect("DigestValue");
1847        assert_eq!(digest_value.text(), Some("digest"));
1848        assert_eq!(
1849            digest_value
1850                .next_sibling_element()
1851                .map(|node| node.tag_name().name()),
1852            None
1853        );
1854    }
1855
1856    #[test]
1857    fn replacement_fails_when_nested_dsig_values_are_skipped() {
1858        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>"#;
1859        let err =
1860            fill_digest_values(source, ["outer", "nested"]).expect_err("nested target skipped");
1861        assert!(matches!(
1862            err,
1863            XmlMutationError::ValueCountMismatch {
1864                element: "DigestValue",
1865                expected: 2,
1866                actual: 1
1867            }
1868        ));
1869    }
1870
1871    #[test]
1872    fn indexed_digest_replacement_ignores_nested_signatures() {
1873        // Digest counts and replacements must use the same nearest-Signature
1874        // boundary or a nested Object signature can exhaust the value list.
1875        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>"#;
1876        let filled =
1877            fill_signed_info_digest_values_at_index_with_options(source, ["outer-new"], 0, None)
1878                .expect("outer signature replacement must ignore nested signatures");
1879        let document = roxmltree::Document::parse(&filled).expect("filled XML must parse");
1880        let values = document
1881            .descendants()
1882            .filter(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
1883            .filter_map(|node| node.text())
1884            .collect::<Vec<_>>();
1885
1886        assert_eq!(values, ["outer-new", "inner-keep"]);
1887    }
1888
1889    #[test]
1890    fn key_info_source_merge_preserves_placeholder_attributes() {
1891        // Placeholder identity can be referenced from SignedInfo, so filling
1892        // its children must not replace the element that owns the ID.
1893        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>"#;
1894        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:example:key-info"><X509Certificate>Y2VydA==</X509Certificate><ext:Metadata/></X509Data>"#;
1895
1896        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1897            .expect("matching source must populate the placeholder");
1898        let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
1899        let x509_data = document
1900            .descendants()
1901            .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
1902            .expect("X509Data");
1903
1904        assert_eq!(x509_data.attribute("Id"), Some("key-info"));
1905        assert_eq!(
1906            x509_data
1907                .children()
1908                .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Certificate")))
1909                .and_then(|node| node.text()),
1910            Some("Y2VydA==")
1911        );
1912        assert!(
1913            x509_data
1914                .children()
1915                .any(|node| node.has_tag_name(("urn:example:key-info", "Metadata")))
1916        );
1917    }
1918
1919    #[test]
1920    fn key_info_source_merge_preserves_comment_and_processing_instruction() {
1921        // Comments and processing instructions are caller-owned content, not an
1922        // empty placeholder that the generated identity may silently replace.
1923        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>"#;
1924        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#"><X509Certificate>Y2VydA==</X509Certificate></X509Data>"#;
1925
1926        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1927            .expect("generated identity must be appended without erasing caller content");
1928
1929        assert!(merged.contains("<!--keep-->"));
1930        assert!(merged.contains("<?audit preserve?>"));
1931        let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
1932        let x509_sources = document
1933            .descendants()
1934            .filter(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
1935            .collect::<Vec<_>>();
1936        assert_eq!(x509_sources.len(), 2);
1937        assert!(x509_sources.iter().any(|source| {
1938            source
1939                .children()
1940                .any(|node| node.has_tag_name((XMLDSIG_NS, "X509Certificate")))
1941        }));
1942    }
1943
1944    #[test]
1945    fn key_info_source_merge_preserves_non_xml_whitespace_text() {
1946        // XML only classifies space, tab, CR, and LF as whitespace. A non-breaking
1947        // space is caller-owned character data and must not turn X509Data into a
1948        // reusable placeholder that signing silently overwrites.
1949        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>";
1950        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#"><X509Certificate>Y2VydA==</X509Certificate></X509Data>"#;
1951
1952        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1953            .expect("generated identity must not replace non-whitespace character data");
1954        let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
1955        let x509_sources = document
1956            .descendants()
1957            .filter(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
1958            .collect::<Vec<_>>();
1959
1960        assert_eq!(x509_sources.len(), 2);
1961        assert!(
1962            x509_sources
1963                .iter()
1964                .any(|source| source.text() == Some("\u{00a0}"))
1965        );
1966    }
1967
1968    #[test]
1969    fn key_info_source_merge_replaces_populated_key_name_identity() {
1970        // A generated key name is authoritative identity metadata. Retaining a
1971        // populated template value would let document-order resolvers select it.
1972        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>"#;
1973        let generated =
1974            r#"<ds:KeyName xmlns:ds="http://www.w3.org/2000/09/xmldsig#">generated</ds:KeyName>"#;
1975
1976        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1977            .expect("generated KeyName must replace stale template identity");
1978        let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
1979        let key_names = document
1980            .descendants()
1981            .filter(|node| node.has_tag_name((XMLDSIG_NS, "KeyName")))
1982            .filter_map(|node| node.text())
1983            .collect::<Vec<_>>();
1984
1985        assert_eq!(key_names, ["generated"]);
1986    }
1987
1988    #[test]
1989    fn key_info_source_merge_preserves_x509_revocation_metadata() {
1990        // A generated certificate replaces stale identity assertions, but the
1991        // caller's CRL and extension metadata still apply to that X509 source.
1992        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>"#;
1993        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#"><X509Certificate>Z2VuZXJhdGVk</X509Certificate></X509Data>"#;
1994
1995        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1996            .expect("generated identity must preserve revocation metadata");
1997        let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
1998        let x509_sources = document
1999            .descendants()
2000            .filter(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
2001            .collect::<Vec<_>>();
2002
2003        assert_eq!(x509_sources.len(), 1);
2004        let x509_data = x509_sources[0];
2005        assert_eq!(x509_data.attribute("Id"), Some("caller"));
2006        assert_eq!(
2007            x509_data
2008                .children()
2009                .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Certificate")))
2010                .and_then(|node| node.text()),
2011            Some("Z2VuZXJhdGVk")
2012        );
2013        assert!(!merged.contains("c3RhbGU="));
2014        assert!(!merged.contains("CN=stale"));
2015        assert_eq!(
2016            x509_data
2017                .children()
2018                .find(|node| node.has_tag_name((XMLDSIG_NS, "X509CRL")))
2019                .and_then(|node| node.text()),
2020            Some("Y3Js")
2021        );
2022        assert_eq!(
2023            x509_data
2024                .children()
2025                .find(|node| node.has_tag_name(("urn:example:x509", "Policy")))
2026                .and_then(|node| node.text()),
2027            Some("keep")
2028        );
2029    }
2030
2031    #[test]
2032    fn key_info_source_merge_reports_required_and_observed_counts() {
2033        // Mutation diagnostics are a structured API: expected is the required
2034        // singleton count and actual is the number observed in the template.
2035        for (source, actual) in [
2036            (
2037                r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/>"#,
2038                0,
2039            ),
2040            (
2041                r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo/><ds:KeyInfo/></ds:Signature>"#,
2042                2,
2043            ),
2044        ] {
2045            let error = merge_key_info_source_at_index_with_options(
2046                source,
2047                r#"<ds:KeyName xmlns:ds="http://www.w3.org/2000/09/xmldsig#">key</ds:KeyName>"#,
2048                0,
2049                None,
2050            )
2051            .expect_err("KeyInfo must be a singleton");
2052            assert!(matches!(
2053                error,
2054                XmlMutationError::ValueCountMismatch {
2055                    element: "KeyInfo",
2056                    expected: 1,
2057                    actual: observed,
2058                } if observed == actual
2059            ));
2060        }
2061    }
2062
2063    #[test]
2064    fn key_info_source_merge_rejects_conflicting_placeholder_namespaces() {
2065        // A generated child cannot reuse a prefix that the placeholder owns
2066        // with another URI; emitting both declarations would create invalid XML.
2067        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>"#;
2068        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:writer"><ext:Metadata/></X509Data>"#;
2069
2070        let error = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2071            .expect_err("conflicting namespace bindings must fail before serialization");
2072
2073        assert!(matches!(
2074            error,
2075            XmlMutationError::ConflictingKeyInfoNamespace { prefix } if prefix == "ext"
2076        ));
2077    }
2078
2079    #[test]
2080    fn key_info_source_merge_allows_shadowing_inherited_namespaces() {
2081        // An ancestor binding is context, not an attribute owned by the empty
2082        // placeholder. The generated source may validly shadow it locally.
2083        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>"#;
2084        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:generated"><ext:Metadata/></X509Data>"#;
2085
2086        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2087            .expect("generated source may shadow an inherited namespace");
2088        let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
2089        let x509_data = document
2090            .descendants()
2091            .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
2092            .expect("X509Data");
2093
2094        assert_eq!(
2095            x509_data.lookup_namespace_uri(Some("ext")),
2096            Some("urn:generated")
2097        );
2098        assert!(
2099            x509_data
2100                .children()
2101                .any(|node| node.has_tag_name(("urn:generated", "Metadata")))
2102        );
2103    }
2104
2105    #[test]
2106    fn key_info_source_merge_detects_redundant_owned_namespace_conflicts() {
2107        // A direct declaration remains owned by the placeholder even when it
2108        // repeats the parent binding; replacing it would duplicate xmlns:ext.
2109        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>"#;
2110        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:writer"><ext:Metadata/></X509Data>"#;
2111
2112        let error = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2113            .expect_err("placeholder-owned namespace conflicts must be typed");
2114
2115        assert!(matches!(
2116            error,
2117            XmlMutationError::ConflictingKeyInfoNamespace { prefix } if prefix == "ext"
2118        ));
2119    }
2120
2121    #[test]
2122    fn key_info_source_merge_preserves_generated_attributes() {
2123        // Writer-owned identity must survive placeholder reuse so later
2124        // reference resolution observes the same element the writer emitted.
2125        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo><ds:X509Data/></ds:KeyInfo></ds:Signature>"#;
2126        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>"#;
2127
2128        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2129            .expect("generated attributes must populate the placeholder");
2130        let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
2131        let x509_data = document
2132            .descendants()
2133            .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
2134            .expect("X509Data");
2135
2136        assert_eq!(x509_data.attribute("Id"), Some("generated"));
2137        assert_eq!(
2138            x509_data.attribute(("urn:key-info", "role")),
2139            Some("signing")
2140        );
2141    }
2142
2143    #[test]
2144    fn key_info_source_merge_accepts_whitespace_around_namespace_equals() {
2145        // XML permits whitespace around '='. Namespace ownership must come
2146        // from the parsed element rather than an exact lexical substring.
2147        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo/></ds:Signature>"#;
2148        let generated = r#"<ext:Metadata xmlns:ext = "urn:key-info">value</ext:Metadata>"#;
2149
2150        let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2151            .expect("valid namespace declaration whitespace must be preserved");
2152        let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
2153        assert!(
2154            document
2155                .descendants()
2156                .any(|node| node.has_tag_name(("urn:key-info", "Metadata")))
2157        );
2158    }
2159
2160    #[test]
2161    fn key_info_source_merge_rejects_conflicting_generated_attributes() {
2162        // Silently choosing template or writer identity would make signed
2163        // references ambiguous, so incompatible expanded attributes fail.
2164        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo><ds:X509Data Id="template"/></ds:KeyInfo></ds:Signature>"#;
2165        let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" Id="generated"><X509Certificate>Y2VydA==</X509Certificate></X509Data>"#;
2166
2167        let error = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2168            .expect_err("conflicting attributes must fail before serialization");
2169
2170        assert!(matches!(
2171            error,
2172            XmlMutationError::ConflictingKeyInfoAttribute { name } if name == "Id"
2173        ));
2174    }
2175
2176    #[test]
2177    fn key_info_source_merge_rejects_empty_writer_output() {
2178        // An empty writer result is a writer-contract violation, not a malformed
2179        // signature append target, and callers need to distinguish the two.
2180        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo/></ds:Signature>"#;
2181
2182        let error = merge_key_info_source_at_index_with_options(source, "  ", 0, None)
2183            .expect_err("a key-info writer must emit an element child");
2184
2185        assert!(matches!(error, XmlMutationError::EmptyKeyInfoSource));
2186    }
2187
2188    #[test]
2189    fn key_info_source_merge_applies_policy_to_writer_fragments() {
2190        // A custom writer is an untrusted allocation boundary: its wrapper must
2191        // obey the same node ceiling as the caller's signing template.
2192        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo/></ds:Signature>"#;
2193        let children = (0..64).map(|_| "<part/>").collect::<String>();
2194        let generated = format!(
2195            r#"<ds:KeyName xmlns:ds="http://www.w3.org/2000/09/xmldsig#">{children}</ds:KeyName>"#
2196        );
2197        let policy = crate::policy::SigningPolicy {
2198            resources: crate::policy::ResourcePolicy {
2199                max_xml_nodes: 32,
2200                ..crate::policy::ResourcePolicy::default()
2201            },
2202            ..crate::policy::SigningPolicy::default()
2203        };
2204
2205        let error =
2206            merge_key_info_source_at_index_with_options(source, &generated, 0, Some(&policy))
2207                .expect_err("writer fragment must obey the signing node ceiling");
2208
2209        assert!(matches!(
2210            error,
2211            XmlMutationError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2212                resource: crate::policy::resource_name::XML_NODES,
2213                maximum: 32,
2214                actual: 33,
2215            })
2216        ));
2217    }
2218
2219    #[test]
2220    fn key_info_source_merge_bounds_synthesized_wrapper_before_parsing() {
2221        // The template and writer fragment can each fit while the namespace-
2222        // complete wrapper synthesized for fragment parsing crosses the byte
2223        // ceiling. Report that allocation boundary before parsing or merging.
2224        let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:inherited"><ds:KeyInfo/></ds:Signature>"#;
2225        let generated =
2226            r#"<ds:KeyName xmlns:ds="http://www.w3.org/2000/09/xmldsig#">recipient</ds:KeyName>"#;
2227        let document = roxmltree::Document::parse(source).expect("source must parse");
2228        let key_info = document
2229            .descendants()
2230            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
2231            .expect("KeyInfo");
2232        let wrapped =
2233            wrap_key_info_children(generated, key_info, None).expect("wrapper must serialize");
2234        let maximum = wrapped.len() - 1;
2235        assert!(source.len() <= maximum);
2236        assert!(generated.len() <= maximum);
2237        let policy = crate::policy::SigningPolicy {
2238            resources: crate::policy::ResourcePolicy {
2239                max_xml_document_bytes: maximum,
2240                ..crate::policy::ResourcePolicy::default()
2241            },
2242            ..crate::policy::SigningPolicy::default()
2243        };
2244
2245        let error =
2246            merge_key_info_source_at_index_with_options(source, generated, 0, Some(&policy))
2247                .expect_err("synthesized wrapper must be bounded before parsing");
2248
2249        assert!(matches!(
2250            error,
2251            XmlMutationError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2252                resource: crate::policy::resource_name::XML_DOCUMENT,
2253                maximum: observed_maximum,
2254                actual,
2255            }) if observed_maximum == maximum && actual == wrapped.len()
2256        ));
2257    }
2258}