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