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