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::{ParseError, Reference, SignatureAlgorithm, XMLDSIG_NS};
21use super::parse::{parse_reference, parse_signed_info};
22use super::signature::{
23 SignatureVerificationError, verify_ecdsa_signature_pem, verify_rsa_signature_pem,
24};
25use super::transforms::{
26 DEFAULT_IMPLICIT_C14N_URI, Transform, XPATH_TRANSFORM_URI, execute_transforms,
27};
28use super::uri::{UriReferenceResolver, parse_xpointer_id_fragment};
29
30const MAX_SIGNATURE_VALUE_LEN: usize = 8192;
31const MAX_SIGNATURE_VALUE_TEXT_LEN: usize = 65_536;
32pub trait VerifyingKey {
37 fn verify(
39 &self,
40 algorithm: SignatureAlgorithm,
41 signed_data: &[u8],
42 signature_value: &[u8],
43 ) -> Result<bool, DsigError>;
44}
45
46pub trait KeyResolver {
51 fn resolve<'a>(&'a self, xml: &str) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError>;
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66#[must_use = "pass the policy to VerifyContext::allowed_uri_types(), or store it for reuse"]
67pub struct UriTypeSet {
68 allow_empty: bool,
69 allow_same_document: bool,
70 allow_external: bool,
71}
72
73impl UriTypeSet {
74 pub const fn new(allow_empty: bool, allow_same_document: bool, allow_external: bool) -> Self {
76 Self {
77 allow_empty,
78 allow_same_document,
79 allow_external,
80 }
81 }
82
83 pub const SAME_DOCUMENT: Self = Self {
85 allow_empty: true,
86 allow_same_document: true,
87 allow_external: false,
88 };
89
90 pub const ALL: Self = Self {
95 allow_empty: true,
96 allow_same_document: true,
97 allow_external: true,
98 };
99
100 fn allows(self, uri: &str) -> bool {
101 if uri.is_empty() {
102 return self.allow_empty;
103 }
104 if uri.starts_with('#') {
105 return self.allow_same_document;
106 }
107 self.allow_external
108 }
109}
110
111impl Default for UriTypeSet {
112 fn default() -> Self {
113 Self::SAME_DOCUMENT
114 }
115}
116
117#[must_use = "configure the context and call verify(), or store it for reuse"]
119pub struct VerifyContext<'a> {
120 key: Option<&'a dyn VerifyingKey>,
121 key_resolver: Option<&'a dyn KeyResolver>,
122 process_manifests: bool,
123 allowed_uri_types: UriTypeSet,
124 allowed_transforms: Option<HashSet<String>>,
125 store_pre_digest: bool,
126}
127
128impl<'a> VerifyContext<'a> {
129 pub fn new() -> Self {
138 Self {
139 key: None,
140 key_resolver: None,
141 process_manifests: false,
142 allowed_uri_types: UriTypeSet::default(),
143 allowed_transforms: None,
144 store_pre_digest: false,
145 }
146 }
147
148 pub fn key(mut self, key: &'a dyn VerifyingKey) -> Self {
150 self.key = Some(key);
151 self
152 }
153
154 pub fn key_resolver(mut self, resolver: &'a dyn KeyResolver) -> Self {
156 self.key_resolver = Some(resolver);
157 self
158 }
159
160 pub fn process_manifests(mut self, enabled: bool) -> Self {
186 self.process_manifests = enabled;
187 self
188 }
189
190 pub fn allowed_uri_types(mut self, types: UriTypeSet) -> Self {
192 self.allowed_uri_types = types;
193 self
194 }
195
196 pub fn allowed_transforms<I, S>(mut self, transforms: I) -> Self
207 where
208 I: IntoIterator<Item = S>,
209 S: Into<String>,
210 {
211 self.allowed_transforms = Some(transforms.into_iter().map(Into::into).collect());
212 self
213 }
214
215 pub fn store_pre_digest(mut self, enabled: bool) -> Self {
217 self.store_pre_digest = enabled;
218 self
219 }
220
221 fn allowed_transform_uris(&self) -> Option<&HashSet<String>> {
222 self.allowed_transforms.as_ref()
223 }
224
225 pub fn verify(&self, xml: &str) -> Result<VerifyResult, DsigError> {
231 verify_signature_with_context(xml, self)
232 }
233}
234
235impl Default for VerifyContext<'_> {
236 fn default() -> Self {
237 Self::new()
238 }
239}
240
241#[derive(Debug)]
243#[non_exhaustive]
244#[must_use = "inspect status before accepting the reference result"]
245pub struct ReferenceResult {
246 pub reference_set: ReferenceSet,
248 pub reference_index: usize,
250 pub uri: String,
252 pub digest_algorithm: DigestAlgorithm,
254 pub status: DsigStatus,
256 pub pre_digest_data: Option<Vec<u8>>,
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
262#[non_exhaustive]
263pub enum ReferenceSet {
264 SignedInfo,
266 Manifest,
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
272#[non_exhaustive]
273pub enum DsigStatus {
274 Valid,
276 Invalid(FailureReason),
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282#[non_exhaustive]
283pub enum FailureReason {
284 ReferenceDigestMismatch {
286 ref_index: usize,
296 },
297 ReferencePolicyViolation {
299 ref_index: usize,
301 },
302 ReferenceProcessingFailure {
304 ref_index: usize,
306 },
307 SignatureMismatch,
309 KeyNotFound,
311}
312
313#[derive(Debug)]
315#[non_exhaustive]
316#[must_use = "check first_failure/results before accepting the reference set"]
317pub struct ReferencesResult {
318 pub results: Vec<ReferenceResult>,
321 pub first_failure: Option<usize>,
323}
324
325impl ReferencesResult {
326 #[must_use]
328 pub fn all_valid(&self) -> bool {
329 self.results
330 .iter()
331 .all(|result| matches!(result.status, DsigStatus::Valid))
332 }
333}
334
335pub fn process_reference(
353 reference: &Reference,
354 resolver: &UriReferenceResolver<'_>,
355 signature_node: Node<'_, '_>,
356 reference_set: ReferenceSet,
357 reference_index: usize,
358 store_pre_digest: bool,
359) -> Result<ReferenceResult, ReferenceProcessingError> {
360 let uri = reference
363 .uri
364 .as_deref()
365 .ok_or(ReferenceProcessingError::MissingUri)?;
366 let initial_data = resolver
367 .dereference(uri)
368 .map_err(ReferenceProcessingError::UriDereference)?;
369
370 let pre_digest_bytes = execute_transforms(signature_node, initial_data, &reference.transforms)
372 .map_err(ReferenceProcessingError::Transform)?;
373
374 let computed_digest = compute_digest(reference.digest_method, &pre_digest_bytes);
376
377 let status = if constant_time_eq(&computed_digest, &reference.digest_value) {
379 DsigStatus::Valid
380 } else {
381 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch {
382 ref_index: reference_index,
383 })
384 };
385
386 Ok(ReferenceResult {
387 reference_set,
388 reference_index,
389 uri: uri.to_owned(),
390 digest_algorithm: reference.digest_method,
391 status,
392 pre_digest_data: if store_pre_digest {
393 Some(pre_digest_bytes)
394 } else {
395 None
396 },
397 })
398}
399
400pub fn process_all_references(
412 references: &[Reference],
413 resolver: &UriReferenceResolver<'_>,
414 signature_node: Node<'_, '_>,
415 store_pre_digest: bool,
416) -> Result<ReferencesResult, ReferenceProcessingError> {
417 let mut results = Vec::with_capacity(references.len());
418
419 for (i, reference) in references.iter().enumerate() {
420 let result = process_reference(
421 reference,
422 resolver,
423 signature_node,
424 ReferenceSet::SignedInfo,
425 i,
426 store_pre_digest,
427 )?;
428 let failed = matches!(result.status, DsigStatus::Invalid(_));
429 results.push(result);
430
431 if failed {
432 return Ok(ReferencesResult {
433 results,
434 first_failure: Some(i),
435 });
436 }
437 }
438
439 Ok(ReferencesResult {
440 results,
441 first_failure: None,
442 })
443}
444
445#[derive(Debug, thiserror::Error)]
449#[non_exhaustive]
450pub enum ReferenceProcessingError {
451 #[error("reference URI is required; omitted URI references are not supported")]
453 MissingUri,
454
455 #[error("URI dereference failed: {0}")]
457 UriDereference(#[source] super::types::TransformError),
458
459 #[error("transform failed: {0}")]
461 Transform(#[source] super::types::TransformError),
462}
463
464#[derive(Debug)]
466#[non_exhaustive]
467#[must_use = "inspect status before accepting the document"]
468pub struct VerifyResult {
469 pub status: DsigStatus,
471 pub signed_info_references: Vec<ReferenceResult>,
475 pub manifest_references: Vec<ReferenceResult>,
482 pub canonicalized_signed_info: Option<Vec<u8>>,
485}
486
487#[derive(Debug, thiserror::Error)]
489#[non_exhaustive]
490pub enum DsigError {
491 #[error("XML parse error: {0}")]
493 XmlParse(#[from] roxmltree::Error),
494
495 #[error("missing required element: <{element}>")]
497 MissingElement {
498 element: &'static str,
500 },
501
502 #[error("invalid Signature structure: {reason}")]
504 InvalidStructure {
505 reason: &'static str,
507 },
508
509 #[error("failed to parse SignedInfo: {0}")]
511 ParseSignedInfo(#[from] super::parse::ParseError),
512
513 #[error("failed to parse Manifest reference: {0}")]
515 ParseManifestReference(#[source] ParseError),
516
517 #[error("reference processing failed: {0}")]
519 Reference(#[from] ReferenceProcessingError),
520
521 #[error("SignedInfo canonicalization failed: {0}")]
523 Canonicalization(#[from] crate::c14n::C14nError),
524
525 #[error("invalid SignatureValue base64: {0}")]
527 SignatureValueBase64(#[from] base64::DecodeError),
528
529 #[error("signature verification failed: {0}")]
531 Crypto(#[from] SignatureVerificationError),
532
533 #[error("reference URI is not allowed by policy: {uri}")]
535 DisallowedUri {
536 uri: String,
538 },
539
540 #[error("transform is not allowed by policy: {algorithm}")]
542 DisallowedTransform {
543 algorithm: String,
545 },
546}
547
548type SignatureVerificationPipelineError = DsigError;
549
550pub fn verify_signature_with_pem_key(
568 xml: &str,
569 public_key_pem: &str,
570 store_pre_digest: bool,
571) -> Result<VerifyResult, DsigError> {
572 struct PemVerifyingKey<'a> {
573 public_key_pem: &'a str,
574 }
575
576 impl VerifyingKey for PemVerifyingKey<'_> {
577 fn verify(
578 &self,
579 algorithm: SignatureAlgorithm,
580 signed_data: &[u8],
581 signature_value: &[u8],
582 ) -> Result<bool, DsigError> {
583 verify_with_algorithm(algorithm, self.public_key_pem, signed_data, signature_value)
584 }
585 }
586
587 let key = PemVerifyingKey { public_key_pem };
588 VerifyContext::new()
589 .key(&key)
590 .store_pre_digest(store_pre_digest)
591 .verify(xml)
592}
593
594fn verify_signature_with_context(
595 xml: &str,
596 ctx: &VerifyContext<'_>,
597) -> Result<VerifyResult, SignatureVerificationPipelineError> {
598 let doc = Document::parse(xml)?;
599 let mut signatures = doc.descendants().filter(|node| {
600 node.is_element()
601 && node.tag_name().name() == "Signature"
602 && node.tag_name().namespace() == Some(XMLDSIG_NS)
603 });
604 let signature_node = match (signatures.next(), signatures.next()) {
605 (None, _) => {
606 return Err(SignatureVerificationPipelineError::MissingElement {
607 element: "Signature",
608 });
609 }
610 (Some(node), None) => node,
611 (Some(_), Some(_)) => {
612 return Err(SignatureVerificationPipelineError::InvalidStructure {
613 reason: "Signature must appear exactly once in document",
614 });
615 }
616 };
617
618 let signature_children = parse_signature_children(signature_node)?;
619 let signed_info_node = signature_children.signed_info_node;
620
621 let signed_info = parse_signed_info(signed_info_node)?;
622 enforce_reference_policies(
623 &signed_info.references,
624 ctx.allowed_uri_types,
625 ctx.allowed_transform_uris(),
626 )?;
627
628 let resolver = UriReferenceResolver::new(&doc);
629 let references = process_all_references(
630 &signed_info.references,
631 &resolver,
632 signature_node,
633 ctx.store_pre_digest,
634 )?;
635
636 let manifest_references = if ctx.process_manifests {
637 let signed_info_reference_nodes =
638 collect_signed_info_reference_nodes(&signed_info.references, &resolver);
639 process_manifest_references(signature_node, &resolver, ctx, &signed_info_reference_nodes)?
640 } else {
641 Vec::new()
642 };
643
644 if let Some(first_failure) = references.first_failure {
645 let status = references.results[first_failure].status;
646 return Ok(VerifyResult {
647 status,
648 signed_info_references: references.results,
649 manifest_references,
650 canonicalized_signed_info: None,
651 });
652 }
653
654 let signed_info_subtree: HashSet<_> = signed_info_node
655 .descendants()
656 .map(|node: Node<'_, '_>| node.id())
657 .collect();
658 let mut canonical_signed_info = Vec::new();
659 canonicalize(
660 &doc,
661 Some(&|node| signed_info_subtree.contains(&node.id())),
662 &signed_info.c14n_method,
663 &mut canonical_signed_info,
664 )?;
665
666 let signature_value = decode_signature_value(signature_children.signature_value_node)?;
667 let Some(resolved_key) = resolve_verifying_key(ctx, xml)? else {
668 return Ok(VerifyResult {
669 status: DsigStatus::Invalid(FailureReason::KeyNotFound),
670 signed_info_references: references.results,
671 manifest_references,
672 canonicalized_signed_info: if ctx.store_pre_digest {
673 Some(canonical_signed_info)
674 } else {
675 None
676 },
677 });
678 };
679 let verifier = resolved_key.as_ref();
680 let signature_valid = verifier.verify(
681 signed_info.signature_method,
682 &canonical_signed_info,
683 &signature_value,
684 )?;
685
686 Ok(VerifyResult {
687 status: if signature_valid {
688 DsigStatus::Valid
689 } else {
690 DsigStatus::Invalid(FailureReason::SignatureMismatch)
691 },
692 signed_info_references: references.results,
693 manifest_references,
694 canonicalized_signed_info: if ctx.store_pre_digest {
695 Some(canonical_signed_info)
696 } else {
697 None
698 },
699 })
700}
701
702fn process_manifest_references(
703 signature_node: Node<'_, '_>,
704 resolver: &UriReferenceResolver<'_>,
705 ctx: &VerifyContext<'_>,
706 signed_info_reference_nodes: &HashSet<NodeId>,
707) -> Result<Vec<ReferenceResult>, SignatureVerificationPipelineError> {
708 let manifest_references =
709 parse_manifest_references(signature_node, signed_info_reference_nodes)?;
710 if manifest_references.is_empty() {
711 return Ok(Vec::new());
712 }
713 let mut results = Vec::with_capacity(manifest_references.len());
714 for (index, reference) in manifest_references.iter().enumerate() {
715 match enforce_reference_policies(
716 std::slice::from_ref(reference),
717 ctx.allowed_uri_types,
718 ctx.allowed_transform_uris(),
719 ) {
720 Ok(()) => {}
721 Err(
722 SignatureVerificationPipelineError::DisallowedUri { .. }
723 | SignatureVerificationPipelineError::DisallowedTransform { .. },
724 ) => {
725 results.push(manifest_reference_invalid_result(
726 reference,
727 index,
728 FailureReason::ReferencePolicyViolation { ref_index: index },
729 ));
730 continue;
731 }
732 Err(SignatureVerificationPipelineError::Reference(
733 ReferenceProcessingError::MissingUri,
734 )) => {
735 results.push(manifest_reference_invalid_result(
736 reference,
737 index,
738 FailureReason::ReferenceProcessingFailure { ref_index: index },
739 ));
740 continue;
741 }
742 Err(_) => {
743 results.push(manifest_reference_invalid_result(
746 reference,
747 index,
748 FailureReason::ReferenceProcessingFailure { ref_index: index },
749 ));
750 continue;
751 }
752 }
753
754 match process_reference(
755 reference,
756 resolver,
757 signature_node,
758 ReferenceSet::Manifest,
759 index,
760 ctx.store_pre_digest,
761 ) {
762 Ok(result) => results.push(result),
763 Err(_) => results.push(manifest_reference_invalid_result(
764 reference,
765 index,
766 FailureReason::ReferenceProcessingFailure { ref_index: index },
767 )),
768 }
769 }
770 Ok(results)
771}
772
773fn manifest_reference_invalid_result(
774 reference: &Reference,
775 index: usize,
776 reason: FailureReason,
777) -> ReferenceResult {
778 ReferenceResult {
779 reference_set: ReferenceSet::Manifest,
780 reference_index: index,
781 uri: reference
782 .uri
783 .clone()
784 .unwrap_or_else(|| "<omitted>".to_owned()),
785 digest_algorithm: reference.digest_method,
786 status: DsigStatus::Invalid(reason),
787 pre_digest_data: None,
788 }
789}
790
791fn parse_manifest_references(
792 signature_node: Node<'_, '_>,
793 signed_info_reference_nodes: &HashSet<NodeId>,
794) -> Result<Vec<Reference>, SignatureVerificationPipelineError> {
795 let mut references = Vec::new();
796 for object_node in signature_node.children().filter(|node| {
797 node.is_element()
798 && node.tag_name().namespace() == Some(XMLDSIG_NS)
799 && node.tag_name().name() == "Object"
800 }) {
801 let object_is_signed = signed_info_reference_nodes.contains(&object_node.id());
802 for manifest_node in object_node.children().filter(|node| {
803 node.is_element()
804 && node.tag_name().namespace() == Some(XMLDSIG_NS)
805 && node.tag_name().name() == "Manifest"
806 }) {
807 let manifest_is_signed = signed_info_reference_nodes.contains(&manifest_node.id());
808 if !object_is_signed && !manifest_is_signed {
809 continue;
810 }
811 let mut manifest_children = Vec::new();
812 for child in manifest_node.children() {
813 if child.is_text()
814 && child.text().is_some_and(|text| {
815 text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
816 })
817 {
818 return Err(SignatureVerificationPipelineError::InvalidStructure {
819 reason: "Manifest contains non-whitespace mixed content",
820 });
821 }
822 if child.is_element() {
823 manifest_children.push(child);
824 }
825 }
826 if manifest_children.is_empty() {
827 return Err(SignatureVerificationPipelineError::InvalidStructure {
828 reason: "Manifest must contain at least one ds:Reference element child",
829 });
830 }
831 for child in manifest_children {
832 if child.tag_name().namespace() != Some(XMLDSIG_NS)
833 || child.tag_name().name() != "Reference"
834 {
835 return Err(SignatureVerificationPipelineError::InvalidStructure {
836 reason: "Manifest must contain only ds:Reference element children",
837 });
838 }
839 references.push(
840 parse_reference(child)
841 .map_err(SignatureVerificationPipelineError::ParseManifestReference)?,
842 );
843 }
844 }
845 }
846 Ok(references)
847}
848
849fn collect_signed_info_reference_nodes(
850 references: &[Reference],
851 resolver: &UriReferenceResolver<'_>,
852) -> HashSet<NodeId> {
853 references
854 .iter()
855 .filter_map(|reference| reference.uri.as_deref())
856 .filter_map(signed_info_reference_id_from_uri)
857 .filter_map(|id| resolver.node_id_for_id(id))
858 .collect()
859}
860
861fn signed_info_reference_id_from_uri(uri: &str) -> Option<&str> {
862 let fragment = uri.strip_prefix('#')?;
863 if fragment.is_empty() || fragment == "xpointer(/)" {
864 return None;
865 }
866 if let Some(id) = parse_xpointer_id_fragment(fragment) {
867 return (!id.is_empty()).then_some(id);
868 }
869 (!fragment.starts_with("xpointer(")).then_some(fragment)
870}
871
872enum ResolvedVerifyingKey<'a> {
873 Borrowed(&'a dyn VerifyingKey),
874 Owned(Box<dyn VerifyingKey + 'a>),
875}
876
877impl ResolvedVerifyingKey<'_> {
878 fn as_ref(&self) -> &dyn VerifyingKey {
879 match self {
880 Self::Borrowed(key) => *key,
881 Self::Owned(key) => key.as_ref(),
882 }
883 }
884}
885
886fn resolve_verifying_key<'k>(
887 ctx: &VerifyContext<'k>,
888 xml: &str,
889) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
890 if let Some(key) = ctx.key {
891 return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
892 }
893 if let Some(resolver) = ctx.key_resolver {
894 let resolved = resolver.resolve(xml)?;
895 return Ok(resolved.map(ResolvedVerifyingKey::Owned));
896 }
897 Ok(None)
898}
899
900fn enforce_reference_policies(
901 references: &[Reference],
902 allowed_uri_types: UriTypeSet,
903 allowed_transforms: Option<&HashSet<String>>,
904) -> Result<(), SignatureVerificationPipelineError> {
905 for reference in references {
906 let uri = reference
907 .uri
908 .as_deref()
909 .ok_or(SignatureVerificationPipelineError::Reference(
910 ReferenceProcessingError::MissingUri,
911 ))?;
912 if !allowed_uri_types.allows(uri) {
913 return Err(SignatureVerificationPipelineError::DisallowedUri {
914 uri: uri.to_owned(),
915 });
916 }
917
918 if let Some(allowed) = allowed_transforms {
919 for transform in &reference.transforms {
920 let transform_uri = transform_uri(transform);
921 if !allowed.contains(transform_uri) {
922 return Err(SignatureVerificationPipelineError::DisallowedTransform {
923 algorithm: transform_uri.to_owned(),
924 });
925 }
926 }
927
928 let has_explicit_c14n = reference
929 .transforms
930 .iter()
931 .any(|transform| matches!(transform, Transform::C14n(_)));
932 if !has_explicit_c14n && !allowed.contains(DEFAULT_IMPLICIT_C14N_URI) {
933 return Err(SignatureVerificationPipelineError::DisallowedTransform {
934 algorithm: DEFAULT_IMPLICIT_C14N_URI.to_owned(),
935 });
936 }
937 }
938 }
939 Ok(())
940}
941
942fn transform_uri(transform: &Transform) -> &'static str {
943 match transform {
944 Transform::Enveloped => super::transforms::ENVELOPED_SIGNATURE_URI,
945 Transform::XpathExcludeAllSignatures => XPATH_TRANSFORM_URI,
946 Transform::C14n(algo) => algo.uri(),
947 }
948}
949
950#[derive(Debug, Clone, Copy)]
951struct SignatureChildNodes<'a, 'input> {
952 signed_info_node: Node<'a, 'input>,
953 signature_value_node: Node<'a, 'input>,
954}
955
956fn parse_signature_children<'a, 'input>(
957 signature_node: Node<'a, 'input>,
958) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
959 let mut signed_info_node: Option<Node<'_, '_>> = None;
960 let mut signature_value_node: Option<Node<'_, '_>> = None;
961 let mut signed_info_index: Option<usize> = None;
962 let mut signature_value_index: Option<usize> = None;
963 for (zero_based_index, child) in signature_node
964 .children()
965 .filter(|node| node.is_element())
966 .enumerate()
967 {
968 let element_index = zero_based_index + 1;
969 if child.tag_name().namespace() != Some(XMLDSIG_NS) {
970 continue;
971 }
972 match child.tag_name().name() {
973 "SignedInfo" => {
974 if signed_info_node.is_some() {
975 return Err(SignatureVerificationPipelineError::InvalidStructure {
976 reason: "SignedInfo must appear exactly once under Signature",
977 });
978 }
979 signed_info_node = Some(child);
980 signed_info_index = Some(element_index);
981 }
982 "SignatureValue" => {
983 if signature_value_node.is_some() {
984 return Err(SignatureVerificationPipelineError::InvalidStructure {
985 reason: "SignatureValue must appear exactly once under Signature",
986 });
987 }
988 signature_value_node = Some(child);
989 signature_value_index = Some(element_index);
990 }
991 _ => {}
992 }
993 }
994
995 let signed_info_node =
996 signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
997 element: "SignedInfo",
998 })?;
999 let signature_value_node =
1000 signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
1001 element: "SignatureValue",
1002 })?;
1003 if signed_info_index != Some(1) {
1004 return Err(SignatureVerificationPipelineError::InvalidStructure {
1005 reason: "SignedInfo must be the first element child of Signature",
1006 });
1007 }
1008 if signature_value_index != Some(2) {
1009 return Err(SignatureVerificationPipelineError::InvalidStructure {
1010 reason: "SignatureValue must be the second element child of Signature",
1011 });
1012 }
1013 Ok(SignatureChildNodes {
1014 signed_info_node,
1015 signature_value_node,
1016 })
1017}
1018
1019fn decode_signature_value(
1020 signature_value_node: Node<'_, '_>,
1021) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
1022 if signature_value_node
1023 .children()
1024 .any(|child| child.is_element())
1025 {
1026 return Err(SignatureVerificationPipelineError::InvalidStructure {
1027 reason: "SignatureValue must not contain element children",
1028 });
1029 }
1030
1031 let mut normalized = String::new();
1032 let mut raw_text_len = 0usize;
1033 for child in signature_value_node
1034 .children()
1035 .filter(|child| child.is_text())
1036 {
1037 if let Some(text) = child.text() {
1038 push_normalized_signature_text(text, &mut raw_text_len, &mut normalized)?;
1039 }
1040 }
1041
1042 Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
1043}
1044
1045fn push_normalized_signature_text(
1046 text: &str,
1047 raw_text_len: &mut usize,
1048 normalized: &mut String,
1049) -> Result<(), SignatureVerificationPipelineError> {
1050 for ch in text.chars() {
1051 if raw_text_len.saturating_add(ch.len_utf8()) > MAX_SIGNATURE_VALUE_TEXT_LEN {
1052 return Err(SignatureVerificationPipelineError::InvalidStructure {
1053 reason: "SignatureValue exceeds maximum allowed text length",
1054 });
1055 }
1056 *raw_text_len = raw_text_len.saturating_add(ch.len_utf8());
1057 if matches!(ch, ' ' | '\t' | '\r' | '\n') {
1058 continue;
1059 }
1060 if ch.is_ascii_whitespace() {
1061 let invalid_byte =
1062 u8::try_from(u32::from(ch)).expect("ASCII whitespace always fits into u8");
1063 return Err(SignatureVerificationPipelineError::SignatureValueBase64(
1064 base64::DecodeError::InvalidByte(normalized.len(), invalid_byte),
1065 ));
1066 }
1067 if normalized.len().saturating_add(ch.len_utf8()) > MAX_SIGNATURE_VALUE_LEN {
1068 return Err(SignatureVerificationPipelineError::InvalidStructure {
1069 reason: "SignatureValue exceeds maximum allowed length",
1070 });
1071 }
1072 normalized.push(ch);
1073 }
1074 Ok(())
1075}
1076
1077fn verify_with_algorithm(
1078 algorithm: SignatureAlgorithm,
1079 public_key_pem: &str,
1080 signed_data: &[u8],
1081 signature_value: &[u8],
1082) -> Result<bool, SignatureVerificationPipelineError> {
1083 match algorithm {
1084 SignatureAlgorithm::RsaSha1
1085 | SignatureAlgorithm::RsaSha256
1086 | SignatureAlgorithm::RsaSha384
1087 | SignatureAlgorithm::RsaSha512 => Ok(verify_rsa_signature_pem(
1088 algorithm,
1089 public_key_pem,
1090 signed_data,
1091 signature_value,
1092 )?),
1093 SignatureAlgorithm::EcdsaP256Sha256 | SignatureAlgorithm::EcdsaP384Sha384 => {
1094 match verify_ecdsa_signature_pem(
1098 algorithm,
1099 public_key_pem,
1100 signed_data,
1101 signature_value,
1102 ) {
1103 Ok(valid) => Ok(valid),
1104 Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
1105 Err(error) => Err(error.into()),
1106 }
1107 }
1108 }
1109}
1110
1111#[cfg(test)]
1112#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
1113mod tests {
1114 use super::*;
1115 use crate::xmldsig::digest::DigestAlgorithm;
1116 use crate::xmldsig::parse::{Reference, parse_signed_info};
1117 use crate::xmldsig::transforms::Transform;
1118 use crate::xmldsig::uri::UriReferenceResolver;
1119 use base64::Engine;
1120 use roxmltree::Document;
1121
1122 fn make_reference(
1126 uri: &str,
1127 transforms: Vec<Transform>,
1128 digest_method: DigestAlgorithm,
1129 digest_value: Vec<u8>,
1130 ) -> Reference {
1131 Reference {
1132 uri: Some(uri.to_string()),
1133 id: None,
1134 ref_type: None,
1135 transforms,
1136 digest_method,
1137 digest_value,
1138 }
1139 }
1140
1141 struct RejectingKey;
1142
1143 impl VerifyingKey for RejectingKey {
1144 fn verify(
1145 &self,
1146 _algorithm: SignatureAlgorithm,
1147 _signed_data: &[u8],
1148 _signature_value: &[u8],
1149 ) -> Result<bool, SignatureVerificationPipelineError> {
1150 Ok(false)
1151 }
1152 }
1153
1154 struct AcceptingKey;
1155
1156 impl VerifyingKey for AcceptingKey {
1157 fn verify(
1158 &self,
1159 _algorithm: SignatureAlgorithm,
1160 _signed_data: &[u8],
1161 _signature_value: &[u8],
1162 ) -> Result<bool, SignatureVerificationPipelineError> {
1163 Ok(true)
1164 }
1165 }
1166
1167 struct PanicResolver;
1168
1169 impl KeyResolver for PanicResolver {
1170 fn resolve<'a>(
1171 &'a self,
1172 _xml: &str,
1173 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
1174 {
1175 panic!("resolver should not be called when references already fail");
1176 }
1177 }
1178
1179 struct MissingKeyResolver;
1180
1181 impl KeyResolver for MissingKeyResolver {
1182 fn resolve<'a>(
1183 &'a self,
1184 _xml: &str,
1185 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
1186 {
1187 Ok(None)
1188 }
1189 }
1190
1191 fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
1192 format!(
1193 r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1194 <ds:SignedInfo>
1195 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1196 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1197 <ds:Reference URI="{reference_uri}">
1198 {transforms_xml}
1199 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1200 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
1201 </ds:Reference>
1202 </ds:SignedInfo>
1203 <ds:SignatureValue>AQ==</ds:SignatureValue>
1204</ds:Signature>"#
1205 )
1206 }
1207
1208 fn signature_with_target_reference(signature_value_b64: &str) -> String {
1209 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1210 <target ID="target">payload</target>
1211 <ds:Signature>
1212 <ds:SignedInfo>
1213 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1214 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1215 <ds:Reference URI="#target">
1216 <ds:Transforms>
1217 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1218 </ds:Transforms>
1219 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1220 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
1221 </ds:Reference>
1222 </ds:SignedInfo>
1223 <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
1224 </ds:Signature>
1225</root>"##;
1226
1227 let doc = Document::parse(xml_template).unwrap();
1228 let sig_node = doc
1229 .descendants()
1230 .find(|node| node.is_element() && node.tag_name().name() == "Signature")
1231 .unwrap();
1232 let signed_info_node = sig_node
1233 .children()
1234 .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
1235 .unwrap();
1236 let signed_info = parse_signed_info(signed_info_node).unwrap();
1237 let reference = &signed_info.references[0];
1238 let resolver = UriReferenceResolver::new(&doc);
1239 let initial_data = resolver
1240 .dereference(reference.uri.as_deref().unwrap())
1241 .unwrap();
1242 let pre_digest =
1243 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
1244 .unwrap();
1245 let digest = compute_digest(reference.digest_method, &pre_digest);
1246 let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
1247 xml_template
1248 .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
1249 .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
1250 }
1251
1252 #[test]
1253 fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
1254 let xml = signature_with_target_reference("AQ==");
1255
1256 let result = VerifyContext::new()
1257 .verify(&xml)
1258 .expect("missing key config must be reported as verification status");
1259 assert!(
1260 matches!(
1261 result.status,
1262 DsigStatus::Invalid(FailureReason::KeyNotFound)
1263 ),
1264 "unexpected status: {:?}",
1265 result.status
1266 );
1267 }
1268
1269 #[test]
1270 fn verify_context_rejects_disallowed_uri() {
1271 let xml = minimal_signature_xml("http://example.com/external", "");
1272 let err = VerifyContext::new()
1273 .key(&RejectingKey)
1274 .verify(&xml)
1275 .expect_err("external URI should be rejected by default policy");
1276 assert!(matches!(
1277 err,
1278 SignatureVerificationPipelineError::DisallowedUri { .. }
1279 ));
1280 }
1281
1282 #[test]
1283 fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
1284 let xml = minimal_signature_xml("", "");
1285 let err = VerifyContext::new()
1286 .key(&RejectingKey)
1287 .allowed_uri_types(UriTypeSet::new(false, true, false))
1288 .verify(&xml)
1289 .expect_err("empty URI must be rejected when empty references are disabled");
1290 assert!(matches!(
1291 err,
1292 SignatureVerificationPipelineError::DisallowedUri { ref uri } if uri.is_empty()
1293 ));
1294 }
1295
1296 #[test]
1297 fn verify_context_rejects_disallowed_transform() {
1298 let xml = minimal_signature_xml(
1299 "",
1300 r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
1301 );
1302 let err = VerifyContext::new()
1303 .key(&RejectingKey)
1304 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
1305 .verify(&xml)
1306 .expect_err("enveloped transform should be rejected by allowlist");
1307 assert!(matches!(
1308 err,
1309 SignatureVerificationPipelineError::DisallowedTransform { .. }
1310 ));
1311 }
1312
1313 fn signature_with_manifest_xml(valid_manifest_digest: bool) -> String {
1314 signature_with_manifest_xml_with_manifest_mutation(valid_manifest_digest, |xml| xml)
1315 }
1316
1317 fn signature_with_manifest_xml_with_manifest_mutation<F>(
1318 valid_manifest_digest: bool,
1319 mutate_manifest: F,
1320 ) -> String
1321 where
1322 F: FnOnce(String) -> String,
1323 {
1324 const TMP_SIGNED_INFO_DIGEST: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
1325 const INVALID_MANIFEST_DIGEST: &str = "//////////////////////////8=";
1326 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1327 <target ID="target">payload</target>
1328 <ds:Signature>
1329 <ds:SignedInfo>
1330 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1331 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1332 <ds:Reference URI="#manifest">
1333 <ds:Transforms>
1334 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1335 </ds:Transforms>
1336 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1337 <ds:DigestValue>SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER</ds:DigestValue>
1338 </ds:Reference>
1339 </ds:SignedInfo>
1340 <ds:SignatureValue>AQ==</ds:SignatureValue>
1341 <ds:Object>
1342 <ds:Manifest ID="manifest">
1343 <ds:Reference URI="#target">
1344 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1345 <ds:DigestValue>MANIFEST_DIGEST_PLACEHOLDER</ds:DigestValue>
1346 </ds:Reference>
1347 </ds:Manifest>
1348 </ds:Object>
1349 </ds:Signature>
1350</root>"##;
1351 let seed_xml = xml_template.replace(
1352 "SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER",
1353 TMP_SIGNED_INFO_DIGEST,
1354 );
1355 let doc = Document::parse(&seed_xml).unwrap();
1356 let signature_node = doc
1357 .descendants()
1358 .find(|node| {
1359 node.is_element()
1360 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1361 && node.tag_name().name() == "Signature"
1362 })
1363 .unwrap();
1364 let resolver = UriReferenceResolver::new(&doc);
1365 let initial_data = resolver.dereference("#target").unwrap();
1366 let manifest_pre_digest =
1367 crate::xmldsig::execute_transforms(signature_node, initial_data, &[]).unwrap();
1368 let computed_manifest_digest_b64 = base64::engine::general_purpose::STANDARD
1369 .encode(compute_digest(DigestAlgorithm::Sha1, &manifest_pre_digest));
1370 let final_manifest_digest_b64 = if valid_manifest_digest {
1371 computed_manifest_digest_b64.as_str()
1372 } else {
1373 INVALID_MANIFEST_DIGEST
1374 };
1375 let xml_with_manifest_digest = mutate_manifest(
1376 seed_xml.replace("MANIFEST_DIGEST_PLACEHOLDER", final_manifest_digest_b64),
1377 );
1378 let signed_doc = Document::parse(&xml_with_manifest_digest).unwrap();
1379 let signed_signature_node = signed_doc
1380 .descendants()
1381 .find(|node| {
1382 node.is_element()
1383 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1384 && node.tag_name().name() == "Signature"
1385 })
1386 .unwrap();
1387 let signed_info_node = signed_signature_node
1388 .children()
1389 .find(|node| {
1390 node.is_element()
1391 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1392 && node.tag_name().name() == "SignedInfo"
1393 })
1394 .unwrap();
1395 let signed_info = parse_signed_info(signed_info_node).unwrap();
1396 let object_reference = &signed_info.references[0];
1397 let signed_resolver = UriReferenceResolver::new(&signed_doc);
1398 let signed_initial_data = signed_resolver
1399 .dereference(object_reference.uri.as_deref().unwrap())
1400 .unwrap();
1401 let signed_pre_digest = crate::xmldsig::execute_transforms(
1402 signed_signature_node,
1403 signed_initial_data,
1404 &object_reference.transforms,
1405 )
1406 .unwrap();
1407 let signed_digest_b64 = base64::engine::general_purpose::STANDARD.encode(compute_digest(
1408 object_reference.digest_method,
1409 &signed_pre_digest,
1410 ));
1411
1412 xml_with_manifest_digest.replacen(TMP_SIGNED_INFO_DIGEST, &signed_digest_b64, 1)
1413 }
1414
1415 #[test]
1416 fn verify_context_processes_manifest_references_when_enabled() {
1417 let xml = signature_with_manifest_xml(true);
1418
1419 let result_without_manifests = VerifyContext::new()
1420 .key(&RejectingKey)
1421 .verify(&xml)
1422 .expect("manifest processing disabled should still verify SignedInfo");
1423 assert!(
1424 result_without_manifests.manifest_references.is_empty(),
1425 "manifest results must stay empty when manifest processing is disabled",
1426 );
1427 assert!(matches!(
1428 result_without_manifests.status,
1429 DsigStatus::Invalid(FailureReason::SignatureMismatch)
1430 ));
1431
1432 let malformed_manifest_xml = signature_with_manifest_xml(true).replacen(
1433 "</ds:Object>",
1434 "</ds:Object><ds:Object><ds:Manifest><ds:Foo/></ds:Manifest></ds:Object>",
1435 1,
1436 );
1437 let malformed_with_manifests_disabled = VerifyContext::new()
1438 .key(&RejectingKey)
1439 .verify(&malformed_manifest_xml)
1440 .expect("malformed Manifest must be ignored when manifest processing is disabled");
1441 assert!(
1442 malformed_with_manifests_disabled
1443 .manifest_references
1444 .is_empty(),
1445 "manifest parser must not run when process_manifests is disabled",
1446 );
1447 assert!(matches!(
1448 malformed_with_manifests_disabled.status,
1449 DsigStatus::Invalid(FailureReason::SignatureMismatch)
1450 ));
1451
1452 let result_with_manifests = VerifyContext::new()
1453 .key(&RejectingKey)
1454 .process_manifests(true)
1455 .verify(&xml)
1456 .expect("manifest references should be processed when enabled");
1457 assert_eq!(result_with_manifests.manifest_references.len(), 1);
1458 assert_eq!(
1459 result_with_manifests.manifest_references[0].reference_set,
1460 ReferenceSet::Manifest
1461 );
1462 assert_eq!(
1463 result_with_manifests.manifest_references[0].reference_index,
1464 0
1465 );
1466 assert!(matches!(
1467 result_with_manifests.manifest_references[0].status,
1468 DsigStatus::Valid
1469 ));
1470 assert!(matches!(
1471 result_with_manifests.status,
1472 DsigStatus::Invalid(FailureReason::SignatureMismatch)
1473 ));
1474 }
1475
1476 #[test]
1477 fn verify_context_processes_manifest_when_signedinfo_references_object() {
1478 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1479 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
1480 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
1481 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
1482 });
1483
1484 let result = VerifyContext::new()
1485 .key(&RejectingKey)
1486 .process_manifests(true)
1487 .verify(&xml)
1488 .expect("manifest references should be processed when SignedInfo references ds:Object");
1489 assert_eq!(
1490 result.manifest_references.len(),
1491 1,
1492 "signed ds:Object should enable processing of its direct-child ds:Manifest",
1493 );
1494 assert_eq!(
1495 result.manifest_references[0].reference_set,
1496 ReferenceSet::Manifest
1497 );
1498 assert_eq!(result.manifest_references[0].reference_index, 0);
1499 assert!(matches!(
1500 result.manifest_references[0].status,
1501 DsigStatus::Valid
1502 ));
1503 }
1504
1505 #[test]
1506 fn verify_context_manifest_digest_mismatch_is_non_fatal() {
1507 let xml = signature_with_manifest_xml(false);
1508 let result = VerifyContext::new()
1509 .key(&RejectingKey)
1510 .process_manifests(true)
1511 .verify(&xml)
1512 .expect("manifest digest mismatches should be reported as reference status");
1513 assert_eq!(result.manifest_references.len(), 1);
1514 assert_eq!(
1515 result.manifest_references[0].reference_set,
1516 ReferenceSet::Manifest
1517 );
1518 assert_eq!(result.manifest_references[0].reference_index, 0);
1519 assert!(matches!(
1520 result.manifest_references[0].status,
1521 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1522 ));
1523 assert!(matches!(
1524 result.status,
1525 DsigStatus::Invalid(FailureReason::SignatureMismatch)
1526 ));
1527 }
1528
1529 #[test]
1530 fn verify_context_manifest_digest_mismatch_is_non_fatal_with_accepting_key() {
1531 let xml = signature_with_manifest_xml(false);
1532 let result = VerifyContext::new()
1533 .key(&AcceptingKey)
1534 .process_manifests(true)
1535 .verify(&xml)
1536 .expect("manifest digest mismatches should be recorded while signature stays valid");
1537 assert_eq!(result.manifest_references.len(), 1);
1538 assert!(matches!(
1539 result.manifest_references[0].status,
1540 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1541 ));
1542 assert!(matches!(result.status, DsigStatus::Valid));
1543 }
1544
1545 #[test]
1546 fn verify_context_keeps_manifest_results_when_signedinfo_reference_fails() {
1547 let xml = signature_with_manifest_xml(true);
1548 let (signed_info_prefix, object_suffix) = xml
1549 .split_once("<ds:Object>")
1550 .expect("fixture should contain ds:Object");
1551 let open = "<ds:DigestValue>";
1552 let close = "</ds:DigestValue>";
1553 let digest_start = signed_info_prefix
1554 .find(open)
1555 .expect("SignedInfo should contain DigestValue");
1556 let digest_end = signed_info_prefix[digest_start + open.len()..]
1557 .find(close)
1558 .map(|offset| digest_start + open.len() + offset)
1559 .expect("SignedInfo DigestValue must be closed");
1560 let broken_signed_info_prefix = format!(
1561 "{}{}AAAAAAAAAAAAAAAAAAAAAAAAAAA={}{}",
1562 &signed_info_prefix[..digest_start],
1563 open,
1564 close,
1565 &signed_info_prefix[digest_end + close.len()..],
1566 );
1567 let broken_xml = format!("{broken_signed_info_prefix}<ds:Object>{object_suffix}");
1568 let result = VerifyContext::new()
1569 .key(&RejectingKey)
1570 .process_manifests(true)
1571 .verify(&broken_xml)
1572 .expect("manifest references should still be processed on SignedInfo digest failure");
1573 assert!(matches!(
1574 result.status,
1575 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1576 ));
1577 assert_eq!(
1578 result.manifest_references.len(),
1579 1,
1580 "manifest diagnostics must be preserved even when SignedInfo fails early",
1581 );
1582 }
1583
1584 #[test]
1585 fn verify_context_records_manifest_policy_violations_without_aborting() {
1586 let xml = signature_with_manifest_xml(true);
1587 let (prefix, object_suffix) = xml
1588 .split_once("<ds:Object>")
1589 .expect("fixture should contain ds:Object");
1590 let mutated_object_suffix =
1591 object_suffix.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1);
1592 let broken_xml = format!("{prefix}<ds:Object>{mutated_object_suffix}");
1593 let result = VerifyContext::new()
1594 .key(&RejectingKey)
1595 .process_manifests(true)
1596 .verify(&broken_xml)
1597 .expect("manifest policy violations should be recorded, not abort verify()");
1598 assert_eq!(result.manifest_references.len(), 1);
1599 assert!(matches!(
1600 result.manifest_references[0].status,
1601 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
1602 ));
1603 assert!(matches!(
1604 result.status,
1605 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1606 ));
1607 }
1608
1609 #[test]
1610 fn verify_context_records_manifest_policy_violations_with_accepting_key() {
1611 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1612 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
1613 });
1614 let result = VerifyContext::new()
1615 .key(&AcceptingKey)
1616 .process_manifests(true)
1617 .verify(&broken_xml)
1618 .expect("manifest policy violations should be recorded while signature stays valid");
1619 assert_eq!(result.manifest_references.len(), 1);
1620 assert!(matches!(
1621 result.manifest_references[0].status,
1622 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
1623 ));
1624 assert!(matches!(result.status, DsigStatus::Valid));
1625 }
1626
1627 #[test]
1628 fn verify_context_records_manifest_missing_uri_as_processing_failure() {
1629 let xml = signature_with_manifest_xml(true);
1630 let (prefix, object_suffix) = xml
1631 .split_once("<ds:Object>")
1632 .expect("fixture should contain ds:Object");
1633 let mutated_object_suffix =
1634 object_suffix.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1);
1635 let broken_xml = format!("{prefix}<ds:Object>{mutated_object_suffix}");
1636
1637 let result = VerifyContext::new()
1638 .key(&RejectingKey)
1639 .process_manifests(true)
1640 .verify(&broken_xml)
1641 .expect("manifest missing URI should be recorded as non-fatal processing failure");
1642 assert_eq!(result.manifest_references.len(), 1);
1643 assert_eq!(result.manifest_references[0].uri, "<omitted>");
1644 assert!(matches!(
1645 result.manifest_references[0].status,
1646 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
1647 ));
1648 assert!(matches!(
1649 result.status,
1650 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1651 ));
1652 }
1653
1654 #[test]
1655 fn verify_context_records_manifest_missing_uri_with_accepting_key() {
1656 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1657 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
1658 });
1659
1660 let result = VerifyContext::new()
1661 .key(&AcceptingKey)
1662 .process_manifests(true)
1663 .verify(&broken_xml)
1664 .expect("manifest missing URI should be recorded while signature stays valid");
1665 assert_eq!(result.manifest_references.len(), 1);
1666 assert_eq!(result.manifest_references[0].uri, "<omitted>");
1667 assert!(matches!(
1668 result.manifest_references[0].status,
1669 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
1670 ));
1671 assert!(matches!(result.status, DsigStatus::Valid));
1672 }
1673
1674 #[test]
1675 fn verify_context_ignores_nested_manifests_in_object() {
1676 let xml = signature_with_manifest_xml(true)
1677 .replacen(
1678 "<ds:Manifest ID=\"manifest\">",
1679 "<wrapper><ds:Manifest ID=\"manifest\">",
1680 1,
1681 )
1682 .replacen("</ds:Manifest>", "</ds:Manifest></wrapper>", 1);
1683
1684 let result = VerifyContext::new()
1685 .key(&RejectingKey)
1686 .process_manifests(true)
1687 .verify(&xml)
1688 .expect("nested Manifest nodes are ignored in strict mode");
1689 assert!(
1690 result.manifest_references.is_empty(),
1691 "only direct ds:Manifest children of ds:Object must be processed"
1692 );
1693 }
1694
1695 #[test]
1696 fn verify_context_reports_manifest_reference_parse_errors_explicitly() {
1697 let xml = signature_with_manifest_xml(true);
1698 let (prefix, object_suffix) = xml
1699 .split_once("<ds:Object>")
1700 .expect("fixture should contain ds:Object");
1701 let open = "<ds:DigestValue>";
1702 let close = "</ds:DigestValue>";
1703 let digest_start = object_suffix
1704 .find(open)
1705 .expect("manifest should contain DigestValue");
1706 let digest_end = object_suffix[digest_start + open.len()..]
1707 .find(close)
1708 .map(|offset| digest_start + open.len() + offset)
1709 .expect("manifest DigestValue must be closed");
1710 let broken_object_suffix = format!(
1711 "{}{}!!!{}{}",
1712 &object_suffix[..digest_start],
1713 open,
1714 close,
1715 &object_suffix[digest_end + close.len()..],
1716 );
1717 let broken_xml = format!("{prefix}<ds:Object>{broken_object_suffix}");
1718
1719 let err = VerifyContext::new()
1720 .key(&RejectingKey)
1721 .process_manifests(true)
1722 .verify(&broken_xml)
1723 .expect_err("invalid Manifest DigestValue must map to ParseManifestReference");
1724 assert!(matches!(
1725 err,
1726 SignatureVerificationPipelineError::ParseManifestReference(_)
1727 ));
1728 }
1729
1730 #[test]
1731 fn verify_context_rejects_manifest_non_whitespace_mixed_content() {
1732 let xml = signature_with_manifest_xml(true).replacen(
1733 "<ds:Manifest ID=\"manifest\">",
1734 "<ds:Manifest ID=\"manifest\">junk",
1735 1,
1736 );
1737
1738 let err = VerifyContext::new()
1739 .key(&RejectingKey)
1740 .process_manifests(true)
1741 .verify(&xml)
1742 .expect_err("Manifest mixed content must fail verification");
1743 assert!(matches!(
1744 err,
1745 SignatureVerificationPipelineError::InvalidStructure {
1746 reason: "Manifest contains non-whitespace mixed content"
1747 }
1748 ));
1749 }
1750
1751 #[test]
1752 fn verify_context_rejects_empty_manifest_children() {
1753 let xml = signature_with_manifest_xml(true);
1754 let (prefix, rest) = xml
1755 .split_once("<ds:Manifest ID=\"manifest\">")
1756 .expect("fixture should contain Manifest");
1757 let (_, suffix) = rest
1758 .split_once("</ds:Manifest>")
1759 .expect("fixture should contain closing Manifest");
1760 let xml = format!("{prefix}<ds:Manifest ID=\"manifest\"></ds:Manifest>{suffix}");
1761
1762 let err = VerifyContext::new()
1763 .key(&RejectingKey)
1764 .process_manifests(true)
1765 .verify(&xml)
1766 .expect_err("empty Manifest must fail verification");
1767 assert!(matches!(
1768 err,
1769 SignatureVerificationPipelineError::InvalidStructure {
1770 reason: "Manifest must contain at least one ds:Reference element child"
1771 }
1772 ));
1773 }
1774
1775 #[test]
1776 fn verify_context_ignores_unsigned_malformed_manifest_blocks() {
1777 let xml = signature_with_manifest_xml(true).replacen(
1778 "</ds:Object>",
1779 "</ds:Object><ds:Object><ds:Manifest>junk<ds:Foo/></ds:Manifest></ds:Object>",
1780 1,
1781 );
1782 let result = VerifyContext::new()
1783 .key(&AcceptingKey)
1784 .process_manifests(true)
1785 .verify(&xml)
1786 .expect("unsigned malformed Manifest must be ignored");
1787 assert_eq!(
1788 result.manifest_references.len(),
1789 1,
1790 "only signed Manifest references must be reported",
1791 );
1792 assert!(matches!(result.status, DsigStatus::Valid));
1793 }
1794
1795 #[test]
1796 fn verify_context_skips_ambiguous_manifest_id_blocks() {
1797 let xml = signature_with_manifest_xml(true).replacen(
1798 "</ds:Object>",
1799 "</ds:Object><ds:Object><ds:Manifest ID=\"manifest\">junk<ds:Foo/></ds:Manifest></ds:Object>",
1800 1,
1801 );
1802 let err = VerifyContext::new()
1803 .key(&RejectingKey)
1804 .process_manifests(true)
1805 .verify(&xml)
1806 .expect_err("ambiguous manifest IDs should make SignedInfo #manifest dereference fail");
1807 assert!(matches!(
1808 err,
1809 SignatureVerificationPipelineError::Reference(
1810 ReferenceProcessingError::UriDereference(
1811 crate::xmldsig::types::TransformError::ElementNotFound(id)
1812 )
1813 ) if id == "manifest"
1814 ));
1815 }
1816
1817 #[test]
1818 fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
1819 let xml = minimal_signature_xml("", "");
1820 let err = VerifyContext::new()
1821 .key(&RejectingKey)
1822 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
1823 .verify(&xml)
1824 .expect_err("implicit default C14N must be checked against allowlist");
1825 assert!(matches!(
1826 err,
1827 SignatureVerificationPipelineError::DisallowedTransform { .. }
1828 ));
1829 }
1830
1831 #[test]
1832 fn verify_context_skips_resolver_when_reference_processing_fails() {
1833 let xml = minimal_signature_xml("", "");
1834 let result = VerifyContext::new()
1835 .key_resolver(&PanicResolver)
1836 .verify(&xml)
1837 .expect("reference digest mismatch should short-circuit before resolver");
1838 assert!(matches!(
1839 result.status,
1840 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1841 ));
1842 }
1843
1844 #[test]
1845 fn verify_context_reports_key_not_found_when_resolver_misses() {
1846 let xml = signature_with_target_reference("AQ==");
1847 let result = VerifyContext::new()
1848 .key_resolver(&MissingKeyResolver)
1849 .verify(&xml)
1850 .expect("resolver miss should report status, not pipeline error");
1851 assert!(matches!(
1852 result.status,
1853 DsigStatus::Invalid(FailureReason::KeyNotFound)
1854 ));
1855 assert_eq!(
1856 result.signed_info_references.len(),
1857 1,
1858 "KeyNotFound path must preserve SignedInfo reference diagnostics",
1859 );
1860 assert!(matches!(
1861 result.signed_info_references[0].status,
1862 DsigStatus::Valid
1863 ));
1864 }
1865
1866 #[test]
1867 fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
1868 let xml = signature_with_target_reference("@@@");
1869
1870 let err = VerifyContext::new()
1871 .key_resolver(&MissingKeyResolver)
1872 .verify(&xml)
1873 .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
1874 assert!(matches!(
1875 err,
1876 SignatureVerificationPipelineError::SignatureValueBase64(_)
1877 ));
1878 }
1879
1880 #[test]
1881 fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
1882 let xml = signature_with_target_reference("@@@");
1883
1884 let err = VerifyContext::new()
1885 .verify(&xml)
1886 .expect_err("invalid SignatureValue must remain a decode error");
1887 assert!(matches!(
1888 err,
1889 SignatureVerificationPipelineError::SignatureValueBase64(_)
1890 ));
1891 }
1892
1893 #[test]
1894 fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
1895 let references = vec![Reference {
1896 uri: None,
1897 id: None,
1898 ref_type: None,
1899 transforms: vec![],
1900 digest_method: DigestAlgorithm::Sha256,
1901 digest_value: vec![0; 32],
1902 }];
1903 let uri_types = UriTypeSet {
1904 allow_empty: false,
1905 allow_same_document: true,
1906 allow_external: false,
1907 };
1908
1909 let err = enforce_reference_policies(&references, uri_types, None)
1910 .expect_err("missing URI must fail before allow_empty policy is evaluated");
1911 assert!(matches!(
1912 err,
1913 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
1914 ));
1915 }
1916
1917 #[test]
1918 fn push_normalized_signature_text_rejects_form_feed() {
1919 let mut normalized = String::new();
1920 let mut raw_text_len = 0usize;
1921 let err =
1922 push_normalized_signature_text("ab\u{000C}cd", &mut raw_text_len, &mut normalized)
1923 .expect_err("form-feed must not be treated as XML base64 whitespace");
1924 assert!(matches!(
1925 err,
1926 SignatureVerificationPipelineError::SignatureValueBase64(
1927 base64::DecodeError::InvalidByte(_, 0x0C)
1928 )
1929 ));
1930 }
1931
1932 #[test]
1933 fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
1934 let mut normalized = "A".repeat(MAX_SIGNATURE_VALUE_LEN - 1);
1935 let mut raw_text_len = normalized.len();
1936 let err = push_normalized_signature_text("é", &mut raw_text_len, &mut normalized)
1937 .expect_err("multibyte characters must not bypass byte-size limit");
1938 assert!(matches!(
1939 err,
1940 SignatureVerificationPipelineError::InvalidStructure {
1941 reason: "SignatureValue exceeds maximum allowed length"
1942 }
1943 ));
1944 }
1945
1946 #[test]
1949 fn reference_with_correct_digest_passes() {
1950 let xml = r##"<root>
1953 <data>hello world</data>
1954 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
1955 <ds:SignedInfo/>
1956 </ds:Signature>
1957 </root>"##;
1958 let doc = Document::parse(xml).unwrap();
1959 let resolver = UriReferenceResolver::new(&doc);
1960 let sig_node = doc
1961 .descendants()
1962 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
1963 .unwrap();
1964
1965 let initial_data = resolver.dereference("").unwrap();
1967 let transforms = vec![
1968 Transform::Enveloped,
1969 Transform::C14n(
1970 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
1971 .unwrap(),
1972 ),
1973 ];
1974 let pre_digest_bytes =
1975 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
1976 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
1977
1978 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
1980
1981 let result = process_reference(
1982 &reference,
1983 &resolver,
1984 sig_node,
1985 ReferenceSet::SignedInfo,
1986 0,
1987 false,
1988 )
1989 .unwrap();
1990 assert!(
1991 matches!(result.status, DsigStatus::Valid),
1992 "digest should match"
1993 );
1994 assert!(result.pre_digest_data.is_none());
1995 }
1996
1997 #[test]
1998 fn reference_with_wrong_digest_fails() {
1999 let xml = r##"<root>
2000 <data>hello</data>
2001 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2002 <ds:SignedInfo/>
2003 </ds:Signature>
2004 </root>"##;
2005 let doc = Document::parse(xml).unwrap();
2006 let resolver = UriReferenceResolver::new(&doc);
2007 let sig_node = doc
2008 .descendants()
2009 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2010 .unwrap();
2011
2012 let transforms = vec![Transform::Enveloped];
2013 let wrong_digest = vec![0u8; 32];
2015 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
2016
2017 let result = process_reference(
2018 &reference,
2019 &resolver,
2020 sig_node,
2021 ReferenceSet::SignedInfo,
2022 0,
2023 false,
2024 )
2025 .unwrap();
2026 assert!(matches!(
2027 result.status,
2028 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
2029 ));
2030 }
2031
2032 #[test]
2033 fn reference_with_wrong_digest_preserves_supplied_ref_index() {
2034 let xml = r##"<root>
2035 <data>hello</data>
2036 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2037 <ds:SignedInfo/>
2038 </ds:Signature>
2039 </root>"##;
2040 let doc = Document::parse(xml).unwrap();
2041 let resolver = UriReferenceResolver::new(&doc);
2042 let sig_node = doc
2043 .descendants()
2044 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2045 .unwrap();
2046
2047 let reference = make_reference(
2048 "",
2049 vec![Transform::Enveloped],
2050 DigestAlgorithm::Sha256,
2051 vec![0u8; 32],
2052 );
2053 let result = process_reference(
2054 &reference,
2055 &resolver,
2056 sig_node,
2057 ReferenceSet::SignedInfo,
2058 7,
2059 false,
2060 )
2061 .unwrap();
2062 assert!(matches!(
2063 result.status,
2064 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
2065 ));
2066 }
2067
2068 #[test]
2069 fn reference_stores_pre_digest_data() {
2070 let xml = "<root><child>text</child></root>";
2071 let doc = Document::parse(xml).unwrap();
2072 let resolver = UriReferenceResolver::new(&doc);
2073
2074 let initial_data = resolver.dereference("").unwrap();
2076 let pre_digest =
2077 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2078 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2079
2080 let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
2081 let result = process_reference(
2082 &reference,
2083 &resolver,
2084 doc.root_element(),
2085 ReferenceSet::SignedInfo,
2086 0,
2087 true,
2088 )
2089 .unwrap();
2090
2091 assert!(matches!(result.status, DsigStatus::Valid));
2092 assert!(result.pre_digest_data.is_some());
2093 assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
2094 }
2095
2096 #[test]
2099 fn reference_with_id_uri() {
2100 let xml = r##"<root>
2101 <item ID="target">specific content</item>
2102 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2103 <ds:SignedInfo/>
2104 </ds:Signature>
2105 </root>"##;
2106 let doc = Document::parse(xml).unwrap();
2107 let resolver = UriReferenceResolver::new(&doc);
2108 let sig_node = doc
2109 .descendants()
2110 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2111 .unwrap();
2112
2113 let initial_data = resolver.dereference("#target").unwrap();
2115 let transforms = vec![Transform::C14n(
2116 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
2117 .unwrap(),
2118 )];
2119 let pre_digest =
2120 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
2121 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2122
2123 let reference = make_reference(
2124 "#target",
2125 transforms,
2126 DigestAlgorithm::Sha256,
2127 expected_digest,
2128 );
2129 let result = process_reference(
2130 &reference,
2131 &resolver,
2132 sig_node,
2133 ReferenceSet::SignedInfo,
2134 0,
2135 false,
2136 )
2137 .unwrap();
2138 assert!(matches!(result.status, DsigStatus::Valid));
2139 }
2140
2141 #[test]
2142 fn reference_with_nonexistent_id_fails() {
2143 let xml = "<root><child/></root>";
2144 let doc = Document::parse(xml).unwrap();
2145 let resolver = UriReferenceResolver::new(&doc);
2146
2147 let reference =
2148 make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
2149 let result = process_reference(
2150 &reference,
2151 &resolver,
2152 doc.root_element(),
2153 ReferenceSet::SignedInfo,
2154 0,
2155 false,
2156 );
2157 assert!(result.is_err());
2158 }
2159
2160 #[test]
2161 fn reference_with_absent_uri_fails_closed() {
2162 let xml = "<root><child>text</child></root>";
2163 let doc = Document::parse(xml).unwrap();
2164 let resolver = UriReferenceResolver::new(&doc);
2165
2166 let reference = Reference {
2167 uri: None, id: None,
2169 ref_type: None,
2170 transforms: vec![],
2171 digest_method: DigestAlgorithm::Sha256,
2172 digest_value: vec![0; 32],
2173 };
2174
2175 let result = process_reference(
2176 &reference,
2177 &resolver,
2178 doc.root_element(),
2179 ReferenceSet::SignedInfo,
2180 0,
2181 false,
2182 );
2183 assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
2184 }
2185
2186 #[test]
2189 fn all_references_pass() {
2190 let xml = "<root><child>text</child></root>";
2191 let doc = Document::parse(xml).unwrap();
2192 let resolver = UriReferenceResolver::new(&doc);
2193
2194 let initial_data = resolver.dereference("").unwrap();
2196 let pre_digest =
2197 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2198 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2199
2200 let refs = vec![
2201 make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
2202 make_reference("", vec![], DigestAlgorithm::Sha256, digest),
2203 ];
2204
2205 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
2206 assert!(result.all_valid());
2207 assert_eq!(result.results.len(), 2);
2208 assert!(result.first_failure.is_none());
2209 }
2210
2211 #[test]
2212 fn fail_fast_on_first_mismatch() {
2213 let xml = "<root><child>text</child></root>";
2214 let doc = Document::parse(xml).unwrap();
2215 let resolver = UriReferenceResolver::new(&doc);
2216
2217 let wrong_digest = vec![0u8; 32];
2218 let refs = vec![
2219 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
2220 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
2222 ];
2223
2224 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
2225 assert!(!result.all_valid());
2226 assert_eq!(result.first_failure, Some(0));
2227 assert_eq!(result.results.len(), 1);
2229 assert!(matches!(
2230 result.results[0].status,
2231 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
2232 ));
2233 }
2234
2235 #[test]
2236 fn fail_fast_second_reference() {
2237 let xml = "<root><child>text</child></root>";
2238 let doc = Document::parse(xml).unwrap();
2239 let resolver = UriReferenceResolver::new(&doc);
2240
2241 let initial_data = resolver.dereference("").unwrap();
2243 let pre_digest =
2244 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2245 let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2246 let wrong_digest = vec![0u8; 32];
2247
2248 let refs = vec![
2249 make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
2250 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
2251 ];
2252
2253 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
2254 assert!(!result.all_valid());
2255 assert_eq!(result.first_failure, Some(1));
2256 assert_eq!(result.results.len(), 2);
2258 assert!(matches!(result.results[0].status, DsigStatus::Valid));
2259 assert!(matches!(
2260 result.results[1].status,
2261 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
2262 ));
2263 }
2264
2265 #[test]
2266 fn empty_references_list() {
2267 let xml = "<root/>";
2268 let doc = Document::parse(xml).unwrap();
2269 let resolver = UriReferenceResolver::new(&doc);
2270
2271 let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
2272 assert!(result.all_valid());
2273 assert!(result.results.is_empty());
2274 }
2275
2276 #[test]
2279 fn reference_sha1_digest() {
2280 let xml = "<root>content</root>";
2281 let doc = Document::parse(xml).unwrap();
2282 let resolver = UriReferenceResolver::new(&doc);
2283
2284 let initial_data = resolver.dereference("").unwrap();
2285 let pre_digest =
2286 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2287 let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
2288
2289 let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
2290 let result = process_reference(
2291 &reference,
2292 &resolver,
2293 doc.root_element(),
2294 ReferenceSet::SignedInfo,
2295 0,
2296 false,
2297 )
2298 .unwrap();
2299 assert!(matches!(result.status, DsigStatus::Valid));
2300 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
2301 }
2302
2303 #[test]
2304 fn reference_sha512_digest() {
2305 let xml = "<root>content</root>";
2306 let doc = Document::parse(xml).unwrap();
2307 let resolver = UriReferenceResolver::new(&doc);
2308
2309 let initial_data = resolver.dereference("").unwrap();
2310 let pre_digest =
2311 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2312 let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
2313
2314 let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
2315 let result = process_reference(
2316 &reference,
2317 &resolver,
2318 doc.root_element(),
2319 ReferenceSet::SignedInfo,
2320 0,
2321 false,
2322 )
2323 .unwrap();
2324 assert!(matches!(result.status, DsigStatus::Valid));
2325 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
2326 }
2327
2328 #[test]
2331 fn saml_enveloped_reference_processing() {
2332 let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
2334 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
2335 ID="_resp1">
2336 <saml:Assertion ID="_assert1">
2337 <saml:Subject>user@example.com</saml:Subject>
2338 </saml:Assertion>
2339 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2340 <ds:SignedInfo>
2341 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2342 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2343 <ds:Reference URI="">
2344 <ds:Transforms>
2345 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
2346 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2347 </ds:Transforms>
2348 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2349 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
2350 </ds:Reference>
2351 </ds:SignedInfo>
2352 <ds:SignatureValue>fakesig==</ds:SignatureValue>
2353 </ds:Signature>
2354 </samlp:Response>"##;
2355 let doc = Document::parse(xml).unwrap();
2356 let resolver = UriReferenceResolver::new(&doc);
2357 let sig_node = doc
2358 .descendants()
2359 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2360 .unwrap();
2361
2362 let signed_info_node = sig_node
2364 .children()
2365 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
2366 .unwrap();
2367 let signed_info = parse_signed_info(signed_info_node).unwrap();
2368 let reference = &signed_info.references[0];
2369
2370 let initial_data = resolver.dereference("").unwrap();
2372 let pre_digest =
2373 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
2374 .unwrap();
2375 let correct_digest = compute_digest(reference.digest_method, &pre_digest);
2376
2377 let corrected_ref = make_reference(
2379 "",
2380 reference.transforms.clone(),
2381 reference.digest_method,
2382 correct_digest,
2383 );
2384
2385 let result = process_reference(
2387 &corrected_ref,
2388 &resolver,
2389 sig_node,
2390 ReferenceSet::SignedInfo,
2391 0,
2392 true,
2393 )
2394 .unwrap();
2395 assert!(
2396 matches!(result.status, DsigStatus::Valid),
2397 "SAML reference should verify"
2398 );
2399 assert!(result.pre_digest_data.is_some());
2400
2401 let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
2403 assert!(
2404 pre_digest_str.contains("samlp:Response"),
2405 "pre-digest should contain Response"
2406 );
2407 assert!(
2408 !pre_digest_str.contains("SignatureValue"),
2409 "pre-digest should NOT contain Signature"
2410 );
2411 }
2412
2413 #[test]
2414 fn pipeline_missing_signed_info_returns_missing_element() {
2415 let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
2416
2417 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
2418 .expect_err("missing SignedInfo must fail before crypto stage");
2419 assert!(matches!(
2420 err,
2421 SignatureVerificationPipelineError::MissingElement {
2422 element: "SignedInfo"
2423 }
2424 ));
2425 }
2426
2427 #[test]
2428 fn pipeline_multiple_signature_elements_are_rejected() {
2429 let xml = r#"
2430<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2431 <ds:Signature>
2432 <ds:SignedInfo/>
2433 </ds:Signature>
2434 <ds:Signature/>
2435</root>
2436"#;
2437
2438 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
2439 .expect_err("multiple signatures must fail closed");
2440 assert!(matches!(
2441 err,
2442 SignatureVerificationPipelineError::InvalidStructure {
2443 reason: "Signature must appear exactly once in document",
2444 }
2445 ));
2446 }
2447}