1use 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#[derive(Debug, thiserror::Error)]
17pub enum XmlMutationError {
18 #[error("XML parsing error: {0}")]
20 XmlParse(#[from] roxmltree::Error),
21 #[error("XML read error: {0}")]
23 Read(#[from] quick_xml::Error),
24 #[error("XML write error: {0}")]
26 Write(#[from] std::io::Error),
27 #[error("XML writer emitted invalid UTF-8: {0}")]
29 InvalidUtf8(#[from] std::string::FromUtf8Error),
30 #[error("signature template root must be one XMLDSig Signature element")]
32 InvalidSignatureTemplate,
33 #[error("expected {expected} XMLDSig {element} values, got {actual}")]
35 ValueCountMismatch {
36 element: &'static str,
38 expected: usize,
40 actual: usize,
42 },
43 #[error("source XML must contain a root element")]
45 MissingRootElement,
46}
47
48pub 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
107pub 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
116pub 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
144pub 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
153pub 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
173pub 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}