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