1use base64::Engine;
14use roxmltree::{Document, Node, NodeId};
15use std::cell::Cell;
16use std::collections::{HashMap, HashSet};
17
18use crate::c14n::{canonicalize_bounded_with_xml_base_budget, is_output_limit_error};
19use crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING;
20
21#[cfg(test)]
22use super::digest::compute_digest;
23use super::digest::{DigestAlgorithm, constant_time_eq};
24#[cfg(test)]
25use super::parse::MAX_REFERENCES_PER_SIGNATURE;
26#[cfg(test)]
27use super::parse::parse_key_info;
28use super::parse::{
29 KeyInfo, MAX_X509_DATA_TOTAL_BINARY_LEN, MAX_X509_DECODED_BINARY_LEN, ParseError, Reference,
30 RetrievalMethodTransforms, SignatureAlgorithm, XMLDSIG_NS,
31};
32use super::parse::{
33 parse_key_info_with_provider_and_xml_base_budget, parse_reference_with_xpath_budget,
34 parse_signed_info_with_xpath_budget, parse_x509_certificate,
35 parse_x509_data_dispatch_with_budget_and_provider, reference_digest_method,
36};
37use super::signature::{
38 SignatureVerificationError, verify_dsa_signature_spki, verify_ecdsa_signature_pem,
39 verify_rsa_signature_pem,
40};
41#[cfg(test)]
42use super::transforms::BASE64_TRANSFORM_URI;
43use super::transforms::{
44 DEFAULT_IMPLICIT_C14N_URI, Transform, TransformExecutionBudget, TransformOptions,
45 XPATH_TRANSFORM_URI, XPathHereSemantics, XPathSignatureParseBudget,
46 execute_transforms_with_options_and_budget, transform_chain_produces_binary,
47};
48use super::uri::{UriReferenceResolver, same_document_reference_id};
49use super::whitespace::{is_xml_whitespace_only, normalize_xml_base64_bytes};
50
51const MAX_SIGNATURE_VALUE_LEN: usize = 8192;
52const MAX_SIGNATURE_VALUE_TEXT_LEN: usize = 65_536;
53const MAX_RETRIEVAL_METHOD_COUNT: usize = 64;
54pub trait VerifyingKey {
59 fn validate_policy(
65 &self,
66 _policy: &crate::policy::VerificationPolicy,
67 ) -> Result<(), DsigError> {
68 Ok(())
69 }
70
71 fn validate_signature_value(
77 &self,
78 algorithm: SignatureAlgorithm,
79 signature_value: &[u8],
80 ) -> Result<bool, DsigError> {
81 Ok(super::signature::signature_value_matches_algorithm(
82 algorithm,
83 signature_value,
84 ))
85 }
86
87 fn verify(
89 &self,
90 algorithm: SignatureAlgorithm,
91 signed_data: &[u8],
92 signature_value: &[u8],
93 ) -> Result<bool, DsigError>;
94}
95
96pub trait KeyResolver {
101 fn resolve<'a>(
108 &'a self,
109 key_info: Option<&KeyInfo>,
110 algorithm: SignatureAlgorithm,
111 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError>;
112
113 fn resolve_with_policy<'a>(
119 &'a self,
120 key_info: Option<&KeyInfo>,
121 algorithm: SignatureAlgorithm,
122 _policy: &crate::policy::VerificationPolicy,
123 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
124 self.resolve(key_info, algorithm)
125 }
126
127 fn resolve_with_policy_and_provider<'a>(
133 &'a self,
134 key_info: Option<&KeyInfo>,
135 algorithm: SignatureAlgorithm,
136 policy: &crate::policy::VerificationPolicy,
137 _provider: &dyn crate::provider::CryptoProvider,
138 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
139 self.resolve_with_policy(key_info, algorithm, policy)
140 }
141
142 fn consumes_document_key_info(&self) -> bool {
149 false
150 }
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158#[must_use = "pass the policy to VerifyContext::allowed_uri_types(), or store it for reuse"]
159pub struct UriTypeSet {
160 allow_empty: bool,
161 allow_same_document: bool,
162 allow_external: bool,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166enum UriClass {
167 Empty,
168 SameDocument,
169 External,
170}
171
172fn classify_uri(uri: &str) -> UriClass {
173 if uri.is_empty() {
174 UriClass::Empty
175 } else if uri.starts_with('#') {
176 UriClass::SameDocument
177 } else {
178 UriClass::External
179 }
180}
181
182impl UriTypeSet {
183 pub const fn new(allow_empty: bool, allow_same_document: bool, allow_external: bool) -> Self {
185 Self {
186 allow_empty,
187 allow_same_document,
188 allow_external,
189 }
190 }
191
192 pub const SAME_DOCUMENT: Self = Self {
194 allow_empty: true,
195 allow_same_document: true,
196 allow_external: false,
197 };
198
199 pub const ALL: Self = Self {
203 allow_empty: true,
204 allow_same_document: true,
205 allow_external: true,
206 };
207
208 fn allows(self, uri: &str) -> bool {
209 match classify_uri(uri) {
210 UriClass::Empty => self.allow_empty,
211 UriClass::SameDocument => self.allow_same_document,
212 UriClass::External => self.allow_external,
213 }
214 }
215}
216
217impl Default for UriTypeSet {
218 fn default() -> Self {
219 Self::SAME_DOCUMENT
220 }
221}
222
223#[must_use = "configure the context and call verify(), or store it for reuse"]
225pub struct VerifyContext<'a> {
226 key: Option<&'a dyn VerifyingKey>,
227 key_resolver: Option<&'a dyn KeyResolver>,
228 policy: crate::policy::VerificationPolicy,
229 provider: &'a dyn crate::provider::CryptoProvider,
230 store_pre_digest: bool,
231 external_resources: Option<&'a HashMap<String, Vec<u8>>>,
232}
233
234impl<'a> VerifyContext<'a> {
235 pub fn new() -> Self {
244 Self {
245 key: None,
246 key_resolver: None,
247 policy: crate::policy::VerificationPolicy::default(),
248 provider: crate::provider::default_provider(),
249 store_pre_digest: false,
250 external_resources: None,
251 }
252 }
253
254 pub fn key(mut self, key: &'a dyn VerifyingKey) -> Self {
261 self.key = Some(key);
262 self
263 }
264
265 pub fn key_resolver(mut self, resolver: &'a dyn KeyResolver) -> Self {
267 self.key_resolver = Some(resolver);
268 self
269 }
270
271 pub fn policy(mut self, policy: crate::policy::VerificationPolicy) -> Self {
273 self.policy = policy;
274 self
275 }
276
277 pub fn provider(mut self, provider: &'a dyn crate::provider::CryptoProvider) -> Self {
279 self.provider = provider;
280 self
281 }
282
283 pub fn process_manifests(mut self, enabled: bool) -> Self {
314 self.policy.process_manifests = enabled;
315 self
316 }
317
318 pub fn allowed_uri_types(mut self, types: UriTypeSet) -> Self {
320 self.policy.reference_uri_types = types;
321 self
322 }
323
324 pub fn allowed_retrieval_method_uri_types(mut self, types: UriTypeSet) -> Self {
331 self.policy.retrieval_uri_types = types;
332 self
333 }
334
335 pub fn external_resources(mut self, resources: &'a HashMap<String, Vec<u8>>) -> Self {
343 self.external_resources = Some(resources);
344 self
345 }
346
347 pub fn allow_internal_dtd(mut self, enabled: bool) -> Self {
350 self.policy.xml.allow_internal_dtd = enabled;
351 self
352 }
353
354 pub fn allowed_transforms<I, S>(mut self, transforms: I) -> Self
365 where
366 I: IntoIterator<Item = S>,
367 S: Into<String>,
368 {
369 self.policy.transforms = Some(transforms.into_iter().map(Into::into).collect());
370 self
371 }
372
373 pub fn store_pre_digest(mut self, enabled: bool) -> Self {
381 self.store_pre_digest = enabled;
382 self
383 }
384
385 pub fn xpath_here_semantics(mut self, semantics: XPathHereSemantics) -> Self {
391 self.policy.xpath_here_semantics = semantics;
392 self
393 }
394
395 fn allowed_transform_uris(&self) -> Option<&HashSet<String>> {
396 self.policy.transforms.as_ref()
397 }
398
399 fn transform_options(&self) -> TransformOptions {
400 TransformOptions::default()
401 .allow_internal_dtd(self.policy.xml.allow_internal_dtd)
402 .xpath_here_semantics(self.policy.xpath_here_semantics)
403 }
404
405 pub fn verify(&self, xml: &str) -> Result<VerifyResult, DsigError> {
413 verify_signature_with_context(xml, self)
414 }
415}
416
417impl Default for VerifyContext<'_> {
418 fn default() -> Self {
419 Self::new()
420 }
421}
422
423#[derive(Debug)]
425#[non_exhaustive]
426#[must_use = "inspect status before accepting the reference result"]
427pub struct ReferenceResult {
428 pub reference_set: ReferenceSet,
430 pub reference_index: usize,
432 pub uri: String,
434 pub digest_algorithm: DigestAlgorithm,
436 pub status: DsigStatus,
438 pub pre_digest_data: Option<Vec<u8>>,
440}
441
442#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444#[non_exhaustive]
445pub enum ReferenceSet {
446 SignedInfo,
448 Manifest,
450}
451
452#[derive(Debug, Clone, Copy, PartialEq, Eq)]
454#[non_exhaustive]
455pub enum DsigStatus {
456 Valid,
458 Invalid(FailureReason),
460}
461
462#[derive(Debug, Clone, Copy, PartialEq, Eq)]
464#[non_exhaustive]
465pub enum FailureReason {
466 ReferenceDigestMismatch {
468 ref_index: usize,
478 },
479 ReferencePolicyViolation {
481 ref_index: usize,
483 },
484 ReferenceProcessingFailure {
486 ref_index: usize,
488 },
489 SignatureMismatch,
491 KeyNotFound,
493}
494
495#[derive(Debug)]
497#[non_exhaustive]
498#[must_use = "check first_failure/results before accepting the reference set"]
499pub struct ReferencesResult {
500 pub results: Vec<ReferenceResult>,
503 pub first_failure: Option<usize>,
505}
506
507impl ReferencesResult {
508 #[must_use]
510 pub fn all_valid(&self) -> bool {
511 self.results
512 .iter()
513 .all(|result| matches!(result.status, DsigStatus::Valid))
514 }
515}
516
517pub fn process_reference(
536 reference: &Reference,
537 resolver: &UriReferenceResolver<'_>,
538 signature_node: Node<'_, '_>,
539 reference_set: ReferenceSet,
540 reference_index: usize,
541 store_pre_digest: bool,
542) -> Result<ReferenceResult, ReferenceProcessingError> {
543 let execution_budget = TransformExecutionBudget::default();
544 let canonicalized_data_budget = CanonicalizedDataBudget::default();
545 let execution = ReferenceExecutionContext {
546 store_pre_digest,
547 transform_options: TransformOptions::default(),
548 transform_budget: &execution_budget,
549 canonicalized_data_budget: &canonicalized_data_budget,
550 provider: crate::provider::default_provider(),
551 };
552 process_reference_with_options(
553 reference,
554 resolver,
555 signature_node,
556 reference_set,
557 reference_index,
558 reference_origin_node(signature_node, reference_set, reference_index),
559 &execution,
560 )
561}
562
563fn reference_origin_node<'a, 'input>(
564 signature_node: Node<'a, 'input>,
565 reference_set: ReferenceSet,
566 reference_index: usize,
567) -> Option<Node<'a, 'input>> {
568 let is_reference = |node: &Node<'_, '_>| {
569 node.is_element()
570 && node.tag_name().namespace() == Some(XMLDSIG_NS)
571 && node.tag_name().name() == "Reference"
572 };
573 match reference_set {
574 ReferenceSet::SignedInfo => signature_node
575 .children()
576 .find(|node| {
577 node.is_element()
578 && node.tag_name().namespace() == Some(XMLDSIG_NS)
579 && node.tag_name().name() == "SignedInfo"
580 })?
581 .children()
582 .filter(is_reference)
583 .nth(reference_index),
584 ReferenceSet::Manifest => signature_node
585 .children()
586 .filter(|node| {
587 node.is_element()
588 && node.tag_name().namespace() == Some(XMLDSIG_NS)
589 && node.tag_name().name() == "Object"
590 })
591 .flat_map(|object| {
592 object.children().filter(|node| {
593 node.is_element()
594 && node.tag_name().namespace() == Some(XMLDSIG_NS)
595 && node.tag_name().name() == "Manifest"
596 })
597 })
598 .flat_map(|manifest| manifest.children().filter(is_reference))
599 .nth(reference_index),
600 }
601}
602
603struct ReferenceExecutionContext<'a> {
604 store_pre_digest: bool,
605 transform_options: TransformOptions,
606 transform_budget: &'a TransformExecutionBudget,
607 canonicalized_data_budget: &'a CanonicalizedDataBudget,
608 provider: &'a dyn crate::provider::CryptoProvider,
609}
610
611struct CanonicalizedDataBudget {
612 remaining: Cell<usize>,
613 max_bytes: usize,
614}
615
616impl Default for CanonicalizedDataBudget {
617 fn default() -> Self {
618 Self {
619 remaining: Cell::new(CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING),
620 max_bytes: CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
621 }
622 }
623}
624
625impl CanonicalizedDataBudget {
626 fn remaining(&self) -> usize {
627 self.remaining.get()
628 }
629
630 fn charge(&self, bytes: usize) -> Result<(), ReferenceProcessingError> {
631 let Some(remaining) = self.remaining.get().checked_sub(bytes) else {
632 self.remaining.set(0);
633 return Err(ReferenceProcessingError::CanonicalizedDataTooLarge {
634 max_bytes: self.max_bytes,
635 });
636 };
637 self.remaining.set(remaining);
638 Ok(())
639 }
640
641 fn with_limit(max_bytes: usize) -> Self {
642 Self {
643 remaining: Cell::new(max_bytes),
644 max_bytes,
645 }
646 }
647}
648
649fn process_reference_with_options(
650 reference: &Reference,
651 resolver: &UriReferenceResolver<'_>,
652 signature_node: Node<'_, '_>,
653 reference_set: ReferenceSet,
654 reference_index: usize,
655 reference_node: Option<Node<'_, '_>>,
656 execution: &ReferenceExecutionContext<'_>,
657) -> Result<ReferenceResult, ReferenceProcessingError> {
658 let uri = reference
661 .uri
662 .as_deref()
663 .ok_or(ReferenceProcessingError::MissingUri)?;
664 let initial_data = reference_node
665 .map_or_else(
666 || {
667 resolver.dereference_with_budget(
668 uri,
669 execution.transform_budget.node_set_materialization(),
670 )
671 },
672 |node| {
673 resolver.dereference_from_with_budget(
674 uri,
675 node,
676 execution.transform_budget.node_set_materialization(),
677 execution.transform_budget.xml_base_resolution(),
678 )
679 },
680 )
681 .map_err(ReferenceProcessingError::UriDereference)?;
682
683 let pre_digest_bytes = execute_transforms_with_options_and_budget(
685 signature_node,
686 initial_data,
687 &reference.transforms,
688 execution.transform_options,
689 execution.transform_budget,
690 )
691 .map_err(ReferenceProcessingError::Transform)?;
692
693 let computed_digest = super::compute_digest_with_provider(
695 execution.provider,
696 reference.digest_method,
697 &pre_digest_bytes,
698 )?;
699
700 let status = if constant_time_eq(&computed_digest, &reference.digest_value) {
702 DsigStatus::Valid
703 } else {
704 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch {
705 ref_index: reference_index,
706 })
707 };
708
709 let pre_digest_data = if execution.store_pre_digest {
710 execution
711 .canonicalized_data_budget
712 .charge(pre_digest_bytes.len())?;
713 Some(pre_digest_bytes)
714 } else {
715 None
716 };
717
718 Ok(ReferenceResult {
719 reference_set,
720 reference_index,
721 uri: uri.to_owned(),
722 digest_algorithm: reference.digest_method,
723 status,
724 pre_digest_data,
725 })
726}
727
728pub fn process_all_references(
740 references: &[Reference],
741 resolver: &UriReferenceResolver<'_>,
742 signature_node: Node<'_, '_>,
743 store_pre_digest: bool,
744) -> Result<ReferencesResult, ReferenceProcessingError> {
745 let execution_budget = TransformExecutionBudget::default();
746 let canonicalized_data_budget = CanonicalizedDataBudget::default();
747 let execution = ReferenceExecutionContext {
748 store_pre_digest,
749 transform_options: TransformOptions::default(),
750 transform_budget: &execution_budget,
751 canonicalized_data_budget: &canonicalized_data_budget,
752 provider: crate::provider::default_provider(),
753 };
754 process_all_references_with_options(references, resolver, signature_node, &execution)
755}
756
757fn process_all_references_with_options(
758 references: &[Reference],
759 resolver: &UriReferenceResolver<'_>,
760 signature_node: Node<'_, '_>,
761 execution: &ReferenceExecutionContext<'_>,
762) -> Result<ReferencesResult, ReferenceProcessingError> {
763 let mut results = Vec::with_capacity(references.len());
764
765 for (i, reference) in references.iter().enumerate() {
766 let result = process_reference_with_options(
767 reference,
768 resolver,
769 signature_node,
770 ReferenceSet::SignedInfo,
771 i,
772 reference_origin_node(signature_node, ReferenceSet::SignedInfo, i),
773 execution,
774 )?;
775 let failed = matches!(result.status, DsigStatus::Invalid(_));
776 results.push(result);
777
778 if failed {
779 return Ok(ReferencesResult {
780 results,
781 first_failure: Some(i),
782 });
783 }
784 }
785
786 Ok(ReferencesResult {
787 results,
788 first_failure: None,
789 })
790}
791
792#[derive(Debug, thiserror::Error)]
796#[non_exhaustive]
797pub enum ReferenceProcessingError {
798 #[error("cryptographic provider error: {0}")]
800 Provider(#[from] crate::provider::ProviderError),
801
802 #[error("reference URI is required; omitted URI references are not supported")]
804 MissingUri,
805
806 #[error("URI dereference failed: {0}")]
808 UriDereference(#[source] super::types::TransformError),
809
810 #[error("transform failed: {0}")]
812 Transform(#[source] super::types::TransformError),
813
814 #[error("canonicalized signature data exceeds signature-wide maximum of {max_bytes} bytes")]
816 CanonicalizedDataTooLarge {
817 max_bytes: usize,
819 },
820}
821
822#[derive(Debug)]
824#[non_exhaustive]
825#[must_use = "inspect status before accepting the document"]
826pub struct VerifyResult {
827 pub status: DsigStatus,
832 pub signed_info_references: Vec<ReferenceResult>,
836 pub manifest_references: Vec<ReferenceResult>,
846 pub canonicalized_signed_info: Option<Vec<u8>>,
849}
850
851#[derive(Debug, thiserror::Error)]
853#[non_exhaustive]
854pub enum DsigError {
855 #[error("verification policy violation: {0}")]
857 Policy(#[from] crate::policy::PolicyViolation),
858
859 #[error("cryptographic provider error: {0}")]
861 Provider(#[from] crate::provider::ProviderError),
862
863 #[error("XML parse error: {0}")]
865 XmlParse(#[from] roxmltree::Error),
866
867 #[error("missing required element: <{element}>")]
869 MissingElement {
870 element: &'static str,
872 },
873
874 #[error("invalid Signature structure: {reason}")]
876 InvalidStructure {
877 reason: &'static str,
879 },
880
881 #[error("failed to parse SignedInfo: {0}")]
883 ParseSignedInfo(#[from] super::parse::ParseError),
884
885 #[error("failed to parse KeyInfo: {0}")]
887 ParseKeyInfo(#[source] super::parse::ParseError),
888
889 #[error("key resolution failed: {0}")]
891 KeyResolution(#[from] super::keys::KeyResolutionError),
892
893 #[error("failed to parse Manifest reference: {0}")]
895 ParseManifestReference(#[source] ParseError),
896
897 #[error("reference processing failed: {0}")]
899 Reference(#[from] ReferenceProcessingError),
900
901 #[error("SignedInfo canonicalization failed: {0}")]
903 Canonicalization(#[from] crate::c14n::C14nError),
904
905 #[error("invalid SignatureValue base64: {0}")]
907 SignatureValueBase64(#[from] base64::DecodeError),
908
909 #[error("signature verification failed: {0}")]
911 Crypto(#[from] SignatureVerificationError),
912
913 #[error("reference URI is not allowed by policy: {uri}")]
915 DisallowedUri {
916 uri: String,
918 },
919
920 #[error("transform is not allowed by policy: {algorithm}")]
922 DisallowedTransform {
923 algorithm: String,
925 },
926}
927
928type SignatureVerificationPipelineError = DsigError;
929
930pub fn verify_signature_with_pem_key(
958 xml: &str,
959 public_key_pem: &str,
960 store_pre_digest: bool,
961) -> Result<VerifyResult, DsigError> {
962 struct PemVerifyingKey<'a> {
963 public_key_pem: &'a str,
964 }
965
966 impl VerifyingKey for PemVerifyingKey<'_> {
967 fn verify(
968 &self,
969 algorithm: SignatureAlgorithm,
970 signed_data: &[u8],
971 signature_value: &[u8],
972 ) -> Result<bool, DsigError> {
973 verify_with_algorithm(algorithm, self.public_key_pem, signed_data, signature_value)
974 }
975 }
976
977 let key = PemVerifyingKey { public_key_pem };
978 VerifyContext::new()
979 .key(&key)
980 .store_pre_digest(store_pre_digest)
981 .verify(xml)
982}
983
984fn verify_signature_with_context(
985 xml: &str,
986 ctx: &VerifyContext<'_>,
987) -> Result<VerifyResult, SignatureVerificationPipelineError> {
988 ctx.policy.validate()?;
989 ctx.policy.resources.validate_xml_document_len(xml.len())?;
990 let doc = Document::parse_with_options(
991 xml,
992 roxmltree::ParsingOptions {
993 allow_dtd: ctx.policy.xml.allow_internal_dtd,
994 nodes_limit: ctx.policy.resources.effective_xml_nodes(),
995 entity_resolver: None,
996 },
997 )?;
998 let execution_budget = TransformExecutionBudget::from_resources(&ctx.policy.resources);
999 let mut signatures = doc.descendants().filter(|node| {
1000 node.is_element()
1001 && node.tag_name().name() == "Signature"
1002 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1003 });
1004 let signature_node = match (signatures.next(), signatures.next()) {
1005 (None, _) => {
1006 return Err(SignatureVerificationPipelineError::MissingElement {
1007 element: "Signature",
1008 });
1009 }
1010 (Some(node), None) => node,
1011 (Some(_), Some(_)) => {
1012 return Err(SignatureVerificationPipelineError::InvalidStructure {
1013 reason: "Signature must appear exactly once in document",
1014 });
1015 }
1016 };
1017
1018 let signature_children = parse_signature_children(signature_node)?;
1019 let signed_info_node = signature_children.signed_info_node;
1020 let should_parse_key_info = match (ctx.key, ctx.key_resolver) {
1021 (Some(_), _) => false,
1022 (None, Some(resolver)) => resolver.consumes_document_key_info(),
1023 (None, None) => true,
1024 };
1025 let mut key_info = if should_parse_key_info {
1026 signature_children
1027 .key_info_node
1028 .map(|node| {
1029 parse_key_info_with_provider_and_xml_base_budget(
1030 node,
1031 ctx.provider,
1032 execution_budget.xml_base_resolution(),
1033 )
1034 })
1035 .transpose()
1036 .map_err(SignatureVerificationPipelineError::ParseKeyInfo)?
1037 } else {
1038 None
1039 };
1040
1041 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
1042 let signed_info =
1043 parse_signed_info_with_xpath_budget(signed_info_node, &mut xpath_parse_budget)?;
1044 if signed_info.references.len() > ctx.policy.resources.max_references {
1045 return Err(crate::policy::PolicyViolation::ResourceLimit {
1046 resource: "signature references",
1047 maximum: ctx.policy.resources.max_references,
1048 actual: signed_info.references.len(),
1049 }
1050 .into());
1051 }
1052 for reference in &signed_info.references {
1053 if reference.transforms.len() > ctx.policy.resources.max_transforms_per_reference {
1054 return Err(crate::policy::PolicyViolation::ResourceLimit {
1055 resource: "reference transforms",
1056 maximum: ctx.policy.resources.max_transforms_per_reference,
1057 actual: reference.transforms.len(),
1058 }
1059 .into());
1060 }
1061 }
1062 ctx.policy
1063 .check_signature_algorithm(signed_info.signature_method)?;
1064 for reference in &signed_info.references {
1065 if ctx
1066 .policy
1067 .digest_algorithms
1068 .as_ref()
1069 .is_some_and(|allowed| !allowed.contains(&reference.digest_method))
1070 {
1071 return Err(crate::policy::PolicyViolation::Algorithm {
1072 operation: "verification",
1073 algorithm: reference.digest_method.uri().to_string(),
1074 }
1075 .into());
1076 }
1077 }
1078 enforce_reference_policies(
1079 &signed_info.references,
1080 ctx.policy.reference_uri_types,
1081 ctx.allowed_transform_uris(),
1082 )?;
1083 enforce_transform_allowed(ctx.allowed_transform_uris(), signed_info.c14n_method.uri())?;
1084
1085 if let Some(resources) = ctx.external_resources {
1086 let mut total = 0usize;
1087 for bytes in resources.values() {
1088 if bytes.len() > ctx.policy.resources.max_external_resource_bytes {
1089 return Err(crate::policy::PolicyViolation::ResourceLimit {
1090 resource: "external resource bytes",
1091 maximum: ctx.policy.resources.max_external_resource_bytes,
1092 actual: bytes.len(),
1093 }
1094 .into());
1095 }
1096 total = total.checked_add(bytes.len()).ok_or(
1097 SignatureVerificationPipelineError::InvalidStructure {
1098 reason: "external resource total length overflow",
1099 },
1100 )?;
1101 }
1102 if total > ctx.policy.resources.max_external_resource_total_bytes {
1103 return Err(crate::policy::PolicyViolation::ResourceLimit {
1104 resource: "aggregate external resource bytes",
1105 maximum: ctx.policy.resources.max_external_resource_total_bytes,
1106 actual: total,
1107 }
1108 .into());
1109 }
1110 }
1111 let resolver = UriReferenceResolver::new(&doc).with_external_resource_limits(
1112 ctx.policy.resources.max_external_resource_bytes,
1113 ctx.policy.resources.max_external_resource_total_bytes,
1114 );
1115 let resolver = match ctx.external_resources {
1116 Some(resources) => resolver.with_external_resources(resources),
1117 None => resolver,
1118 };
1119 let retrieval_materialization = if let Some(info) = key_info.as_mut() {
1120 materialize_retrieval_methods(
1121 info,
1122 &resolver,
1123 ctx.policy.retrieval_uri_types,
1124 ctx.allowed_transform_uris(),
1125 ctx.provider,
1126 )?
1127 } else {
1128 RetrievalMaterialization::default()
1129 };
1130 let canonicalized_data_budget =
1131 CanonicalizedDataBudget::with_limit(ctx.policy.resources.effective_canonicalized_bytes());
1132 let execution = ReferenceExecutionContext {
1133 store_pre_digest: ctx.store_pre_digest,
1134 transform_options: ctx.transform_options(),
1135 transform_budget: &execution_budget,
1136 canonicalized_data_budget: &canonicalized_data_budget,
1137 provider: ctx.provider,
1138 };
1139 let references = process_all_references_with_options(
1140 &signed_info.references,
1141 &resolver,
1142 signature_node,
1143 &execution,
1144 )?;
1145
1146 if let Some(first_failure) = references.first_failure {
1147 let status = references.results[first_failure].status;
1148 return Ok(VerifyResult {
1149 status,
1150 signed_info_references: references.results,
1151 manifest_references: Vec::new(),
1152 canonicalized_signed_info: None,
1153 });
1154 }
1155
1156 let signed_info_subtree: HashSet<_> = signed_info_node
1157 .descendants()
1158 .map(|node: Node<'_, '_>| node.id())
1159 .collect();
1160 let mut canonical_signed_info = Vec::new();
1161 let signed_info_limit = canonicalized_data_budget
1162 .remaining()
1163 .min(execution_budget.remaining_c14n_output());
1164 canonicalize_bounded_with_xml_base_budget(
1165 &doc,
1166 Some(&|node| signed_info_subtree.contains(&node.id())),
1167 &signed_info.c14n_method,
1168 signed_info_limit,
1169 execution_budget.xml_base_resolution(),
1170 &mut canonical_signed_info,
1171 )
1172 .map_err(|error| {
1173 if is_output_limit_error(&error) {
1174 SignatureVerificationPipelineError::Reference(
1175 ReferenceProcessingError::CanonicalizedDataTooLarge {
1176 max_bytes: canonicalized_data_budget.max_bytes,
1177 },
1178 )
1179 } else {
1180 SignatureVerificationPipelineError::Canonicalization(error)
1181 }
1182 })?;
1183 execution_budget
1184 .charge_c14n_output(canonical_signed_info.len())
1185 .map_err(ReferenceProcessingError::Transform)?;
1186 canonicalized_data_budget.charge(canonical_signed_info.len())?;
1187
1188 let signature_value = decode_signature_value(signature_children.signature_value_node)?;
1189 if signed_info.signature_method == SignatureAlgorithm::HmacSha1 {
1190 let expected_bits = signed_info.hmac_output_length_bits.unwrap_or(160);
1191 if signature_value.len() != expected_bits / 8 {
1192 return Err(SignatureVerificationPipelineError::InvalidStructure {
1193 reason: "SignatureValue length does not match HMACOutputLength",
1194 });
1195 }
1196 }
1197 let Some(resolved_key) =
1198 resolve_verifying_key(ctx, key_info.as_ref(), signed_info.signature_method)?
1199 else {
1200 if let Some(error) = retrieval_materialization.deferred_error {
1201 return Err(error);
1202 }
1203 return Ok(VerifyResult {
1204 status: DsigStatus::Invalid(FailureReason::KeyNotFound),
1205 signed_info_references: references.results,
1206 manifest_references: Vec::new(),
1207 canonicalized_signed_info: if ctx.store_pre_digest {
1208 Some(canonical_signed_info)
1209 } else {
1210 None
1211 },
1212 });
1213 };
1214 let verifier = resolved_key.as_ref();
1215 verifier.validate_policy(&ctx.policy)?;
1216 if !verifier.validate_signature_value(signed_info.signature_method, &signature_value)? {
1217 return Ok(VerifyResult {
1218 status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
1219 signed_info_references: references.results,
1220 manifest_references: Vec::new(),
1221 canonicalized_signed_info: if ctx.store_pre_digest {
1222 Some(canonical_signed_info)
1223 } else {
1224 None
1225 },
1226 });
1227 }
1228 let signature_valid = ctx.provider.verify(
1229 verifier,
1230 signed_info.signature_method,
1231 &canonical_signed_info,
1232 &signature_value,
1233 )?;
1234
1235 if !signature_valid {
1236 return Ok(VerifyResult {
1237 status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
1238 signed_info_references: references.results,
1239 manifest_references: Vec::new(),
1240 canonicalized_signed_info: if ctx.store_pre_digest {
1241 Some(canonical_signed_info)
1242 } else {
1243 None
1244 },
1245 });
1246 }
1247
1248 let manifest_references = if ctx.policy.process_manifests {
1249 let signed_info_reference_nodes =
1250 collect_authenticated_signed_info_reference_nodes(&signed_info.references, &resolver);
1251 let remaining_reference_capacity = ctx
1252 .policy
1253 .resources
1254 .max_references
1255 .checked_sub(signed_info.references.len())
1256 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
1257 reason: "SignedInfo exceeds the per-signature Reference limit",
1258 })?;
1259 process_manifest_references(
1260 signature_node,
1261 &resolver,
1262 ctx,
1263 &signed_info_reference_nodes,
1264 remaining_reference_capacity,
1265 &execution,
1266 &mut xpath_parse_budget,
1267 )?
1268 } else {
1269 Vec::new()
1270 };
1271
1272 Ok(VerifyResult {
1273 status: DsigStatus::Valid,
1274 signed_info_references: references.results,
1275 manifest_references,
1276 canonicalized_signed_info: if ctx.store_pre_digest {
1277 Some(canonical_signed_info)
1278 } else {
1279 None
1280 },
1281 })
1282}
1283
1284#[derive(Debug, Default)]
1285struct RetrievalMaterialization {
1286 deferred_error: Option<SignatureVerificationPipelineError>,
1287}
1288
1289fn materialize_retrieval_methods(
1290 key_info: &mut KeyInfo,
1291 resolver: &UriReferenceResolver<'_>,
1292 allowed_uri_types: UriTypeSet,
1293 allowed_transforms: Option<&HashSet<String>>,
1294 provider: &dyn crate::provider::CryptoProvider,
1295) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
1296 let retrieval_count = key_info
1297 .sources
1298 .iter()
1299 .filter(|source| matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. }))
1300 .count();
1301 if retrieval_count > MAX_RETRIEVAL_METHOD_COUNT {
1302 return Err(SignatureVerificationPipelineError::InvalidStructure {
1303 reason: "KeyInfo contains too many RetrievalMethod elements",
1304 });
1305 }
1306
1307 let mut total_binary_len = existing_x509_binary_len(key_info)?;
1308 let mut seen = HashSet::new();
1309 let mut materialized = Vec::with_capacity(key_info.sources.len());
1310 let mut outcome = RetrievalMaterialization::default();
1311 for source in std::mem::take(&mut key_info.sources) {
1312 let super::parse::KeyInfoSource::RetrievalMethod {
1313 uri: resolved_uri,
1314 resource_type,
1315 transforms,
1316 } = source
1317 else {
1318 materialized.push(source);
1319 continue;
1320 };
1321
1322 let identity = (resolved_uri.clone(), resource_type.clone(), transforms);
1323 if !seen.insert(identity) {
1324 continue;
1325 }
1326
1327 if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#rawX509Certificate")
1328 {
1329 if transforms != RetrievalMethodTransforms::None
1330 || classify_uri(&resolved_uri) != UriClass::External
1331 {
1332 return Err(SignatureVerificationPipelineError::InvalidStructure {
1333 reason: "raw X509 RetrievalMethod requires an untransformed external URI",
1334 });
1335 }
1336 if !allowed_uri_types.allows(&resolved_uri) {
1337 return Err(SignatureVerificationPipelineError::DisallowedUri {
1338 uri: resolved_uri,
1339 });
1340 }
1341 let certificate = resolver.external_resource(&resolved_uri).map_err(|error| {
1342 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
1343 error,
1344 ))
1345 })?;
1346 let Some(certificate) = certificate else {
1347 outcome.deferred_error.get_or_insert_with(|| {
1348 SignatureVerificationPipelineError::Reference(
1349 ReferenceProcessingError::Transform(super::TransformError::UnsupportedUri(
1350 resolved_uri.clone(),
1351 )),
1352 )
1353 });
1354 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
1355 uri: resolved_uri,
1356 resource_type,
1357 transforms,
1358 });
1359 continue;
1360 };
1361 if certificate.len() > MAX_X509_DECODED_BINARY_LEN {
1362 return Err(SignatureVerificationPipelineError::InvalidStructure {
1363 reason: "raw X509 RetrievalMethod certificate exceeds maximum allowed length",
1364 });
1365 }
1366 add_retrieval_binary_usage(&mut total_binary_len, certificate.len())?;
1367 let parsed = match parse_x509_certificate(certificate) {
1368 Ok(parsed) => parsed,
1369 Err(error) => {
1370 outcome
1371 .deferred_error
1372 .get_or_insert(SignatureVerificationPipelineError::ParseKeyInfo(error));
1373 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
1374 uri: resolved_uri,
1375 resource_type,
1376 transforms,
1377 });
1378 continue;
1379 }
1380 };
1381 materialized.push(super::parse::KeyInfoSource::X509Data(
1382 super::parse::X509DataInfo {
1383 certificates: vec![certificate.to_vec()],
1384 parsed_certificates: vec![parsed],
1385 certificate_chain: vec![0],
1386 ..super::parse::X509DataInfo::default()
1387 },
1388 ));
1389 } else if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#X509Data") {
1390 if !allowed_uri_types.allows(&resolved_uri) {
1391 return Err(SignatureVerificationPipelineError::DisallowedUri {
1392 uri: resolved_uri,
1393 });
1394 }
1395 let id = same_document_reference_id(&resolved_uri).ok_or(
1396 SignatureVerificationPipelineError::InvalidStructure {
1397 reason: "X509Data RetrievalMethod requires a same-document URI",
1398 },
1399 )?;
1400 let target = resolver.node_for_id(id).ok_or(
1401 SignatureVerificationPipelineError::InvalidStructure {
1402 reason: "X509Data RetrievalMethod target is missing or ambiguous",
1403 },
1404 )?;
1405 let node = match transforms {
1406 RetrievalMethodTransforms::None
1407 if target.has_tag_name((XMLDSIG_NS, "X509Data")) =>
1408 {
1409 target
1410 }
1411 RetrievalMethodTransforms::None => {
1412 return Err(SignatureVerificationPipelineError::InvalidStructure {
1413 reason: "untransformed X509Data RetrievalMethod must target X509Data directly",
1414 });
1415 }
1416 RetrievalMethodTransforms::X509DataNodeSetFilter => {
1417 enforce_transform_allowed(allowed_transforms, XPATH_TRANSFORM_URI)?;
1418 select_retrieved_x509_data_root(target)?
1419 }
1420 RetrievalMethodTransforms::Unsupported => {
1421 return Err(SignatureVerificationPipelineError::InvalidStructure {
1422 reason: "X509Data RetrievalMethod contains unsupported transforms",
1423 });
1424 }
1425 };
1426 let data = parse_x509_data_dispatch_with_budget_and_provider(
1427 node,
1428 &mut total_binary_len,
1429 provider,
1430 )
1431 .map_err(SignatureVerificationPipelineError::ParseKeyInfo)?;
1432 materialized.push(super::parse::KeyInfoSource::X509Data(data));
1433 } else {
1434 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
1435 uri: resolved_uri,
1436 resource_type,
1437 transforms,
1438 });
1439 }
1440 }
1441 key_info.sources = materialized;
1442 Ok(outcome)
1443}
1444
1445fn select_retrieved_x509_data_root<'a, 'input>(
1446 target: Node<'a, 'input>,
1447) -> Result<Node<'a, 'input>, SignatureVerificationPipelineError> {
1448 let mut roots = target.descendants().filter(|candidate| {
1453 candidate.is_element()
1454 && candidate.tag_name().namespace() == Some(XMLDSIG_NS)
1455 && candidate.tag_name().name() == "X509Data"
1456 });
1457 let root = roots
1458 .next()
1459 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
1460 reason: "X509Data RetrievalMethod selected no X509Data element",
1461 })?;
1462 if roots.next().is_some() {
1463 return Err(SignatureVerificationPipelineError::InvalidStructure {
1464 reason: "X509Data RetrievalMethod selected multiple X509Data elements",
1465 });
1466 }
1467 Ok(root)
1468}
1469
1470fn existing_x509_binary_len(
1471 key_info: &KeyInfo,
1472) -> Result<usize, SignatureVerificationPipelineError> {
1473 let mut total = 0usize;
1474 for source in &key_info.sources {
1475 if let super::parse::KeyInfoSource::X509Data(info) = source {
1476 for len in info
1477 .certificates
1478 .iter()
1479 .chain(&info.skis)
1480 .chain(&info.crls)
1481 .map(Vec::len)
1482 .chain(info.digests.iter().map(|(_, digest)| digest.len()))
1483 {
1484 add_retrieval_binary_usage(&mut total, len)?;
1485 }
1486 }
1487 }
1488 Ok(total)
1489}
1490
1491fn add_retrieval_binary_usage(
1492 total: &mut usize,
1493 delta: usize,
1494) -> Result<(), SignatureVerificationPipelineError> {
1495 *total =
1496 total
1497 .checked_add(delta)
1498 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
1499 reason: "RetrievalMethod X509Data binary length overflow",
1500 })?;
1501 if *total > MAX_X509_DATA_TOTAL_BINARY_LEN {
1502 return Err(SignatureVerificationPipelineError::InvalidStructure {
1503 reason: "RetrievalMethod X509Data exceeds maximum aggregate binary length",
1504 });
1505 }
1506 Ok(())
1507}
1508
1509fn process_manifest_references(
1510 signature_node: Node<'_, '_>,
1511 resolver: &UriReferenceResolver<'_>,
1512 ctx: &VerifyContext<'_>,
1513 signed_info_reference_nodes: &HashSet<NodeId>,
1514 remaining_reference_capacity: usize,
1515 execution: &ReferenceExecutionContext<'_>,
1516 xpath_parse_budget: &mut XPathSignatureParseBudget,
1517) -> Result<Vec<ReferenceResult>, SignatureVerificationPipelineError> {
1518 let parsed = parse_manifest_references(
1519 signature_node,
1520 signed_info_reference_nodes,
1521 remaining_reference_capacity,
1522 xpath_parse_budget,
1523 )?;
1524 let manifest_references = parsed.references;
1525 let mut results = parsed.invalid_results;
1526 if manifest_references.is_empty() && results.is_empty() {
1527 return Ok(Vec::new());
1528 }
1529 results.reserve(manifest_references.len());
1530 for (index, reference, reference_node_id) in &manifest_references {
1531 if execution.transform_budget.remaining_c14n_output() == 0 {
1532 results.push(manifest_reference_invalid_result(
1533 reference,
1534 *index,
1535 FailureReason::ReferenceProcessingFailure { ref_index: *index },
1536 ));
1537 continue;
1538 }
1539 if reference.transforms.len() > ctx.policy.resources.max_transforms_per_reference {
1540 results.push(manifest_reference_invalid_result(
1541 reference,
1542 *index,
1543 FailureReason::ReferencePolicyViolation { ref_index: *index },
1544 ));
1545 continue;
1546 }
1547 if ctx
1548 .policy
1549 .digest_algorithms
1550 .as_ref()
1551 .is_some_and(|allowed| !allowed.contains(&reference.digest_method))
1552 {
1553 results.push(manifest_reference_invalid_result(
1554 reference,
1555 *index,
1556 FailureReason::ReferencePolicyViolation { ref_index: *index },
1557 ));
1558 continue;
1559 }
1560 match enforce_reference_policies(
1561 std::slice::from_ref(reference),
1562 ctx.policy.reference_uri_types,
1563 ctx.allowed_transform_uris(),
1564 ) {
1565 Ok(()) => {}
1566 Err(
1567 SignatureVerificationPipelineError::DisallowedUri { .. }
1568 | SignatureVerificationPipelineError::DisallowedTransform { .. },
1569 ) => {
1570 results.push(manifest_reference_invalid_result(
1571 reference,
1572 *index,
1573 FailureReason::ReferencePolicyViolation { ref_index: *index },
1574 ));
1575 continue;
1576 }
1577 Err(SignatureVerificationPipelineError::Reference(
1578 ReferenceProcessingError::MissingUri,
1579 )) => {
1580 results.push(manifest_reference_invalid_result(
1581 reference,
1582 *index,
1583 FailureReason::ReferenceProcessingFailure { ref_index: *index },
1584 ));
1585 continue;
1586 }
1587 Err(_) => {
1588 results.push(manifest_reference_invalid_result(
1591 reference,
1592 *index,
1593 FailureReason::ReferenceProcessingFailure { ref_index: *index },
1594 ));
1595 continue;
1596 }
1597 }
1598
1599 match process_reference_with_options(
1600 reference,
1601 resolver,
1602 signature_node,
1603 ReferenceSet::Manifest,
1604 *index,
1605 resolver.node_for_node_id(*reference_node_id),
1606 execution,
1607 ) {
1608 Ok(result) => results.push(result),
1609 Err(_) => results.push(manifest_reference_invalid_result(
1610 reference,
1611 *index,
1612 FailureReason::ReferenceProcessingFailure { ref_index: *index },
1613 )),
1614 }
1615 }
1616 results.sort_by_key(|result| result.reference_index);
1617 Ok(results)
1618}
1619
1620fn manifest_reference_invalid_result(
1621 reference: &Reference,
1622 index: usize,
1623 reason: FailureReason,
1624) -> ReferenceResult {
1625 ReferenceResult {
1626 reference_set: ReferenceSet::Manifest,
1627 reference_index: index,
1628 uri: reference
1629 .uri
1630 .clone()
1631 .unwrap_or_else(|| "<omitted>".to_owned()),
1632 digest_algorithm: reference.digest_method,
1633 status: DsigStatus::Invalid(reason),
1634 pre_digest_data: None,
1635 }
1636}
1637
1638fn parse_manifest_references(
1639 signature_node: Node<'_, '_>,
1640 signed_info_reference_nodes: &HashSet<NodeId>,
1641 remaining_reference_capacity: usize,
1642 xpath_parse_budget: &mut XPathSignatureParseBudget,
1643) -> Result<ParsedManifestReferences, SignatureVerificationPipelineError> {
1644 let mut references = Vec::new();
1645 let mut invalid = Vec::new();
1646 let mut reference_index = 0usize;
1647 for object_node in signature_node.children().filter(|node| {
1648 node.is_element()
1649 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1650 && node.tag_name().name() == "Object"
1651 }) {
1652 let object_is_signed = signed_info_reference_nodes.contains(&object_node.id());
1653 for manifest_node in object_node.children().filter(|node| {
1654 node.is_element()
1655 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1656 && node.tag_name().name() == "Manifest"
1657 }) {
1658 let manifest_is_signed = signed_info_reference_nodes.contains(&manifest_node.id());
1659 if !object_is_signed && !manifest_is_signed {
1660 continue;
1661 }
1662 let mut manifest_children = Vec::new();
1663 for child in manifest_node.children() {
1664 if child.is_text()
1665 && child.text().is_some_and(|text| {
1666 text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
1667 })
1668 {
1669 return Err(SignatureVerificationPipelineError::InvalidStructure {
1670 reason: "Manifest contains non-whitespace mixed content",
1671 });
1672 }
1673 if child.is_element() {
1674 manifest_children.push(child);
1675 }
1676 }
1677 if manifest_children.is_empty() {
1678 return Err(SignatureVerificationPipelineError::InvalidStructure {
1679 reason: "Manifest must contain at least one ds:Reference element child",
1680 });
1681 }
1682 for child in manifest_children {
1683 if child.tag_name().namespace() != Some(XMLDSIG_NS)
1684 || child.tag_name().name() != "Reference"
1685 {
1686 return Err(SignatureVerificationPipelineError::InvalidStructure {
1687 reason: "Manifest must contain only ds:Reference element children",
1688 });
1689 }
1690 if references.len() + invalid.len() >= remaining_reference_capacity {
1691 return Err(SignatureVerificationPipelineError::InvalidStructure {
1692 reason: "signed Manifests exceed the per-signature Reference limit",
1693 });
1694 }
1695 match parse_reference_with_xpath_budget(child, xpath_parse_budget) {
1696 Ok(reference) => references.push((reference_index, reference, child.id())),
1697 Err(ParseError::Transform(super::TransformError::UnsupportedTransform(_))) => {
1698 let digest_algorithm = reference_digest_method(child).map_err(|error| {
1699 SignatureVerificationPipelineError::ParseManifestReference(error)
1700 })?;
1701 invalid.push(ReferenceResult {
1702 reference_set: ReferenceSet::Manifest,
1703 reference_index,
1704 uri: child.attribute("URI").unwrap_or("<omitted>").to_owned(),
1705 digest_algorithm,
1706 status: DsigStatus::Invalid(
1707 FailureReason::ReferenceProcessingFailure {
1708 ref_index: reference_index,
1709 },
1710 ),
1711 pre_digest_data: None,
1712 });
1713 }
1714 Err(error) => {
1715 return Err(SignatureVerificationPipelineError::ParseManifestReference(
1716 error,
1717 ));
1718 }
1719 }
1720 reference_index += 1;
1721 }
1722 }
1723 }
1724 Ok(ParsedManifestReferences {
1725 references,
1726 invalid_results: invalid,
1727 })
1728}
1729
1730struct ParsedManifestReferences {
1731 references: Vec<(usize, Reference, NodeId)>,
1732 invalid_results: Vec<ReferenceResult>,
1733}
1734
1735fn collect_authenticated_signed_info_reference_nodes(
1736 references: &[Reference],
1737 resolver: &UriReferenceResolver<'_>,
1738) -> HashSet<NodeId> {
1739 references
1740 .iter()
1741 .filter(|reference| {
1745 reference
1746 .transforms
1747 .iter()
1748 .all(transform_preserves_manifest_structure)
1749 })
1750 .filter_map(|reference| reference.uri.as_deref())
1751 .filter_map(same_document_reference_id)
1752 .filter_map(|id| resolver.node_id_for_id(id))
1753 .collect()
1754}
1755
1756fn transform_preserves_manifest_structure(transform: &Transform) -> bool {
1757 match transform {
1758 Transform::C14n(_) => true,
1759 Transform::Enveloped
1764 | Transform::XpathExcludeAllSignatures
1765 | Transform::XPath(_)
1766 | Transform::XPathFilter2(_)
1767 | Transform::Base64Decode => false,
1768 }
1769}
1770
1771enum ResolvedVerifyingKey<'a> {
1772 Borrowed(&'a dyn VerifyingKey),
1773 Owned(Box<dyn VerifyingKey + 'a>),
1774}
1775
1776impl ResolvedVerifyingKey<'_> {
1777 fn as_ref(&self) -> &dyn VerifyingKey {
1778 match self {
1779 Self::Borrowed(key) => *key,
1780 Self::Owned(key) => key.as_ref(),
1781 }
1782 }
1783}
1784
1785fn resolve_verifying_key<'k>(
1786 ctx: &VerifyContext<'k>,
1787 key_info: Option<&KeyInfo>,
1788 algorithm: SignatureAlgorithm,
1789) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
1790 if let Some(key) = ctx.key {
1791 return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
1792 }
1793 if let Some(resolver) = ctx.key_resolver {
1794 let resolved = resolver.resolve_with_policy_and_provider(
1795 key_info,
1796 algorithm,
1797 &ctx.policy,
1798 ctx.provider,
1799 )?;
1800 return Ok(resolved.map(ResolvedVerifyingKey::Owned));
1801 }
1802 Ok(None)
1803}
1804
1805fn enforce_reference_policies(
1806 references: &[Reference],
1807 allowed_uri_types: UriTypeSet,
1808 allowed_transforms: Option<&HashSet<String>>,
1809) -> Result<(), SignatureVerificationPipelineError> {
1810 for reference in references {
1811 let uri = reference
1812 .uri
1813 .as_deref()
1814 .ok_or(SignatureVerificationPipelineError::Reference(
1815 ReferenceProcessingError::MissingUri,
1816 ))?;
1817 if !allowed_uri_types.allows(uri) {
1818 return Err(SignatureVerificationPipelineError::DisallowedUri {
1819 uri: uri.to_owned(),
1820 });
1821 }
1822
1823 if let Some(allowed) = allowed_transforms {
1824 for transform in &reference.transforms {
1825 let transform_uri = transform.algorithm_uri();
1826 enforce_transform_allowed(Some(allowed), transform_uri)?;
1827 }
1828
1829 let produces_binary = transform_chain_produces_binary(
1834 classify_uri(uri) == UriClass::External,
1835 &reference.transforms,
1836 );
1837 if !produces_binary {
1838 enforce_transform_allowed(Some(allowed), DEFAULT_IMPLICIT_C14N_URI)?;
1839 }
1840 }
1841 }
1842 Ok(())
1843}
1844
1845fn enforce_transform_allowed(
1846 allowed_transforms: Option<&HashSet<String>>,
1847 algorithm: &str,
1848) -> Result<(), SignatureVerificationPipelineError> {
1849 if allowed_transforms.is_some_and(|allowed| !allowed.contains(algorithm)) {
1850 return Err(SignatureVerificationPipelineError::DisallowedTransform {
1851 algorithm: algorithm.to_owned(),
1852 });
1853 }
1854 Ok(())
1855}
1856
1857#[derive(Debug, Clone, Copy)]
1858struct SignatureChildNodes<'a, 'input> {
1859 signed_info_node: Node<'a, 'input>,
1860 signature_value_node: Node<'a, 'input>,
1861 key_info_node: Option<Node<'a, 'input>>,
1862}
1863
1864fn parse_signature_children<'a, 'input>(
1865 signature_node: Node<'a, 'input>,
1866) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
1867 let mut signed_info_node: Option<Node<'_, '_>> = None;
1868 let mut signature_value_node: Option<Node<'_, '_>> = None;
1869 let mut key_info_node: Option<Node<'_, '_>> = None;
1870 let mut signed_info_index: Option<usize> = None;
1871 let mut signature_value_index: Option<usize> = None;
1872 let mut key_info_index: Option<usize> = None;
1873 let mut first_unexpected_dsig_index: Option<usize> = None;
1874
1875 let mut element_index = 0usize;
1876 for child in signature_node.children() {
1877 if child.is_text() {
1878 if child
1879 .text()
1880 .is_some_and(|text| !is_xml_whitespace_only(text))
1881 {
1882 return Err(SignatureVerificationPipelineError::InvalidStructure {
1883 reason: "Signature must not contain non-whitespace mixed content",
1884 });
1885 }
1886 continue;
1887 }
1888 if !child.is_element() {
1889 continue;
1890 }
1891
1892 element_index += 1;
1893 if child.tag_name().namespace() != Some(XMLDSIG_NS) {
1894 return Err(SignatureVerificationPipelineError::InvalidStructure {
1895 reason: "Signature must contain only XMLDSIG element children",
1896 });
1897 }
1898 match child.tag_name().name() {
1899 "SignedInfo" => {
1900 if signed_info_node.is_some() {
1901 return Err(SignatureVerificationPipelineError::InvalidStructure {
1902 reason: "SignedInfo must appear exactly once under Signature",
1903 });
1904 }
1905 signed_info_node = Some(child);
1906 signed_info_index = Some(element_index);
1907 }
1908 "SignatureValue" => {
1909 if signature_value_node.is_some() {
1910 return Err(SignatureVerificationPipelineError::InvalidStructure {
1911 reason: "SignatureValue must appear exactly once under Signature",
1912 });
1913 }
1914 signature_value_node = Some(child);
1915 signature_value_index = Some(element_index);
1916 }
1917 "KeyInfo" => {
1918 if key_info_node.is_some() {
1919 return Err(SignatureVerificationPipelineError::InvalidStructure {
1920 reason: "KeyInfo must appear at most once under Signature",
1921 });
1922 }
1923 key_info_node = Some(child);
1924 key_info_index = Some(element_index);
1925 }
1926 "Object" => {
1927 }
1930 _ => {
1931 if first_unexpected_dsig_index.is_none() {
1932 first_unexpected_dsig_index = Some(element_index);
1933 }
1934 }
1935 }
1936 }
1937
1938 let signed_info_node =
1939 signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
1940 element: "SignedInfo",
1941 })?;
1942 let signature_value_node =
1943 signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
1944 element: "SignatureValue",
1945 })?;
1946 if signed_info_index != Some(1) {
1947 return Err(SignatureVerificationPipelineError::InvalidStructure {
1948 reason: "SignedInfo must be the first element child of Signature",
1949 });
1950 }
1951 if signature_value_index != Some(2) {
1952 return Err(SignatureVerificationPipelineError::InvalidStructure {
1953 reason: "SignatureValue must be the second element child of Signature",
1954 });
1955 }
1956 if let Some(index) = key_info_index
1957 && index != 3
1958 {
1959 return Err(SignatureVerificationPipelineError::InvalidStructure {
1960 reason: "KeyInfo must be the third element child of Signature when present",
1961 });
1962 }
1963
1964 let allowed_prefix_end = key_info_index.unwrap_or(2);
1965 if let Some(unexpected_index) = first_unexpected_dsig_index {
1966 return Err(SignatureVerificationPipelineError::InvalidStructure {
1967 reason: if unexpected_index > allowed_prefix_end {
1968 "After SignedInfo, SignatureValue, and optional KeyInfo, Signature may contain only Object elements"
1969 } else {
1970 "Signature may contain SignedInfo first, SignatureValue second, optional KeyInfo third, and Object elements thereafter"
1971 },
1972 });
1973 }
1974
1975 Ok(SignatureChildNodes {
1976 signed_info_node,
1977 signature_value_node,
1978 key_info_node,
1979 })
1980}
1981
1982fn decode_signature_value(
1983 signature_value_node: Node<'_, '_>,
1984) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
1985 if signature_value_node
1986 .children()
1987 .any(|child| child.is_element())
1988 {
1989 return Err(SignatureVerificationPipelineError::InvalidStructure {
1990 reason: "SignatureValue must not contain element children",
1991 });
1992 }
1993
1994 let mut normalized = Vec::new();
1995 let mut raw_text_len = 0usize;
1996 for child in signature_value_node
1997 .children()
1998 .filter(|child| child.is_text())
1999 {
2000 if let Some(text) = child.text() {
2001 push_normalized_signature_text(text, &mut raw_text_len, &mut normalized)?;
2002 }
2003 }
2004
2005 Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
2006}
2007
2008fn push_normalized_signature_text(
2009 text: &str,
2010 raw_text_len: &mut usize,
2011 normalized: &mut Vec<u8>,
2012) -> Result<(), SignatureVerificationPipelineError> {
2013 if raw_text_len.saturating_add(text.len()) > MAX_SIGNATURE_VALUE_TEXT_LEN {
2014 return Err(SignatureVerificationPipelineError::InvalidStructure {
2015 reason: "SignatureValue exceeds maximum allowed text length",
2016 });
2017 }
2018 *raw_text_len = raw_text_len.saturating_add(text.len());
2019
2020 normalize_xml_base64_bytes(text.as_bytes(), normalized, |_| true).map_err(|err| {
2021 SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(
2022 err.normalized_offset,
2023 err.invalid_byte,
2024 ))
2025 })?;
2026 if normalized.len() > MAX_SIGNATURE_VALUE_LEN {
2027 return Err(SignatureVerificationPipelineError::InvalidStructure {
2028 reason: "SignatureValue exceeds maximum allowed length",
2029 });
2030 }
2031
2032 Ok(())
2033}
2034
2035fn verify_with_algorithm(
2036 algorithm: SignatureAlgorithm,
2037 public_key_pem: &str,
2038 signed_data: &[u8],
2039 signature_value: &[u8],
2040) -> Result<bool, SignatureVerificationPipelineError> {
2041 match algorithm {
2042 SignatureAlgorithm::DsaSha1 => {
2043 let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
2044 .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
2045 if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
2046 return Err(SignatureVerificationError::InvalidKeyPem.into());
2047 }
2048 Ok(verify_dsa_signature_spki(
2049 algorithm,
2050 &pem.contents,
2051 signed_data,
2052 signature_value,
2053 )?)
2054 }
2055 SignatureAlgorithm::HmacSha1 => Err(SignatureVerificationError::UnsupportedAlgorithm {
2056 uri: algorithm.uri().to_string(),
2057 }
2058 .into()),
2059 SignatureAlgorithm::RsaSha1
2060 | SignatureAlgorithm::RsaSha256
2061 | SignatureAlgorithm::RsaSha384
2062 | SignatureAlgorithm::RsaSha512 => Ok(verify_rsa_signature_pem(
2063 algorithm,
2064 public_key_pem,
2065 signed_data,
2066 signature_value,
2067 )?),
2068 SignatureAlgorithm::EcdsaSha256 | SignatureAlgorithm::EcdsaSha384 => {
2069 match verify_ecdsa_signature_pem(
2073 algorithm,
2074 public_key_pem,
2075 signed_data,
2076 signature_value,
2077 ) {
2078 Ok(valid) => Ok(valid),
2079 Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
2080 Err(error) => Err(error.into()),
2081 }
2082 }
2083 }
2084}
2085
2086#[cfg(test)]
2087#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
2088mod tests {
2089 use super::*;
2090 use crate::c14n::C14nAlgorithm;
2091 use crate::xmldsig::TransformError;
2092 use crate::xmldsig::digest::DigestAlgorithm;
2093 use crate::xmldsig::parse::{Reference, parse_signed_info};
2094 use crate::xmldsig::transforms::Transform;
2095 use crate::xmldsig::uri::UriReferenceResolver;
2096 use base64::Engine;
2097 use roxmltree::Document;
2098
2099 fn make_reference(
2103 uri: &str,
2104 transforms: Vec<Transform>,
2105 digest_method: DigestAlgorithm,
2106 digest_value: Vec<u8>,
2107 ) -> Reference {
2108 Reference {
2109 uri: Some(uri.to_string()),
2110 id: None,
2111 ref_type: None,
2112 transforms,
2113 digest_method,
2114 digest_value,
2115 }
2116 }
2117
2118 #[test]
2119 fn reference_resolution_uses_each_elements_effective_xml_base() {
2120 let first = b"first payload";
2123 let second = b"second payload";
2124 let first_digest = base64::engine::general_purpose::STANDARD
2125 .encode(compute_digest(DigestAlgorithm::Sha256, first));
2126 let second_digest = base64::engine::general_purpose::STANDARD
2127 .encode(compute_digest(DigestAlgorithm::Sha256, second));
2128 let xml = format!(
2129 r#"<root xml:base="https://example.test/base/" xmlns:ds="{XMLDSIG_NS}">
2130 <ds:Signature><ds:SignedInfo>
2131 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2132 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2133 <ds:Reference xml:base="one/" URI="payload.bin">
2134 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2135 <ds:DigestValue>{first_digest}</ds:DigestValue>
2136 </ds:Reference>
2137 <ds:Reference xml:base="../two/" URI="payload.bin">
2138 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2139 <ds:DigestValue>{second_digest}</ds:DigestValue>
2140 </ds:Reference>
2141 </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>
2142 </root>"#
2143 );
2144 let document = Document::parse(&xml).unwrap();
2145 let signature = document
2146 .descendants()
2147 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
2148 .unwrap();
2149 let signed_info_node = signature
2150 .children()
2151 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
2152 .unwrap();
2153 let signed_info = parse_signed_info(signed_info_node).unwrap();
2154 let resources = HashMap::from([
2155 (
2156 "https://example.test/base/one/payload.bin".into(),
2157 first.to_vec(),
2158 ),
2159 (
2160 "https://example.test/two/payload.bin".into(),
2161 second.to_vec(),
2162 ),
2163 ]);
2164 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
2165
2166 let result = process_all_references(&signed_info.references, &resolver, signature, false)
2167 .expect("each Reference should resolve against its own effective base");
2168
2169 assert!(result.all_valid());
2170 }
2171
2172 #[test]
2173 fn internal_dtd_opt_in_applies_to_detached_xml_transforms() {
2174 let detached = b"<!DOCTYPE payload [<!ELEMENT payload (#PCDATA)>]><payload>ok</payload>";
2177 let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
2178 DigestAlgorithm::Sha256,
2179 b"<payload>ok</payload>",
2180 ));
2181 let xml = format!(
2182 r#"<root xmlns:ds="{XMLDSIG_NS}">
2183 <ds:Signature>
2184 <ds:SignedInfo>
2185 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2186 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2187 <ds:Reference URI="urn:detached-dtd">
2188 <ds:Transforms>
2189 <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
2190 </ds:Transforms>
2191 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2192 <ds:DigestValue>{digest}</ds:DigestValue>
2193 </ds:Reference>
2194 </ds:SignedInfo>
2195 <ds:SignatureValue>AQ==</ds:SignatureValue>
2196 </ds:Signature>
2197</root>"#
2198 );
2199 let resources = HashMap::from([("urn:detached-dtd".to_owned(), detached.to_vec())]);
2200 let key = AcceptingKey;
2201
2202 let default_error = VerifyContext::new()
2203 .key(&key)
2204 .allowed_uri_types(UriTypeSet::ALL)
2205 .external_resources(&resources)
2206 .verify(&xml)
2207 .expect_err("internal DTD parsing must remain disabled by default");
2208 assert!(matches!(
2209 default_error,
2210 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
2211 crate::xmldsig::TransformError::XmlParse(_)
2212 ))
2213 ));
2214
2215 let result = VerifyContext::new()
2216 .key(&key)
2217 .allowed_uri_types(UriTypeSet::ALL)
2218 .external_resources(&resources)
2219 .allow_internal_dtd(true)
2220 .verify(&xml)
2221 .expect("the explicit DTD opt-in must cover detached XML transforms");
2222
2223 assert_eq!(result.status, DsigStatus::Valid);
2224
2225 let external_entity = br#"<!DOCTYPE payload [
2226 <!ENTITY ext SYSTEM "file:///etc/passwd">
2227 ]><payload>&ext;</payload>"#;
2228 let external_entity_resources =
2229 HashMap::from([("urn:detached-dtd".to_owned(), external_entity.to_vec())]);
2230 let external_entity_error = VerifyContext::new()
2231 .key(&key)
2232 .allowed_uri_types(UriTypeSet::ALL)
2233 .external_resources(&external_entity_resources)
2234 .allow_internal_dtd(true)
2235 .verify(&xml)
2236 .expect_err("the internal-DTD opt-in must not resolve external entities");
2237 assert!(matches!(
2238 external_entity_error,
2239 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
2240 crate::xmldsig::TransformError::XmlParse(_)
2241 ))
2242 ));
2243 }
2244
2245 #[test]
2246 fn verification_policy_bounds_reference_canonicalization() {
2247 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
2250 let xml = format!(
2251 r#"<root xmlns:ds="{XMLDSIG_NS}"><payload>{}</payload><ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI=""><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"#,
2252 "payload".repeat(16)
2253 );
2254 let policy = crate::policy::VerificationPolicy {
2255 resources: crate::policy::ResourcePolicy {
2256 max_canonicalized_bytes: 64,
2257 ..crate::policy::ResourcePolicy::default()
2258 },
2259 ..crate::policy::VerificationPolicy::default()
2260 };
2261
2262 let error = VerifyContext::new()
2263 .key(&AcceptingKey)
2264 .policy(policy)
2265 .verify(&xml)
2266 .expect_err("reference canonicalization must consume the policy budget");
2267
2268 assert!(
2269 matches!(
2270 error,
2271 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
2272 TransformError::C14nOutputTooLarge { max_bytes: 64 }
2273 ))
2274 ),
2275 "unexpected error: {error:?}"
2276 );
2277 }
2278
2279 #[test]
2280 fn verification_policy_bounds_document_bytes_before_parsing() {
2281 let xml = format!("<root>{}</root>", "x".repeat(1_024));
2284 let policy = crate::policy::VerificationPolicy {
2285 resources: crate::policy::ResourcePolicy {
2286 max_xml_document_bytes: xml.len() - 1,
2287 ..crate::policy::ResourcePolicy::default()
2288 },
2289 ..crate::policy::VerificationPolicy::default()
2290 };
2291
2292 assert!(matches!(
2293 VerifyContext::new().policy(policy).verify(&xml),
2294 Err(SignatureVerificationPipelineError::Policy(
2295 crate::policy::PolicyViolation::ResourceLimit {
2296 resource: "XML document",
2297 maximum,
2298 actual,
2299 }
2300 )) if maximum == xml.len() - 1 && actual == xml.len()
2301 ));
2302 }
2303
2304 #[test]
2305 fn verification_policy_shares_canonicalization_budget_with_signed_info() {
2306 let payload_text = "x".repeat(700);
2310 let canonical_payload = format!("<payload ID=\"payload\">{payload_text}</payload>");
2311 let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
2312 DigestAlgorithm::Sha256,
2313 canonical_payload.as_bytes(),
2314 ));
2315 let xml = format!(
2316 r##"<root xmlns:ds="{XMLDSIG_NS}">{canonical_payload}<ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#payload"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"##
2317 );
2318 let policy = crate::policy::VerificationPolicy {
2319 resources: crate::policy::ResourcePolicy {
2320 max_canonicalized_bytes: 1_024,
2321 ..crate::policy::ResourcePolicy::default()
2322 },
2323 ..crate::policy::VerificationPolicy::default()
2324 };
2325
2326 let error = VerifyContext::new()
2327 .key(&AcceptingKey)
2328 .policy(policy)
2329 .verify(&xml)
2330 .expect_err("SignedInfo must consume the remaining operation C14N budget");
2331
2332 assert!(matches!(
2333 error,
2334 SignatureVerificationPipelineError::Reference(
2335 ReferenceProcessingError::CanonicalizedDataTooLarge { max_bytes: 1_024 }
2336 )
2337 ));
2338 }
2339
2340 #[test]
2341 fn manifest_processing_stops_after_c14n_budget_exhaustion() {
2342 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
2345 let xml = format!(
2346 r##"<root xmlns:ds="{XMLDSIG_NS}"><payload Id="payload">too large</payload><ds:Signature><ds:Object Id="signed-object"><ds:Manifest><ds:Reference URI="#payload"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference><ds:Reference URI="urn:small"><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:Manifest></ds:Object></ds:Signature></root>"##
2347 );
2348 let document = Document::parse(&xml).expect("test signature must parse");
2349 let signature = document
2350 .descendants()
2351 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
2352 .expect("test signature must contain Signature");
2353 let object = signature
2354 .children()
2355 .find(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
2356 .expect("test signature must contain Object");
2357 let resources = HashMap::from([("urn:small".to_owned(), b"small".to_vec())]);
2358 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
2359 let transform_budget = TransformExecutionBudget::with_c14n_limit(8);
2360 let canonicalized_data_budget = CanonicalizedDataBudget::default();
2361 let execution = ReferenceExecutionContext {
2362 store_pre_digest: false,
2363 transform_options: TransformOptions::default(),
2364 transform_budget: &transform_budget,
2365 canonicalized_data_budget: &canonicalized_data_budget,
2366 provider: crate::provider::default_provider(),
2367 };
2368 let ctx = VerifyContext::new()
2369 .allowed_uri_types(UriTypeSet::ALL)
2370 .external_resources(&resources);
2371 let authenticated = HashSet::from([object.id()]);
2372 let mut xpath_budget = XPathSignatureParseBudget::default();
2373
2374 let results = process_manifest_references(
2375 signature,
2376 &resolver,
2377 &ctx,
2378 &authenticated,
2379 2,
2380 &execution,
2381 &mut xpath_budget,
2382 )
2383 .expect("resource exhaustion is reported per Manifest reference");
2384
2385 assert_eq!(results.len(), 2);
2386 assert!(results.iter().all(|result| {
2387 matches!(
2388 result.status,
2389 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { .. })
2390 )
2391 }));
2392 }
2393
2394 #[test]
2395 fn verification_policy_bounds_detached_xml_nodes() {
2396 let detached = format!("<payload>{}</payload>", "<n/>".repeat(32));
2399 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
2400 let xml = format!(
2401 r#"<root xmlns:ds="{XMLDSIG_NS}"><ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="urn:detached-nodes"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>true()</ds:XPath></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"#
2402 );
2403 let resources = HashMap::from([("urn:detached-nodes".to_owned(), detached.into_bytes())]);
2404 let policy = crate::policy::VerificationPolicy {
2405 reference_uri_types: UriTypeSet::ALL,
2406 resources: crate::policy::ResourcePolicy {
2407 max_xml_nodes: 24,
2408 ..crate::policy::ResourcePolicy::default()
2409 },
2410 ..crate::policy::VerificationPolicy::default()
2411 };
2412
2413 let error = VerifyContext::new()
2414 .key(&AcceptingKey)
2415 .policy(policy)
2416 .external_resources(&resources)
2417 .verify(&xml)
2418 .expect_err("detached XML must inherit the policy node ceiling");
2419
2420 assert!(
2421 matches!(
2422 error,
2423 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
2424 TransformError::XmlNodeLimit
2425 ))
2426 ),
2427 "unexpected error: {error:?}"
2428 );
2429 }
2430
2431 #[test]
2432 fn query_only_reference_resolves_against_relative_xml_base() {
2433 let payload = b"query-selected payload";
2436 let digest = base64::engine::general_purpose::STANDARD
2437 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
2438 let xml = format!(
2439 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo>
2440 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2441 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2442 <ds:Reference xml:base="a/b?old" URI="?new">
2443 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2444 <ds:DigestValue>{digest}</ds:DigestValue>
2445 </ds:Reference>
2446 </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>"#
2447 );
2448 let document = Document::parse(&xml).unwrap();
2449 let signature = document.root_element();
2450 let signed_info_node = signature
2451 .children()
2452 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
2453 .unwrap();
2454 let signed_info = parse_signed_info(signed_info_node).unwrap();
2455 let resources = HashMap::from([("a/b?new".to_string(), payload.to_vec())]);
2456 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
2457
2458 let result = process_all_references(&signed_info.references, &resolver, signature, false)
2459 .expect("query-only URI must resolve against the complete relative base path");
2460
2461 assert!(result.all_valid());
2462 }
2463
2464 #[test]
2465 fn manifest_reference_resolution_uses_its_effective_xml_base() {
2466 let payload = b"manifest payload";
2469 let digest = base64::engine::general_purpose::STANDARD
2470 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
2471 let xml = format!(
2472 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
2473 <ds:Object><ds:Manifest xml:base="manifests/">
2474 <ds:Reference URI="payload.bin">
2475 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2476 <ds:DigestValue>{digest}</ds:DigestValue>
2477 </ds:Reference>
2478 </ds:Manifest></ds:Object>
2479 </ds:Signature>"#
2480 );
2481 let document = Document::parse(&xml).unwrap();
2482 let signature = document.root_element();
2483 let reference_node = signature
2484 .descendants()
2485 .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
2486 .unwrap();
2487 let reference = super::super::parse::parse_reference(reference_node).unwrap();
2488 let resources = HashMap::from([(
2489 "https://example.test/manifests/payload.bin".to_string(),
2490 payload.to_vec(),
2491 )]);
2492 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
2493
2494 let result = process_reference(
2495 &reference,
2496 &resolver,
2497 signature,
2498 ReferenceSet::Manifest,
2499 0,
2500 false,
2501 )
2502 .expect("Manifest Reference should inherit its own XML Base context");
2503
2504 assert_eq!(result.status, DsigStatus::Valid);
2505 }
2506
2507 #[test]
2508 fn manifest_reference_index_ignores_nested_manifest_descendants() {
2509 let payload = b"direct manifest payload";
2512 let digest = base64::engine::general_purpose::STANDARD
2513 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
2514 let xml = format!(
2515 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
2516 <ds:Object><wrapper><ds:Manifest xml:base="nested/">
2517 <ds:Reference URI="payload.bin">
2518 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2519 <ds:DigestValue>{digest}</ds:DigestValue>
2520 </ds:Reference>
2521 </ds:Manifest></wrapper></ds:Object>
2522 <ds:Object><ds:Manifest xml:base="direct/">
2523 <ds:Reference URI="payload.bin">
2524 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2525 <ds:DigestValue>{digest}</ds:DigestValue>
2526 </ds:Reference>
2527 </ds:Manifest></ds:Object>
2528 </ds:Signature>"#
2529 );
2530 let document = Document::parse(&xml).unwrap();
2531 let signature = document.root_element();
2532 let direct_reference_node = signature
2533 .children()
2534 .filter(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
2535 .nth(1)
2536 .unwrap()
2537 .children()
2538 .find(|node| node.has_tag_name((XMLDSIG_NS, "Manifest")))
2539 .unwrap()
2540 .children()
2541 .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
2542 .unwrap();
2543 let reference = super::super::parse::parse_reference(direct_reference_node).unwrap();
2544 let resources = HashMap::from([(
2545 "https://example.test/direct/payload.bin".to_string(),
2546 payload.to_vec(),
2547 )]);
2548 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
2549
2550 let result = process_reference(
2551 &reference,
2552 &resolver,
2553 signature,
2554 ReferenceSet::Manifest,
2555 0,
2556 false,
2557 )
2558 .expect("Manifest index must select the direct Object/Manifest reference");
2559
2560 assert_eq!(result.status, DsigStatus::Valid);
2561 }
2562
2563 struct RejectingKey;
2564
2565 impl VerifyingKey for RejectingKey {
2566 fn verify(
2567 &self,
2568 _algorithm: SignatureAlgorithm,
2569 _signed_data: &[u8],
2570 _signature_value: &[u8],
2571 ) -> Result<bool, SignatureVerificationPipelineError> {
2572 Ok(false)
2573 }
2574 }
2575
2576 struct AcceptingKey;
2577
2578 impl VerifyingKey for AcceptingKey {
2579 fn verify(
2580 &self,
2581 _algorithm: SignatureAlgorithm,
2582 _signed_data: &[u8],
2583 _signature_value: &[u8],
2584 ) -> Result<bool, SignatureVerificationPipelineError> {
2585 Ok(true)
2586 }
2587 }
2588
2589 struct PanicResolver;
2590
2591 impl KeyResolver for PanicResolver {
2592 fn resolve<'a>(
2593 &'a self,
2594 _key_info: Option<&KeyInfo>,
2595 _algorithm: SignatureAlgorithm,
2596 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
2597 {
2598 panic!("resolver should not be called when references already fail");
2599 }
2600 }
2601
2602 struct MissingKeyResolver;
2603
2604 impl KeyResolver for MissingKeyResolver {
2605 fn resolve<'a>(
2606 &'a self,
2607 _key_info: Option<&KeyInfo>,
2608 _algorithm: SignatureAlgorithm,
2609 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
2610 {
2611 Ok(None)
2612 }
2613 }
2614
2615 struct ConsumingKeyInfoResolver;
2616
2617 impl KeyResolver for ConsumingKeyInfoResolver {
2618 fn resolve<'a>(
2619 &'a self,
2620 _key_info: Option<&KeyInfo>,
2621 _algorithm: SignatureAlgorithm,
2622 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
2623 {
2624 Ok(None)
2625 }
2626
2627 fn consumes_document_key_info(&self) -> bool {
2628 true
2629 }
2630 }
2631
2632 struct FallbackKeyInfoResolver;
2633
2634 impl KeyResolver for FallbackKeyInfoResolver {
2635 fn resolve<'a>(
2636 &'a self,
2637 key_info: Option<&KeyInfo>,
2638 _algorithm: SignatureAlgorithm,
2639 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
2640 {
2641 let sources = &key_info.expect("KeyInfo must be parsed").sources;
2642 assert!(matches!(
2643 sources.as_slice(),
2644 [
2645 super::super::parse::KeyInfoSource::RetrievalMethod { .. },
2646 super::super::parse::KeyInfoSource::KeyName(name),
2647 ] if name == "fallback"
2648 ));
2649 Ok(Some(Box::new(AcceptingKey)))
2650 }
2651
2652 fn consumes_document_key_info(&self) -> bool {
2653 true
2654 }
2655 }
2656
2657 struct EarlyKeyInfoResolver;
2658
2659 impl KeyResolver for EarlyKeyInfoResolver {
2660 fn resolve<'a>(
2661 &'a self,
2662 key_info: Option<&KeyInfo>,
2663 _algorithm: SignatureAlgorithm,
2664 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
2665 {
2666 let sources = &key_info.expect("KeyInfo must be parsed").sources;
2667 assert!(matches!(
2668 sources.as_slice(),
2669 [
2670 super::super::parse::KeyInfoSource::KeyName(name),
2671 super::super::parse::KeyInfoSource::RetrievalMethod { .. },
2672 ] if name == "primary"
2673 ));
2674 Ok(Some(Box::new(AcceptingKey)))
2675 }
2676
2677 fn consumes_document_key_info(&self) -> bool {
2678 true
2679 }
2680 }
2681
2682 fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
2683 format!(
2684 r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2685 <ds:SignedInfo>
2686 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2687 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2688 <ds:Reference URI="{reference_uri}">
2689 {transforms_xml}
2690 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
2691 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
2692 </ds:Reference>
2693 </ds:SignedInfo>
2694 <ds:SignatureValue>AQ==</ds:SignatureValue>
2695</ds:Signature>"#
2696 )
2697 }
2698
2699 fn signature_with_target_reference(signature_value_b64: &str) -> String {
2700 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2701 <target ID="target">payload</target>
2702 <ds:Signature>
2703 <ds:SignedInfo>
2704 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2705 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2706 <ds:Reference URI="#target">
2707 <ds:Transforms>
2708 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2709 </ds:Transforms>
2710 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
2711 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
2712 </ds:Reference>
2713 </ds:SignedInfo>
2714 <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
2715 </ds:Signature>
2716</root>"##;
2717
2718 let doc = Document::parse(xml_template).unwrap();
2719 let sig_node = doc
2720 .descendants()
2721 .find(|node| node.is_element() && node.tag_name().name() == "Signature")
2722 .unwrap();
2723 let signed_info_node = sig_node
2724 .children()
2725 .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
2726 .unwrap();
2727 let signed_info = parse_signed_info(signed_info_node).unwrap();
2728 let reference = &signed_info.references[0];
2729 let resolver = UriReferenceResolver::new(&doc);
2730 let initial_data = resolver
2731 .dereference(reference.uri.as_deref().unwrap())
2732 .unwrap();
2733 let pre_digest =
2734 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
2735 .unwrap();
2736 let digest = compute_digest(reference.digest_method, &pre_digest);
2737 let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
2738 xml_template
2739 .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
2740 .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
2741 }
2742
2743 #[test]
2744 fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
2745 let xml = signature_with_target_reference("AQ==");
2746
2747 let result = VerifyContext::new()
2748 .verify(&xml)
2749 .expect("missing key config must be reported as verification status");
2750 assert!(
2751 matches!(
2752 result.status,
2753 DsigStatus::Invalid(FailureReason::KeyNotFound)
2754 ),
2755 "unexpected status: {:?}",
2756 result.status
2757 );
2758 }
2759
2760 #[test]
2761 fn verify_context_rejects_disallowed_uri() {
2762 let xml = minimal_signature_xml("http://example.com/external", "");
2763 let err = VerifyContext::new()
2764 .key(&RejectingKey)
2765 .verify(&xml)
2766 .expect_err("external URI should be rejected by default policy");
2767 assert!(matches!(
2768 err,
2769 SignatureVerificationPipelineError::DisallowedUri { .. }
2770 ));
2771 }
2772
2773 #[test]
2774 fn verify_context_bounds_effective_xml_base_components() {
2775 let mut xml = minimal_signature_xml("payload", "");
2778 for _ in 0..65 {
2779 xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
2780 }
2781 let resources = HashMap::new();
2782 let error = VerifyContext::new()
2783 .key(&AcceptingKey)
2784 .allowed_uri_types(UriTypeSet::ALL)
2785 .external_resources(&resources)
2786 .verify(&xml)
2787 .expect_err("XML Base component work must be bounded before lookup");
2788
2789 assert!(
2790 error.to_string().contains("XML Base resolution"),
2791 "unexpected error: {error:?}"
2792 );
2793 }
2794
2795 #[test]
2796 fn verify_context_bounds_cumulative_xml_base_resolution_bytes() {
2797 let mut xml = minimal_signature_xml("payload", "");
2800 for _ in 0..2 {
2801 xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
2802 }
2803 let resources = HashMap::new();
2804 let mut policy = crate::policy::VerificationPolicy::default();
2805 policy.resources.max_xml_base_resolution_bytes = 32;
2806 let error = VerifyContext::new()
2807 .policy(policy)
2808 .key(&AcceptingKey)
2809 .allowed_uri_types(UriTypeSet::ALL)
2810 .external_resources(&resources)
2811 .verify(&xml)
2812 .expect_err("cumulative XML Base copies must obey the operation budget");
2813
2814 assert!(matches!(
2815 error,
2816 SignatureVerificationPipelineError::Reference(
2817 ReferenceProcessingError::UriDereference(
2818 TransformError::XmlBaseResolutionTooLarge { max_bytes: 32, .. }
2819 )
2820 )
2821 ));
2822 }
2823
2824 #[test]
2825 fn verify_context_applies_xml_base_policy_to_signed_info_c14n() {
2826 let xml = signature_with_target_reference("AQ==")
2830 .replacen(
2831 "http://www.w3.org/2001/10/xml-exc-c14n#",
2832 "http://www.w3.org/2006/12/xml-c14n11",
2833 1,
2834 )
2835 .replace(
2836 " <ds:Signature>",
2837 " <outer xml:base=\"one/\"><inner xml:base=\"two/\"><ds:Signature>",
2838 )
2839 .replace(" </ds:Signature>", " </ds:Signature></inner></outer>");
2840 let policy = crate::policy::VerificationPolicy {
2841 resources: crate::policy::ResourcePolicy {
2842 max_xml_base_components: 1,
2843 ..crate::policy::ResourcePolicy::default()
2844 },
2845 ..crate::policy::VerificationPolicy::default()
2846 };
2847
2848 let error = VerifyContext::new()
2849 .key(&AcceptingKey)
2850 .policy(policy)
2851 .verify(&xml)
2852 .expect_err("SignedInfo C14N must use the operation XML Base budget");
2853
2854 assert!(matches!(
2855 error,
2856 SignatureVerificationPipelineError::Canonicalization(
2857 crate::c14n::C14nError::XmlBaseComponentsTooLarge { max: 1, actual: 2 }
2858 )
2859 ));
2860 }
2861
2862 #[test]
2863 fn verify_context_meters_repeated_external_dereferences() {
2864 let payload = b"payload";
2867 let digest = base64::engine::general_purpose::STANDARD.encode(
2868 crate::xmldsig::compute_digest(DigestAlgorithm::Sha1, payload),
2869 );
2870 let reference = format!(
2871 r#"<ds:Reference URI="urn:payload"><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference>"#
2872 );
2873 let xml = format!(
2874 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>{reference}{reference}</ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature>"#
2875 );
2876 let resources = HashMap::from([("urn:payload".to_owned(), payload.to_vec())]);
2877 let policy = crate::policy::VerificationPolicy {
2878 reference_uri_types: UriTypeSet::ALL,
2879 resources: crate::policy::ResourcePolicy {
2880 max_external_resource_bytes: payload.len(),
2881 max_external_resource_total_bytes: payload.len(),
2882 ..crate::policy::ResourcePolicy::default()
2883 },
2884 ..crate::policy::VerificationPolicy::default()
2885 };
2886
2887 let error = VerifyContext::new()
2888 .key(&AcceptingKey)
2889 .policy(policy)
2890 .external_resources(&resources)
2891 .verify(&xml)
2892 .expect_err("the second dereference must exhaust the aggregate byte ceiling");
2893
2894 assert!(
2895 error
2896 .to_string()
2897 .contains("aggregate external resource bytes")
2898 );
2899 }
2900
2901 #[test]
2902 fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
2903 let xml = minimal_signature_xml("", "");
2904 let err = VerifyContext::new()
2905 .key(&RejectingKey)
2906 .allowed_uri_types(UriTypeSet::new(false, true, false))
2907 .verify(&xml)
2908 .expect_err("empty URI must be rejected when empty references are disabled");
2909 assert!(matches!(
2910 err,
2911 SignatureVerificationPipelineError::DisallowedUri { ref uri } if uri.is_empty()
2912 ));
2913 }
2914
2915 #[test]
2916 fn verify_context_rejects_disallowed_transform() {
2917 let xml = minimal_signature_xml(
2918 "",
2919 r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
2920 );
2921 let err = VerifyContext::new()
2922 .key(&RejectingKey)
2923 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
2924 .verify(&xml)
2925 .expect_err("enveloped transform should be rejected by allowlist");
2926 assert!(matches!(
2927 err,
2928 SignatureVerificationPipelineError::DisallowedTransform { .. }
2929 ));
2930 }
2931
2932 #[test]
2933 fn verify_context_applies_transform_allowlist_to_signed_info_c14n() {
2934 let xml = signature_with_target_reference("AQ==").replacen(
2937 "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
2938 "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"/>",
2939 1,
2940 );
2941 let error = VerifyContext::new()
2942 .key(&AcceptingKey)
2943 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
2944 .verify(&xml)
2945 .expect_err("SignedInfo C14N must obey the operation transform allowlist");
2946
2947 assert!(matches!(
2948 error,
2949 SignatureVerificationPipelineError::DisallowedTransform { ref algorithm }
2950 if algorithm == "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
2951 ));
2952 }
2953
2954 #[test]
2955 fn verify_context_applies_transform_allowlist_to_key_retrieval() {
2956 let xml = signature_with_target_reference("AQ==")
2959 .replacen(
2960 "</ds:Signature>",
2961 r##"<ds:KeyInfo><ds:RetrievalMethod URI="#keys" Type="http://www.w3.org/2000/09/xmldsig#X509Data"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath></ds:Transform></ds:Transforms></ds:RetrievalMethod></ds:KeyInfo></ds:Signature>"##,
2962 1,
2963 )
2964 .replacen(
2965 "</root>",
2966 r#"<holder ID="keys"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder></root>"#,
2967 1,
2968 );
2969 let error = VerifyContext::new()
2970 .key_resolver(&ConsumingKeyInfoResolver)
2971 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
2972 .verify(&xml)
2973 .expect_err("RetrievalMethod XPath must obey the operation transform allowlist");
2974
2975 assert!(matches!(
2976 error,
2977 SignatureVerificationPipelineError::DisallowedTransform { ref algorithm }
2978 if algorithm == XPATH_TRANSFORM_URI
2979 ));
2980 }
2981
2982 fn signature_with_manifest_xml(valid_manifest_digest: bool) -> String {
2983 signature_with_manifest_xml_with_manifest_mutation(valid_manifest_digest, |xml| xml)
2984 }
2985
2986 fn signature_with_manifest_xml_with_manifest_mutation<F>(
2987 valid_manifest_digest: bool,
2988 mutate_manifest: F,
2989 ) -> String
2990 where
2991 F: FnOnce(String) -> String,
2992 {
2993 const TMP_SIGNED_INFO_DIGEST: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
2994 const INVALID_MANIFEST_DIGEST: &str = "//////////////////////////8=";
2995 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2996 <target ID="target">payload</target>
2997 <ds:Signature>
2998 <ds:SignedInfo>
2999 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3000 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3001 <ds:Reference URI="#manifest">
3002 <ds:Transforms>
3003 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3004 </ds:Transforms>
3005 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3006 <ds:DigestValue>SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER</ds:DigestValue>
3007 </ds:Reference>
3008 </ds:SignedInfo>
3009 <ds:SignatureValue>AQ==</ds:SignatureValue>
3010 <ds:Object>
3011 <ds:Manifest ID="manifest">
3012 <ds:Reference URI="#target">
3013 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3014 <ds:DigestValue>MANIFEST_DIGEST_PLACEHOLDER</ds:DigestValue>
3015 </ds:Reference>
3016 </ds:Manifest>
3017 </ds:Object>
3018 </ds:Signature>
3019</root>"##;
3020 let seed_xml = xml_template.replace(
3021 "SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER",
3022 TMP_SIGNED_INFO_DIGEST,
3023 );
3024 let doc = Document::parse(&seed_xml).unwrap();
3025 let signature_node = doc
3026 .descendants()
3027 .find(|node| {
3028 node.is_element()
3029 && node.tag_name().namespace() == Some(XMLDSIG_NS)
3030 && node.tag_name().name() == "Signature"
3031 })
3032 .unwrap();
3033 let resolver = UriReferenceResolver::new(&doc);
3034 let initial_data = resolver.dereference("#target").unwrap();
3035 let manifest_pre_digest =
3036 crate::xmldsig::execute_transforms(signature_node, initial_data, &[]).unwrap();
3037 let computed_manifest_digest_b64 = base64::engine::general_purpose::STANDARD
3038 .encode(compute_digest(DigestAlgorithm::Sha1, &manifest_pre_digest));
3039 let final_manifest_digest_b64 = if valid_manifest_digest {
3040 computed_manifest_digest_b64.as_str()
3041 } else {
3042 INVALID_MANIFEST_DIGEST
3043 };
3044 let xml_with_manifest_digest = mutate_manifest(
3045 seed_xml.replace("MANIFEST_DIGEST_PLACEHOLDER", final_manifest_digest_b64),
3046 );
3047 let signed_doc = Document::parse(&xml_with_manifest_digest).unwrap();
3048 let signed_signature_node = signed_doc
3049 .descendants()
3050 .find(|node| {
3051 node.is_element()
3052 && node.tag_name().namespace() == Some(XMLDSIG_NS)
3053 && node.tag_name().name() == "Signature"
3054 })
3055 .unwrap();
3056 let signed_info_node = signed_signature_node
3057 .children()
3058 .find(|node| {
3059 node.is_element()
3060 && node.tag_name().namespace() == Some(XMLDSIG_NS)
3061 && node.tag_name().name() == "SignedInfo"
3062 })
3063 .unwrap();
3064 let signed_info = parse_signed_info(signed_info_node).unwrap();
3065 let object_reference = &signed_info.references[0];
3066 let signed_resolver = UriReferenceResolver::new(&signed_doc);
3067 let signed_initial_data = signed_resolver
3068 .dereference(object_reference.uri.as_deref().unwrap())
3069 .unwrap();
3070 let signed_pre_digest = crate::xmldsig::execute_transforms(
3071 signed_signature_node,
3072 signed_initial_data,
3073 &object_reference.transforms,
3074 )
3075 .unwrap();
3076 let signed_digest_b64 = base64::engine::general_purpose::STANDARD.encode(compute_digest(
3077 object_reference.digest_method,
3078 &signed_pre_digest,
3079 ));
3080
3081 xml_with_manifest_digest.replacen(TMP_SIGNED_INFO_DIGEST, &signed_digest_b64, 1)
3082 }
3083
3084 fn replace_fixture_manifest_digest(xml: &str, replacement: &str) -> String {
3085 let object_marker = "<ds:Object>";
3086 let object_start = xml
3087 .find(object_marker)
3088 .expect("fixture should contain ds:Object")
3089 + object_marker.len();
3090 let open = "<ds:DigestValue>";
3091 let close = "</ds:DigestValue>";
3092 let value_start = xml[object_start..]
3093 .find(open)
3094 .map(|offset| object_start + offset + open.len())
3095 .expect("Manifest should contain DigestValue");
3096 let value_end = xml[value_start..]
3097 .find(close)
3098 .map(|offset| value_start + offset)
3099 .expect("Manifest DigestValue must be closed");
3100
3101 format!("{}{replacement}{}", &xml[..value_start], &xml[value_end..])
3102 }
3103
3104 #[test]
3105 fn verify_context_processes_manifest_references_when_enabled() {
3106 let xml = signature_with_manifest_xml(true);
3107
3108 let result_without_manifests = VerifyContext::new()
3109 .key(&RejectingKey)
3110 .verify(&xml)
3111 .expect("manifest processing disabled should still verify SignedInfo");
3112 assert!(
3113 result_without_manifests.manifest_references.is_empty(),
3114 "manifest results must stay empty when manifest processing is disabled",
3115 );
3116 assert!(matches!(
3117 result_without_manifests.status,
3118 DsigStatus::Invalid(FailureReason::SignatureMismatch)
3119 ));
3120
3121 let malformed_manifest_xml = signature_with_manifest_xml(true).replacen(
3122 "</ds:Object>",
3123 "</ds:Object><ds:Object><ds:Manifest><ds:Foo/></ds:Manifest></ds:Object>",
3124 1,
3125 );
3126 let malformed_with_manifests_disabled = VerifyContext::new()
3127 .key(&RejectingKey)
3128 .verify(&malformed_manifest_xml)
3129 .expect("malformed Manifest must be ignored when manifest processing is disabled");
3130 assert!(
3131 malformed_with_manifests_disabled
3132 .manifest_references
3133 .is_empty(),
3134 "manifest parser must not run when process_manifests is disabled",
3135 );
3136 assert!(matches!(
3137 malformed_with_manifests_disabled.status,
3138 DsigStatus::Invalid(FailureReason::SignatureMismatch)
3139 ));
3140
3141 let result_with_manifests = VerifyContext::new()
3142 .key(&AcceptingKey)
3143 .process_manifests(true)
3144 .verify(&xml)
3145 .expect("manifest references should be processed when enabled");
3146 assert_eq!(result_with_manifests.manifest_references.len(), 1);
3147 assert_eq!(
3148 result_with_manifests.manifest_references[0].reference_set,
3149 ReferenceSet::Manifest
3150 );
3151 assert_eq!(
3152 result_with_manifests.manifest_references[0].reference_index,
3153 0
3154 );
3155 assert!(matches!(
3156 result_with_manifests.manifest_references[0].status,
3157 DsigStatus::Valid
3158 ));
3159 assert!(matches!(result_with_manifests.status, DsigStatus::Valid));
3160 }
3161
3162 #[test]
3163 fn verify_context_skips_manifest_work_when_signature_value_is_invalid() {
3164 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3168 replace_fixture_manifest_digest(&xml, "!!!")
3169 });
3170 assert!(
3171 xml.split_once("<ds:Object>")
3172 .is_some_and(|(_, object)| object.contains("<ds:DigestValue>!!!</ds:DigestValue>")),
3173 "fixture mutation must corrupt the nested Manifest DigestValue",
3174 );
3175
3176 let result = VerifyContext::new()
3177 .key(&RejectingKey)
3178 .process_manifests(true)
3179 .verify(&xml)
3180 .expect("invalid SignatureValue must short-circuit Manifest parsing");
3181
3182 assert!(matches!(
3183 result.status,
3184 DsigStatus::Invalid(FailureReason::SignatureMismatch)
3185 ));
3186 assert!(result.manifest_references.is_empty());
3187 }
3188
3189 #[test]
3190 fn verify_context_shares_xpath_parse_budget_with_manifest_references() {
3191 let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
3194 .repeat(64);
3195 let transform = format!(
3196 r#"<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</ds:Transform>"#
3197 );
3198 let max_transforms = transform.repeat(16);
3199 let max_manifest_reference = format!(
3200 r##"<ds:Reference URI="#target"><ds:Transforms>{max_transforms}</ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue></ds:Reference>"##
3201 );
3202 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3203 xml.replacen(
3204 r##"<ds:Reference URI="#target">"##,
3205 &format!(
3206 r##"<ds:Reference URI="#target"><ds:Transforms>{}</ds:Transforms>"##,
3207 max_transforms
3208 ),
3209 1,
3210 )
3211 .replacen(
3212 "</ds:SignedInfo>",
3213 r##"<ds:Reference URI="#target"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>false()</ds:XPath></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</ds:DigestValue></ds:Reference></ds:SignedInfo>"##,
3214 1,
3215 )
3216 .replacen(
3217 "</ds:Manifest>",
3218 &format!("{}</ds:Manifest>", max_manifest_reference.repeat(3)),
3219 1,
3220 )
3221 });
3222
3223 let error = VerifyContext::new()
3224 .key(&AcceptingKey)
3225 .process_manifests(true)
3226 .verify(&xml)
3227 .expect_err("SignedInfo and Manifest References must share one XPath parse budget");
3228
3229 assert!(
3230 matches!(
3231 &error,
3232 SignatureVerificationPipelineError::ParseManifestReference(source)
3233 if source.to_string().contains("signature-wide XPath expression budget")
3234 ),
3235 "unexpected error: {error:?}"
3236 );
3237 }
3238
3239 #[test]
3240 fn verify_context_processes_manifest_when_signedinfo_references_object() {
3241 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3242 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
3243 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
3244 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
3245 });
3246
3247 let result = VerifyContext::new()
3248 .key(&AcceptingKey)
3249 .process_manifests(true)
3250 .verify(&xml)
3251 .expect("manifest references should be processed when SignedInfo references ds:Object");
3252 assert_eq!(
3253 result.manifest_references.len(),
3254 1,
3255 "signed ds:Object should enable processing of its direct-child ds:Manifest",
3256 );
3257 assert_eq!(
3258 result.manifest_references[0].reference_set,
3259 ReferenceSet::Manifest
3260 );
3261 assert_eq!(result.manifest_references[0].reference_index, 0);
3262 assert!(matches!(
3263 result.manifest_references[0].status,
3264 DsigStatus::Valid
3265 ));
3266 }
3267
3268 #[test]
3269 fn verify_context_skips_manifest_removed_by_enveloped_transform() {
3270 for target_object in [false, true] {
3274 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3275 let xml = xml.replacen(
3276 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
3277 r#"<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
3278 1,
3279 );
3280 if target_object {
3281 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
3282 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
3283 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
3284 } else {
3285 xml
3286 }
3287 });
3288
3289 let result = VerifyContext::new()
3290 .key(&AcceptingKey)
3291 .process_manifests(true)
3292 .store_pre_digest(true)
3293 .verify(&xml)
3294 .expect("an emptied reference remains a valid core digest input");
3295
3296 assert!(matches!(result.status, DsigStatus::Valid));
3297 assert_eq!(
3298 result.signed_info_references[0].pre_digest_data.as_deref(),
3299 Some([].as_slice()),
3300 "target_object={target_object} must have empty transformed bytes",
3301 );
3302 assert!(
3303 result.manifest_references.is_empty(),
3304 "target_object={target_object} must not authenticate the Manifest",
3305 );
3306 }
3307 }
3308
3309 #[test]
3310 fn verify_context_ignores_manifest_excluded_from_signed_object() {
3311 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3315 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
3316 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
3317 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
3318 .replacen(
3319 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
3320 r#"<ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>not(ancestor-or-self::ds:Manifest)</ds:XPath></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
3321 1,
3322 )
3323 });
3324
3325 let result = VerifyContext::new()
3326 .key(&AcceptingKey)
3327 .process_manifests(true)
3328 .verify(&xml)
3329 .expect("excluded Manifest content must be ignored, not parsed");
3330
3331 assert!(matches!(result.status, DsigStatus::Valid));
3332 assert!(
3333 result.manifest_references.is_empty(),
3334 "a transform-excluded Manifest is not authenticated by SignedInfo",
3335 );
3336 }
3337
3338 #[test]
3339 fn verify_context_skips_manifest_digest_work_when_signature_is_invalid() {
3340 let xml = signature_with_manifest_xml(false);
3341 let result = VerifyContext::new()
3342 .key(&RejectingKey)
3343 .process_manifests(true)
3344 .verify(&xml)
3345 .expect("invalid SignatureValue must short-circuit Manifest digest work");
3346 assert!(result.manifest_references.is_empty());
3347 assert!(matches!(
3348 result.status,
3349 DsigStatus::Invalid(FailureReason::SignatureMismatch)
3350 ));
3351 }
3352
3353 #[test]
3354 fn verify_context_manifest_digest_mismatch_is_non_fatal_with_accepting_key() {
3355 let xml = signature_with_manifest_xml(false);
3356 let result = VerifyContext::new()
3357 .key(&AcceptingKey)
3358 .process_manifests(true)
3359 .verify(&xml)
3360 .expect("manifest digest mismatches should be recorded while signature stays valid");
3361 assert_eq!(result.manifest_references.len(), 1);
3362 assert!(matches!(
3363 result.manifest_references[0].status,
3364 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
3365 ));
3366 assert!(matches!(result.status, DsigStatus::Valid));
3367 }
3368
3369 #[test]
3370 fn verify_context_skips_manifest_parsing_when_signedinfo_reference_fails() {
3371 let xml = signature_with_manifest_xml(true);
3374 let (signed_info_prefix, object_suffix) = xml
3375 .split_once("<ds:Object>")
3376 .expect("fixture should contain ds:Object");
3377 let open = "<ds:DigestValue>";
3378 let close = "</ds:DigestValue>";
3379 let digest_start = signed_info_prefix
3380 .find(open)
3381 .expect("SignedInfo should contain DigestValue");
3382 let digest_end = signed_info_prefix[digest_start + open.len()..]
3383 .find(close)
3384 .map(|offset| digest_start + open.len() + offset)
3385 .expect("SignedInfo DigestValue must be closed");
3386 let broken_signed_info_prefix = format!(
3387 "{}{}AAAAAAAAAAAAAAAAAAAAAAAAAAA={}{}",
3388 &signed_info_prefix[..digest_start],
3389 open,
3390 close,
3391 &signed_info_prefix[digest_end + close.len()..],
3392 );
3393 let broken_xml = format!("{broken_signed_info_prefix}<ds:Object>{object_suffix}");
3394 let result = VerifyContext::new()
3395 .key(&RejectingKey)
3396 .process_manifests(true)
3397 .verify(&broken_xml)
3398 .expect("SignedInfo digest failure should return without parsing Manifests");
3399 assert!(matches!(
3400 result.status,
3401 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
3402 ));
3403 assert!(
3404 result.manifest_references.is_empty(),
3405 "unauthenticated Manifest content must not be parsed",
3406 );
3407 }
3408
3409 #[test]
3410 fn verify_context_skips_manifest_policy_work_when_signature_is_invalid() {
3411 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3414 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
3415 });
3416 let result = VerifyContext::new()
3417 .key(&RejectingKey)
3418 .process_manifests(true)
3419 .verify(&broken_xml)
3420 .expect("invalid SignatureValue must short-circuit Manifest policy work");
3421 assert!(result.manifest_references.is_empty());
3422 assert!(matches!(
3423 result.status,
3424 DsigStatus::Invalid(FailureReason::SignatureMismatch)
3425 ));
3426 }
3427
3428 #[test]
3429 fn verify_context_records_manifest_policy_violations_with_accepting_key() {
3430 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3431 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
3432 });
3433 let result = VerifyContext::new()
3434 .key(&AcceptingKey)
3435 .process_manifests(true)
3436 .verify(&broken_xml)
3437 .expect("manifest policy violations should be recorded while signature stays valid");
3438 assert_eq!(result.manifest_references.len(), 1);
3439 assert!(matches!(
3440 result.manifest_references[0].status,
3441 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
3442 ));
3443 assert!(matches!(result.status, DsigStatus::Valid));
3444 }
3445
3446 #[test]
3447 fn verify_context_applies_digest_policy_to_manifest_references() {
3448 let policy = crate::policy::VerificationPolicy {
3451 process_manifests: true,
3452 digest_algorithms: Some(HashSet::from([DigestAlgorithm::Sha1])),
3453 ..crate::policy::VerificationPolicy::default()
3454 };
3455 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
3456 let legacy = "http://www.w3.org/2000/09/xmldsig#sha1";
3457 let offset = xml
3458 .rfind(legacy)
3459 .expect("Manifest DigestMethod must be present");
3460 xml.replace_range(offset..offset + legacy.len(), DigestAlgorithm::Sha256.uri());
3461 let value_start = xml[offset..]
3462 .find("<ds:DigestValue>")
3463 .map(|relative| offset + relative + "<ds:DigestValue>".len())
3464 .expect("Manifest DigestValue must be present");
3465 let value_end = xml[value_start..]
3466 .find("</ds:DigestValue>")
3467 .map(|relative| value_start + relative)
3468 .expect("Manifest DigestValue must be closed");
3469 xml.replace_range(
3470 value_start..value_end,
3471 &base64::engine::general_purpose::STANDARD.encode([0_u8; 32]),
3472 );
3473 xml
3474 });
3475 let result = VerifyContext::new()
3476 .key(&AcceptingKey)
3477 .policy(policy)
3478 .verify(&xml)
3479 .expect("a disallowed Manifest digest is a per-reference result");
3480
3481 assert!(matches!(result.status, DsigStatus::Valid));
3482 assert!(matches!(
3483 result.manifest_references[0].status,
3484 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
3485 ));
3486 }
3487
3488 #[test]
3489 fn verify_context_applies_transform_count_policy_to_manifest_references() {
3490 let policy = crate::policy::VerificationPolicy {
3493 process_manifests: true,
3494 resources: crate::policy::ResourcePolicy {
3495 max_transforms_per_reference: 1,
3496 ..crate::policy::ResourcePolicy::default()
3497 },
3498 ..crate::policy::VerificationPolicy::default()
3499 };
3500 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
3501 let manifest_start = xml
3502 .find("<ds:Manifest")
3503 .expect("fixture must contain a Manifest");
3504 let manifest = xml[manifest_start..].replacen(
3505 "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
3506 concat!(
3507 "<ds:Transforms>",
3508 "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
3509 "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
3510 "</ds:Transforms>",
3511 "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>"
3512 ),
3513 1,
3514 );
3515 xml.replace_range(manifest_start.., &manifest);
3516 xml
3517 });
3518 let result = VerifyContext::new()
3519 .key(&AcceptingKey)
3520 .policy(policy)
3521 .verify(&xml)
3522 .expect("Manifest transform policy is a per-reference result");
3523
3524 assert!(matches!(result.status, DsigStatus::Valid));
3525 assert!(matches!(
3526 result.manifest_references[0].status,
3527 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
3528 ));
3529 }
3530
3531 #[test]
3532 fn verify_context_skips_manifest_uri_work_when_signature_is_invalid() {
3533 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3536 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
3537 });
3538
3539 let result = VerifyContext::new()
3540 .key(&RejectingKey)
3541 .process_manifests(true)
3542 .verify(&broken_xml)
3543 .expect("invalid SignatureValue must short-circuit Manifest URI processing");
3544 assert!(result.manifest_references.is_empty());
3545 assert!(matches!(
3546 result.status,
3547 DsigStatus::Invalid(FailureReason::SignatureMismatch)
3548 ));
3549 }
3550
3551 #[test]
3552 fn verify_context_records_manifest_missing_uri_with_accepting_key() {
3553 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3554 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
3555 });
3556
3557 let result = VerifyContext::new()
3558 .key(&AcceptingKey)
3559 .process_manifests(true)
3560 .verify(&broken_xml)
3561 .expect("manifest missing URI should be recorded while signature stays valid");
3562 assert_eq!(result.manifest_references.len(), 1);
3563 assert_eq!(result.manifest_references[0].uri, "<omitted>");
3564 assert!(matches!(
3565 result.manifest_references[0].status,
3566 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
3567 ));
3568 assert!(matches!(result.status, DsigStatus::Valid));
3569 }
3570
3571 #[test]
3572 fn verify_context_ignores_nested_manifests_in_object() {
3573 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3576 xml.replacen(
3577 "<ds:Manifest ID=\"manifest\">",
3578 "<wrapper><ds:Manifest ID=\"manifest\">",
3579 1,
3580 )
3581 .replacen("</ds:Manifest>", "</ds:Manifest></wrapper>", 1)
3582 });
3583
3584 let result = VerifyContext::new()
3585 .key(&AcceptingKey)
3586 .process_manifests(true)
3587 .verify(&xml)
3588 .expect("nested Manifest nodes are ignored in strict mode");
3589 assert!(
3590 result.manifest_references.is_empty(),
3591 "only direct ds:Manifest children of ds:Object must be processed"
3592 );
3593 assert!(matches!(result.status, DsigStatus::Valid));
3594 }
3595
3596 #[test]
3597 fn verify_context_reports_manifest_reference_parse_errors_explicitly() {
3598 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3601 replace_fixture_manifest_digest(&xml, "!!!")
3602 });
3603
3604 let err = VerifyContext::new()
3605 .key(&AcceptingKey)
3606 .process_manifests(true)
3607 .verify(&broken_xml)
3608 .expect_err("invalid Manifest DigestValue must map to ParseManifestReference");
3609 assert!(matches!(
3610 err,
3611 SignatureVerificationPipelineError::ParseManifestReference(_)
3612 ));
3613 }
3614
3615 #[test]
3616 fn verify_context_reports_unsupported_manifest_transform_with_declared_digest() {
3617 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3620 let xml = xml.replacen(
3621 "<ds:Reference URI=\"#target\">",
3622 "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
3623 1,
3624 );
3625 let xml = xml.replacen(
3626 "</ds:Transforms>\n <ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
3627 "</ds:Transforms>\n <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>",
3628 1,
3629 );
3630 replace_fixture_manifest_digest(&xml, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
3631 });
3632 assert!(xml.contains("urn:unsupported"));
3633 assert!(xml.contains("http://www.w3.org/2001/04/xmlenc#sha256"));
3634
3635 let result = VerifyContext::new()
3636 .key(&AcceptingKey)
3637 .process_manifests(true)
3638 .verify(&xml)
3639 .expect("unsupported Manifest transform is a per-reference result");
3640 assert_eq!(result.status, DsigStatus::Valid);
3641 assert_eq!(result.manifest_references.len(), 1);
3642 assert_eq!(
3643 result.manifest_references[0].digest_algorithm,
3644 DigestAlgorithm::Sha256
3645 );
3646 assert!(matches!(
3647 result.manifest_references[0].status,
3648 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
3649 ));
3650 }
3651
3652 #[test]
3653 fn manifest_reference_limit_counts_unsupported_entries() {
3654 let references = (0..=MAX_REFERENCES_PER_SIGNATURE)
3655 .map(|index| {
3656 format!(
3657 r##"<ds:Reference URI="#target-{index}"><ds:Transforms><ds:Transform Algorithm="urn:unsupported"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue></ds:Reference>"##
3658 )
3659 })
3660 .collect::<String>();
3661 let xml = format!(
3662 r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:Object Id="signed"><ds:Manifest>{references}</ds:Manifest></ds:Object></ds:Signature>"#
3663 );
3664 let document = Document::parse(&xml).unwrap();
3665 let signature = document.root_element();
3666 let object = signature.children().find(|node| node.is_element()).unwrap();
3667 let authenticated = HashSet::from([object.id()]);
3668
3669 let error = match parse_manifest_references(
3670 signature,
3671 &authenticated,
3672 MAX_REFERENCES_PER_SIGNATURE,
3673 &mut XPathSignatureParseBudget::default(),
3674 ) {
3675 Ok(_) => panic!("unsupported references must consume the same aggregate limit"),
3676 Err(error) => error,
3677 };
3678 assert!(matches!(
3679 error,
3680 SignatureVerificationPipelineError::InvalidStructure {
3681 reason: "signed Manifests exceed the per-signature Reference limit"
3682 }
3683 ));
3684 }
3685
3686 #[test]
3687 fn manifest_reference_limit_includes_signed_info_references() {
3688 let xml = signature_with_manifest_xml(true);
3691 let reference_start = xml
3692 .find(r##"<ds:Reference URI="#manifest">"##)
3693 .expect("fixture SignedInfo must reference the Manifest");
3694 let reference_end = xml[reference_start..]
3695 .find("</ds:Reference>")
3696 .map(|offset| reference_start + offset + "</ds:Reference>".len())
3697 .expect("fixture SignedInfo Reference must be closed");
3698 let repeated = xml[reference_start..reference_end].repeat(MAX_REFERENCES_PER_SIGNATURE);
3699 let xml = format!(
3700 "{}{repeated}{}",
3701 &xml[..reference_start],
3702 &xml[reference_end..]
3703 );
3704
3705 let error = VerifyContext::new()
3706 .key(&AcceptingKey)
3707 .process_manifests(true)
3708 .verify(&xml)
3709 .expect_err("one Manifest Reference must exceed the exhausted signature-wide limit");
3710
3711 assert!(matches!(
3712 error,
3713 SignatureVerificationPipelineError::InvalidStructure {
3714 reason: "signed Manifests exceed the per-signature Reference limit"
3715 }
3716 ));
3717 }
3718
3719 #[test]
3720 fn configured_reference_limit_is_shared_with_manifests() {
3721 let policy = crate::policy::VerificationPolicy {
3724 process_manifests: true,
3725 resources: crate::policy::ResourcePolicy {
3726 max_references: 1,
3727 ..crate::policy::ResourcePolicy::default()
3728 },
3729 ..crate::policy::VerificationPolicy::default()
3730 };
3731
3732 let error = VerifyContext::new()
3733 .key(&AcceptingKey)
3734 .policy(policy)
3735 .verify(&signature_with_manifest_xml(true))
3736 .expect_err("Manifest must exceed the caller-selected aggregate limit");
3737 assert!(matches!(
3738 error,
3739 SignatureVerificationPipelineError::InvalidStructure {
3740 reason: "signed Manifests exceed the per-signature Reference limit"
3741 }
3742 ));
3743 }
3744
3745 #[test]
3746 fn retrieval_method_materializes_single_x509_data_subtree() {
3747 for uri in [
3748 "#target",
3749 "#xpointer(id('target'))",
3750 "#xpointer(id("target"))",
3751 ] {
3752 for target_xml in [
3753 r#"<ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>"#,
3754 r#"<holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>"#,
3755 ] {
3756 let xml = format!(
3757 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo><ds:RetrievalMethod URI="{uri}" Type="http://www.w3.org/2000/09/xmldsig#X509Data"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath></ds:Transform></ds:Transforms></ds:RetrievalMethod></ds:KeyInfo>{target_xml}</root>"#
3758 );
3759 let document = Document::parse(&xml).unwrap();
3760 let key_info_node = document
3761 .descendants()
3762 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
3763 .unwrap();
3764 let mut key_info = parse_key_info(key_info_node).unwrap();
3765 let resolver = UriReferenceResolver::new(&document);
3766
3767 materialize_retrieval_methods(
3768 &mut key_info,
3769 &resolver,
3770 UriTypeSet::SAME_DOCUMENT,
3771 None,
3772 crate::provider::default_provider(),
3773 )
3774 .expect("XPath filter must produce one X509Data-rooted node-set");
3775 assert!(matches!(
3776 key_info.sources.as_slice(),
3777 [super::super::parse::KeyInfoSource::X509Data(info)]
3778 if info.subject_names == ["CN=leaf"]
3779 ));
3780 }
3781 }
3782 }
3783
3784 #[test]
3785 fn retrieval_method_materializes_direct_untransformed_x509_data() {
3786 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3789 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
3790 <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
3791 </root>"##;
3792 let document = Document::parse(xml).unwrap();
3793 let key_info_node = document
3794 .descendants()
3795 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
3796 .unwrap();
3797 let mut key_info = parse_key_info(key_info_node).unwrap();
3798
3799 materialize_retrieval_methods(
3800 &mut key_info,
3801 &UriReferenceResolver::new(&document),
3802 UriTypeSet::SAME_DOCUMENT,
3803 None,
3804 crate::provider::default_provider(),
3805 )
3806 .expect("a direct X509Data target needs no transform");
3807 assert!(matches!(
3808 key_info.sources.as_slice(),
3809 [super::super::parse::KeyInfoSource::X509Data(info)]
3810 if info.subject_names == ["CN=leaf"]
3811 ));
3812 }
3813
3814 #[test]
3815 fn raw_x509_retrieval_method_uses_inherited_xml_base() {
3816 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
3819 let xml = format!(
3820 r#"<root xml:base="https://example.test/keys/nested/" xmlns:ds="{XMLDSIG_NS}">
3821 <ds:KeyInfo><ds:RetrievalMethod URI="../signer.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>
3822 </root>"#
3823 );
3824 let document = Document::parse(&xml).unwrap();
3825 let key_info_node = document
3826 .descendants()
3827 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
3828 .unwrap();
3829 let mut key_info = parse_key_info(key_info_node).unwrap();
3830 let certificate = include_bytes!(
3831 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
3832 )
3833 .to_vec();
3834 let resources = HashMap::from([(
3835 "https://example.test/keys/signer.der".to_string(),
3836 certificate,
3837 )]);
3838 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
3839
3840 materialize_retrieval_methods(
3841 &mut key_info,
3842 &resolver,
3843 UriTypeSet::ALL,
3844 None,
3845 crate::provider::default_provider(),
3846 )
3847 .expect("RetrievalMethod should resolve against inherited xml:base");
3848
3849 assert!(matches!(
3850 key_info.sources.as_slice(),
3851 [super::super::parse::KeyInfoSource::X509Data(info)]
3852 if info.certificates.len() == 1
3853 ));
3854 }
3855
3856 #[test]
3857 fn retrieval_method_requires_xpath_for_x509_data_below_uri_root() {
3858 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3861 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
3862 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
3863 </root>"##;
3864 let document = Document::parse(xml).unwrap();
3865 let key_info_node = document
3866 .descendants()
3867 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
3868 .unwrap();
3869 let mut key_info = parse_key_info(key_info_node).unwrap();
3870
3871 let error = materialize_retrieval_methods(
3872 &mut key_info,
3873 &UriReferenceResolver::new(&document),
3874 UriTypeSet::SAME_DOCUMENT,
3875 None,
3876 crate::provider::default_provider(),
3877 )
3878 .expect_err("a wrapper target requires an explicit selection transform");
3879 assert!(matches!(
3880 error,
3881 SignatureVerificationPipelineError::InvalidStructure {
3882 reason: "untransformed X509Data RetrievalMethod must target X509Data directly"
3883 }
3884 ));
3885 }
3886
3887 #[test]
3888 fn retrieval_method_rejects_target_inside_external_x509_data_ancestor() {
3889 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3892 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath></ds:Transform></ds:Transforms></ds:RetrievalMethod></ds:KeyInfo>
3893 <ds:X509Data><ds:X509SubjectName Id="target">CN=leaf</ds:X509SubjectName></ds:X509Data>
3894 </root>"##;
3895 let document = Document::parse(xml).unwrap();
3896 let key_info_node = document
3897 .descendants()
3898 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
3899 .unwrap();
3900 let mut key_info = parse_key_info(key_info_node).unwrap();
3901
3902 let error = materialize_retrieval_methods(
3903 &mut key_info,
3904 &UriReferenceResolver::new(&document),
3905 UriTypeSet::SAME_DOCUMENT,
3906 None,
3907 crate::provider::default_provider(),
3908 )
3909 .expect_err("filter output without an X509Data root must be rejected");
3910 assert!(matches!(
3911 error,
3912 SignatureVerificationPipelineError::InvalidStructure {
3913 reason: "X509Data RetrievalMethod selected no X509Data element"
3914 }
3915 ));
3916 }
3917
3918 #[test]
3919 fn retrieval_method_rejects_ambiguous_x509_data_relation() {
3920 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3922 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath></ds:Transform></ds:Transforms></ds:RetrievalMethod></ds:KeyInfo>
3923 <holder Id="target"><ds:X509Data/><ds:X509Data/></holder>
3924 </root>"##;
3925 let document = Document::parse(xml).unwrap();
3926 let key_info_node = document
3927 .descendants()
3928 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
3929 .unwrap();
3930 let mut key_info = parse_key_info(key_info_node).unwrap();
3931
3932 let error = materialize_retrieval_methods(
3933 &mut key_info,
3934 &UriReferenceResolver::new(&document),
3935 UriTypeSet::SAME_DOCUMENT,
3936 None,
3937 crate::provider::default_provider(),
3938 )
3939 .expect_err("multiple transformed X509Data roots must be rejected");
3940 assert!(matches!(
3941 error,
3942 SignatureVerificationPipelineError::InvalidStructure {
3943 reason: "X509Data RetrievalMethod selected multiple X509Data elements"
3944 }
3945 ));
3946 }
3947
3948 #[test]
3949 fn retrieval_method_materialization_preserves_key_info_order() {
3950 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3953 <ds:KeyInfo>
3954 <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath></ds:Transform></ds:Transforms></ds:RetrievalMethod>
3955 <ds:KeyName>fallback</ds:KeyName>
3956 </ds:KeyInfo>
3957 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
3958 </root>"##;
3959 let document = Document::parse(xml).unwrap();
3960 let key_info_node = document
3961 .descendants()
3962 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
3963 .unwrap();
3964 let mut key_info = parse_key_info(key_info_node).unwrap();
3965
3966 materialize_retrieval_methods(
3967 &mut key_info,
3968 &UriReferenceResolver::new(&document),
3969 UriTypeSet::SAME_DOCUMENT,
3970 None,
3971 crate::provider::default_provider(),
3972 )
3973 .unwrap();
3974 assert!(matches!(
3975 key_info.sources.as_slice(),
3976 [
3977 super::super::parse::KeyInfoSource::X509Data(_),
3978 super::super::parse::KeyInfoSource::KeyName(name)
3979 ] if name == "fallback"
3980 ));
3981 }
3982
3983 #[test]
3984 fn retrieval_method_materialization_bounds_repeated_sources() {
3985 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
3988 let certificate = include_bytes!(
3989 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
3990 )
3991 .to_vec();
3992 let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
3993 let mut key_info = KeyInfo {
3994 sources: (0..=64)
3995 .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
3996 uri: "urn:certificate".into(),
3997 resource_type: Some(RAW_X509_TYPE.into()),
3998 transforms: RetrievalMethodTransforms::None,
3999 })
4000 .collect(),
4001 };
4002 let document = Document::parse("<root/>").unwrap();
4003 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4004
4005 let error = materialize_retrieval_methods(
4006 &mut key_info,
4007 &resolver,
4008 UriTypeSet::ALL,
4009 None,
4010 crate::provider::default_provider(),
4011 )
4012 .expect_err("retrieval count must be bounded before materialization");
4013 assert!(matches!(
4014 error,
4015 SignatureVerificationPipelineError::InvalidStructure {
4016 reason: "KeyInfo contains too many RetrievalMethod elements"
4017 }
4018 ));
4019 }
4020
4021 #[test]
4022 fn retrieval_method_materialization_deduplicates_within_count_limit() {
4023 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
4026 let certificate = include_bytes!(
4027 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
4028 )
4029 .to_vec();
4030 let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
4031 let mut key_info = KeyInfo {
4032 sources: (0..MAX_RETRIEVAL_METHOD_COUNT)
4033 .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
4034 uri: "urn:certificate".into(),
4035 resource_type: Some(RAW_X509_TYPE.into()),
4036 transforms: RetrievalMethodTransforms::None,
4037 })
4038 .collect(),
4039 };
4040 let document = Document::parse("<root/>").unwrap();
4041 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4042
4043 materialize_retrieval_methods(
4044 &mut key_info,
4045 &resolver,
4046 UriTypeSet::ALL,
4047 None,
4048 crate::provider::default_provider(),
4049 )
4050 .unwrap();
4051 assert!(matches!(
4052 key_info.sources.as_slice(),
4053 [super::super::parse::KeyInfoSource::X509Data(info)]
4054 if info.certificates.len() == 1
4055 ));
4056 }
4057
4058 #[test]
4059 fn raw_x509_retrieval_rejects_empty_same_document_uri() {
4060 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
4063 let certificate = include_bytes!(
4064 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
4065 )
4066 .to_vec();
4067 let resources = HashMap::from([(String::new(), certificate)]);
4068 let mut key_info = KeyInfo {
4069 sources: vec![super::super::parse::KeyInfoSource::RetrievalMethod {
4070 uri: String::new(),
4071 resource_type: Some(RAW_X509_TYPE.into()),
4072 transforms: RetrievalMethodTransforms::None,
4073 }],
4074 };
4075 let document = Document::parse("<root/>").unwrap();
4076 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4077
4078 let error = materialize_retrieval_methods(
4079 &mut key_info,
4080 &resolver,
4081 UriTypeSet::ALL,
4082 None,
4083 crate::provider::default_provider(),
4084 )
4085 .expect_err("empty URI must retain same-document semantics");
4086 assert!(matches!(
4087 error,
4088 SignatureVerificationPipelineError::InvalidStructure {
4089 reason: "raw X509 RetrievalMethod requires an untransformed external URI"
4090 }
4091 ));
4092 }
4093
4094 #[test]
4095 fn verify_context_does_not_hide_malformed_digest_behind_unsupported_transform() {
4096 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4098 let xml = xml.replacen(
4099 "<ds:Reference URI=\"#target\">",
4100 "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
4101 1,
4102 );
4103 replace_fixture_manifest_digest(&xml, "!!!")
4104 });
4105
4106 let error = VerifyContext::new()
4107 .key(&AcceptingKey)
4108 .process_manifests(true)
4109 .verify(&broken_xml)
4110 .expect_err("malformed Manifest digest must not become a validity result");
4111 assert!(matches!(
4112 error,
4113 SignatureVerificationPipelineError::ParseManifestReference(_)
4114 ));
4115 }
4116
4117 #[test]
4118 fn verify_context_rejects_manifest_non_whitespace_mixed_content() {
4119 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4122 xml.replacen(
4123 "<ds:Manifest ID=\"manifest\">",
4124 "<ds:Manifest ID=\"manifest\">junk",
4125 1,
4126 )
4127 });
4128
4129 let err = VerifyContext::new()
4130 .key(&AcceptingKey)
4131 .process_manifests(true)
4132 .verify(&xml)
4133 .expect_err("Manifest mixed content must fail verification");
4134 assert!(matches!(
4135 err,
4136 SignatureVerificationPipelineError::InvalidStructure {
4137 reason: "Manifest contains non-whitespace mixed content"
4138 }
4139 ));
4140 }
4141
4142 #[test]
4143 fn verify_context_rejects_empty_manifest_children() {
4144 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4147 let (prefix, rest) = xml
4148 .split_once("<ds:Manifest ID=\"manifest\">")
4149 .expect("fixture should contain Manifest");
4150 let (_, suffix) = rest
4151 .split_once("</ds:Manifest>")
4152 .expect("fixture should contain closing Manifest");
4153 format!("{prefix}<ds:Manifest ID=\"manifest\"></ds:Manifest>{suffix}")
4154 });
4155
4156 let err = VerifyContext::new()
4157 .key(&AcceptingKey)
4158 .process_manifests(true)
4159 .verify(&xml)
4160 .expect_err("empty Manifest must fail verification");
4161 assert!(matches!(
4162 err,
4163 SignatureVerificationPipelineError::InvalidStructure {
4164 reason: "Manifest must contain at least one ds:Reference element child"
4165 }
4166 ));
4167 }
4168
4169 #[test]
4170 fn verify_context_ignores_unsigned_malformed_manifest_blocks() {
4171 let xml = signature_with_manifest_xml(true).replacen(
4172 "</ds:Object>",
4173 "</ds:Object><ds:Object><ds:Manifest>junk<ds:Foo/></ds:Manifest></ds:Object>",
4174 1,
4175 );
4176 let result = VerifyContext::new()
4177 .key(&AcceptingKey)
4178 .process_manifests(true)
4179 .verify(&xml)
4180 .expect("unsigned malformed Manifest must be ignored");
4181 assert_eq!(
4182 result.manifest_references.len(),
4183 1,
4184 "only signed Manifest references must be reported",
4185 );
4186 assert!(matches!(result.status, DsigStatus::Valid));
4187 }
4188
4189 #[test]
4190 fn verify_context_skips_ambiguous_manifest_id_blocks() {
4191 let xml = signature_with_manifest_xml(true).replacen(
4192 "</ds:Object>",
4193 "</ds:Object><ds:Object><ds:Manifest ID=\"manifest\">junk<ds:Foo/></ds:Manifest></ds:Object>",
4194 1,
4195 );
4196 let err = VerifyContext::new()
4197 .key(&RejectingKey)
4198 .process_manifests(true)
4199 .verify(&xml)
4200 .expect_err("ambiguous manifest IDs should make SignedInfo #manifest dereference fail");
4201 assert!(matches!(
4202 err,
4203 SignatureVerificationPipelineError::Reference(
4204 ReferenceProcessingError::UriDereference(
4205 crate::xmldsig::types::TransformError::ElementNotFound(id)
4206 )
4207 ) if id == "manifest"
4208 ));
4209 }
4210
4211 #[test]
4212 fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
4213 let xml = minimal_signature_xml("", "");
4214 let err = VerifyContext::new()
4215 .key(&RejectingKey)
4216 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
4217 .verify(&xml)
4218 .expect_err("implicit default C14N must be checked against allowlist");
4219 assert!(matches!(
4220 err,
4221 SignatureVerificationPipelineError::DisallowedTransform { .. }
4222 ));
4223 }
4224
4225 #[test]
4226 fn verify_context_skips_resolver_when_reference_processing_fails() {
4227 let xml = minimal_signature_xml("", "");
4228 let result = VerifyContext::new()
4229 .key_resolver(&PanicResolver)
4230 .verify(&xml)
4231 .expect("reference digest mismatch should short-circuit before resolver");
4232 assert!(matches!(
4233 result.status,
4234 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
4235 ));
4236 }
4237
4238 #[test]
4239 fn verify_context_reports_key_not_found_when_resolver_misses() {
4240 let xml = signature_with_target_reference("AQ==");
4241 let result = VerifyContext::new()
4242 .key_resolver(&MissingKeyResolver)
4243 .verify(&xml)
4244 .expect("resolver miss should report status, not pipeline error");
4245 assert!(matches!(
4246 result.status,
4247 DsigStatus::Invalid(FailureReason::KeyNotFound)
4248 ));
4249 assert_eq!(
4250 result.signed_info_references.len(),
4251 1,
4252 "KeyNotFound path must preserve SignedInfo reference diagnostics",
4253 );
4254 assert!(matches!(
4255 result.signed_info_references[0].status,
4256 DsigStatus::Valid
4257 ));
4258 }
4259
4260 #[test]
4261 fn verify_context_resolver_can_ignore_malformed_keyinfo_by_default() {
4262 let base_xml = signature_with_target_reference("AQ==");
4263 let xml = base_xml
4264 .replace(
4265 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
4266 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
4267 )
4268 .replace(
4269 "</ds:SignatureValue>\n </ds:Signature>",
4270 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
4271 );
4272
4273 let result = VerifyContext::new()
4274 .key_resolver(&MissingKeyResolver)
4275 .verify(&xml)
4276 .expect("resolver path should not hard-fail on advisory malformed KeyInfo by default");
4277 assert!(matches!(
4278 result.status,
4279 DsigStatus::Invalid(FailureReason::KeyNotFound)
4280 ));
4281 }
4282
4283 #[test]
4284 fn verify_context_resolver_can_opt_in_to_keyinfo_parse_failures() {
4285 let base_xml = signature_with_target_reference("AQ==");
4286 let xml = base_xml
4287 .replace(
4288 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
4289 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
4290 )
4291 .replace(
4292 "</ds:SignatureValue>\n </ds:Signature>",
4293 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
4294 );
4295
4296 let err = VerifyContext::new()
4297 .key_resolver(&ConsumingKeyInfoResolver)
4298 .verify(&xml)
4299 .expect_err("resolver opted into KeyInfo parsing, malformed KeyInfo must fail");
4300 assert!(matches!(
4301 err,
4302 SignatureVerificationPipelineError::ParseKeyInfo(_)
4303 ));
4304 }
4305
4306 #[test]
4307 fn verify_context_ignores_unsupported_retrieval_before_valid_key_source() {
4308 let xml = signature_with_target_reference("AQ==").replace(
4311 "</ds:SignatureValue>\n </ds:Signature>",
4312 r##"</ds:SignatureValue>
4313 <ds:KeyInfo>
4314 <ds:RetrievalMethod URI="#vendor" Type="urn:vendor:key">
4315 <ds:Transforms><ds:Transform Algorithm="urn:vendor:transform"/></ds:Transforms>
4316 </ds:RetrievalMethod>
4317 <ds:KeyName>fallback</ds:KeyName>
4318 </ds:KeyInfo>
4319 </ds:Signature>"##,
4320 );
4321
4322 let result = VerifyContext::new()
4323 .key_resolver(&FallbackKeyInfoResolver)
4324 .verify(&xml)
4325 .expect("unsupported advisory retrieval must not abort key resolution");
4326 assert_eq!(result.status, DsigStatus::Valid);
4327 }
4328
4329 #[test]
4330 fn verify_context_does_not_eagerly_fail_unused_retrieval_fallback() {
4331 let xml = signature_with_target_reference("AQ==").replace(
4334 "</ds:SignatureValue>\n </ds:Signature>",
4335 r#"</ds:SignatureValue>
4336 <ds:KeyInfo>
4337 <ds:KeyName>primary</ds:KeyName>
4338 <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
4339 </ds:KeyInfo>
4340 </ds:Signature>"#,
4341 );
4342
4343 let result = VerifyContext::new()
4344 .key_resolver(&EarlyKeyInfoResolver)
4345 .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
4346 .verify(&xml)
4347 .expect("an unused missing retrieval fallback must not abort verification");
4348
4349 assert_eq!(result.status, DsigStatus::Valid);
4350 }
4351
4352 #[test]
4353 fn verify_context_does_not_eagerly_parse_unused_retrieval_fallback() {
4354 let xml = signature_with_target_reference("AQ==").replace(
4357 "</ds:SignatureValue>\n </ds:Signature>",
4358 r#"</ds:SignatureValue>
4359 <ds:KeyInfo>
4360 <ds:KeyName>primary</ds:KeyName>
4361 <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
4362 </ds:KeyInfo>
4363 </ds:Signature>"#,
4364 );
4365 let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
4366
4367 let result = VerifyContext::new()
4368 .key_resolver(&EarlyKeyInfoResolver)
4369 .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
4370 .external_resources(&resources)
4371 .verify(&xml)
4372 .expect("an unused malformed retrieval fallback must not abort verification");
4373
4374 assert_eq!(result.status, DsigStatus::Valid);
4375 }
4376
4377 #[test]
4378 fn verify_context_reports_missing_retrieval_when_no_key_source_resolves() {
4379 let xml = signature_with_target_reference("AQ==").replace(
4382 "</ds:SignatureValue>\n </ds:Signature>",
4383 r#"</ds:SignatureValue>
4384 <ds:KeyInfo>
4385 <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
4386 </ds:KeyInfo>
4387 </ds:Signature>"#,
4388 );
4389
4390 let error = VerifyContext::new()
4391 .key_resolver(&ConsumingKeyInfoResolver)
4392 .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
4393 .verify(&xml)
4394 .expect_err("a missing sole RetrievalMethod must remain an explicit error");
4395
4396 assert!(matches!(
4397 error,
4398 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
4399 crate::xmldsig::TransformError::UnsupportedUri(uri)
4400 )) if uri == "missing.der"
4401 ));
4402 }
4403
4404 #[test]
4405 fn verify_context_reports_malformed_retrieval_when_no_key_source_resolves() {
4406 let xml = signature_with_target_reference("AQ==").replace(
4409 "</ds:SignatureValue>\n </ds:Signature>",
4410 r#"</ds:SignatureValue>
4411 <ds:KeyInfo>
4412 <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
4413 </ds:KeyInfo>
4414 </ds:Signature>"#,
4415 );
4416 let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
4417
4418 let error = VerifyContext::new()
4419 .key_resolver(&ConsumingKeyInfoResolver)
4420 .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
4421 .external_resources(&resources)
4422 .verify(&xml)
4423 .expect_err("a malformed sole RetrievalMethod must remain a parse error");
4424
4425 assert!(matches!(
4426 error,
4427 SignatureVerificationPipelineError::ParseKeyInfo(_)
4428 ));
4429 }
4430
4431 #[test]
4432 fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
4433 let xml = signature_with_target_reference("@@@");
4434
4435 let err = VerifyContext::new()
4436 .key_resolver(&MissingKeyResolver)
4437 .verify(&xml)
4438 .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
4439 assert!(matches!(
4440 err,
4441 SignatureVerificationPipelineError::SignatureValueBase64(_)
4442 ));
4443 }
4444
4445 #[test]
4446 fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
4447 let xml = signature_with_target_reference("@@@");
4448
4449 let err = VerifyContext::new()
4450 .verify(&xml)
4451 .expect_err("invalid SignatureValue must remain a decode error");
4452 assert!(matches!(
4453 err,
4454 SignatureVerificationPipelineError::SignatureValueBase64(_)
4455 ));
4456 }
4457
4458 #[test]
4459 fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
4460 let references = vec![Reference {
4461 uri: None,
4462 id: None,
4463 ref_type: None,
4464 transforms: vec![],
4465 digest_method: DigestAlgorithm::Sha256,
4466 digest_value: vec![0; 32],
4467 }];
4468 let uri_types = UriTypeSet {
4469 allow_empty: false,
4470 allow_same_document: true,
4471 allow_external: false,
4472 };
4473
4474 let err = enforce_reference_policies(&references, uri_types, None)
4475 .expect_err("missing URI must fail before allow_empty policy is evaluated");
4476 assert!(matches!(
4477 err,
4478 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
4479 ));
4480 }
4481
4482 #[test]
4483 fn enforce_reference_policies_checks_only_terminal_binary_output() {
4484 let c14n = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI).unwrap();
4485 let allowed = HashSet::from([
4486 BASE64_TRANSFORM_URI.to_owned(),
4487 DEFAULT_IMPLICIT_C14N_URI.to_owned(),
4488 ]);
4489 let without_implicit_c14n = HashSet::from([BASE64_TRANSFORM_URI.to_owned()]);
4490
4491 for transforms in [
4492 vec![Transform::Base64Decode, Transform::C14n(c14n)],
4493 vec![Transform::Base64Decode, Transform::Base64Decode],
4494 ] {
4495 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, vec![0; 32]);
4496 enforce_reference_policies(
4497 std::slice::from_ref(&reference),
4498 UriTypeSet::default(),
4499 Some(&allowed),
4500 )
4501 .expect("terminal binary output must not require implicit C14N");
4502 }
4503
4504 let terminal_base64 = make_reference(
4505 "",
4506 vec![Transform::Base64Decode, Transform::Base64Decode],
4507 DigestAlgorithm::Sha256,
4508 vec![0; 32],
4509 );
4510 enforce_reference_policies(
4511 std::slice::from_ref(&terminal_base64),
4512 UriTypeSet::default(),
4513 Some(&without_implicit_c14n),
4514 )
4515 .expect("terminal Base64 output must not require implicit C14N");
4516
4517 let no_transforms = make_reference("", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
4518 let error = enforce_reference_policies(
4519 std::slice::from_ref(&no_transforms),
4520 UriTypeSet::default(),
4521 Some(&without_implicit_c14n),
4522 )
4523 .expect_err("a node-set result must require allowlisted implicit C14N");
4524 assert!(matches!(
4525 error,
4526 SignatureVerificationPipelineError::DisallowedTransform { ref algorithm }
4527 if algorithm == DEFAULT_IMPLICIT_C14N_URI
4528 ));
4529
4530 let detached = make_reference("urn:payload", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
4531 enforce_reference_policies(
4532 std::slice::from_ref(&detached),
4533 UriTypeSet::ALL,
4534 Some(&without_implicit_c14n),
4535 )
4536 .expect("external octets without transforms must not require implicit C14N");
4537
4538 let external_xpath = make_reference(
4539 "urn:payload",
4540 vec![Transform::XPath(
4541 super::super::transforms::XPathExpression::new("true()"),
4542 )],
4543 DigestAlgorithm::Sha256,
4544 vec![0; 32],
4545 );
4546 let error = enforce_reference_policies(
4547 std::slice::from_ref(&external_xpath),
4548 UriTypeSet::ALL,
4549 Some(&HashSet::from([XPATH_TRANSFORM_URI.to_owned()])),
4550 )
4551 .expect_err("external XML converted to a node-set must require implicit C14N");
4552 assert!(matches!(
4553 error,
4554 SignatureVerificationPipelineError::DisallowedTransform { ref algorithm }
4555 if algorithm == DEFAULT_IMPLICIT_C14N_URI
4556 ));
4557 }
4558
4559 #[test]
4560 fn stored_pre_digest_budget_counts_repeated_external_references() {
4561 let document =
4564 Document::parse("<ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"/>")
4565 .unwrap();
4566 let payload = vec![b'x'; 7];
4567 let digest = compute_digest(DigestAlgorithm::Sha256, &payload);
4568 let references = (0..5)
4569 .map(|_| {
4570 make_reference(
4571 "urn:repeated",
4572 Vec::new(),
4573 DigestAlgorithm::Sha256,
4574 digest.clone(),
4575 )
4576 })
4577 .collect::<Vec<_>>();
4578 let resources = HashMap::from([("urn:repeated".to_owned(), payload)]);
4579 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4580 let transform_budget = TransformExecutionBudget::default();
4581 let canonicalized_data_budget = CanonicalizedDataBudget::with_limit(32);
4582 let execution = ReferenceExecutionContext {
4583 store_pre_digest: true,
4584 transform_options: TransformOptions::default(),
4585 transform_budget: &transform_budget,
4586 canonicalized_data_budget: &canonicalized_data_budget,
4587 provider: crate::provider::default_provider(),
4588 };
4589
4590 let error = process_all_references_with_options(
4591 &references,
4592 &resolver,
4593 document.root_element(),
4594 &execution,
4595 )
4596 .expect_err(
4597 "retained diagnostics must not multiply one external allocation past the aggregate cap",
4598 );
4599 assert!(matches!(
4600 error,
4601 ReferenceProcessingError::CanonicalizedDataTooLarge { max_bytes: 32 }
4602 ));
4603 }
4604
4605 #[test]
4606 fn canonical_signed_info_obeys_policy_without_diagnostic_retention() {
4607 let xml = signature_with_target_reference("AQ==");
4611 let marker = "<ds:SignatureMethod";
4612 let padding = " ".repeat(1_025);
4613 let xml = xml.replacen(marker, &format!("{padding}{marker}"), 1);
4614 let policy = crate::policy::VerificationPolicy {
4615 resources: crate::policy::ResourcePolicy {
4616 max_canonicalized_bytes: 1_024,
4617 ..crate::policy::ResourcePolicy::default()
4618 },
4619 ..crate::policy::VerificationPolicy::default()
4620 };
4621
4622 let error = VerifyContext::new()
4623 .key(&AcceptingKey)
4624 .policy(policy)
4625 .verify(&xml)
4626 .expect_err("canonicalized SignedInfo must remain policy-bounded");
4627
4628 assert!(matches!(
4629 error,
4630 SignatureVerificationPipelineError::Reference(
4631 ReferenceProcessingError::CanonicalizedDataTooLarge { .. }
4632 )
4633 ));
4634 }
4635
4636 #[test]
4637 fn push_normalized_signature_text_rejects_form_feed() {
4638 let mut normalized = Vec::new();
4639 let mut raw_text_len = 0usize;
4640 let err =
4641 push_normalized_signature_text("ab\u{000C}cd", &mut raw_text_len, &mut normalized)
4642 .expect_err("form-feed must not be treated as XML base64 whitespace");
4643 assert!(matches!(
4644 err,
4645 SignatureVerificationPipelineError::SignatureValueBase64(
4646 base64::DecodeError::InvalidByte(_, 0x0C)
4647 )
4648 ));
4649 }
4650
4651 #[test]
4652 fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
4653 let mut normalized = vec![b'A'; MAX_SIGNATURE_VALUE_LEN - 1];
4654 let mut raw_text_len = normalized.len();
4655 let err = push_normalized_signature_text("é", &mut raw_text_len, &mut normalized)
4656 .expect_err("multibyte characters must not bypass byte-size limit");
4657 assert!(matches!(
4658 err,
4659 SignatureVerificationPipelineError::InvalidStructure {
4660 reason: "SignatureValue exceeds maximum allowed length"
4661 }
4662 ));
4663 }
4664
4665 #[test]
4668 fn reference_with_correct_digest_passes() {
4669 let xml = r##"<root>
4672 <data>hello world</data>
4673 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
4674 <ds:SignedInfo/>
4675 </ds:Signature>
4676 </root>"##;
4677 let doc = Document::parse(xml).unwrap();
4678 let resolver = UriReferenceResolver::new(&doc);
4679 let sig_node = doc
4680 .descendants()
4681 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
4682 .unwrap();
4683
4684 let initial_data = resolver.dereference("").unwrap();
4686 let transforms = vec![
4687 Transform::Enveloped,
4688 Transform::C14n(
4689 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
4690 .unwrap(),
4691 ),
4692 ];
4693 let pre_digest_bytes =
4694 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
4695 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
4696
4697 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
4699
4700 let result = process_reference(
4701 &reference,
4702 &resolver,
4703 sig_node,
4704 ReferenceSet::SignedInfo,
4705 0,
4706 false,
4707 )
4708 .unwrap();
4709 assert!(
4710 matches!(result.status, DsigStatus::Valid),
4711 "digest should match"
4712 );
4713 assert!(result.pre_digest_data.is_none());
4714 }
4715
4716 #[test]
4717 fn reference_with_wrong_digest_fails() {
4718 let xml = r##"<root>
4719 <data>hello</data>
4720 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4721 <ds:SignedInfo/>
4722 </ds:Signature>
4723 </root>"##;
4724 let doc = Document::parse(xml).unwrap();
4725 let resolver = UriReferenceResolver::new(&doc);
4726 let sig_node = doc
4727 .descendants()
4728 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
4729 .unwrap();
4730
4731 let transforms = vec![Transform::Enveloped];
4732 let wrong_digest = vec![0u8; 32];
4734 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
4735
4736 let result = process_reference(
4737 &reference,
4738 &resolver,
4739 sig_node,
4740 ReferenceSet::SignedInfo,
4741 0,
4742 false,
4743 )
4744 .unwrap();
4745 assert!(matches!(
4746 result.status,
4747 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
4748 ));
4749 }
4750
4751 #[test]
4752 fn reference_with_wrong_digest_preserves_supplied_ref_index() {
4753 let xml = r##"<root>
4754 <data>hello</data>
4755 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4756 <ds:SignedInfo/>
4757 </ds:Signature>
4758 </root>"##;
4759 let doc = Document::parse(xml).unwrap();
4760 let resolver = UriReferenceResolver::new(&doc);
4761 let sig_node = doc
4762 .descendants()
4763 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
4764 .unwrap();
4765
4766 let reference = make_reference(
4767 "",
4768 vec![Transform::Enveloped],
4769 DigestAlgorithm::Sha256,
4770 vec![0u8; 32],
4771 );
4772 let result = process_reference(
4773 &reference,
4774 &resolver,
4775 sig_node,
4776 ReferenceSet::SignedInfo,
4777 7,
4778 false,
4779 )
4780 .unwrap();
4781 assert!(matches!(
4782 result.status,
4783 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
4784 ));
4785 }
4786
4787 #[test]
4788 fn reference_stores_pre_digest_data() {
4789 let xml = "<root><child>text</child></root>";
4790 let doc = Document::parse(xml).unwrap();
4791 let resolver = UriReferenceResolver::new(&doc);
4792
4793 let initial_data = resolver.dereference("").unwrap();
4795 let pre_digest =
4796 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
4797 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
4798
4799 let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
4800 let result = process_reference(
4801 &reference,
4802 &resolver,
4803 doc.root_element(),
4804 ReferenceSet::SignedInfo,
4805 0,
4806 true,
4807 )
4808 .unwrap();
4809
4810 assert!(matches!(result.status, DsigStatus::Valid));
4811 assert!(result.pre_digest_data.is_some());
4812 assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
4813 }
4814
4815 #[test]
4818 fn reference_with_id_uri() {
4819 let xml = r##"<root>
4820 <item ID="target">specific content</item>
4821 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4822 <ds:SignedInfo/>
4823 </ds:Signature>
4824 </root>"##;
4825 let doc = Document::parse(xml).unwrap();
4826 let resolver = UriReferenceResolver::new(&doc);
4827 let sig_node = doc
4828 .descendants()
4829 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
4830 .unwrap();
4831
4832 let initial_data = resolver.dereference("#target").unwrap();
4834 let transforms = vec![Transform::C14n(
4835 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
4836 .unwrap(),
4837 )];
4838 let pre_digest =
4839 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
4840 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
4841
4842 let reference = make_reference(
4843 "#target",
4844 transforms,
4845 DigestAlgorithm::Sha256,
4846 expected_digest,
4847 );
4848 let result = process_reference(
4849 &reference,
4850 &resolver,
4851 sig_node,
4852 ReferenceSet::SignedInfo,
4853 0,
4854 false,
4855 )
4856 .unwrap();
4857 assert!(matches!(result.status, DsigStatus::Valid));
4858 }
4859
4860 #[test]
4861 fn reference_with_nonexistent_id_fails() {
4862 let xml = "<root><child/></root>";
4863 let doc = Document::parse(xml).unwrap();
4864 let resolver = UriReferenceResolver::new(&doc);
4865
4866 let reference =
4867 make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
4868 let result = process_reference(
4869 &reference,
4870 &resolver,
4871 doc.root_element(),
4872 ReferenceSet::SignedInfo,
4873 0,
4874 false,
4875 );
4876 assert!(result.is_err());
4877 }
4878
4879 #[test]
4880 fn reference_with_absent_uri_fails_closed() {
4881 let xml = "<root><child>text</child></root>";
4882 let doc = Document::parse(xml).unwrap();
4883 let resolver = UriReferenceResolver::new(&doc);
4884
4885 let reference = Reference {
4886 uri: None, id: None,
4888 ref_type: None,
4889 transforms: vec![],
4890 digest_method: DigestAlgorithm::Sha256,
4891 digest_value: vec![0; 32],
4892 };
4893
4894 let result = process_reference(
4895 &reference,
4896 &resolver,
4897 doc.root_element(),
4898 ReferenceSet::SignedInfo,
4899 0,
4900 false,
4901 );
4902 assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
4903 }
4904
4905 #[test]
4908 fn all_references_pass() {
4909 let xml = "<root><child>text</child></root>";
4910 let doc = Document::parse(xml).unwrap();
4911 let resolver = UriReferenceResolver::new(&doc);
4912
4913 let initial_data = resolver.dereference("").unwrap();
4915 let pre_digest =
4916 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
4917 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
4918
4919 let refs = vec![
4920 make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
4921 make_reference("", vec![], DigestAlgorithm::Sha256, digest),
4922 ];
4923
4924 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
4925 assert!(result.all_valid());
4926 assert_eq!(result.results.len(), 2);
4927 assert!(result.first_failure.is_none());
4928 }
4929
4930 #[test]
4931 fn reference_processing_shares_xpath_work_across_references() {
4932 let document = Document::parse("<root/>").unwrap();
4935 let resolver = UriReferenceResolver::new(&document);
4936 let transform = Transform::XPath(super::super::transforms::XPathExpression::new("true()"));
4937 let initial_data = resolver.dereference("").unwrap();
4938 let pre_digest = crate::xmldsig::execute_transforms(
4939 document.root_element(),
4940 initial_data,
4941 std::slice::from_ref(&transform),
4942 )
4943 .unwrap();
4944 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
4945 let references = vec![
4946 make_reference(
4947 "",
4948 vec![transform.clone()],
4949 DigestAlgorithm::Sha256,
4950 digest.clone(),
4951 ),
4952 make_reference("", vec![transform], DigestAlgorithm::Sha256, digest),
4953 ];
4954 let budget = TransformExecutionBudget::with_xpath_limit(12);
4955 let canonicalized_data_budget = CanonicalizedDataBudget::default();
4956 let execution = ReferenceExecutionContext {
4957 store_pre_digest: false,
4958 transform_options: TransformOptions::default(),
4959 transform_budget: &budget,
4960 canonicalized_data_budget: &canonicalized_data_budget,
4961 provider: crate::provider::default_provider(),
4962 };
4963
4964 let error = process_all_references_with_options(
4965 &references,
4966 &resolver,
4967 document.root_element(),
4968 &execution,
4969 )
4970 .expect_err("the second Reference must consume the first Reference's XPath work");
4971
4972 assert!(error.to_string().contains("signature-wide"));
4973 }
4974
4975 #[test]
4976 fn reference_processing_shares_node_set_materialization_across_references() {
4977 let document = Document::parse(
4981 r#"<root xmlns:n="urn:0123456789"><target Id="selected">payload</target></root>"#,
4982 )
4983 .unwrap();
4984 let resolver = UriReferenceResolver::new(&document);
4985 let initial_data = resolver.dereference("#selected").unwrap();
4986 let pre_digest =
4987 crate::xmldsig::execute_transforms(document.root_element(), initial_data, &[]).unwrap();
4988 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
4989 let references = vec![
4990 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest.clone()),
4991 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest),
4992 ];
4993 let budget = TransformExecutionBudget::with_node_set_materialization_limit(30);
4994 let canonicalized_data_budget = CanonicalizedDataBudget::default();
4995 let execution = ReferenceExecutionContext {
4996 store_pre_digest: false,
4997 transform_options: TransformOptions::default(),
4998 transform_budget: &budget,
4999 canonicalized_data_budget: &canonicalized_data_budget,
5000 provider: crate::provider::default_provider(),
5001 };
5002
5003 let error = process_all_references_with_options(
5004 &references,
5005 &resolver,
5006 document.root_element(),
5007 &execution,
5008 )
5009 .expect_err("the second Reference must consume the first Reference's materialization work");
5010
5011 assert!(error.to_string().contains("cumulative owned string bytes"));
5012 }
5013
5014 #[test]
5015 fn fail_fast_on_first_mismatch() {
5016 let xml = "<root><child>text</child></root>";
5017 let doc = Document::parse(xml).unwrap();
5018 let resolver = UriReferenceResolver::new(&doc);
5019
5020 let wrong_digest = vec![0u8; 32];
5021 let refs = vec![
5022 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
5023 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
5025 ];
5026
5027 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
5028 assert!(!result.all_valid());
5029 assert_eq!(result.first_failure, Some(0));
5030 assert_eq!(result.results.len(), 1);
5032 assert!(matches!(
5033 result.results[0].status,
5034 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
5035 ));
5036 }
5037
5038 #[test]
5039 fn fail_fast_second_reference() {
5040 let xml = "<root><child>text</child></root>";
5041 let doc = Document::parse(xml).unwrap();
5042 let resolver = UriReferenceResolver::new(&doc);
5043
5044 let initial_data = resolver.dereference("").unwrap();
5046 let pre_digest =
5047 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
5048 let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
5049 let wrong_digest = vec![0u8; 32];
5050
5051 let refs = vec![
5052 make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
5053 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
5054 ];
5055
5056 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
5057 assert!(!result.all_valid());
5058 assert_eq!(result.first_failure, Some(1));
5059 assert_eq!(result.results.len(), 2);
5061 assert!(matches!(result.results[0].status, DsigStatus::Valid));
5062 assert!(matches!(
5063 result.results[1].status,
5064 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
5065 ));
5066 }
5067
5068 #[test]
5069 fn empty_references_list() {
5070 let xml = "<root/>";
5071 let doc = Document::parse(xml).unwrap();
5072 let resolver = UriReferenceResolver::new(&doc);
5073
5074 let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
5075 assert!(result.all_valid());
5076 assert!(result.results.is_empty());
5077 }
5078
5079 #[test]
5082 fn reference_sha1_digest() {
5083 let xml = "<root>content</root>";
5084 let doc = Document::parse(xml).unwrap();
5085 let resolver = UriReferenceResolver::new(&doc);
5086
5087 let initial_data = resolver.dereference("").unwrap();
5088 let pre_digest =
5089 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
5090 let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
5091
5092 let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
5093 let result = process_reference(
5094 &reference,
5095 &resolver,
5096 doc.root_element(),
5097 ReferenceSet::SignedInfo,
5098 0,
5099 false,
5100 )
5101 .unwrap();
5102 assert!(matches!(result.status, DsigStatus::Valid));
5103 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
5104 }
5105
5106 #[test]
5107 fn reference_sha512_digest() {
5108 let xml = "<root>content</root>";
5109 let doc = Document::parse(xml).unwrap();
5110 let resolver = UriReferenceResolver::new(&doc);
5111
5112 let initial_data = resolver.dereference("").unwrap();
5113 let pre_digest =
5114 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
5115 let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
5116
5117 let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
5118 let result = process_reference(
5119 &reference,
5120 &resolver,
5121 doc.root_element(),
5122 ReferenceSet::SignedInfo,
5123 0,
5124 false,
5125 )
5126 .unwrap();
5127 assert!(matches!(result.status, DsigStatus::Valid));
5128 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
5129 }
5130
5131 #[test]
5134 fn saml_enveloped_reference_processing() {
5135 let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
5137 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
5138 ID="_resp1">
5139 <saml:Assertion ID="_assert1">
5140 <saml:Subject>user@example.com</saml:Subject>
5141 </saml:Assertion>
5142 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5143 <ds:SignedInfo>
5144 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5145 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5146 <ds:Reference URI="">
5147 <ds:Transforms>
5148 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
5149 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5150 </ds:Transforms>
5151 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5152 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
5153 </ds:Reference>
5154 </ds:SignedInfo>
5155 <ds:SignatureValue>fakesig==</ds:SignatureValue>
5156 </ds:Signature>
5157 </samlp:Response>"##;
5158 let doc = Document::parse(xml).unwrap();
5159 let resolver = UriReferenceResolver::new(&doc);
5160 let sig_node = doc
5161 .descendants()
5162 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
5163 .unwrap();
5164
5165 let signed_info_node = sig_node
5167 .children()
5168 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
5169 .unwrap();
5170 let signed_info = parse_signed_info(signed_info_node).unwrap();
5171 let reference = &signed_info.references[0];
5172
5173 let initial_data = resolver.dereference("").unwrap();
5175 let pre_digest =
5176 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
5177 .unwrap();
5178 let correct_digest = compute_digest(reference.digest_method, &pre_digest);
5179
5180 let corrected_ref = make_reference(
5182 "",
5183 reference.transforms.clone(),
5184 reference.digest_method,
5185 correct_digest,
5186 );
5187
5188 let result = process_reference(
5190 &corrected_ref,
5191 &resolver,
5192 sig_node,
5193 ReferenceSet::SignedInfo,
5194 0,
5195 true,
5196 )
5197 .unwrap();
5198 assert!(
5199 matches!(result.status, DsigStatus::Valid),
5200 "SAML reference should verify"
5201 );
5202 assert!(result.pre_digest_data.is_some());
5203
5204 let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
5206 assert!(
5207 pre_digest_str.contains("samlp:Response"),
5208 "pre-digest should contain Response"
5209 );
5210 assert!(
5211 !pre_digest_str.contains("SignatureValue"),
5212 "pre-digest should NOT contain Signature"
5213 );
5214 }
5215
5216 #[test]
5217 fn pipeline_missing_signed_info_returns_missing_element() {
5218 let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
5219
5220 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
5221 .expect_err("missing SignedInfo must fail before crypto stage");
5222 assert!(matches!(
5223 err,
5224 SignatureVerificationPipelineError::MissingElement {
5225 element: "SignedInfo"
5226 }
5227 ));
5228 }
5229
5230 #[test]
5231 fn pipeline_multiple_signature_elements_are_rejected() {
5232 let xml = r#"
5233<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5234 <ds:Signature>
5235 <ds:SignedInfo/>
5236 </ds:Signature>
5237 <ds:Signature/>
5238</root>
5239"#;
5240
5241 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
5242 .expect_err("multiple signatures must fail closed");
5243 assert!(matches!(
5244 err,
5245 SignatureVerificationPipelineError::InvalidStructure {
5246 reason: "Signature must appear exactly once in document",
5247 }
5248 ));
5249 }
5250
5251 #[test]
5252 fn pipeline_reports_keyinfo_parse_error() {
5253 let xml = r#"
5254<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
5255 xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
5256 <ds:SignedInfo>
5257 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5258 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5259 <ds:Reference URI="">
5260 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
5261 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
5262 </ds:Reference>
5263 </ds:SignedInfo>
5264 <ds:SignatureValue>AA==</ds:SignatureValue>
5265 <ds:KeyInfo>
5266 <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
5267 </ds:KeyInfo>
5268</ds:Signature>
5269"#;
5270
5271 let err = VerifyContext::new().verify(xml).expect_err(
5272 "invalid KeyInfo must map to ParseKeyInfo when no explicit key is supplied",
5273 );
5274 assert!(matches!(
5275 err,
5276 SignatureVerificationPipelineError::ParseKeyInfo(_)
5277 ));
5278 }
5279
5280 #[test]
5281 fn pipeline_ignores_malformed_keyinfo_when_explicit_key_is_supplied() {
5282 let base_xml = signature_with_target_reference("AQ==");
5283 let xml = base_xml
5284 .replace(
5285 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
5286 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
5287 )
5288 .replace(
5289 "</ds:SignatureValue>\n </ds:Signature>",
5290 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
5291 );
5292
5293 let result = VerifyContext::new()
5294 .key(&RejectingKey)
5295 .verify(&xml)
5296 .expect("explicit key path should not fail on malformed KeyInfo");
5297 assert!(matches!(
5298 result.status,
5299 DsigStatus::Invalid(FailureReason::SignatureMismatch)
5300 ));
5301 }
5302
5303 #[test]
5304 fn pipeline_rejects_foreign_element_children_under_signature() {
5305 let base_xml = signature_with_target_reference("AQ==");
5306 let xml = base_xml
5307 .replace(
5308 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
5309 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:foo="urn:example:foo">"#,
5310 )
5311 .replace(
5312 "</ds:SignedInfo>\n <ds:SignatureValue>",
5313 "</ds:SignedInfo>\n <foo:Bar/>\n <ds:SignatureValue>",
5314 );
5315
5316 let err = VerifyContext::new()
5317 .key(&RejectingKey)
5318 .verify(&xml)
5319 .expect_err("foreign element children under Signature must fail closed");
5320 assert!(matches!(
5321 err,
5322 SignatureVerificationPipelineError::InvalidStructure {
5323 reason: "Signature must contain only XMLDSIG element children",
5324 }
5325 ));
5326 }
5327
5328 #[test]
5329 fn pipeline_rejects_non_whitespace_mixed_content_under_signature() {
5330 let base_xml = signature_with_target_reference("AQ==");
5331 let xml = base_xml.replace(
5332 "</ds:SignedInfo>\n <ds:SignatureValue>",
5333 "</ds:SignedInfo>\n oops\n <ds:SignatureValue>",
5334 );
5335
5336 let err = VerifyContext::new()
5337 .key(&RejectingKey)
5338 .verify(&xml)
5339 .expect_err("non-whitespace mixed content under Signature must fail closed");
5340 assert!(matches!(
5341 err,
5342 SignatureVerificationPipelineError::InvalidStructure {
5343 reason: "Signature must not contain non-whitespace mixed content",
5344 }
5345 ));
5346 }
5347
5348 #[test]
5349 fn pipeline_rejects_keyinfo_out_of_order() {
5350 let base_xml = signature_with_target_reference("AQ==");
5351 let xml = base_xml.replace(
5352 "</ds:SignatureValue>\n </ds:Signature>",
5353 "</ds:SignatureValue>\n <ds:Object/>\n <ds:KeyInfo><ds:KeyName>late</ds:KeyName></ds:KeyInfo>\n </ds:Signature>",
5354 );
5355
5356 let err = VerifyContext::new()
5357 .key(&RejectingKey)
5358 .verify(&xml)
5359 .expect_err("KeyInfo after Object must be rejected by Signature child order checks");
5360 assert!(matches!(
5361 err,
5362 SignatureVerificationPipelineError::InvalidStructure {
5363 reason: "KeyInfo must be the third element child of Signature when present"
5364 }
5365 ));
5366 }
5367
5368 #[test]
5369 fn pipeline_accepts_comments_and_processing_instructions_under_signature() {
5370 let xml = r#"
5371<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5372 <?dbg keep ?>
5373 <!-- signature metadata -->
5374 <ds:SignedInfo>
5375 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5376 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5377 <ds:Reference URI="">
5378 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
5379 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
5380 </ds:Reference>
5381 </ds:SignedInfo>
5382 <!-- between required children -->
5383 <ds:SignatureValue>AA==</ds:SignatureValue>
5384</ds:Signature>
5385"#;
5386
5387 let doc = Document::parse(xml).expect("test XML must parse");
5388 let signature_node = doc.root_element();
5389 let parsed = parse_signature_children(signature_node)
5390 .expect("comment/PI nodes under Signature must be ignored");
5391
5392 assert_eq!(parsed.signed_info_node.tag_name().name(), "SignedInfo");
5393 assert_eq!(
5394 parsed.signature_value_node.tag_name().name(),
5395 "SignatureValue"
5396 );
5397 assert!(parsed.key_info_node.is_none());
5398 }
5399}