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