1use std::{collections::HashSet, io::Write, ops::Range};
7
8use quick_xml::events::{BytesText, Event};
9use quick_xml::name::{Namespace, ResolveResult};
10use quick_xml::reader::NsReader;
11use quick_xml::{Reader, Writer};
12
13use super::parse::XMLDSIG_NS;
14use super::whitespace::is_xml_whitespace_only;
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<roxmltree::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<roxmltree::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<roxmltree::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] roxmltree::Error),
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 || is_matching_empty_placeholder(node, &generated_key_material_sources)
591 {
592 return Vec::new();
593 }
594 if generated_x509_data
595 && is_dsig_x509_data(node.tag_name().namespace(), node.tag_name().name())
596 {
597 return node
598 .children()
599 .filter(|child| child.is_element() && is_x509_identity_child(*child))
600 .map(|child| child.range())
601 .collect();
602 }
603 vec![node.range()]
604 })
605 .collect::<Vec<_>>();
606 stale_ranges.sort_by_key(|range| std::cmp::Reverse(range.start));
607 for range in stale_ranges {
608 output.replace_range(range, "");
609 }
610 }
611
612 for (_, _, source) in sources {
613 output = merge_one_key_info_source_at_index_with_options(
614 &output,
615 &source,
616 target_signature,
617 policy,
618 budget,
619 )?;
620 }
621 Ok(output)
622}
623
624fn merge_one_key_info_source_at_index_with_options(
625 xml: &str,
626 key_info_source: &str,
627 target_signature: usize,
628 policy: Option<&crate::policy::SigningPolicy>,
629 budget: Option<&XmlParseWorkBudget>,
630) -> Result<String, XmlMutationError> {
631 let document = parse_mutation_xml_with_budget(xml, policy, budget)?;
632 let source_document = parse_mutation_xml_with_budget(key_info_source, policy, budget)?;
633 let source = source_document.root_element();
634 let source_content = element_inner_xml(key_info_source, source.range())?;
635 let Some(signature) = signature_node(&document, target_signature) else {
636 return Err(XmlMutationError::ValueCountMismatch {
637 element: "Signature",
638 expected: 1,
639 actual: 0,
640 });
641 };
642 let key_infos = signature
643 .children()
644 .filter(|node| is_dsig_node(*node, "KeyInfo"))
645 .collect::<Vec<_>>();
646 if key_infos.len() != 1 {
647 return Err(XmlMutationError::ValueCountMismatch {
648 element: "KeyInfo",
649 expected: 1,
650 actual: key_infos.len(),
651 });
652 }
653 let key_info = key_infos[0];
654
655 let source_is_x509_data =
656 is_dsig_x509_data(source.tag_name().namespace(), source.tag_name().name());
657 if let Some(placeholder) = key_info.children().find(|node| {
658 node.is_element()
659 && node.tag_name() == source.tag_name()
660 && (is_reusable_placeholder(*node)
661 || (source_is_x509_data && has_x509_mergeable_metadata(*node)))
662 }) {
663 let placeholder_fragment = &xml[placeholder.range()];
664 let placeholder_opening_end = element_opening_end(placeholder_fragment)
665 .ok_or(XmlMutationError::InvalidAppendTarget)?;
666 let placeholder_owned_namespaces =
667 owned_namespace_declarations(&placeholder_fragment[..placeholder_opening_end - 1])?;
668 let generated_namespace_attributes =
669 source
670 .namespaces()
671 .try_fold(String::new(), |mut attributes, namespace| {
672 let prefix = namespace.name().unwrap_or_default();
673 if placeholder_owned_namespaces.contains(prefix) {
674 let declared = placeholder
675 .namespaces()
676 .find(|declared| declared.name() == namespace.name())
677 .ok_or(XmlMutationError::InvalidAppendTarget)?;
678 if declared.uri() != namespace.uri() {
679 return Err(XmlMutationError::ConflictingKeyInfoNamespace {
680 prefix: prefix.to_owned(),
681 });
682 }
683 return Ok(attributes);
684 }
685 if placeholder
686 .parent_element()
687 .and_then(|parent| parent.lookup_namespace_uri(namespace.name()))
688 == Some(namespace.uri())
689 {
690 return Ok(attributes);
691 }
692 let attribute = namespace
693 .name()
694 .map_or_else(|| "xmlns".to_owned(), |prefix| format!("xmlns:{prefix}"));
695 attributes.push_str(&format!(
696 " {attribute}=\"{}\"",
697 quick_xml::escape::escape(namespace.uri())
698 ));
699 Ok(attributes)
700 })?;
701 let generated_attributes =
702 source
703 .attributes()
704 .try_fold(String::new(), |mut attributes, attribute| {
705 let existing = placeholder.attributes().find(|candidate| {
706 candidate.namespace() == attribute.namespace()
707 && candidate.name() == attribute.name()
708 });
709 if let Some(existing) = existing {
710 if existing.value() != attribute.value() {
711 return Err(XmlMutationError::ConflictingKeyInfoAttribute {
712 name: attribute.name().to_owned(),
713 });
714 }
715 return Ok(attributes);
716 }
717 let qualified_name = match attribute.namespace() {
718 None => attribute.name().to_owned(),
719 Some("http://www.w3.org/XML/1998/namespace") => {
720 format!("xml:{}", attribute.name())
721 }
722 Some(namespace) => {
723 let prefix = source
724 .lookup_prefix(namespace)
725 .ok_or(XmlMutationError::InvalidAppendTarget)?;
726 format!("{prefix}:{}", attribute.name())
727 }
728 };
729 attributes.push_str(&format!(
730 " {qualified_name}=\"{}\"",
731 quick_xml::escape::escape(attribute.value())
732 ));
733 Ok(attributes)
734 })?;
735 let generated_attributes =
736 format!("{generated_namespace_attributes}{generated_attributes}");
737 let output = if is_reusable_placeholder(placeholder) {
738 replace_element_content(
739 xml,
740 placeholder.range(),
741 source_content,
742 &generated_attributes,
743 policy,
744 )?
745 } else {
746 append_element_content(
747 xml,
748 placeholder.range(),
749 source_content,
750 &generated_attributes,
751 policy,
752 )?
753 };
754 parse_mutation_xml_with_budget(&output, policy, budget)?;
755 return Ok(output);
756 }
757
758 let range = key_info.range();
759 let raw_key_info = &xml[range.clone()];
760 let output = if raw_key_info.trim_end().ends_with("/>") {
761 let name_end = raw_key_info[1..]
762 .find(|character: char| {
763 character.is_ascii_whitespace() || character == '/' || character == '>'
764 })
765 .map(|offset| offset + 1)
766 .ok_or(XmlMutationError::InvalidAppendTarget)?;
767 let qualified_name = &raw_key_info[1..name_end];
768 let empty_end = raw_key_info
769 .rfind("/>")
770 .ok_or(XmlMutationError::InvalidAppendTarget)?;
771 let expanded_len = empty_end
772 .checked_add(1)
773 .and_then(|length| length.checked_add(key_info_source.len()))
774 .and_then(|length| length.checked_add(2))
775 .and_then(|length| length.checked_add(qualified_name.len()))
776 .and_then(|length| length.checked_add(1))
777 .ok_or_else(|| projected_xml_length_overflow(policy))?;
778 validate_projected_replacement_len(xml, range.len(), expanded_len, policy)?;
779 let expanded = format!(
780 "{}>{}</{}>",
781 &raw_key_info[..empty_end],
782 key_info_source,
783 qualified_name
784 );
785 let mut output = xml.to_owned();
786 output.replace_range(range, &expanded);
787 output
788 } else {
789 let closing = raw_key_info
790 .rfind("</")
791 .map(|offset| range.start + offset)
792 .ok_or(XmlMutationError::InvalidAppendTarget)?;
793 validate_projected_replacement_len(xml, 0, key_info_source.len(), policy)?;
794 let mut output = xml.to_owned();
795 output.insert_str(closing, key_info_source);
796 output
797 };
798 parse_mutation_xml_with_budget(&output, policy, budget)?;
799 Ok(output)
800}
801
802fn wrap_key_info_children(
803 source: &str,
804 key_info: roxmltree::Node<'_, '_>,
805 policy: Option<&crate::policy::SigningPolicy>,
806) -> Result<String, XmlMutationError> {
807 const OPEN: &str = "<KeyInfoFragment";
808 const CLOSE: &str = "</KeyInfoFragment>";
809 let projected = key_info
810 .namespaces()
811 .try_fold(OPEN.len(), |length, namespace| {
812 let declaration_len = match namespace.name() {
813 Some(prefix) => "xmlns:"
814 .len()
815 .checked_add(prefix.len())
816 .ok_or_else(|| projected_xml_length_overflow(policy))?,
817 None => "xmlns".len(),
818 };
819 let escaped_uri = quick_xml::escape::escape(namespace.uri());
820 length
821 .checked_add(4)
822 .and_then(|length| length.checked_add(declaration_len))
823 .and_then(|length| length.checked_add(escaped_uri.len()))
824 .ok_or_else(|| projected_xml_length_overflow(policy))
825 })?;
826 let projected = projected
827 .checked_add(1)
828 .and_then(|length| length.checked_add(source.len()))
829 .and_then(|length| length.checked_add(CLOSE.len()))
830 .ok_or_else(|| projected_xml_length_overflow(policy))?;
831 if let Some(policy) = policy {
832 policy.resources.validate_xml_document_len(projected)?;
833 }
834
835 let mut wrapper = String::with_capacity(projected);
836 wrapper.push_str(OPEN);
837 for namespace in key_info.namespaces() {
838 let declaration = namespace
839 .name()
840 .map_or_else(|| "xmlns".to_owned(), |prefix| format!("xmlns:{prefix}"));
841 wrapper.push_str(&format!(
842 " {declaration}=\"{}\"",
843 quick_xml::escape::escape(namespace.uri())
844 ));
845 }
846 wrapper.push('>');
847 wrapper.push_str(source);
848 wrapper.push_str(CLOSE);
849 Ok(wrapper)
850}
851
852fn standalone_element(
853 source: &str,
854 node: roxmltree::Node<'_, '_>,
855) -> Result<String, XmlMutationError> {
856 let fragment = &source[node.range()];
857 let opening_end = element_opening_end(fragment).ok_or(XmlMutationError::InvalidAppendTarget)?;
858 let opening = &fragment[..opening_end - 1];
859 let namespace_insertion = opening.strip_suffix('/').map_or(opening.len(), str::len);
860 let mut output = opening[..namespace_insertion].to_owned();
861 let owned_namespaces = owned_namespace_declarations(opening)?;
862 for namespace in node.namespaces() {
863 let declaration = namespace
864 .name()
865 .map_or_else(|| "xmlns".to_owned(), |prefix| format!("xmlns:{prefix}"));
866 if !owned_namespaces.contains(namespace.name().unwrap_or_default()) {
867 output.push_str(&format!(
868 " {declaration}=\"{}\"",
869 quick_xml::escape::escape(namespace.uri())
870 ));
871 }
872 }
873 output.push_str(&opening[namespace_insertion..]);
874 output.push_str(&fragment[opening_end - 1..]);
875 Ok(output)
876}
877
878fn owned_namespace_declarations(opening: &str) -> Result<HashSet<String>, XmlMutationError> {
879 let standalone = format!("{} />", opening.trim_end_matches('/'));
880 let mut reader = Reader::from_str(&standalone);
881 let event = reader.read_event()?;
882 let element = match event {
883 Event::Start(element) | Event::Empty(element) => element,
884 _ => return Err(XmlMutationError::InvalidAppendTarget),
885 };
886 element
887 .attributes()
888 .map(|attribute| {
889 let attribute = attribute.map_err(|_| XmlMutationError::InvalidAppendTarget)?;
890 let name = std::str::from_utf8(attribute.key.as_ref())
891 .map_err(|_| XmlMutationError::InvalidAppendTarget)?;
892 Ok(match name {
893 "xmlns" => Some(String::new()),
894 _ => name.strip_prefix("xmlns:").map(str::to_owned),
895 })
896 })
897 .filter_map(|result| result.transpose())
898 .collect()
899}
900
901fn is_matching_empty_placeholder(
902 node: roxmltree::Node<'_, '_>,
903 generated_sources: &[(Option<&str>, &str)],
904) -> bool {
905 generated_sources.iter().any(|(namespace, name)| {
906 node.tag_name().namespace() == *namespace
907 && node.tag_name().name() == *name
908 && is_reusable_placeholder(node)
909 })
910}
911
912fn is_reusable_placeholder(node: roxmltree::Node<'_, '_>) -> bool {
913 node.children()
914 .all(|child| child.is_text() && child.text().is_some_and(is_xml_whitespace_only))
915}
916
917fn has_cryptographic_identity_content(node: roxmltree::Node<'_, '_>) -> bool {
918 if is_dsig_x509_data(node.tag_name().namespace(), node.tag_name().name()) {
919 return node
920 .children()
921 .any(|child| child.is_element() && is_x509_identity_child(child));
922 }
923 if node.children().any(|child| child.is_element()) {
924 return true;
925 }
926 match (node.tag_name().namespace(), node.tag_name().name()) {
927 (Some(XMLDSIG_NS), "KeyName") => node
928 .children()
929 .filter_map(|child| child.text())
930 .any(|text| !is_xml_whitespace_only(text)),
931 (Some(XMLDSIG_NS), "RetrievalMethod") => node.attribute("URI").is_some(),
932 (Some("http://www.w3.org/2009/xmldsig11#"), "DEREncodedKeyValue") => node
933 .children()
934 .filter_map(|child| child.text())
935 .any(|text| !is_xml_whitespace_only(text)),
936 _ => false,
937 }
938}
939
940fn is_dsig_x509_data(namespace: Option<&str>, name: &str) -> bool {
941 namespace == Some(XMLDSIG_NS) && name == "X509Data"
942}
943
944fn is_x509_identity_child(node: roxmltree::Node<'_, '_>) -> bool {
945 matches!(
946 (node.tag_name().namespace(), node.tag_name().name()),
947 (
948 Some(XMLDSIG_NS),
949 "X509IssuerSerial" | "X509SKI" | "X509SubjectName" | "X509Certificate"
950 ) | (Some("http://www.w3.org/2009/xmldsig11#"), "X509Digest")
951 )
952}
953
954fn has_x509_mergeable_metadata(node: roxmltree::Node<'_, '_>) -> bool {
955 node.children().any(|child| child.is_element()) && !has_cryptographic_identity_content(node)
956}
957
958fn is_cryptographic_key_info_source(namespace: Option<&str>, name: &str) -> bool {
959 matches!(
960 (namespace, name),
961 (
962 Some(XMLDSIG_NS),
963 "KeyValue" | "RetrievalMethod" | "X509Data" | "PGPData" | "SPKIData"
964 ) | (
965 Some("http://www.w3.org/2009/xmldsig11#"),
966 "DEREncodedKeyValue" | "KeyInfoReference"
967 )
968 )
969}
970
971fn is_dsig_key_name(namespace: Option<&str>, name: &str) -> bool {
972 namespace == Some(XMLDSIG_NS) && name == "KeyName"
973}
974
975fn element_inner_xml(xml: &str, range: Range<usize>) -> Result<&str, XmlMutationError> {
976 let element = &xml[range];
977 if element.trim_end().ends_with("/>") {
978 return Ok("");
979 }
980 let content_start =
981 element_opening_end(element).ok_or(XmlMutationError::InvalidAppendTarget)?;
982 let content_end = element
983 .rfind("</")
984 .ok_or(XmlMutationError::InvalidAppendTarget)?;
985 Ok(&element[content_start..content_end])
986}
987
988fn replace_element_content(
989 xml: &str,
990 range: Range<usize>,
991 content: &str,
992 namespace_attributes: &str,
993 policy: Option<&crate::policy::SigningPolicy>,
994) -> Result<String, XmlMutationError> {
995 let element = &xml[range.clone()];
996 let replacement_len = if element.trim_end().ends_with("/>") {
997 let name_end = element[1..]
998 .find(|character: char| {
999 character.is_ascii_whitespace() || character == '/' || character == '>'
1000 })
1001 .map(|offset| offset + 1)
1002 .ok_or(XmlMutationError::InvalidAppendTarget)?;
1003 let qualified_name = &element[1..name_end];
1004 let empty_end = element
1005 .rfind("/>")
1006 .ok_or(XmlMutationError::InvalidAppendTarget)?;
1007 empty_end
1008 .checked_add(namespace_attributes.len())
1009 .and_then(|length| length.checked_add(1))
1010 .and_then(|length| length.checked_add(content.len()))
1011 .and_then(|length| length.checked_add(2))
1012 .and_then(|length| length.checked_add(qualified_name.len()))
1013 .and_then(|length| length.checked_add(1))
1014 .ok_or_else(|| projected_xml_length_overflow(policy))?
1015 } else {
1016 let content_start =
1017 element_opening_end(element).ok_or(XmlMutationError::InvalidAppendTarget)?;
1018 let content_end = element
1019 .rfind("</")
1020 .ok_or(XmlMutationError::InvalidAppendTarget)?;
1021 (content_start - 1)
1022 .checked_add(namespace_attributes.len())
1023 .and_then(|length| length.checked_add(1))
1024 .and_then(|length| length.checked_add(content.len()))
1025 .and_then(|length| length.checked_add(element.len() - content_end))
1026 .ok_or_else(|| projected_xml_length_overflow(policy))?
1027 };
1028 validate_projected_replacement_len(xml, range.len(), replacement_len, policy)?;
1029
1030 let mut output = xml.to_owned();
1031 if element.trim_end().ends_with("/>") {
1032 let name_end = element[1..]
1033 .find(|character: char| {
1034 character.is_ascii_whitespace() || character == '/' || character == '>'
1035 })
1036 .map(|offset| offset + 1)
1037 .ok_or(XmlMutationError::InvalidAppendTarget)?;
1038 let qualified_name = &element[1..name_end];
1039 let empty_end = element
1040 .rfind("/>")
1041 .ok_or(XmlMutationError::InvalidAppendTarget)?;
1042 output.replace_range(
1043 range,
1044 &format!(
1045 "{}{}>{}</{}>",
1046 &element[..empty_end],
1047 namespace_attributes,
1048 content,
1049 qualified_name
1050 ),
1051 );
1052 } else {
1053 let content_start =
1054 element_opening_end(element).ok_or(XmlMutationError::InvalidAppendTarget)?;
1055 let content_end = element
1056 .rfind("</")
1057 .ok_or(XmlMutationError::InvalidAppendTarget)?;
1058 let replacement = format!(
1059 "{}{}>{}{}",
1060 &element[..content_start - 1],
1061 namespace_attributes,
1062 content,
1063 &element[content_end..]
1064 );
1065 output.replace_range(range, &replacement);
1066 }
1067 Ok(output)
1068}
1069
1070fn append_element_content(
1071 xml: &str,
1072 range: Range<usize>,
1073 content: &str,
1074 namespace_attributes: &str,
1075 policy: Option<&crate::policy::SigningPolicy>,
1076) -> Result<String, XmlMutationError> {
1077 let element = &xml[range.clone()];
1078 let content_start =
1079 element_opening_end(element).ok_or(XmlMutationError::InvalidAppendTarget)?;
1080 let content_end = element
1081 .rfind("</")
1082 .ok_or(XmlMutationError::InvalidAppendTarget)?;
1083 let replacement_len = (content_start - 1)
1084 .checked_add(namespace_attributes.len())
1085 .and_then(|length| length.checked_add(1))
1086 .and_then(|length| length.checked_add(content_end - content_start))
1087 .and_then(|length| length.checked_add(content.len()))
1088 .and_then(|length| length.checked_add(element.len() - content_end))
1089 .ok_or_else(|| projected_xml_length_overflow(policy))?;
1090 validate_projected_replacement_len(xml, range.len(), replacement_len, policy)?;
1091 let replacement = format!(
1092 "{}{}>{}{}{}",
1093 &element[..content_start - 1],
1094 namespace_attributes,
1095 &element[content_start..content_end],
1096 content,
1097 &element[content_end..]
1098 );
1099 let mut output = xml.to_owned();
1100 output.replace_range(range, &replacement);
1101 Ok(output)
1102}
1103
1104fn validate_projected_replacement_len(
1105 xml: &str,
1106 removed_len: usize,
1107 added_len: usize,
1108 policy: Option<&crate::policy::SigningPolicy>,
1109) -> Result<usize, XmlMutationError> {
1110 let projected = xml
1111 .len()
1112 .checked_sub(removed_len)
1113 .and_then(|length| length.checked_add(added_len))
1114 .ok_or_else(|| projected_xml_length_overflow(policy))?;
1115 if let Some(policy) = policy {
1116 policy.resources.validate_xml_document_len(projected)?;
1117 }
1118 Ok(projected)
1119}
1120
1121fn element_opening_end(fragment: &str) -> Option<usize> {
1122 let mut quote = None;
1123 for (offset, character) in fragment.char_indices() {
1124 match (quote, character) {
1125 (None, '\'' | '"') => quote = Some(character),
1126 (Some(delimiter), current) if delimiter == current => quote = None,
1127 (None, '>') => return Some(offset + 1),
1128 _ => {}
1129 }
1130 }
1131 None
1132}
1133
1134fn fill_dsig_values<I, S>(
1135 xml: &str,
1136 local_name: &'static str,
1137 values: I,
1138) -> Result<String, XmlMutationError>
1139where
1140 I: IntoIterator<Item = S>,
1141 S: AsRef<str>,
1142{
1143 let values: Vec<String> = values
1144 .into_iter()
1145 .map(|value| value.as_ref().to_owned())
1146 .collect();
1147 let expected = count_dsig_elements(xml, local_name)?;
1148 if expected != values.len() {
1149 return Err(XmlMutationError::ValueCountMismatch {
1150 element: local_name,
1151 expected,
1152 actual: values.len(),
1153 });
1154 }
1155
1156 fill_dsig_values_matching(xml, local_name, values, None, None, |_, _| true)
1157}
1158
1159fn fill_dsig_values_matching(
1160 xml: &str,
1161 local_name: &'static str,
1162 values: Vec<String>,
1163 policy: Option<&crate::policy::SigningPolicy>,
1164 budget: Option<&XmlParseWorkBudget>,
1165 mut should_replace: impl FnMut(&[(bool, Vec<u8>, Option<usize>)], &ResolveResult<'_>) -> bool,
1166) -> Result<String, XmlMutationError> {
1167 if let Some(budget) = budget {
1168 budget.charge_policy(xml.len())?;
1169 }
1170 let mut reader = NsReader::from_str(xml);
1171 let mut writer = Writer::new(Vec::new());
1172 let mut buf = Vec::new();
1173 let mut value_index = 0usize;
1174 let mut replacing_depth: Option<usize> = None;
1175 let mut element_stack: Vec<(bool, Vec<u8>, Option<usize>)> = Vec::new();
1176 let mut signature_index = 0usize;
1177
1178 loop {
1179 let (namespace, event) = reader.read_resolved_event_into(&mut buf)?;
1180 if let Some(depth) = replacing_depth.as_mut() {
1181 match event {
1182 Event::Start(_) => *depth += 1,
1183 Event::End(end) if *depth == 0 => {
1184 writer.write_event(Event::End(end))?;
1185 replacing_depth = None;
1186 element_stack.pop();
1187 }
1188 Event::End(_) => *depth -= 1,
1189 Event::Eof => break,
1190 _ => {}
1191 }
1192 buf.clear();
1193 continue;
1194 }
1195
1196 match event {
1197 Event::Start(element)
1198 if is_dsig_element(&namespace, element.local_name().as_ref(), local_name)
1199 && should_replace(&element_stack, &namespace) =>
1200 {
1201 let signature = signature_stack_index(
1202 &namespace,
1203 element.local_name().as_ref(),
1204 &mut signature_index,
1205 );
1206 element_stack.push((
1207 is_dsig_namespace(&namespace),
1208 element.local_name().as_ref().to_vec(),
1209 signature,
1210 ));
1211 writer.write_event(Event::Start(element))?;
1212 writer.write_event(Event::Text(BytesText::new(&values[value_index])))?;
1213 value_index += 1;
1214 replacing_depth = Some(0);
1215 }
1216 Event::Empty(element)
1217 if is_dsig_element(&namespace, element.local_name().as_ref(), local_name)
1218 && should_replace(&element_stack, &namespace) =>
1219 {
1220 let _signature = signature_stack_index(
1221 &namespace,
1222 element.local_name().as_ref(),
1223 &mut signature_index,
1224 );
1225 writer.write_event(Event::Start(element.borrow()))?;
1226 writer.write_event(Event::Text(BytesText::new(&values[value_index])))?;
1227 value_index += 1;
1228 writer.write_event(Event::End(element.to_end()))?;
1229 }
1230 Event::Start(element) => {
1231 let signature = signature_stack_index(
1232 &namespace,
1233 element.local_name().as_ref(),
1234 &mut signature_index,
1235 );
1236 element_stack.push((
1237 is_dsig_namespace(&namespace),
1238 element.local_name().as_ref().to_vec(),
1239 signature,
1240 ));
1241 writer.write_event(Event::Start(element))?;
1242 }
1243 Event::Empty(element) => {
1244 let _signature = signature_stack_index(
1245 &namespace,
1246 element.local_name().as_ref(),
1247 &mut signature_index,
1248 );
1249 writer.write_event(Event::Empty(element))?
1250 }
1251 Event::End(element) => {
1252 element_stack.pop();
1253 writer.write_event(Event::End(element))?;
1254 }
1255 Event::Eof => break,
1256 event => writer.write_event(event)?,
1257 }
1258 buf.clear();
1259 }
1260
1261 if value_index != values.len() {
1262 return Err(XmlMutationError::ValueCountMismatch {
1263 element: local_name,
1264 expected: values.len(),
1265 actual: value_index,
1266 });
1267 }
1268
1269 let output = String::from_utf8(writer.into_inner())?;
1270 parse_mutation_xml_with_budget(&output, policy, budget)?;
1271 Ok(output)
1272}
1273
1274fn fill_dsig_element_raw_matching(
1275 xml: &str,
1276 local_name: &'static str,
1277 content: &str,
1278 policy: Option<&crate::policy::SigningPolicy>,
1279 mut should_replace: impl FnMut(&[(bool, Vec<u8>, Option<usize>)], &ResolveResult<'_>) -> bool,
1280) -> Result<String, XmlMutationError> {
1281 let mut reader = NsReader::from_str(xml);
1282 let mut writer = Writer::new(Vec::new());
1283 let mut buf = Vec::new();
1284 let mut replacing_depth: Option<usize> = None;
1285 let mut element_stack: Vec<(bool, Vec<u8>, Option<usize>)> = Vec::new();
1286 let mut signature_index = 0usize;
1287
1288 loop {
1289 let (namespace, event) = reader.read_resolved_event_into(&mut buf)?;
1290 if let Some(depth) = replacing_depth.as_mut() {
1291 match event {
1292 Event::Start(_) => *depth += 1,
1293 Event::End(end) if *depth == 0 => {
1294 writer.write_event(Event::End(end))?;
1295 replacing_depth = None;
1296 element_stack.pop();
1297 }
1298 Event::End(_) => *depth -= 1,
1299 Event::Eof => break,
1300 _ => {}
1301 }
1302 buf.clear();
1303 continue;
1304 }
1305
1306 match event {
1307 Event::Start(element)
1308 if is_dsig_element(&namespace, element.local_name().as_ref(), local_name)
1309 && should_replace(&element_stack, &namespace) =>
1310 {
1311 let signature = signature_stack_index(
1312 &namespace,
1313 element.local_name().as_ref(),
1314 &mut signature_index,
1315 );
1316 element_stack.push((
1317 is_dsig_namespace(&namespace),
1318 element.local_name().as_ref().to_vec(),
1319 signature,
1320 ));
1321 writer.write_event(Event::Start(element))?;
1322 writer.get_mut().write_all(content.as_bytes())?;
1323 replacing_depth = Some(0);
1324 }
1325 Event::Empty(element)
1326 if is_dsig_element(&namespace, element.local_name().as_ref(), local_name)
1327 && should_replace(&element_stack, &namespace) =>
1328 {
1329 let _signature = signature_stack_index(
1330 &namespace,
1331 element.local_name().as_ref(),
1332 &mut signature_index,
1333 );
1334 writer.write_event(Event::Start(element.borrow()))?;
1335 writer.get_mut().write_all(content.as_bytes())?;
1336 writer.write_event(Event::End(element.to_end()))?;
1337 }
1338 Event::Start(element) => {
1339 let signature = signature_stack_index(
1340 &namespace,
1341 element.local_name().as_ref(),
1342 &mut signature_index,
1343 );
1344 element_stack.push((
1345 is_dsig_namespace(&namespace),
1346 element.local_name().as_ref().to_vec(),
1347 signature,
1348 ));
1349 writer.write_event(Event::Start(element))?;
1350 }
1351 Event::Empty(element) => {
1352 let _signature = signature_stack_index(
1353 &namespace,
1354 element.local_name().as_ref(),
1355 &mut signature_index,
1356 );
1357 writer.write_event(Event::Empty(element))?
1358 }
1359 Event::End(element) => {
1360 element_stack.pop();
1361 writer.write_event(Event::End(element))?;
1362 }
1363 Event::Eof => break,
1364 event => writer.write_event(event)?,
1365 }
1366 buf.clear();
1367 }
1368
1369 let output = String::from_utf8(writer.into_inner())?;
1370 parse_mutation_xml_with_options(&output, policy)?;
1371 Ok(output)
1372}
1373
1374fn validate_signature_template(
1375 signature_template: &str,
1376 policy: Option<&crate::policy::SigningPolicy>,
1377) -> Result<(), XmlMutationError> {
1378 let document = parse_mutation_xml_with_options(signature_template, policy)?;
1379 let root = document.root_element();
1380 if root.tag_name().namespace() == Some(XMLDSIG_NS) && root.tag_name().name() == "Signature" {
1381 Ok(())
1382 } else {
1383 Err(XmlMutationError::InvalidSignatureTemplate)
1384 }
1385}
1386
1387fn count_dsig_elements(xml: &str, local_name: &str) -> Result<usize, XmlMutationError> {
1388 let document = parse_mutation_xml_with_options(xml, None)?;
1389 Ok(document
1390 .descendants()
1391 .filter(|node| {
1392 node.is_element()
1393 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1394 && node.tag_name().name() == local_name
1395 })
1396 .count())
1397}
1398
1399fn count_signed_info_digest_values(
1400 xml: &str,
1401 target_signature: usize,
1402 policy: Option<&crate::policy::SigningPolicy>,
1403 budget: Option<&XmlParseWorkBudget>,
1404) -> Result<usize, XmlMutationError> {
1405 let document = parse_mutation_xml_with_budget(xml, policy, budget)?;
1406 let Some(signature) = signature_node(&document, target_signature) else {
1407 return Ok(0);
1408 };
1409 Ok(document
1410 .descendants()
1411 .filter(|node| is_direct_signed_info_reference_digest(*node, signature))
1412 .count())
1413}
1414
1415fn count_direct_signature_values(
1416 xml: &str,
1417 target_signature: usize,
1418 policy: Option<&crate::policy::SigningPolicy>,
1419 budget: Option<&XmlParseWorkBudget>,
1420) -> Result<usize, XmlMutationError> {
1421 let document = parse_mutation_xml_with_budget(xml, policy, budget)?;
1422 let Some(signature) = signature_node(&document, target_signature) else {
1423 return Ok(0);
1424 };
1425 Ok(document
1426 .descendants()
1427 .filter(|node| {
1428 node.is_element()
1429 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1430 && node.tag_name().name() == "SignatureValue"
1431 && node.parent().is_some_and(|parent| parent == signature)
1432 })
1433 .count())
1434}
1435
1436fn count_direct_key_infos(
1437 xml: &str,
1438 target_signature: usize,
1439 policy: Option<&crate::policy::SigningPolicy>,
1440) -> Result<usize, XmlMutationError> {
1441 let document = parse_mutation_xml_with_options(xml, policy)?;
1442 let Some(signature) = signature_node(&document, target_signature) else {
1443 return Ok(0);
1444 };
1445 Ok(document
1446 .descendants()
1447 .filter(|node| {
1448 node.is_element()
1449 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1450 && node.tag_name().name() == "KeyInfo"
1451 && node.parent().is_some_and(|parent| parent == signature)
1452 })
1453 .count())
1454}
1455
1456fn signature_node<'a>(
1457 document: &'a roxmltree::Document<'a>,
1458 target_signature: usize,
1459) -> Option<roxmltree::Node<'a, 'a>> {
1460 document
1461 .descendants()
1462 .filter(|node| is_dsig_node(*node, "Signature"))
1463 .nth(target_signature)
1464}
1465
1466fn last_signature_index(
1467 xml: &str,
1468 policy: Option<&crate::policy::SigningPolicy>,
1469 budget: Option<&XmlParseWorkBudget>,
1470) -> Result<usize, XmlMutationError> {
1471 let document = parse_mutation_xml_with_budget(xml, policy, budget)?;
1472 document
1473 .descendants()
1474 .filter(|node| is_dsig_node(*node, "Signature"))
1475 .enumerate()
1476 .last()
1477 .map(|(index, _)| index)
1478 .ok_or(XmlMutationError::ValueCountMismatch {
1479 element: "Signature",
1480 expected: 1,
1481 actual: 0,
1482 })
1483}
1484
1485fn is_direct_signed_info_reference_digest(
1486 node: roxmltree::Node<'_, '_>,
1487 signature: roxmltree::Node<'_, '_>,
1488) -> bool {
1489 node.is_element()
1490 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1491 && node.tag_name().name() == "DigestValue"
1492 && node
1493 .parent()
1494 .is_some_and(|parent| is_dsig_node(parent, "Reference"))
1495 && node
1496 .parent()
1497 .and_then(|parent| parent.parent())
1498 .is_some_and(|grandparent| is_dsig_node(grandparent, "SignedInfo"))
1499 && node
1500 .parent()
1501 .and_then(|parent| parent.parent())
1502 .and_then(|grandparent| grandparent.parent())
1503 .is_some_and(|parent| parent == signature)
1504}
1505
1506fn is_dsig_node(node: roxmltree::Node<'_, '_>, expected_local: &str) -> bool {
1507 node.is_element()
1508 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1509 && node.tag_name().name() == expected_local
1510}
1511
1512fn is_signed_info_reference_context(
1513 element_stack: &[(bool, Vec<u8>, Option<usize>)],
1514 namespace: &ResolveResult<'_>,
1515 target_signature: usize,
1516) -> bool {
1517 is_dsig_namespace(namespace)
1518 && is_in_target_signature(element_stack, target_signature)
1519 && matches!(
1520 element_stack,
1521 [.., (true, signed_info, _), (true, reference, _)]
1522 if signed_info.as_slice() == b"SignedInfo"
1523 && reference.as_slice() == b"Reference"
1524 )
1525}
1526
1527fn is_direct_signature_context(
1528 element_stack: &[(bool, Vec<u8>, Option<usize>)],
1529 namespace: &ResolveResult<'_>,
1530 target_signature: usize,
1531) -> bool {
1532 is_dsig_namespace(namespace)
1533 && is_in_target_signature(element_stack, target_signature)
1534 && matches!(
1535 element_stack,
1536 [.., (true, signature, Some(index))]
1537 if signature.as_slice() == b"Signature" && *index == target_signature
1538 )
1539}
1540
1541fn is_in_target_signature(
1542 element_stack: &[(bool, Vec<u8>, Option<usize>)],
1543 target_signature: usize,
1544) -> bool {
1545 element_stack
1546 .iter()
1547 .rev()
1548 .find(|(is_dsig, local_name, _)| *is_dsig && local_name.as_slice() == b"Signature")
1549 .is_some_and(|(_, _, signature)| *signature == Some(target_signature))
1550}
1551
1552fn is_dsig_element(namespace: &ResolveResult<'_>, local: &[u8], expected_local: &str) -> bool {
1553 is_dsig_namespace(namespace) && local == expected_local.as_bytes()
1554}
1555
1556fn is_dsig_namespace(namespace: &ResolveResult<'_>) -> bool {
1557 matches!(namespace, ResolveResult::Bound(Namespace(ns)) if *ns == XMLDSIG_NS.as_bytes())
1558}
1559
1560fn signature_stack_index(
1561 namespace: &ResolveResult<'_>,
1562 local_name: &[u8],
1563 next_signature_index: &mut usize,
1564) -> Option<usize> {
1565 if is_dsig_namespace(namespace) && local_name == b"Signature" {
1566 let index = *next_signature_index;
1567 *next_signature_index += 1;
1568 Some(index)
1569 } else {
1570 None
1571 }
1572}
1573
1574#[cfg(test)]
1575mod tests {
1576 use crate::c14n::{C14nAlgorithm, C14nMode};
1577 use crate::xmldsig::{
1578 DigestAlgorithm, ReferenceBuilder, SignatureAlgorithm, SignatureBuilder, Transform,
1579 };
1580
1581 use super::*;
1582
1583 fn template(reference_count: usize) -> String {
1584 let mut builder = SignatureBuilder::new(
1585 C14nAlgorithm::new(C14nMode::Exclusive1_0, false),
1586 SignatureAlgorithm::RsaSha256,
1587 )
1588 .ns_prefix("ds");
1589 for index in 0..reference_count {
1590 builder = builder.add_reference(
1591 ReferenceBuilder::new(DigestAlgorithm::Sha256)
1592 .uri(format!("#ref-{index}"))
1593 .transform(Transform::Enveloped),
1594 );
1595 }
1596 builder.build_template().expect("valid template")
1597 }
1598
1599 #[test]
1600 fn signature_value_projection_matches_streaming_mutation() {
1601 for placeholder in [
1604 "<ds:SignatureValue/>",
1605 "<ds:SignatureValue></ds:SignatureValue>",
1606 ] {
1607 let xml = format!(
1608 "<root><ds:Signature xmlns:ds=\"{XMLDSIG_NS}\"><ds:SignedInfo/>{placeholder}</ds:Signature></root>"
1609 );
1610 let value = "A".repeat(341);
1611 let projected = projected_signature_value_output_len_at_index_with_options(
1612 &xml,
1613 value.len(),
1614 0,
1615 Some(&crate::policy::SigningPolicy::default()),
1616 )
1617 .expect("project SignatureValue output length");
1618 let mutated = fill_signature_value_at_index_with_options(
1619 &xml,
1620 &value,
1621 0,
1622 Some(&crate::policy::SigningPolicy::default()),
1623 )
1624 .expect("fill SignatureValue");
1625
1626 assert_eq!(projected, mutated.len());
1627 }
1628 }
1629
1630 #[test]
1631 fn streaming_mutation_scan_consumes_the_shared_parse_budget() {
1632 let xml = format!(
1635 "<root><ds:Signature xmlns:ds=\"{XMLDSIG_NS}\"><ds:SignatureValue/></ds:Signature></root>"
1636 );
1637 let resources = crate::policy::ResourcePolicy::default();
1638 let budget = XmlParseWorkBudget::from_resources(&resources);
1639 let output = fill_signature_value_at_index_with_budget(
1640 &xml,
1641 "signature",
1642 0,
1643 Some(&crate::policy::SigningPolicy::default()),
1644 Some(&budget),
1645 )
1646 .expect("streaming mutation must succeed");
1647
1648 let dom_passes = 2 + usize::from(cfg!(feature = "xml-backend-xmloxide"));
1649 assert_eq!(
1650 budget.consumed(),
1651 xml.len() * (dom_passes + 1) + output.len() * dom_passes
1652 );
1653 }
1654
1655 #[test]
1656 fn appends_signature_template_to_non_empty_root() {
1657 let signed = append_signature_to_root("<root><payload ID=\"ref-0\"/></root>", &template(1))
1658 .expect("append signature");
1659 let document = roxmltree::Document::parse(&signed).expect("parse output");
1660 let root = document.root_element();
1661 let children: Vec<_> = root
1662 .children()
1663 .filter(roxmltree::Node::is_element)
1664 .map(|node| node.tag_name().name())
1665 .collect();
1666 assert_eq!(children, ["payload", "Signature"]);
1667 assert_eq!(
1668 root.last_element_child()
1669 .expect("signature")
1670 .tag_name()
1671 .namespace(),
1672 Some(XMLDSIG_NS)
1673 );
1674 }
1675
1676 #[test]
1677 fn appends_signature_template_to_empty_root() {
1678 let signed = append_signature_to_root("<root/>", &template(1)).expect("append signature");
1679 let document = roxmltree::Document::parse(&signed).expect("parse output");
1680 let root = document.root_element();
1681 assert_eq!(
1682 root.first_element_child()
1683 .expect("signature")
1684 .tag_name()
1685 .name(),
1686 "Signature"
1687 );
1688 }
1689
1690 #[test]
1691 fn appends_signature_template_to_selected_empty_element() {
1692 let source = r#"<root xmlns:s="urn:scope"><s:scope Id="urn:selected/item"/></root>"#;
1695 let mut document = crate::XmlDocument::parse(source).expect("source must parse");
1696 let registrations = [crate::IdAttributeRegistration::global("Id")];
1697 let scope = document.with_view(|view| {
1698 view.node_for_id("urn:selected/item", ®istrations)
1699 .expect("selected scope")
1700 });
1701 document
1702 .append_child(scope, &template(1))
1703 .expect("selected empty element must accept a signature");
1704 let signed = document.into_xml();
1705 let output = roxmltree::Document::parse(&signed).expect("output must parse");
1706 let scope = output
1707 .descendants()
1708 .find(|node| node.has_tag_name(("urn:scope", "scope")))
1709 .expect("qualified scope must remain");
1710
1711 assert!(
1712 scope
1713 .children()
1714 .any(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
1715 );
1716 assert_eq!(scope.attribute("Id"), Some("urn:selected/item"));
1717 }
1718
1719 #[test]
1720 fn rejects_non_signature_template() {
1721 let err = append_signature_to_root("<root/>", "<NotSignature/>")
1722 .expect_err("template must be a Signature");
1723 assert!(matches!(err, XmlMutationError::InvalidSignatureTemplate));
1724 }
1725
1726 #[test]
1727 fn signature_template_validation_applies_the_active_policy_first() {
1728 let template = format!(
1731 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo>{}</ds:SignedInfo><ds:SignatureValue/></ds:Signature>"#,
1732 "<part/>".repeat(16),
1733 );
1734
1735 let byte_policy = crate::policy::SigningPolicy {
1736 resources: crate::policy::ResourcePolicy {
1737 max_xml_document_bytes: template.len() - 1,
1738 ..crate::policy::ResourcePolicy::default()
1739 },
1740 ..crate::policy::SigningPolicy::default()
1741 };
1742 let byte_error =
1743 append_signature_to_root_with_options("not XML", &template, Some(&byte_policy))
1744 .expect_err("template byte policy must win before source parsing");
1745 assert!(matches!(
1746 byte_error,
1747 XmlMutationError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1748 resource: crate::policy::resource_name::XML_DOCUMENT,
1749 maximum,
1750 actual,
1751 }) if maximum == template.len() - 1 && actual == template.len()
1752 ));
1753
1754 let node_policy = crate::policy::SigningPolicy {
1755 resources: crate::policy::ResourcePolicy {
1756 max_xml_nodes: 2,
1757 ..crate::policy::ResourcePolicy::default()
1758 },
1759 ..crate::policy::SigningPolicy::default()
1760 };
1761 let node_error =
1762 append_signature_to_root_with_options("not XML", &template, Some(&node_policy))
1763 .expect_err("template node policy must win before source parsing");
1764 assert!(matches!(
1765 node_error,
1766 XmlMutationError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1767 resource: crate::policy::resource_name::XML_NODES,
1768 maximum: 2,
1769 actual: 3,
1770 })
1771 ));
1772 }
1773
1774 #[test]
1775 fn fills_digest_values_in_xml_dsig_document_order() {
1776 let signed = append_signature_to_root("<root/>", &template(2)).expect("append signature");
1777 let filled =
1778 fill_digest_values(&signed, ["digest-one", "digest-two"]).expect("fill digest values");
1779 let document = roxmltree::Document::parse(&filled).expect("parse output");
1780 let values: Vec<_> = document
1781 .descendants()
1782 .filter(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
1783 .map(|node| node.text())
1784 .collect();
1785 assert_eq!(values, [Some("digest-one"), Some("digest-two")]);
1786 }
1787
1788 #[test]
1789 fn fills_signature_value_without_touching_digest_values() {
1790 let signed = append_signature_to_root("<root/>", &template(1)).expect("append signature");
1791 let filled =
1792 fill_signature_values(&signed, ["signature&bytes"]).expect("fill signature value");
1793 let document = roxmltree::Document::parse(&filled).expect("parse output");
1794 let signature_value = document
1795 .descendants()
1796 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignatureValue")))
1797 .expect("SignatureValue");
1798 assert_eq!(signature_value.text(), Some("signature&bytes"));
1799 let digest_value = document
1800 .descendants()
1801 .find(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
1802 .expect("DigestValue");
1803 assert_eq!(digest_value.text(), None);
1804 }
1805
1806 #[test]
1807 fn replacement_count_must_match_dsig_elements() {
1808 let signed = append_signature_to_root("<root/>", &template(2)).expect("append signature");
1809 let err = fill_digest_values(&signed, ["only-one"]).expect_err("mismatch");
1810 assert!(matches!(
1811 err,
1812 XmlMutationError::ValueCountMismatch {
1813 element: "DigestValue",
1814 expected: 2,
1815 actual: 1
1816 }
1817 ));
1818 }
1819
1820 #[test]
1821 fn does_not_replace_foreign_same_local_name_elements() {
1822 let source = r#"<root xmlns:foreign="urn:test"><foreign:DigestValue>keep</foreign:DigestValue></root>"#;
1823 let signed = append_signature_to_root(source, &template(1)).expect("append signature");
1824 let filled = fill_digest_values(&signed, ["digest"]).expect("fill digest");
1825 let document = roxmltree::Document::parse(&filled).expect("parse output");
1826 let foreign = document
1827 .descendants()
1828 .find(|node| node.has_tag_name(("urn:test", "DigestValue")))
1829 .expect("foreign DigestValue");
1830 assert_eq!(foreign.text(), Some("keep"));
1831 let dsig = document
1832 .descendants()
1833 .find(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
1834 .expect("dsig DigestValue");
1835 assert_eq!(dsig.text(), Some("digest"));
1836 }
1837
1838 #[test]
1839 fn replacement_preserves_target_end_after_self_closing_child() {
1840 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>"#;
1841 let filled = fill_digest_values(source, ["digest"]).expect("fill digest");
1842 let document = roxmltree::Document::parse(&filled).expect("parse output");
1843 let digest_value = document
1844 .descendants()
1845 .find(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
1846 .expect("DigestValue");
1847 assert_eq!(digest_value.text(), Some("digest"));
1848 assert_eq!(
1849 digest_value
1850 .next_sibling_element()
1851 .map(|node| node.tag_name().name()),
1852 None
1853 );
1854 }
1855
1856 #[test]
1857 fn replacement_fails_when_nested_dsig_values_are_skipped() {
1858 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>"#;
1859 let err =
1860 fill_digest_values(source, ["outer", "nested"]).expect_err("nested target skipped");
1861 assert!(matches!(
1862 err,
1863 XmlMutationError::ValueCountMismatch {
1864 element: "DigestValue",
1865 expected: 2,
1866 actual: 1
1867 }
1868 ));
1869 }
1870
1871 #[test]
1872 fn indexed_digest_replacement_ignores_nested_signatures() {
1873 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>"#;
1876 let filled =
1877 fill_signed_info_digest_values_at_index_with_options(source, ["outer-new"], 0, None)
1878 .expect("outer signature replacement must ignore nested signatures");
1879 let document = roxmltree::Document::parse(&filled).expect("filled XML must parse");
1880 let values = document
1881 .descendants()
1882 .filter(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue")))
1883 .filter_map(|node| node.text())
1884 .collect::<Vec<_>>();
1885
1886 assert_eq!(values, ["outer-new", "inner-keep"]);
1887 }
1888
1889 #[test]
1890 fn key_info_source_merge_preserves_placeholder_attributes() {
1891 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>"#;
1894 let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:example:key-info"><X509Certificate>Y2VydA==</X509Certificate><ext:Metadata/></X509Data>"#;
1895
1896 let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1897 .expect("matching source must populate the placeholder");
1898 let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
1899 let x509_data = document
1900 .descendants()
1901 .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
1902 .expect("X509Data");
1903
1904 assert_eq!(x509_data.attribute("Id"), Some("key-info"));
1905 assert_eq!(
1906 x509_data
1907 .children()
1908 .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Certificate")))
1909 .and_then(|node| node.text()),
1910 Some("Y2VydA==")
1911 );
1912 assert!(
1913 x509_data
1914 .children()
1915 .any(|node| node.has_tag_name(("urn:example:key-info", "Metadata")))
1916 );
1917 }
1918
1919 #[test]
1920 fn key_info_source_merge_preserves_comment_and_processing_instruction() {
1921 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>"#;
1924 let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#"><X509Certificate>Y2VydA==</X509Certificate></X509Data>"#;
1925
1926 let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1927 .expect("generated identity must be appended without erasing caller content");
1928
1929 assert!(merged.contains("<!--keep-->"));
1930 assert!(merged.contains("<?audit preserve?>"));
1931 let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
1932 let x509_sources = document
1933 .descendants()
1934 .filter(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
1935 .collect::<Vec<_>>();
1936 assert_eq!(x509_sources.len(), 2);
1937 assert!(x509_sources.iter().any(|source| {
1938 source
1939 .children()
1940 .any(|node| node.has_tag_name((XMLDSIG_NS, "X509Certificate")))
1941 }));
1942 }
1943
1944 #[test]
1945 fn key_info_source_merge_preserves_non_xml_whitespace_text() {
1946 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>";
1950 let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#"><X509Certificate>Y2VydA==</X509Certificate></X509Data>"#;
1951
1952 let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1953 .expect("generated identity must not replace non-whitespace character data");
1954 let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
1955 let x509_sources = document
1956 .descendants()
1957 .filter(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
1958 .collect::<Vec<_>>();
1959
1960 assert_eq!(x509_sources.len(), 2);
1961 assert!(
1962 x509_sources
1963 .iter()
1964 .any(|source| source.text() == Some("\u{00a0}"))
1965 );
1966 }
1967
1968 #[test]
1969 fn key_info_source_merge_replaces_populated_key_name_identity() {
1970 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>"#;
1973 let generated =
1974 r#"<ds:KeyName xmlns:ds="http://www.w3.org/2000/09/xmldsig#">generated</ds:KeyName>"#;
1975
1976 let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1977 .expect("generated KeyName must replace stale template identity");
1978 let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
1979 let key_names = document
1980 .descendants()
1981 .filter(|node| node.has_tag_name((XMLDSIG_NS, "KeyName")))
1982 .filter_map(|node| node.text())
1983 .collect::<Vec<_>>();
1984
1985 assert_eq!(key_names, ["generated"]);
1986 }
1987
1988 #[test]
1989 fn key_info_source_merge_preserves_x509_revocation_metadata() {
1990 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>"#;
1993 let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#"><X509Certificate>Z2VuZXJhdGVk</X509Certificate></X509Data>"#;
1994
1995 let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
1996 .expect("generated identity must preserve revocation metadata");
1997 let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
1998 let x509_sources = document
1999 .descendants()
2000 .filter(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
2001 .collect::<Vec<_>>();
2002
2003 assert_eq!(x509_sources.len(), 1);
2004 let x509_data = x509_sources[0];
2005 assert_eq!(x509_data.attribute("Id"), Some("caller"));
2006 assert_eq!(
2007 x509_data
2008 .children()
2009 .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Certificate")))
2010 .and_then(|node| node.text()),
2011 Some("Z2VuZXJhdGVk")
2012 );
2013 assert!(!merged.contains("c3RhbGU="));
2014 assert!(!merged.contains("CN=stale"));
2015 assert_eq!(
2016 x509_data
2017 .children()
2018 .find(|node| node.has_tag_name((XMLDSIG_NS, "X509CRL")))
2019 .and_then(|node| node.text()),
2020 Some("Y3Js")
2021 );
2022 assert_eq!(
2023 x509_data
2024 .children()
2025 .find(|node| node.has_tag_name(("urn:example:x509", "Policy")))
2026 .and_then(|node| node.text()),
2027 Some("keep")
2028 );
2029 }
2030
2031 #[test]
2032 fn key_info_source_merge_reports_required_and_observed_counts() {
2033 for (source, actual) in [
2036 (
2037 r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/>"#,
2038 0,
2039 ),
2040 (
2041 r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo/><ds:KeyInfo/></ds:Signature>"#,
2042 2,
2043 ),
2044 ] {
2045 let error = merge_key_info_source_at_index_with_options(
2046 source,
2047 r#"<ds:KeyName xmlns:ds="http://www.w3.org/2000/09/xmldsig#">key</ds:KeyName>"#,
2048 0,
2049 None,
2050 )
2051 .expect_err("KeyInfo must be a singleton");
2052 assert!(matches!(
2053 error,
2054 XmlMutationError::ValueCountMismatch {
2055 element: "KeyInfo",
2056 expected: 1,
2057 actual: observed,
2058 } if observed == actual
2059 ));
2060 }
2061 }
2062
2063 #[test]
2064 fn key_info_source_merge_rejects_conflicting_placeholder_namespaces() {
2065 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>"#;
2068 let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:writer"><ext:Metadata/></X509Data>"#;
2069
2070 let error = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2071 .expect_err("conflicting namespace bindings must fail before serialization");
2072
2073 assert!(matches!(
2074 error,
2075 XmlMutationError::ConflictingKeyInfoNamespace { prefix } if prefix == "ext"
2076 ));
2077 }
2078
2079 #[test]
2080 fn key_info_source_merge_allows_shadowing_inherited_namespaces() {
2081 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>"#;
2084 let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:generated"><ext:Metadata/></X509Data>"#;
2085
2086 let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2087 .expect("generated source may shadow an inherited namespace");
2088 let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
2089 let x509_data = document
2090 .descendants()
2091 .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
2092 .expect("X509Data");
2093
2094 assert_eq!(
2095 x509_data.lookup_namespace_uri(Some("ext")),
2096 Some("urn:generated")
2097 );
2098 assert!(
2099 x509_data
2100 .children()
2101 .any(|node| node.has_tag_name(("urn:generated", "Metadata")))
2102 );
2103 }
2104
2105 #[test]
2106 fn key_info_source_merge_detects_redundant_owned_namespace_conflicts() {
2107 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>"#;
2110 let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:writer"><ext:Metadata/></X509Data>"#;
2111
2112 let error = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2113 .expect_err("placeholder-owned namespace conflicts must be typed");
2114
2115 assert!(matches!(
2116 error,
2117 XmlMutationError::ConflictingKeyInfoNamespace { prefix } if prefix == "ext"
2118 ));
2119 }
2120
2121 #[test]
2122 fn key_info_source_merge_preserves_generated_attributes() {
2123 let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo><ds:X509Data/></ds:KeyInfo></ds:Signature>"#;
2126 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>"#;
2127
2128 let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2129 .expect("generated attributes must populate the placeholder");
2130 let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
2131 let x509_data = document
2132 .descendants()
2133 .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Data")))
2134 .expect("X509Data");
2135
2136 assert_eq!(x509_data.attribute("Id"), Some("generated"));
2137 assert_eq!(
2138 x509_data.attribute(("urn:key-info", "role")),
2139 Some("signing")
2140 );
2141 }
2142
2143 #[test]
2144 fn key_info_source_merge_accepts_whitespace_around_namespace_equals() {
2145 let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo/></ds:Signature>"#;
2148 let generated = r#"<ext:Metadata xmlns:ext = "urn:key-info">value</ext:Metadata>"#;
2149
2150 let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2151 .expect("valid namespace declaration whitespace must be preserved");
2152 let document = roxmltree::Document::parse(&merged).expect("merged XML must parse");
2153 assert!(
2154 document
2155 .descendants()
2156 .any(|node| node.has_tag_name(("urn:key-info", "Metadata")))
2157 );
2158 }
2159
2160 #[test]
2161 fn key_info_source_merge_rejects_conflicting_generated_attributes() {
2162 let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo><ds:X509Data Id="template"/></ds:KeyInfo></ds:Signature>"#;
2165 let generated = r#"<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#" Id="generated"><X509Certificate>Y2VydA==</X509Certificate></X509Data>"#;
2166
2167 let error = merge_key_info_source_at_index_with_options(source, generated, 0, None)
2168 .expect_err("conflicting attributes must fail before serialization");
2169
2170 assert!(matches!(
2171 error,
2172 XmlMutationError::ConflictingKeyInfoAttribute { name } if name == "Id"
2173 ));
2174 }
2175
2176 #[test]
2177 fn key_info_source_merge_rejects_empty_writer_output() {
2178 let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo/></ds:Signature>"#;
2181
2182 let error = merge_key_info_source_at_index_with_options(source, " ", 0, None)
2183 .expect_err("a key-info writer must emit an element child");
2184
2185 assert!(matches!(error, XmlMutationError::EmptyKeyInfoSource));
2186 }
2187
2188 #[test]
2189 fn key_info_source_merge_applies_policy_to_writer_fragments() {
2190 let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo/></ds:Signature>"#;
2193 let children = (0..64).map(|_| "<part/>").collect::<String>();
2194 let generated = format!(
2195 r#"<ds:KeyName xmlns:ds="http://www.w3.org/2000/09/xmldsig#">{children}</ds:KeyName>"#
2196 );
2197 let policy = crate::policy::SigningPolicy {
2198 resources: crate::policy::ResourcePolicy {
2199 max_xml_nodes: 32,
2200 ..crate::policy::ResourcePolicy::default()
2201 },
2202 ..crate::policy::SigningPolicy::default()
2203 };
2204
2205 let error =
2206 merge_key_info_source_at_index_with_options(source, &generated, 0, Some(&policy))
2207 .expect_err("writer fragment must obey the signing node ceiling");
2208
2209 assert!(matches!(
2210 error,
2211 XmlMutationError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2212 resource: crate::policy::resource_name::XML_NODES,
2213 maximum: 32,
2214 actual: 33,
2215 })
2216 ));
2217 }
2218
2219 #[test]
2220 fn key_info_source_merge_bounds_synthesized_wrapper_before_parsing() {
2221 let source = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:inherited"><ds:KeyInfo/></ds:Signature>"#;
2225 let generated =
2226 r#"<ds:KeyName xmlns:ds="http://www.w3.org/2000/09/xmldsig#">recipient</ds:KeyName>"#;
2227 let document = roxmltree::Document::parse(source).expect("source must parse");
2228 let key_info = document
2229 .descendants()
2230 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
2231 .expect("KeyInfo");
2232 let wrapped =
2233 wrap_key_info_children(generated, key_info, None).expect("wrapper must serialize");
2234 let maximum = wrapped.len() - 1;
2235 assert!(source.len() <= maximum);
2236 assert!(generated.len() <= maximum);
2237 let policy = crate::policy::SigningPolicy {
2238 resources: crate::policy::ResourcePolicy {
2239 max_xml_document_bytes: maximum,
2240 ..crate::policy::ResourcePolicy::default()
2241 },
2242 ..crate::policy::SigningPolicy::default()
2243 };
2244
2245 let error =
2246 merge_key_info_source_at_index_with_options(source, generated, 0, Some(&policy))
2247 .expect_err("synthesized wrapper must be bounded before parsing");
2248
2249 assert!(matches!(
2250 error,
2251 XmlMutationError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2252 resource: crate::policy::resource_name::XML_DOCUMENT,
2253 maximum: observed_maximum,
2254 actual,
2255 }) if observed_maximum == maximum && actual == wrapped.len()
2256 ));
2257 }
2258}