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::io::Write;
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;
14
15/// Errors produced by XMLDSig XML mutation helpers.
16#[derive(Debug, thiserror::Error)]
17pub enum XmlMutationError {
18    /// Input XML or generated template is not parseable XML.
19    #[error("XML parsing error: {0}")]
20    XmlParse(#[from] roxmltree::Error),
21    /// The streaming XML reader failed.
22    #[error("XML read error: {0}")]
23    Read(#[from] quick_xml::Error),
24    /// The streaming XML writer failed.
25    #[error("XML write error: {0}")]
26    Write(#[from] std::io::Error),
27    /// The writer unexpectedly emitted non-UTF-8 bytes.
28    #[error("XML writer emitted invalid UTF-8: {0}")]
29    InvalidUtf8(#[from] std::string::FromUtf8Error),
30    /// A template did not contain exactly one XMLDSig `<Signature>` root.
31    #[error("signature template root must be one XMLDSig Signature element")]
32    InvalidSignatureTemplate,
33    /// A replacement call supplied a different number of values than matching elements.
34    #[error("expected {expected} XMLDSig {element} values, got {actual}")]
35    ValueCountMismatch {
36        /// XMLDSig element local name.
37        element: &'static str,
38        /// Number of matching XMLDSig elements in the document.
39        expected: usize,
40        /// Number of values supplied by the caller.
41        actual: usize,
42    },
43    /// The source XML did not contain a root element that can receive a signature.
44    #[error("source XML must contain a root element")]
45    MissingRootElement,
46}
47
48/// Append a generated XMLDSig `<Signature>` template as the last child of the
49/// source document root.
50pub fn append_signature_to_root(
51    xml: &str,
52    signature_template: &str,
53) -> Result<String, XmlMutationError> {
54    validate_signature_template(signature_template)?;
55    let source = roxmltree::Document::parse(xml)?;
56    if !source.root().children().any(|node| node.is_element()) {
57        return Err(XmlMutationError::MissingRootElement);
58    }
59
60    let mut reader = Reader::from_str(xml);
61    let mut writer = Writer::new(Vec::new());
62    let mut root_depth = 0usize;
63    let mut saw_root = false;
64    let mut buf = Vec::new();
65
66    loop {
67        match reader.read_event_into(&mut buf)? {
68            Event::Start(element) if root_depth == 0 => {
69                saw_root = true;
70                root_depth = 1;
71                writer.write_event(Event::Start(element))?;
72            }
73            Event::Start(element) => {
74                root_depth += 1;
75                writer.write_event(Event::Start(element))?;
76            }
77            Event::Empty(element) if root_depth == 0 => {
78                saw_root = true;
79                writer.write_event(Event::Start(element.borrow()))?;
80                writer.get_mut().write_all(signature_template.as_bytes())?;
81                writer.write_event(Event::End(element.to_end()))?;
82            }
83            Event::End(element) if root_depth == 1 => {
84                writer.get_mut().write_all(signature_template.as_bytes())?;
85                writer.write_event(Event::End(element))?;
86                root_depth = 0;
87            }
88            Event::End(element) => {
89                root_depth = root_depth.saturating_sub(1);
90                writer.write_event(Event::End(element))?;
91            }
92            Event::Eof => break,
93            event => writer.write_event(event)?,
94        }
95        buf.clear();
96    }
97
98    if !saw_root {
99        return Err(XmlMutationError::MissingRootElement);
100    }
101
102    let output = String::from_utf8(writer.into_inner())?;
103    roxmltree::Document::parse(&output)?;
104    Ok(output)
105}
106
107/// Fill XMLDSig `<DigestValue>` elements in document order.
108pub fn fill_digest_values<I, S>(xml: &str, values: I) -> Result<String, XmlMutationError>
109where
110    I: IntoIterator<Item = S>,
111    S: AsRef<str>,
112{
113    fill_dsig_values(xml, "DigestValue", values)
114}
115
116/// Fill `<DigestValue>` elements for direct `<SignedInfo>/<Reference>` children.
117pub fn fill_signed_info_digest_values<I, S>(
118    xml: &str,
119    values: I,
120) -> Result<String, XmlMutationError>
121where
122    I: IntoIterator<Item = S>,
123    S: AsRef<str>,
124{
125    let values: Vec<String> = values
126        .into_iter()
127        .map(|value| value.as_ref().to_owned())
128        .collect();
129    let target_signature = last_signature_index(xml)?;
130    let expected = count_signed_info_digest_values(xml)?;
131    if expected != values.len() {
132        return Err(XmlMutationError::ValueCountMismatch {
133            element: "DigestValue",
134            expected,
135            actual: values.len(),
136        });
137    }
138
139    fill_dsig_values_matching(xml, "DigestValue", values, |stack, namespace| {
140        is_signed_info_reference_context(stack, namespace, target_signature)
141    })
142}
143
144/// Fill XMLDSig `<SignatureValue>` elements in document order.
145pub fn fill_signature_values<I, S>(xml: &str, values: I) -> Result<String, XmlMutationError>
146where
147    I: IntoIterator<Item = S>,
148    S: AsRef<str>,
149{
150    fill_dsig_values(xml, "SignatureValue", values)
151}
152
153/// Fill the direct `<Signature>/<SignatureValue>` child for a signing template.
154pub fn fill_signature_value(xml: &str, value: &str) -> Result<String, XmlMutationError> {
155    let target_signature = last_signature_index(xml)?;
156    let expected = count_direct_signature_values(xml)?;
157    if expected != 1 {
158        return Err(XmlMutationError::ValueCountMismatch {
159            element: "SignatureValue",
160            expected,
161            actual: 1,
162        });
163    }
164
165    fill_dsig_values_matching(
166        xml,
167        "SignatureValue",
168        vec![value.to_owned()],
169        |stack, namespace| is_direct_signature_context(stack, namespace, target_signature),
170    )
171}
172
173/// Fill the direct `<Signature>/<KeyInfo>` child with XML child content.
174pub fn fill_key_info(xml: &str, key_info_content: &str) -> Result<String, XmlMutationError> {
175    let target_signature = last_signature_index(xml)?;
176    let expected = count_direct_key_infos(xml)?;
177    if expected != 1 {
178        return Err(XmlMutationError::ValueCountMismatch {
179            element: "KeyInfo",
180            expected,
181            actual: 1,
182        });
183    }
184
185    fill_dsig_element_raw_matching(xml, "KeyInfo", key_info_content, |stack, namespace| {
186        is_direct_signature_context(stack, namespace, target_signature)
187    })
188}
189
190fn fill_dsig_values<I, S>(
191    xml: &str,
192    local_name: &'static str,
193    values: I,
194) -> Result<String, XmlMutationError>
195where
196    I: IntoIterator<Item = S>,
197    S: AsRef<str>,
198{
199    let values: Vec<String> = values
200        .into_iter()
201        .map(|value| value.as_ref().to_owned())
202        .collect();
203    let expected = count_dsig_elements(xml, local_name)?;
204    if expected != values.len() {
205        return Err(XmlMutationError::ValueCountMismatch {
206            element: local_name,
207            expected,
208            actual: values.len(),
209        });
210    }
211
212    fill_dsig_values_matching(xml, local_name, values, |_, _| true)
213}
214
215fn fill_dsig_values_matching(
216    xml: &str,
217    local_name: &'static str,
218    values: Vec<String>,
219    mut should_replace: impl FnMut(&[(bool, Vec<u8>, Option<usize>)], &ResolveResult<'_>) -> bool,
220) -> Result<String, XmlMutationError> {
221    let mut reader = NsReader::from_str(xml);
222    let mut writer = Writer::new(Vec::new());
223    let mut buf = Vec::new();
224    let mut value_index = 0usize;
225    let mut replacing_depth: Option<usize> = None;
226    let mut element_stack: Vec<(bool, Vec<u8>, Option<usize>)> = Vec::new();
227    let mut signature_index = 0usize;
228
229    loop {
230        let (namespace, event) = reader.read_resolved_event_into(&mut buf)?;
231        if let Some(depth) = replacing_depth.as_mut() {
232            match event {
233                Event::Start(_) => *depth += 1,
234                Event::End(end) if *depth == 0 => {
235                    writer.write_event(Event::End(end))?;
236                    replacing_depth = None;
237                    element_stack.pop();
238                }
239                Event::End(_) => *depth -= 1,
240                Event::Eof => break,
241                _ => {}
242            }
243            buf.clear();
244            continue;
245        }
246
247        match event {
248            Event::Start(element)
249                if is_dsig_element(&namespace, element.local_name().as_ref(), local_name)
250                    && should_replace(&element_stack, &namespace) =>
251            {
252                let signature = signature_stack_index(
253                    &namespace,
254                    element.local_name().as_ref(),
255                    &mut signature_index,
256                );
257                element_stack.push((
258                    is_dsig_namespace(&namespace),
259                    element.local_name().as_ref().to_vec(),
260                    signature,
261                ));
262                writer.write_event(Event::Start(element))?;
263                writer.write_event(Event::Text(BytesText::new(&values[value_index])))?;
264                value_index += 1;
265                replacing_depth = Some(0);
266            }
267            Event::Empty(element)
268                if is_dsig_element(&namespace, element.local_name().as_ref(), local_name)
269                    && should_replace(&element_stack, &namespace) =>
270            {
271                let _signature = signature_stack_index(
272                    &namespace,
273                    element.local_name().as_ref(),
274                    &mut signature_index,
275                );
276                writer.write_event(Event::Start(element.borrow()))?;
277                writer.write_event(Event::Text(BytesText::new(&values[value_index])))?;
278                value_index += 1;
279                writer.write_event(Event::End(element.to_end()))?;
280            }
281            Event::Start(element) => {
282                let signature = signature_stack_index(
283                    &namespace,
284                    element.local_name().as_ref(),
285                    &mut signature_index,
286                );
287                element_stack.push((
288                    is_dsig_namespace(&namespace),
289                    element.local_name().as_ref().to_vec(),
290                    signature,
291                ));
292                writer.write_event(Event::Start(element))?;
293            }
294            Event::Empty(element) => {
295                let _signature = signature_stack_index(
296                    &namespace,
297                    element.local_name().as_ref(),
298                    &mut signature_index,
299                );
300                writer.write_event(Event::Empty(element))?
301            }
302            Event::End(element) => {
303                element_stack.pop();
304                writer.write_event(Event::End(element))?;
305            }
306            Event::Eof => break,
307            event => writer.write_event(event)?,
308        }
309        buf.clear();
310    }
311
312    if value_index != values.len() {
313        return Err(XmlMutationError::ValueCountMismatch {
314            element: local_name,
315            expected: values.len(),
316            actual: value_index,
317        });
318    }
319
320    let output = String::from_utf8(writer.into_inner())?;
321    roxmltree::Document::parse(&output)?;
322    Ok(output)
323}
324
325fn fill_dsig_element_raw_matching(
326    xml: &str,
327    local_name: &'static str,
328    content: &str,
329    mut should_replace: impl FnMut(&[(bool, Vec<u8>, Option<usize>)], &ResolveResult<'_>) -> bool,
330) -> Result<String, XmlMutationError> {
331    let mut reader = NsReader::from_str(xml);
332    let mut writer = Writer::new(Vec::new());
333    let mut buf = Vec::new();
334    let mut replacing_depth: Option<usize> = None;
335    let mut element_stack: Vec<(bool, Vec<u8>, Option<usize>)> = Vec::new();
336    let mut signature_index = 0usize;
337
338    loop {
339        let (namespace, event) = reader.read_resolved_event_into(&mut buf)?;
340        if let Some(depth) = replacing_depth.as_mut() {
341            match event {
342                Event::Start(_) => *depth += 1,
343                Event::End(end) if *depth == 0 => {
344                    writer.write_event(Event::End(end))?;
345                    replacing_depth = None;
346                    element_stack.pop();
347                }
348                Event::End(_) => *depth -= 1,
349                Event::Eof => break,
350                _ => {}
351            }
352            buf.clear();
353            continue;
354        }
355
356        match event {
357            Event::Start(element)
358                if is_dsig_element(&namespace, element.local_name().as_ref(), local_name)
359                    && should_replace(&element_stack, &namespace) =>
360            {
361                let signature = signature_stack_index(
362                    &namespace,
363                    element.local_name().as_ref(),
364                    &mut signature_index,
365                );
366                element_stack.push((
367                    is_dsig_namespace(&namespace),
368                    element.local_name().as_ref().to_vec(),
369                    signature,
370                ));
371                writer.write_event(Event::Start(element))?;
372                writer.get_mut().write_all(content.as_bytes())?;
373                replacing_depth = Some(0);
374            }
375            Event::Empty(element)
376                if is_dsig_element(&namespace, element.local_name().as_ref(), local_name)
377                    && should_replace(&element_stack, &namespace) =>
378            {
379                let _signature = signature_stack_index(
380                    &namespace,
381                    element.local_name().as_ref(),
382                    &mut signature_index,
383                );
384                writer.write_event(Event::Start(element.borrow()))?;
385                writer.get_mut().write_all(content.as_bytes())?;
386                writer.write_event(Event::End(element.to_end()))?;
387            }
388            Event::Start(element) => {
389                let signature = signature_stack_index(
390                    &namespace,
391                    element.local_name().as_ref(),
392                    &mut signature_index,
393                );
394                element_stack.push((
395                    is_dsig_namespace(&namespace),
396                    element.local_name().as_ref().to_vec(),
397                    signature,
398                ));
399                writer.write_event(Event::Start(element))?;
400            }
401            Event::Empty(element) => {
402                let _signature = signature_stack_index(
403                    &namespace,
404                    element.local_name().as_ref(),
405                    &mut signature_index,
406                );
407                writer.write_event(Event::Empty(element))?
408            }
409            Event::End(element) => {
410                element_stack.pop();
411                writer.write_event(Event::End(element))?;
412            }
413            Event::Eof => break,
414            event => writer.write_event(event)?,
415        }
416        buf.clear();
417    }
418
419    let output = String::from_utf8(writer.into_inner())?;
420    roxmltree::Document::parse(&output)?;
421    Ok(output)
422}
423
424fn validate_signature_template(signature_template: &str) -> Result<(), XmlMutationError> {
425    let document = roxmltree::Document::parse(signature_template)?;
426    let root = document.root_element();
427    if root.tag_name().namespace() == Some(XMLDSIG_NS) && root.tag_name().name() == "Signature" {
428        Ok(())
429    } else {
430        Err(XmlMutationError::InvalidSignatureTemplate)
431    }
432}
433
434fn count_dsig_elements(xml: &str, local_name: &str) -> Result<usize, XmlMutationError> {
435    let document = roxmltree::Document::parse(xml)?;
436    Ok(document
437        .descendants()
438        .filter(|node| {
439            node.is_element()
440                && node.tag_name().namespace() == Some(XMLDSIG_NS)
441                && node.tag_name().name() == local_name
442        })
443        .count())
444}
445
446fn count_signed_info_digest_values(xml: &str) -> Result<usize, XmlMutationError> {
447    let document = roxmltree::Document::parse(xml)?;
448    let Some(signature) = last_signature_node(&document) else {
449        return Ok(0);
450    };
451    Ok(document
452        .descendants()
453        .filter(|node| is_direct_signed_info_reference_digest(*node, signature))
454        .count())
455}
456
457fn count_direct_signature_values(xml: &str) -> Result<usize, XmlMutationError> {
458    let document = roxmltree::Document::parse(xml)?;
459    let Some(signature) = last_signature_node(&document) else {
460        return Ok(0);
461    };
462    Ok(document
463        .descendants()
464        .filter(|node| {
465            node.is_element()
466                && node.tag_name().namespace() == Some(XMLDSIG_NS)
467                && node.tag_name().name() == "SignatureValue"
468                && node.parent().is_some_and(|parent| parent == signature)
469        })
470        .count())
471}
472
473fn count_direct_key_infos(xml: &str) -> Result<usize, XmlMutationError> {
474    let document = roxmltree::Document::parse(xml)?;
475    let Some(signature) = last_signature_node(&document) else {
476        return Ok(0);
477    };
478    Ok(document
479        .descendants()
480        .filter(|node| {
481            node.is_element()
482                && node.tag_name().namespace() == Some(XMLDSIG_NS)
483                && node.tag_name().name() == "KeyInfo"
484                && node.parent().is_some_and(|parent| parent == signature)
485        })
486        .count())
487}
488
489fn last_signature_node<'a>(
490    document: &'a roxmltree::Document<'a>,
491) -> Option<roxmltree::Node<'a, 'a>> {
492    document
493        .descendants()
494        .rfind(|node| is_dsig_node(*node, "Signature"))
495}
496
497fn last_signature_index(xml: &str) -> Result<usize, XmlMutationError> {
498    let document = roxmltree::Document::parse(xml)?;
499    document
500        .descendants()
501        .filter(|node| is_dsig_node(*node, "Signature"))
502        .enumerate()
503        .last()
504        .map(|(index, _)| index)
505        .ok_or(XmlMutationError::ValueCountMismatch {
506            element: "Signature",
507            expected: 1,
508            actual: 0,
509        })
510}
511
512fn is_direct_signed_info_reference_digest(
513    node: roxmltree::Node<'_, '_>,
514    signature: roxmltree::Node<'_, '_>,
515) -> bool {
516    node.is_element()
517        && node.tag_name().namespace() == Some(XMLDSIG_NS)
518        && node.tag_name().name() == "DigestValue"
519        && node
520            .parent()
521            .is_some_and(|parent| is_dsig_node(parent, "Reference"))
522        && node
523            .parent()
524            .and_then(|parent| parent.parent())
525            .is_some_and(|grandparent| is_dsig_node(grandparent, "SignedInfo"))
526        && node
527            .parent()
528            .and_then(|parent| parent.parent())
529            .and_then(|grandparent| grandparent.parent())
530            .is_some_and(|parent| parent == signature)
531}
532
533fn is_dsig_node(node: roxmltree::Node<'_, '_>, expected_local: &str) -> bool {
534    node.is_element()
535        && node.tag_name().namespace() == Some(XMLDSIG_NS)
536        && node.tag_name().name() == expected_local
537}
538
539fn is_signed_info_reference_context(
540    element_stack: &[(bool, Vec<u8>, Option<usize>)],
541    namespace: &ResolveResult<'_>,
542    target_signature: usize,
543) -> bool {
544    is_dsig_namespace(namespace)
545        && is_in_target_signature(element_stack, target_signature)
546        && matches!(
547            element_stack,
548            [.., (true, signed_info, _), (true, reference, _)]
549                if signed_info.as_slice() == b"SignedInfo"
550                    && reference.as_slice() == b"Reference"
551        )
552}
553
554fn is_direct_signature_context(
555    element_stack: &[(bool, Vec<u8>, Option<usize>)],
556    namespace: &ResolveResult<'_>,
557    target_signature: usize,
558) -> bool {
559    is_dsig_namespace(namespace)
560        && is_in_target_signature(element_stack, target_signature)
561        && matches!(
562            element_stack,
563            [.., (true, signature, Some(index))]
564                if signature.as_slice() == b"Signature" && *index == target_signature
565        )
566}
567
568fn is_in_target_signature(
569    element_stack: &[(bool, Vec<u8>, Option<usize>)],
570    target_signature: usize,
571) -> bool {
572    element_stack
573        .iter()
574        .rev()
575        .any(|(is_dsig, local_name, signature)| {
576            *is_dsig
577                && local_name.as_slice() == b"Signature"
578                && *signature == Some(target_signature)
579        })
580}
581
582fn is_dsig_element(namespace: &ResolveResult<'_>, local: &[u8], expected_local: &str) -> bool {
583    is_dsig_namespace(namespace) && local == expected_local.as_bytes()
584}
585
586fn is_dsig_namespace(namespace: &ResolveResult<'_>) -> bool {
587    matches!(namespace, ResolveResult::Bound(Namespace(ns)) if *ns == XMLDSIG_NS.as_bytes())
588}
589
590fn signature_stack_index(
591    namespace: &ResolveResult<'_>,
592    local_name: &[u8],
593    next_signature_index: &mut usize,
594) -> Option<usize> {
595    if is_dsig_namespace(namespace) && local_name == b"Signature" {
596        let index = *next_signature_index;
597        *next_signature_index += 1;
598        Some(index)
599    } else {
600        None
601    }
602}
603
604#[cfg(test)]
605mod tests {
606    use crate::c14n::{C14nAlgorithm, C14nMode};
607    use crate::xmldsig::{
608        DigestAlgorithm, ReferenceBuilder, SignatureAlgorithm, SignatureBuilder, Transform,
609    };
610
611    use super::*;
612
613    fn template(reference_count: usize) -> String {
614        let mut builder = SignatureBuilder::new(
615            C14nAlgorithm::new(C14nMode::Exclusive1_0, false),
616            SignatureAlgorithm::RsaSha256,
617        )
618        .ns_prefix("ds");
619        for index in 0..reference_count {
620            builder = builder.add_reference(
621                ReferenceBuilder::new(DigestAlgorithm::Sha256)
622                    .uri(format!("#ref-{index}"))
623                    .transform(Transform::Enveloped),
624            );
625        }
626        builder.build_template().expect("valid template")
627    }
628
629    #[test]
630    fn appends_signature_template_to_non_empty_root() {
631        let signed = append_signature_to_root("<root><payload ID=\"ref-0\"/></root>", &template(1))
632            .expect("append signature");
633        let document = roxmltree::Document::parse(&signed).expect("parse output");
634        let root = document.root_element();
635        let children: Vec<_> = root
636            .children()
637            .filter(roxmltree::Node::is_element)
638            .map(|node| node.tag_name().name())
639            .collect();
640        assert_eq!(children, ["payload", "Signature"]);
641        assert_eq!(
642            root.last_element_child()
643                .expect("signature")
644                .tag_name()
645                .namespace(),
646            Some(XMLDSIG_NS)
647        );
648    }
649
650    #[test]
651    fn appends_signature_template_to_empty_root() {
652        let signed = append_signature_to_root("<root/>", &template(1)).expect("append signature");
653        let document = roxmltree::Document::parse(&signed).expect("parse output");
654        let root = document.root_element();
655        assert_eq!(
656            root.first_element_child()
657                .expect("signature")
658                .tag_name()
659                .name(),
660            "Signature"
661        );
662    }
663
664    #[test]
665    fn rejects_non_signature_template() {
666        let err = append_signature_to_root("<root/>", "<NotSignature/>")
667            .expect_err("template must be a Signature");
668        assert!(matches!(err, XmlMutationError::InvalidSignatureTemplate));
669    }
670
671    #[test]
672    fn fills_digest_values_in_xml_dsig_document_order() {
673        let signed = append_signature_to_root("<root/>", &template(2)).expect("append signature");
674        let filled =
675            fill_digest_values(&signed, ["digest-one", "digest-two"]).expect("fill digest values");
676        let document = roxmltree::Document::parse(&filled).expect("parse output");
677        let values: Vec<_> = document
678            .descendants()
679            .filter(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
680            .map(|node| node.text())
681            .collect();
682        assert_eq!(values, [Some("digest-one"), Some("digest-two")]);
683    }
684
685    #[test]
686    fn fills_signature_value_without_touching_digest_values() {
687        let signed = append_signature_to_root("<root/>", &template(1)).expect("append signature");
688        let filled =
689            fill_signature_values(&signed, ["signature&bytes"]).expect("fill signature value");
690        let document = roxmltree::Document::parse(&filled).expect("parse output");
691        let signature_value = document
692            .descendants()
693            .find(|node| node.has_tag_name((XMLDSIG_NS, "SignatureValue")))
694            .expect("SignatureValue");
695        assert_eq!(signature_value.text(), Some("signature&bytes"));
696        let digest_value = document
697            .descendants()
698            .find(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
699            .expect("DigestValue");
700        assert_eq!(digest_value.text(), None);
701    }
702
703    #[test]
704    fn replacement_count_must_match_dsig_elements() {
705        let signed = append_signature_to_root("<root/>", &template(2)).expect("append signature");
706        let err = fill_digest_values(&signed, ["only-one"]).expect_err("mismatch");
707        assert!(matches!(
708            err,
709            XmlMutationError::ValueCountMismatch {
710                element: "DigestValue",
711                expected: 2,
712                actual: 1
713            }
714        ));
715    }
716
717    #[test]
718    fn does_not_replace_foreign_same_local_name_elements() {
719        let source = r#"<root xmlns:foreign="urn:test"><foreign:DigestValue>keep</foreign:DigestValue></root>"#;
720        let signed = append_signature_to_root(source, &template(1)).expect("append signature");
721        let filled = fill_digest_values(&signed, ["digest"]).expect("fill digest");
722        let document = roxmltree::Document::parse(&filled).expect("parse output");
723        let foreign = document
724            .descendants()
725            .find(|node| node.has_tag_name(("urn:test", "DigestValue")))
726            .expect("foreign DigestValue");
727        assert_eq!(foreign.text(), Some("keep"));
728        let dsig = document
729            .descendants()
730            .find(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
731            .expect("dsig DigestValue");
732        assert_eq!(dsig.text(), Some("digest"));
733    }
734
735    #[test]
736    fn replacement_preserves_target_end_after_self_closing_child() {
737        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>"#;
738        let filled = fill_digest_values(source, ["digest"]).expect("fill digest");
739        let document = roxmltree::Document::parse(&filled).expect("parse output");
740        let digest_value = document
741            .descendants()
742            .find(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
743            .expect("DigestValue");
744        assert_eq!(digest_value.text(), Some("digest"));
745        assert_eq!(
746            digest_value
747                .next_sibling_element()
748                .map(|node| node.tag_name().name()),
749            None
750        );
751    }
752
753    #[test]
754    fn replacement_fails_when_nested_dsig_values_are_skipped() {
755        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>"#;
756        let err =
757            fill_digest_values(source, ["outer", "nested"]).expect_err("nested target skipped");
758        assert!(matches!(
759            err,
760            XmlMutationError::ValueCountMismatch {
761                element: "DigestValue",
762                expected: 2,
763                actual: 1
764            }
765        ));
766    }
767}