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_key_info, 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};
29use super::whitespace::{is_xml_whitespace_only, normalize_xml_base64_text};
30
31const MAX_SIGNATURE_VALUE_LEN: usize = 8192;
32const MAX_SIGNATURE_VALUE_TEXT_LEN: usize = 65_536;
33pub trait VerifyingKey {
38 fn verify(
40 &self,
41 algorithm: SignatureAlgorithm,
42 signed_data: &[u8],
43 signature_value: &[u8],
44 ) -> Result<bool, DsigError>;
45}
46
47pub trait KeyResolver {
52 fn resolve<'a>(&'a self, xml: &str) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError>;
59
60 fn consumes_document_key_info(&self) -> bool {
67 false
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77#[must_use = "pass the policy to VerifyContext::allowed_uri_types(), or store it for reuse"]
78pub struct UriTypeSet {
79 allow_empty: bool,
80 allow_same_document: bool,
81 allow_external: bool,
82}
83
84impl UriTypeSet {
85 pub const fn new(allow_empty: bool, allow_same_document: bool, allow_external: bool) -> Self {
87 Self {
88 allow_empty,
89 allow_same_document,
90 allow_external,
91 }
92 }
93
94 pub const SAME_DOCUMENT: Self = Self {
96 allow_empty: true,
97 allow_same_document: true,
98 allow_external: false,
99 };
100
101 pub const ALL: Self = Self {
106 allow_empty: true,
107 allow_same_document: true,
108 allow_external: true,
109 };
110
111 fn allows(self, uri: &str) -> bool {
112 if uri.is_empty() {
113 return self.allow_empty;
114 }
115 if uri.starts_with('#') {
116 return self.allow_same_document;
117 }
118 self.allow_external
119 }
120}
121
122impl Default for UriTypeSet {
123 fn default() -> Self {
124 Self::SAME_DOCUMENT
125 }
126}
127
128#[must_use = "configure the context and call verify(), or store it for reuse"]
130pub struct VerifyContext<'a> {
131 key: Option<&'a dyn VerifyingKey>,
132 key_resolver: Option<&'a dyn KeyResolver>,
133 process_manifests: bool,
134 allowed_uri_types: UriTypeSet,
135 allowed_transforms: Option<HashSet<String>>,
136 store_pre_digest: bool,
137}
138
139impl<'a> VerifyContext<'a> {
140 pub fn new() -> Self {
149 Self {
150 key: None,
151 key_resolver: None,
152 process_manifests: false,
153 allowed_uri_types: UriTypeSet::default(),
154 allowed_transforms: None,
155 store_pre_digest: false,
156 }
157 }
158
159 pub fn key(mut self, key: &'a dyn VerifyingKey) -> Self {
161 self.key = Some(key);
162 self
163 }
164
165 pub fn key_resolver(mut self, resolver: &'a dyn KeyResolver) -> Self {
167 self.key_resolver = Some(resolver);
168 self
169 }
170
171 pub fn process_manifests(mut self, enabled: bool) -> Self {
197 self.process_manifests = enabled;
198 self
199 }
200
201 pub fn allowed_uri_types(mut self, types: UriTypeSet) -> Self {
203 self.allowed_uri_types = types;
204 self
205 }
206
207 pub fn allowed_transforms<I, S>(mut self, transforms: I) -> Self
218 where
219 I: IntoIterator<Item = S>,
220 S: Into<String>,
221 {
222 self.allowed_transforms = Some(transforms.into_iter().map(Into::into).collect());
223 self
224 }
225
226 pub fn store_pre_digest(mut self, enabled: bool) -> Self {
228 self.store_pre_digest = enabled;
229 self
230 }
231
232 fn allowed_transform_uris(&self) -> Option<&HashSet<String>> {
233 self.allowed_transforms.as_ref()
234 }
235
236 pub fn verify(&self, xml: &str) -> Result<VerifyResult, DsigError> {
242 verify_signature_with_context(xml, self)
243 }
244}
245
246impl Default for VerifyContext<'_> {
247 fn default() -> Self {
248 Self::new()
249 }
250}
251
252#[derive(Debug)]
254#[non_exhaustive]
255#[must_use = "inspect status before accepting the reference result"]
256pub struct ReferenceResult {
257 pub reference_set: ReferenceSet,
259 pub reference_index: usize,
261 pub uri: String,
263 pub digest_algorithm: DigestAlgorithm,
265 pub status: DsigStatus,
267 pub pre_digest_data: Option<Vec<u8>>,
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273#[non_exhaustive]
274pub enum ReferenceSet {
275 SignedInfo,
277 Manifest,
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283#[non_exhaustive]
284pub enum DsigStatus {
285 Valid,
287 Invalid(FailureReason),
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293#[non_exhaustive]
294pub enum FailureReason {
295 ReferenceDigestMismatch {
297 ref_index: usize,
307 },
308 ReferencePolicyViolation {
310 ref_index: usize,
312 },
313 ReferenceProcessingFailure {
315 ref_index: usize,
317 },
318 SignatureMismatch,
320 KeyNotFound,
322}
323
324#[derive(Debug)]
326#[non_exhaustive]
327#[must_use = "check first_failure/results before accepting the reference set"]
328pub struct ReferencesResult {
329 pub results: Vec<ReferenceResult>,
332 pub first_failure: Option<usize>,
334}
335
336impl ReferencesResult {
337 #[must_use]
339 pub fn all_valid(&self) -> bool {
340 self.results
341 .iter()
342 .all(|result| matches!(result.status, DsigStatus::Valid))
343 }
344}
345
346pub fn process_reference(
364 reference: &Reference,
365 resolver: &UriReferenceResolver<'_>,
366 signature_node: Node<'_, '_>,
367 reference_set: ReferenceSet,
368 reference_index: usize,
369 store_pre_digest: bool,
370) -> Result<ReferenceResult, ReferenceProcessingError> {
371 let uri = reference
374 .uri
375 .as_deref()
376 .ok_or(ReferenceProcessingError::MissingUri)?;
377 let initial_data = resolver
378 .dereference(uri)
379 .map_err(ReferenceProcessingError::UriDereference)?;
380
381 let pre_digest_bytes = execute_transforms(signature_node, initial_data, &reference.transforms)
383 .map_err(ReferenceProcessingError::Transform)?;
384
385 let computed_digest = compute_digest(reference.digest_method, &pre_digest_bytes);
387
388 let status = if constant_time_eq(&computed_digest, &reference.digest_value) {
390 DsigStatus::Valid
391 } else {
392 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch {
393 ref_index: reference_index,
394 })
395 };
396
397 Ok(ReferenceResult {
398 reference_set,
399 reference_index,
400 uri: uri.to_owned(),
401 digest_algorithm: reference.digest_method,
402 status,
403 pre_digest_data: if store_pre_digest {
404 Some(pre_digest_bytes)
405 } else {
406 None
407 },
408 })
409}
410
411pub fn process_all_references(
423 references: &[Reference],
424 resolver: &UriReferenceResolver<'_>,
425 signature_node: Node<'_, '_>,
426 store_pre_digest: bool,
427) -> Result<ReferencesResult, ReferenceProcessingError> {
428 let mut results = Vec::with_capacity(references.len());
429
430 for (i, reference) in references.iter().enumerate() {
431 let result = process_reference(
432 reference,
433 resolver,
434 signature_node,
435 ReferenceSet::SignedInfo,
436 i,
437 store_pre_digest,
438 )?;
439 let failed = matches!(result.status, DsigStatus::Invalid(_));
440 results.push(result);
441
442 if failed {
443 return Ok(ReferencesResult {
444 results,
445 first_failure: Some(i),
446 });
447 }
448 }
449
450 Ok(ReferencesResult {
451 results,
452 first_failure: None,
453 })
454}
455
456#[derive(Debug, thiserror::Error)]
460#[non_exhaustive]
461pub enum ReferenceProcessingError {
462 #[error("reference URI is required; omitted URI references are not supported")]
464 MissingUri,
465
466 #[error("URI dereference failed: {0}")]
468 UriDereference(#[source] super::types::TransformError),
469
470 #[error("transform failed: {0}")]
472 Transform(#[source] super::types::TransformError),
473}
474
475#[derive(Debug)]
477#[non_exhaustive]
478#[must_use = "inspect status before accepting the document"]
479pub struct VerifyResult {
480 pub status: DsigStatus,
482 pub signed_info_references: Vec<ReferenceResult>,
486 pub manifest_references: Vec<ReferenceResult>,
493 pub canonicalized_signed_info: Option<Vec<u8>>,
496}
497
498#[derive(Debug, thiserror::Error)]
500#[non_exhaustive]
501pub enum DsigError {
502 #[error("XML parse error: {0}")]
504 XmlParse(#[from] roxmltree::Error),
505
506 #[error("missing required element: <{element}>")]
508 MissingElement {
509 element: &'static str,
511 },
512
513 #[error("invalid Signature structure: {reason}")]
515 InvalidStructure {
516 reason: &'static str,
518 },
519
520 #[error("failed to parse SignedInfo: {0}")]
522 ParseSignedInfo(#[from] super::parse::ParseError),
523
524 #[error("failed to parse KeyInfo: {0}")]
526 ParseKeyInfo(#[source] super::parse::ParseError),
527
528 #[error("failed to parse Manifest reference: {0}")]
530 ParseManifestReference(#[source] ParseError),
531
532 #[error("reference processing failed: {0}")]
534 Reference(#[from] ReferenceProcessingError),
535
536 #[error("SignedInfo canonicalization failed: {0}")]
538 Canonicalization(#[from] crate::c14n::C14nError),
539
540 #[error("invalid SignatureValue base64: {0}")]
542 SignatureValueBase64(#[from] base64::DecodeError),
543
544 #[error("signature verification failed: {0}")]
546 Crypto(#[from] SignatureVerificationError),
547
548 #[error("reference URI is not allowed by policy: {uri}")]
550 DisallowedUri {
551 uri: String,
553 },
554
555 #[error("transform is not allowed by policy: {algorithm}")]
557 DisallowedTransform {
558 algorithm: String,
560 },
561}
562
563type SignatureVerificationPipelineError = DsigError;
564
565pub fn verify_signature_with_pem_key(
593 xml: &str,
594 public_key_pem: &str,
595 store_pre_digest: bool,
596) -> Result<VerifyResult, DsigError> {
597 struct PemVerifyingKey<'a> {
598 public_key_pem: &'a str,
599 }
600
601 impl VerifyingKey for PemVerifyingKey<'_> {
602 fn verify(
603 &self,
604 algorithm: SignatureAlgorithm,
605 signed_data: &[u8],
606 signature_value: &[u8],
607 ) -> Result<bool, DsigError> {
608 verify_with_algorithm(algorithm, self.public_key_pem, signed_data, signature_value)
609 }
610 }
611
612 let key = PemVerifyingKey { public_key_pem };
613 VerifyContext::new()
614 .key(&key)
615 .store_pre_digest(store_pre_digest)
616 .verify(xml)
617}
618
619fn verify_signature_with_context(
620 xml: &str,
621 ctx: &VerifyContext<'_>,
622) -> Result<VerifyResult, SignatureVerificationPipelineError> {
623 let doc = Document::parse(xml)?;
624 let mut signatures = doc.descendants().filter(|node| {
625 node.is_element()
626 && node.tag_name().name() == "Signature"
627 && node.tag_name().namespace() == Some(XMLDSIG_NS)
628 });
629 let signature_node = match (signatures.next(), signatures.next()) {
630 (None, _) => {
631 return Err(SignatureVerificationPipelineError::MissingElement {
632 element: "Signature",
633 });
634 }
635 (Some(node), None) => node,
636 (Some(_), Some(_)) => {
637 return Err(SignatureVerificationPipelineError::InvalidStructure {
638 reason: "Signature must appear exactly once in document",
639 });
640 }
641 };
642
643 let signature_children = parse_signature_children(signature_node)?;
644 let signed_info_node = signature_children.signed_info_node;
645 let should_parse_key_info = match (ctx.key, ctx.key_resolver) {
646 (Some(_), _) => false,
647 (None, Some(resolver)) => resolver.consumes_document_key_info(),
648 (None, None) => true,
649 };
650 if should_parse_key_info && let Some(key_info_node) = signature_children.key_info_node {
651 parse_key_info(key_info_node).map_err(SignatureVerificationPipelineError::ParseKeyInfo)?;
653 }
654
655 let signed_info = parse_signed_info(signed_info_node)?;
656 enforce_reference_policies(
657 &signed_info.references,
658 ctx.allowed_uri_types,
659 ctx.allowed_transform_uris(),
660 )?;
661
662 let resolver = UriReferenceResolver::new(&doc);
663 let references = process_all_references(
664 &signed_info.references,
665 &resolver,
666 signature_node,
667 ctx.store_pre_digest,
668 )?;
669
670 let manifest_references = if ctx.process_manifests {
671 let signed_info_reference_nodes =
672 collect_signed_info_reference_nodes(&signed_info.references, &resolver);
673 process_manifest_references(signature_node, &resolver, ctx, &signed_info_reference_nodes)?
674 } else {
675 Vec::new()
676 };
677
678 if let Some(first_failure) = references.first_failure {
679 let status = references.results[first_failure].status;
680 return Ok(VerifyResult {
681 status,
682 signed_info_references: references.results,
683 manifest_references,
684 canonicalized_signed_info: None,
685 });
686 }
687
688 let signed_info_subtree: HashSet<_> = signed_info_node
689 .descendants()
690 .map(|node: Node<'_, '_>| node.id())
691 .collect();
692 let mut canonical_signed_info = Vec::new();
693 canonicalize(
694 &doc,
695 Some(&|node| signed_info_subtree.contains(&node.id())),
696 &signed_info.c14n_method,
697 &mut canonical_signed_info,
698 )?;
699
700 let signature_value = decode_signature_value(signature_children.signature_value_node)?;
701 let Some(resolved_key) = resolve_verifying_key(ctx, xml)? else {
702 return Ok(VerifyResult {
703 status: DsigStatus::Invalid(FailureReason::KeyNotFound),
704 signed_info_references: references.results,
705 manifest_references,
706 canonicalized_signed_info: if ctx.store_pre_digest {
707 Some(canonical_signed_info)
708 } else {
709 None
710 },
711 });
712 };
713 let verifier = resolved_key.as_ref();
714 let signature_valid = verifier.verify(
715 signed_info.signature_method,
716 &canonical_signed_info,
717 &signature_value,
718 )?;
719
720 Ok(VerifyResult {
721 status: if signature_valid {
722 DsigStatus::Valid
723 } else {
724 DsigStatus::Invalid(FailureReason::SignatureMismatch)
725 },
726 signed_info_references: references.results,
727 manifest_references,
728 canonicalized_signed_info: if ctx.store_pre_digest {
729 Some(canonical_signed_info)
730 } else {
731 None
732 },
733 })
734}
735
736fn process_manifest_references(
737 signature_node: Node<'_, '_>,
738 resolver: &UriReferenceResolver<'_>,
739 ctx: &VerifyContext<'_>,
740 signed_info_reference_nodes: &HashSet<NodeId>,
741) -> Result<Vec<ReferenceResult>, SignatureVerificationPipelineError> {
742 let manifest_references =
743 parse_manifest_references(signature_node, signed_info_reference_nodes)?;
744 if manifest_references.is_empty() {
745 return Ok(Vec::new());
746 }
747 let mut results = Vec::with_capacity(manifest_references.len());
748 for (index, reference) in manifest_references.iter().enumerate() {
749 match enforce_reference_policies(
750 std::slice::from_ref(reference),
751 ctx.allowed_uri_types,
752 ctx.allowed_transform_uris(),
753 ) {
754 Ok(()) => {}
755 Err(
756 SignatureVerificationPipelineError::DisallowedUri { .. }
757 | SignatureVerificationPipelineError::DisallowedTransform { .. },
758 ) => {
759 results.push(manifest_reference_invalid_result(
760 reference,
761 index,
762 FailureReason::ReferencePolicyViolation { ref_index: index },
763 ));
764 continue;
765 }
766 Err(SignatureVerificationPipelineError::Reference(
767 ReferenceProcessingError::MissingUri,
768 )) => {
769 results.push(manifest_reference_invalid_result(
770 reference,
771 index,
772 FailureReason::ReferenceProcessingFailure { ref_index: index },
773 ));
774 continue;
775 }
776 Err(_) => {
777 results.push(manifest_reference_invalid_result(
780 reference,
781 index,
782 FailureReason::ReferenceProcessingFailure { ref_index: index },
783 ));
784 continue;
785 }
786 }
787
788 match process_reference(
789 reference,
790 resolver,
791 signature_node,
792 ReferenceSet::Manifest,
793 index,
794 ctx.store_pre_digest,
795 ) {
796 Ok(result) => results.push(result),
797 Err(_) => results.push(manifest_reference_invalid_result(
798 reference,
799 index,
800 FailureReason::ReferenceProcessingFailure { ref_index: index },
801 )),
802 }
803 }
804 Ok(results)
805}
806
807fn manifest_reference_invalid_result(
808 reference: &Reference,
809 index: usize,
810 reason: FailureReason,
811) -> ReferenceResult {
812 ReferenceResult {
813 reference_set: ReferenceSet::Manifest,
814 reference_index: index,
815 uri: reference
816 .uri
817 .clone()
818 .unwrap_or_else(|| "<omitted>".to_owned()),
819 digest_algorithm: reference.digest_method,
820 status: DsigStatus::Invalid(reason),
821 pre_digest_data: None,
822 }
823}
824
825fn parse_manifest_references(
826 signature_node: Node<'_, '_>,
827 signed_info_reference_nodes: &HashSet<NodeId>,
828) -> Result<Vec<Reference>, SignatureVerificationPipelineError> {
829 let mut references = Vec::new();
830 for object_node in signature_node.children().filter(|node| {
831 node.is_element()
832 && node.tag_name().namespace() == Some(XMLDSIG_NS)
833 && node.tag_name().name() == "Object"
834 }) {
835 let object_is_signed = signed_info_reference_nodes.contains(&object_node.id());
836 for manifest_node in object_node.children().filter(|node| {
837 node.is_element()
838 && node.tag_name().namespace() == Some(XMLDSIG_NS)
839 && node.tag_name().name() == "Manifest"
840 }) {
841 let manifest_is_signed = signed_info_reference_nodes.contains(&manifest_node.id());
842 if !object_is_signed && !manifest_is_signed {
843 continue;
844 }
845 let mut manifest_children = Vec::new();
846 for child in manifest_node.children() {
847 if child.is_text()
848 && child.text().is_some_and(|text| {
849 text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
850 })
851 {
852 return Err(SignatureVerificationPipelineError::InvalidStructure {
853 reason: "Manifest contains non-whitespace mixed content",
854 });
855 }
856 if child.is_element() {
857 manifest_children.push(child);
858 }
859 }
860 if manifest_children.is_empty() {
861 return Err(SignatureVerificationPipelineError::InvalidStructure {
862 reason: "Manifest must contain at least one ds:Reference element child",
863 });
864 }
865 for child in manifest_children {
866 if child.tag_name().namespace() != Some(XMLDSIG_NS)
867 || child.tag_name().name() != "Reference"
868 {
869 return Err(SignatureVerificationPipelineError::InvalidStructure {
870 reason: "Manifest must contain only ds:Reference element children",
871 });
872 }
873 references.push(
874 parse_reference(child)
875 .map_err(SignatureVerificationPipelineError::ParseManifestReference)?,
876 );
877 }
878 }
879 }
880 Ok(references)
881}
882
883fn collect_signed_info_reference_nodes(
884 references: &[Reference],
885 resolver: &UriReferenceResolver<'_>,
886) -> HashSet<NodeId> {
887 references
888 .iter()
889 .filter_map(|reference| reference.uri.as_deref())
890 .filter_map(signed_info_reference_id_from_uri)
891 .filter_map(|id| resolver.node_id_for_id(id))
892 .collect()
893}
894
895fn signed_info_reference_id_from_uri(uri: &str) -> Option<&str> {
896 let fragment = uri.strip_prefix('#')?;
897 if fragment.is_empty() || fragment == "xpointer(/)" {
898 return None;
899 }
900 if let Some(id) = parse_xpointer_id_fragment(fragment) {
901 return (!id.is_empty()).then_some(id);
902 }
903 (!fragment.starts_with("xpointer(")).then_some(fragment)
904}
905
906enum ResolvedVerifyingKey<'a> {
907 Borrowed(&'a dyn VerifyingKey),
908 Owned(Box<dyn VerifyingKey + 'a>),
909}
910
911impl ResolvedVerifyingKey<'_> {
912 fn as_ref(&self) -> &dyn VerifyingKey {
913 match self {
914 Self::Borrowed(key) => *key,
915 Self::Owned(key) => key.as_ref(),
916 }
917 }
918}
919
920fn resolve_verifying_key<'k>(
921 ctx: &VerifyContext<'k>,
922 xml: &str,
923) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
924 if let Some(key) = ctx.key {
925 return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
926 }
927 if let Some(resolver) = ctx.key_resolver {
928 let resolved = resolver.resolve(xml)?;
929 return Ok(resolved.map(ResolvedVerifyingKey::Owned));
930 }
931 Ok(None)
932}
933
934fn enforce_reference_policies(
935 references: &[Reference],
936 allowed_uri_types: UriTypeSet,
937 allowed_transforms: Option<&HashSet<String>>,
938) -> Result<(), SignatureVerificationPipelineError> {
939 for reference in references {
940 let uri = reference
941 .uri
942 .as_deref()
943 .ok_or(SignatureVerificationPipelineError::Reference(
944 ReferenceProcessingError::MissingUri,
945 ))?;
946 if !allowed_uri_types.allows(uri) {
947 return Err(SignatureVerificationPipelineError::DisallowedUri {
948 uri: uri.to_owned(),
949 });
950 }
951
952 if let Some(allowed) = allowed_transforms {
953 for transform in &reference.transforms {
954 let transform_uri = transform_uri(transform);
955 if !allowed.contains(transform_uri) {
956 return Err(SignatureVerificationPipelineError::DisallowedTransform {
957 algorithm: transform_uri.to_owned(),
958 });
959 }
960 }
961
962 let has_explicit_c14n = reference
963 .transforms
964 .iter()
965 .any(|transform| matches!(transform, Transform::C14n(_)));
966 if !has_explicit_c14n && !allowed.contains(DEFAULT_IMPLICIT_C14N_URI) {
967 return Err(SignatureVerificationPipelineError::DisallowedTransform {
968 algorithm: DEFAULT_IMPLICIT_C14N_URI.to_owned(),
969 });
970 }
971 }
972 }
973 Ok(())
974}
975
976fn transform_uri(transform: &Transform) -> &'static str {
977 match transform {
978 Transform::Enveloped => super::transforms::ENVELOPED_SIGNATURE_URI,
979 Transform::XpathExcludeAllSignatures => XPATH_TRANSFORM_URI,
980 Transform::C14n(algo) => algo.uri(),
981 }
982}
983
984#[derive(Debug, Clone, Copy)]
985struct SignatureChildNodes<'a, 'input> {
986 signed_info_node: Node<'a, 'input>,
987 signature_value_node: Node<'a, 'input>,
988 key_info_node: Option<Node<'a, 'input>>,
989}
990
991fn parse_signature_children<'a, 'input>(
992 signature_node: Node<'a, 'input>,
993) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
994 let mut signed_info_node: Option<Node<'_, '_>> = None;
995 let mut signature_value_node: Option<Node<'_, '_>> = None;
996 let mut key_info_node: Option<Node<'_, '_>> = None;
997 let mut signed_info_index: Option<usize> = None;
998 let mut signature_value_index: Option<usize> = None;
999 let mut key_info_index: Option<usize> = None;
1000 let mut first_unexpected_dsig_index: Option<usize> = None;
1001
1002 let mut element_index = 0usize;
1003 for child in signature_node.children() {
1004 if child.is_text() {
1005 if child
1006 .text()
1007 .is_some_and(|text| !is_xml_whitespace_only(text))
1008 {
1009 return Err(SignatureVerificationPipelineError::InvalidStructure {
1010 reason: "Signature must not contain non-whitespace mixed content",
1011 });
1012 }
1013 continue;
1014 }
1015 if !child.is_element() {
1016 continue;
1017 }
1018
1019 element_index += 1;
1020 if child.tag_name().namespace() != Some(XMLDSIG_NS) {
1021 return Err(SignatureVerificationPipelineError::InvalidStructure {
1022 reason: "Signature must contain only XMLDSIG element children",
1023 });
1024 }
1025 match child.tag_name().name() {
1026 "SignedInfo" => {
1027 if signed_info_node.is_some() {
1028 return Err(SignatureVerificationPipelineError::InvalidStructure {
1029 reason: "SignedInfo must appear exactly once under Signature",
1030 });
1031 }
1032 signed_info_node = Some(child);
1033 signed_info_index = Some(element_index);
1034 }
1035 "SignatureValue" => {
1036 if signature_value_node.is_some() {
1037 return Err(SignatureVerificationPipelineError::InvalidStructure {
1038 reason: "SignatureValue must appear exactly once under Signature",
1039 });
1040 }
1041 signature_value_node = Some(child);
1042 signature_value_index = Some(element_index);
1043 }
1044 "KeyInfo" => {
1045 if key_info_node.is_some() {
1046 return Err(SignatureVerificationPipelineError::InvalidStructure {
1047 reason: "KeyInfo must appear at most once under Signature",
1048 });
1049 }
1050 key_info_node = Some(child);
1051 key_info_index = Some(element_index);
1052 }
1053 "Object" => {
1054 }
1057 _ => {
1058 if first_unexpected_dsig_index.is_none() {
1059 first_unexpected_dsig_index = Some(element_index);
1060 }
1061 }
1062 }
1063 }
1064
1065 let signed_info_node =
1066 signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
1067 element: "SignedInfo",
1068 })?;
1069 let signature_value_node =
1070 signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
1071 element: "SignatureValue",
1072 })?;
1073 if signed_info_index != Some(1) {
1074 return Err(SignatureVerificationPipelineError::InvalidStructure {
1075 reason: "SignedInfo must be the first element child of Signature",
1076 });
1077 }
1078 if signature_value_index != Some(2) {
1079 return Err(SignatureVerificationPipelineError::InvalidStructure {
1080 reason: "SignatureValue must be the second element child of Signature",
1081 });
1082 }
1083 if let Some(index) = key_info_index
1084 && index != 3
1085 {
1086 return Err(SignatureVerificationPipelineError::InvalidStructure {
1087 reason: "KeyInfo must be the third element child of Signature when present",
1088 });
1089 }
1090
1091 let allowed_prefix_end = key_info_index.unwrap_or(2);
1092 if let Some(unexpected_index) = first_unexpected_dsig_index {
1093 return Err(SignatureVerificationPipelineError::InvalidStructure {
1094 reason: if unexpected_index > allowed_prefix_end {
1095 "After SignedInfo, SignatureValue, and optional KeyInfo, Signature may contain only Object elements"
1096 } else {
1097 "Signature may contain SignedInfo first, SignatureValue second, optional KeyInfo third, and Object elements thereafter"
1098 },
1099 });
1100 }
1101
1102 Ok(SignatureChildNodes {
1103 signed_info_node,
1104 signature_value_node,
1105 key_info_node,
1106 })
1107}
1108
1109fn decode_signature_value(
1110 signature_value_node: Node<'_, '_>,
1111) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
1112 if signature_value_node
1113 .children()
1114 .any(|child| child.is_element())
1115 {
1116 return Err(SignatureVerificationPipelineError::InvalidStructure {
1117 reason: "SignatureValue must not contain element children",
1118 });
1119 }
1120
1121 let mut normalized = String::new();
1122 let mut raw_text_len = 0usize;
1123 for child in signature_value_node
1124 .children()
1125 .filter(|child| child.is_text())
1126 {
1127 if let Some(text) = child.text() {
1128 push_normalized_signature_text(text, &mut raw_text_len, &mut normalized)?;
1129 }
1130 }
1131
1132 Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
1133}
1134
1135fn push_normalized_signature_text(
1136 text: &str,
1137 raw_text_len: &mut usize,
1138 normalized: &mut String,
1139) -> Result<(), SignatureVerificationPipelineError> {
1140 if raw_text_len.saturating_add(text.len()) > MAX_SIGNATURE_VALUE_TEXT_LEN {
1141 return Err(SignatureVerificationPipelineError::InvalidStructure {
1142 reason: "SignatureValue exceeds maximum allowed text length",
1143 });
1144 }
1145 *raw_text_len = raw_text_len.saturating_add(text.len());
1146
1147 normalize_xml_base64_text(text, normalized).map_err(|err| {
1148 SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(
1149 err.normalized_offset,
1150 err.invalid_byte,
1151 ))
1152 })?;
1153 if normalized.len() > MAX_SIGNATURE_VALUE_LEN {
1154 return Err(SignatureVerificationPipelineError::InvalidStructure {
1155 reason: "SignatureValue exceeds maximum allowed length",
1156 });
1157 }
1158
1159 Ok(())
1160}
1161
1162fn verify_with_algorithm(
1163 algorithm: SignatureAlgorithm,
1164 public_key_pem: &str,
1165 signed_data: &[u8],
1166 signature_value: &[u8],
1167) -> Result<bool, SignatureVerificationPipelineError> {
1168 match algorithm {
1169 SignatureAlgorithm::RsaSha1
1170 | SignatureAlgorithm::RsaSha256
1171 | SignatureAlgorithm::RsaSha384
1172 | SignatureAlgorithm::RsaSha512 => Ok(verify_rsa_signature_pem(
1173 algorithm,
1174 public_key_pem,
1175 signed_data,
1176 signature_value,
1177 )?),
1178 SignatureAlgorithm::EcdsaP256Sha256 | SignatureAlgorithm::EcdsaP384Sha384 => {
1179 match verify_ecdsa_signature_pem(
1183 algorithm,
1184 public_key_pem,
1185 signed_data,
1186 signature_value,
1187 ) {
1188 Ok(valid) => Ok(valid),
1189 Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
1190 Err(error) => Err(error.into()),
1191 }
1192 }
1193 }
1194}
1195
1196#[cfg(test)]
1197#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
1198mod tests {
1199 use super::*;
1200 use crate::xmldsig::digest::DigestAlgorithm;
1201 use crate::xmldsig::parse::{Reference, parse_signed_info};
1202 use crate::xmldsig::transforms::Transform;
1203 use crate::xmldsig::uri::UriReferenceResolver;
1204 use base64::Engine;
1205 use roxmltree::Document;
1206
1207 fn make_reference(
1211 uri: &str,
1212 transforms: Vec<Transform>,
1213 digest_method: DigestAlgorithm,
1214 digest_value: Vec<u8>,
1215 ) -> Reference {
1216 Reference {
1217 uri: Some(uri.to_string()),
1218 id: None,
1219 ref_type: None,
1220 transforms,
1221 digest_method,
1222 digest_value,
1223 }
1224 }
1225
1226 struct RejectingKey;
1227
1228 impl VerifyingKey for RejectingKey {
1229 fn verify(
1230 &self,
1231 _algorithm: SignatureAlgorithm,
1232 _signed_data: &[u8],
1233 _signature_value: &[u8],
1234 ) -> Result<bool, SignatureVerificationPipelineError> {
1235 Ok(false)
1236 }
1237 }
1238
1239 struct AcceptingKey;
1240
1241 impl VerifyingKey for AcceptingKey {
1242 fn verify(
1243 &self,
1244 _algorithm: SignatureAlgorithm,
1245 _signed_data: &[u8],
1246 _signature_value: &[u8],
1247 ) -> Result<bool, SignatureVerificationPipelineError> {
1248 Ok(true)
1249 }
1250 }
1251
1252 struct PanicResolver;
1253
1254 impl KeyResolver for PanicResolver {
1255 fn resolve<'a>(
1256 &'a self,
1257 _xml: &str,
1258 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
1259 {
1260 panic!("resolver should not be called when references already fail");
1261 }
1262 }
1263
1264 struct MissingKeyResolver;
1265
1266 impl KeyResolver for MissingKeyResolver {
1267 fn resolve<'a>(
1268 &'a self,
1269 _xml: &str,
1270 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
1271 {
1272 Ok(None)
1273 }
1274 }
1275
1276 struct ConsumingKeyInfoResolver;
1277
1278 impl KeyResolver for ConsumingKeyInfoResolver {
1279 fn resolve<'a>(
1280 &'a self,
1281 _xml: &str,
1282 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
1283 {
1284 Ok(None)
1285 }
1286
1287 fn consumes_document_key_info(&self) -> bool {
1288 true
1289 }
1290 }
1291
1292 fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
1293 format!(
1294 r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1295 <ds:SignedInfo>
1296 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1297 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1298 <ds:Reference URI="{reference_uri}">
1299 {transforms_xml}
1300 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1301 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
1302 </ds:Reference>
1303 </ds:SignedInfo>
1304 <ds:SignatureValue>AQ==</ds:SignatureValue>
1305</ds:Signature>"#
1306 )
1307 }
1308
1309 fn signature_with_target_reference(signature_value_b64: &str) -> String {
1310 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1311 <target ID="target">payload</target>
1312 <ds:Signature>
1313 <ds:SignedInfo>
1314 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1315 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1316 <ds:Reference URI="#target">
1317 <ds:Transforms>
1318 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1319 </ds:Transforms>
1320 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1321 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
1322 </ds:Reference>
1323 </ds:SignedInfo>
1324 <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
1325 </ds:Signature>
1326</root>"##;
1327
1328 let doc = Document::parse(xml_template).unwrap();
1329 let sig_node = doc
1330 .descendants()
1331 .find(|node| node.is_element() && node.tag_name().name() == "Signature")
1332 .unwrap();
1333 let signed_info_node = sig_node
1334 .children()
1335 .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
1336 .unwrap();
1337 let signed_info = parse_signed_info(signed_info_node).unwrap();
1338 let reference = &signed_info.references[0];
1339 let resolver = UriReferenceResolver::new(&doc);
1340 let initial_data = resolver
1341 .dereference(reference.uri.as_deref().unwrap())
1342 .unwrap();
1343 let pre_digest =
1344 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
1345 .unwrap();
1346 let digest = compute_digest(reference.digest_method, &pre_digest);
1347 let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
1348 xml_template
1349 .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
1350 .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
1351 }
1352
1353 #[test]
1354 fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
1355 let xml = signature_with_target_reference("AQ==");
1356
1357 let result = VerifyContext::new()
1358 .verify(&xml)
1359 .expect("missing key config must be reported as verification status");
1360 assert!(
1361 matches!(
1362 result.status,
1363 DsigStatus::Invalid(FailureReason::KeyNotFound)
1364 ),
1365 "unexpected status: {:?}",
1366 result.status
1367 );
1368 }
1369
1370 #[test]
1371 fn verify_context_rejects_disallowed_uri() {
1372 let xml = minimal_signature_xml("http://example.com/external", "");
1373 let err = VerifyContext::new()
1374 .key(&RejectingKey)
1375 .verify(&xml)
1376 .expect_err("external URI should be rejected by default policy");
1377 assert!(matches!(
1378 err,
1379 SignatureVerificationPipelineError::DisallowedUri { .. }
1380 ));
1381 }
1382
1383 #[test]
1384 fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
1385 let xml = minimal_signature_xml("", "");
1386 let err = VerifyContext::new()
1387 .key(&RejectingKey)
1388 .allowed_uri_types(UriTypeSet::new(false, true, false))
1389 .verify(&xml)
1390 .expect_err("empty URI must be rejected when empty references are disabled");
1391 assert!(matches!(
1392 err,
1393 SignatureVerificationPipelineError::DisallowedUri { ref uri } if uri.is_empty()
1394 ));
1395 }
1396
1397 #[test]
1398 fn verify_context_rejects_disallowed_transform() {
1399 let xml = minimal_signature_xml(
1400 "",
1401 r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
1402 );
1403 let err = VerifyContext::new()
1404 .key(&RejectingKey)
1405 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
1406 .verify(&xml)
1407 .expect_err("enveloped transform should be rejected by allowlist");
1408 assert!(matches!(
1409 err,
1410 SignatureVerificationPipelineError::DisallowedTransform { .. }
1411 ));
1412 }
1413
1414 fn signature_with_manifest_xml(valid_manifest_digest: bool) -> String {
1415 signature_with_manifest_xml_with_manifest_mutation(valid_manifest_digest, |xml| xml)
1416 }
1417
1418 fn signature_with_manifest_xml_with_manifest_mutation<F>(
1419 valid_manifest_digest: bool,
1420 mutate_manifest: F,
1421 ) -> String
1422 where
1423 F: FnOnce(String) -> String,
1424 {
1425 const TMP_SIGNED_INFO_DIGEST: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
1426 const INVALID_MANIFEST_DIGEST: &str = "//////////////////////////8=";
1427 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1428 <target ID="target">payload</target>
1429 <ds:Signature>
1430 <ds:SignedInfo>
1431 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1432 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1433 <ds:Reference URI="#manifest">
1434 <ds:Transforms>
1435 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1436 </ds:Transforms>
1437 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1438 <ds:DigestValue>SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER</ds:DigestValue>
1439 </ds:Reference>
1440 </ds:SignedInfo>
1441 <ds:SignatureValue>AQ==</ds:SignatureValue>
1442 <ds:Object>
1443 <ds:Manifest ID="manifest">
1444 <ds:Reference URI="#target">
1445 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1446 <ds:DigestValue>MANIFEST_DIGEST_PLACEHOLDER</ds:DigestValue>
1447 </ds:Reference>
1448 </ds:Manifest>
1449 </ds:Object>
1450 </ds:Signature>
1451</root>"##;
1452 let seed_xml = xml_template.replace(
1453 "SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER",
1454 TMP_SIGNED_INFO_DIGEST,
1455 );
1456 let doc = Document::parse(&seed_xml).unwrap();
1457 let signature_node = doc
1458 .descendants()
1459 .find(|node| {
1460 node.is_element()
1461 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1462 && node.tag_name().name() == "Signature"
1463 })
1464 .unwrap();
1465 let resolver = UriReferenceResolver::new(&doc);
1466 let initial_data = resolver.dereference("#target").unwrap();
1467 let manifest_pre_digest =
1468 crate::xmldsig::execute_transforms(signature_node, initial_data, &[]).unwrap();
1469 let computed_manifest_digest_b64 = base64::engine::general_purpose::STANDARD
1470 .encode(compute_digest(DigestAlgorithm::Sha1, &manifest_pre_digest));
1471 let final_manifest_digest_b64 = if valid_manifest_digest {
1472 computed_manifest_digest_b64.as_str()
1473 } else {
1474 INVALID_MANIFEST_DIGEST
1475 };
1476 let xml_with_manifest_digest = mutate_manifest(
1477 seed_xml.replace("MANIFEST_DIGEST_PLACEHOLDER", final_manifest_digest_b64),
1478 );
1479 let signed_doc = Document::parse(&xml_with_manifest_digest).unwrap();
1480 let signed_signature_node = signed_doc
1481 .descendants()
1482 .find(|node| {
1483 node.is_element()
1484 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1485 && node.tag_name().name() == "Signature"
1486 })
1487 .unwrap();
1488 let signed_info_node = signed_signature_node
1489 .children()
1490 .find(|node| {
1491 node.is_element()
1492 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1493 && node.tag_name().name() == "SignedInfo"
1494 })
1495 .unwrap();
1496 let signed_info = parse_signed_info(signed_info_node).unwrap();
1497 let object_reference = &signed_info.references[0];
1498 let signed_resolver = UriReferenceResolver::new(&signed_doc);
1499 let signed_initial_data = signed_resolver
1500 .dereference(object_reference.uri.as_deref().unwrap())
1501 .unwrap();
1502 let signed_pre_digest = crate::xmldsig::execute_transforms(
1503 signed_signature_node,
1504 signed_initial_data,
1505 &object_reference.transforms,
1506 )
1507 .unwrap();
1508 let signed_digest_b64 = base64::engine::general_purpose::STANDARD.encode(compute_digest(
1509 object_reference.digest_method,
1510 &signed_pre_digest,
1511 ));
1512
1513 xml_with_manifest_digest.replacen(TMP_SIGNED_INFO_DIGEST, &signed_digest_b64, 1)
1514 }
1515
1516 #[test]
1517 fn verify_context_processes_manifest_references_when_enabled() {
1518 let xml = signature_with_manifest_xml(true);
1519
1520 let result_without_manifests = VerifyContext::new()
1521 .key(&RejectingKey)
1522 .verify(&xml)
1523 .expect("manifest processing disabled should still verify SignedInfo");
1524 assert!(
1525 result_without_manifests.manifest_references.is_empty(),
1526 "manifest results must stay empty when manifest processing is disabled",
1527 );
1528 assert!(matches!(
1529 result_without_manifests.status,
1530 DsigStatus::Invalid(FailureReason::SignatureMismatch)
1531 ));
1532
1533 let malformed_manifest_xml = signature_with_manifest_xml(true).replacen(
1534 "</ds:Object>",
1535 "</ds:Object><ds:Object><ds:Manifest><ds:Foo/></ds:Manifest></ds:Object>",
1536 1,
1537 );
1538 let malformed_with_manifests_disabled = VerifyContext::new()
1539 .key(&RejectingKey)
1540 .verify(&malformed_manifest_xml)
1541 .expect("malformed Manifest must be ignored when manifest processing is disabled");
1542 assert!(
1543 malformed_with_manifests_disabled
1544 .manifest_references
1545 .is_empty(),
1546 "manifest parser must not run when process_manifests is disabled",
1547 );
1548 assert!(matches!(
1549 malformed_with_manifests_disabled.status,
1550 DsigStatus::Invalid(FailureReason::SignatureMismatch)
1551 ));
1552
1553 let result_with_manifests = VerifyContext::new()
1554 .key(&RejectingKey)
1555 .process_manifests(true)
1556 .verify(&xml)
1557 .expect("manifest references should be processed when enabled");
1558 assert_eq!(result_with_manifests.manifest_references.len(), 1);
1559 assert_eq!(
1560 result_with_manifests.manifest_references[0].reference_set,
1561 ReferenceSet::Manifest
1562 );
1563 assert_eq!(
1564 result_with_manifests.manifest_references[0].reference_index,
1565 0
1566 );
1567 assert!(matches!(
1568 result_with_manifests.manifest_references[0].status,
1569 DsigStatus::Valid
1570 ));
1571 assert!(matches!(
1572 result_with_manifests.status,
1573 DsigStatus::Invalid(FailureReason::SignatureMismatch)
1574 ));
1575 }
1576
1577 #[test]
1578 fn verify_context_processes_manifest_when_signedinfo_references_object() {
1579 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1580 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
1581 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
1582 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
1583 });
1584
1585 let result = VerifyContext::new()
1586 .key(&RejectingKey)
1587 .process_manifests(true)
1588 .verify(&xml)
1589 .expect("manifest references should be processed when SignedInfo references ds:Object");
1590 assert_eq!(
1591 result.manifest_references.len(),
1592 1,
1593 "signed ds:Object should enable processing of its direct-child ds:Manifest",
1594 );
1595 assert_eq!(
1596 result.manifest_references[0].reference_set,
1597 ReferenceSet::Manifest
1598 );
1599 assert_eq!(result.manifest_references[0].reference_index, 0);
1600 assert!(matches!(
1601 result.manifest_references[0].status,
1602 DsigStatus::Valid
1603 ));
1604 }
1605
1606 #[test]
1607 fn verify_context_manifest_digest_mismatch_is_non_fatal() {
1608 let xml = signature_with_manifest_xml(false);
1609 let result = VerifyContext::new()
1610 .key(&RejectingKey)
1611 .process_manifests(true)
1612 .verify(&xml)
1613 .expect("manifest digest mismatches should be reported as reference status");
1614 assert_eq!(result.manifest_references.len(), 1);
1615 assert_eq!(
1616 result.manifest_references[0].reference_set,
1617 ReferenceSet::Manifest
1618 );
1619 assert_eq!(result.manifest_references[0].reference_index, 0);
1620 assert!(matches!(
1621 result.manifest_references[0].status,
1622 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1623 ));
1624 assert!(matches!(
1625 result.status,
1626 DsigStatus::Invalid(FailureReason::SignatureMismatch)
1627 ));
1628 }
1629
1630 #[test]
1631 fn verify_context_manifest_digest_mismatch_is_non_fatal_with_accepting_key() {
1632 let xml = signature_with_manifest_xml(false);
1633 let result = VerifyContext::new()
1634 .key(&AcceptingKey)
1635 .process_manifests(true)
1636 .verify(&xml)
1637 .expect("manifest digest mismatches should be recorded while signature stays valid");
1638 assert_eq!(result.manifest_references.len(), 1);
1639 assert!(matches!(
1640 result.manifest_references[0].status,
1641 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1642 ));
1643 assert!(matches!(result.status, DsigStatus::Valid));
1644 }
1645
1646 #[test]
1647 fn verify_context_keeps_manifest_results_when_signedinfo_reference_fails() {
1648 let xml = signature_with_manifest_xml(true);
1649 let (signed_info_prefix, object_suffix) = xml
1650 .split_once("<ds:Object>")
1651 .expect("fixture should contain ds:Object");
1652 let open = "<ds:DigestValue>";
1653 let close = "</ds:DigestValue>";
1654 let digest_start = signed_info_prefix
1655 .find(open)
1656 .expect("SignedInfo should contain DigestValue");
1657 let digest_end = signed_info_prefix[digest_start + open.len()..]
1658 .find(close)
1659 .map(|offset| digest_start + open.len() + offset)
1660 .expect("SignedInfo DigestValue must be closed");
1661 let broken_signed_info_prefix = format!(
1662 "{}{}AAAAAAAAAAAAAAAAAAAAAAAAAAA={}{}",
1663 &signed_info_prefix[..digest_start],
1664 open,
1665 close,
1666 &signed_info_prefix[digest_end + close.len()..],
1667 );
1668 let broken_xml = format!("{broken_signed_info_prefix}<ds:Object>{object_suffix}");
1669 let result = VerifyContext::new()
1670 .key(&RejectingKey)
1671 .process_manifests(true)
1672 .verify(&broken_xml)
1673 .expect("manifest references should still be processed on SignedInfo digest failure");
1674 assert!(matches!(
1675 result.status,
1676 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1677 ));
1678 assert_eq!(
1679 result.manifest_references.len(),
1680 1,
1681 "manifest diagnostics must be preserved even when SignedInfo fails early",
1682 );
1683 }
1684
1685 #[test]
1686 fn verify_context_records_manifest_policy_violations_without_aborting() {
1687 let xml = signature_with_manifest_xml(true);
1688 let (prefix, object_suffix) = xml
1689 .split_once("<ds:Object>")
1690 .expect("fixture should contain ds:Object");
1691 let mutated_object_suffix =
1692 object_suffix.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1);
1693 let broken_xml = format!("{prefix}<ds:Object>{mutated_object_suffix}");
1694 let result = VerifyContext::new()
1695 .key(&RejectingKey)
1696 .process_manifests(true)
1697 .verify(&broken_xml)
1698 .expect("manifest policy violations should be recorded, not abort verify()");
1699 assert_eq!(result.manifest_references.len(), 1);
1700 assert!(matches!(
1701 result.manifest_references[0].status,
1702 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
1703 ));
1704 assert!(matches!(
1705 result.status,
1706 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1707 ));
1708 }
1709
1710 #[test]
1711 fn verify_context_records_manifest_policy_violations_with_accepting_key() {
1712 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1713 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
1714 });
1715 let result = VerifyContext::new()
1716 .key(&AcceptingKey)
1717 .process_manifests(true)
1718 .verify(&broken_xml)
1719 .expect("manifest policy violations should be recorded while signature stays valid");
1720 assert_eq!(result.manifest_references.len(), 1);
1721 assert!(matches!(
1722 result.manifest_references[0].status,
1723 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
1724 ));
1725 assert!(matches!(result.status, DsigStatus::Valid));
1726 }
1727
1728 #[test]
1729 fn verify_context_records_manifest_missing_uri_as_processing_failure() {
1730 let xml = signature_with_manifest_xml(true);
1731 let (prefix, object_suffix) = xml
1732 .split_once("<ds:Object>")
1733 .expect("fixture should contain ds:Object");
1734 let mutated_object_suffix =
1735 object_suffix.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1);
1736 let broken_xml = format!("{prefix}<ds:Object>{mutated_object_suffix}");
1737
1738 let result = VerifyContext::new()
1739 .key(&RejectingKey)
1740 .process_manifests(true)
1741 .verify(&broken_xml)
1742 .expect("manifest missing URI should be recorded as non-fatal processing failure");
1743 assert_eq!(result.manifest_references.len(), 1);
1744 assert_eq!(result.manifest_references[0].uri, "<omitted>");
1745 assert!(matches!(
1746 result.manifest_references[0].status,
1747 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
1748 ));
1749 assert!(matches!(
1750 result.status,
1751 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1752 ));
1753 }
1754
1755 #[test]
1756 fn verify_context_records_manifest_missing_uri_with_accepting_key() {
1757 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1758 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
1759 });
1760
1761 let result = VerifyContext::new()
1762 .key(&AcceptingKey)
1763 .process_manifests(true)
1764 .verify(&broken_xml)
1765 .expect("manifest missing URI should be recorded while signature stays valid");
1766 assert_eq!(result.manifest_references.len(), 1);
1767 assert_eq!(result.manifest_references[0].uri, "<omitted>");
1768 assert!(matches!(
1769 result.manifest_references[0].status,
1770 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
1771 ));
1772 assert!(matches!(result.status, DsigStatus::Valid));
1773 }
1774
1775 #[test]
1776 fn verify_context_ignores_nested_manifests_in_object() {
1777 let xml = signature_with_manifest_xml(true)
1778 .replacen(
1779 "<ds:Manifest ID=\"manifest\">",
1780 "<wrapper><ds:Manifest ID=\"manifest\">",
1781 1,
1782 )
1783 .replacen("</ds:Manifest>", "</ds:Manifest></wrapper>", 1);
1784
1785 let result = VerifyContext::new()
1786 .key(&RejectingKey)
1787 .process_manifests(true)
1788 .verify(&xml)
1789 .expect("nested Manifest nodes are ignored in strict mode");
1790 assert!(
1791 result.manifest_references.is_empty(),
1792 "only direct ds:Manifest children of ds:Object must be processed"
1793 );
1794 }
1795
1796 #[test]
1797 fn verify_context_reports_manifest_reference_parse_errors_explicitly() {
1798 let xml = signature_with_manifest_xml(true);
1799 let (prefix, object_suffix) = xml
1800 .split_once("<ds:Object>")
1801 .expect("fixture should contain ds:Object");
1802 let open = "<ds:DigestValue>";
1803 let close = "</ds:DigestValue>";
1804 let digest_start = object_suffix
1805 .find(open)
1806 .expect("manifest should contain DigestValue");
1807 let digest_end = object_suffix[digest_start + open.len()..]
1808 .find(close)
1809 .map(|offset| digest_start + open.len() + offset)
1810 .expect("manifest DigestValue must be closed");
1811 let broken_object_suffix = format!(
1812 "{}{}!!!{}{}",
1813 &object_suffix[..digest_start],
1814 open,
1815 close,
1816 &object_suffix[digest_end + close.len()..],
1817 );
1818 let broken_xml = format!("{prefix}<ds:Object>{broken_object_suffix}");
1819
1820 let err = VerifyContext::new()
1821 .key(&RejectingKey)
1822 .process_manifests(true)
1823 .verify(&broken_xml)
1824 .expect_err("invalid Manifest DigestValue must map to ParseManifestReference");
1825 assert!(matches!(
1826 err,
1827 SignatureVerificationPipelineError::ParseManifestReference(_)
1828 ));
1829 }
1830
1831 #[test]
1832 fn verify_context_rejects_manifest_non_whitespace_mixed_content() {
1833 let xml = signature_with_manifest_xml(true).replacen(
1834 "<ds:Manifest ID=\"manifest\">",
1835 "<ds:Manifest ID=\"manifest\">junk",
1836 1,
1837 );
1838
1839 let err = VerifyContext::new()
1840 .key(&RejectingKey)
1841 .process_manifests(true)
1842 .verify(&xml)
1843 .expect_err("Manifest mixed content must fail verification");
1844 assert!(matches!(
1845 err,
1846 SignatureVerificationPipelineError::InvalidStructure {
1847 reason: "Manifest contains non-whitespace mixed content"
1848 }
1849 ));
1850 }
1851
1852 #[test]
1853 fn verify_context_rejects_empty_manifest_children() {
1854 let xml = signature_with_manifest_xml(true);
1855 let (prefix, rest) = xml
1856 .split_once("<ds:Manifest ID=\"manifest\">")
1857 .expect("fixture should contain Manifest");
1858 let (_, suffix) = rest
1859 .split_once("</ds:Manifest>")
1860 .expect("fixture should contain closing Manifest");
1861 let xml = format!("{prefix}<ds:Manifest ID=\"manifest\"></ds:Manifest>{suffix}");
1862
1863 let err = VerifyContext::new()
1864 .key(&RejectingKey)
1865 .process_manifests(true)
1866 .verify(&xml)
1867 .expect_err("empty Manifest must fail verification");
1868 assert!(matches!(
1869 err,
1870 SignatureVerificationPipelineError::InvalidStructure {
1871 reason: "Manifest must contain at least one ds:Reference element child"
1872 }
1873 ));
1874 }
1875
1876 #[test]
1877 fn verify_context_ignores_unsigned_malformed_manifest_blocks() {
1878 let xml = signature_with_manifest_xml(true).replacen(
1879 "</ds:Object>",
1880 "</ds:Object><ds:Object><ds:Manifest>junk<ds:Foo/></ds:Manifest></ds:Object>",
1881 1,
1882 );
1883 let result = VerifyContext::new()
1884 .key(&AcceptingKey)
1885 .process_manifests(true)
1886 .verify(&xml)
1887 .expect("unsigned malformed Manifest must be ignored");
1888 assert_eq!(
1889 result.manifest_references.len(),
1890 1,
1891 "only signed Manifest references must be reported",
1892 );
1893 assert!(matches!(result.status, DsigStatus::Valid));
1894 }
1895
1896 #[test]
1897 fn verify_context_skips_ambiguous_manifest_id_blocks() {
1898 let xml = signature_with_manifest_xml(true).replacen(
1899 "</ds:Object>",
1900 "</ds:Object><ds:Object><ds:Manifest ID=\"manifest\">junk<ds:Foo/></ds:Manifest></ds:Object>",
1901 1,
1902 );
1903 let err = VerifyContext::new()
1904 .key(&RejectingKey)
1905 .process_manifests(true)
1906 .verify(&xml)
1907 .expect_err("ambiguous manifest IDs should make SignedInfo #manifest dereference fail");
1908 assert!(matches!(
1909 err,
1910 SignatureVerificationPipelineError::Reference(
1911 ReferenceProcessingError::UriDereference(
1912 crate::xmldsig::types::TransformError::ElementNotFound(id)
1913 )
1914 ) if id == "manifest"
1915 ));
1916 }
1917
1918 #[test]
1919 fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
1920 let xml = minimal_signature_xml("", "");
1921 let err = VerifyContext::new()
1922 .key(&RejectingKey)
1923 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
1924 .verify(&xml)
1925 .expect_err("implicit default C14N must be checked against allowlist");
1926 assert!(matches!(
1927 err,
1928 SignatureVerificationPipelineError::DisallowedTransform { .. }
1929 ));
1930 }
1931
1932 #[test]
1933 fn verify_context_skips_resolver_when_reference_processing_fails() {
1934 let xml = minimal_signature_xml("", "");
1935 let result = VerifyContext::new()
1936 .key_resolver(&PanicResolver)
1937 .verify(&xml)
1938 .expect("reference digest mismatch should short-circuit before resolver");
1939 assert!(matches!(
1940 result.status,
1941 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1942 ));
1943 }
1944
1945 #[test]
1946 fn verify_context_reports_key_not_found_when_resolver_misses() {
1947 let xml = signature_with_target_reference("AQ==");
1948 let result = VerifyContext::new()
1949 .key_resolver(&MissingKeyResolver)
1950 .verify(&xml)
1951 .expect("resolver miss should report status, not pipeline error");
1952 assert!(matches!(
1953 result.status,
1954 DsigStatus::Invalid(FailureReason::KeyNotFound)
1955 ));
1956 assert_eq!(
1957 result.signed_info_references.len(),
1958 1,
1959 "KeyNotFound path must preserve SignedInfo reference diagnostics",
1960 );
1961 assert!(matches!(
1962 result.signed_info_references[0].status,
1963 DsigStatus::Valid
1964 ));
1965 }
1966
1967 #[test]
1968 fn verify_context_resolver_can_ignore_malformed_keyinfo_by_default() {
1969 let base_xml = signature_with_target_reference("AQ==");
1970 let xml = base_xml
1971 .replace(
1972 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
1973 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
1974 )
1975 .replace(
1976 "</ds:SignatureValue>\n </ds:Signature>",
1977 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
1978 );
1979
1980 let result = VerifyContext::new()
1981 .key_resolver(&MissingKeyResolver)
1982 .verify(&xml)
1983 .expect("resolver path should not hard-fail on advisory malformed KeyInfo by default");
1984 assert!(matches!(
1985 result.status,
1986 DsigStatus::Invalid(FailureReason::KeyNotFound)
1987 ));
1988 }
1989
1990 #[test]
1991 fn verify_context_resolver_can_opt_in_to_keyinfo_parse_failures() {
1992 let base_xml = signature_with_target_reference("AQ==");
1993 let xml = base_xml
1994 .replace(
1995 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
1996 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
1997 )
1998 .replace(
1999 "</ds:SignatureValue>\n </ds:Signature>",
2000 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
2001 );
2002
2003 let err = VerifyContext::new()
2004 .key_resolver(&ConsumingKeyInfoResolver)
2005 .verify(&xml)
2006 .expect_err("resolver opted into KeyInfo parsing, malformed KeyInfo must fail");
2007 assert!(matches!(
2008 err,
2009 SignatureVerificationPipelineError::ParseKeyInfo(_)
2010 ));
2011 }
2012
2013 #[test]
2014 fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
2015 let xml = signature_with_target_reference("@@@");
2016
2017 let err = VerifyContext::new()
2018 .key_resolver(&MissingKeyResolver)
2019 .verify(&xml)
2020 .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
2021 assert!(matches!(
2022 err,
2023 SignatureVerificationPipelineError::SignatureValueBase64(_)
2024 ));
2025 }
2026
2027 #[test]
2028 fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
2029 let xml = signature_with_target_reference("@@@");
2030
2031 let err = VerifyContext::new()
2032 .verify(&xml)
2033 .expect_err("invalid SignatureValue must remain a decode error");
2034 assert!(matches!(
2035 err,
2036 SignatureVerificationPipelineError::SignatureValueBase64(_)
2037 ));
2038 }
2039
2040 #[test]
2041 fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
2042 let references = vec![Reference {
2043 uri: None,
2044 id: None,
2045 ref_type: None,
2046 transforms: vec![],
2047 digest_method: DigestAlgorithm::Sha256,
2048 digest_value: vec![0; 32],
2049 }];
2050 let uri_types = UriTypeSet {
2051 allow_empty: false,
2052 allow_same_document: true,
2053 allow_external: false,
2054 };
2055
2056 let err = enforce_reference_policies(&references, uri_types, None)
2057 .expect_err("missing URI must fail before allow_empty policy is evaluated");
2058 assert!(matches!(
2059 err,
2060 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
2061 ));
2062 }
2063
2064 #[test]
2065 fn push_normalized_signature_text_rejects_form_feed() {
2066 let mut normalized = String::new();
2067 let mut raw_text_len = 0usize;
2068 let err =
2069 push_normalized_signature_text("ab\u{000C}cd", &mut raw_text_len, &mut normalized)
2070 .expect_err("form-feed must not be treated as XML base64 whitespace");
2071 assert!(matches!(
2072 err,
2073 SignatureVerificationPipelineError::SignatureValueBase64(
2074 base64::DecodeError::InvalidByte(_, 0x0C)
2075 )
2076 ));
2077 }
2078
2079 #[test]
2080 fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
2081 let mut normalized = "A".repeat(MAX_SIGNATURE_VALUE_LEN - 1);
2082 let mut raw_text_len = normalized.len();
2083 let err = push_normalized_signature_text("é", &mut raw_text_len, &mut normalized)
2084 .expect_err("multibyte characters must not bypass byte-size limit");
2085 assert!(matches!(
2086 err,
2087 SignatureVerificationPipelineError::InvalidStructure {
2088 reason: "SignatureValue exceeds maximum allowed length"
2089 }
2090 ));
2091 }
2092
2093 #[test]
2096 fn reference_with_correct_digest_passes() {
2097 let xml = r##"<root>
2100 <data>hello world</data>
2101 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
2102 <ds:SignedInfo/>
2103 </ds:Signature>
2104 </root>"##;
2105 let doc = Document::parse(xml).unwrap();
2106 let resolver = UriReferenceResolver::new(&doc);
2107 let sig_node = doc
2108 .descendants()
2109 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2110 .unwrap();
2111
2112 let initial_data = resolver.dereference("").unwrap();
2114 let transforms = vec![
2115 Transform::Enveloped,
2116 Transform::C14n(
2117 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
2118 .unwrap(),
2119 ),
2120 ];
2121 let pre_digest_bytes =
2122 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
2123 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
2124
2125 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
2127
2128 let result = process_reference(
2129 &reference,
2130 &resolver,
2131 sig_node,
2132 ReferenceSet::SignedInfo,
2133 0,
2134 false,
2135 )
2136 .unwrap();
2137 assert!(
2138 matches!(result.status, DsigStatus::Valid),
2139 "digest should match"
2140 );
2141 assert!(result.pre_digest_data.is_none());
2142 }
2143
2144 #[test]
2145 fn reference_with_wrong_digest_fails() {
2146 let xml = r##"<root>
2147 <data>hello</data>
2148 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2149 <ds:SignedInfo/>
2150 </ds:Signature>
2151 </root>"##;
2152 let doc = Document::parse(xml).unwrap();
2153 let resolver = UriReferenceResolver::new(&doc);
2154 let sig_node = doc
2155 .descendants()
2156 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2157 .unwrap();
2158
2159 let transforms = vec![Transform::Enveloped];
2160 let wrong_digest = vec![0u8; 32];
2162 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
2163
2164 let result = process_reference(
2165 &reference,
2166 &resolver,
2167 sig_node,
2168 ReferenceSet::SignedInfo,
2169 0,
2170 false,
2171 )
2172 .unwrap();
2173 assert!(matches!(
2174 result.status,
2175 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
2176 ));
2177 }
2178
2179 #[test]
2180 fn reference_with_wrong_digest_preserves_supplied_ref_index() {
2181 let xml = r##"<root>
2182 <data>hello</data>
2183 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2184 <ds:SignedInfo/>
2185 </ds:Signature>
2186 </root>"##;
2187 let doc = Document::parse(xml).unwrap();
2188 let resolver = UriReferenceResolver::new(&doc);
2189 let sig_node = doc
2190 .descendants()
2191 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2192 .unwrap();
2193
2194 let reference = make_reference(
2195 "",
2196 vec![Transform::Enveloped],
2197 DigestAlgorithm::Sha256,
2198 vec![0u8; 32],
2199 );
2200 let result = process_reference(
2201 &reference,
2202 &resolver,
2203 sig_node,
2204 ReferenceSet::SignedInfo,
2205 7,
2206 false,
2207 )
2208 .unwrap();
2209 assert!(matches!(
2210 result.status,
2211 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
2212 ));
2213 }
2214
2215 #[test]
2216 fn reference_stores_pre_digest_data() {
2217 let xml = "<root><child>text</child></root>";
2218 let doc = Document::parse(xml).unwrap();
2219 let resolver = UriReferenceResolver::new(&doc);
2220
2221 let initial_data = resolver.dereference("").unwrap();
2223 let pre_digest =
2224 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2225 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2226
2227 let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
2228 let result = process_reference(
2229 &reference,
2230 &resolver,
2231 doc.root_element(),
2232 ReferenceSet::SignedInfo,
2233 0,
2234 true,
2235 )
2236 .unwrap();
2237
2238 assert!(matches!(result.status, DsigStatus::Valid));
2239 assert!(result.pre_digest_data.is_some());
2240 assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
2241 }
2242
2243 #[test]
2246 fn reference_with_id_uri() {
2247 let xml = r##"<root>
2248 <item ID="target">specific content</item>
2249 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2250 <ds:SignedInfo/>
2251 </ds:Signature>
2252 </root>"##;
2253 let doc = Document::parse(xml).unwrap();
2254 let resolver = UriReferenceResolver::new(&doc);
2255 let sig_node = doc
2256 .descendants()
2257 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2258 .unwrap();
2259
2260 let initial_data = resolver.dereference("#target").unwrap();
2262 let transforms = vec![Transform::C14n(
2263 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
2264 .unwrap(),
2265 )];
2266 let pre_digest =
2267 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
2268 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2269
2270 let reference = make_reference(
2271 "#target",
2272 transforms,
2273 DigestAlgorithm::Sha256,
2274 expected_digest,
2275 );
2276 let result = process_reference(
2277 &reference,
2278 &resolver,
2279 sig_node,
2280 ReferenceSet::SignedInfo,
2281 0,
2282 false,
2283 )
2284 .unwrap();
2285 assert!(matches!(result.status, DsigStatus::Valid));
2286 }
2287
2288 #[test]
2289 fn reference_with_nonexistent_id_fails() {
2290 let xml = "<root><child/></root>";
2291 let doc = Document::parse(xml).unwrap();
2292 let resolver = UriReferenceResolver::new(&doc);
2293
2294 let reference =
2295 make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
2296 let result = process_reference(
2297 &reference,
2298 &resolver,
2299 doc.root_element(),
2300 ReferenceSet::SignedInfo,
2301 0,
2302 false,
2303 );
2304 assert!(result.is_err());
2305 }
2306
2307 #[test]
2308 fn reference_with_absent_uri_fails_closed() {
2309 let xml = "<root><child>text</child></root>";
2310 let doc = Document::parse(xml).unwrap();
2311 let resolver = UriReferenceResolver::new(&doc);
2312
2313 let reference = Reference {
2314 uri: None, id: None,
2316 ref_type: None,
2317 transforms: vec![],
2318 digest_method: DigestAlgorithm::Sha256,
2319 digest_value: vec![0; 32],
2320 };
2321
2322 let result = process_reference(
2323 &reference,
2324 &resolver,
2325 doc.root_element(),
2326 ReferenceSet::SignedInfo,
2327 0,
2328 false,
2329 );
2330 assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
2331 }
2332
2333 #[test]
2336 fn all_references_pass() {
2337 let xml = "<root><child>text</child></root>";
2338 let doc = Document::parse(xml).unwrap();
2339 let resolver = UriReferenceResolver::new(&doc);
2340
2341 let initial_data = resolver.dereference("").unwrap();
2343 let pre_digest =
2344 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2345 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2346
2347 let refs = vec![
2348 make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
2349 make_reference("", vec![], DigestAlgorithm::Sha256, digest),
2350 ];
2351
2352 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
2353 assert!(result.all_valid());
2354 assert_eq!(result.results.len(), 2);
2355 assert!(result.first_failure.is_none());
2356 }
2357
2358 #[test]
2359 fn fail_fast_on_first_mismatch() {
2360 let xml = "<root><child>text</child></root>";
2361 let doc = Document::parse(xml).unwrap();
2362 let resolver = UriReferenceResolver::new(&doc);
2363
2364 let wrong_digest = vec![0u8; 32];
2365 let refs = vec![
2366 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
2367 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
2369 ];
2370
2371 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
2372 assert!(!result.all_valid());
2373 assert_eq!(result.first_failure, Some(0));
2374 assert_eq!(result.results.len(), 1);
2376 assert!(matches!(
2377 result.results[0].status,
2378 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
2379 ));
2380 }
2381
2382 #[test]
2383 fn fail_fast_second_reference() {
2384 let xml = "<root><child>text</child></root>";
2385 let doc = Document::parse(xml).unwrap();
2386 let resolver = UriReferenceResolver::new(&doc);
2387
2388 let initial_data = resolver.dereference("").unwrap();
2390 let pre_digest =
2391 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2392 let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2393 let wrong_digest = vec![0u8; 32];
2394
2395 let refs = vec![
2396 make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
2397 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
2398 ];
2399
2400 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
2401 assert!(!result.all_valid());
2402 assert_eq!(result.first_failure, Some(1));
2403 assert_eq!(result.results.len(), 2);
2405 assert!(matches!(result.results[0].status, DsigStatus::Valid));
2406 assert!(matches!(
2407 result.results[1].status,
2408 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
2409 ));
2410 }
2411
2412 #[test]
2413 fn empty_references_list() {
2414 let xml = "<root/>";
2415 let doc = Document::parse(xml).unwrap();
2416 let resolver = UriReferenceResolver::new(&doc);
2417
2418 let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
2419 assert!(result.all_valid());
2420 assert!(result.results.is_empty());
2421 }
2422
2423 #[test]
2426 fn reference_sha1_digest() {
2427 let xml = "<root>content</root>";
2428 let doc = Document::parse(xml).unwrap();
2429 let resolver = UriReferenceResolver::new(&doc);
2430
2431 let initial_data = resolver.dereference("").unwrap();
2432 let pre_digest =
2433 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2434 let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
2435
2436 let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
2437 let result = process_reference(
2438 &reference,
2439 &resolver,
2440 doc.root_element(),
2441 ReferenceSet::SignedInfo,
2442 0,
2443 false,
2444 )
2445 .unwrap();
2446 assert!(matches!(result.status, DsigStatus::Valid));
2447 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
2448 }
2449
2450 #[test]
2451 fn reference_sha512_digest() {
2452 let xml = "<root>content</root>";
2453 let doc = Document::parse(xml).unwrap();
2454 let resolver = UriReferenceResolver::new(&doc);
2455
2456 let initial_data = resolver.dereference("").unwrap();
2457 let pre_digest =
2458 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2459 let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
2460
2461 let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
2462 let result = process_reference(
2463 &reference,
2464 &resolver,
2465 doc.root_element(),
2466 ReferenceSet::SignedInfo,
2467 0,
2468 false,
2469 )
2470 .unwrap();
2471 assert!(matches!(result.status, DsigStatus::Valid));
2472 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
2473 }
2474
2475 #[test]
2478 fn saml_enveloped_reference_processing() {
2479 let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
2481 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
2482 ID="_resp1">
2483 <saml:Assertion ID="_assert1">
2484 <saml:Subject>user@example.com</saml:Subject>
2485 </saml:Assertion>
2486 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2487 <ds:SignedInfo>
2488 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2489 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2490 <ds:Reference URI="">
2491 <ds:Transforms>
2492 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
2493 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2494 </ds:Transforms>
2495 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2496 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
2497 </ds:Reference>
2498 </ds:SignedInfo>
2499 <ds:SignatureValue>fakesig==</ds:SignatureValue>
2500 </ds:Signature>
2501 </samlp:Response>"##;
2502 let doc = Document::parse(xml).unwrap();
2503 let resolver = UriReferenceResolver::new(&doc);
2504 let sig_node = doc
2505 .descendants()
2506 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2507 .unwrap();
2508
2509 let signed_info_node = sig_node
2511 .children()
2512 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
2513 .unwrap();
2514 let signed_info = parse_signed_info(signed_info_node).unwrap();
2515 let reference = &signed_info.references[0];
2516
2517 let initial_data = resolver.dereference("").unwrap();
2519 let pre_digest =
2520 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
2521 .unwrap();
2522 let correct_digest = compute_digest(reference.digest_method, &pre_digest);
2523
2524 let corrected_ref = make_reference(
2526 "",
2527 reference.transforms.clone(),
2528 reference.digest_method,
2529 correct_digest,
2530 );
2531
2532 let result = process_reference(
2534 &corrected_ref,
2535 &resolver,
2536 sig_node,
2537 ReferenceSet::SignedInfo,
2538 0,
2539 true,
2540 )
2541 .unwrap();
2542 assert!(
2543 matches!(result.status, DsigStatus::Valid),
2544 "SAML reference should verify"
2545 );
2546 assert!(result.pre_digest_data.is_some());
2547
2548 let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
2550 assert!(
2551 pre_digest_str.contains("samlp:Response"),
2552 "pre-digest should contain Response"
2553 );
2554 assert!(
2555 !pre_digest_str.contains("SignatureValue"),
2556 "pre-digest should NOT contain Signature"
2557 );
2558 }
2559
2560 #[test]
2561 fn pipeline_missing_signed_info_returns_missing_element() {
2562 let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
2563
2564 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
2565 .expect_err("missing SignedInfo must fail before crypto stage");
2566 assert!(matches!(
2567 err,
2568 SignatureVerificationPipelineError::MissingElement {
2569 element: "SignedInfo"
2570 }
2571 ));
2572 }
2573
2574 #[test]
2575 fn pipeline_multiple_signature_elements_are_rejected() {
2576 let xml = r#"
2577<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2578 <ds:Signature>
2579 <ds:SignedInfo/>
2580 </ds:Signature>
2581 <ds:Signature/>
2582</root>
2583"#;
2584
2585 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
2586 .expect_err("multiple signatures must fail closed");
2587 assert!(matches!(
2588 err,
2589 SignatureVerificationPipelineError::InvalidStructure {
2590 reason: "Signature must appear exactly once in document",
2591 }
2592 ));
2593 }
2594
2595 #[test]
2596 fn pipeline_reports_keyinfo_parse_error() {
2597 let xml = r#"
2598<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
2599 xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2600 <ds:SignedInfo>
2601 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2602 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2603 <ds:Reference URI="">
2604 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
2605 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
2606 </ds:Reference>
2607 </ds:SignedInfo>
2608 <ds:SignatureValue>AA==</ds:SignatureValue>
2609 <ds:KeyInfo>
2610 <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
2611 </ds:KeyInfo>
2612</ds:Signature>
2613"#;
2614
2615 let err = VerifyContext::new().verify(xml).expect_err(
2616 "invalid KeyInfo must map to ParseKeyInfo when no explicit key is supplied",
2617 );
2618 assert!(matches!(
2619 err,
2620 SignatureVerificationPipelineError::ParseKeyInfo(_)
2621 ));
2622 }
2623
2624 #[test]
2625 fn pipeline_ignores_malformed_keyinfo_when_explicit_key_is_supplied() {
2626 let base_xml = signature_with_target_reference("AQ==");
2627 let xml = base_xml
2628 .replace(
2629 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
2630 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
2631 )
2632 .replace(
2633 "</ds:SignatureValue>\n </ds:Signature>",
2634 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
2635 );
2636
2637 let result = VerifyContext::new()
2638 .key(&RejectingKey)
2639 .verify(&xml)
2640 .expect("explicit key path should not fail on malformed KeyInfo");
2641 assert!(matches!(
2642 result.status,
2643 DsigStatus::Invalid(FailureReason::SignatureMismatch)
2644 ));
2645 }
2646
2647 #[test]
2648 fn pipeline_rejects_foreign_element_children_under_signature() {
2649 let base_xml = signature_with_target_reference("AQ==");
2650 let xml = base_xml
2651 .replace(
2652 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
2653 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:foo="urn:example:foo">"#,
2654 )
2655 .replace(
2656 "</ds:SignedInfo>\n <ds:SignatureValue>",
2657 "</ds:SignedInfo>\n <foo:Bar/>\n <ds:SignatureValue>",
2658 );
2659
2660 let err = VerifyContext::new()
2661 .key(&RejectingKey)
2662 .verify(&xml)
2663 .expect_err("foreign element children under Signature must fail closed");
2664 assert!(matches!(
2665 err,
2666 SignatureVerificationPipelineError::InvalidStructure {
2667 reason: "Signature must contain only XMLDSIG element children",
2668 }
2669 ));
2670 }
2671
2672 #[test]
2673 fn pipeline_rejects_non_whitespace_mixed_content_under_signature() {
2674 let base_xml = signature_with_target_reference("AQ==");
2675 let xml = base_xml.replace(
2676 "</ds:SignedInfo>\n <ds:SignatureValue>",
2677 "</ds:SignedInfo>\n oops\n <ds:SignatureValue>",
2678 );
2679
2680 let err = VerifyContext::new()
2681 .key(&RejectingKey)
2682 .verify(&xml)
2683 .expect_err("non-whitespace mixed content under Signature must fail closed");
2684 assert!(matches!(
2685 err,
2686 SignatureVerificationPipelineError::InvalidStructure {
2687 reason: "Signature must not contain non-whitespace mixed content",
2688 }
2689 ));
2690 }
2691
2692 #[test]
2693 fn pipeline_rejects_keyinfo_out_of_order() {
2694 let base_xml = signature_with_target_reference("AQ==");
2695 let xml = base_xml.replace(
2696 "</ds:SignatureValue>\n </ds:Signature>",
2697 "</ds:SignatureValue>\n <ds:Object/>\n <ds:KeyInfo><ds:KeyName>late</ds:KeyName></ds:KeyInfo>\n </ds:Signature>",
2698 );
2699
2700 let err = VerifyContext::new()
2701 .key(&RejectingKey)
2702 .verify(&xml)
2703 .expect_err("KeyInfo after Object must be rejected by Signature child order checks");
2704 assert!(matches!(
2705 err,
2706 SignatureVerificationPipelineError::InvalidStructure {
2707 reason: "KeyInfo must be the third element child of Signature when present"
2708 }
2709 ));
2710 }
2711
2712 #[test]
2713 fn pipeline_accepts_comments_and_processing_instructions_under_signature() {
2714 let xml = r#"
2715<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2716 <?dbg keep ?>
2717 <!-- signature metadata -->
2718 <ds:SignedInfo>
2719 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2720 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2721 <ds:Reference URI="">
2722 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
2723 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
2724 </ds:Reference>
2725 </ds:SignedInfo>
2726 <!-- between required children -->
2727 <ds:SignatureValue>AA==</ds:SignatureValue>
2728</ds:Signature>
2729"#;
2730
2731 let doc = Document::parse(xml).expect("test XML must parse");
2732 let signature_node = doc.root_element();
2733 let parsed = parse_signature_children(signature_node)
2734 .expect("comment/PI nodes under Signature must be ignored");
2735
2736 assert_eq!(parsed.signed_info_node.tag_name().name(), "SignedInfo");
2737 assert_eq!(
2738 parsed.signature_value_node.tag_name().name(),
2739 "SignatureValue"
2740 );
2741 assert!(parsed.key_info_node.is_none());
2742 }
2743}