1use base64::Engine;
14use roxmltree::{Document, Node, NodeId};
15use std::collections::HashSet;
16
17use crate::c14n::canonicalize;
18
19use super::digest::{DigestAlgorithm, compute_digest, constant_time_eq};
20use super::parse::{
21 KeyInfo, MAX_REFERENCES_PER_SIGNATURE, ParseError, Reference, SignatureAlgorithm, XMLDSIG_NS,
22};
23use super::parse::{
24 parse_key_info, parse_reference_with_xpath_budget, parse_signed_info_with_xpath_budget,
25};
26use super::signature::{
27 SignatureVerificationError, verify_ecdsa_signature_pem, verify_rsa_signature_pem,
28};
29use super::transforms::{
30 BASE64_TRANSFORM_URI, DEFAULT_IMPLICIT_C14N_URI, Transform, TransformExecutionBudget,
31 TransformOptions, XPATH_TRANSFORM_URI, XPathHereSemantics, XPathSignatureParseBudget,
32 execute_transforms_with_options_and_budget,
33};
34use super::uri::{UriReferenceResolver, parse_xpointer_id_fragment};
35use super::whitespace::{is_xml_whitespace_only, normalize_xml_base64_bytes};
36
37const MAX_SIGNATURE_VALUE_LEN: usize = 8192;
38const MAX_SIGNATURE_VALUE_TEXT_LEN: usize = 65_536;
39pub trait VerifyingKey {
44 fn verify(
46 &self,
47 algorithm: SignatureAlgorithm,
48 signed_data: &[u8],
49 signature_value: &[u8],
50 ) -> Result<bool, DsigError>;
51}
52
53pub trait KeyResolver {
58 fn resolve<'a>(
65 &'a self,
66 key_info: Option<&KeyInfo>,
67 algorithm: SignatureAlgorithm,
68 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError>;
69
70 fn consumes_document_key_info(&self) -> bool {
77 false
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87#[must_use = "pass the policy to VerifyContext::allowed_uri_types(), or store it for reuse"]
88pub struct UriTypeSet {
89 allow_empty: bool,
90 allow_same_document: bool,
91 allow_external: bool,
92}
93
94impl UriTypeSet {
95 pub const fn new(allow_empty: bool, allow_same_document: bool, allow_external: bool) -> Self {
97 Self {
98 allow_empty,
99 allow_same_document,
100 allow_external,
101 }
102 }
103
104 pub const SAME_DOCUMENT: Self = Self {
106 allow_empty: true,
107 allow_same_document: true,
108 allow_external: false,
109 };
110
111 pub const ALL: Self = Self {
116 allow_empty: true,
117 allow_same_document: true,
118 allow_external: true,
119 };
120
121 fn allows(self, uri: &str) -> bool {
122 if uri.is_empty() {
123 return self.allow_empty;
124 }
125 if uri.starts_with('#') {
126 return self.allow_same_document;
127 }
128 self.allow_external
129 }
130}
131
132impl Default for UriTypeSet {
133 fn default() -> Self {
134 Self::SAME_DOCUMENT
135 }
136}
137
138#[must_use = "configure the context and call verify(), or store it for reuse"]
140pub struct VerifyContext<'a> {
141 key: Option<&'a dyn VerifyingKey>,
142 key_resolver: Option<&'a dyn KeyResolver>,
143 process_manifests: bool,
144 allowed_uri_types: UriTypeSet,
145 allowed_transforms: Option<HashSet<String>>,
146 store_pre_digest: bool,
147 transform_options: TransformOptions,
148}
149
150impl<'a> VerifyContext<'a> {
151 pub fn new() -> Self {
160 Self {
161 key: None,
162 key_resolver: None,
163 process_manifests: false,
164 allowed_uri_types: UriTypeSet::default(),
165 allowed_transforms: None,
166 store_pre_digest: false,
167 transform_options: TransformOptions::default(),
168 }
169 }
170
171 pub fn key(mut self, key: &'a dyn VerifyingKey) -> Self {
173 self.key = Some(key);
174 self
175 }
176
177 pub fn key_resolver(mut self, resolver: &'a dyn KeyResolver) -> Self {
179 self.key_resolver = Some(resolver);
180 self
181 }
182
183 pub fn process_manifests(mut self, enabled: bool) -> Self {
214 self.process_manifests = enabled;
215 self
216 }
217
218 pub fn allowed_uri_types(mut self, types: UriTypeSet) -> Self {
220 self.allowed_uri_types = types;
221 self
222 }
223
224 pub fn allowed_transforms<I, S>(mut self, transforms: I) -> Self
235 where
236 I: IntoIterator<Item = S>,
237 S: Into<String>,
238 {
239 self.allowed_transforms = Some(transforms.into_iter().map(Into::into).collect());
240 self
241 }
242
243 pub fn store_pre_digest(mut self, enabled: bool) -> Self {
245 self.store_pre_digest = enabled;
246 self
247 }
248
249 pub fn xpath_here_semantics(mut self, semantics: XPathHereSemantics) -> Self {
255 self.transform_options = self.transform_options.xpath_here_semantics(semantics);
256 self
257 }
258
259 fn allowed_transform_uris(&self) -> Option<&HashSet<String>> {
260 self.allowed_transforms.as_ref()
261 }
262
263 pub fn verify(&self, xml: &str) -> Result<VerifyResult, DsigError> {
271 verify_signature_with_context(xml, self)
272 }
273}
274
275impl Default for VerifyContext<'_> {
276 fn default() -> Self {
277 Self::new()
278 }
279}
280
281#[derive(Debug)]
283#[non_exhaustive]
284#[must_use = "inspect status before accepting the reference result"]
285pub struct ReferenceResult {
286 pub reference_set: ReferenceSet,
288 pub reference_index: usize,
290 pub uri: String,
292 pub digest_algorithm: DigestAlgorithm,
294 pub status: DsigStatus,
296 pub pre_digest_data: Option<Vec<u8>>,
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
302#[non_exhaustive]
303pub enum ReferenceSet {
304 SignedInfo,
306 Manifest,
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312#[non_exhaustive]
313pub enum DsigStatus {
314 Valid,
316 Invalid(FailureReason),
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322#[non_exhaustive]
323pub enum FailureReason {
324 ReferenceDigestMismatch {
326 ref_index: usize,
336 },
337 ReferencePolicyViolation {
339 ref_index: usize,
341 },
342 ReferenceProcessingFailure {
344 ref_index: usize,
346 },
347 SignatureMismatch,
349 KeyNotFound,
351}
352
353#[derive(Debug)]
355#[non_exhaustive]
356#[must_use = "check first_failure/results before accepting the reference set"]
357pub struct ReferencesResult {
358 pub results: Vec<ReferenceResult>,
361 pub first_failure: Option<usize>,
363}
364
365impl ReferencesResult {
366 #[must_use]
368 pub fn all_valid(&self) -> bool {
369 self.results
370 .iter()
371 .all(|result| matches!(result.status, DsigStatus::Valid))
372 }
373}
374
375pub fn process_reference(
393 reference: &Reference,
394 resolver: &UriReferenceResolver<'_>,
395 signature_node: Node<'_, '_>,
396 reference_set: ReferenceSet,
397 reference_index: usize,
398 store_pre_digest: bool,
399) -> Result<ReferenceResult, ReferenceProcessingError> {
400 let execution_budget = TransformExecutionBudget::default();
401 let execution = ReferenceExecutionContext {
402 store_pre_digest,
403 transform_options: TransformOptions::default(),
404 transform_budget: &execution_budget,
405 };
406 process_reference_with_options(
407 reference,
408 resolver,
409 signature_node,
410 reference_set,
411 reference_index,
412 &execution,
413 )
414}
415
416struct ReferenceExecutionContext<'a> {
417 store_pre_digest: bool,
418 transform_options: TransformOptions,
419 transform_budget: &'a TransformExecutionBudget,
420}
421
422fn process_reference_with_options(
423 reference: &Reference,
424 resolver: &UriReferenceResolver<'_>,
425 signature_node: Node<'_, '_>,
426 reference_set: ReferenceSet,
427 reference_index: usize,
428 execution: &ReferenceExecutionContext<'_>,
429) -> Result<ReferenceResult, ReferenceProcessingError> {
430 let uri = reference
433 .uri
434 .as_deref()
435 .ok_or(ReferenceProcessingError::MissingUri)?;
436 let initial_data = resolver
437 .dereference_with_budget(uri, execution.transform_budget.node_set_materialization())
438 .map_err(ReferenceProcessingError::UriDereference)?;
439
440 let pre_digest_bytes = execute_transforms_with_options_and_budget(
442 signature_node,
443 initial_data,
444 &reference.transforms,
445 execution.transform_options,
446 execution.transform_budget,
447 )
448 .map_err(ReferenceProcessingError::Transform)?;
449
450 let computed_digest = compute_digest(reference.digest_method, &pre_digest_bytes);
452
453 let status = if constant_time_eq(&computed_digest, &reference.digest_value) {
455 DsigStatus::Valid
456 } else {
457 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch {
458 ref_index: reference_index,
459 })
460 };
461
462 Ok(ReferenceResult {
463 reference_set,
464 reference_index,
465 uri: uri.to_owned(),
466 digest_algorithm: reference.digest_method,
467 status,
468 pre_digest_data: if execution.store_pre_digest {
469 Some(pre_digest_bytes)
470 } else {
471 None
472 },
473 })
474}
475
476pub fn process_all_references(
488 references: &[Reference],
489 resolver: &UriReferenceResolver<'_>,
490 signature_node: Node<'_, '_>,
491 store_pre_digest: bool,
492) -> Result<ReferencesResult, ReferenceProcessingError> {
493 let execution_budget = TransformExecutionBudget::default();
494 let execution = ReferenceExecutionContext {
495 store_pre_digest,
496 transform_options: TransformOptions::default(),
497 transform_budget: &execution_budget,
498 };
499 process_all_references_with_options(references, resolver, signature_node, &execution)
500}
501
502fn process_all_references_with_options(
503 references: &[Reference],
504 resolver: &UriReferenceResolver<'_>,
505 signature_node: Node<'_, '_>,
506 execution: &ReferenceExecutionContext<'_>,
507) -> Result<ReferencesResult, ReferenceProcessingError> {
508 let mut results = Vec::with_capacity(references.len());
509
510 for (i, reference) in references.iter().enumerate() {
511 let result = process_reference_with_options(
512 reference,
513 resolver,
514 signature_node,
515 ReferenceSet::SignedInfo,
516 i,
517 execution,
518 )?;
519 let failed = matches!(result.status, DsigStatus::Invalid(_));
520 results.push(result);
521
522 if failed {
523 return Ok(ReferencesResult {
524 results,
525 first_failure: Some(i),
526 });
527 }
528 }
529
530 Ok(ReferencesResult {
531 results,
532 first_failure: None,
533 })
534}
535
536#[derive(Debug, thiserror::Error)]
540#[non_exhaustive]
541pub enum ReferenceProcessingError {
542 #[error("reference URI is required; omitted URI references are not supported")]
544 MissingUri,
545
546 #[error("URI dereference failed: {0}")]
548 UriDereference(#[source] super::types::TransformError),
549
550 #[error("transform failed: {0}")]
552 Transform(#[source] super::types::TransformError),
553}
554
555#[derive(Debug)]
557#[non_exhaustive]
558#[must_use = "inspect status before accepting the document"]
559pub struct VerifyResult {
560 pub status: DsigStatus,
565 pub signed_info_references: Vec<ReferenceResult>,
569 pub manifest_references: Vec<ReferenceResult>,
579 pub canonicalized_signed_info: Option<Vec<u8>>,
582}
583
584#[derive(Debug, thiserror::Error)]
586#[non_exhaustive]
587pub enum DsigError {
588 #[error("XML parse error: {0}")]
590 XmlParse(#[from] roxmltree::Error),
591
592 #[error("missing required element: <{element}>")]
594 MissingElement {
595 element: &'static str,
597 },
598
599 #[error("invalid Signature structure: {reason}")]
601 InvalidStructure {
602 reason: &'static str,
604 },
605
606 #[error("failed to parse SignedInfo: {0}")]
608 ParseSignedInfo(#[from] super::parse::ParseError),
609
610 #[error("failed to parse KeyInfo: {0}")]
612 ParseKeyInfo(#[source] super::parse::ParseError),
613
614 #[error("key resolution failed: {0}")]
616 KeyResolution(#[from] super::keys::KeyResolutionError),
617
618 #[error("failed to parse Manifest reference: {0}")]
620 ParseManifestReference(#[source] ParseError),
621
622 #[error("reference processing failed: {0}")]
624 Reference(#[from] ReferenceProcessingError),
625
626 #[error("SignedInfo canonicalization failed: {0}")]
628 Canonicalization(#[from] crate::c14n::C14nError),
629
630 #[error("invalid SignatureValue base64: {0}")]
632 SignatureValueBase64(#[from] base64::DecodeError),
633
634 #[error("signature verification failed: {0}")]
636 Crypto(#[from] SignatureVerificationError),
637
638 #[error("reference URI is not allowed by policy: {uri}")]
640 DisallowedUri {
641 uri: String,
643 },
644
645 #[error("transform is not allowed by policy: {algorithm}")]
647 DisallowedTransform {
648 algorithm: String,
650 },
651}
652
653type SignatureVerificationPipelineError = DsigError;
654
655pub fn verify_signature_with_pem_key(
683 xml: &str,
684 public_key_pem: &str,
685 store_pre_digest: bool,
686) -> Result<VerifyResult, DsigError> {
687 struct PemVerifyingKey<'a> {
688 public_key_pem: &'a str,
689 }
690
691 impl VerifyingKey for PemVerifyingKey<'_> {
692 fn verify(
693 &self,
694 algorithm: SignatureAlgorithm,
695 signed_data: &[u8],
696 signature_value: &[u8],
697 ) -> Result<bool, DsigError> {
698 verify_with_algorithm(algorithm, self.public_key_pem, signed_data, signature_value)
699 }
700 }
701
702 let key = PemVerifyingKey { public_key_pem };
703 VerifyContext::new()
704 .key(&key)
705 .store_pre_digest(store_pre_digest)
706 .verify(xml)
707}
708
709fn verify_signature_with_context(
710 xml: &str,
711 ctx: &VerifyContext<'_>,
712) -> Result<VerifyResult, SignatureVerificationPipelineError> {
713 let doc = Document::parse(xml)?;
714 let mut signatures = doc.descendants().filter(|node| {
715 node.is_element()
716 && node.tag_name().name() == "Signature"
717 && node.tag_name().namespace() == Some(XMLDSIG_NS)
718 });
719 let signature_node = match (signatures.next(), signatures.next()) {
720 (None, _) => {
721 return Err(SignatureVerificationPipelineError::MissingElement {
722 element: "Signature",
723 });
724 }
725 (Some(node), None) => node,
726 (Some(_), Some(_)) => {
727 return Err(SignatureVerificationPipelineError::InvalidStructure {
728 reason: "Signature must appear exactly once in document",
729 });
730 }
731 };
732
733 let signature_children = parse_signature_children(signature_node)?;
734 let signed_info_node = signature_children.signed_info_node;
735 let should_parse_key_info = match (ctx.key, ctx.key_resolver) {
736 (Some(_), _) => false,
737 (None, Some(resolver)) => resolver.consumes_document_key_info(),
738 (None, None) => true,
739 };
740 let key_info = if should_parse_key_info {
741 signature_children
742 .key_info_node
743 .map(parse_key_info)
744 .transpose()
745 .map_err(SignatureVerificationPipelineError::ParseKeyInfo)?
746 } else {
747 None
748 };
749
750 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
751 let signed_info =
752 parse_signed_info_with_xpath_budget(signed_info_node, &mut xpath_parse_budget)?;
753 enforce_reference_policies(
754 &signed_info.references,
755 ctx.allowed_uri_types,
756 ctx.allowed_transform_uris(),
757 )?;
758
759 let resolver = UriReferenceResolver::new(&doc);
760 let execution_budget = TransformExecutionBudget::default();
761 let execution = ReferenceExecutionContext {
762 store_pre_digest: ctx.store_pre_digest,
763 transform_options: ctx.transform_options,
764 transform_budget: &execution_budget,
765 };
766 let references = process_all_references_with_options(
767 &signed_info.references,
768 &resolver,
769 signature_node,
770 &execution,
771 )?;
772
773 if let Some(first_failure) = references.first_failure {
774 let status = references.results[first_failure].status;
775 return Ok(VerifyResult {
776 status,
777 signed_info_references: references.results,
778 manifest_references: Vec::new(),
779 canonicalized_signed_info: None,
780 });
781 }
782
783 let signed_info_subtree: HashSet<_> = signed_info_node
784 .descendants()
785 .map(|node: Node<'_, '_>| node.id())
786 .collect();
787 let mut canonical_signed_info = Vec::new();
788 canonicalize(
789 &doc,
790 Some(&|node| signed_info_subtree.contains(&node.id())),
791 &signed_info.c14n_method,
792 &mut canonical_signed_info,
793 )?;
794
795 let signature_value = decode_signature_value(signature_children.signature_value_node)?;
796 let Some(resolved_key) =
797 resolve_verifying_key(ctx, key_info.as_ref(), signed_info.signature_method)?
798 else {
799 return Ok(VerifyResult {
800 status: DsigStatus::Invalid(FailureReason::KeyNotFound),
801 signed_info_references: references.results,
802 manifest_references: Vec::new(),
803 canonicalized_signed_info: if ctx.store_pre_digest {
804 Some(canonical_signed_info)
805 } else {
806 None
807 },
808 });
809 };
810 let verifier = resolved_key.as_ref();
811 let signature_valid = verifier.verify(
812 signed_info.signature_method,
813 &canonical_signed_info,
814 &signature_value,
815 )?;
816
817 if !signature_valid {
818 return Ok(VerifyResult {
819 status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
820 signed_info_references: references.results,
821 manifest_references: Vec::new(),
822 canonicalized_signed_info: if ctx.store_pre_digest {
823 Some(canonical_signed_info)
824 } else {
825 None
826 },
827 });
828 }
829
830 let manifest_references = if ctx.process_manifests {
831 let signed_info_reference_nodes =
832 collect_authenticated_signed_info_reference_nodes(&signed_info.references, &resolver);
833 process_manifest_references(
834 signature_node,
835 &resolver,
836 ctx,
837 &signed_info_reference_nodes,
838 &execution,
839 &mut xpath_parse_budget,
840 )?
841 } else {
842 Vec::new()
843 };
844
845 Ok(VerifyResult {
846 status: DsigStatus::Valid,
847 signed_info_references: references.results,
848 manifest_references,
849 canonicalized_signed_info: if ctx.store_pre_digest {
850 Some(canonical_signed_info)
851 } else {
852 None
853 },
854 })
855}
856
857fn process_manifest_references(
858 signature_node: Node<'_, '_>,
859 resolver: &UriReferenceResolver<'_>,
860 ctx: &VerifyContext<'_>,
861 signed_info_reference_nodes: &HashSet<NodeId>,
862 execution: &ReferenceExecutionContext<'_>,
863 xpath_parse_budget: &mut XPathSignatureParseBudget,
864) -> Result<Vec<ReferenceResult>, SignatureVerificationPipelineError> {
865 let manifest_references = parse_manifest_references(
866 signature_node,
867 signed_info_reference_nodes,
868 xpath_parse_budget,
869 )?;
870 if manifest_references.is_empty() {
871 return Ok(Vec::new());
872 }
873 let mut results = Vec::with_capacity(manifest_references.len());
874 for (index, reference) in manifest_references.iter().enumerate() {
875 match enforce_reference_policies(
876 std::slice::from_ref(reference),
877 ctx.allowed_uri_types,
878 ctx.allowed_transform_uris(),
879 ) {
880 Ok(()) => {}
881 Err(
882 SignatureVerificationPipelineError::DisallowedUri { .. }
883 | SignatureVerificationPipelineError::DisallowedTransform { .. },
884 ) => {
885 results.push(manifest_reference_invalid_result(
886 reference,
887 index,
888 FailureReason::ReferencePolicyViolation { ref_index: index },
889 ));
890 continue;
891 }
892 Err(SignatureVerificationPipelineError::Reference(
893 ReferenceProcessingError::MissingUri,
894 )) => {
895 results.push(manifest_reference_invalid_result(
896 reference,
897 index,
898 FailureReason::ReferenceProcessingFailure { ref_index: index },
899 ));
900 continue;
901 }
902 Err(_) => {
903 results.push(manifest_reference_invalid_result(
906 reference,
907 index,
908 FailureReason::ReferenceProcessingFailure { ref_index: index },
909 ));
910 continue;
911 }
912 }
913
914 match process_reference_with_options(
915 reference,
916 resolver,
917 signature_node,
918 ReferenceSet::Manifest,
919 index,
920 execution,
921 ) {
922 Ok(result) => results.push(result),
923 Err(_) => results.push(manifest_reference_invalid_result(
924 reference,
925 index,
926 FailureReason::ReferenceProcessingFailure { ref_index: index },
927 )),
928 }
929 }
930 Ok(results)
931}
932
933fn manifest_reference_invalid_result(
934 reference: &Reference,
935 index: usize,
936 reason: FailureReason,
937) -> ReferenceResult {
938 ReferenceResult {
939 reference_set: ReferenceSet::Manifest,
940 reference_index: index,
941 uri: reference
942 .uri
943 .clone()
944 .unwrap_or_else(|| "<omitted>".to_owned()),
945 digest_algorithm: reference.digest_method,
946 status: DsigStatus::Invalid(reason),
947 pre_digest_data: None,
948 }
949}
950
951fn parse_manifest_references(
952 signature_node: Node<'_, '_>,
953 signed_info_reference_nodes: &HashSet<NodeId>,
954 xpath_parse_budget: &mut XPathSignatureParseBudget,
955) -> Result<Vec<Reference>, SignatureVerificationPipelineError> {
956 let mut references = Vec::new();
957 for object_node in signature_node.children().filter(|node| {
958 node.is_element()
959 && node.tag_name().namespace() == Some(XMLDSIG_NS)
960 && node.tag_name().name() == "Object"
961 }) {
962 let object_is_signed = signed_info_reference_nodes.contains(&object_node.id());
963 for manifest_node in object_node.children().filter(|node| {
964 node.is_element()
965 && node.tag_name().namespace() == Some(XMLDSIG_NS)
966 && node.tag_name().name() == "Manifest"
967 }) {
968 let manifest_is_signed = signed_info_reference_nodes.contains(&manifest_node.id());
969 if !object_is_signed && !manifest_is_signed {
970 continue;
971 }
972 let mut manifest_children = Vec::new();
973 for child in manifest_node.children() {
974 if child.is_text()
975 && child.text().is_some_and(|text| {
976 text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
977 })
978 {
979 return Err(SignatureVerificationPipelineError::InvalidStructure {
980 reason: "Manifest contains non-whitespace mixed content",
981 });
982 }
983 if child.is_element() {
984 manifest_children.push(child);
985 }
986 }
987 if manifest_children.is_empty() {
988 return Err(SignatureVerificationPipelineError::InvalidStructure {
989 reason: "Manifest must contain at least one ds:Reference element child",
990 });
991 }
992 for child in manifest_children {
993 if child.tag_name().namespace() != Some(XMLDSIG_NS)
994 || child.tag_name().name() != "Reference"
995 {
996 return Err(SignatureVerificationPipelineError::InvalidStructure {
997 reason: "Manifest must contain only ds:Reference element children",
998 });
999 }
1000 if references.len() == MAX_REFERENCES_PER_SIGNATURE {
1001 return Err(SignatureVerificationPipelineError::InvalidStructure {
1002 reason: "signed Manifests exceed the per-signature Reference limit",
1003 });
1004 }
1005 references.push(
1006 parse_reference_with_xpath_budget(child, xpath_parse_budget)
1007 .map_err(SignatureVerificationPipelineError::ParseManifestReference)?,
1008 );
1009 }
1010 }
1011 }
1012 Ok(references)
1013}
1014
1015fn collect_authenticated_signed_info_reference_nodes(
1016 references: &[Reference],
1017 resolver: &UriReferenceResolver<'_>,
1018) -> HashSet<NodeId> {
1019 references
1020 .iter()
1021 .filter(|reference| {
1025 reference
1026 .transforms
1027 .iter()
1028 .all(transform_preserves_manifest_structure)
1029 })
1030 .filter_map(|reference| reference.uri.as_deref())
1031 .filter_map(signed_info_reference_id_from_uri)
1032 .filter_map(|id| resolver.node_id_for_id(id))
1033 .collect()
1034}
1035
1036fn transform_preserves_manifest_structure(transform: &Transform) -> bool {
1037 match transform {
1038 Transform::C14n(_) => true,
1039 Transform::Enveloped
1044 | Transform::XpathExcludeAllSignatures
1045 | Transform::XPath(_)
1046 | Transform::XPathFilter2(_)
1047 | Transform::Base64Decode => false,
1048 }
1049}
1050
1051fn signed_info_reference_id_from_uri(uri: &str) -> Option<&str> {
1052 let fragment = uri.strip_prefix('#')?;
1053 if fragment.is_empty() || fragment == "xpointer(/)" {
1054 return None;
1055 }
1056 if let Some(id) = parse_xpointer_id_fragment(fragment) {
1057 return (!id.is_empty()).then_some(id);
1058 }
1059 (!fragment.starts_with("xpointer(")).then_some(fragment)
1060}
1061
1062enum ResolvedVerifyingKey<'a> {
1063 Borrowed(&'a dyn VerifyingKey),
1064 Owned(Box<dyn VerifyingKey + 'a>),
1065}
1066
1067impl ResolvedVerifyingKey<'_> {
1068 fn as_ref(&self) -> &dyn VerifyingKey {
1069 match self {
1070 Self::Borrowed(key) => *key,
1071 Self::Owned(key) => key.as_ref(),
1072 }
1073 }
1074}
1075
1076fn resolve_verifying_key<'k>(
1077 ctx: &VerifyContext<'k>,
1078 key_info: Option<&KeyInfo>,
1079 algorithm: SignatureAlgorithm,
1080) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
1081 if let Some(key) = ctx.key {
1082 return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
1083 }
1084 if let Some(resolver) = ctx.key_resolver {
1085 let resolved = resolver.resolve(key_info, algorithm)?;
1086 return Ok(resolved.map(ResolvedVerifyingKey::Owned));
1087 }
1088 Ok(None)
1089}
1090
1091fn enforce_reference_policies(
1092 references: &[Reference],
1093 allowed_uri_types: UriTypeSet,
1094 allowed_transforms: Option<&HashSet<String>>,
1095) -> Result<(), SignatureVerificationPipelineError> {
1096 for reference in references {
1097 let uri = reference
1098 .uri
1099 .as_deref()
1100 .ok_or(SignatureVerificationPipelineError::Reference(
1101 ReferenceProcessingError::MissingUri,
1102 ))?;
1103 if !allowed_uri_types.allows(uri) {
1104 return Err(SignatureVerificationPipelineError::DisallowedUri {
1105 uri: uri.to_owned(),
1106 });
1107 }
1108
1109 if let Some(allowed) = allowed_transforms {
1110 for transform in &reference.transforms {
1111 let transform_uri = transform_uri(transform);
1112 if !allowed.contains(transform_uri) {
1113 return Err(SignatureVerificationPipelineError::DisallowedTransform {
1114 algorithm: transform_uri.to_owned(),
1115 });
1116 }
1117 }
1118
1119 let produces_binary = reference.transforms.last().is_some_and(|transform| {
1120 matches!(transform, Transform::C14n(_) | Transform::Base64Decode)
1121 });
1122 if !produces_binary && !allowed.contains(DEFAULT_IMPLICIT_C14N_URI) {
1123 return Err(SignatureVerificationPipelineError::DisallowedTransform {
1124 algorithm: DEFAULT_IMPLICIT_C14N_URI.to_owned(),
1125 });
1126 }
1127 }
1128 }
1129 Ok(())
1130}
1131
1132fn transform_uri(transform: &Transform) -> &'static str {
1133 match transform {
1134 Transform::Enveloped => super::transforms::ENVELOPED_SIGNATURE_URI,
1135 Transform::XpathExcludeAllSignatures | Transform::XPath(_) => XPATH_TRANSFORM_URI,
1136 Transform::XPathFilter2(_) => super::transforms::XPATH_FILTER2_TRANSFORM_URI,
1137 Transform::C14n(algo) => algo.uri(),
1138 Transform::Base64Decode => BASE64_TRANSFORM_URI,
1139 }
1140}
1141
1142#[derive(Debug, Clone, Copy)]
1143struct SignatureChildNodes<'a, 'input> {
1144 signed_info_node: Node<'a, 'input>,
1145 signature_value_node: Node<'a, 'input>,
1146 key_info_node: Option<Node<'a, 'input>>,
1147}
1148
1149fn parse_signature_children<'a, 'input>(
1150 signature_node: Node<'a, 'input>,
1151) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
1152 let mut signed_info_node: Option<Node<'_, '_>> = None;
1153 let mut signature_value_node: Option<Node<'_, '_>> = None;
1154 let mut key_info_node: Option<Node<'_, '_>> = None;
1155 let mut signed_info_index: Option<usize> = None;
1156 let mut signature_value_index: Option<usize> = None;
1157 let mut key_info_index: Option<usize> = None;
1158 let mut first_unexpected_dsig_index: Option<usize> = None;
1159
1160 let mut element_index = 0usize;
1161 for child in signature_node.children() {
1162 if child.is_text() {
1163 if child
1164 .text()
1165 .is_some_and(|text| !is_xml_whitespace_only(text))
1166 {
1167 return Err(SignatureVerificationPipelineError::InvalidStructure {
1168 reason: "Signature must not contain non-whitespace mixed content",
1169 });
1170 }
1171 continue;
1172 }
1173 if !child.is_element() {
1174 continue;
1175 }
1176
1177 element_index += 1;
1178 if child.tag_name().namespace() != Some(XMLDSIG_NS) {
1179 return Err(SignatureVerificationPipelineError::InvalidStructure {
1180 reason: "Signature must contain only XMLDSIG element children",
1181 });
1182 }
1183 match child.tag_name().name() {
1184 "SignedInfo" => {
1185 if signed_info_node.is_some() {
1186 return Err(SignatureVerificationPipelineError::InvalidStructure {
1187 reason: "SignedInfo must appear exactly once under Signature",
1188 });
1189 }
1190 signed_info_node = Some(child);
1191 signed_info_index = Some(element_index);
1192 }
1193 "SignatureValue" => {
1194 if signature_value_node.is_some() {
1195 return Err(SignatureVerificationPipelineError::InvalidStructure {
1196 reason: "SignatureValue must appear exactly once under Signature",
1197 });
1198 }
1199 signature_value_node = Some(child);
1200 signature_value_index = Some(element_index);
1201 }
1202 "KeyInfo" => {
1203 if key_info_node.is_some() {
1204 return Err(SignatureVerificationPipelineError::InvalidStructure {
1205 reason: "KeyInfo must appear at most once under Signature",
1206 });
1207 }
1208 key_info_node = Some(child);
1209 key_info_index = Some(element_index);
1210 }
1211 "Object" => {
1212 }
1215 _ => {
1216 if first_unexpected_dsig_index.is_none() {
1217 first_unexpected_dsig_index = Some(element_index);
1218 }
1219 }
1220 }
1221 }
1222
1223 let signed_info_node =
1224 signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
1225 element: "SignedInfo",
1226 })?;
1227 let signature_value_node =
1228 signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
1229 element: "SignatureValue",
1230 })?;
1231 if signed_info_index != Some(1) {
1232 return Err(SignatureVerificationPipelineError::InvalidStructure {
1233 reason: "SignedInfo must be the first element child of Signature",
1234 });
1235 }
1236 if signature_value_index != Some(2) {
1237 return Err(SignatureVerificationPipelineError::InvalidStructure {
1238 reason: "SignatureValue must be the second element child of Signature",
1239 });
1240 }
1241 if let Some(index) = key_info_index
1242 && index != 3
1243 {
1244 return Err(SignatureVerificationPipelineError::InvalidStructure {
1245 reason: "KeyInfo must be the third element child of Signature when present",
1246 });
1247 }
1248
1249 let allowed_prefix_end = key_info_index.unwrap_or(2);
1250 if let Some(unexpected_index) = first_unexpected_dsig_index {
1251 return Err(SignatureVerificationPipelineError::InvalidStructure {
1252 reason: if unexpected_index > allowed_prefix_end {
1253 "After SignedInfo, SignatureValue, and optional KeyInfo, Signature may contain only Object elements"
1254 } else {
1255 "Signature may contain SignedInfo first, SignatureValue second, optional KeyInfo third, and Object elements thereafter"
1256 },
1257 });
1258 }
1259
1260 Ok(SignatureChildNodes {
1261 signed_info_node,
1262 signature_value_node,
1263 key_info_node,
1264 })
1265}
1266
1267fn decode_signature_value(
1268 signature_value_node: Node<'_, '_>,
1269) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
1270 if signature_value_node
1271 .children()
1272 .any(|child| child.is_element())
1273 {
1274 return Err(SignatureVerificationPipelineError::InvalidStructure {
1275 reason: "SignatureValue must not contain element children",
1276 });
1277 }
1278
1279 let mut normalized = Vec::new();
1280 let mut raw_text_len = 0usize;
1281 for child in signature_value_node
1282 .children()
1283 .filter(|child| child.is_text())
1284 {
1285 if let Some(text) = child.text() {
1286 push_normalized_signature_text(text, &mut raw_text_len, &mut normalized)?;
1287 }
1288 }
1289
1290 Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
1291}
1292
1293fn push_normalized_signature_text(
1294 text: &str,
1295 raw_text_len: &mut usize,
1296 normalized: &mut Vec<u8>,
1297) -> Result<(), SignatureVerificationPipelineError> {
1298 if raw_text_len.saturating_add(text.len()) > MAX_SIGNATURE_VALUE_TEXT_LEN {
1299 return Err(SignatureVerificationPipelineError::InvalidStructure {
1300 reason: "SignatureValue exceeds maximum allowed text length",
1301 });
1302 }
1303 *raw_text_len = raw_text_len.saturating_add(text.len());
1304
1305 normalize_xml_base64_bytes(text.as_bytes(), normalized, |_| true).map_err(|err| {
1306 SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(
1307 err.normalized_offset,
1308 err.invalid_byte,
1309 ))
1310 })?;
1311 if normalized.len() > MAX_SIGNATURE_VALUE_LEN {
1312 return Err(SignatureVerificationPipelineError::InvalidStructure {
1313 reason: "SignatureValue exceeds maximum allowed length",
1314 });
1315 }
1316
1317 Ok(())
1318}
1319
1320fn verify_with_algorithm(
1321 algorithm: SignatureAlgorithm,
1322 public_key_pem: &str,
1323 signed_data: &[u8],
1324 signature_value: &[u8],
1325) -> Result<bool, SignatureVerificationPipelineError> {
1326 match algorithm {
1327 SignatureAlgorithm::RsaSha1
1328 | SignatureAlgorithm::RsaSha256
1329 | SignatureAlgorithm::RsaSha384
1330 | SignatureAlgorithm::RsaSha512 => Ok(verify_rsa_signature_pem(
1331 algorithm,
1332 public_key_pem,
1333 signed_data,
1334 signature_value,
1335 )?),
1336 SignatureAlgorithm::EcdsaP256Sha256 | SignatureAlgorithm::EcdsaP384Sha384 => {
1337 match verify_ecdsa_signature_pem(
1341 algorithm,
1342 public_key_pem,
1343 signed_data,
1344 signature_value,
1345 ) {
1346 Ok(valid) => Ok(valid),
1347 Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
1348 Err(error) => Err(error.into()),
1349 }
1350 }
1351 }
1352}
1353
1354#[cfg(test)]
1355#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
1356mod tests {
1357 use super::*;
1358 use crate::c14n::C14nAlgorithm;
1359 use crate::xmldsig::digest::DigestAlgorithm;
1360 use crate::xmldsig::parse::{Reference, parse_signed_info};
1361 use crate::xmldsig::transforms::Transform;
1362 use crate::xmldsig::uri::UriReferenceResolver;
1363 use base64::Engine;
1364 use roxmltree::Document;
1365
1366 fn make_reference(
1370 uri: &str,
1371 transforms: Vec<Transform>,
1372 digest_method: DigestAlgorithm,
1373 digest_value: Vec<u8>,
1374 ) -> Reference {
1375 Reference {
1376 uri: Some(uri.to_string()),
1377 id: None,
1378 ref_type: None,
1379 transforms,
1380 digest_method,
1381 digest_value,
1382 }
1383 }
1384
1385 struct RejectingKey;
1386
1387 impl VerifyingKey for RejectingKey {
1388 fn verify(
1389 &self,
1390 _algorithm: SignatureAlgorithm,
1391 _signed_data: &[u8],
1392 _signature_value: &[u8],
1393 ) -> Result<bool, SignatureVerificationPipelineError> {
1394 Ok(false)
1395 }
1396 }
1397
1398 struct AcceptingKey;
1399
1400 impl VerifyingKey for AcceptingKey {
1401 fn verify(
1402 &self,
1403 _algorithm: SignatureAlgorithm,
1404 _signed_data: &[u8],
1405 _signature_value: &[u8],
1406 ) -> Result<bool, SignatureVerificationPipelineError> {
1407 Ok(true)
1408 }
1409 }
1410
1411 struct PanicResolver;
1412
1413 impl KeyResolver for PanicResolver {
1414 fn resolve<'a>(
1415 &'a self,
1416 _key_info: Option<&KeyInfo>,
1417 _algorithm: SignatureAlgorithm,
1418 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
1419 {
1420 panic!("resolver should not be called when references already fail");
1421 }
1422 }
1423
1424 struct MissingKeyResolver;
1425
1426 impl KeyResolver for MissingKeyResolver {
1427 fn resolve<'a>(
1428 &'a self,
1429 _key_info: Option<&KeyInfo>,
1430 _algorithm: SignatureAlgorithm,
1431 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
1432 {
1433 Ok(None)
1434 }
1435 }
1436
1437 struct ConsumingKeyInfoResolver;
1438
1439 impl KeyResolver for ConsumingKeyInfoResolver {
1440 fn resolve<'a>(
1441 &'a self,
1442 _key_info: Option<&KeyInfo>,
1443 _algorithm: SignatureAlgorithm,
1444 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
1445 {
1446 Ok(None)
1447 }
1448
1449 fn consumes_document_key_info(&self) -> bool {
1450 true
1451 }
1452 }
1453
1454 fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
1455 format!(
1456 r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1457 <ds:SignedInfo>
1458 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1459 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1460 <ds:Reference URI="{reference_uri}">
1461 {transforms_xml}
1462 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1463 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
1464 </ds:Reference>
1465 </ds:SignedInfo>
1466 <ds:SignatureValue>AQ==</ds:SignatureValue>
1467</ds:Signature>"#
1468 )
1469 }
1470
1471 fn signature_with_target_reference(signature_value_b64: &str) -> String {
1472 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1473 <target ID="target">payload</target>
1474 <ds:Signature>
1475 <ds:SignedInfo>
1476 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1477 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1478 <ds:Reference URI="#target">
1479 <ds:Transforms>
1480 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1481 </ds:Transforms>
1482 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1483 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
1484 </ds:Reference>
1485 </ds:SignedInfo>
1486 <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
1487 </ds:Signature>
1488</root>"##;
1489
1490 let doc = Document::parse(xml_template).unwrap();
1491 let sig_node = doc
1492 .descendants()
1493 .find(|node| node.is_element() && node.tag_name().name() == "Signature")
1494 .unwrap();
1495 let signed_info_node = sig_node
1496 .children()
1497 .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
1498 .unwrap();
1499 let signed_info = parse_signed_info(signed_info_node).unwrap();
1500 let reference = &signed_info.references[0];
1501 let resolver = UriReferenceResolver::new(&doc);
1502 let initial_data = resolver
1503 .dereference(reference.uri.as_deref().unwrap())
1504 .unwrap();
1505 let pre_digest =
1506 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
1507 .unwrap();
1508 let digest = compute_digest(reference.digest_method, &pre_digest);
1509 let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
1510 xml_template
1511 .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
1512 .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
1513 }
1514
1515 #[test]
1516 fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
1517 let xml = signature_with_target_reference("AQ==");
1518
1519 let result = VerifyContext::new()
1520 .verify(&xml)
1521 .expect("missing key config must be reported as verification status");
1522 assert!(
1523 matches!(
1524 result.status,
1525 DsigStatus::Invalid(FailureReason::KeyNotFound)
1526 ),
1527 "unexpected status: {:?}",
1528 result.status
1529 );
1530 }
1531
1532 #[test]
1533 fn verify_context_rejects_disallowed_uri() {
1534 let xml = minimal_signature_xml("http://example.com/external", "");
1535 let err = VerifyContext::new()
1536 .key(&RejectingKey)
1537 .verify(&xml)
1538 .expect_err("external URI should be rejected by default policy");
1539 assert!(matches!(
1540 err,
1541 SignatureVerificationPipelineError::DisallowedUri { .. }
1542 ));
1543 }
1544
1545 #[test]
1546 fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
1547 let xml = minimal_signature_xml("", "");
1548 let err = VerifyContext::new()
1549 .key(&RejectingKey)
1550 .allowed_uri_types(UriTypeSet::new(false, true, false))
1551 .verify(&xml)
1552 .expect_err("empty URI must be rejected when empty references are disabled");
1553 assert!(matches!(
1554 err,
1555 SignatureVerificationPipelineError::DisallowedUri { ref uri } if uri.is_empty()
1556 ));
1557 }
1558
1559 #[test]
1560 fn verify_context_rejects_disallowed_transform() {
1561 let xml = minimal_signature_xml(
1562 "",
1563 r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
1564 );
1565 let err = VerifyContext::new()
1566 .key(&RejectingKey)
1567 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
1568 .verify(&xml)
1569 .expect_err("enveloped transform should be rejected by allowlist");
1570 assert!(matches!(
1571 err,
1572 SignatureVerificationPipelineError::DisallowedTransform { .. }
1573 ));
1574 }
1575
1576 fn signature_with_manifest_xml(valid_manifest_digest: bool) -> String {
1577 signature_with_manifest_xml_with_manifest_mutation(valid_manifest_digest, |xml| xml)
1578 }
1579
1580 fn signature_with_manifest_xml_with_manifest_mutation<F>(
1581 valid_manifest_digest: bool,
1582 mutate_manifest: F,
1583 ) -> String
1584 where
1585 F: FnOnce(String) -> String,
1586 {
1587 const TMP_SIGNED_INFO_DIGEST: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
1588 const INVALID_MANIFEST_DIGEST: &str = "//////////////////////////8=";
1589 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1590 <target ID="target">payload</target>
1591 <ds:Signature>
1592 <ds:SignedInfo>
1593 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1594 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1595 <ds:Reference URI="#manifest">
1596 <ds:Transforms>
1597 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1598 </ds:Transforms>
1599 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1600 <ds:DigestValue>SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER</ds:DigestValue>
1601 </ds:Reference>
1602 </ds:SignedInfo>
1603 <ds:SignatureValue>AQ==</ds:SignatureValue>
1604 <ds:Object>
1605 <ds:Manifest ID="manifest">
1606 <ds:Reference URI="#target">
1607 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1608 <ds:DigestValue>MANIFEST_DIGEST_PLACEHOLDER</ds:DigestValue>
1609 </ds:Reference>
1610 </ds:Manifest>
1611 </ds:Object>
1612 </ds:Signature>
1613</root>"##;
1614 let seed_xml = xml_template.replace(
1615 "SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER",
1616 TMP_SIGNED_INFO_DIGEST,
1617 );
1618 let doc = Document::parse(&seed_xml).unwrap();
1619 let signature_node = doc
1620 .descendants()
1621 .find(|node| {
1622 node.is_element()
1623 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1624 && node.tag_name().name() == "Signature"
1625 })
1626 .unwrap();
1627 let resolver = UriReferenceResolver::new(&doc);
1628 let initial_data = resolver.dereference("#target").unwrap();
1629 let manifest_pre_digest =
1630 crate::xmldsig::execute_transforms(signature_node, initial_data, &[]).unwrap();
1631 let computed_manifest_digest_b64 = base64::engine::general_purpose::STANDARD
1632 .encode(compute_digest(DigestAlgorithm::Sha1, &manifest_pre_digest));
1633 let final_manifest_digest_b64 = if valid_manifest_digest {
1634 computed_manifest_digest_b64.as_str()
1635 } else {
1636 INVALID_MANIFEST_DIGEST
1637 };
1638 let xml_with_manifest_digest = mutate_manifest(
1639 seed_xml.replace("MANIFEST_DIGEST_PLACEHOLDER", final_manifest_digest_b64),
1640 );
1641 let signed_doc = Document::parse(&xml_with_manifest_digest).unwrap();
1642 let signed_signature_node = signed_doc
1643 .descendants()
1644 .find(|node| {
1645 node.is_element()
1646 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1647 && node.tag_name().name() == "Signature"
1648 })
1649 .unwrap();
1650 let signed_info_node = signed_signature_node
1651 .children()
1652 .find(|node| {
1653 node.is_element()
1654 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1655 && node.tag_name().name() == "SignedInfo"
1656 })
1657 .unwrap();
1658 let signed_info = parse_signed_info(signed_info_node).unwrap();
1659 let object_reference = &signed_info.references[0];
1660 let signed_resolver = UriReferenceResolver::new(&signed_doc);
1661 let signed_initial_data = signed_resolver
1662 .dereference(object_reference.uri.as_deref().unwrap())
1663 .unwrap();
1664 let signed_pre_digest = crate::xmldsig::execute_transforms(
1665 signed_signature_node,
1666 signed_initial_data,
1667 &object_reference.transforms,
1668 )
1669 .unwrap();
1670 let signed_digest_b64 = base64::engine::general_purpose::STANDARD.encode(compute_digest(
1671 object_reference.digest_method,
1672 &signed_pre_digest,
1673 ));
1674
1675 xml_with_manifest_digest.replacen(TMP_SIGNED_INFO_DIGEST, &signed_digest_b64, 1)
1676 }
1677
1678 fn replace_fixture_manifest_digest(xml: &str, replacement: &str) -> String {
1679 let object_marker = "<ds:Object>";
1680 let object_start = xml
1681 .find(object_marker)
1682 .expect("fixture should contain ds:Object")
1683 + object_marker.len();
1684 let open = "<ds:DigestValue>";
1685 let close = "</ds:DigestValue>";
1686 let value_start = xml[object_start..]
1687 .find(open)
1688 .map(|offset| object_start + offset + open.len())
1689 .expect("Manifest should contain DigestValue");
1690 let value_end = xml[value_start..]
1691 .find(close)
1692 .map(|offset| value_start + offset)
1693 .expect("Manifest DigestValue must be closed");
1694
1695 format!("{}{replacement}{}", &xml[..value_start], &xml[value_end..])
1696 }
1697
1698 #[test]
1699 fn verify_context_processes_manifest_references_when_enabled() {
1700 let xml = signature_with_manifest_xml(true);
1701
1702 let result_without_manifests = VerifyContext::new()
1703 .key(&RejectingKey)
1704 .verify(&xml)
1705 .expect("manifest processing disabled should still verify SignedInfo");
1706 assert!(
1707 result_without_manifests.manifest_references.is_empty(),
1708 "manifest results must stay empty when manifest processing is disabled",
1709 );
1710 assert!(matches!(
1711 result_without_manifests.status,
1712 DsigStatus::Invalid(FailureReason::SignatureMismatch)
1713 ));
1714
1715 let malformed_manifest_xml = signature_with_manifest_xml(true).replacen(
1716 "</ds:Object>",
1717 "</ds:Object><ds:Object><ds:Manifest><ds:Foo/></ds:Manifest></ds:Object>",
1718 1,
1719 );
1720 let malformed_with_manifests_disabled = VerifyContext::new()
1721 .key(&RejectingKey)
1722 .verify(&malformed_manifest_xml)
1723 .expect("malformed Manifest must be ignored when manifest processing is disabled");
1724 assert!(
1725 malformed_with_manifests_disabled
1726 .manifest_references
1727 .is_empty(),
1728 "manifest parser must not run when process_manifests is disabled",
1729 );
1730 assert!(matches!(
1731 malformed_with_manifests_disabled.status,
1732 DsigStatus::Invalid(FailureReason::SignatureMismatch)
1733 ));
1734
1735 let result_with_manifests = VerifyContext::new()
1736 .key(&AcceptingKey)
1737 .process_manifests(true)
1738 .verify(&xml)
1739 .expect("manifest references should be processed when enabled");
1740 assert_eq!(result_with_manifests.manifest_references.len(), 1);
1741 assert_eq!(
1742 result_with_manifests.manifest_references[0].reference_set,
1743 ReferenceSet::Manifest
1744 );
1745 assert_eq!(
1746 result_with_manifests.manifest_references[0].reference_index,
1747 0
1748 );
1749 assert!(matches!(
1750 result_with_manifests.manifest_references[0].status,
1751 DsigStatus::Valid
1752 ));
1753 assert!(matches!(result_with_manifests.status, DsigStatus::Valid));
1754 }
1755
1756 #[test]
1757 fn verify_context_skips_manifest_work_when_signature_value_is_invalid() {
1758 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1762 replace_fixture_manifest_digest(&xml, "!!!")
1763 });
1764 assert!(
1765 xml.split_once("<ds:Object>")
1766 .is_some_and(|(_, object)| object.contains("<ds:DigestValue>!!!</ds:DigestValue>")),
1767 "fixture mutation must corrupt the nested Manifest DigestValue",
1768 );
1769
1770 let result = VerifyContext::new()
1771 .key(&RejectingKey)
1772 .process_manifests(true)
1773 .verify(&xml)
1774 .expect("invalid SignatureValue must short-circuit Manifest parsing");
1775
1776 assert!(matches!(
1777 result.status,
1778 DsigStatus::Invalid(FailureReason::SignatureMismatch)
1779 ));
1780 assert!(result.manifest_references.is_empty());
1781 }
1782
1783 #[test]
1784 fn verify_context_shares_xpath_parse_budget_with_manifest_references() {
1785 let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
1788 .repeat(64);
1789 let transform = format!(
1790 r#"<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</ds:Transform>"#
1791 );
1792 let max_transforms = transform.repeat(16);
1793 let max_manifest_reference = format!(
1794 r##"<ds:Reference URI="#target"><ds:Transforms>{max_transforms}</ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue></ds:Reference>"##
1795 );
1796 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1797 xml.replacen(
1798 r##"<ds:Reference URI="#target">"##,
1799 &format!(
1800 r##"<ds:Reference URI="#target"><ds:Transforms>{}</ds:Transforms>"##,
1801 max_transforms
1802 ),
1803 1,
1804 )
1805 .replacen(
1806 "</ds:SignedInfo>",
1807 r##"<ds:Reference URI="#target"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>false()</ds:XPath></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</ds:DigestValue></ds:Reference></ds:SignedInfo>"##,
1808 1,
1809 )
1810 .replacen(
1811 "</ds:Manifest>",
1812 &format!("{}</ds:Manifest>", max_manifest_reference.repeat(3)),
1813 1,
1814 )
1815 });
1816
1817 let error = VerifyContext::new()
1818 .key(&AcceptingKey)
1819 .process_manifests(true)
1820 .verify(&xml)
1821 .expect_err("SignedInfo and Manifest References must share one XPath parse budget");
1822
1823 assert!(
1824 matches!(
1825 &error,
1826 SignatureVerificationPipelineError::ParseManifestReference(source)
1827 if source.to_string().contains("signature-wide XPath expression budget")
1828 ),
1829 "unexpected error: {error:?}"
1830 );
1831 }
1832
1833 #[test]
1834 fn verify_context_processes_manifest_when_signedinfo_references_object() {
1835 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1836 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
1837 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
1838 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
1839 });
1840
1841 let result = VerifyContext::new()
1842 .key(&AcceptingKey)
1843 .process_manifests(true)
1844 .verify(&xml)
1845 .expect("manifest references should be processed when SignedInfo references ds:Object");
1846 assert_eq!(
1847 result.manifest_references.len(),
1848 1,
1849 "signed ds:Object should enable processing of its direct-child ds:Manifest",
1850 );
1851 assert_eq!(
1852 result.manifest_references[0].reference_set,
1853 ReferenceSet::Manifest
1854 );
1855 assert_eq!(result.manifest_references[0].reference_index, 0);
1856 assert!(matches!(
1857 result.manifest_references[0].status,
1858 DsigStatus::Valid
1859 ));
1860 }
1861
1862 #[test]
1863 fn verify_context_skips_manifest_removed_by_enveloped_transform() {
1864 for target_object in [false, true] {
1868 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1869 let xml = xml.replacen(
1870 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
1871 r#"<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
1872 1,
1873 );
1874 if target_object {
1875 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
1876 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
1877 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
1878 } else {
1879 xml
1880 }
1881 });
1882
1883 let result = VerifyContext::new()
1884 .key(&AcceptingKey)
1885 .process_manifests(true)
1886 .store_pre_digest(true)
1887 .verify(&xml)
1888 .expect("an emptied reference remains a valid core digest input");
1889
1890 assert!(matches!(result.status, DsigStatus::Valid));
1891 assert_eq!(
1892 result.signed_info_references[0].pre_digest_data.as_deref(),
1893 Some([].as_slice()),
1894 "target_object={target_object} must have empty transformed bytes",
1895 );
1896 assert!(
1897 result.manifest_references.is_empty(),
1898 "target_object={target_object} must not authenticate the Manifest",
1899 );
1900 }
1901 }
1902
1903 #[test]
1904 fn verify_context_ignores_manifest_excluded_from_signed_object() {
1905 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1909 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
1910 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
1911 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
1912 .replacen(
1913 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
1914 r#"<ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>not(ancestor-or-self::ds:Manifest)</ds:XPath></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
1915 1,
1916 )
1917 });
1918
1919 let result = VerifyContext::new()
1920 .key(&AcceptingKey)
1921 .process_manifests(true)
1922 .verify(&xml)
1923 .expect("excluded Manifest content must be ignored, not parsed");
1924
1925 assert!(matches!(result.status, DsigStatus::Valid));
1926 assert!(
1927 result.manifest_references.is_empty(),
1928 "a transform-excluded Manifest is not authenticated by SignedInfo",
1929 );
1930 }
1931
1932 #[test]
1933 fn verify_context_skips_manifest_digest_work_when_signature_is_invalid() {
1934 let xml = signature_with_manifest_xml(false);
1935 let result = VerifyContext::new()
1936 .key(&RejectingKey)
1937 .process_manifests(true)
1938 .verify(&xml)
1939 .expect("invalid SignatureValue must short-circuit Manifest digest work");
1940 assert!(result.manifest_references.is_empty());
1941 assert!(matches!(
1942 result.status,
1943 DsigStatus::Invalid(FailureReason::SignatureMismatch)
1944 ));
1945 }
1946
1947 #[test]
1948 fn verify_context_manifest_digest_mismatch_is_non_fatal_with_accepting_key() {
1949 let xml = signature_with_manifest_xml(false);
1950 let result = VerifyContext::new()
1951 .key(&AcceptingKey)
1952 .process_manifests(true)
1953 .verify(&xml)
1954 .expect("manifest digest mismatches should be recorded while signature stays valid");
1955 assert_eq!(result.manifest_references.len(), 1);
1956 assert!(matches!(
1957 result.manifest_references[0].status,
1958 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1959 ));
1960 assert!(matches!(result.status, DsigStatus::Valid));
1961 }
1962
1963 #[test]
1964 fn verify_context_skips_manifest_parsing_when_signedinfo_reference_fails() {
1965 let xml = signature_with_manifest_xml(true);
1968 let (signed_info_prefix, object_suffix) = xml
1969 .split_once("<ds:Object>")
1970 .expect("fixture should contain ds:Object");
1971 let open = "<ds:DigestValue>";
1972 let close = "</ds:DigestValue>";
1973 let digest_start = signed_info_prefix
1974 .find(open)
1975 .expect("SignedInfo should contain DigestValue");
1976 let digest_end = signed_info_prefix[digest_start + open.len()..]
1977 .find(close)
1978 .map(|offset| digest_start + open.len() + offset)
1979 .expect("SignedInfo DigestValue must be closed");
1980 let broken_signed_info_prefix = format!(
1981 "{}{}AAAAAAAAAAAAAAAAAAAAAAAAAAA={}{}",
1982 &signed_info_prefix[..digest_start],
1983 open,
1984 close,
1985 &signed_info_prefix[digest_end + close.len()..],
1986 );
1987 let broken_xml = format!("{broken_signed_info_prefix}<ds:Object>{object_suffix}");
1988 let result = VerifyContext::new()
1989 .key(&RejectingKey)
1990 .process_manifests(true)
1991 .verify(&broken_xml)
1992 .expect("SignedInfo digest failure should return without parsing Manifests");
1993 assert!(matches!(
1994 result.status,
1995 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1996 ));
1997 assert!(
1998 result.manifest_references.is_empty(),
1999 "unauthenticated Manifest content must not be parsed",
2000 );
2001 }
2002
2003 #[test]
2004 fn verify_context_skips_manifest_policy_work_when_signature_is_invalid() {
2005 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2008 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
2009 });
2010 let result = VerifyContext::new()
2011 .key(&RejectingKey)
2012 .process_manifests(true)
2013 .verify(&broken_xml)
2014 .expect("invalid SignatureValue must short-circuit Manifest policy work");
2015 assert!(result.manifest_references.is_empty());
2016 assert!(matches!(
2017 result.status,
2018 DsigStatus::Invalid(FailureReason::SignatureMismatch)
2019 ));
2020 }
2021
2022 #[test]
2023 fn verify_context_records_manifest_policy_violations_with_accepting_key() {
2024 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2025 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
2026 });
2027 let result = VerifyContext::new()
2028 .key(&AcceptingKey)
2029 .process_manifests(true)
2030 .verify(&broken_xml)
2031 .expect("manifest policy violations should be recorded while signature stays valid");
2032 assert_eq!(result.manifest_references.len(), 1);
2033 assert!(matches!(
2034 result.manifest_references[0].status,
2035 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
2036 ));
2037 assert!(matches!(result.status, DsigStatus::Valid));
2038 }
2039
2040 #[test]
2041 fn verify_context_skips_manifest_uri_work_when_signature_is_invalid() {
2042 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2045 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
2046 });
2047
2048 let result = VerifyContext::new()
2049 .key(&RejectingKey)
2050 .process_manifests(true)
2051 .verify(&broken_xml)
2052 .expect("invalid SignatureValue must short-circuit Manifest URI processing");
2053 assert!(result.manifest_references.is_empty());
2054 assert!(matches!(
2055 result.status,
2056 DsigStatus::Invalid(FailureReason::SignatureMismatch)
2057 ));
2058 }
2059
2060 #[test]
2061 fn verify_context_records_manifest_missing_uri_with_accepting_key() {
2062 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2063 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
2064 });
2065
2066 let result = VerifyContext::new()
2067 .key(&AcceptingKey)
2068 .process_manifests(true)
2069 .verify(&broken_xml)
2070 .expect("manifest missing URI should be recorded while signature stays valid");
2071 assert_eq!(result.manifest_references.len(), 1);
2072 assert_eq!(result.manifest_references[0].uri, "<omitted>");
2073 assert!(matches!(
2074 result.manifest_references[0].status,
2075 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
2076 ));
2077 assert!(matches!(result.status, DsigStatus::Valid));
2078 }
2079
2080 #[test]
2081 fn verify_context_ignores_nested_manifests_in_object() {
2082 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2085 xml.replacen(
2086 "<ds:Manifest ID=\"manifest\">",
2087 "<wrapper><ds:Manifest ID=\"manifest\">",
2088 1,
2089 )
2090 .replacen("</ds:Manifest>", "</ds:Manifest></wrapper>", 1)
2091 });
2092
2093 let result = VerifyContext::new()
2094 .key(&AcceptingKey)
2095 .process_manifests(true)
2096 .verify(&xml)
2097 .expect("nested Manifest nodes are ignored in strict mode");
2098 assert!(
2099 result.manifest_references.is_empty(),
2100 "only direct ds:Manifest children of ds:Object must be processed"
2101 );
2102 assert!(matches!(result.status, DsigStatus::Valid));
2103 }
2104
2105 #[test]
2106 fn verify_context_reports_manifest_reference_parse_errors_explicitly() {
2107 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2110 replace_fixture_manifest_digest(&xml, "!!!")
2111 });
2112
2113 let err = VerifyContext::new()
2114 .key(&AcceptingKey)
2115 .process_manifests(true)
2116 .verify(&broken_xml)
2117 .expect_err("invalid Manifest DigestValue must map to ParseManifestReference");
2118 assert!(matches!(
2119 err,
2120 SignatureVerificationPipelineError::ParseManifestReference(_)
2121 ));
2122 }
2123
2124 #[test]
2125 fn verify_context_rejects_manifest_non_whitespace_mixed_content() {
2126 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2129 xml.replacen(
2130 "<ds:Manifest ID=\"manifest\">",
2131 "<ds:Manifest ID=\"manifest\">junk",
2132 1,
2133 )
2134 });
2135
2136 let err = VerifyContext::new()
2137 .key(&AcceptingKey)
2138 .process_manifests(true)
2139 .verify(&xml)
2140 .expect_err("Manifest mixed content must fail verification");
2141 assert!(matches!(
2142 err,
2143 SignatureVerificationPipelineError::InvalidStructure {
2144 reason: "Manifest contains non-whitespace mixed content"
2145 }
2146 ));
2147 }
2148
2149 #[test]
2150 fn verify_context_rejects_empty_manifest_children() {
2151 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2154 let (prefix, rest) = xml
2155 .split_once("<ds:Manifest ID=\"manifest\">")
2156 .expect("fixture should contain Manifest");
2157 let (_, suffix) = rest
2158 .split_once("</ds:Manifest>")
2159 .expect("fixture should contain closing Manifest");
2160 format!("{prefix}<ds:Manifest ID=\"manifest\"></ds:Manifest>{suffix}")
2161 });
2162
2163 let err = VerifyContext::new()
2164 .key(&AcceptingKey)
2165 .process_manifests(true)
2166 .verify(&xml)
2167 .expect_err("empty Manifest must fail verification");
2168 assert!(matches!(
2169 err,
2170 SignatureVerificationPipelineError::InvalidStructure {
2171 reason: "Manifest must contain at least one ds:Reference element child"
2172 }
2173 ));
2174 }
2175
2176 #[test]
2177 fn verify_context_ignores_unsigned_malformed_manifest_blocks() {
2178 let xml = signature_with_manifest_xml(true).replacen(
2179 "</ds:Object>",
2180 "</ds:Object><ds:Object><ds:Manifest>junk<ds:Foo/></ds:Manifest></ds:Object>",
2181 1,
2182 );
2183 let result = VerifyContext::new()
2184 .key(&AcceptingKey)
2185 .process_manifests(true)
2186 .verify(&xml)
2187 .expect("unsigned malformed Manifest must be ignored");
2188 assert_eq!(
2189 result.manifest_references.len(),
2190 1,
2191 "only signed Manifest references must be reported",
2192 );
2193 assert!(matches!(result.status, DsigStatus::Valid));
2194 }
2195
2196 #[test]
2197 fn verify_context_skips_ambiguous_manifest_id_blocks() {
2198 let xml = signature_with_manifest_xml(true).replacen(
2199 "</ds:Object>",
2200 "</ds:Object><ds:Object><ds:Manifest ID=\"manifest\">junk<ds:Foo/></ds:Manifest></ds:Object>",
2201 1,
2202 );
2203 let err = VerifyContext::new()
2204 .key(&RejectingKey)
2205 .process_manifests(true)
2206 .verify(&xml)
2207 .expect_err("ambiguous manifest IDs should make SignedInfo #manifest dereference fail");
2208 assert!(matches!(
2209 err,
2210 SignatureVerificationPipelineError::Reference(
2211 ReferenceProcessingError::UriDereference(
2212 crate::xmldsig::types::TransformError::ElementNotFound(id)
2213 )
2214 ) if id == "manifest"
2215 ));
2216 }
2217
2218 #[test]
2219 fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
2220 let xml = minimal_signature_xml("", "");
2221 let err = VerifyContext::new()
2222 .key(&RejectingKey)
2223 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
2224 .verify(&xml)
2225 .expect_err("implicit default C14N must be checked against allowlist");
2226 assert!(matches!(
2227 err,
2228 SignatureVerificationPipelineError::DisallowedTransform { .. }
2229 ));
2230 }
2231
2232 #[test]
2233 fn verify_context_skips_resolver_when_reference_processing_fails() {
2234 let xml = minimal_signature_xml("", "");
2235 let result = VerifyContext::new()
2236 .key_resolver(&PanicResolver)
2237 .verify(&xml)
2238 .expect("reference digest mismatch should short-circuit before resolver");
2239 assert!(matches!(
2240 result.status,
2241 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
2242 ));
2243 }
2244
2245 #[test]
2246 fn verify_context_reports_key_not_found_when_resolver_misses() {
2247 let xml = signature_with_target_reference("AQ==");
2248 let result = VerifyContext::new()
2249 .key_resolver(&MissingKeyResolver)
2250 .verify(&xml)
2251 .expect("resolver miss should report status, not pipeline error");
2252 assert!(matches!(
2253 result.status,
2254 DsigStatus::Invalid(FailureReason::KeyNotFound)
2255 ));
2256 assert_eq!(
2257 result.signed_info_references.len(),
2258 1,
2259 "KeyNotFound path must preserve SignedInfo reference diagnostics",
2260 );
2261 assert!(matches!(
2262 result.signed_info_references[0].status,
2263 DsigStatus::Valid
2264 ));
2265 }
2266
2267 #[test]
2268 fn verify_context_resolver_can_ignore_malformed_keyinfo_by_default() {
2269 let base_xml = signature_with_target_reference("AQ==");
2270 let xml = base_xml
2271 .replace(
2272 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
2273 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
2274 )
2275 .replace(
2276 "</ds:SignatureValue>\n </ds:Signature>",
2277 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
2278 );
2279
2280 let result = VerifyContext::new()
2281 .key_resolver(&MissingKeyResolver)
2282 .verify(&xml)
2283 .expect("resolver path should not hard-fail on advisory malformed KeyInfo by default");
2284 assert!(matches!(
2285 result.status,
2286 DsigStatus::Invalid(FailureReason::KeyNotFound)
2287 ));
2288 }
2289
2290 #[test]
2291 fn verify_context_resolver_can_opt_in_to_keyinfo_parse_failures() {
2292 let base_xml = signature_with_target_reference("AQ==");
2293 let xml = base_xml
2294 .replace(
2295 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
2296 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
2297 )
2298 .replace(
2299 "</ds:SignatureValue>\n </ds:Signature>",
2300 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
2301 );
2302
2303 let err = VerifyContext::new()
2304 .key_resolver(&ConsumingKeyInfoResolver)
2305 .verify(&xml)
2306 .expect_err("resolver opted into KeyInfo parsing, malformed KeyInfo must fail");
2307 assert!(matches!(
2308 err,
2309 SignatureVerificationPipelineError::ParseKeyInfo(_)
2310 ));
2311 }
2312
2313 #[test]
2314 fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
2315 let xml = signature_with_target_reference("@@@");
2316
2317 let err = VerifyContext::new()
2318 .key_resolver(&MissingKeyResolver)
2319 .verify(&xml)
2320 .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
2321 assert!(matches!(
2322 err,
2323 SignatureVerificationPipelineError::SignatureValueBase64(_)
2324 ));
2325 }
2326
2327 #[test]
2328 fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
2329 let xml = signature_with_target_reference("@@@");
2330
2331 let err = VerifyContext::new()
2332 .verify(&xml)
2333 .expect_err("invalid SignatureValue must remain a decode error");
2334 assert!(matches!(
2335 err,
2336 SignatureVerificationPipelineError::SignatureValueBase64(_)
2337 ));
2338 }
2339
2340 #[test]
2341 fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
2342 let references = vec![Reference {
2343 uri: None,
2344 id: None,
2345 ref_type: None,
2346 transforms: vec![],
2347 digest_method: DigestAlgorithm::Sha256,
2348 digest_value: vec![0; 32],
2349 }];
2350 let uri_types = UriTypeSet {
2351 allow_empty: false,
2352 allow_same_document: true,
2353 allow_external: false,
2354 };
2355
2356 let err = enforce_reference_policies(&references, uri_types, None)
2357 .expect_err("missing URI must fail before allow_empty policy is evaluated");
2358 assert!(matches!(
2359 err,
2360 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
2361 ));
2362 }
2363
2364 #[test]
2365 fn enforce_reference_policies_checks_only_terminal_binary_output() {
2366 let c14n = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI).unwrap();
2367 let allowed = HashSet::from([
2368 BASE64_TRANSFORM_URI.to_owned(),
2369 DEFAULT_IMPLICIT_C14N_URI.to_owned(),
2370 ]);
2371 let without_implicit_c14n = HashSet::from([BASE64_TRANSFORM_URI.to_owned()]);
2372
2373 for transforms in [
2374 vec![Transform::Base64Decode, Transform::C14n(c14n)],
2375 vec![Transform::Base64Decode, Transform::Base64Decode],
2376 ] {
2377 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, vec![0; 32]);
2378 enforce_reference_policies(
2379 std::slice::from_ref(&reference),
2380 UriTypeSet::default(),
2381 Some(&allowed),
2382 )
2383 .expect("terminal binary output must not require implicit C14N");
2384 }
2385
2386 let terminal_base64 = make_reference(
2387 "",
2388 vec![Transform::Base64Decode, Transform::Base64Decode],
2389 DigestAlgorithm::Sha256,
2390 vec![0; 32],
2391 );
2392 enforce_reference_policies(
2393 std::slice::from_ref(&terminal_base64),
2394 UriTypeSet::default(),
2395 Some(&without_implicit_c14n),
2396 )
2397 .expect("terminal Base64 output must not require implicit C14N");
2398
2399 let no_transforms = make_reference("", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
2400 let error = enforce_reference_policies(
2401 std::slice::from_ref(&no_transforms),
2402 UriTypeSet::default(),
2403 Some(&without_implicit_c14n),
2404 )
2405 .expect_err("a node-set result must require allowlisted implicit C14N");
2406 assert!(matches!(
2407 error,
2408 SignatureVerificationPipelineError::DisallowedTransform { ref algorithm }
2409 if algorithm == DEFAULT_IMPLICIT_C14N_URI
2410 ));
2411 }
2412
2413 #[test]
2414 fn push_normalized_signature_text_rejects_form_feed() {
2415 let mut normalized = Vec::new();
2416 let mut raw_text_len = 0usize;
2417 let err =
2418 push_normalized_signature_text("ab\u{000C}cd", &mut raw_text_len, &mut normalized)
2419 .expect_err("form-feed must not be treated as XML base64 whitespace");
2420 assert!(matches!(
2421 err,
2422 SignatureVerificationPipelineError::SignatureValueBase64(
2423 base64::DecodeError::InvalidByte(_, 0x0C)
2424 )
2425 ));
2426 }
2427
2428 #[test]
2429 fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
2430 let mut normalized = vec![b'A'; MAX_SIGNATURE_VALUE_LEN - 1];
2431 let mut raw_text_len = normalized.len();
2432 let err = push_normalized_signature_text("é", &mut raw_text_len, &mut normalized)
2433 .expect_err("multibyte characters must not bypass byte-size limit");
2434 assert!(matches!(
2435 err,
2436 SignatureVerificationPipelineError::InvalidStructure {
2437 reason: "SignatureValue exceeds maximum allowed length"
2438 }
2439 ));
2440 }
2441
2442 #[test]
2445 fn reference_with_correct_digest_passes() {
2446 let xml = r##"<root>
2449 <data>hello world</data>
2450 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
2451 <ds:SignedInfo/>
2452 </ds:Signature>
2453 </root>"##;
2454 let doc = Document::parse(xml).unwrap();
2455 let resolver = UriReferenceResolver::new(&doc);
2456 let sig_node = doc
2457 .descendants()
2458 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2459 .unwrap();
2460
2461 let initial_data = resolver.dereference("").unwrap();
2463 let transforms = vec![
2464 Transform::Enveloped,
2465 Transform::C14n(
2466 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
2467 .unwrap(),
2468 ),
2469 ];
2470 let pre_digest_bytes =
2471 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
2472 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
2473
2474 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
2476
2477 let result = process_reference(
2478 &reference,
2479 &resolver,
2480 sig_node,
2481 ReferenceSet::SignedInfo,
2482 0,
2483 false,
2484 )
2485 .unwrap();
2486 assert!(
2487 matches!(result.status, DsigStatus::Valid),
2488 "digest should match"
2489 );
2490 assert!(result.pre_digest_data.is_none());
2491 }
2492
2493 #[test]
2494 fn reference_with_wrong_digest_fails() {
2495 let xml = r##"<root>
2496 <data>hello</data>
2497 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2498 <ds:SignedInfo/>
2499 </ds:Signature>
2500 </root>"##;
2501 let doc = Document::parse(xml).unwrap();
2502 let resolver = UriReferenceResolver::new(&doc);
2503 let sig_node = doc
2504 .descendants()
2505 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2506 .unwrap();
2507
2508 let transforms = vec![Transform::Enveloped];
2509 let wrong_digest = vec![0u8; 32];
2511 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
2512
2513 let result = process_reference(
2514 &reference,
2515 &resolver,
2516 sig_node,
2517 ReferenceSet::SignedInfo,
2518 0,
2519 false,
2520 )
2521 .unwrap();
2522 assert!(matches!(
2523 result.status,
2524 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
2525 ));
2526 }
2527
2528 #[test]
2529 fn reference_with_wrong_digest_preserves_supplied_ref_index() {
2530 let xml = r##"<root>
2531 <data>hello</data>
2532 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2533 <ds:SignedInfo/>
2534 </ds:Signature>
2535 </root>"##;
2536 let doc = Document::parse(xml).unwrap();
2537 let resolver = UriReferenceResolver::new(&doc);
2538 let sig_node = doc
2539 .descendants()
2540 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2541 .unwrap();
2542
2543 let reference = make_reference(
2544 "",
2545 vec![Transform::Enveloped],
2546 DigestAlgorithm::Sha256,
2547 vec![0u8; 32],
2548 );
2549 let result = process_reference(
2550 &reference,
2551 &resolver,
2552 sig_node,
2553 ReferenceSet::SignedInfo,
2554 7,
2555 false,
2556 )
2557 .unwrap();
2558 assert!(matches!(
2559 result.status,
2560 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
2561 ));
2562 }
2563
2564 #[test]
2565 fn reference_stores_pre_digest_data() {
2566 let xml = "<root><child>text</child></root>";
2567 let doc = Document::parse(xml).unwrap();
2568 let resolver = UriReferenceResolver::new(&doc);
2569
2570 let initial_data = resolver.dereference("").unwrap();
2572 let pre_digest =
2573 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2574 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2575
2576 let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
2577 let result = process_reference(
2578 &reference,
2579 &resolver,
2580 doc.root_element(),
2581 ReferenceSet::SignedInfo,
2582 0,
2583 true,
2584 )
2585 .unwrap();
2586
2587 assert!(matches!(result.status, DsigStatus::Valid));
2588 assert!(result.pre_digest_data.is_some());
2589 assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
2590 }
2591
2592 #[test]
2595 fn reference_with_id_uri() {
2596 let xml = r##"<root>
2597 <item ID="target">specific content</item>
2598 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2599 <ds:SignedInfo/>
2600 </ds:Signature>
2601 </root>"##;
2602 let doc = Document::parse(xml).unwrap();
2603 let resolver = UriReferenceResolver::new(&doc);
2604 let sig_node = doc
2605 .descendants()
2606 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2607 .unwrap();
2608
2609 let initial_data = resolver.dereference("#target").unwrap();
2611 let transforms = vec![Transform::C14n(
2612 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
2613 .unwrap(),
2614 )];
2615 let pre_digest =
2616 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
2617 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2618
2619 let reference = make_reference(
2620 "#target",
2621 transforms,
2622 DigestAlgorithm::Sha256,
2623 expected_digest,
2624 );
2625 let result = process_reference(
2626 &reference,
2627 &resolver,
2628 sig_node,
2629 ReferenceSet::SignedInfo,
2630 0,
2631 false,
2632 )
2633 .unwrap();
2634 assert!(matches!(result.status, DsigStatus::Valid));
2635 }
2636
2637 #[test]
2638 fn reference_with_nonexistent_id_fails() {
2639 let xml = "<root><child/></root>";
2640 let doc = Document::parse(xml).unwrap();
2641 let resolver = UriReferenceResolver::new(&doc);
2642
2643 let reference =
2644 make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
2645 let result = process_reference(
2646 &reference,
2647 &resolver,
2648 doc.root_element(),
2649 ReferenceSet::SignedInfo,
2650 0,
2651 false,
2652 );
2653 assert!(result.is_err());
2654 }
2655
2656 #[test]
2657 fn reference_with_absent_uri_fails_closed() {
2658 let xml = "<root><child>text</child></root>";
2659 let doc = Document::parse(xml).unwrap();
2660 let resolver = UriReferenceResolver::new(&doc);
2661
2662 let reference = Reference {
2663 uri: None, id: None,
2665 ref_type: None,
2666 transforms: vec![],
2667 digest_method: DigestAlgorithm::Sha256,
2668 digest_value: vec![0; 32],
2669 };
2670
2671 let result = process_reference(
2672 &reference,
2673 &resolver,
2674 doc.root_element(),
2675 ReferenceSet::SignedInfo,
2676 0,
2677 false,
2678 );
2679 assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
2680 }
2681
2682 #[test]
2685 fn all_references_pass() {
2686 let xml = "<root><child>text</child></root>";
2687 let doc = Document::parse(xml).unwrap();
2688 let resolver = UriReferenceResolver::new(&doc);
2689
2690 let initial_data = resolver.dereference("").unwrap();
2692 let pre_digest =
2693 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2694 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2695
2696 let refs = vec![
2697 make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
2698 make_reference("", vec![], DigestAlgorithm::Sha256, digest),
2699 ];
2700
2701 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
2702 assert!(result.all_valid());
2703 assert_eq!(result.results.len(), 2);
2704 assert!(result.first_failure.is_none());
2705 }
2706
2707 #[test]
2708 fn reference_processing_shares_xpath_work_across_references() {
2709 let document = Document::parse("<root/>").unwrap();
2712 let resolver = UriReferenceResolver::new(&document);
2713 let transform = Transform::XPath(super::super::transforms::XPathExpression::new("true()"));
2714 let initial_data = resolver.dereference("").unwrap();
2715 let pre_digest = crate::xmldsig::execute_transforms(
2716 document.root_element(),
2717 initial_data,
2718 std::slice::from_ref(&transform),
2719 )
2720 .unwrap();
2721 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2722 let references = vec![
2723 make_reference(
2724 "",
2725 vec![transform.clone()],
2726 DigestAlgorithm::Sha256,
2727 digest.clone(),
2728 ),
2729 make_reference("", vec![transform], DigestAlgorithm::Sha256, digest),
2730 ];
2731 let budget = TransformExecutionBudget::with_xpath_limit(12);
2732 let execution = ReferenceExecutionContext {
2733 store_pre_digest: false,
2734 transform_options: TransformOptions::default(),
2735 transform_budget: &budget,
2736 };
2737
2738 let error = process_all_references_with_options(
2739 &references,
2740 &resolver,
2741 document.root_element(),
2742 &execution,
2743 )
2744 .expect_err("the second Reference must consume the first Reference's XPath work");
2745
2746 assert!(error.to_string().contains("signature-wide"));
2747 }
2748
2749 #[test]
2750 fn reference_processing_shares_node_set_materialization_across_references() {
2751 let document = Document::parse(
2755 r#"<root xmlns:n="urn:0123456789"><target Id="selected">payload</target></root>"#,
2756 )
2757 .unwrap();
2758 let resolver = UriReferenceResolver::new(&document);
2759 let initial_data = resolver.dereference("#selected").unwrap();
2760 let pre_digest =
2761 crate::xmldsig::execute_transforms(document.root_element(), initial_data, &[]).unwrap();
2762 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2763 let references = vec![
2764 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest.clone()),
2765 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest),
2766 ];
2767 let budget = TransformExecutionBudget::with_node_set_materialization_limit(30);
2768 let execution = ReferenceExecutionContext {
2769 store_pre_digest: false,
2770 transform_options: TransformOptions::default(),
2771 transform_budget: &budget,
2772 };
2773
2774 let error = process_all_references_with_options(
2775 &references,
2776 &resolver,
2777 document.root_element(),
2778 &execution,
2779 )
2780 .expect_err("the second Reference must consume the first Reference's materialization work");
2781
2782 assert!(error.to_string().contains("cumulative owned string bytes"));
2783 }
2784
2785 #[test]
2786 fn fail_fast_on_first_mismatch() {
2787 let xml = "<root><child>text</child></root>";
2788 let doc = Document::parse(xml).unwrap();
2789 let resolver = UriReferenceResolver::new(&doc);
2790
2791 let wrong_digest = vec![0u8; 32];
2792 let refs = vec![
2793 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
2794 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
2796 ];
2797
2798 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
2799 assert!(!result.all_valid());
2800 assert_eq!(result.first_failure, Some(0));
2801 assert_eq!(result.results.len(), 1);
2803 assert!(matches!(
2804 result.results[0].status,
2805 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
2806 ));
2807 }
2808
2809 #[test]
2810 fn fail_fast_second_reference() {
2811 let xml = "<root><child>text</child></root>";
2812 let doc = Document::parse(xml).unwrap();
2813 let resolver = UriReferenceResolver::new(&doc);
2814
2815 let initial_data = resolver.dereference("").unwrap();
2817 let pre_digest =
2818 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2819 let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2820 let wrong_digest = vec![0u8; 32];
2821
2822 let refs = vec![
2823 make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
2824 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
2825 ];
2826
2827 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
2828 assert!(!result.all_valid());
2829 assert_eq!(result.first_failure, Some(1));
2830 assert_eq!(result.results.len(), 2);
2832 assert!(matches!(result.results[0].status, DsigStatus::Valid));
2833 assert!(matches!(
2834 result.results[1].status,
2835 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
2836 ));
2837 }
2838
2839 #[test]
2840 fn empty_references_list() {
2841 let xml = "<root/>";
2842 let doc = Document::parse(xml).unwrap();
2843 let resolver = UriReferenceResolver::new(&doc);
2844
2845 let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
2846 assert!(result.all_valid());
2847 assert!(result.results.is_empty());
2848 }
2849
2850 #[test]
2853 fn reference_sha1_digest() {
2854 let xml = "<root>content</root>";
2855 let doc = Document::parse(xml).unwrap();
2856 let resolver = UriReferenceResolver::new(&doc);
2857
2858 let initial_data = resolver.dereference("").unwrap();
2859 let pre_digest =
2860 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2861 let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
2862
2863 let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
2864 let result = process_reference(
2865 &reference,
2866 &resolver,
2867 doc.root_element(),
2868 ReferenceSet::SignedInfo,
2869 0,
2870 false,
2871 )
2872 .unwrap();
2873 assert!(matches!(result.status, DsigStatus::Valid));
2874 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
2875 }
2876
2877 #[test]
2878 fn reference_sha512_digest() {
2879 let xml = "<root>content</root>";
2880 let doc = Document::parse(xml).unwrap();
2881 let resolver = UriReferenceResolver::new(&doc);
2882
2883 let initial_data = resolver.dereference("").unwrap();
2884 let pre_digest =
2885 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2886 let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
2887
2888 let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
2889 let result = process_reference(
2890 &reference,
2891 &resolver,
2892 doc.root_element(),
2893 ReferenceSet::SignedInfo,
2894 0,
2895 false,
2896 )
2897 .unwrap();
2898 assert!(matches!(result.status, DsigStatus::Valid));
2899 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
2900 }
2901
2902 #[test]
2905 fn saml_enveloped_reference_processing() {
2906 let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
2908 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
2909 ID="_resp1">
2910 <saml:Assertion ID="_assert1">
2911 <saml:Subject>user@example.com</saml:Subject>
2912 </saml:Assertion>
2913 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2914 <ds:SignedInfo>
2915 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2916 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2917 <ds:Reference URI="">
2918 <ds:Transforms>
2919 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
2920 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2921 </ds:Transforms>
2922 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2923 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
2924 </ds:Reference>
2925 </ds:SignedInfo>
2926 <ds:SignatureValue>fakesig==</ds:SignatureValue>
2927 </ds:Signature>
2928 </samlp:Response>"##;
2929 let doc = Document::parse(xml).unwrap();
2930 let resolver = UriReferenceResolver::new(&doc);
2931 let sig_node = doc
2932 .descendants()
2933 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2934 .unwrap();
2935
2936 let signed_info_node = sig_node
2938 .children()
2939 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
2940 .unwrap();
2941 let signed_info = parse_signed_info(signed_info_node).unwrap();
2942 let reference = &signed_info.references[0];
2943
2944 let initial_data = resolver.dereference("").unwrap();
2946 let pre_digest =
2947 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
2948 .unwrap();
2949 let correct_digest = compute_digest(reference.digest_method, &pre_digest);
2950
2951 let corrected_ref = make_reference(
2953 "",
2954 reference.transforms.clone(),
2955 reference.digest_method,
2956 correct_digest,
2957 );
2958
2959 let result = process_reference(
2961 &corrected_ref,
2962 &resolver,
2963 sig_node,
2964 ReferenceSet::SignedInfo,
2965 0,
2966 true,
2967 )
2968 .unwrap();
2969 assert!(
2970 matches!(result.status, DsigStatus::Valid),
2971 "SAML reference should verify"
2972 );
2973 assert!(result.pre_digest_data.is_some());
2974
2975 let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
2977 assert!(
2978 pre_digest_str.contains("samlp:Response"),
2979 "pre-digest should contain Response"
2980 );
2981 assert!(
2982 !pre_digest_str.contains("SignatureValue"),
2983 "pre-digest should NOT contain Signature"
2984 );
2985 }
2986
2987 #[test]
2988 fn pipeline_missing_signed_info_returns_missing_element() {
2989 let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
2990
2991 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
2992 .expect_err("missing SignedInfo must fail before crypto stage");
2993 assert!(matches!(
2994 err,
2995 SignatureVerificationPipelineError::MissingElement {
2996 element: "SignedInfo"
2997 }
2998 ));
2999 }
3000
3001 #[test]
3002 fn pipeline_multiple_signature_elements_are_rejected() {
3003 let xml = r#"
3004<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3005 <ds:Signature>
3006 <ds:SignedInfo/>
3007 </ds:Signature>
3008 <ds:Signature/>
3009</root>
3010"#;
3011
3012 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
3013 .expect_err("multiple signatures must fail closed");
3014 assert!(matches!(
3015 err,
3016 SignatureVerificationPipelineError::InvalidStructure {
3017 reason: "Signature must appear exactly once in document",
3018 }
3019 ));
3020 }
3021
3022 #[test]
3023 fn pipeline_reports_keyinfo_parse_error() {
3024 let xml = r#"
3025<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
3026 xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
3027 <ds:SignedInfo>
3028 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3029 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3030 <ds:Reference URI="">
3031 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3032 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
3033 </ds:Reference>
3034 </ds:SignedInfo>
3035 <ds:SignatureValue>AA==</ds:SignatureValue>
3036 <ds:KeyInfo>
3037 <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
3038 </ds:KeyInfo>
3039</ds:Signature>
3040"#;
3041
3042 let err = VerifyContext::new().verify(xml).expect_err(
3043 "invalid KeyInfo must map to ParseKeyInfo when no explicit key is supplied",
3044 );
3045 assert!(matches!(
3046 err,
3047 SignatureVerificationPipelineError::ParseKeyInfo(_)
3048 ));
3049 }
3050
3051 #[test]
3052 fn pipeline_ignores_malformed_keyinfo_when_explicit_key_is_supplied() {
3053 let base_xml = signature_with_target_reference("AQ==");
3054 let xml = base_xml
3055 .replace(
3056 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
3057 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
3058 )
3059 .replace(
3060 "</ds:SignatureValue>\n </ds:Signature>",
3061 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
3062 );
3063
3064 let result = VerifyContext::new()
3065 .key(&RejectingKey)
3066 .verify(&xml)
3067 .expect("explicit key path should not fail on malformed KeyInfo");
3068 assert!(matches!(
3069 result.status,
3070 DsigStatus::Invalid(FailureReason::SignatureMismatch)
3071 ));
3072 }
3073
3074 #[test]
3075 fn pipeline_rejects_foreign_element_children_under_signature() {
3076 let base_xml = signature_with_target_reference("AQ==");
3077 let xml = base_xml
3078 .replace(
3079 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
3080 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:foo="urn:example:foo">"#,
3081 )
3082 .replace(
3083 "</ds:SignedInfo>\n <ds:SignatureValue>",
3084 "</ds:SignedInfo>\n <foo:Bar/>\n <ds:SignatureValue>",
3085 );
3086
3087 let err = VerifyContext::new()
3088 .key(&RejectingKey)
3089 .verify(&xml)
3090 .expect_err("foreign element children under Signature must fail closed");
3091 assert!(matches!(
3092 err,
3093 SignatureVerificationPipelineError::InvalidStructure {
3094 reason: "Signature must contain only XMLDSIG element children",
3095 }
3096 ));
3097 }
3098
3099 #[test]
3100 fn pipeline_rejects_non_whitespace_mixed_content_under_signature() {
3101 let base_xml = signature_with_target_reference("AQ==");
3102 let xml = base_xml.replace(
3103 "</ds:SignedInfo>\n <ds:SignatureValue>",
3104 "</ds:SignedInfo>\n oops\n <ds:SignatureValue>",
3105 );
3106
3107 let err = VerifyContext::new()
3108 .key(&RejectingKey)
3109 .verify(&xml)
3110 .expect_err("non-whitespace mixed content under Signature must fail closed");
3111 assert!(matches!(
3112 err,
3113 SignatureVerificationPipelineError::InvalidStructure {
3114 reason: "Signature must not contain non-whitespace mixed content",
3115 }
3116 ));
3117 }
3118
3119 #[test]
3120 fn pipeline_rejects_keyinfo_out_of_order() {
3121 let base_xml = signature_with_target_reference("AQ==");
3122 let xml = base_xml.replace(
3123 "</ds:SignatureValue>\n </ds:Signature>",
3124 "</ds:SignatureValue>\n <ds:Object/>\n <ds:KeyInfo><ds:KeyName>late</ds:KeyName></ds:KeyInfo>\n </ds:Signature>",
3125 );
3126
3127 let err = VerifyContext::new()
3128 .key(&RejectingKey)
3129 .verify(&xml)
3130 .expect_err("KeyInfo after Object must be rejected by Signature child order checks");
3131 assert!(matches!(
3132 err,
3133 SignatureVerificationPipelineError::InvalidStructure {
3134 reason: "KeyInfo must be the third element child of Signature when present"
3135 }
3136 ));
3137 }
3138
3139 #[test]
3140 fn pipeline_accepts_comments_and_processing_instructions_under_signature() {
3141 let xml = r#"
3142<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3143 <?dbg keep ?>
3144 <!-- signature metadata -->
3145 <ds:SignedInfo>
3146 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3147 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3148 <ds:Reference URI="">
3149 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3150 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
3151 </ds:Reference>
3152 </ds:SignedInfo>
3153 <!-- between required children -->
3154 <ds:SignatureValue>AA==</ds:SignatureValue>
3155</ds:Signature>
3156"#;
3157
3158 let doc = Document::parse(xml).expect("test XML must parse");
3159 let signature_node = doc.root_element();
3160 let parsed = parse_signature_children(signature_node)
3161 .expect("comment/PI nodes under Signature must be ignored");
3162
3163 assert_eq!(parsed.signed_info_node.tag_name().name(), "SignedInfo");
3164 assert_eq!(
3165 parsed.signature_value_node.tag_name().name(),
3166 "SignatureValue"
3167 );
3168 assert!(parsed.key_info_node.is_none());
3169 }
3170}