1use 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#[derive(Debug, thiserror::Error)]
55pub enum XmlMutationError {
56 #[error("signing policy violation: {0}")]
58 Policy(#[from] crate::policy::PolicyViolation),
59 #[error("XML parsing error: {0}")]
61 XmlParse(#[from] roxmltree::Error),
62 #[error("XML read error: {0}")]
64 Read(#[from] quick_xml::Error),
65 #[error("XML write error: {0}")]
67 Write(#[from] std::io::Error),
68 #[error("XML writer emitted invalid UTF-8: {0}")]
70 InvalidUtf8(#[from] std::string::FromUtf8Error),
71 #[error("signature template root must be one XMLDSig Signature element")]
73 InvalidSignatureTemplate,
74 #[error("expected {expected} XMLDSig {element} values, got {actual}")]
76 ValueCountMismatch {
77 element: &'static str,
79 expected: usize,
81 actual: usize,
83 },
84 #[error("source XML must contain a root element")]
86 MissingRootElement,
87 #[error("selected source element cannot receive a signature")]
89 InvalidAppendTarget,
90 #[error("key-info writer emitted no element child")]
92 EmptyKeyInfoSource,
93 #[error("key-info placeholder conflicts with generated namespace prefix {prefix}")]
95 ConflictingKeyInfoNamespace {
96 prefix: String,
98 },
99 #[error("key-info placeholder conflicts with generated attribute {name}")]
101 ConflictingKeyInfoAttribute {
102 name: String,
104 },
105}
106
107pub 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
226pub 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
235pub 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
375pub 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
384pub 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
518pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}