1use base64::Engine;
14use roxmltree::{Node, NodeId};
15use std::cell::Cell;
16use std::collections::{HashMap, HashSet};
17
18use crate::c14n::canonicalize_bounded_with_xml_base_budget;
19use crate::document::{DocumentParseSettings, DocumentView, XmlDocument};
20use crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING;
21
22#[cfg(test)]
23use super::digest::compute_digest;
24use super::digest::{DigestAlgorithm, constant_time_eq};
25#[cfg(test)]
26use super::parse::MAX_REFERENCES_PER_SIGNATURE;
27#[cfg(test)]
28use super::parse::parse_key_info;
29use super::parse::{
30 KeyInfo, MAX_X509_DATA_TOTAL_BINARY_LEN, MAX_X509_DECODED_BINARY_LEN, ParseError, Reference,
31 RetrievalMethodTransforms, SignatureAlgorithm, XMLDSIG_NS,
32};
33use super::parse::{
34 parse_key_info_with_policy_budgets, parse_reference_with_xpath_budget,
35 parse_signed_info_with_xpath_budget, parse_x509_certificate,
36 parse_x509_data_dispatch_with_budget_and_provider, reference_digest_method,
37};
38use super::signature::{
39 SignatureVerificationError, verify_dsa_signature_spki, verify_ecdsa_signature_pem,
40 verify_rsa_signature_pem,
41};
42#[cfg(test)]
43use super::transforms::BASE64_TRANSFORM_URI;
44use super::transforms::{
45 DEFAULT_IMPLICIT_C14N_URI, Transform, TransformExecutionBudget, TransformOptions,
46 XPATH_TRANSFORM_URI, XPathHereSemantics, XPathSignatureParseBudget,
47 execute_transforms_with_options_and_budget, map_c14n_resource_policy_violation,
48 transform_chain_produces_binary,
49};
50use super::types::{NodeSet, TransformError};
51use super::uri::UriReferenceResolver;
52use super::whitespace::{is_xml_whitespace_only, normalize_xml_base64_bytes};
53
54const MAX_SIGNATURE_VALUE_LEN: usize = 8192;
55const MAX_SIGNATURE_VALUE_TEXT_LEN: usize = 65_536;
56const MAX_RETRIEVAL_METHOD_COUNT: usize = 64;
57pub trait VerifyingKey {
62 fn validate_policy(
68 &self,
69 _policy: &crate::policy::VerificationPolicy,
70 ) -> Result<(), DsigError> {
71 Ok(())
72 }
73
74 fn validate_signature_value(
80 &self,
81 algorithm: SignatureAlgorithm,
82 signature_value: &[u8],
83 ) -> Result<bool, DsigError> {
84 Ok(super::signature::signature_value_matches_algorithm(
85 algorithm,
86 signature_value,
87 ))
88 }
89
90 fn validate_signature_value_with_policy(
98 &self,
99 policy: &crate::policy::VerificationPolicy,
100 algorithm: SignatureAlgorithm,
101 signature_value: &[u8],
102 ) -> Result<bool, DsigError> {
103 let _ = policy;
104 self.validate_signature_value(algorithm, signature_value)
105 }
106
107 fn verify(
109 &self,
110 algorithm: SignatureAlgorithm,
111 signed_data: &[u8],
112 signature_value: &[u8],
113 ) -> Result<bool, DsigError>;
114
115 fn verify_with_policy(
121 &self,
122 policy: &crate::policy::VerificationPolicy,
123 algorithm: SignatureAlgorithm,
124 signed_data: &[u8],
125 signature_value: &[u8],
126 ) -> Result<bool, DsigError> {
127 let _ = policy;
128 self.verify(algorithm, signed_data, signature_value)
129 }
130}
131
132pub trait KeyResolver {
137 fn resolve<'a>(
144 &'a self,
145 key_info: Option<&KeyInfo>,
146 algorithm: SignatureAlgorithm,
147 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError>;
148
149 fn resolve_with_policy<'a>(
159 &'a self,
160 key_info: Option<&KeyInfo>,
161 algorithm: SignatureAlgorithm,
162 _policy: &crate::policy::VerificationPolicy,
163 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
164 self.resolve(key_info, algorithm)
165 }
166
167 fn resolve_with_policy_and_provider<'a>(
173 &'a self,
174 key_info: Option<&KeyInfo>,
175 algorithm: SignatureAlgorithm,
176 policy: &crate::policy::VerificationPolicy,
177 _provider: &dyn crate::provider::CryptoProvider,
178 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
179 self.resolve_with_policy(key_info, algorithm, policy)
180 }
181
182 fn consumes_document_key_info(&self) -> bool {
189 false
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198#[must_use = "pass the policy to VerifyContext::allowed_uri_types(), or store it for reuse"]
199pub struct UriTypeSet {
200 allow_empty: bool,
201 allow_same_document: bool,
202 allow_external: bool,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206enum UriClass {
207 Empty,
208 SameDocument,
209 External,
210}
211
212fn classify_uri(uri: &str) -> UriClass {
213 if uri.is_empty() {
214 UriClass::Empty
215 } else if uri.starts_with('#') {
216 UriClass::SameDocument
217 } else {
218 UriClass::External
219 }
220}
221
222impl UriTypeSet {
223 pub const fn new(allow_empty: bool, allow_same_document: bool, allow_external: bool) -> Self {
225 Self {
226 allow_empty,
227 allow_same_document,
228 allow_external,
229 }
230 }
231
232 pub const SAME_DOCUMENT: Self = Self {
234 allow_empty: true,
235 allow_same_document: true,
236 allow_external: false,
237 };
238
239 pub const ALL: Self = Self {
243 allow_empty: true,
244 allow_same_document: true,
245 allow_external: true,
246 };
247
248 pub(crate) fn allows(self, uri: &str) -> bool {
249 match classify_uri(uri) {
250 UriClass::Empty => self.allow_empty,
251 UriClass::SameDocument => self.allow_same_document,
252 UriClass::External => self.allow_external,
253 }
254 }
255}
256
257impl Default for UriTypeSet {
258 fn default() -> Self {
259 Self::SAME_DOCUMENT
260 }
261}
262
263#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
265pub enum SignatureSelection<'a> {
266 #[default]
268 UniqueDocumentSignature,
269 FirstDocumentSignature,
271 FirstSignatureUnderId(&'a str),
273}
274
275#[must_use = "configure the context and call verify(), or store it for reuse"]
277pub struct VerifyContext<'a> {
278 key: Option<&'a dyn VerifyingKey>,
279 key_resolver: Option<&'a dyn KeyResolver>,
280 policy: crate::policy::VerificationPolicy,
281 provider: &'a dyn crate::provider::CryptoProvider,
282 store_pre_digest: bool,
283 external_resources: Option<&'a HashMap<String, Vec<u8>>>,
284 signature_selection: SignatureSelection<'a>,
285 id_attributes: &'a [crate::IdAttributeRegistration],
286}
287
288impl<'a> VerifyContext<'a> {
289 pub fn new() -> Self {
298 Self {
299 key: None,
300 key_resolver: None,
301 policy: crate::policy::VerificationPolicy::default(),
302 provider: crate::provider::default_provider(),
303 store_pre_digest: false,
304 external_resources: None,
305 signature_selection: SignatureSelection::UniqueDocumentSignature,
306 id_attributes: &[],
307 }
308 }
309
310 pub fn key(mut self, key: &'a dyn VerifyingKey) -> Self {
317 self.key = Some(key);
318 self
319 }
320
321 pub fn key_resolver(mut self, resolver: &'a dyn KeyResolver) -> Self {
323 self.key_resolver = Some(resolver);
324 self
325 }
326
327 pub fn policy(mut self, policy: crate::policy::VerificationPolicy) -> Self {
329 self.policy = policy;
330 self
331 }
332
333 pub fn provider(mut self, provider: &'a dyn crate::provider::CryptoProvider) -> Self {
335 self.provider = provider;
336 self
337 }
338
339 pub fn process_manifests(mut self, enabled: bool) -> Self {
370 self.policy.manifest_processing = if enabled {
371 crate::policy::ManifestProcessing::Process
372 } else {
373 crate::policy::ManifestProcessing::Ignore
374 };
375 self
376 }
377
378 pub fn allowed_uri_types(mut self, types: UriTypeSet) -> Self {
380 self.policy.uris.references = types;
381 self
382 }
383
384 pub fn allowed_retrieval_method_uri_types(mut self, types: UriTypeSet) -> Self {
391 self.policy.uris.retrieval_methods = types;
392 self
393 }
394
395 pub fn external_resources(mut self, resources: &'a HashMap<String, Vec<u8>>) -> Self {
403 self.external_resources = Some(resources);
404 self
405 }
406
407 pub fn start_node_id(mut self, id: &'a str) -> Self {
413 self.signature_selection = SignatureSelection::FirstSignatureUnderId(id);
414 self
415 }
416
417 pub fn first_document_signature(mut self) -> Self {
422 self.signature_selection = SignatureSelection::FirstDocumentSignature;
423 self
424 }
425
426 pub fn id_attributes(mut self, registrations: &'a [crate::IdAttributeRegistration]) -> Self {
428 self.id_attributes = registrations;
429 self
430 }
431
432 pub fn allow_internal_dtd(mut self, enabled: bool) -> Self {
435 self.policy.xml.allow_internal_dtd = enabled;
436 self
437 }
438
439 pub fn allowed_transforms<I, S>(mut self, transforms: I) -> Self
450 where
451 I: IntoIterator<Item = S>,
452 S: Into<String>,
453 {
454 self.policy.transforms.allowed_algorithms =
455 Some(transforms.into_iter().map(Into::into).collect());
456 self
457 }
458
459 pub fn store_pre_digest(mut self, enabled: bool) -> Self {
467 self.store_pre_digest = enabled;
468 self
469 }
470
471 pub fn xpath_here_semantics(mut self, semantics: XPathHereSemantics) -> Self {
477 self.policy.transforms.xpath_here_semantics = semantics;
478 self
479 }
480
481 fn allowed_transform_uris(&self) -> Option<&HashSet<String>> {
482 self.policy.transforms.allowed_algorithms.as_ref()
483 }
484
485 fn transform_options(&self) -> TransformOptions {
486 TransformOptions::default()
487 .allow_internal_dtd(self.policy.xml.allow_internal_dtd)
488 .xpath_here_semantics(self.policy.transforms.xpath_here_semantics)
489 }
490
491 pub fn verify(&self, xml: &str) -> Result<VerifyResult, DsigError> {
499 verify_signature_with_context(xml, self)
500 }
501
502 pub fn verify_document(&self, document: &XmlDocument) -> Result<VerifyResult, DsigError> {
508 verify_signature_document_with_context(document, self)
509 }
510}
511
512impl Default for VerifyContext<'_> {
513 fn default() -> Self {
514 Self::new()
515 }
516}
517
518#[derive(Debug)]
520#[non_exhaustive]
521#[must_use = "inspect status before accepting the reference result"]
522pub struct ReferenceResult {
523 pub reference_set: ReferenceSet,
525 pub reference_index: usize,
527 pub uri: String,
529 pub digest_algorithm: DigestAlgorithm,
531 pub status: DsigStatus,
533 pub pre_digest_data: Option<Vec<u8>>,
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, Eq)]
539#[non_exhaustive]
540pub enum ReferenceSet {
541 SignedInfo,
543 Manifest,
545}
546
547#[derive(Debug, Clone, Copy, PartialEq, Eq)]
549#[non_exhaustive]
550pub enum DsigStatus {
551 Valid,
553 Invalid(FailureReason),
555}
556
557#[derive(Debug, Clone, Copy, PartialEq, Eq)]
559#[non_exhaustive]
560pub enum FailureReason {
561 ReferenceDigestMismatch {
563 ref_index: usize,
573 },
574 ReferencePolicyViolation {
576 ref_index: usize,
578 },
579 ReferenceProcessingFailure {
581 ref_index: usize,
583 },
584 SignatureMismatch,
586 KeyNotFound,
588}
589
590#[derive(Debug)]
592#[non_exhaustive]
593#[must_use = "check first_failure/results before accepting the reference set"]
594pub struct ReferencesResult {
595 pub results: Vec<ReferenceResult>,
598 pub first_failure: Option<usize>,
600}
601
602impl ReferencesResult {
603 #[must_use]
605 pub fn all_valid(&self) -> bool {
606 self.results
607 .iter()
608 .all(|result| matches!(result.status, DsigStatus::Valid))
609 }
610}
611
612pub fn process_reference(
631 reference: &Reference,
632 resolver: &UriReferenceResolver<'_>,
633 signature_node: Node<'_, '_>,
634 reference_set: ReferenceSet,
635 reference_index: usize,
636 store_pre_digest: bool,
637) -> Result<ReferenceResult, ReferenceProcessingError> {
638 let execution_budget = TransformExecutionBudget::default();
639 let canonicalized_data_budget = CanonicalizedDataBudget::default();
640 let execution = ReferenceExecutionContext {
641 store_pre_digest,
642 transform_options: TransformOptions::default(),
643 transform_budget: &execution_budget,
644 canonicalized_data_budget: &canonicalized_data_budget,
645 provider: crate::provider::default_provider(),
646 };
647 process_reference_with_options(
648 reference,
649 resolver,
650 signature_node,
651 reference_set,
652 reference_index,
653 reference_origin_node(signature_node, reference_set, reference_index),
654 &execution,
655 )
656}
657
658fn reference_origin_node<'a, 'input>(
659 signature_node: Node<'a, 'input>,
660 reference_set: ReferenceSet,
661 reference_index: usize,
662) -> Option<Node<'a, 'input>> {
663 let is_reference = |node: &Node<'_, '_>| {
664 node.is_element()
665 && node.tag_name().namespace() == Some(XMLDSIG_NS)
666 && node.tag_name().name() == "Reference"
667 };
668 match reference_set {
669 ReferenceSet::SignedInfo => signature_node
670 .children()
671 .find(|node| {
672 node.is_element()
673 && node.tag_name().namespace() == Some(XMLDSIG_NS)
674 && node.tag_name().name() == "SignedInfo"
675 })?
676 .children()
677 .filter(is_reference)
678 .nth(reference_index),
679 ReferenceSet::Manifest => signature_node
680 .children()
681 .filter(|node| {
682 node.is_element()
683 && node.tag_name().namespace() == Some(XMLDSIG_NS)
684 && node.tag_name().name() == "Object"
685 })
686 .flat_map(|object| {
687 object.children().filter(|node| {
688 node.is_element()
689 && node.tag_name().namespace() == Some(XMLDSIG_NS)
690 && node.tag_name().name() == "Manifest"
691 })
692 })
693 .flat_map(|manifest| manifest.children().filter(is_reference))
694 .nth(reference_index),
695 }
696}
697
698struct ReferenceExecutionContext<'a> {
699 store_pre_digest: bool,
700 transform_options: TransformOptions,
701 transform_budget: &'a TransformExecutionBudget,
702 canonicalized_data_budget: &'a CanonicalizedDataBudget,
703 provider: &'a dyn crate::provider::CryptoProvider,
704}
705
706struct CanonicalizedDataBudget {
707 remaining: Cell<usize>,
708 max_bytes: usize,
709}
710
711impl Default for CanonicalizedDataBudget {
712 fn default() -> Self {
713 Self {
714 remaining: Cell::new(CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING),
715 max_bytes: CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
716 }
717 }
718}
719
720impl CanonicalizedDataBudget {
721 fn remaining(&self) -> usize {
722 self.remaining.get()
723 }
724
725 fn charge(&self, bytes: usize) -> Result<(), ReferenceProcessingError> {
726 let available = self.remaining.get();
727 let Some(remaining) = available.checked_sub(bytes) else {
728 self.remaining.set(0);
729 return Err(crate::policy::PolicyViolation::ResourceLimit {
730 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
731 maximum: self.max_bytes,
732 actual: self
733 .max_bytes
734 .saturating_add(bytes.saturating_sub(available)),
735 }
736 .into());
737 };
738 self.remaining.set(remaining);
739 Ok(())
740 }
741
742 fn with_limit(max_bytes: usize) -> Self {
743 Self {
744 remaining: Cell::new(max_bytes),
745 max_bytes,
746 }
747 }
748}
749
750fn process_reference_with_options(
751 reference: &Reference,
752 resolver: &UriReferenceResolver<'_>,
753 signature_node: Node<'_, '_>,
754 reference_set: ReferenceSet,
755 reference_index: usize,
756 reference_node: Option<Node<'_, '_>>,
757 execution: &ReferenceExecutionContext<'_>,
758) -> Result<ReferenceResult, ReferenceProcessingError> {
759 let uri = reference
762 .uri
763 .as_deref()
764 .ok_or(ReferenceProcessingError::MissingUri)?;
765 let initial_data = reference_node
766 .map_or_else(
767 || {
768 resolver.dereference_with_budget(
769 uri,
770 execution.transform_budget.node_set_materialization(),
771 )
772 },
773 |node| {
774 resolver.dereference_from_with_budget(
775 uri,
776 node,
777 execution.transform_budget.node_set_materialization(),
778 execution.transform_budget.xml_base_resolution(),
779 )
780 },
781 )
782 .map_err(ReferenceProcessingError::UriDereference)?;
783
784 let pre_digest_bytes = execute_transforms_with_options_and_budget(
786 signature_node,
787 initial_data,
788 &reference.transforms,
789 execution.transform_options,
790 execution.transform_budget,
791 )
792 .map_err(ReferenceProcessingError::Transform)?;
793
794 let computed_digest = super::compute_digest_with_provider(
796 execution.provider,
797 reference.digest_method,
798 &pre_digest_bytes,
799 )?;
800
801 let status = if constant_time_eq(&computed_digest, &reference.digest_value) {
803 DsigStatus::Valid
804 } else {
805 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch {
806 ref_index: reference_index,
807 })
808 };
809
810 let pre_digest_data = if execution.store_pre_digest {
811 execution
812 .canonicalized_data_budget
813 .charge(pre_digest_bytes.len())?;
814 Some(pre_digest_bytes)
815 } else {
816 None
817 };
818
819 Ok(ReferenceResult {
820 reference_set,
821 reference_index,
822 uri: uri.to_owned(),
823 digest_algorithm: reference.digest_method,
824 status,
825 pre_digest_data,
826 })
827}
828
829pub fn process_all_references(
841 references: &[Reference],
842 resolver: &UriReferenceResolver<'_>,
843 signature_node: Node<'_, '_>,
844 store_pre_digest: bool,
845) -> Result<ReferencesResult, ReferenceProcessingError> {
846 let execution_budget = TransformExecutionBudget::default();
847 let canonicalized_data_budget = CanonicalizedDataBudget::default();
848 let execution = ReferenceExecutionContext {
849 store_pre_digest,
850 transform_options: TransformOptions::default(),
851 transform_budget: &execution_budget,
852 canonicalized_data_budget: &canonicalized_data_budget,
853 provider: crate::provider::default_provider(),
854 };
855 process_all_references_with_options(references, resolver, signature_node, &execution)
856}
857
858fn process_all_references_with_options(
859 references: &[Reference],
860 resolver: &UriReferenceResolver<'_>,
861 signature_node: Node<'_, '_>,
862 execution: &ReferenceExecutionContext<'_>,
863) -> Result<ReferencesResult, ReferenceProcessingError> {
864 let mut results = Vec::with_capacity(references.len());
865
866 for (i, reference) in references.iter().enumerate() {
867 let result = process_reference_with_options(
868 reference,
869 resolver,
870 signature_node,
871 ReferenceSet::SignedInfo,
872 i,
873 reference_origin_node(signature_node, ReferenceSet::SignedInfo, i),
874 execution,
875 )?;
876 let failed = matches!(result.status, DsigStatus::Invalid(_));
877 results.push(result);
878
879 if failed {
880 return Ok(ReferencesResult {
881 results,
882 first_failure: Some(i),
883 });
884 }
885 }
886
887 Ok(ReferencesResult {
888 results,
889 first_failure: None,
890 })
891}
892
893#[derive(Debug, thiserror::Error)]
897#[non_exhaustive]
898pub enum ReferenceProcessingError {
899 #[error("verification policy violation: {0}")]
901 Policy(#[from] crate::policy::PolicyViolation),
902
903 #[error("cryptographic provider error: {0}")]
905 Provider(#[from] crate::provider::ProviderError),
906
907 #[error("reference URI is required; omitted URI references are not supported")]
909 MissingUri,
910
911 #[error("URI dereference failed: {0}")]
913 UriDereference(#[source] super::types::TransformError),
914
915 #[error("transform failed: {0}")]
917 Transform(#[source] super::types::TransformError),
918}
919
920impl ReferenceProcessingError {
921 fn into_policy_violation(self) -> Result<crate::policy::PolicyViolation, Self> {
922 match self {
923 Self::Policy(error)
924 | Self::UriDereference(TransformError::Policy(error))
925 | Self::Transform(TransformError::Policy(error)) => Ok(error),
926 error => Err(error),
927 }
928 }
929}
930
931#[derive(Debug)]
933#[non_exhaustive]
934#[must_use = "inspect status before accepting the document"]
935pub struct VerifyResult {
936 pub status: DsigStatus,
941 pub signed_info_references: Vec<ReferenceResult>,
945 pub manifest_references: Vec<ReferenceResult>,
955 pub canonicalized_signed_info: Option<Vec<u8>>,
958}
959
960#[derive(Debug, thiserror::Error)]
962#[non_exhaustive]
963pub enum DsigError {
964 #[error("verification policy violation: {0}")]
966 Policy(#[from] crate::policy::PolicyViolation),
967
968 #[error("cryptographic provider error: {0}")]
970 Provider(#[from] crate::provider::ProviderError),
971
972 #[error("XML parse error: {0}")]
974 XmlParse(#[from] roxmltree::Error),
975
976 #[error("XML document error: {0}")]
978 Document(#[from] crate::document::XmlDocumentError),
979
980 #[error("missing required element: <{element}>")]
982 MissingElement {
983 element: &'static str,
985 },
986
987 #[error("invalid Signature structure: {reason}")]
989 InvalidStructure {
990 reason: &'static str,
992 },
993
994 #[error("selected node ID is missing or ambiguous: {id}")]
996 SelectedNodeUnavailable {
997 id: String,
999 },
1000
1001 #[error("failed to parse SignedInfo: {0}")]
1003 ParseSignedInfo(super::parse::ParseError),
1004
1005 #[error("failed to parse KeyInfo: {0}")]
1007 ParseKeyInfo(#[source] super::parse::ParseError),
1008
1009 #[error("key resolution failed: {0}")]
1011 KeyResolution(#[from] super::keys::KeyResolutionError),
1012
1013 #[error("failed to parse Manifest reference: {0}")]
1015 ParseManifestReference(#[source] ParseError),
1016
1017 #[error("reference processing failed: {0}")]
1019 Reference(ReferenceProcessingError),
1020
1021 #[error("SignedInfo canonicalization failed: {0}")]
1023 Canonicalization(#[from] crate::c14n::C14nError),
1024
1025 #[error("invalid SignatureValue base64: {0}")]
1027 SignatureValueBase64(#[from] base64::DecodeError),
1028
1029 #[error("signature verification failed: {0}")]
1031 Crypto(#[from] SignatureVerificationError),
1032}
1033
1034impl From<super::parse::ParseError> for DsigError {
1035 fn from(error: super::parse::ParseError) -> Self {
1036 match error {
1037 super::parse::ParseError::Policy(error) => Self::Policy(error),
1038 super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1039 Self::Policy(error)
1040 }
1041 error => Self::ParseSignedInfo(error),
1042 }
1043 }
1044}
1045
1046fn map_key_info_parse_error(error: super::parse::ParseError) -> DsigError {
1047 match error {
1048 super::parse::ParseError::Policy(error)
1049 | super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1050 DsigError::Policy(error)
1051 }
1052 error => DsigError::ParseKeyInfo(error),
1053 }
1054}
1055
1056fn map_manifest_parse_error(error: super::parse::ParseError) -> DsigError {
1057 match error {
1058 super::parse::ParseError::Policy(error)
1059 | super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1060 DsigError::Policy(error)
1061 }
1062 error => DsigError::ParseManifestReference(error),
1063 }
1064}
1065
1066impl From<ReferenceProcessingError> for DsigError {
1067 fn from(error: ReferenceProcessingError) -> Self {
1068 match error.into_policy_violation() {
1069 Ok(error) => Self::Policy(error),
1070 Err(error) => Self::Reference(error),
1071 }
1072 }
1073}
1074
1075type SignatureVerificationPipelineError = DsigError;
1076
1077pub fn verify_signature_with_pem_key(
1105 xml: &str,
1106 public_key_pem: &str,
1107 store_pre_digest: bool,
1108) -> Result<VerifyResult, DsigError> {
1109 struct PemVerifyingKey<'a> {
1110 public_key_pem: &'a str,
1111 }
1112
1113 impl VerifyingKey for PemVerifyingKey<'_> {
1114 fn verify(
1115 &self,
1116 algorithm: SignatureAlgorithm,
1117 signed_data: &[u8],
1118 signature_value: &[u8],
1119 ) -> Result<bool, DsigError> {
1120 verify_with_algorithm(algorithm, self.public_key_pem, signed_data, signature_value)
1121 }
1122 }
1123
1124 let key = PemVerifyingKey { public_key_pem };
1125 VerifyContext::new()
1126 .key(&key)
1127 .store_pre_digest(store_pre_digest)
1128 .verify(xml)
1129}
1130
1131fn verify_signature_with_context(
1132 xml: &str,
1133 ctx: &VerifyContext<'_>,
1134) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1135 ctx.policy.validate()?;
1136 ctx.policy.resources.validate_xml_document_len(xml.len())?;
1137 let execution_budget = TransformExecutionBudget::from_resources(&ctx.policy.resources);
1138 let settings = DocumentParseSettings::from_policy(&ctx.policy.xml, &ctx.policy.resources);
1139 let document = XmlDocument::parse_with_settings_and_budget(
1140 xml.to_owned(),
1141 settings,
1142 execution_budget.xml_parse_work(),
1143 )
1144 .map_err(|error| match error.into_policy_violation(settings) {
1145 Ok(error) => DsigError::Policy(error),
1146 Err(crate::document::XmlDocumentError::Parse(error)) => DsigError::XmlParse(error),
1147 Err(error) => DsigError::Document(error),
1148 })?;
1149 verify_signature_document_with_context_and_budget(&document, ctx, &execution_budget)
1150}
1151
1152#[cfg(test)]
1153mod xml_parse_budget_tests {
1154 use super::*;
1155
1156 #[test]
1157 fn verification_initial_parse_uses_the_policy_work_budget() {
1158 let xml = "<root/>";
1161 let mut policy = crate::policy::VerificationPolicy::default();
1162 policy.resources.max_xml_parse_work_bytes = 0;
1163
1164 let error = VerifyContext::new()
1165 .policy(policy)
1166 .verify(xml)
1167 .expect_err("a zero parse-work budget must reject the input parse");
1168
1169 assert!(matches!(
1170 error,
1171 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1172 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
1173 maximum: 0,
1174 actual,
1175 }) if actual == xml.len()
1176 ));
1177 }
1178}
1179
1180fn verify_signature_document_with_context(
1181 document: &XmlDocument,
1182 ctx: &VerifyContext<'_>,
1183) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1184 let execution_budget = TransformExecutionBudget::from_resources(&ctx.policy.resources);
1185 verify_signature_document_with_context_and_budget(document, ctx, &execution_budget)
1186}
1187
1188fn verify_signature_document_with_context_and_budget(
1189 document: &XmlDocument,
1190 ctx: &VerifyContext<'_>,
1191 execution_budget: &TransformExecutionBudget,
1192) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1193 document.validate_operation_policy(&ctx.policy.xml, &ctx.policy.resources)?;
1194 document.with_view(|view| verify_signature_view(view, ctx, execution_budget))
1195}
1196
1197fn verify_signature_view<'a>(
1198 view: DocumentView<'a>,
1199 ctx: &VerifyContext<'_>,
1200 execution_budget: &TransformExecutionBudget,
1201) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1202 ctx.policy.validate()?;
1203 let doc = view.document();
1204 let resolver = UriReferenceResolver::with_document_view(view, ctx.id_attributes)
1205 .with_same_document_id_semantics(ctx.policy.transforms.same_document_id_semantics)
1206 .with_external_resource_limits(
1207 ctx.policy.resources.max_external_resource_bytes,
1208 ctx.policy.resources.max_external_resource_total_bytes,
1209 );
1210 let resolver = match ctx.external_resources {
1211 Some(resources) => resolver.with_external_resources(resources),
1212 None => resolver,
1213 };
1214 let start_node = match ctx.signature_selection {
1215 SignatureSelection::FirstSignatureUnderId(id) => {
1216 resolver.node_for_id(id).ok_or_else(|| {
1217 SignatureVerificationPipelineError::SelectedNodeUnavailable { id: id.to_owned() }
1218 })?
1219 }
1220 SignatureSelection::UniqueDocumentSignature
1221 | SignatureSelection::FirstDocumentSignature => doc.root(),
1222 };
1223 let mut signatures = start_node.descendants().filter(|node| {
1224 node.is_element()
1225 && node.tag_name().name() == "Signature"
1226 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1227 });
1228 let signature_node = match (signatures.next(), ctx.signature_selection) {
1229 (None, _) => {
1230 return Err(SignatureVerificationPipelineError::MissingElement {
1231 element: "Signature",
1232 });
1233 }
1234 (
1238 Some(node),
1239 SignatureSelection::FirstDocumentSignature
1240 | SignatureSelection::FirstSignatureUnderId(_),
1241 ) => node,
1242 (Some(node), SignatureSelection::UniqueDocumentSignature)
1243 if signatures.next().is_none() =>
1244 {
1245 node
1246 }
1247 (Some(_), SignatureSelection::UniqueDocumentSignature) => {
1248 return Err(SignatureVerificationPipelineError::InvalidStructure {
1249 reason: "Signature must appear exactly once in document",
1250 });
1251 }
1252 };
1253
1254 let signature_children = parse_signature_children(signature_node)?;
1255 let signed_info_node = signature_children.signed_info_node;
1256 let should_parse_key_info = match (ctx.key, ctx.key_resolver) {
1257 (Some(_), _) => false,
1258 (None, Some(resolver)) => resolver.consumes_document_key_info(),
1259 (None, None) => true,
1260 };
1261 let mut key_info = if should_parse_key_info {
1262 signature_children
1263 .key_info_node
1264 .map(|node| {
1265 parse_key_info_with_policy_budgets(
1266 node,
1267 ctx.provider,
1268 execution_budget.xml_base_resolution(),
1269 &ctx.policy.resources,
1270 )
1271 })
1272 .transpose()
1273 .map_err(map_key_info_parse_error)?
1274 } else {
1275 None
1276 };
1277
1278 let mut xpath_parse_budget = XPathSignatureParseBudget::from_resources(&ctx.policy.resources);
1279 let signed_info =
1280 parse_signed_info_with_xpath_budget(signed_info_node, &mut xpath_parse_budget)?;
1281 if signed_info.references.len() > ctx.policy.resources.max_references {
1282 return Err(crate::policy::PolicyViolation::ResourceLimit {
1283 resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
1284 maximum: ctx.policy.resources.max_references,
1285 actual: signed_info.references.len(),
1286 }
1287 .into());
1288 }
1289 for reference in &signed_info.references {
1290 if reference.transforms.len() > ctx.policy.resources.max_transforms_per_reference {
1291 return Err(crate::policy::PolicyViolation::ResourceLimit {
1292 resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
1293 maximum: ctx.policy.resources.max_transforms_per_reference,
1294 actual: reference.transforms.len(),
1295 }
1296 .into());
1297 }
1298 }
1299 ctx.policy
1300 .check_signature_algorithm(signed_info.signature_method)?;
1301 for reference in &signed_info.references {
1302 if ctx
1303 .policy
1304 .digest_algorithms
1305 .as_ref()
1306 .is_some_and(|allowed| !allowed.contains(&reference.digest_method))
1307 {
1308 return Err(crate::policy::PolicyViolation::Algorithm {
1309 operation: "verification",
1310 algorithm: reference.digest_method.uri().to_string(),
1311 }
1312 .into());
1313 }
1314 }
1315 enforce_reference_policies(
1316 &signed_info.references,
1317 ctx.policy.uris.references,
1318 ctx.allowed_transform_uris(),
1319 )?;
1320 enforce_transform_allowed(ctx.allowed_transform_uris(), signed_info.c14n_method.uri())?;
1321
1322 if let Some(resources) = ctx.external_resources {
1323 let mut total = 0usize;
1324 for bytes in resources.values() {
1325 if bytes.len() > ctx.policy.resources.max_external_resource_bytes {
1326 return Err(crate::policy::PolicyViolation::ResourceLimit {
1327 resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
1328 maximum: ctx.policy.resources.max_external_resource_bytes,
1329 actual: bytes.len(),
1330 }
1331 .into());
1332 }
1333 total = total.checked_add(bytes.len()).ok_or(
1334 SignatureVerificationPipelineError::InvalidStructure {
1335 reason: "external resource total length overflow",
1336 },
1337 )?;
1338 }
1339 if total > ctx.policy.resources.max_external_resource_total_bytes {
1340 return Err(crate::policy::PolicyViolation::ResourceLimit {
1341 resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
1342 maximum: ctx.policy.resources.max_external_resource_total_bytes,
1343 actual: total,
1344 }
1345 .into());
1346 }
1347 }
1348 let retrieval_materialization = if let Some(info) = key_info.as_mut() {
1349 let mut retrieval_budgets = RetrievalMaterializationBudgets {
1350 xpath_parse: &mut xpath_parse_budget,
1351 execution: execution_budget,
1352 resources: &ctx.policy.resources,
1353 };
1354 materialize_retrieval_methods_with_budgets(
1355 info,
1356 &resolver,
1357 ctx.policy.uris.retrieval_methods,
1358 ctx.allowed_transform_uris(),
1359 ctx.provider,
1360 &mut retrieval_budgets,
1361 )?
1362 } else {
1363 RetrievalMaterialization::default()
1364 };
1365 let canonicalized_data_budget =
1366 CanonicalizedDataBudget::with_limit(ctx.policy.resources.effective_canonicalized_bytes());
1367 let execution = ReferenceExecutionContext {
1368 store_pre_digest: ctx.store_pre_digest,
1369 transform_options: ctx.transform_options(),
1370 transform_budget: execution_budget,
1371 canonicalized_data_budget: &canonicalized_data_budget,
1372 provider: ctx.provider,
1373 };
1374 let references = process_all_references_with_options(
1375 &signed_info.references,
1376 &resolver,
1377 signature_node,
1378 &execution,
1379 )?;
1380
1381 if let Some(first_failure) = references.first_failure {
1382 let status = references.results[first_failure].status;
1383 return Ok(VerifyResult {
1384 status,
1385 signed_info_references: references.results,
1386 manifest_references: Vec::new(),
1387 canonicalized_signed_info: None,
1388 });
1389 }
1390
1391 let signed_info_subtree: HashSet<_> = signed_info_node
1392 .descendants()
1393 .map(|node: Node<'_, '_>| node.id())
1394 .collect();
1395 let mut canonical_signed_info = Vec::new();
1396 let signed_info_limit = canonicalized_data_budget
1397 .remaining()
1398 .min(execution_budget.remaining_c14n_output());
1399 canonicalize_bounded_with_xml_base_budget(
1400 doc,
1401 Some(&|node| signed_info_subtree.contains(&node.id())),
1402 &signed_info.c14n_method,
1403 signed_info_limit,
1404 execution_budget.xml_base_resolution(),
1405 &mut canonical_signed_info,
1406 )
1407 .map_err(|error| {
1408 if let Some(violation) = map_c14n_resource_policy_violation(
1409 &error,
1410 crate::policy::resource_name::CANONICALIZED_BYTES,
1411 canonicalized_data_budget.max_bytes,
1412 ) {
1413 SignatureVerificationPipelineError::Policy(violation)
1414 } else {
1415 SignatureVerificationPipelineError::Canonicalization(error)
1416 }
1417 })?;
1418 execution_budget
1419 .charge_c14n_output(canonical_signed_info.len())
1420 .map_err(ReferenceProcessingError::Transform)?;
1421 canonicalized_data_budget.charge(canonical_signed_info.len())?;
1422
1423 let signature_value = decode_signature_value(signature_children.signature_value_node)?;
1424 if signed_info.signature_method == SignatureAlgorithm::HmacSha1 {
1425 let expected_bits = signed_info.hmac_output_length_bits.unwrap_or(160);
1426 if signature_value.len() != expected_bits / 8 {
1427 return Err(SignatureVerificationPipelineError::InvalidStructure {
1428 reason: "SignatureValue length does not match HMACOutputLength",
1429 });
1430 }
1431 }
1432 let Some(resolved_key) =
1433 resolve_verifying_key(ctx, key_info.as_ref(), signed_info.signature_method)?
1434 else {
1435 if let Some(error) = retrieval_materialization.deferred_error {
1436 return Err(error);
1437 }
1438 return Ok(VerifyResult {
1439 status: DsigStatus::Invalid(FailureReason::KeyNotFound),
1440 signed_info_references: references.results,
1441 manifest_references: Vec::new(),
1442 canonicalized_signed_info: if ctx.store_pre_digest {
1443 Some(canonical_signed_info)
1444 } else {
1445 None
1446 },
1447 });
1448 };
1449 let verifier = resolved_key.as_ref();
1450 verifier.validate_policy(&ctx.policy)?;
1451 if !verifier.validate_signature_value_with_policy(
1452 &ctx.policy,
1453 signed_info.signature_method,
1454 &signature_value,
1455 )? {
1456 return Ok(VerifyResult {
1457 status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
1458 signed_info_references: references.results,
1459 manifest_references: Vec::new(),
1460 canonicalized_signed_info: if ctx.store_pre_digest {
1461 Some(canonical_signed_info)
1462 } else {
1463 None
1464 },
1465 });
1466 }
1467 ctx.provider
1468 .require_capability(crate::provider::ProviderCapability::Verify(
1469 signed_info.signature_method,
1470 ))?;
1471 let policy_verifier = PolicyVerifyingKey {
1472 key: verifier,
1473 policy: &ctx.policy,
1474 };
1475 let signature_valid = ctx.provider.verify(
1476 &policy_verifier,
1477 signed_info.signature_method,
1478 &canonical_signed_info,
1479 &signature_value,
1480 )?;
1481
1482 if !signature_valid {
1483 return Ok(VerifyResult {
1484 status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
1485 signed_info_references: references.results,
1486 manifest_references: Vec::new(),
1487 canonicalized_signed_info: if ctx.store_pre_digest {
1488 Some(canonical_signed_info)
1489 } else {
1490 None
1491 },
1492 });
1493 }
1494
1495 let manifest_references = if ctx.policy.manifest_processing
1496 == crate::policy::ManifestProcessing::Process
1497 {
1498 let signed_info_reference_nodes =
1499 collect_authenticated_signed_info_reference_nodes(&signed_info.references, &resolver);
1500 let remaining_reference_capacity = ctx
1501 .policy
1502 .resources
1503 .max_references
1504 .checked_sub(signed_info.references.len())
1505 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
1506 reason: "SignedInfo exceeds the per-signature Reference limit",
1507 })?;
1508 process_manifest_references(
1509 signature_node,
1510 &resolver,
1511 ctx,
1512 &signed_info_reference_nodes,
1513 remaining_reference_capacity,
1514 &execution,
1515 &mut xpath_parse_budget,
1516 )?
1517 } else {
1518 Vec::new()
1519 };
1520
1521 Ok(VerifyResult {
1522 status: DsigStatus::Valid,
1523 signed_info_references: references.results,
1524 manifest_references,
1525 canonicalized_signed_info: if ctx.store_pre_digest {
1526 Some(canonical_signed_info)
1527 } else {
1528 None
1529 },
1530 })
1531}
1532
1533struct PolicyVerifyingKey<'a> {
1534 key: &'a dyn VerifyingKey,
1535 policy: &'a crate::policy::VerificationPolicy,
1536}
1537
1538impl VerifyingKey for PolicyVerifyingKey<'_> {
1539 fn validate_policy(&self, policy: &crate::policy::VerificationPolicy) -> Result<(), DsigError> {
1540 self.key.validate_policy(policy)
1541 }
1542
1543 fn validate_signature_value(
1544 &self,
1545 algorithm: SignatureAlgorithm,
1546 signature_value: &[u8],
1547 ) -> Result<bool, DsigError> {
1548 self.key
1549 .validate_signature_value_with_policy(self.policy, algorithm, signature_value)
1550 }
1551
1552 fn verify(
1553 &self,
1554 algorithm: SignatureAlgorithm,
1555 signed_data: &[u8],
1556 signature_value: &[u8],
1557 ) -> Result<bool, DsigError> {
1558 self.key
1559 .verify_with_policy(self.policy, algorithm, signed_data, signature_value)
1560 }
1561}
1562
1563#[derive(Debug, Default)]
1564struct RetrievalMaterialization {
1565 deferred_error: Option<SignatureVerificationPipelineError>,
1566}
1567
1568struct RetrievalMaterializationBudgets<'a> {
1569 xpath_parse: &'a mut XPathSignatureParseBudget,
1570 execution: &'a TransformExecutionBudget,
1571 resources: &'a crate::policy::ResourcePolicy,
1572}
1573
1574fn materialize_retrieval_methods_with_budgets(
1575 key_info: &mut KeyInfo,
1576 resolver: &UriReferenceResolver<'_>,
1577 allowed_uri_types: UriTypeSet,
1578 allowed_transforms: Option<&HashSet<String>>,
1579 provider: &dyn crate::provider::CryptoProvider,
1580 budgets: &mut RetrievalMaterializationBudgets<'_>,
1581) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
1582 let retrieval_count = key_info
1583 .sources
1584 .iter()
1585 .filter(|source| matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. }))
1586 .count();
1587 if retrieval_count > MAX_RETRIEVAL_METHOD_COUNT {
1588 return Err(SignatureVerificationPipelineError::InvalidStructure {
1589 reason: "KeyInfo contains too many RetrievalMethod elements",
1590 });
1591 }
1592
1593 let mut total_binary_len = existing_x509_binary_len(key_info)?;
1594 let mut materialized_candidate_preflight_count = key_info.embedded_candidate_count();
1595 let mut seen = HashSet::new();
1596 let mut materialized = Vec::with_capacity(key_info.sources.len());
1597 let mut outcome = RetrievalMaterialization::default();
1598 for source in std::mem::take(&mut key_info.sources) {
1599 let super::parse::KeyInfoSource::RetrievalMethod {
1600 uri: resolved_uri,
1601 resource_type,
1602 transforms,
1603 } = source
1604 else {
1605 materialized.push(source);
1606 continue;
1607 };
1608
1609 let identity = (
1610 resolved_uri.clone(),
1611 resource_type.clone(),
1612 transforms.clone(),
1613 );
1614 if !seen.insert(identity) {
1615 continue;
1616 }
1617
1618 if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#rawX509Certificate")
1619 {
1620 if transforms != RetrievalMethodTransforms::None
1621 || classify_uri(&resolved_uri) != UriClass::External
1622 {
1623 return Err(SignatureVerificationPipelineError::InvalidStructure {
1624 reason: "raw X509 RetrievalMethod requires an untransformed external URI",
1625 });
1626 }
1627 if !allowed_uri_types.allows(&resolved_uri) {
1628 return Err(crate::policy::PolicyViolation::Uri {
1629 operation: "verification",
1630 reason: "retrieval method URI class is not permitted",
1631 }
1632 .into());
1633 }
1634 let certificate = resolver.external_resource(&resolved_uri).map_err(|error| {
1635 SignatureVerificationPipelineError::from(ReferenceProcessingError::Transform(error))
1636 })?;
1637 let Some(certificate) = certificate else {
1638 outcome.deferred_error.get_or_insert_with(|| {
1639 SignatureVerificationPipelineError::Reference(
1640 ReferenceProcessingError::Transform(super::TransformError::UnsupportedUri(
1641 resolved_uri.clone(),
1642 )),
1643 )
1644 });
1645 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
1646 uri: resolved_uri,
1647 resource_type,
1648 transforms,
1649 });
1650 continue;
1651 };
1652 materialized_candidate_preflight_count =
1653 materialized_candidate_preflight_count.saturating_add(1);
1654 budgets
1655 .resources
1656 .validate_key_candidates(materialized_candidate_preflight_count)?;
1657 if certificate.len() > MAX_X509_DECODED_BINARY_LEN {
1658 return Err(SignatureVerificationPipelineError::InvalidStructure {
1659 reason: "raw X509 RetrievalMethod certificate exceeds maximum allowed length",
1660 });
1661 }
1662 add_retrieval_binary_usage(&mut total_binary_len, certificate.len())?;
1663 let parsed = match parse_x509_certificate(certificate) {
1664 Ok(parsed) => parsed,
1665 Err(error) => {
1666 let error = map_key_info_parse_error(error);
1667 if matches!(error, SignatureVerificationPipelineError::Policy(_)) {
1668 return Err(error);
1669 }
1670 outcome.deferred_error.get_or_insert(error);
1671 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
1672 uri: resolved_uri,
1673 resource_type,
1674 transforms,
1675 });
1676 continue;
1677 }
1678 };
1679 materialized.push(super::parse::KeyInfoSource::X509Data(
1680 super::parse::X509DataInfo {
1681 certificates: vec![certificate.to_vec()],
1682 parsed_certificates: vec![parsed],
1683 certificate_chain: vec![0],
1684 ..super::parse::X509DataInfo::default()
1685 },
1686 ));
1687 } else if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#X509Data") {
1688 if !allowed_uri_types.allows(&resolved_uri) {
1689 return Err(crate::policy::PolicyViolation::Uri {
1690 operation: "verification",
1691 reason: "retrieval method URI class is not permitted",
1692 }
1693 .into());
1694 }
1695 let target = resolver
1696 .node_for_same_document_reference(&resolved_uri)
1697 .map_err(ReferenceProcessingError::Transform)?
1698 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
1699 reason: "X509Data RetrievalMethod target is missing or ambiguous",
1700 })?;
1701 let node = match transforms {
1702 RetrievalMethodTransforms::None
1703 if target.has_tag_name((XMLDSIG_NS, "X509Data")) =>
1704 {
1705 target
1706 }
1707 RetrievalMethodTransforms::None => {
1708 return Err(SignatureVerificationPipelineError::InvalidStructure {
1709 reason: "untransformed X509Data RetrievalMethod must target X509Data directly",
1710 });
1711 }
1712 RetrievalMethodTransforms::X509DataNodeSetFilter {
1713 expression,
1714 namespaces,
1715 } => {
1716 enforce_transform_allowed(allowed_transforms, XPATH_TRANSFORM_URI)?;
1717 budgets
1718 .xpath_parse
1719 .validate_expression(&expression)
1720 .map_err(ReferenceProcessingError::Transform)?;
1721 budgets
1722 .xpath_parse
1723 .validate_namespaces(&namespaces)
1724 .map_err(ReferenceProcessingError::Transform)?;
1725 select_retrieved_x509_data_root(target, budgets.execution)?
1726 }
1727 RetrievalMethodTransforms::Unsupported => {
1728 return Err(SignatureVerificationPipelineError::InvalidStructure {
1729 reason: "X509Data RetrievalMethod contains unsupported transforms",
1730 });
1731 }
1732 };
1733 let data = parse_x509_data_dispatch_with_budget_and_provider(
1734 node,
1735 &mut total_binary_len,
1736 &mut materialized_candidate_preflight_count,
1737 provider,
1738 budgets.resources,
1739 )
1740 .map_err(map_key_info_parse_error)?;
1741 materialized.push(super::parse::KeyInfoSource::X509Data(data));
1742 } else {
1743 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
1744 uri: resolved_uri,
1745 resource_type,
1746 transforms,
1747 });
1748 }
1749 }
1750 key_info.sources = materialized;
1751 Ok(outcome)
1752}
1753
1754fn select_retrieved_x509_data_root<'a, 'input>(
1755 target: Node<'a, 'input>,
1756 execution_budget: &TransformExecutionBudget,
1757) -> Result<Node<'a, 'input>, SignatureVerificationPipelineError> {
1758 let context_nodes = NodeSet::ensure_subtree_materialization_fits_with_budget(
1763 target,
1764 false,
1765 execution_budget.node_set_materialization(),
1766 )
1767 .map_err(ReferenceProcessingError::Transform)?;
1768 execution_budget
1769 .validate_xpath_context_evaluations(context_nodes)
1770 .map_err(ReferenceProcessingError::Transform)?;
1771 execution_budget
1772 .charge_xpath_work(context_nodes)
1773 .map_err(ReferenceProcessingError::Transform)?;
1774 execution_budget
1775 .charge_node_filter_work(context_nodes)
1776 .map_err(ReferenceProcessingError::Transform)?;
1777 let mut root = None;
1778 for candidate in target.descendants() {
1779 if !candidate.is_element()
1780 || candidate.tag_name().namespace() != Some(XMLDSIG_NS)
1781 || candidate.tag_name().name() != "X509Data"
1782 {
1783 continue;
1784 }
1785 if root.replace(candidate).is_some() {
1786 return Err(SignatureVerificationPipelineError::InvalidStructure {
1787 reason: "X509Data RetrievalMethod selected multiple X509Data elements",
1788 });
1789 }
1790 }
1791 root.ok_or(SignatureVerificationPipelineError::InvalidStructure {
1792 reason: "X509Data RetrievalMethod selected no X509Data element",
1793 })
1794}
1795
1796#[cfg(test)]
1797fn materialize_retrieval_methods(
1798 key_info: &mut KeyInfo,
1799 resolver: &UriReferenceResolver<'_>,
1800 allowed_uri_types: UriTypeSet,
1801 allowed_transforms: Option<&HashSet<String>>,
1802 provider: &dyn crate::provider::CryptoProvider,
1803) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
1804 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
1805 let execution_budget = TransformExecutionBudget::default();
1806 let resources = crate::policy::ResourcePolicy::default();
1807 let mut budgets = RetrievalMaterializationBudgets {
1808 xpath_parse: &mut xpath_parse_budget,
1809 execution: &execution_budget,
1810 resources: &resources,
1811 };
1812 materialize_retrieval_methods_with_budgets(
1813 key_info,
1814 resolver,
1815 allowed_uri_types,
1816 allowed_transforms,
1817 provider,
1818 &mut budgets,
1819 )
1820}
1821
1822fn existing_x509_binary_len(
1823 key_info: &KeyInfo,
1824) -> Result<usize, SignatureVerificationPipelineError> {
1825 let mut total = 0usize;
1826 for source in &key_info.sources {
1827 if let super::parse::KeyInfoSource::X509Data(info) = source {
1828 for len in info
1829 .certificates
1830 .iter()
1831 .chain(&info.skis)
1832 .chain(&info.crls)
1833 .map(Vec::len)
1834 .chain(info.digests.iter().map(|(_, digest)| digest.len()))
1835 {
1836 add_retrieval_binary_usage(&mut total, len)?;
1837 }
1838 }
1839 }
1840 Ok(total)
1841}
1842
1843fn add_retrieval_binary_usage(
1844 total: &mut usize,
1845 delta: usize,
1846) -> Result<(), SignatureVerificationPipelineError> {
1847 *total =
1848 total
1849 .checked_add(delta)
1850 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
1851 reason: "RetrievalMethod X509Data binary length overflow",
1852 })?;
1853 if *total > MAX_X509_DATA_TOTAL_BINARY_LEN {
1854 return Err(SignatureVerificationPipelineError::InvalidStructure {
1855 reason: "RetrievalMethod X509Data exceeds maximum aggregate binary length",
1856 });
1857 }
1858 Ok(())
1859}
1860
1861fn manifest_reference_failure_reason(
1862 error: ReferenceProcessingError,
1863 ref_index: usize,
1864) -> FailureReason {
1865 match error.into_policy_violation() {
1866 Ok(_) => FailureReason::ReferencePolicyViolation { ref_index },
1867 Err(_) => FailureReason::ReferenceProcessingFailure { ref_index },
1868 }
1869}
1870
1871fn process_manifest_references(
1872 signature_node: Node<'_, '_>,
1873 resolver: &UriReferenceResolver<'_>,
1874 ctx: &VerifyContext<'_>,
1875 signed_info_reference_nodes: &HashSet<NodeId>,
1876 remaining_reference_capacity: usize,
1877 execution: &ReferenceExecutionContext<'_>,
1878 xpath_parse_budget: &mut XPathSignatureParseBudget,
1879) -> Result<Vec<ReferenceResult>, SignatureVerificationPipelineError> {
1880 let mut authenticated_nodes = signed_info_reference_nodes.clone();
1881 let mut processed_manifests = HashSet::new();
1882 let mut remaining_reference_capacity = remaining_reference_capacity;
1883 let mut next_reference_index = 0usize;
1884 let mut results = Vec::new();
1885 loop {
1886 let parsed = parse_manifest_references(
1887 signature_node,
1888 &authenticated_nodes,
1889 &mut processed_manifests,
1890 &mut remaining_reference_capacity,
1891 &mut next_reference_index,
1892 xpath_parse_budget,
1893 ctx.allowed_transform_uris(),
1894 )?;
1895 let manifest_references = parsed.references;
1896 results.extend(parsed.invalid_results);
1897 if manifest_references.is_empty() {
1898 break;
1899 }
1900 results.reserve(manifest_references.len());
1901 for (index, reference, reference_node_id) in &manifest_references {
1902 let result = if execution.transform_budget.remaining_c14n_output() == 0
1903 || reference.transforms.len() > ctx.policy.resources.max_transforms_per_reference
1904 || ctx
1905 .policy
1906 .digest_algorithms
1907 .as_ref()
1908 .is_some_and(|allowed| !allowed.contains(&reference.digest_method))
1909 {
1910 manifest_reference_invalid_result(
1911 reference,
1912 *index,
1913 FailureReason::ReferencePolicyViolation { ref_index: *index },
1914 )
1915 } else {
1916 match enforce_reference_policies(
1917 std::slice::from_ref(reference),
1918 ctx.policy.uris.references,
1919 ctx.allowed_transform_uris(),
1920 ) {
1921 Ok(()) => process_reference_with_options(
1922 reference,
1923 resolver,
1924 signature_node,
1925 ReferenceSet::Manifest,
1926 *index,
1927 resolver.node_for_node_id(*reference_node_id),
1928 execution,
1929 )
1930 .unwrap_or_else(|error| {
1931 manifest_reference_invalid_result(
1932 reference,
1933 *index,
1934 manifest_reference_failure_reason(error, *index),
1935 )
1936 }),
1937 Err(SignatureVerificationPipelineError::Policy(_)) => {
1938 manifest_reference_invalid_result(
1939 reference,
1940 *index,
1941 FailureReason::ReferencePolicyViolation { ref_index: *index },
1942 )
1943 }
1944 Err(_) => manifest_reference_invalid_result(
1945 reference,
1946 *index,
1947 FailureReason::ReferenceProcessingFailure { ref_index: *index },
1948 ),
1949 }
1950 };
1951 if result.status == DsigStatus::Valid
1952 && reference
1953 .transforms
1954 .iter()
1955 .all(transform_preserves_manifest_structure)
1956 && let Ok(Some(target_id)) = reference.uri.as_deref().map_or_else(
1957 || Ok(None),
1958 |uri| resolver.node_id_for_same_document_reference(uri),
1959 )
1960 {
1961 authenticated_nodes.insert(target_id);
1964 }
1965 results.push(result);
1966 }
1967 }
1968 results.sort_by_key(|result| result.reference_index);
1969 Ok(results)
1970}
1971
1972fn manifest_reference_invalid_result(
1973 reference: &Reference,
1974 index: usize,
1975 reason: FailureReason,
1976) -> ReferenceResult {
1977 ReferenceResult {
1978 reference_set: ReferenceSet::Manifest,
1979 reference_index: index,
1980 uri: reference
1981 .uri
1982 .clone()
1983 .unwrap_or_else(|| "<omitted>".to_owned()),
1984 digest_algorithm: reference.digest_method,
1985 status: DsigStatus::Invalid(reason),
1986 pre_digest_data: None,
1987 }
1988}
1989
1990fn parse_manifest_references(
1991 signature_node: Node<'_, '_>,
1992 authenticated_nodes: &HashSet<NodeId>,
1993 processed_manifests: &mut HashSet<NodeId>,
1994 remaining_reference_capacity: &mut usize,
1995 next_reference_index: &mut usize,
1996 xpath_parse_budget: &mut XPathSignatureParseBudget,
1997 allowed_transforms: Option<&HashSet<String>>,
1998) -> Result<ParsedManifestReferences, SignatureVerificationPipelineError> {
1999 let mut references = Vec::new();
2000 let mut invalid = Vec::new();
2001 for object_node in signature_node.children().filter(|node| {
2002 node.is_element()
2003 && node.tag_name().namespace() == Some(XMLDSIG_NS)
2004 && node.tag_name().name() == "Object"
2005 }) {
2006 let object_is_signed = authenticated_nodes.contains(&object_node.id());
2007 for manifest_node in object_node.children().filter(|node| {
2008 node.is_element()
2009 && node.tag_name().namespace() == Some(XMLDSIG_NS)
2010 && node.tag_name().name() == "Manifest"
2011 }) {
2012 let manifest_is_signed = authenticated_nodes.contains(&manifest_node.id());
2013 if !object_is_signed && !manifest_is_signed {
2016 continue;
2017 }
2018 if !processed_manifests.insert(manifest_node.id()) {
2019 continue;
2020 }
2021 let mut manifest_children = Vec::new();
2022 for child in manifest_node.children() {
2023 if child.is_text()
2024 && child.text().is_some_and(|text| {
2025 text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
2026 })
2027 {
2028 return Err(SignatureVerificationPipelineError::InvalidStructure {
2029 reason: "Manifest contains non-whitespace mixed content",
2030 });
2031 }
2032 if child.is_element() {
2033 manifest_children.push(child);
2034 }
2035 }
2036 if manifest_children.is_empty() {
2037 return Err(SignatureVerificationPipelineError::InvalidStructure {
2038 reason: "Manifest must contain at least one ds:Reference element child",
2039 });
2040 }
2041 for child in manifest_children {
2042 if child.tag_name().namespace() != Some(XMLDSIG_NS)
2043 || child.tag_name().name() != "Reference"
2044 {
2045 return Err(SignatureVerificationPipelineError::InvalidStructure {
2046 reason: "Manifest must contain only ds:Reference element children",
2047 });
2048 }
2049 if *remaining_reference_capacity == 0 {
2050 return Err(SignatureVerificationPipelineError::InvalidStructure {
2051 reason: "signed Manifests exceed the per-signature Reference limit",
2052 });
2053 }
2054 *remaining_reference_capacity -= 1;
2055 let reference_index = *next_reference_index;
2056 *next_reference_index += 1;
2057 match parse_reference_with_xpath_budget(child, xpath_parse_budget) {
2058 Ok(reference) => references.push((reference_index, reference, child.id())),
2059 Err(ParseError::Transform(super::TransformError::UnsupportedTransform(
2060 uri,
2061 ))) => {
2062 let digest_algorithm =
2063 reference_digest_method(child).map_err(map_manifest_parse_error)?;
2064 let reason =
2065 if allowed_transforms.is_some_and(|allowed| !allowed.contains(&uri)) {
2066 FailureReason::ReferencePolicyViolation {
2067 ref_index: reference_index,
2068 }
2069 } else {
2070 FailureReason::ReferenceProcessingFailure {
2071 ref_index: reference_index,
2072 }
2073 };
2074 invalid.push(ReferenceResult {
2075 reference_set: ReferenceSet::Manifest,
2076 reference_index,
2077 uri: child.attribute("URI").unwrap_or("<omitted>").to_owned(),
2078 digest_algorithm,
2079 status: DsigStatus::Invalid(reason),
2080 pre_digest_data: None,
2081 });
2082 }
2083 Err(error) => return Err(map_manifest_parse_error(error)),
2084 }
2085 }
2086 }
2087 }
2088 Ok(ParsedManifestReferences {
2089 references,
2090 invalid_results: invalid,
2091 })
2092}
2093
2094struct ParsedManifestReferences {
2095 references: Vec<(usize, Reference, NodeId)>,
2096 invalid_results: Vec<ReferenceResult>,
2097}
2098
2099fn collect_authenticated_signed_info_reference_nodes(
2100 references: &[Reference],
2101 resolver: &UriReferenceResolver<'_>,
2102) -> HashSet<NodeId> {
2103 references
2104 .iter()
2105 .filter(|reference| {
2109 reference
2110 .transforms
2111 .iter()
2112 .all(transform_preserves_manifest_structure)
2113 })
2114 .filter_map(|reference| reference.uri.as_deref())
2115 .filter_map(|uri| {
2116 resolver
2117 .node_id_for_same_document_reference(uri)
2118 .ok()
2119 .flatten()
2120 })
2121 .collect()
2122}
2123
2124fn transform_preserves_manifest_structure(transform: &Transform) -> bool {
2125 match transform {
2126 Transform::C14n(_) => true,
2127 Transform::Enveloped
2132 | Transform::XpathExcludeAllSignatures
2133 | Transform::XPath(_)
2134 | Transform::XPathFilter2(_)
2135 | Transform::Base64Decode => false,
2136 }
2137}
2138
2139enum ResolvedVerifyingKey<'a> {
2140 Borrowed(&'a dyn VerifyingKey),
2141 Owned(Box<dyn VerifyingKey + 'a>),
2142}
2143
2144impl ResolvedVerifyingKey<'_> {
2145 fn as_ref(&self) -> &dyn VerifyingKey {
2146 match self {
2147 Self::Borrowed(key) => *key,
2148 Self::Owned(key) => key.as_ref(),
2149 }
2150 }
2151}
2152
2153fn resolve_verifying_key<'k>(
2154 ctx: &VerifyContext<'k>,
2155 key_info: Option<&KeyInfo>,
2156 algorithm: SignatureAlgorithm,
2157) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
2158 if let Some(key) = ctx.key {
2159 if !ctx.policy.key_sources.preset_key {
2160 return Err(crate::policy::PolicyViolation::KeyTrust {
2161 reason: "pre-resolved verification keys are disabled",
2162 }
2163 .into());
2164 }
2165 require_verifying_key_candidate_capacity(&ctx.policy)?;
2166 return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
2167 }
2168 if let Some(resolver) = ctx.key_resolver {
2169 require_verifying_key_candidate_capacity(&ctx.policy)?;
2170 let resolved = resolver.resolve_with_policy_and_provider(
2171 key_info,
2172 algorithm,
2173 &ctx.policy,
2174 ctx.provider,
2175 )?;
2176 return Ok(resolved.map(ResolvedVerifyingKey::Owned));
2177 }
2178 Ok(None)
2179}
2180
2181fn require_verifying_key_candidate_capacity(
2182 policy: &crate::policy::VerificationPolicy,
2183) -> Result<(), SignatureVerificationPipelineError> {
2184 policy
2185 .resources
2186 .validate_key_candidates(1)
2187 .map_err(Into::into)
2188}
2189
2190fn enforce_reference_policies(
2191 references: &[Reference],
2192 allowed_uri_types: UriTypeSet,
2193 allowed_transforms: Option<&HashSet<String>>,
2194) -> Result<(), SignatureVerificationPipelineError> {
2195 for reference in references {
2196 let uri = reference
2197 .uri
2198 .as_deref()
2199 .ok_or(SignatureVerificationPipelineError::Reference(
2200 ReferenceProcessingError::MissingUri,
2201 ))?;
2202 if !allowed_uri_types.allows(uri) {
2203 return Err(crate::policy::PolicyViolation::Uri {
2204 operation: "verification",
2205 reason: "reference URI class is not permitted",
2206 }
2207 .into());
2208 }
2209
2210 if let Some(allowed) = allowed_transforms {
2211 for transform in &reference.transforms {
2212 let transform_uri = transform.algorithm_uri();
2213 enforce_transform_allowed(Some(allowed), transform_uri)?;
2214 }
2215
2216 let produces_binary = transform_chain_produces_binary(
2221 classify_uri(uri) == UriClass::External,
2222 &reference.transforms,
2223 );
2224 if !produces_binary {
2225 enforce_transform_allowed(Some(allowed), DEFAULT_IMPLICIT_C14N_URI)?;
2226 }
2227 }
2228 }
2229 Ok(())
2230}
2231
2232fn enforce_transform_allowed(
2233 allowed_transforms: Option<&HashSet<String>>,
2234 algorithm: &str,
2235) -> Result<(), SignatureVerificationPipelineError> {
2236 if allowed_transforms.is_some_and(|allowed| !allowed.contains(algorithm)) {
2237 return Err(crate::policy::PolicyViolation::Algorithm {
2238 operation: "verification transform",
2239 algorithm: algorithm.to_owned(),
2240 }
2241 .into());
2242 }
2243 Ok(())
2244}
2245
2246#[derive(Debug, Clone, Copy)]
2247pub(super) struct SignatureChildNodes<'a, 'input> {
2248 signed_info_node: Node<'a, 'input>,
2249 signature_value_node: Node<'a, 'input>,
2250 key_info_node: Option<Node<'a, 'input>>,
2251}
2252
2253pub(super) fn parse_signature_children<'a, 'input>(
2254 signature_node: Node<'a, 'input>,
2255) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
2256 let mut signed_info_node: Option<Node<'_, '_>> = None;
2257 let mut signature_value_node: Option<Node<'_, '_>> = None;
2258 let mut key_info_node: Option<Node<'_, '_>> = None;
2259 let mut signed_info_index: Option<usize> = None;
2260 let mut signature_value_index: Option<usize> = None;
2261 let mut key_info_index: Option<usize> = None;
2262 let mut first_unexpected_dsig_index: Option<usize> = None;
2263
2264 let mut element_index = 0usize;
2265 for child in signature_node.children() {
2266 if child.is_text() {
2267 if child
2268 .text()
2269 .is_some_and(|text| !is_xml_whitespace_only(text))
2270 {
2271 return Err(SignatureVerificationPipelineError::InvalidStructure {
2272 reason: "Signature must not contain non-whitespace mixed content",
2273 });
2274 }
2275 continue;
2276 }
2277 if !child.is_element() {
2278 continue;
2279 }
2280
2281 element_index += 1;
2282 if child.tag_name().namespace() != Some(XMLDSIG_NS) {
2283 return Err(SignatureVerificationPipelineError::InvalidStructure {
2284 reason: "Signature must contain only XMLDSIG element children",
2285 });
2286 }
2287 match child.tag_name().name() {
2288 "SignedInfo" => {
2289 if signed_info_node.is_some() {
2290 return Err(SignatureVerificationPipelineError::InvalidStructure {
2291 reason: "SignedInfo must appear exactly once under Signature",
2292 });
2293 }
2294 signed_info_node = Some(child);
2295 signed_info_index = Some(element_index);
2296 }
2297 "SignatureValue" => {
2298 if signature_value_node.is_some() {
2299 return Err(SignatureVerificationPipelineError::InvalidStructure {
2300 reason: "SignatureValue must appear exactly once under Signature",
2301 });
2302 }
2303 signature_value_node = Some(child);
2304 signature_value_index = Some(element_index);
2305 }
2306 "KeyInfo" => {
2307 if key_info_node.is_some() {
2308 return Err(SignatureVerificationPipelineError::InvalidStructure {
2309 reason: "KeyInfo must appear at most once under Signature",
2310 });
2311 }
2312 key_info_node = Some(child);
2313 key_info_index = Some(element_index);
2314 }
2315 "Object" => {
2316 }
2319 _ => {
2320 if first_unexpected_dsig_index.is_none() {
2321 first_unexpected_dsig_index = Some(element_index);
2322 }
2323 }
2324 }
2325 }
2326
2327 let signed_info_node =
2328 signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
2329 element: "SignedInfo",
2330 })?;
2331 let signature_value_node =
2332 signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
2333 element: "SignatureValue",
2334 })?;
2335 if signed_info_index != Some(1) {
2336 return Err(SignatureVerificationPipelineError::InvalidStructure {
2337 reason: "SignedInfo must be the first element child of Signature",
2338 });
2339 }
2340 if signature_value_index != Some(2) {
2341 return Err(SignatureVerificationPipelineError::InvalidStructure {
2342 reason: "SignatureValue must be the second element child of Signature",
2343 });
2344 }
2345 if let Some(index) = key_info_index
2346 && index != 3
2347 {
2348 return Err(SignatureVerificationPipelineError::InvalidStructure {
2349 reason: "KeyInfo must be the third element child of Signature when present",
2350 });
2351 }
2352
2353 let allowed_prefix_end = key_info_index.unwrap_or(2);
2354 if let Some(unexpected_index) = first_unexpected_dsig_index {
2355 return Err(SignatureVerificationPipelineError::InvalidStructure {
2356 reason: if unexpected_index > allowed_prefix_end {
2357 "After SignedInfo, SignatureValue, and optional KeyInfo, Signature may contain only Object elements"
2358 } else {
2359 "Signature may contain SignedInfo first, SignatureValue second, optional KeyInfo third, and Object elements thereafter"
2360 },
2361 });
2362 }
2363
2364 Ok(SignatureChildNodes {
2365 signed_info_node,
2366 signature_value_node,
2367 key_info_node,
2368 })
2369}
2370
2371fn decode_signature_value(
2372 signature_value_node: Node<'_, '_>,
2373) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
2374 if signature_value_node
2375 .children()
2376 .any(|child| child.is_element())
2377 {
2378 return Err(SignatureVerificationPipelineError::InvalidStructure {
2379 reason: "SignatureValue must not contain element children",
2380 });
2381 }
2382
2383 let mut normalized = Vec::new();
2384 let mut raw_text_len = 0usize;
2385 for child in signature_value_node
2386 .children()
2387 .filter(|child| child.is_text())
2388 {
2389 if let Some(text) = child.text() {
2390 push_normalized_signature_text(text, &mut raw_text_len, &mut normalized)?;
2391 }
2392 }
2393
2394 Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
2395}
2396
2397fn push_normalized_signature_text(
2398 text: &str,
2399 raw_text_len: &mut usize,
2400 normalized: &mut Vec<u8>,
2401) -> Result<(), SignatureVerificationPipelineError> {
2402 if raw_text_len.saturating_add(text.len()) > MAX_SIGNATURE_VALUE_TEXT_LEN {
2403 return Err(SignatureVerificationPipelineError::InvalidStructure {
2404 reason: "SignatureValue exceeds maximum allowed text length",
2405 });
2406 }
2407 *raw_text_len = raw_text_len.saturating_add(text.len());
2408
2409 normalize_xml_base64_bytes(text.as_bytes(), normalized, |_| true).map_err(|err| {
2410 SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(
2411 err.normalized_offset,
2412 err.invalid_byte,
2413 ))
2414 })?;
2415 if normalized.len() > MAX_SIGNATURE_VALUE_LEN {
2416 return Err(SignatureVerificationPipelineError::InvalidStructure {
2417 reason: "SignatureValue exceeds maximum allowed length",
2418 });
2419 }
2420
2421 Ok(())
2422}
2423
2424fn verify_with_algorithm(
2425 algorithm: SignatureAlgorithm,
2426 public_key_pem: &str,
2427 signed_data: &[u8],
2428 signature_value: &[u8],
2429) -> Result<bool, SignatureVerificationPipelineError> {
2430 match algorithm {
2431 SignatureAlgorithm::DsaSha1 => {
2432 let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
2433 .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
2434 if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
2435 return Err(SignatureVerificationError::InvalidKeyPem.into());
2436 }
2437 Ok(verify_dsa_signature_spki(
2438 algorithm,
2439 &pem.contents,
2440 signed_data,
2441 signature_value,
2442 )?)
2443 }
2444 SignatureAlgorithm::HmacSha1 => Err(SignatureVerificationError::UnsupportedAlgorithm {
2445 uri: algorithm.uri().to_string(),
2446 }
2447 .into()),
2448 SignatureAlgorithm::RsaSha1
2449 | SignatureAlgorithm::RsaSha256
2450 | SignatureAlgorithm::RsaSha384
2451 | SignatureAlgorithm::RsaSha512 => Ok(verify_rsa_signature_pem(
2452 algorithm,
2453 public_key_pem,
2454 signed_data,
2455 signature_value,
2456 )?),
2457 SignatureAlgorithm::EcdsaSha256 | SignatureAlgorithm::EcdsaSha384 => {
2458 match verify_ecdsa_signature_pem(
2462 algorithm,
2463 public_key_pem,
2464 signed_data,
2465 signature_value,
2466 ) {
2467 Ok(valid) => Ok(valid),
2468 Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
2469 Err(error) => Err(error.into()),
2470 }
2471 }
2472 }
2473}
2474
2475#[cfg(test)]
2476#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
2477mod tests {
2478 use super::*;
2479 use crate::c14n::C14nAlgorithm;
2480 use crate::xmldsig::TransformError;
2481 use crate::xmldsig::digest::DigestAlgorithm;
2482 use crate::xmldsig::parse::{Reference, parse_signed_info};
2483 use crate::xmldsig::transforms::Transform;
2484 use crate::xmldsig::uri::UriReferenceResolver;
2485 use base64::Engine;
2486 use roxmltree::Document;
2487
2488 fn make_reference(
2492 uri: &str,
2493 transforms: Vec<Transform>,
2494 digest_method: DigestAlgorithm,
2495 digest_value: Vec<u8>,
2496 ) -> Reference {
2497 Reference {
2498 uri: Some(uri.to_string()),
2499 id: None,
2500 ref_type: None,
2501 transforms,
2502 digest_method,
2503 digest_value,
2504 }
2505 }
2506
2507 #[test]
2508 fn reference_resolution_uses_each_elements_effective_xml_base() {
2509 let first = b"first payload";
2512 let second = b"second payload";
2513 let first_digest = base64::engine::general_purpose::STANDARD
2514 .encode(compute_digest(DigestAlgorithm::Sha256, first));
2515 let second_digest = base64::engine::general_purpose::STANDARD
2516 .encode(compute_digest(DigestAlgorithm::Sha256, second));
2517 let xml = format!(
2518 r#"<root xml:base="https://example.test/base/" xmlns:ds="{XMLDSIG_NS}">
2519 <ds:Signature><ds:SignedInfo>
2520 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2521 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2522 <ds:Reference xml:base="one/" URI="payload.bin">
2523 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2524 <ds:DigestValue>{first_digest}</ds:DigestValue>
2525 </ds:Reference>
2526 <ds:Reference xml:base="../two/" URI="payload.bin">
2527 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2528 <ds:DigestValue>{second_digest}</ds:DigestValue>
2529 </ds:Reference>
2530 </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>
2531 </root>"#
2532 );
2533 let document = Document::parse(&xml).unwrap();
2534 let signature = document
2535 .descendants()
2536 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
2537 .unwrap();
2538 let signed_info_node = signature
2539 .children()
2540 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
2541 .unwrap();
2542 let signed_info = parse_signed_info(signed_info_node).unwrap();
2543 let resources = HashMap::from([
2544 (
2545 "https://example.test/base/one/payload.bin".into(),
2546 first.to_vec(),
2547 ),
2548 (
2549 "https://example.test/two/payload.bin".into(),
2550 second.to_vec(),
2551 ),
2552 ]);
2553 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
2554
2555 let result = process_all_references(&signed_info.references, &resolver, signature, false)
2556 .expect("each Reference should resolve against its own effective base");
2557
2558 assert!(result.all_valid());
2559 }
2560
2561 #[test]
2562 fn internal_dtd_opt_in_applies_to_detached_xml_transforms() {
2563 let detached = b"<!DOCTYPE payload [<!ELEMENT payload (#PCDATA)>]><payload>ok</payload>";
2566 let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
2567 DigestAlgorithm::Sha256,
2568 b"<payload>ok</payload>",
2569 ));
2570 let xml = format!(
2571 r#"<root xmlns:ds="{XMLDSIG_NS}">
2572 <ds:Signature>
2573 <ds:SignedInfo>
2574 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2575 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2576 <ds:Reference URI="urn:detached-dtd">
2577 <ds:Transforms>
2578 <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
2579 </ds:Transforms>
2580 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2581 <ds:DigestValue>{digest}</ds:DigestValue>
2582 </ds:Reference>
2583 </ds:SignedInfo>
2584 <ds:SignatureValue>AQ==</ds:SignatureValue>
2585 </ds:Signature>
2586</root>"#
2587 );
2588 let resources = HashMap::from([("urn:detached-dtd".to_owned(), detached.to_vec())]);
2589 let key = AcceptingKey;
2590
2591 let default_error = VerifyContext::new()
2592 .key(&key)
2593 .allowed_uri_types(UriTypeSet::ALL)
2594 .external_resources(&resources)
2595 .verify(&xml)
2596 .expect_err("internal DTD parsing must remain disabled by default");
2597 assert!(matches!(
2598 default_error,
2599 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
2600 crate::xmldsig::TransformError::XmlParse(_)
2601 ))
2602 ));
2603
2604 let result = VerifyContext::new()
2605 .key(&key)
2606 .allowed_uri_types(UriTypeSet::ALL)
2607 .external_resources(&resources)
2608 .allow_internal_dtd(true)
2609 .verify(&xml)
2610 .expect("the explicit DTD opt-in must cover detached XML transforms");
2611
2612 assert_eq!(result.status, DsigStatus::Valid);
2613
2614 let external_entity = br#"<!DOCTYPE payload [
2615 <!ENTITY ext SYSTEM "file:///etc/passwd">
2616 ]><payload>&ext;</payload>"#;
2617 let external_entity_resources =
2618 HashMap::from([("urn:detached-dtd".to_owned(), external_entity.to_vec())]);
2619 let external_entity_error = VerifyContext::new()
2620 .key(&key)
2621 .allowed_uri_types(UriTypeSet::ALL)
2622 .external_resources(&external_entity_resources)
2623 .allow_internal_dtd(true)
2624 .verify(&xml)
2625 .expect_err("the internal-DTD opt-in must not resolve external entities");
2626 assert!(matches!(
2627 external_entity_error,
2628 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
2629 crate::xmldsig::TransformError::XmlParse(_)
2630 ))
2631 ));
2632 }
2633
2634 #[test]
2635 fn owned_verification_revalidates_internal_dtd_provenance() {
2636 let document = XmlDocument::parse_with_settings(
2639 "<!DOCTYPE root [<!ENTITY value \"ok\">]><root>&value;</root>".into(),
2640 DocumentParseSettings::new(
2641 true,
2642 crate::hard_limits::XML_DOCUMENT_NODE_CEILING,
2643 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
2644 ),
2645 )
2646 .expect("explicitly permitted DTD fixture must parse");
2647
2648 assert!(matches!(
2649 VerifyContext::new().verify_document(&document),
2650 Err(DsigError::Policy(
2651 crate::policy::PolicyViolation::XmlInput {
2652 reason: "owned document requires internal DTD support"
2653 }
2654 ))
2655 ));
2656 assert!(!matches!(
2657 VerifyContext::new()
2658 .allow_internal_dtd(true)
2659 .verify_document(&document),
2660 Err(DsigError::Policy(
2661 crate::policy::PolicyViolation::XmlInput { .. }
2662 ))
2663 ));
2664 }
2665
2666 #[test]
2667 fn verification_policy_bounds_reference_canonicalization() {
2668 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
2671 let xml = format!(
2672 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>"#,
2673 "payload".repeat(16)
2674 );
2675 let policy = crate::policy::VerificationPolicy {
2676 resources: crate::policy::ResourcePolicy {
2677 max_canonicalized_bytes: 64,
2678 ..crate::policy::ResourcePolicy::default()
2679 },
2680 ..crate::policy::VerificationPolicy::default()
2681 };
2682
2683 let error = VerifyContext::new()
2684 .key(&AcceptingKey)
2685 .policy(policy)
2686 .verify(&xml)
2687 .expect_err("reference canonicalization must consume the policy budget");
2688
2689 assert!(
2690 matches!(
2691 error,
2692 SignatureVerificationPipelineError::Policy(
2693 crate::policy::PolicyViolation::ResourceLimit {
2694 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
2695 maximum: 64,
2696 ..
2697 }
2698 )
2699 ),
2700 "unexpected error: {error:?}"
2701 );
2702 }
2703
2704 #[test]
2705 fn verification_policy_bounds_document_bytes_before_parsing() {
2706 let xml = format!("<root>{}</root>", "x".repeat(1_024));
2709 let policy = crate::policy::VerificationPolicy {
2710 resources: crate::policy::ResourcePolicy {
2711 max_xml_document_bytes: xml.len() - 1,
2712 ..crate::policy::ResourcePolicy::default()
2713 },
2714 ..crate::policy::VerificationPolicy::default()
2715 };
2716
2717 assert!(matches!(
2718 VerifyContext::new().policy(policy).verify(&xml),
2719 Err(SignatureVerificationPipelineError::Policy(
2720 crate::policy::PolicyViolation::ResourceLimit {
2721 resource: crate::policy::resource_name::XML_DOCUMENT,
2722 maximum,
2723 actual,
2724 }
2725 )) if maximum == xml.len() - 1 && actual == xml.len()
2726 ));
2727 }
2728
2729 #[test]
2730 fn verification_entry_points_enforce_policy_depth() {
2731 let xml = "<root><child><leaf/></child></root>";
2734 let policy = crate::policy::VerificationPolicy {
2735 resources: crate::policy::ResourcePolicy {
2736 max_xml_depth: 2,
2737 ..crate::policy::ResourcePolicy::default()
2738 },
2739 ..crate::policy::VerificationPolicy::default()
2740 };
2741 let document = XmlDocument::parse(xml).expect("wide retained fixture must parse");
2742
2743 assert!(matches!(
2744 VerifyContext::new().policy(policy.clone()).verify(xml),
2745 Err(DsigError::Policy(
2746 crate::policy::PolicyViolation::ResourceLimit {
2747 resource: crate::policy::resource_name::XML_DEPTH,
2748 maximum: 2,
2749 actual: 3,
2750 }
2751 ))
2752 ));
2753 assert!(matches!(
2754 VerifyContext::new()
2755 .policy(policy)
2756 .verify_document(&document),
2757 Err(DsigError::Policy(
2758 crate::policy::PolicyViolation::ResourceLimit {
2759 resource: crate::policy::resource_name::XML_DEPTH,
2760 maximum: 2,
2761 actual: 3,
2762 }
2763 ))
2764 ));
2765 }
2766
2767 #[test]
2768 fn verification_policy_bounds_base64_transform_input() {
2769 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
2772 let xml = format!(
2773 r##"<root xmlns:ds="{XMLDSIG_NS}"><payload ID="payload">QUJDRA==</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/2000/09/xmldsig#base64"/></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>"##
2774 );
2775 let policy = crate::policy::VerificationPolicy {
2776 resources: crate::policy::ResourcePolicy {
2777 max_base64_transform_input_bytes: 4,
2778 ..crate::policy::ResourcePolicy::default()
2779 },
2780 ..crate::policy::VerificationPolicy::default()
2781 };
2782
2783 let error = VerifyContext::new()
2784 .key(&AcceptingKey)
2785 .policy(policy)
2786 .verify(&xml)
2787 .expect_err("Base64 input must use the operation policy ceiling");
2788
2789 assert!(matches!(
2790 error,
2791 SignatureVerificationPipelineError::Policy(
2792 crate::policy::PolicyViolation::ResourceLimit {
2793 resource: crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
2794 maximum: 4,
2795 ..
2796 }
2797 )
2798 ));
2799 }
2800
2801 #[test]
2802 fn verification_policy_bounds_cumulative_base64_transform_output() {
2803 let first_digest = base64::engine::general_purpose::STANDARD
2806 .encode(compute_digest(DigestAlgorithm::Sha256, b"a"));
2807 let second_digest = base64::engine::general_purpose::STANDARD
2808 .encode(compute_digest(DigestAlgorithm::Sha256, b"b"));
2809 let xml = format!(
2810 r##"<root xmlns:ds="{XMLDSIG_NS}"><first ID="first">YQ==</first><second ID="second">Yg==</second><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="#first"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#base64"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{first_digest}</ds:DigestValue></ds:Reference><ds:Reference URI="#second"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#base64"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{second_digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"##
2811 );
2812 let policy = crate::policy::VerificationPolicy {
2813 resources: crate::policy::ResourcePolicy {
2814 max_base64_transform_input_bytes: 8,
2815 max_base64_transform_output_bytes: 1,
2816 ..crate::policy::ResourcePolicy::default()
2817 },
2818 ..crate::policy::VerificationPolicy::default()
2819 };
2820
2821 let error = VerifyContext::new()
2822 .key(&AcceptingKey)
2823 .policy(policy)
2824 .verify(&xml)
2825 .expect_err("references must share the Base64 output allowance");
2826
2827 assert!(matches!(
2828 error,
2829 SignatureVerificationPipelineError::Policy(
2830 crate::policy::PolicyViolation::ResourceLimit {
2831 resource: crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
2832 maximum: 1,
2833 actual: 2,
2834 }
2835 )
2836 ));
2837 }
2838
2839 #[test]
2840 fn verification_policy_bounds_xpath_source_before_compilation() {
2841 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
2844 let xml = format!(
2845 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=""><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>"#
2846 );
2847 let policy = crate::policy::VerificationPolicy {
2848 resources: crate::policy::ResourcePolicy {
2849 max_xpath_expression_bytes: 4,
2850 ..crate::policy::ResourcePolicy::default()
2851 },
2852 ..crate::policy::VerificationPolicy::default()
2853 };
2854
2855 let error = VerifyContext::new()
2856 .key(&AcceptingKey)
2857 .policy(policy)
2858 .verify(&xml)
2859 .expect_err("XPath source must use the operation policy ceiling");
2860
2861 assert!(matches!(
2862 error,
2863 SignatureVerificationPipelineError::Policy(
2864 crate::policy::PolicyViolation::ResourceLimit {
2865 resource: crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
2866 maximum: 4,
2867 ..
2868 }
2869 )
2870 ));
2871 }
2872
2873 #[test]
2874 fn verification_policy_shares_canonicalization_budget_with_signed_info() {
2875 let payload_text = "x".repeat(700);
2879 let canonical_payload = format!("<payload ID=\"payload\">{payload_text}</payload>");
2880 let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
2881 DigestAlgorithm::Sha256,
2882 canonical_payload.as_bytes(),
2883 ));
2884 let xml = format!(
2885 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>"##
2886 );
2887 let policy = crate::policy::VerificationPolicy {
2888 resources: crate::policy::ResourcePolicy {
2889 max_canonicalized_bytes: 1_024,
2890 ..crate::policy::ResourcePolicy::default()
2891 },
2892 ..crate::policy::VerificationPolicy::default()
2893 };
2894
2895 let error = VerifyContext::new()
2896 .key(&AcceptingKey)
2897 .policy(policy)
2898 .verify(&xml)
2899 .expect_err("SignedInfo must consume the remaining operation C14N budget");
2900
2901 assert!(
2902 matches!(
2903 &error,
2904 SignatureVerificationPipelineError::Policy(
2905 crate::policy::PolicyViolation::ResourceLimit {
2906 resource: "canonicalized bytes",
2907 maximum: 1_024,
2908 ..
2909 }
2910 )
2911 ),
2912 "unexpected error: {error:?}"
2913 );
2914 }
2915
2916 #[test]
2917 fn manifest_processing_stops_after_c14n_budget_exhaustion() {
2918 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
2921 let xml = format!(
2922 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>"##
2923 );
2924 let document = Document::parse(&xml).expect("test signature must parse");
2925 let signature = document
2926 .descendants()
2927 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
2928 .expect("test signature must contain Signature");
2929 let object = signature
2930 .children()
2931 .find(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
2932 .expect("test signature must contain Object");
2933 let resources = HashMap::from([("urn:small".to_owned(), b"small".to_vec())]);
2934 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
2935 let transform_budget = TransformExecutionBudget::with_c14n_limit(8);
2936 let canonicalized_data_budget = CanonicalizedDataBudget::default();
2937 let execution = ReferenceExecutionContext {
2938 store_pre_digest: false,
2939 transform_options: TransformOptions::default(),
2940 transform_budget: &transform_budget,
2941 canonicalized_data_budget: &canonicalized_data_budget,
2942 provider: crate::provider::default_provider(),
2943 };
2944 let ctx = VerifyContext::new()
2945 .allowed_uri_types(UriTypeSet::ALL)
2946 .external_resources(&resources);
2947 let authenticated = HashSet::from([object.id()]);
2948 let mut xpath_budget = XPathSignatureParseBudget::default();
2949
2950 let results = process_manifest_references(
2951 signature,
2952 &resolver,
2953 &ctx,
2954 &authenticated,
2955 2,
2956 &execution,
2957 &mut xpath_budget,
2958 )
2959 .expect("resource exhaustion is reported per Manifest reference");
2960
2961 assert_eq!(results.len(), 2);
2962 assert!(results.iter().all(|result| {
2963 matches!(
2964 result.status,
2965 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { .. })
2966 )
2967 }));
2968 }
2969
2970 #[test]
2971 fn verification_policy_bounds_detached_xml_nodes() {
2972 let detached = format!("<payload>{}</payload>", "<n/>".repeat(32));
2975 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
2976 let xml = format!(
2977 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>"#
2978 );
2979 let resources = HashMap::from([("urn:detached-nodes".to_owned(), detached.into_bytes())]);
2980 let policy = crate::policy::VerificationPolicy {
2981 uris: crate::policy::UriPolicy {
2982 references: UriTypeSet::ALL,
2983 ..crate::policy::UriPolicy::default()
2984 },
2985 resources: crate::policy::ResourcePolicy {
2986 max_xml_nodes: 24,
2987 ..crate::policy::ResourcePolicy::default()
2988 },
2989 ..crate::policy::VerificationPolicy::default()
2990 };
2991
2992 let error = VerifyContext::new()
2993 .key(&AcceptingKey)
2994 .policy(policy)
2995 .external_resources(&resources)
2996 .verify(&xml)
2997 .expect_err("detached XML must inherit the policy node ceiling");
2998
2999 assert!(
3000 matches!(
3001 error,
3002 SignatureVerificationPipelineError::Policy(
3003 crate::policy::PolicyViolation::ResourceLimit {
3004 resource: crate::policy::resource_name::XML_NODES,
3005 maximum: 24,
3006 ..
3007 }
3008 )
3009 ),
3010 "unexpected error: {error:?}"
3011 );
3012 }
3013
3014 #[test]
3015 fn query_only_reference_resolves_against_relative_xml_base() {
3016 let payload = b"query-selected payload";
3019 let digest = base64::engine::general_purpose::STANDARD
3020 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
3021 let xml = format!(
3022 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo>
3023 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3024 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3025 <ds:Reference xml:base="a/b?old" URI="?new">
3026 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3027 <ds:DigestValue>{digest}</ds:DigestValue>
3028 </ds:Reference>
3029 </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>"#
3030 );
3031 let document = Document::parse(&xml).unwrap();
3032 let signature = document.root_element();
3033 let signed_info_node = signature
3034 .children()
3035 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
3036 .unwrap();
3037 let signed_info = parse_signed_info(signed_info_node).unwrap();
3038 let resources = HashMap::from([("a/b?new".to_string(), payload.to_vec())]);
3039 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
3040
3041 let result = process_all_references(&signed_info.references, &resolver, signature, false)
3042 .expect("query-only URI must resolve against the complete relative base path");
3043
3044 assert!(result.all_valid());
3045 }
3046
3047 #[test]
3048 fn manifest_reference_resolution_uses_its_effective_xml_base() {
3049 let payload = b"manifest payload";
3052 let digest = base64::engine::general_purpose::STANDARD
3053 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
3054 let xml = format!(
3055 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
3056 <ds:Object><ds:Manifest xml:base="manifests/">
3057 <ds:Reference URI="payload.bin">
3058 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3059 <ds:DigestValue>{digest}</ds:DigestValue>
3060 </ds:Reference>
3061 </ds:Manifest></ds:Object>
3062 </ds:Signature>"#
3063 );
3064 let document = Document::parse(&xml).unwrap();
3065 let signature = document.root_element();
3066 let reference_node = signature
3067 .descendants()
3068 .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
3069 .unwrap();
3070 let reference = super::super::parse::parse_reference(reference_node).unwrap();
3071 let resources = HashMap::from([(
3072 "https://example.test/manifests/payload.bin".to_string(),
3073 payload.to_vec(),
3074 )]);
3075 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
3076
3077 let result = process_reference(
3078 &reference,
3079 &resolver,
3080 signature,
3081 ReferenceSet::Manifest,
3082 0,
3083 false,
3084 )
3085 .expect("Manifest Reference should inherit its own XML Base context");
3086
3087 assert_eq!(result.status, DsigStatus::Valid);
3088 }
3089
3090 #[test]
3091 fn manifest_reference_index_ignores_nested_manifest_descendants() {
3092 let payload = b"direct manifest payload";
3095 let digest = base64::engine::general_purpose::STANDARD
3096 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
3097 let xml = format!(
3098 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
3099 <ds:Object><wrapper><ds:Manifest xml:base="nested/">
3100 <ds:Reference URI="payload.bin">
3101 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3102 <ds:DigestValue>{digest}</ds:DigestValue>
3103 </ds:Reference>
3104 </ds:Manifest></wrapper></ds:Object>
3105 <ds:Object><ds:Manifest xml:base="direct/">
3106 <ds:Reference URI="payload.bin">
3107 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3108 <ds:DigestValue>{digest}</ds:DigestValue>
3109 </ds:Reference>
3110 </ds:Manifest></ds:Object>
3111 </ds:Signature>"#
3112 );
3113 let document = Document::parse(&xml).unwrap();
3114 let signature = document.root_element();
3115 let direct_reference_node = signature
3116 .children()
3117 .filter(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
3118 .nth(1)
3119 .unwrap()
3120 .children()
3121 .find(|node| node.has_tag_name((XMLDSIG_NS, "Manifest")))
3122 .unwrap()
3123 .children()
3124 .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
3125 .unwrap();
3126 let reference = super::super::parse::parse_reference(direct_reference_node).unwrap();
3127 let resources = HashMap::from([(
3128 "https://example.test/direct/payload.bin".to_string(),
3129 payload.to_vec(),
3130 )]);
3131 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
3132
3133 let result = process_reference(
3134 &reference,
3135 &resolver,
3136 signature,
3137 ReferenceSet::Manifest,
3138 0,
3139 false,
3140 )
3141 .expect("Manifest index must select the direct Object/Manifest reference");
3142
3143 assert_eq!(result.status, DsigStatus::Valid);
3144 }
3145
3146 struct RejectingKey;
3147
3148 impl VerifyingKey for RejectingKey {
3149 fn verify(
3150 &self,
3151 _algorithm: SignatureAlgorithm,
3152 _signed_data: &[u8],
3153 _signature_value: &[u8],
3154 ) -> Result<bool, SignatureVerificationPipelineError> {
3155 Ok(false)
3156 }
3157 }
3158
3159 struct AcceptingKey;
3160
3161 impl VerifyingKey for AcceptingKey {
3162 fn verify(
3163 &self,
3164 _algorithm: SignatureAlgorithm,
3165 _signed_data: &[u8],
3166 _signature_value: &[u8],
3167 ) -> Result<bool, SignatureVerificationPipelineError> {
3168 Ok(true)
3169 }
3170 }
3171
3172 struct PanicResolver;
3173
3174 impl KeyResolver for PanicResolver {
3175 fn resolve<'a>(
3176 &'a self,
3177 _key_info: Option<&KeyInfo>,
3178 _algorithm: SignatureAlgorithm,
3179 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
3180 {
3181 panic!("resolver should not be called when references already fail");
3182 }
3183 }
3184
3185 struct MissingKeyResolver;
3186
3187 impl KeyResolver for MissingKeyResolver {
3188 fn resolve<'a>(
3189 &'a self,
3190 _key_info: Option<&KeyInfo>,
3191 _algorithm: SignatureAlgorithm,
3192 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
3193 {
3194 Ok(None)
3195 }
3196 }
3197
3198 struct ConsumingKeyInfoResolver;
3199
3200 impl KeyResolver for ConsumingKeyInfoResolver {
3201 fn resolve<'a>(
3202 &'a self,
3203 _key_info: Option<&KeyInfo>,
3204 _algorithm: SignatureAlgorithm,
3205 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
3206 {
3207 Ok(None)
3208 }
3209
3210 fn consumes_document_key_info(&self) -> bool {
3211 true
3212 }
3213 }
3214
3215 struct FallbackKeyInfoResolver;
3216
3217 impl KeyResolver for FallbackKeyInfoResolver {
3218 fn resolve<'a>(
3219 &'a self,
3220 key_info: Option<&KeyInfo>,
3221 _algorithm: SignatureAlgorithm,
3222 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
3223 {
3224 let sources = &key_info.expect("KeyInfo must be parsed").sources;
3225 assert!(matches!(
3226 sources.as_slice(),
3227 [
3228 super::super::parse::KeyInfoSource::RetrievalMethod { .. },
3229 super::super::parse::KeyInfoSource::KeyName(name),
3230 ] if name == "fallback"
3231 ));
3232 Ok(Some(Box::new(AcceptingKey)))
3233 }
3234
3235 fn consumes_document_key_info(&self) -> bool {
3236 true
3237 }
3238 }
3239
3240 struct EarlyKeyInfoResolver;
3241
3242 impl KeyResolver for EarlyKeyInfoResolver {
3243 fn resolve<'a>(
3244 &'a self,
3245 key_info: Option<&KeyInfo>,
3246 _algorithm: SignatureAlgorithm,
3247 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
3248 {
3249 let sources = &key_info.expect("KeyInfo must be parsed").sources;
3250 assert!(matches!(
3251 sources.as_slice(),
3252 [
3253 super::super::parse::KeyInfoSource::KeyName(name),
3254 super::super::parse::KeyInfoSource::RetrievalMethod { .. },
3255 ] if name == "primary"
3256 ));
3257 Ok(Some(Box::new(AcceptingKey)))
3258 }
3259
3260 fn consumes_document_key_info(&self) -> bool {
3261 true
3262 }
3263 }
3264
3265 fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
3266 format!(
3267 r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3268 <ds:SignedInfo>
3269 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3270 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3271 <ds:Reference URI="{reference_uri}">
3272 {transforms_xml}
3273 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3274 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
3275 </ds:Reference>
3276 </ds:SignedInfo>
3277 <ds:SignatureValue>AQ==</ds:SignatureValue>
3278</ds:Signature>"#
3279 )
3280 }
3281
3282 fn signature_with_target_reference(signature_value_b64: &str) -> String {
3283 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3284 <target ID="target">payload</target>
3285 <ds:Signature>
3286 <ds:SignedInfo>
3287 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3288 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3289 <ds:Reference URI="#target">
3290 <ds:Transforms>
3291 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3292 </ds:Transforms>
3293 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3294 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
3295 </ds:Reference>
3296 </ds:SignedInfo>
3297 <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
3298 </ds:Signature>
3299</root>"##;
3300
3301 let doc = Document::parse(xml_template).unwrap();
3302 let sig_node = doc
3303 .descendants()
3304 .find(|node| node.is_element() && node.tag_name().name() == "Signature")
3305 .unwrap();
3306 let signed_info_node = sig_node
3307 .children()
3308 .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
3309 .unwrap();
3310 let signed_info = parse_signed_info(signed_info_node).unwrap();
3311 let reference = &signed_info.references[0];
3312 let resolver = UriReferenceResolver::new(&doc);
3313 let initial_data = resolver
3314 .dereference(reference.uri.as_deref().unwrap())
3315 .unwrap();
3316 let pre_digest =
3317 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
3318 .unwrap();
3319 let digest = compute_digest(reference.digest_method, &pre_digest);
3320 let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
3321 xml_template
3322 .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
3323 .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
3324 }
3325
3326 #[test]
3327 fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
3328 let xml = signature_with_target_reference("AQ==");
3329
3330 let result = VerifyContext::new()
3331 .verify(&xml)
3332 .expect("missing key config must be reported as verification status");
3333 assert!(
3334 matches!(
3335 result.status,
3336 DsigStatus::Invalid(FailureReason::KeyNotFound)
3337 ),
3338 "unexpected status: {:?}",
3339 result.status
3340 );
3341 }
3342
3343 #[test]
3344 fn verify_context_rejects_disallowed_uri() {
3345 let xml = minimal_signature_xml("http://example.com/external", "");
3346 let err = VerifyContext::new()
3347 .key(&RejectingKey)
3348 .verify(&xml)
3349 .expect_err("external URI should be rejected by default policy");
3350 assert!(matches!(
3351 err,
3352 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
3353 operation: "verification",
3354 ..
3355 })
3356 ));
3357 }
3358
3359 #[test]
3360 fn verify_context_bounds_effective_xml_base_components() {
3361 let mut xml = minimal_signature_xml("payload", "");
3364 for _ in 0..65 {
3365 xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
3366 }
3367 let resources = HashMap::new();
3368 let error = VerifyContext::new()
3369 .key(&AcceptingKey)
3370 .allowed_uri_types(UriTypeSet::ALL)
3371 .external_resources(&resources)
3372 .verify(&xml)
3373 .expect_err("XML Base component work must be bounded before lookup");
3374
3375 assert!(matches!(
3376 error,
3377 SignatureVerificationPipelineError::Policy(
3378 crate::policy::PolicyViolation::ResourceLimit {
3379 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
3380 maximum: 64,
3381 actual: 65,
3382 }
3383 )
3384 ));
3385 }
3386
3387 #[test]
3388 fn verify_context_bounds_cumulative_xml_base_resolution_bytes() {
3389 let mut xml = minimal_signature_xml("payload", "");
3392 for _ in 0..2 {
3393 xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
3394 }
3395 let resources = HashMap::new();
3396 let mut policy = crate::policy::VerificationPolicy::default();
3397 policy.resources.max_xml_base_resolution_bytes = 32;
3398 let error = VerifyContext::new()
3399 .policy(policy)
3400 .key(&AcceptingKey)
3401 .allowed_uri_types(UriTypeSet::ALL)
3402 .external_resources(&resources)
3403 .verify(&xml)
3404 .expect_err("cumulative XML Base copies must obey the operation budget");
3405
3406 assert!(matches!(
3407 error,
3408 SignatureVerificationPipelineError::Policy(
3409 crate::policy::PolicyViolation::ResourceLimit {
3410 resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
3411 maximum: 32,
3412 ..
3413 }
3414 )
3415 ));
3416 }
3417
3418 #[test]
3419 fn verify_context_applies_xml_base_policy_to_signed_info_c14n() {
3420 let xml = signature_with_target_reference("AQ==")
3424 .replacen(
3425 "http://www.w3.org/2001/10/xml-exc-c14n#",
3426 "http://www.w3.org/2006/12/xml-c14n11",
3427 1,
3428 )
3429 .replace(
3430 " <ds:Signature>",
3431 " <outer xml:base=\"one/\"><inner xml:base=\"two/\"><ds:Signature>",
3432 )
3433 .replace(" </ds:Signature>", " </ds:Signature></inner></outer>");
3434 let policy = crate::policy::VerificationPolicy {
3435 resources: crate::policy::ResourcePolicy {
3436 max_xml_base_components: 1,
3437 ..crate::policy::ResourcePolicy::default()
3438 },
3439 ..crate::policy::VerificationPolicy::default()
3440 };
3441
3442 let error = VerifyContext::new()
3443 .key(&AcceptingKey)
3444 .policy(policy)
3445 .verify(&xml)
3446 .expect_err("SignedInfo C14N must use the operation XML Base budget");
3447
3448 assert!(matches!(
3449 error,
3450 SignatureVerificationPipelineError::Policy(
3451 crate::policy::PolicyViolation::ResourceLimit {
3452 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
3453 maximum: 1,
3454 actual: 2,
3455 }
3456 )
3457 ));
3458 }
3459
3460 #[test]
3461 fn verify_context_classifies_signed_info_xml_base_byte_limit_as_policy() {
3462 let xml = signature_with_target_reference("AQ==")
3465 .replacen(
3466 "http://www.w3.org/2001/10/xml-exc-c14n#",
3467 "http://www.w3.org/2006/12/xml-c14n11",
3468 1,
3469 )
3470 .replace(
3471 " <ds:Signature>",
3472 " <outer xml:base=\"segment/\"><ds:Signature>",
3473 )
3474 .replace(" </ds:Signature>", " </ds:Signature></outer>");
3475 let policy = crate::policy::VerificationPolicy {
3476 resources: crate::policy::ResourcePolicy {
3477 max_xml_base_resolution_bytes: 1,
3478 ..crate::policy::ResourcePolicy::default()
3479 },
3480 ..crate::policy::VerificationPolicy::default()
3481 };
3482
3483 let error = VerifyContext::new()
3484 .key(&AcceptingKey)
3485 .policy(policy)
3486 .verify(&xml)
3487 .expect_err("SignedInfo XML Base byte exhaustion must be a policy error");
3488
3489 assert!(matches!(
3490 error,
3491 SignatureVerificationPipelineError::Policy(
3492 crate::policy::PolicyViolation::ResourceLimit {
3493 resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
3494 maximum: 1,
3495 actual,
3496 }
3497 ) if actual > 1
3498 ));
3499 }
3500
3501 #[test]
3502 fn verify_context_meters_repeated_external_dereferences() {
3503 let payload = b"payload";
3506 let digest = base64::engine::general_purpose::STANDARD.encode(
3507 crate::xmldsig::compute_digest(DigestAlgorithm::Sha1, payload),
3508 );
3509 let reference = format!(
3510 r#"<ds:Reference URI="urn:payload"><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference>"#
3511 );
3512 let xml = format!(
3513 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>"#
3514 );
3515 let resources = HashMap::from([("urn:payload".to_owned(), payload.to_vec())]);
3516 let policy = crate::policy::VerificationPolicy {
3517 uris: crate::policy::UriPolicy {
3518 references: UriTypeSet::ALL,
3519 ..crate::policy::UriPolicy::default()
3520 },
3521 resources: crate::policy::ResourcePolicy {
3522 max_external_resource_bytes: payload.len(),
3523 max_external_resource_total_bytes: payload.len(),
3524 ..crate::policy::ResourcePolicy::default()
3525 },
3526 ..crate::policy::VerificationPolicy::default()
3527 };
3528
3529 let error = VerifyContext::new()
3530 .key(&AcceptingKey)
3531 .policy(policy)
3532 .external_resources(&resources)
3533 .verify(&xml)
3534 .expect_err("the second dereference must exhaust the aggregate byte ceiling");
3535
3536 assert!(
3537 error
3538 .to_string()
3539 .contains("aggregate external resource bytes")
3540 );
3541 }
3542
3543 #[test]
3544 fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
3545 let xml = minimal_signature_xml("", "");
3546 let err = VerifyContext::new()
3547 .key(&RejectingKey)
3548 .allowed_uri_types(UriTypeSet::new(false, true, false))
3549 .verify(&xml)
3550 .expect_err("empty URI must be rejected when empty references are disabled");
3551 assert!(matches!(
3552 err,
3553 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
3554 operation: "verification",
3555 ..
3556 })
3557 ));
3558 }
3559
3560 #[test]
3561 fn verify_context_rejects_disallowed_transform() {
3562 let xml = minimal_signature_xml(
3563 "",
3564 r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
3565 );
3566 let err = VerifyContext::new()
3567 .key(&RejectingKey)
3568 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
3569 .verify(&xml)
3570 .expect_err("enveloped transform should be rejected by allowlist");
3571 assert!(matches!(
3572 err,
3573 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
3574 operation: "verification transform",
3575 ..
3576 })
3577 ));
3578 }
3579
3580 #[test]
3581 fn verify_context_applies_transform_allowlist_to_signed_info_c14n() {
3582 let xml = signature_with_target_reference("AQ==").replacen(
3585 "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
3586 "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"/>",
3587 1,
3588 );
3589 let error = VerifyContext::new()
3590 .key(&AcceptingKey)
3591 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
3592 .verify(&xml)
3593 .expect_err("SignedInfo C14N must obey the operation transform allowlist");
3594
3595 assert!(matches!(
3596 error,
3597 SignatureVerificationPipelineError::Policy(
3598 crate::policy::PolicyViolation::Algorithm {
3599 operation: "verification transform",
3600 ref algorithm,
3601 }
3602 )
3603 if algorithm == "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
3604 ));
3605 }
3606
3607 #[test]
3608 fn verify_context_applies_transform_allowlist_to_key_retrieval() {
3609 let xml = signature_with_target_reference("AQ==")
3612 .replacen(
3613 "</ds:Signature>",
3614 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>"##,
3615 1,
3616 )
3617 .replacen(
3618 "</root>",
3619 r#"<holder ID="keys"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder></root>"#,
3620 1,
3621 );
3622 let error = VerifyContext::new()
3623 .key_resolver(&ConsumingKeyInfoResolver)
3624 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
3625 .verify(&xml)
3626 .expect_err("RetrievalMethod XPath must obey the operation transform allowlist");
3627
3628 assert!(matches!(
3629 error,
3630 SignatureVerificationPipelineError::Policy(
3631 crate::policy::PolicyViolation::Algorithm {
3632 operation: "verification transform",
3633 ref algorithm,
3634 }
3635 )
3636 if algorithm == XPATH_TRANSFORM_URI
3637 ));
3638 }
3639
3640 fn signature_with_manifest_xml(valid_manifest_digest: bool) -> String {
3641 signature_with_manifest_xml_with_manifest_mutation(valid_manifest_digest, |xml| xml)
3642 }
3643
3644 fn signature_with_manifest_xml_with_manifest_mutation<F>(
3645 valid_manifest_digest: bool,
3646 mutate_manifest: F,
3647 ) -> String
3648 where
3649 F: FnOnce(String) -> String,
3650 {
3651 const TMP_SIGNED_INFO_DIGEST: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
3652 const INVALID_MANIFEST_DIGEST: &str = "//////////////////////////8=";
3653 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3654 <target ID="target">payload</target>
3655 <ds:Signature>
3656 <ds:SignedInfo>
3657 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3658 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3659 <ds:Reference URI="#manifest">
3660 <ds:Transforms>
3661 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3662 </ds:Transforms>
3663 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3664 <ds:DigestValue>SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER</ds:DigestValue>
3665 </ds:Reference>
3666 </ds:SignedInfo>
3667 <ds:SignatureValue>AQ==</ds:SignatureValue>
3668 <ds:Object>
3669 <ds:Manifest ID="manifest">
3670 <ds:Reference URI="#target">
3671 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3672 <ds:DigestValue>MANIFEST_DIGEST_PLACEHOLDER</ds:DigestValue>
3673 </ds:Reference>
3674 </ds:Manifest>
3675 </ds:Object>
3676 </ds:Signature>
3677</root>"##;
3678 let seed_xml = xml_template.replace(
3679 "SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER",
3680 TMP_SIGNED_INFO_DIGEST,
3681 );
3682 let doc = Document::parse(&seed_xml).unwrap();
3683 let signature_node = doc
3684 .descendants()
3685 .find(|node| {
3686 node.is_element()
3687 && node.tag_name().namespace() == Some(XMLDSIG_NS)
3688 && node.tag_name().name() == "Signature"
3689 })
3690 .unwrap();
3691 let resolver = UriReferenceResolver::new(&doc);
3692 let initial_data = resolver.dereference("#target").unwrap();
3693 let manifest_pre_digest =
3694 crate::xmldsig::execute_transforms(signature_node, initial_data, &[]).unwrap();
3695 let computed_manifest_digest_b64 = base64::engine::general_purpose::STANDARD
3696 .encode(compute_digest(DigestAlgorithm::Sha1, &manifest_pre_digest));
3697 let final_manifest_digest_b64 = if valid_manifest_digest {
3698 computed_manifest_digest_b64.as_str()
3699 } else {
3700 INVALID_MANIFEST_DIGEST
3701 };
3702 let xml_with_manifest_digest = mutate_manifest(
3703 seed_xml.replace("MANIFEST_DIGEST_PLACEHOLDER", final_manifest_digest_b64),
3704 );
3705 let signed_doc = Document::parse(&xml_with_manifest_digest).unwrap();
3706 let signed_signature_node = signed_doc
3707 .descendants()
3708 .find(|node| {
3709 node.is_element()
3710 && node.tag_name().namespace() == Some(XMLDSIG_NS)
3711 && node.tag_name().name() == "Signature"
3712 })
3713 .unwrap();
3714 let signed_info_node = signed_signature_node
3715 .children()
3716 .find(|node| {
3717 node.is_element()
3718 && node.tag_name().namespace() == Some(XMLDSIG_NS)
3719 && node.tag_name().name() == "SignedInfo"
3720 })
3721 .unwrap();
3722 let signed_info = parse_signed_info(signed_info_node).unwrap();
3723 let object_reference = &signed_info.references[0];
3724 let signed_resolver = UriReferenceResolver::new(&signed_doc);
3725 let signed_initial_data = signed_resolver
3726 .dereference(object_reference.uri.as_deref().unwrap())
3727 .unwrap();
3728 let signed_pre_digest = crate::xmldsig::execute_transforms(
3729 signed_signature_node,
3730 signed_initial_data,
3731 &object_reference.transforms,
3732 )
3733 .unwrap();
3734 let signed_digest_b64 = base64::engine::general_purpose::STANDARD.encode(compute_digest(
3735 object_reference.digest_method,
3736 &signed_pre_digest,
3737 ));
3738
3739 xml_with_manifest_digest.replacen(TMP_SIGNED_INFO_DIGEST, &signed_digest_b64, 1)
3740 }
3741
3742 fn replace_fixture_manifest_digest(xml: &str, replacement: &str) -> String {
3743 let object_marker = "<ds:Object>";
3744 let object_start = xml
3745 .find(object_marker)
3746 .expect("fixture should contain ds:Object")
3747 + object_marker.len();
3748 let open = "<ds:DigestValue>";
3749 let close = "</ds:DigestValue>";
3750 let value_start = xml[object_start..]
3751 .find(open)
3752 .map(|offset| object_start + offset + open.len())
3753 .expect("Manifest should contain DigestValue");
3754 let value_end = xml[value_start..]
3755 .find(close)
3756 .map(|offset| value_start + offset)
3757 .expect("Manifest DigestValue must be closed");
3758
3759 format!("{}{replacement}{}", &xml[..value_start], &xml[value_end..])
3760 }
3761
3762 #[test]
3763 fn verify_context_processes_manifest_references_when_enabled() {
3764 let xml = signature_with_manifest_xml(true);
3765
3766 let result_without_manifests = VerifyContext::new()
3767 .key(&RejectingKey)
3768 .verify(&xml)
3769 .expect("manifest processing disabled should still verify SignedInfo");
3770 assert!(
3771 result_without_manifests.manifest_references.is_empty(),
3772 "manifest results must stay empty when manifest processing is disabled",
3773 );
3774 assert!(matches!(
3775 result_without_manifests.status,
3776 DsigStatus::Invalid(FailureReason::SignatureMismatch)
3777 ));
3778
3779 let malformed_manifest_xml = signature_with_manifest_xml(true).replacen(
3780 "</ds:Object>",
3781 "</ds:Object><ds:Object><ds:Manifest><ds:Foo/></ds:Manifest></ds:Object>",
3782 1,
3783 );
3784 let malformed_with_manifests_disabled = VerifyContext::new()
3785 .key(&RejectingKey)
3786 .verify(&malformed_manifest_xml)
3787 .expect("malformed Manifest must be ignored when manifest processing is disabled");
3788 assert!(
3789 malformed_with_manifests_disabled
3790 .manifest_references
3791 .is_empty(),
3792 "manifest parser must not run when process_manifests is disabled",
3793 );
3794 assert!(matches!(
3795 malformed_with_manifests_disabled.status,
3796 DsigStatus::Invalid(FailureReason::SignatureMismatch)
3797 ));
3798
3799 let result_with_manifests = VerifyContext::new()
3800 .key(&AcceptingKey)
3801 .process_manifests(true)
3802 .verify(&xml)
3803 .expect("manifest references should be processed when enabled");
3804 assert_eq!(result_with_manifests.manifest_references.len(), 1);
3805 assert_eq!(
3806 result_with_manifests.manifest_references[0].reference_set,
3807 ReferenceSet::Manifest
3808 );
3809 assert_eq!(
3810 result_with_manifests.manifest_references[0].reference_index,
3811 0
3812 );
3813 assert!(matches!(
3814 result_with_manifests.manifest_references[0].status,
3815 DsigStatus::Valid
3816 ));
3817 assert!(matches!(result_with_manifests.status, DsigStatus::Valid));
3818 }
3819
3820 #[test]
3821 fn verify_context_skips_manifest_work_when_signature_value_is_invalid() {
3822 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3826 replace_fixture_manifest_digest(&xml, "!!!")
3827 });
3828 assert!(
3829 xml.split_once("<ds:Object>")
3830 .is_some_and(|(_, object)| object.contains("<ds:DigestValue>!!!</ds:DigestValue>")),
3831 "fixture mutation must corrupt the nested Manifest DigestValue",
3832 );
3833
3834 let result = VerifyContext::new()
3835 .key(&RejectingKey)
3836 .process_manifests(true)
3837 .verify(&xml)
3838 .expect("invalid SignatureValue must short-circuit Manifest parsing");
3839
3840 assert!(matches!(
3841 result.status,
3842 DsigStatus::Invalid(FailureReason::SignatureMismatch)
3843 ));
3844 assert!(result.manifest_references.is_empty());
3845 }
3846
3847 #[test]
3848 fn verify_context_shares_xpath_parse_budget_with_manifest_references() {
3849 let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
3852 .repeat(64);
3853 let transform = format!(
3854 r#"<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</ds:Transform>"#
3855 );
3856 let max_transforms = transform.repeat(16);
3857 let max_manifest_reference = format!(
3858 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>"##
3859 );
3860 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3861 xml.replacen(
3862 r##"<ds:Reference URI="#target">"##,
3863 &format!(
3864 r##"<ds:Reference URI="#target"><ds:Transforms>{}</ds:Transforms>"##,
3865 max_transforms
3866 ),
3867 1,
3868 )
3869 .replacen(
3870 "</ds:SignedInfo>",
3871 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>"##,
3872 1,
3873 )
3874 .replacen(
3875 "</ds:Manifest>",
3876 &format!("{}</ds:Manifest>", max_manifest_reference.repeat(3)),
3877 1,
3878 )
3879 });
3880
3881 let error = VerifyContext::new()
3882 .key(&AcceptingKey)
3883 .process_manifests(true)
3884 .verify(&xml)
3885 .expect_err("SignedInfo and Manifest References must share one XPath parse budget");
3886
3887 assert!(
3888 matches!(
3889 &error,
3890 SignatureVerificationPipelineError::Policy(
3891 crate::policy::PolicyViolation::ResourceLimit {
3892 resource: "XPath expressions",
3893 ..
3894 }
3895 )
3896 ),
3897 "unexpected error: {error:?}"
3898 );
3899 }
3900
3901 #[test]
3902 fn verify_context_processes_manifest_when_signedinfo_references_object() {
3903 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3904 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
3905 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
3906 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
3907 });
3908
3909 let result = VerifyContext::new()
3910 .key(&AcceptingKey)
3911 .process_manifests(true)
3912 .verify(&xml)
3913 .expect("manifest references should be processed when SignedInfo references ds:Object");
3914 assert_eq!(
3915 result.manifest_references.len(),
3916 1,
3917 "signed ds:Object should enable processing of its direct-child ds:Manifest",
3918 );
3919 assert_eq!(
3920 result.manifest_references[0].reference_set,
3921 ReferenceSet::Manifest
3922 );
3923 assert_eq!(result.manifest_references[0].reference_index, 0);
3924 assert!(matches!(
3925 result.manifest_references[0].status,
3926 DsigStatus::Valid
3927 ));
3928 }
3929
3930 #[test]
3931 fn verify_context_skips_manifest_removed_by_enveloped_transform() {
3932 for target_object in [false, true] {
3936 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3937 let xml = xml.replacen(
3938 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
3939 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#"/>"#,
3940 1,
3941 );
3942 if target_object {
3943 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
3944 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
3945 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
3946 } else {
3947 xml
3948 }
3949 });
3950
3951 let result = VerifyContext::new()
3952 .key(&AcceptingKey)
3953 .process_manifests(true)
3954 .store_pre_digest(true)
3955 .verify(&xml)
3956 .expect("an emptied reference remains a valid core digest input");
3957
3958 assert!(matches!(result.status, DsigStatus::Valid));
3959 assert_eq!(
3960 result.signed_info_references[0].pre_digest_data.as_deref(),
3961 Some([].as_slice()),
3962 "target_object={target_object} must have empty transformed bytes",
3963 );
3964 assert!(
3965 result.manifest_references.is_empty(),
3966 "target_object={target_object} must not authenticate the Manifest",
3967 );
3968 }
3969 }
3970
3971 #[test]
3972 fn verify_context_ignores_manifest_excluded_from_signed_object() {
3973 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3977 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
3978 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
3979 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
3980 .replacen(
3981 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
3982 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#"/>"#,
3983 1,
3984 )
3985 });
3986
3987 let result = VerifyContext::new()
3988 .key(&AcceptingKey)
3989 .process_manifests(true)
3990 .verify(&xml)
3991 .expect("excluded Manifest content must be ignored, not parsed");
3992
3993 assert!(matches!(result.status, DsigStatus::Valid));
3994 assert!(
3995 result.manifest_references.is_empty(),
3996 "a transform-excluded Manifest is not authenticated by SignedInfo",
3997 );
3998 }
3999
4000 #[test]
4001 fn verify_context_skips_manifest_digest_work_when_signature_is_invalid() {
4002 let xml = signature_with_manifest_xml(false);
4003 let result = VerifyContext::new()
4004 .key(&RejectingKey)
4005 .process_manifests(true)
4006 .verify(&xml)
4007 .expect("invalid SignatureValue must short-circuit Manifest digest work");
4008 assert!(result.manifest_references.is_empty());
4009 assert!(matches!(
4010 result.status,
4011 DsigStatus::Invalid(FailureReason::SignatureMismatch)
4012 ));
4013 }
4014
4015 #[test]
4016 fn verify_context_manifest_digest_mismatch_is_non_fatal_with_accepting_key() {
4017 let xml = signature_with_manifest_xml(false);
4018 let result = VerifyContext::new()
4019 .key(&AcceptingKey)
4020 .process_manifests(true)
4021 .verify(&xml)
4022 .expect("manifest digest mismatches should be recorded while signature stays valid");
4023 assert_eq!(result.manifest_references.len(), 1);
4024 assert!(matches!(
4025 result.manifest_references[0].status,
4026 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
4027 ));
4028 assert!(matches!(result.status, DsigStatus::Valid));
4029 }
4030
4031 #[test]
4032 fn verify_context_skips_manifest_parsing_when_signedinfo_reference_fails() {
4033 let xml = signature_with_manifest_xml(true);
4036 let (signed_info_prefix, object_suffix) = xml
4037 .split_once("<ds:Object>")
4038 .expect("fixture should contain ds:Object");
4039 let open = "<ds:DigestValue>";
4040 let close = "</ds:DigestValue>";
4041 let digest_start = signed_info_prefix
4042 .find(open)
4043 .expect("SignedInfo should contain DigestValue");
4044 let digest_end = signed_info_prefix[digest_start + open.len()..]
4045 .find(close)
4046 .map(|offset| digest_start + open.len() + offset)
4047 .expect("SignedInfo DigestValue must be closed");
4048 let broken_signed_info_prefix = format!(
4049 "{}{}AAAAAAAAAAAAAAAAAAAAAAAAAAA={}{}",
4050 &signed_info_prefix[..digest_start],
4051 open,
4052 close,
4053 &signed_info_prefix[digest_end + close.len()..],
4054 );
4055 let broken_xml = format!("{broken_signed_info_prefix}<ds:Object>{object_suffix}");
4056 let result = VerifyContext::new()
4057 .key(&RejectingKey)
4058 .process_manifests(true)
4059 .verify(&broken_xml)
4060 .expect("SignedInfo digest failure should return without parsing Manifests");
4061 assert!(matches!(
4062 result.status,
4063 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
4064 ));
4065 assert!(
4066 result.manifest_references.is_empty(),
4067 "unauthenticated Manifest content must not be parsed",
4068 );
4069 }
4070
4071 #[test]
4072 fn verify_context_skips_manifest_policy_work_when_signature_is_invalid() {
4073 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4076 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
4077 });
4078 let result = VerifyContext::new()
4079 .key(&RejectingKey)
4080 .process_manifests(true)
4081 .verify(&broken_xml)
4082 .expect("invalid SignatureValue must short-circuit Manifest policy work");
4083 assert!(result.manifest_references.is_empty());
4084 assert!(matches!(
4085 result.status,
4086 DsigStatus::Invalid(FailureReason::SignatureMismatch)
4087 ));
4088 }
4089
4090 #[test]
4091 fn verify_context_records_manifest_policy_violations_with_accepting_key() {
4092 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4093 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
4094 });
4095 let result = VerifyContext::new()
4096 .key(&AcceptingKey)
4097 .process_manifests(true)
4098 .verify(&broken_xml)
4099 .expect("manifest policy violations should be recorded while signature stays valid");
4100 assert_eq!(result.manifest_references.len(), 1);
4101 assert!(matches!(
4102 result.manifest_references[0].status,
4103 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
4104 ));
4105 assert!(matches!(result.status, DsigStatus::Valid));
4106 }
4107
4108 #[test]
4109 fn verify_context_applies_digest_policy_to_manifest_references() {
4110 let policy = crate::policy::VerificationPolicy {
4113 manifest_processing: crate::policy::ManifestProcessing::Process,
4114 digest_algorithms: Some(HashSet::from([DigestAlgorithm::Sha1])),
4115 ..crate::policy::VerificationPolicy::default()
4116 };
4117 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
4118 let legacy = "http://www.w3.org/2000/09/xmldsig#sha1";
4119 let offset = xml
4120 .rfind(legacy)
4121 .expect("Manifest DigestMethod must be present");
4122 xml.replace_range(offset..offset + legacy.len(), DigestAlgorithm::Sha256.uri());
4123 let value_start = xml[offset..]
4124 .find("<ds:DigestValue>")
4125 .map(|relative| offset + relative + "<ds:DigestValue>".len())
4126 .expect("Manifest DigestValue must be present");
4127 let value_end = xml[value_start..]
4128 .find("</ds:DigestValue>")
4129 .map(|relative| value_start + relative)
4130 .expect("Manifest DigestValue must be closed");
4131 xml.replace_range(
4132 value_start..value_end,
4133 &base64::engine::general_purpose::STANDARD.encode([0_u8; 32]),
4134 );
4135 xml
4136 });
4137 let result = VerifyContext::new()
4138 .key(&AcceptingKey)
4139 .policy(policy)
4140 .verify(&xml)
4141 .expect("a disallowed Manifest digest is a per-reference result");
4142
4143 assert!(matches!(result.status, DsigStatus::Valid));
4144 assert!(matches!(
4145 result.manifest_references[0].status,
4146 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
4147 ));
4148 }
4149
4150 #[test]
4151 fn verify_context_applies_transform_count_policy_to_manifest_references() {
4152 let policy = crate::policy::VerificationPolicy {
4155 manifest_processing: crate::policy::ManifestProcessing::Process,
4156 resources: crate::policy::ResourcePolicy {
4157 max_transforms_per_reference: 1,
4158 ..crate::policy::ResourcePolicy::default()
4159 },
4160 ..crate::policy::VerificationPolicy::default()
4161 };
4162 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
4163 let manifest_start = xml
4164 .find("<ds:Manifest")
4165 .expect("fixture must contain a Manifest");
4166 let manifest = xml[manifest_start..].replacen(
4167 "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
4168 concat!(
4169 "<ds:Transforms>",
4170 "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
4171 "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
4172 "</ds:Transforms>",
4173 "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>"
4174 ),
4175 1,
4176 );
4177 xml.replace_range(manifest_start.., &manifest);
4178 xml
4179 });
4180 let result = VerifyContext::new()
4181 .key(&AcceptingKey)
4182 .policy(policy)
4183 .verify(&xml)
4184 .expect("Manifest transform policy is a per-reference result");
4185
4186 assert!(matches!(result.status, DsigStatus::Valid));
4187 assert!(matches!(
4188 result.manifest_references[0].status,
4189 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
4190 ));
4191 }
4192
4193 #[test]
4194 fn verify_context_skips_manifest_uri_work_when_signature_is_invalid() {
4195 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4198 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
4199 });
4200
4201 let result = VerifyContext::new()
4202 .key(&RejectingKey)
4203 .process_manifests(true)
4204 .verify(&broken_xml)
4205 .expect("invalid SignatureValue must short-circuit Manifest URI processing");
4206 assert!(result.manifest_references.is_empty());
4207 assert!(matches!(
4208 result.status,
4209 DsigStatus::Invalid(FailureReason::SignatureMismatch)
4210 ));
4211 }
4212
4213 #[test]
4214 fn verify_context_records_manifest_missing_uri_with_accepting_key() {
4215 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4216 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
4217 });
4218
4219 let result = VerifyContext::new()
4220 .key(&AcceptingKey)
4221 .process_manifests(true)
4222 .verify(&broken_xml)
4223 .expect("manifest missing URI should be recorded while signature stays valid");
4224 assert_eq!(result.manifest_references.len(), 1);
4225 assert_eq!(result.manifest_references[0].uri, "<omitted>");
4226 assert!(matches!(
4227 result.manifest_references[0].status,
4228 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
4229 ));
4230 assert!(matches!(result.status, DsigStatus::Valid));
4231 }
4232
4233 #[test]
4234 fn verify_context_ignores_nested_manifests_in_object() {
4235 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4238 xml.replacen(
4239 "<ds:Manifest ID=\"manifest\">",
4240 "<wrapper><ds:Manifest ID=\"manifest\">",
4241 1,
4242 )
4243 .replacen("</ds:Manifest>", "</ds:Manifest></wrapper>", 1)
4244 });
4245
4246 let result = VerifyContext::new()
4247 .key(&AcceptingKey)
4248 .process_manifests(true)
4249 .verify(&xml)
4250 .expect("nested Manifest nodes are ignored in strict mode");
4251 assert!(
4252 result.manifest_references.is_empty(),
4253 "only direct ds:Manifest children of ds:Object must be processed"
4254 );
4255 assert!(matches!(result.status, DsigStatus::Valid));
4256 }
4257
4258 #[test]
4259 fn verify_context_reports_manifest_reference_parse_errors_explicitly() {
4260 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4263 replace_fixture_manifest_digest(&xml, "!!!")
4264 });
4265
4266 let err = VerifyContext::new()
4267 .key(&AcceptingKey)
4268 .process_manifests(true)
4269 .verify(&broken_xml)
4270 .expect_err("invalid Manifest DigestValue must map to ParseManifestReference");
4271 assert!(matches!(
4272 err,
4273 SignatureVerificationPipelineError::ParseManifestReference(_)
4274 ));
4275 }
4276
4277 #[test]
4278 fn verify_context_reports_unsupported_manifest_transform_with_declared_digest() {
4279 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4282 let xml = xml.replacen(
4283 "<ds:Reference URI=\"#target\">",
4284 "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
4285 1,
4286 );
4287 let xml = xml.replacen(
4288 "</ds:Transforms>\n <ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
4289 "</ds:Transforms>\n <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>",
4290 1,
4291 );
4292 replace_fixture_manifest_digest(&xml, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
4293 });
4294 assert!(xml.contains("urn:unsupported"));
4295 assert!(xml.contains("http://www.w3.org/2001/04/xmlenc#sha256"));
4296
4297 let result = VerifyContext::new()
4298 .key(&AcceptingKey)
4299 .process_manifests(true)
4300 .verify(&xml)
4301 .expect("unsupported Manifest transform is a per-reference result");
4302 assert_eq!(result.status, DsigStatus::Valid);
4303 assert_eq!(result.manifest_references.len(), 1);
4304 assert_eq!(
4305 result.manifest_references[0].digest_algorithm,
4306 DigestAlgorithm::Sha256
4307 );
4308 assert!(matches!(
4309 result.manifest_references[0].status,
4310 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
4311 ));
4312
4313 let restricted = VerifyContext::new()
4314 .key(&AcceptingKey)
4315 .process_manifests(true)
4316 .allowed_transforms([
4317 DEFAULT_IMPLICIT_C14N_URI,
4318 "http://www.w3.org/2001/10/xml-exc-c14n#",
4319 ])
4320 .verify(&xml)
4321 .expect("a disallowed Manifest transform is a per-reference policy result");
4322 assert!(matches!(
4323 restricted.manifest_references[0].status,
4324 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
4325 ));
4326 }
4327
4328 #[test]
4329 fn manifest_reference_limit_counts_unsupported_entries() {
4330 let references = (0..=MAX_REFERENCES_PER_SIGNATURE)
4331 .map(|index| {
4332 format!(
4333 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>"##
4334 )
4335 })
4336 .collect::<String>();
4337 let xml = format!(
4338 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>"#
4339 );
4340 let document = Document::parse(&xml).unwrap();
4341 let signature = document.root_element();
4342 let object = signature.children().find(|node| node.is_element()).unwrap();
4343 let authenticated = HashSet::from([object.id()]);
4344 let mut processed = HashSet::new();
4345 let mut remaining = MAX_REFERENCES_PER_SIGNATURE;
4346 let mut next_index = 0;
4347
4348 let error = match parse_manifest_references(
4349 signature,
4350 &authenticated,
4351 &mut processed,
4352 &mut remaining,
4353 &mut next_index,
4354 &mut XPathSignatureParseBudget::default(),
4355 None,
4356 ) {
4357 Ok(_) => panic!("unsupported references must consume the same aggregate limit"),
4358 Err(error) => error,
4359 };
4360 assert!(matches!(
4361 error,
4362 SignatureVerificationPipelineError::InvalidStructure {
4363 reason: "signed Manifests exceed the per-signature Reference limit"
4364 }
4365 ));
4366 }
4367
4368 #[test]
4369 fn unsigned_manifest_remains_eligible_after_trust_expands() {
4370 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4374 let xml = format!(
4375 r##"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:Object Id="outer"><ds:Manifest><ds:Reference URI="#inner"><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:Manifest></ds:Object><ds:Object Id="inner"><ds:Manifest><ds:Reference URI="#payload"><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:Manifest></ds:Object></ds:Signature>"##
4376 );
4377 let document = Document::parse(&xml).expect("nested Manifest fixture must parse");
4378 let signature = document.root_element();
4379 let mut objects = signature.children().filter(|node| node.is_element());
4380 let outer = objects.next().expect("outer Object");
4381 let inner = objects.next().expect("inner Object");
4382 let mut authenticated = HashSet::from([outer.id()]);
4383 let mut processed = HashSet::new();
4384 let mut remaining = 2;
4385 let mut next_index = 0;
4386 let mut xpath_budget = XPathSignatureParseBudget::default();
4387
4388 let first = parse_manifest_references(
4389 signature,
4390 &authenticated,
4391 &mut processed,
4392 &mut remaining,
4393 &mut next_index,
4394 &mut xpath_budget,
4395 None,
4396 )
4397 .expect("outer Manifest must be discovered");
4398 assert_eq!(first.references.len(), 1);
4399 assert_eq!(first.references[0].1.uri.as_deref(), Some("#inner"));
4400
4401 authenticated.insert(inner.id());
4402 let second = parse_manifest_references(
4403 signature,
4404 &authenticated,
4405 &mut processed,
4406 &mut remaining,
4407 &mut next_index,
4408 &mut xpath_budget,
4409 None,
4410 )
4411 .expect("newly authenticated sibling Manifest must remain eligible");
4412 assert_eq!(second.references.len(), 1);
4413 assert_eq!(second.references[0].1.uri.as_deref(), Some("#payload"));
4414 }
4415
4416 #[test]
4417 fn manifest_reference_limit_includes_signed_info_references() {
4418 let xml = signature_with_manifest_xml(true);
4421 let reference_start = xml
4422 .find(r##"<ds:Reference URI="#manifest">"##)
4423 .expect("fixture SignedInfo must reference the Manifest");
4424 let reference_end = xml[reference_start..]
4425 .find("</ds:Reference>")
4426 .map(|offset| reference_start + offset + "</ds:Reference>".len())
4427 .expect("fixture SignedInfo Reference must be closed");
4428 let repeated = xml[reference_start..reference_end].repeat(MAX_REFERENCES_PER_SIGNATURE);
4429 let xml = format!(
4430 "{}{repeated}{}",
4431 &xml[..reference_start],
4432 &xml[reference_end..]
4433 );
4434
4435 let error = VerifyContext::new()
4436 .key(&AcceptingKey)
4437 .process_manifests(true)
4438 .verify(&xml)
4439 .expect_err("one Manifest Reference must exceed the exhausted signature-wide limit");
4440
4441 assert!(matches!(
4442 error,
4443 SignatureVerificationPipelineError::InvalidStructure {
4444 reason: "signed Manifests exceed the per-signature Reference limit"
4445 }
4446 ));
4447 }
4448
4449 #[test]
4450 fn configured_reference_limit_is_shared_with_manifests() {
4451 let policy = crate::policy::VerificationPolicy {
4454 manifest_processing: crate::policy::ManifestProcessing::Process,
4455 resources: crate::policy::ResourcePolicy {
4456 max_references: 1,
4457 ..crate::policy::ResourcePolicy::default()
4458 },
4459 ..crate::policy::VerificationPolicy::default()
4460 };
4461
4462 let error = VerifyContext::new()
4463 .key(&AcceptingKey)
4464 .policy(policy)
4465 .verify(&signature_with_manifest_xml(true))
4466 .expect_err("Manifest must exceed the caller-selected aggregate limit");
4467 assert!(matches!(
4468 error,
4469 SignatureVerificationPipelineError::InvalidStructure {
4470 reason: "signed Manifests exceed the per-signature Reference limit"
4471 }
4472 ));
4473 }
4474
4475 #[test]
4476 fn retrieval_method_materializes_single_x509_data_subtree() {
4477 for uri in [
4478 "#target",
4479 "#xpointer(id('target'))",
4480 "#xpointer(id("target"))",
4481 ] {
4482 for target_xml in [
4483 r#"<ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>"#,
4484 r#"<holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>"#,
4485 ] {
4486 let xml = format!(
4487 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>"#
4488 );
4489 let document = Document::parse(&xml).unwrap();
4490 let key_info_node = document
4491 .descendants()
4492 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
4493 .unwrap();
4494 let mut key_info = parse_key_info(key_info_node).unwrap();
4495 let resolver = UriReferenceResolver::new(&document);
4496
4497 materialize_retrieval_methods(
4498 &mut key_info,
4499 &resolver,
4500 UriTypeSet::SAME_DOCUMENT,
4501 None,
4502 crate::provider::default_provider(),
4503 )
4504 .expect("XPath filter must produce one X509Data-rooted node-set");
4505 assert!(matches!(
4506 key_info.sources.as_slice(),
4507 [super::super::parse::KeyInfoSource::X509Data(info)]
4508 if info.subject_names == ["CN=leaf"]
4509 ));
4510 }
4511 }
4512 }
4513
4514 fn retrieval_method_xpath_signature() -> String {
4515 format!(
4516 r##"<root xmlns:ds="{XMLDSIG_NS}">
4517 <payload Id="payload">ok</payload>
4518 <ds:Signature>
4519 <ds:SignedInfo>
4520 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4521 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4522 <ds:Reference URI="#payload">
4523 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4524 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
4525 </ds:Reference>
4526 </ds:SignedInfo>
4527 <ds:SignatureValue>AQ==</ds:SignatureValue>
4528 <ds:KeyInfo>
4529 <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
4530 <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
4531 <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
4532 </ds:Transform></ds:Transforms>
4533 </ds:RetrievalMethod>
4534 </ds:KeyInfo>
4535 </ds:Signature>
4536 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
4537 </root>"##
4538 )
4539 }
4540
4541 #[test]
4542 fn retrieval_method_xpath_uses_signature_expression_budget() {
4543 let mut policy = crate::policy::VerificationPolicy::default();
4546 policy.resources.max_xpath_expressions = 0;
4547
4548 let error = VerifyContext::new()
4549 .policy(policy)
4550 .verify(&retrieval_method_xpath_signature())
4551 .expect_err("RetrievalMethod XPath must consume the signature parse budget");
4552
4553 assert!(
4554 matches!(
4555 &error,
4556 SignatureVerificationPipelineError::Policy(
4557 crate::policy::PolicyViolation::ResourceLimit {
4558 resource: "XPath expressions",
4559 maximum: 0,
4560 actual: 1,
4561 }
4562 )
4563 ),
4564 "unexpected error: {error:?}"
4565 );
4566 }
4567
4568 #[test]
4569 fn retrieval_method_xpath_obeys_expression_byte_limit() {
4570 let expression = "ancestor-or-self::ds:X509Data";
4573 let padded_expression = format!(" {expression} ");
4574 let xml = retrieval_method_xpath_signature().replace(expression, &padded_expression);
4575 let mut policy = crate::policy::VerificationPolicy::default();
4576 policy.resources.max_xpath_expression_bytes = expression.len();
4577
4578 let error = VerifyContext::new()
4579 .policy(policy)
4580 .verify(&xml)
4581 .expect_err("RetrievalMethod XPath must obey the expression byte limit");
4582
4583 assert!(
4584 matches!(
4585 &error,
4586 SignatureVerificationPipelineError::Policy(
4587 crate::policy::PolicyViolation::ResourceLimit {
4588 resource: "XPath expression bytes",
4589 maximum,
4590 actual,
4591 }
4592 ) if *maximum == expression.len() && *actual == padded_expression.len()
4593 ),
4594 "unexpected error: {error:?}"
4595 );
4596 }
4597
4598 #[test]
4599 fn retrieval_method_xpath_obeys_expression_complexity_limit() {
4600 let mut policy = crate::policy::VerificationPolicy::default();
4603 policy.resources.max_xpath_expression_complexity = 0;
4604
4605 let error = VerifyContext::new()
4606 .policy(policy)
4607 .verify(&retrieval_method_xpath_signature())
4608 .expect_err("RetrievalMethod XPath must obey the complexity limit");
4609
4610 assert!(
4611 matches!(
4612 &error,
4613 SignatureVerificationPipelineError::Policy(
4614 crate::policy::PolicyViolation::ResourceLimit {
4615 resource: "XPath expression complexity",
4616 maximum: 0,
4617 actual,
4618 }
4619 ) if *actual > 0
4620 ),
4621 "unexpected error: {error:?}"
4622 );
4623 }
4624
4625 #[test]
4626 fn retrieval_method_xpath_uses_node_filter_work_budget() {
4627 let mut policy = crate::policy::VerificationPolicy::default();
4630 policy.resources.max_node_set_filter_work = 4;
4631 let xml = retrieval_method_xpath_signature().replace(
4632 "<holder Id=\"target\">",
4633 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
4634 );
4635
4636 let error = VerifyContext::new()
4637 .policy(policy)
4638 .verify(&xml)
4639 .expect_err("RetrievalMethod XPath must consume node-filter work");
4640
4641 assert!(
4642 matches!(
4643 &error,
4644 SignatureVerificationPipelineError::Policy(
4645 crate::policy::PolicyViolation::ResourceLimit {
4646 resource: crate::policy::resource_name::NODE_SET_FILTER_WORK,
4647 maximum: 4,
4648 actual,
4649 }
4650 ) if *actual > 4
4651 ),
4652 "unexpected error: {error:?}"
4653 );
4654 }
4655
4656 #[test]
4657 fn retrieval_method_xpath_charges_every_context_to_evaluation_work() {
4658 let mut policy = crate::policy::VerificationPolicy::default();
4661 policy.resources.max_xpath_evaluation_work = 4;
4662 let xml = retrieval_method_xpath_signature().replace(
4663 "<holder Id=\"target\">",
4664 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
4665 );
4666
4667 let error = VerifyContext::new()
4668 .policy(policy)
4669 .verify(&xml)
4670 .expect_err("RetrievalMethod XPath must charge every evaluation context");
4671
4672 assert!(matches!(
4673 error,
4674 SignatureVerificationPipelineError::Policy(
4675 crate::policy::PolicyViolation::ResourceLimit {
4676 resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
4677 maximum: 4,
4678 actual,
4679 }
4680 ) if actual > 4
4681 ));
4682 }
4683
4684 #[test]
4685 fn retrieval_method_xpath_obeys_namespace_binding_limit() {
4686 let mut policy = crate::policy::VerificationPolicy::default();
4689 policy.resources.max_xpath_namespace_bindings = 0;
4690
4691 let error = VerifyContext::new()
4692 .policy(policy)
4693 .verify(&retrieval_method_xpath_signature())
4694 .expect_err("RetrievalMethod XPath namespaces must obey the binding limit");
4695
4696 assert!(matches!(
4697 error,
4698 SignatureVerificationPipelineError::Policy(
4699 crate::policy::PolicyViolation::ResourceLimit {
4700 resource: crate::policy::resource_name::XPATH_NAMESPACE_BINDINGS,
4701 maximum: 0,
4702 actual,
4703 }
4704 ) if actual > 0
4705 ));
4706 }
4707
4708 #[test]
4709 fn retrieval_method_xpath_obeys_namespace_byte_limit() {
4710 let mut policy = crate::policy::VerificationPolicy::default();
4713 policy.resources.max_xpath_namespace_bytes = 0;
4714
4715 let error = VerifyContext::new()
4716 .policy(policy)
4717 .verify(&retrieval_method_xpath_signature())
4718 .expect_err("RetrievalMethod XPath namespaces must obey the byte limit");
4719
4720 assert!(matches!(
4721 error,
4722 SignatureVerificationPipelineError::Policy(
4723 crate::policy::PolicyViolation::ResourceLimit {
4724 resource: crate::policy::resource_name::XPATH_NAMESPACE_BYTES,
4725 maximum: 0,
4726 actual,
4727 }
4728 ) if actual > 0
4729 ));
4730 }
4731
4732 #[test]
4733 fn retrieval_method_xpath_obeys_context_evaluation_limit() {
4734 let mut policy = crate::policy::VerificationPolicy::default();
4738 policy.resources.max_xpath_context_evaluations = 4;
4739 let xml = retrieval_method_xpath_signature().replace(
4740 "<holder Id=\"target\">",
4741 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
4742 );
4743
4744 let error = VerifyContext::new()
4745 .policy(policy)
4746 .verify(&xml)
4747 .expect_err("RetrievalMethod XPath contexts must obey the evaluation limit");
4748
4749 assert!(matches!(
4750 error,
4751 SignatureVerificationPipelineError::Policy(
4752 crate::policy::PolicyViolation::ResourceLimit {
4753 resource: crate::policy::resource_name::XPATH_CONTEXT_EVALUATIONS,
4754 maximum: 4,
4755 actual,
4756 }
4757 ) if actual > 4
4758 ));
4759 }
4760
4761 #[test]
4762 fn retrieval_method_materializes_direct_untransformed_x509_data() {
4763 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4766 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
4767 <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
4768 </root>"##;
4769 let document = Document::parse(xml).unwrap();
4770 let key_info_node = document
4771 .descendants()
4772 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
4773 .unwrap();
4774 let mut key_info = parse_key_info(key_info_node).unwrap();
4775
4776 materialize_retrieval_methods(
4777 &mut key_info,
4778 &UriReferenceResolver::new(&document),
4779 UriTypeSet::SAME_DOCUMENT,
4780 None,
4781 crate::provider::default_provider(),
4782 )
4783 .expect("a direct X509Data target needs no transform");
4784 assert!(matches!(
4785 key_info.sources.as_slice(),
4786 [super::super::parse::KeyInfoSource::X509Data(info)]
4787 if info.subject_names == ["CN=leaf"]
4788 ));
4789 }
4790
4791 #[test]
4792 fn retrieval_method_respects_configured_same_document_id_semantics() {
4793 fn materialize(
4796 uri: &str,
4797 id: &str,
4798 semantics: crate::policy::SameDocumentIdSemantics,
4799 ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
4800 let xml = format!(
4801 r#"<root xmlns:ds="{XMLDSIG_NS}">
4802 <ds:KeyInfo><ds:RetrievalMethod URI="{uri}" Type="{XMLDSIG_NS}X509Data"/></ds:KeyInfo>
4803 <ds:X509Data Id="{id}"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
4804 </root>"#
4805 );
4806 let document = Document::parse(&xml).unwrap();
4807 let key_info_node = document
4808 .descendants()
4809 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
4810 .unwrap();
4811 let mut key_info = parse_key_info(key_info_node).unwrap();
4812 let resolver =
4813 UriReferenceResolver::new(&document).with_same_document_id_semantics(semantics);
4814
4815 materialize_retrieval_methods(
4816 &mut key_info,
4817 &resolver,
4818 UriTypeSet::SAME_DOCUMENT,
4819 None,
4820 crate::provider::default_provider(),
4821 )
4822 }
4823
4824 assert!(
4825 materialize(
4826 "#12345",
4827 "12345",
4828 crate::policy::SameDocumentIdSemantics::Specification,
4829 )
4830 .is_err(),
4831 "the standards mode must reject a non-NCName bare fragment"
4832 );
4833 assert!(
4834 materialize(
4835 "#visa'3d",
4836 "visa'3d",
4837 crate::policy::SameDocumentIdSemantics::XmlSecBarename,
4838 )
4839 .is_err(),
4840 "the donor barename wrapper cannot represent an apostrophe"
4841 );
4842 assert!(
4843 materialize(
4844 "#visa'3d",
4845 "visa'3d",
4846 crate::policy::SameDocumentIdSemantics::XmlSecVisa3d,
4847 )
4848 .is_ok(),
4849 "Visa3D mode resolves the registered ID without an XPointer literal"
4850 );
4851 }
4852
4853 #[test]
4854 fn raw_x509_retrieval_method_uses_inherited_xml_base() {
4855 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
4858 let xml = format!(
4859 r#"<root xml:base="https://example.test/keys/nested/" xmlns:ds="{XMLDSIG_NS}">
4860 <ds:KeyInfo><ds:RetrievalMethod URI="../signer.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>
4861 </root>"#
4862 );
4863 let document = Document::parse(&xml).unwrap();
4864 let key_info_node = document
4865 .descendants()
4866 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
4867 .unwrap();
4868 let mut key_info = parse_key_info(key_info_node).unwrap();
4869 let certificate = include_bytes!(
4870 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
4871 )
4872 .to_vec();
4873 let resources = HashMap::from([(
4874 "https://example.test/keys/signer.der".to_string(),
4875 certificate,
4876 )]);
4877 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4878
4879 materialize_retrieval_methods(
4880 &mut key_info,
4881 &resolver,
4882 UriTypeSet::ALL,
4883 None,
4884 crate::provider::default_provider(),
4885 )
4886 .expect("RetrievalMethod should resolve against inherited xml:base");
4887
4888 assert!(matches!(
4889 key_info.sources.as_slice(),
4890 [super::super::parse::KeyInfoSource::X509Data(info)]
4891 if info.certificates.len() == 1
4892 ));
4893 }
4894
4895 #[test]
4896 fn retrieval_method_requires_xpath_for_x509_data_below_uri_root() {
4897 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4900 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
4901 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
4902 </root>"##;
4903 let document = Document::parse(xml).unwrap();
4904 let key_info_node = document
4905 .descendants()
4906 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
4907 .unwrap();
4908 let mut key_info = parse_key_info(key_info_node).unwrap();
4909
4910 let error = materialize_retrieval_methods(
4911 &mut key_info,
4912 &UriReferenceResolver::new(&document),
4913 UriTypeSet::SAME_DOCUMENT,
4914 None,
4915 crate::provider::default_provider(),
4916 )
4917 .expect_err("a wrapper target requires an explicit selection transform");
4918 assert!(matches!(
4919 error,
4920 SignatureVerificationPipelineError::InvalidStructure {
4921 reason: "untransformed X509Data RetrievalMethod must target X509Data directly"
4922 }
4923 ));
4924 }
4925
4926 #[test]
4927 fn retrieval_method_rejects_target_inside_external_x509_data_ancestor() {
4928 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4931 <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>
4932 <ds:X509Data><ds:X509SubjectName Id="target">CN=leaf</ds:X509SubjectName></ds:X509Data>
4933 </root>"##;
4934 let document = Document::parse(xml).unwrap();
4935 let key_info_node = document
4936 .descendants()
4937 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
4938 .unwrap();
4939 let mut key_info = parse_key_info(key_info_node).unwrap();
4940
4941 let error = materialize_retrieval_methods(
4942 &mut key_info,
4943 &UriReferenceResolver::new(&document),
4944 UriTypeSet::SAME_DOCUMENT,
4945 None,
4946 crate::provider::default_provider(),
4947 )
4948 .expect_err("filter output without an X509Data root must be rejected");
4949 assert!(matches!(
4950 error,
4951 SignatureVerificationPipelineError::InvalidStructure {
4952 reason: "X509Data RetrievalMethod selected no X509Data element"
4953 }
4954 ));
4955 }
4956
4957 #[test]
4958 fn retrieval_method_rejects_ambiguous_x509_data_relation() {
4959 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4961 <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>
4962 <holder Id="target"><ds:X509Data/><ds:X509Data/></holder>
4963 </root>"##;
4964 let document = Document::parse(xml).unwrap();
4965 let key_info_node = document
4966 .descendants()
4967 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
4968 .unwrap();
4969 let mut key_info = parse_key_info(key_info_node).unwrap();
4970
4971 let error = materialize_retrieval_methods(
4972 &mut key_info,
4973 &UriReferenceResolver::new(&document),
4974 UriTypeSet::SAME_DOCUMENT,
4975 None,
4976 crate::provider::default_provider(),
4977 )
4978 .expect_err("multiple transformed X509Data roots must be rejected");
4979 assert!(matches!(
4980 error,
4981 SignatureVerificationPipelineError::InvalidStructure {
4982 reason: "X509Data RetrievalMethod selected multiple X509Data elements"
4983 }
4984 ));
4985 }
4986
4987 #[test]
4988 fn retrieval_method_materialization_preserves_key_info_order() {
4989 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4992 <ds:KeyInfo>
4993 <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>
4994 <ds:KeyName>fallback</ds:KeyName>
4995 </ds:KeyInfo>
4996 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
4997 </root>"##;
4998 let document = Document::parse(xml).unwrap();
4999 let key_info_node = document
5000 .descendants()
5001 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5002 .unwrap();
5003 let mut key_info = parse_key_info(key_info_node).unwrap();
5004
5005 materialize_retrieval_methods(
5006 &mut key_info,
5007 &UriReferenceResolver::new(&document),
5008 UriTypeSet::SAME_DOCUMENT,
5009 None,
5010 crate::provider::default_provider(),
5011 )
5012 .unwrap();
5013 assert!(matches!(
5014 key_info.sources.as_slice(),
5015 [
5016 super::super::parse::KeyInfoSource::X509Data(_),
5017 super::super::parse::KeyInfoSource::KeyName(name)
5018 ] if name == "fallback"
5019 ));
5020 }
5021
5022 #[test]
5023 fn retrieval_method_materialization_bounds_repeated_sources() {
5024 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
5027 let certificate = include_bytes!(
5028 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
5029 )
5030 .to_vec();
5031 let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
5032 let mut key_info = KeyInfo {
5033 sources: (0..=64)
5034 .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
5035 uri: "urn:certificate".into(),
5036 resource_type: Some(RAW_X509_TYPE.into()),
5037 transforms: RetrievalMethodTransforms::None,
5038 })
5039 .collect(),
5040 };
5041 let document = Document::parse("<root/>").unwrap();
5042 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5043
5044 let error = materialize_retrieval_methods(
5045 &mut key_info,
5046 &resolver,
5047 UriTypeSet::ALL,
5048 None,
5049 crate::provider::default_provider(),
5050 )
5051 .expect_err("retrieval count must be bounded before materialization");
5052 assert!(matches!(
5053 error,
5054 SignatureVerificationPipelineError::InvalidStructure {
5055 reason: "KeyInfo contains too many RetrievalMethod elements"
5056 }
5057 ));
5058 }
5059
5060 #[test]
5061 fn retrieval_method_materialization_deduplicates_within_count_limit() {
5062 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
5065 let certificate = include_bytes!(
5066 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
5067 )
5068 .to_vec();
5069 let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
5070 let mut key_info = KeyInfo {
5071 sources: (0..MAX_RETRIEVAL_METHOD_COUNT)
5072 .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
5073 uri: "urn:certificate".into(),
5074 resource_type: Some(RAW_X509_TYPE.into()),
5075 transforms: RetrievalMethodTransforms::None,
5076 })
5077 .collect(),
5078 };
5079 let document = Document::parse("<root/>").unwrap();
5080 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5081
5082 materialize_retrieval_methods(
5083 &mut key_info,
5084 &resolver,
5085 UriTypeSet::ALL,
5086 None,
5087 crate::provider::default_provider(),
5088 )
5089 .unwrap();
5090 assert!(matches!(
5091 key_info.sources.as_slice(),
5092 [super::super::parse::KeyInfoSource::X509Data(info)]
5093 if info.certificates.len() == 1
5094 ));
5095 }
5096
5097 #[test]
5098 fn retrieval_method_candidate_budget_includes_embedded_key_values() {
5099 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
5102 let resources = HashMap::from([("urn:certificate".to_string(), vec![1, 2, 3])]);
5103 let mut key_info = KeyInfo {
5104 sources: vec![
5105 super::super::parse::KeyInfoSource::KeyValue(
5106 super::super::parse::KeyValueInfo::Unsupported {
5107 namespace: Some(XMLDSIG_NS.into()),
5108 local_name: "FutureKeyValue".into(),
5109 },
5110 ),
5111 super::super::parse::KeyInfoSource::RetrievalMethod {
5112 uri: "urn:certificate".into(),
5113 resource_type: Some(RAW_X509_TYPE.into()),
5114 transforms: RetrievalMethodTransforms::None,
5115 },
5116 ],
5117 };
5118 let document = Document::parse("<root/>").unwrap();
5119 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5120 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
5121 let execution_budget = TransformExecutionBudget::default();
5122 let resource_policy = crate::policy::ResourcePolicy {
5123 max_key_candidates: 1,
5124 ..crate::policy::ResourcePolicy::default()
5125 };
5126 let mut budgets = RetrievalMaterializationBudgets {
5127 xpath_parse: &mut xpath_parse_budget,
5128 execution: &execution_budget,
5129 resources: &resource_policy,
5130 };
5131
5132 let error = materialize_retrieval_methods_with_budgets(
5133 &mut key_info,
5134 &resolver,
5135 UriTypeSet::ALL,
5136 None,
5137 crate::provider::default_provider(),
5138 &mut budgets,
5139 )
5140 .expect_err("the retrieved certificate must exceed the aggregate candidate limit");
5141
5142 assert!(matches!(
5143 error,
5144 SignatureVerificationPipelineError::Policy(
5145 crate::policy::PolicyViolation::ResourceLimit {
5146 resource: crate::policy::resource_name::KEY_CANDIDATES,
5147 maximum: 1,
5148 actual: 2,
5149 }
5150 )
5151 ));
5152 }
5153
5154 #[test]
5155 fn raw_x509_retrieval_rejects_empty_same_document_uri() {
5156 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
5159 let certificate = include_bytes!(
5160 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
5161 )
5162 .to_vec();
5163 let resources = HashMap::from([(String::new(), certificate)]);
5164 let mut key_info = KeyInfo {
5165 sources: vec![super::super::parse::KeyInfoSource::RetrievalMethod {
5166 uri: String::new(),
5167 resource_type: Some(RAW_X509_TYPE.into()),
5168 transforms: RetrievalMethodTransforms::None,
5169 }],
5170 };
5171 let document = Document::parse("<root/>").unwrap();
5172 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5173
5174 let error = materialize_retrieval_methods(
5175 &mut key_info,
5176 &resolver,
5177 UriTypeSet::ALL,
5178 None,
5179 crate::provider::default_provider(),
5180 )
5181 .expect_err("empty URI must retain same-document semantics");
5182 assert!(matches!(
5183 error,
5184 SignatureVerificationPipelineError::InvalidStructure {
5185 reason: "raw X509 RetrievalMethod requires an untransformed external URI"
5186 }
5187 ));
5188 }
5189
5190 #[test]
5191 fn verify_context_does_not_hide_malformed_digest_behind_unsupported_transform() {
5192 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5194 let xml = xml.replacen(
5195 "<ds:Reference URI=\"#target\">",
5196 "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
5197 1,
5198 );
5199 replace_fixture_manifest_digest(&xml, "!!!")
5200 });
5201
5202 let error = VerifyContext::new()
5203 .key(&AcceptingKey)
5204 .process_manifests(true)
5205 .verify(&broken_xml)
5206 .expect_err("malformed Manifest digest must not become a validity result");
5207 assert!(matches!(
5208 error,
5209 SignatureVerificationPipelineError::ParseManifestReference(_)
5210 ));
5211 }
5212
5213 #[test]
5214 fn verify_context_rejects_manifest_non_whitespace_mixed_content() {
5215 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5218 xml.replacen(
5219 "<ds:Manifest ID=\"manifest\">",
5220 "<ds:Manifest ID=\"manifest\">junk",
5221 1,
5222 )
5223 });
5224
5225 let err = VerifyContext::new()
5226 .key(&AcceptingKey)
5227 .process_manifests(true)
5228 .verify(&xml)
5229 .expect_err("Manifest mixed content must fail verification");
5230 assert!(matches!(
5231 err,
5232 SignatureVerificationPipelineError::InvalidStructure {
5233 reason: "Manifest contains non-whitespace mixed content"
5234 }
5235 ));
5236 }
5237
5238 #[test]
5239 fn verify_context_rejects_empty_manifest_children() {
5240 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5243 let (prefix, rest) = xml
5244 .split_once("<ds:Manifest ID=\"manifest\">")
5245 .expect("fixture should contain Manifest");
5246 let (_, suffix) = rest
5247 .split_once("</ds:Manifest>")
5248 .expect("fixture should contain closing Manifest");
5249 format!("{prefix}<ds:Manifest ID=\"manifest\"></ds:Manifest>{suffix}")
5250 });
5251
5252 let err = VerifyContext::new()
5253 .key(&AcceptingKey)
5254 .process_manifests(true)
5255 .verify(&xml)
5256 .expect_err("empty Manifest must fail verification");
5257 assert!(matches!(
5258 err,
5259 SignatureVerificationPipelineError::InvalidStructure {
5260 reason: "Manifest must contain at least one ds:Reference element child"
5261 }
5262 ));
5263 }
5264
5265 #[test]
5266 fn verify_context_ignores_unsigned_malformed_manifest_blocks() {
5267 let xml = signature_with_manifest_xml(true).replacen(
5268 "</ds:Object>",
5269 "</ds:Object><ds:Object><ds:Manifest>junk<ds:Foo/></ds:Manifest></ds:Object>",
5270 1,
5271 );
5272 let result = VerifyContext::new()
5273 .key(&AcceptingKey)
5274 .process_manifests(true)
5275 .verify(&xml)
5276 .expect("unsigned malformed Manifest must be ignored");
5277 assert_eq!(
5278 result.manifest_references.len(),
5279 1,
5280 "only signed Manifest references must be reported",
5281 );
5282 assert!(matches!(result.status, DsigStatus::Valid));
5283 }
5284
5285 #[test]
5286 fn verify_context_skips_ambiguous_manifest_id_blocks() {
5287 let xml = signature_with_manifest_xml(true).replacen(
5288 "</ds:Object>",
5289 "</ds:Object><ds:Object><ds:Manifest ID=\"manifest\">junk<ds:Foo/></ds:Manifest></ds:Object>",
5290 1,
5291 );
5292 let err = VerifyContext::new()
5293 .key(&RejectingKey)
5294 .process_manifests(true)
5295 .verify(&xml)
5296 .expect_err("ambiguous manifest IDs should make SignedInfo #manifest dereference fail");
5297 assert!(matches!(
5298 err,
5299 SignatureVerificationPipelineError::Reference(
5300 ReferenceProcessingError::UriDereference(
5301 crate::xmldsig::types::TransformError::ElementNotFound(id)
5302 )
5303 ) if id == "manifest"
5304 ));
5305 }
5306
5307 #[test]
5308 fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
5309 let xml = minimal_signature_xml("", "");
5310 let err = VerifyContext::new()
5311 .key(&RejectingKey)
5312 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
5313 .verify(&xml)
5314 .expect_err("implicit default C14N must be checked against allowlist");
5315 assert!(matches!(
5316 err,
5317 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
5318 operation: "verification transform",
5319 ..
5320 })
5321 ));
5322 }
5323
5324 #[test]
5325 fn verify_context_skips_resolver_when_reference_processing_fails() {
5326 let xml = minimal_signature_xml("", "");
5327 let result = VerifyContext::new()
5328 .key_resolver(&PanicResolver)
5329 .verify(&xml)
5330 .expect("reference digest mismatch should short-circuit before resolver");
5331 assert!(matches!(
5332 result.status,
5333 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
5334 ));
5335 }
5336
5337 #[test]
5338 fn verify_context_reports_key_not_found_when_resolver_misses() {
5339 let xml = signature_with_target_reference("AQ==");
5340 let result = VerifyContext::new()
5341 .key_resolver(&MissingKeyResolver)
5342 .verify(&xml)
5343 .expect("resolver miss should report status, not pipeline error");
5344 assert!(matches!(
5345 result.status,
5346 DsigStatus::Invalid(FailureReason::KeyNotFound)
5347 ));
5348 assert_eq!(
5349 result.signed_info_references.len(),
5350 1,
5351 "KeyNotFound path must preserve SignedInfo reference diagnostics",
5352 );
5353 assert!(matches!(
5354 result.signed_info_references[0].status,
5355 DsigStatus::Valid
5356 ));
5357 }
5358
5359 #[test]
5360 fn verification_candidate_budget_covers_preset_and_custom_resolver_paths() {
5361 let xml = signature_with_target_reference("AQ==");
5364 let mut policy = crate::policy::VerificationPolicy::default();
5365 policy.resources.max_key_candidates = 0;
5366
5367 let preset_error = VerifyContext::new()
5368 .key(&RejectingKey)
5369 .policy(policy.clone())
5370 .verify(&xml)
5371 .expect_err("a preset key consumes one candidate");
5372 assert!(matches!(
5373 preset_error,
5374 SignatureVerificationPipelineError::Policy(
5375 crate::policy::PolicyViolation::ResourceLimit {
5376 resource: crate::policy::resource_name::KEY_CANDIDATES,
5377 maximum: 0,
5378 actual: 1,
5379 }
5380 )
5381 ));
5382
5383 let resolver_error = VerifyContext::new()
5384 .key_resolver(&PanicResolver)
5385 .policy(policy)
5386 .verify(&xml)
5387 .expect_err("a custom resolver requires candidate capacity before dispatch");
5388 assert!(matches!(
5389 resolver_error,
5390 SignatureVerificationPipelineError::Policy(
5391 crate::policy::PolicyViolation::ResourceLimit {
5392 resource: crate::policy::resource_name::KEY_CANDIDATES,
5393 maximum: 0,
5394 actual: 1,
5395 }
5396 )
5397 ));
5398 }
5399
5400 #[test]
5401 fn verification_candidate_budget_precedes_embedded_x509_parsing() {
5402 let first_certificate = base64::engine::general_purpose::STANDARD.encode(include_bytes!(
5405 "../../tests/fixtures/xmldsig/phaos-xmldsig-three/certs/rsa-cert.der"
5406 ));
5407 let xml = signature_with_target_reference("AQ==").replace(
5408 "</ds:SignatureValue>\n </ds:Signature>",
5409 &format!(
5410 "</ds:SignatureValue>\n <ds:KeyInfo><ds:X509Data><ds:X509Certificate>{first_certificate}</ds:X509Certificate><ds:X509Certificate>AQID</ds:X509Certificate></ds:X509Data></ds:KeyInfo>\n </ds:Signature>"
5411 ),
5412 );
5413 let mut policy = crate::policy::VerificationPolicy::default();
5414 policy.resources.max_key_candidates = 1;
5415
5416 let error = VerifyContext::new()
5417 .policy(policy)
5418 .verify(&xml)
5419 .expect_err("candidate policy must run before embedded certificate parsing");
5420
5421 assert!(matches!(
5422 error,
5423 SignatureVerificationPipelineError::Policy(
5424 crate::policy::PolicyViolation::ResourceLimit {
5425 resource: crate::policy::resource_name::KEY_CANDIDATES,
5426 maximum: 1,
5427 actual: 2,
5428 }
5429 )
5430 ));
5431 }
5432
5433 #[test]
5434 fn verify_context_resolver_can_ignore_malformed_keyinfo_by_default() {
5435 let base_xml = signature_with_target_reference("AQ==");
5436 let xml = base_xml
5437 .replace(
5438 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
5439 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
5440 )
5441 .replace(
5442 "</ds:SignatureValue>\n </ds:Signature>",
5443 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
5444 );
5445
5446 let result = VerifyContext::new()
5447 .key_resolver(&MissingKeyResolver)
5448 .verify(&xml)
5449 .expect("resolver path should not hard-fail on advisory malformed KeyInfo by default");
5450 assert!(matches!(
5451 result.status,
5452 DsigStatus::Invalid(FailureReason::KeyNotFound)
5453 ));
5454 }
5455
5456 #[test]
5457 fn verify_context_resolver_can_opt_in_to_keyinfo_parse_failures() {
5458 let base_xml = signature_with_target_reference("AQ==");
5459 let xml = base_xml
5460 .replace(
5461 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
5462 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
5463 )
5464 .replace(
5465 "</ds:SignatureValue>\n </ds:Signature>",
5466 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
5467 );
5468
5469 let err = VerifyContext::new()
5470 .key_resolver(&ConsumingKeyInfoResolver)
5471 .verify(&xml)
5472 .expect_err("resolver opted into KeyInfo parsing, malformed KeyInfo must fail");
5473 assert!(matches!(
5474 err,
5475 SignatureVerificationPipelineError::ParseKeyInfo(_)
5476 ));
5477 }
5478
5479 #[test]
5480 fn verify_context_ignores_unsupported_retrieval_before_valid_key_source() {
5481 let xml = signature_with_target_reference("AQ==").replace(
5484 "</ds:SignatureValue>\n </ds:Signature>",
5485 r##"</ds:SignatureValue>
5486 <ds:KeyInfo>
5487 <ds:RetrievalMethod URI="#vendor" Type="urn:vendor:key">
5488 <ds:Transforms><ds:Transform Algorithm="urn:vendor:transform"/></ds:Transforms>
5489 </ds:RetrievalMethod>
5490 <ds:KeyName>fallback</ds:KeyName>
5491 </ds:KeyInfo>
5492 </ds:Signature>"##,
5493 );
5494
5495 let result = VerifyContext::new()
5496 .key_resolver(&FallbackKeyInfoResolver)
5497 .verify(&xml)
5498 .expect("unsupported advisory retrieval must not abort key resolution");
5499 assert_eq!(result.status, DsigStatus::Valid);
5500 }
5501
5502 #[test]
5503 fn verify_context_does_not_eagerly_fail_unused_retrieval_fallback() {
5504 let xml = signature_with_target_reference("AQ==").replace(
5507 "</ds:SignatureValue>\n </ds:Signature>",
5508 r#"</ds:SignatureValue>
5509 <ds:KeyInfo>
5510 <ds:KeyName>primary</ds:KeyName>
5511 <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
5512 </ds:KeyInfo>
5513 </ds:Signature>"#,
5514 );
5515
5516 let result = VerifyContext::new()
5517 .key_resolver(&EarlyKeyInfoResolver)
5518 .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
5519 .verify(&xml)
5520 .expect("an unused missing retrieval fallback must not abort verification");
5521
5522 assert_eq!(result.status, DsigStatus::Valid);
5523 }
5524
5525 #[test]
5526 fn verify_context_does_not_eagerly_parse_unused_retrieval_fallback() {
5527 let xml = signature_with_target_reference("AQ==").replace(
5530 "</ds:SignatureValue>\n </ds:Signature>",
5531 r#"</ds:SignatureValue>
5532 <ds:KeyInfo>
5533 <ds:KeyName>primary</ds:KeyName>
5534 <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
5535 </ds:KeyInfo>
5536 </ds:Signature>"#,
5537 );
5538 let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
5539
5540 let result = VerifyContext::new()
5541 .key_resolver(&EarlyKeyInfoResolver)
5542 .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
5543 .external_resources(&resources)
5544 .verify(&xml)
5545 .expect("an unused malformed retrieval fallback must not abort verification");
5546
5547 assert_eq!(result.status, DsigStatus::Valid);
5548 }
5549
5550 #[test]
5551 fn verify_context_reports_missing_retrieval_when_no_key_source_resolves() {
5552 let xml = signature_with_target_reference("AQ==").replace(
5555 "</ds:SignatureValue>\n </ds:Signature>",
5556 r#"</ds:SignatureValue>
5557 <ds:KeyInfo>
5558 <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
5559 </ds:KeyInfo>
5560 </ds:Signature>"#,
5561 );
5562
5563 let error = VerifyContext::new()
5564 .key_resolver(&ConsumingKeyInfoResolver)
5565 .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
5566 .verify(&xml)
5567 .expect_err("a missing sole RetrievalMethod must remain an explicit error");
5568
5569 assert!(matches!(
5570 error,
5571 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
5572 crate::xmldsig::TransformError::UnsupportedUri(uri)
5573 )) if uri == "missing.der"
5574 ));
5575 }
5576
5577 #[test]
5578 fn verify_context_reports_malformed_retrieval_when_no_key_source_resolves() {
5579 let xml = signature_with_target_reference("AQ==").replace(
5582 "</ds:SignatureValue>\n </ds:Signature>",
5583 r#"</ds:SignatureValue>
5584 <ds:KeyInfo>
5585 <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
5586 </ds:KeyInfo>
5587 </ds:Signature>"#,
5588 );
5589 let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
5590
5591 let error = VerifyContext::new()
5592 .key_resolver(&ConsumingKeyInfoResolver)
5593 .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
5594 .external_resources(&resources)
5595 .verify(&xml)
5596 .expect_err("a malformed sole RetrievalMethod must remain a parse error");
5597
5598 assert!(matches!(
5599 error,
5600 SignatureVerificationPipelineError::ParseKeyInfo(_)
5601 ));
5602 }
5603
5604 #[test]
5605 fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
5606 let xml = signature_with_target_reference("@@@");
5607
5608 let err = VerifyContext::new()
5609 .key_resolver(&MissingKeyResolver)
5610 .verify(&xml)
5611 .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
5612 assert!(matches!(
5613 err,
5614 SignatureVerificationPipelineError::SignatureValueBase64(_)
5615 ));
5616 }
5617
5618 #[test]
5619 fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
5620 let xml = signature_with_target_reference("@@@");
5621
5622 let err = VerifyContext::new()
5623 .verify(&xml)
5624 .expect_err("invalid SignatureValue must remain a decode error");
5625 assert!(matches!(
5626 err,
5627 SignatureVerificationPipelineError::SignatureValueBase64(_)
5628 ));
5629 }
5630
5631 #[test]
5632 fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
5633 let references = vec![Reference {
5634 uri: None,
5635 id: None,
5636 ref_type: None,
5637 transforms: vec![],
5638 digest_method: DigestAlgorithm::Sha256,
5639 digest_value: vec![0; 32],
5640 }];
5641 let uri_types = UriTypeSet {
5642 allow_empty: false,
5643 allow_same_document: true,
5644 allow_external: false,
5645 };
5646
5647 let err = enforce_reference_policies(&references, uri_types, None)
5648 .expect_err("missing URI must fail before allow_empty policy is evaluated");
5649 assert!(matches!(
5650 err,
5651 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
5652 ));
5653 }
5654
5655 #[test]
5656 fn enforce_reference_policies_checks_only_terminal_binary_output() {
5657 let c14n = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI).unwrap();
5658 let allowed = HashSet::from([
5659 BASE64_TRANSFORM_URI.to_owned(),
5660 DEFAULT_IMPLICIT_C14N_URI.to_owned(),
5661 ]);
5662 let without_implicit_c14n = HashSet::from([BASE64_TRANSFORM_URI.to_owned()]);
5663
5664 for transforms in [
5665 vec![Transform::Base64Decode, Transform::C14n(c14n)],
5666 vec![Transform::Base64Decode, Transform::Base64Decode],
5667 ] {
5668 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, vec![0; 32]);
5669 enforce_reference_policies(
5670 std::slice::from_ref(&reference),
5671 UriTypeSet::default(),
5672 Some(&allowed),
5673 )
5674 .expect("terminal binary output must not require implicit C14N");
5675 }
5676
5677 let terminal_base64 = make_reference(
5678 "",
5679 vec![Transform::Base64Decode, Transform::Base64Decode],
5680 DigestAlgorithm::Sha256,
5681 vec![0; 32],
5682 );
5683 enforce_reference_policies(
5684 std::slice::from_ref(&terminal_base64),
5685 UriTypeSet::default(),
5686 Some(&without_implicit_c14n),
5687 )
5688 .expect("terminal Base64 output must not require implicit C14N");
5689
5690 let no_transforms = make_reference("", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
5691 let error = enforce_reference_policies(
5692 std::slice::from_ref(&no_transforms),
5693 UriTypeSet::default(),
5694 Some(&without_implicit_c14n),
5695 )
5696 .expect_err("a node-set result must require allowlisted implicit C14N");
5697 assert!(matches!(
5698 error,
5699 SignatureVerificationPipelineError::Policy(
5700 crate::policy::PolicyViolation::Algorithm {
5701 operation: "verification transform",
5702 ref algorithm,
5703 }
5704 )
5705 if algorithm == DEFAULT_IMPLICIT_C14N_URI
5706 ));
5707
5708 let detached = make_reference("urn:payload", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
5709 enforce_reference_policies(
5710 std::slice::from_ref(&detached),
5711 UriTypeSet::ALL,
5712 Some(&without_implicit_c14n),
5713 )
5714 .expect("external octets without transforms must not require implicit C14N");
5715
5716 let external_xpath = make_reference(
5717 "urn:payload",
5718 vec![Transform::XPath(
5719 super::super::transforms::XPathExpression::new("true()"),
5720 )],
5721 DigestAlgorithm::Sha256,
5722 vec![0; 32],
5723 );
5724 let error = enforce_reference_policies(
5725 std::slice::from_ref(&external_xpath),
5726 UriTypeSet::ALL,
5727 Some(&HashSet::from([XPATH_TRANSFORM_URI.to_owned()])),
5728 )
5729 .expect_err("external XML converted to a node-set must require implicit C14N");
5730 assert!(matches!(
5731 error,
5732 SignatureVerificationPipelineError::Policy(
5733 crate::policy::PolicyViolation::Algorithm {
5734 operation: "verification transform",
5735 ref algorithm,
5736 }
5737 )
5738 if algorithm == DEFAULT_IMPLICIT_C14N_URI
5739 ));
5740 }
5741
5742 #[test]
5743 fn stored_pre_digest_budget_counts_repeated_external_references() {
5744 let document =
5747 Document::parse("<ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"/>")
5748 .unwrap();
5749 let payload = vec![b'x'; 7];
5750 let digest = compute_digest(DigestAlgorithm::Sha256, &payload);
5751 let references = (0..5)
5752 .map(|_| {
5753 make_reference(
5754 "urn:repeated",
5755 Vec::new(),
5756 DigestAlgorithm::Sha256,
5757 digest.clone(),
5758 )
5759 })
5760 .collect::<Vec<_>>();
5761 let resources = HashMap::from([("urn:repeated".to_owned(), payload)]);
5762 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5763 let transform_budget = TransformExecutionBudget::default();
5764 let canonicalized_data_budget = CanonicalizedDataBudget::with_limit(32);
5765 let execution = ReferenceExecutionContext {
5766 store_pre_digest: true,
5767 transform_options: TransformOptions::default(),
5768 transform_budget: &transform_budget,
5769 canonicalized_data_budget: &canonicalized_data_budget,
5770 provider: crate::provider::default_provider(),
5771 };
5772
5773 let error = process_all_references_with_options(
5774 &references,
5775 &resolver,
5776 document.root_element(),
5777 &execution,
5778 )
5779 .expect_err(
5780 "retained diagnostics must not multiply one external allocation past the aggregate cap",
5781 );
5782 assert!(matches!(
5783 error,
5784 ReferenceProcessingError::Policy(crate::policy::PolicyViolation::ResourceLimit {
5785 resource: "canonicalized bytes",
5786 maximum: 32,
5787 ..
5788 })
5789 ));
5790 }
5791
5792 #[test]
5793 fn canonical_signed_info_obeys_policy_without_diagnostic_retention() {
5794 let xml = signature_with_target_reference("AQ==");
5798 let marker = "<ds:SignatureMethod";
5799 let padding = " ".repeat(1_025);
5800 let xml = xml.replacen(marker, &format!("{padding}{marker}"), 1);
5801 let policy = crate::policy::VerificationPolicy {
5802 resources: crate::policy::ResourcePolicy {
5803 max_canonicalized_bytes: 1_024,
5804 ..crate::policy::ResourcePolicy::default()
5805 },
5806 ..crate::policy::VerificationPolicy::default()
5807 };
5808
5809 let error = VerifyContext::new()
5810 .key(&AcceptingKey)
5811 .policy(policy)
5812 .verify(&xml)
5813 .expect_err("canonicalized SignedInfo must remain policy-bounded");
5814
5815 assert!(matches!(
5816 error,
5817 SignatureVerificationPipelineError::Policy(
5818 crate::policy::PolicyViolation::ResourceLimit {
5819 resource: "canonicalized bytes",
5820 ..
5821 }
5822 )
5823 ));
5824 }
5825
5826 #[test]
5827 fn push_normalized_signature_text_rejects_form_feed() {
5828 let mut normalized = Vec::new();
5829 let mut raw_text_len = 0usize;
5830 let err =
5831 push_normalized_signature_text("ab\u{000C}cd", &mut raw_text_len, &mut normalized)
5832 .expect_err("form-feed must not be treated as XML base64 whitespace");
5833 assert!(matches!(
5834 err,
5835 SignatureVerificationPipelineError::SignatureValueBase64(
5836 base64::DecodeError::InvalidByte(_, 0x0C)
5837 )
5838 ));
5839 }
5840
5841 #[test]
5842 fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
5843 let mut normalized = vec![b'A'; MAX_SIGNATURE_VALUE_LEN - 1];
5844 let mut raw_text_len = normalized.len();
5845 let err = push_normalized_signature_text("é", &mut raw_text_len, &mut normalized)
5846 .expect_err("multibyte characters must not bypass byte-size limit");
5847 assert!(matches!(
5848 err,
5849 SignatureVerificationPipelineError::InvalidStructure {
5850 reason: "SignatureValue exceeds maximum allowed length"
5851 }
5852 ));
5853 }
5854
5855 #[test]
5858 fn reference_with_correct_digest_passes() {
5859 let xml = r##"<root>
5862 <data>hello world</data>
5863 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
5864 <ds:SignedInfo/>
5865 </ds:Signature>
5866 </root>"##;
5867 let doc = Document::parse(xml).unwrap();
5868 let resolver = UriReferenceResolver::new(&doc);
5869 let sig_node = doc
5870 .descendants()
5871 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
5872 .unwrap();
5873
5874 let initial_data = resolver.dereference("").unwrap();
5876 let transforms = vec![
5877 Transform::Enveloped,
5878 Transform::C14n(
5879 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
5880 .unwrap(),
5881 ),
5882 ];
5883 let pre_digest_bytes =
5884 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
5885 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
5886
5887 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
5889
5890 let result = process_reference(
5891 &reference,
5892 &resolver,
5893 sig_node,
5894 ReferenceSet::SignedInfo,
5895 0,
5896 false,
5897 )
5898 .unwrap();
5899 assert!(
5900 matches!(result.status, DsigStatus::Valid),
5901 "digest should match"
5902 );
5903 assert!(result.pre_digest_data.is_none());
5904 }
5905
5906 #[test]
5907 fn reference_with_wrong_digest_fails() {
5908 let xml = r##"<root>
5909 <data>hello</data>
5910 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5911 <ds:SignedInfo/>
5912 </ds:Signature>
5913 </root>"##;
5914 let doc = Document::parse(xml).unwrap();
5915 let resolver = UriReferenceResolver::new(&doc);
5916 let sig_node = doc
5917 .descendants()
5918 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
5919 .unwrap();
5920
5921 let transforms = vec![Transform::Enveloped];
5922 let wrong_digest = vec![0u8; 32];
5924 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
5925
5926 let result = process_reference(
5927 &reference,
5928 &resolver,
5929 sig_node,
5930 ReferenceSet::SignedInfo,
5931 0,
5932 false,
5933 )
5934 .unwrap();
5935 assert!(matches!(
5936 result.status,
5937 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
5938 ));
5939 }
5940
5941 #[test]
5942 fn reference_with_wrong_digest_preserves_supplied_ref_index() {
5943 let xml = r##"<root>
5944 <data>hello</data>
5945 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5946 <ds:SignedInfo/>
5947 </ds:Signature>
5948 </root>"##;
5949 let doc = Document::parse(xml).unwrap();
5950 let resolver = UriReferenceResolver::new(&doc);
5951 let sig_node = doc
5952 .descendants()
5953 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
5954 .unwrap();
5955
5956 let reference = make_reference(
5957 "",
5958 vec![Transform::Enveloped],
5959 DigestAlgorithm::Sha256,
5960 vec![0u8; 32],
5961 );
5962 let result = process_reference(
5963 &reference,
5964 &resolver,
5965 sig_node,
5966 ReferenceSet::SignedInfo,
5967 7,
5968 false,
5969 )
5970 .unwrap();
5971 assert!(matches!(
5972 result.status,
5973 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
5974 ));
5975 }
5976
5977 #[test]
5978 fn reference_stores_pre_digest_data() {
5979 let xml = "<root><child>text</child></root>";
5980 let doc = Document::parse(xml).unwrap();
5981 let resolver = UriReferenceResolver::new(&doc);
5982
5983 let initial_data = resolver.dereference("").unwrap();
5985 let pre_digest =
5986 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
5987 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
5988
5989 let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
5990 let result = process_reference(
5991 &reference,
5992 &resolver,
5993 doc.root_element(),
5994 ReferenceSet::SignedInfo,
5995 0,
5996 true,
5997 )
5998 .unwrap();
5999
6000 assert!(matches!(result.status, DsigStatus::Valid));
6001 assert!(result.pre_digest_data.is_some());
6002 assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
6003 }
6004
6005 #[test]
6008 fn reference_with_id_uri() {
6009 let xml = r##"<root>
6010 <item ID="target">specific content</item>
6011 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6012 <ds:SignedInfo/>
6013 </ds:Signature>
6014 </root>"##;
6015 let doc = Document::parse(xml).unwrap();
6016 let resolver = UriReferenceResolver::new(&doc);
6017 let sig_node = doc
6018 .descendants()
6019 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
6020 .unwrap();
6021
6022 let initial_data = resolver.dereference("#target").unwrap();
6024 let transforms = vec![Transform::C14n(
6025 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
6026 .unwrap(),
6027 )];
6028 let pre_digest =
6029 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
6030 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
6031
6032 let reference = make_reference(
6033 "#target",
6034 transforms,
6035 DigestAlgorithm::Sha256,
6036 expected_digest,
6037 );
6038 let result = process_reference(
6039 &reference,
6040 &resolver,
6041 sig_node,
6042 ReferenceSet::SignedInfo,
6043 0,
6044 false,
6045 )
6046 .unwrap();
6047 assert!(matches!(result.status, DsigStatus::Valid));
6048 }
6049
6050 #[test]
6051 fn reference_with_nonexistent_id_fails() {
6052 let xml = "<root><child/></root>";
6053 let doc = Document::parse(xml).unwrap();
6054 let resolver = UriReferenceResolver::new(&doc);
6055
6056 let reference =
6057 make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
6058 let result = process_reference(
6059 &reference,
6060 &resolver,
6061 doc.root_element(),
6062 ReferenceSet::SignedInfo,
6063 0,
6064 false,
6065 );
6066 assert!(result.is_err());
6067 }
6068
6069 #[test]
6070 fn reference_with_absent_uri_fails_closed() {
6071 let xml = "<root><child>text</child></root>";
6072 let doc = Document::parse(xml).unwrap();
6073 let resolver = UriReferenceResolver::new(&doc);
6074
6075 let reference = Reference {
6076 uri: None, id: None,
6078 ref_type: None,
6079 transforms: vec![],
6080 digest_method: DigestAlgorithm::Sha256,
6081 digest_value: vec![0; 32],
6082 };
6083
6084 let result = process_reference(
6085 &reference,
6086 &resolver,
6087 doc.root_element(),
6088 ReferenceSet::SignedInfo,
6089 0,
6090 false,
6091 );
6092 assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
6093 }
6094
6095 #[test]
6098 fn all_references_pass() {
6099 let xml = "<root><child>text</child></root>";
6100 let doc = Document::parse(xml).unwrap();
6101 let resolver = UriReferenceResolver::new(&doc);
6102
6103 let initial_data = resolver.dereference("").unwrap();
6105 let pre_digest =
6106 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
6107 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
6108
6109 let refs = vec![
6110 make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
6111 make_reference("", vec![], DigestAlgorithm::Sha256, digest),
6112 ];
6113
6114 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
6115 assert!(result.all_valid());
6116 assert_eq!(result.results.len(), 2);
6117 assert!(result.first_failure.is_none());
6118 }
6119
6120 #[test]
6121 fn reference_processing_shares_xpath_work_across_references() {
6122 let document = Document::parse("<root/>").unwrap();
6125 let resolver = UriReferenceResolver::new(&document);
6126 let transform = Transform::XPath(super::super::transforms::XPathExpression::new("true()"));
6127 let initial_data = resolver.dereference("").unwrap();
6128 let pre_digest = crate::xmldsig::execute_transforms(
6129 document.root_element(),
6130 initial_data,
6131 std::slice::from_ref(&transform),
6132 )
6133 .unwrap();
6134 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
6135 let references = vec![
6136 make_reference(
6137 "",
6138 vec![transform.clone()],
6139 DigestAlgorithm::Sha256,
6140 digest.clone(),
6141 ),
6142 make_reference("", vec![transform], DigestAlgorithm::Sha256, digest),
6143 ];
6144 let budget = TransformExecutionBudget::with_xpath_limit(12);
6145 let canonicalized_data_budget = CanonicalizedDataBudget::default();
6146 let execution = ReferenceExecutionContext {
6147 store_pre_digest: false,
6148 transform_options: TransformOptions::default(),
6149 transform_budget: &budget,
6150 canonicalized_data_budget: &canonicalized_data_budget,
6151 provider: crate::provider::default_provider(),
6152 };
6153
6154 let error = process_all_references_with_options(
6155 &references,
6156 &resolver,
6157 document.root_element(),
6158 &execution,
6159 )
6160 .expect_err("the second Reference must consume the first Reference's XPath work");
6161
6162 assert!(matches!(
6163 error,
6164 ReferenceProcessingError::Transform(TransformError::Policy(
6165 crate::policy::PolicyViolation::ResourceLimit {
6166 resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
6167 ..
6168 }
6169 ))
6170 ));
6171 }
6172
6173 #[test]
6174 fn reference_processing_shares_node_set_materialization_across_references() {
6175 let document = Document::parse(
6179 r#"<root xmlns:n="urn:0123456789"><target Id="selected">payload</target></root>"#,
6180 )
6181 .unwrap();
6182 let resolver = UriReferenceResolver::new(&document);
6183 let initial_data = resolver.dereference("#selected").unwrap();
6184 let pre_digest =
6185 crate::xmldsig::execute_transforms(document.root_element(), initial_data, &[]).unwrap();
6186 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
6187 let references = vec![
6188 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest.clone()),
6189 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest),
6190 ];
6191 let budget = TransformExecutionBudget::with_node_set_materialization_limit(30);
6192 let canonicalized_data_budget = CanonicalizedDataBudget::default();
6193 let execution = ReferenceExecutionContext {
6194 store_pre_digest: false,
6195 transform_options: TransformOptions::default(),
6196 transform_budget: &budget,
6197 canonicalized_data_budget: &canonicalized_data_budget,
6198 provider: crate::provider::default_provider(),
6199 };
6200
6201 let error = process_all_references_with_options(
6202 &references,
6203 &resolver,
6204 document.root_element(),
6205 &execution,
6206 )
6207 .expect_err("the second Reference must consume the first Reference's materialization work");
6208
6209 assert!(matches!(
6210 error,
6211 ReferenceProcessingError::UriDereference(TransformError::Policy(
6212 crate::policy::PolicyViolation::ResourceLimit {
6213 resource: crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
6214 ..
6215 }
6216 ))
6217 ));
6218 }
6219
6220 #[test]
6221 fn fail_fast_on_first_mismatch() {
6222 let xml = "<root><child>text</child></root>";
6223 let doc = Document::parse(xml).unwrap();
6224 let resolver = UriReferenceResolver::new(&doc);
6225
6226 let wrong_digest = vec![0u8; 32];
6227 let refs = vec![
6228 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
6229 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
6231 ];
6232
6233 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
6234 assert!(!result.all_valid());
6235 assert_eq!(result.first_failure, Some(0));
6236 assert_eq!(result.results.len(), 1);
6238 assert!(matches!(
6239 result.results[0].status,
6240 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
6241 ));
6242 }
6243
6244 #[test]
6245 fn fail_fast_second_reference() {
6246 let xml = "<root><child>text</child></root>";
6247 let doc = Document::parse(xml).unwrap();
6248 let resolver = UriReferenceResolver::new(&doc);
6249
6250 let initial_data = resolver.dereference("").unwrap();
6252 let pre_digest =
6253 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
6254 let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
6255 let wrong_digest = vec![0u8; 32];
6256
6257 let refs = vec![
6258 make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
6259 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
6260 ];
6261
6262 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
6263 assert!(!result.all_valid());
6264 assert_eq!(result.first_failure, Some(1));
6265 assert_eq!(result.results.len(), 2);
6267 assert!(matches!(result.results[0].status, DsigStatus::Valid));
6268 assert!(matches!(
6269 result.results[1].status,
6270 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
6271 ));
6272 }
6273
6274 #[test]
6275 fn empty_references_list() {
6276 let xml = "<root/>";
6277 let doc = Document::parse(xml).unwrap();
6278 let resolver = UriReferenceResolver::new(&doc);
6279
6280 let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
6281 assert!(result.all_valid());
6282 assert!(result.results.is_empty());
6283 }
6284
6285 #[test]
6288 fn reference_sha1_digest() {
6289 let xml = "<root>content</root>";
6290 let doc = Document::parse(xml).unwrap();
6291 let resolver = UriReferenceResolver::new(&doc);
6292
6293 let initial_data = resolver.dereference("").unwrap();
6294 let pre_digest =
6295 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
6296 let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
6297
6298 let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
6299 let result = process_reference(
6300 &reference,
6301 &resolver,
6302 doc.root_element(),
6303 ReferenceSet::SignedInfo,
6304 0,
6305 false,
6306 )
6307 .unwrap();
6308 assert!(matches!(result.status, DsigStatus::Valid));
6309 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
6310 }
6311
6312 #[test]
6313 fn reference_sha512_digest() {
6314 let xml = "<root>content</root>";
6315 let doc = Document::parse(xml).unwrap();
6316 let resolver = UriReferenceResolver::new(&doc);
6317
6318 let initial_data = resolver.dereference("").unwrap();
6319 let pre_digest =
6320 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
6321 let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
6322
6323 let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
6324 let result = process_reference(
6325 &reference,
6326 &resolver,
6327 doc.root_element(),
6328 ReferenceSet::SignedInfo,
6329 0,
6330 false,
6331 )
6332 .unwrap();
6333 assert!(matches!(result.status, DsigStatus::Valid));
6334 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
6335 }
6336
6337 #[test]
6340 fn saml_enveloped_reference_processing() {
6341 let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
6343 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
6344 ID="_resp1">
6345 <saml:Assertion ID="_assert1">
6346 <saml:Subject>user@example.com</saml:Subject>
6347 </saml:Assertion>
6348 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6349 <ds:SignedInfo>
6350 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6351 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6352 <ds:Reference URI="">
6353 <ds:Transforms>
6354 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
6355 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6356 </ds:Transforms>
6357 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6358 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6359 </ds:Reference>
6360 </ds:SignedInfo>
6361 <ds:SignatureValue>fakesig==</ds:SignatureValue>
6362 </ds:Signature>
6363 </samlp:Response>"##;
6364 let doc = Document::parse(xml).unwrap();
6365 let resolver = UriReferenceResolver::new(&doc);
6366 let sig_node = doc
6367 .descendants()
6368 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
6369 .unwrap();
6370
6371 let signed_info_node = sig_node
6373 .children()
6374 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
6375 .unwrap();
6376 let signed_info = parse_signed_info(signed_info_node).unwrap();
6377 let reference = &signed_info.references[0];
6378
6379 let initial_data = resolver.dereference("").unwrap();
6381 let pre_digest =
6382 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
6383 .unwrap();
6384 let correct_digest = compute_digest(reference.digest_method, &pre_digest);
6385
6386 let corrected_ref = make_reference(
6388 "",
6389 reference.transforms.clone(),
6390 reference.digest_method,
6391 correct_digest,
6392 );
6393
6394 let result = process_reference(
6396 &corrected_ref,
6397 &resolver,
6398 sig_node,
6399 ReferenceSet::SignedInfo,
6400 0,
6401 true,
6402 )
6403 .unwrap();
6404 assert!(
6405 matches!(result.status, DsigStatus::Valid),
6406 "SAML reference should verify"
6407 );
6408 assert!(result.pre_digest_data.is_some());
6409
6410 let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
6412 assert!(
6413 pre_digest_str.contains("samlp:Response"),
6414 "pre-digest should contain Response"
6415 );
6416 assert!(
6417 !pre_digest_str.contains("SignatureValue"),
6418 "pre-digest should NOT contain Signature"
6419 );
6420 }
6421
6422 #[test]
6423 fn pipeline_missing_signed_info_returns_missing_element() {
6424 let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
6425
6426 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
6427 .expect_err("missing SignedInfo must fail before crypto stage");
6428 assert!(matches!(
6429 err,
6430 SignatureVerificationPipelineError::MissingElement {
6431 element: "SignedInfo"
6432 }
6433 ));
6434 }
6435
6436 #[test]
6437 fn pipeline_multiple_signature_elements_are_rejected() {
6438 let xml = r#"
6439<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6440 <ds:Signature>
6441 <ds:SignedInfo/>
6442 </ds:Signature>
6443 <ds:Signature/>
6444</root>
6445"#;
6446
6447 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
6448 .expect_err("multiple signatures must fail closed");
6449 assert!(matches!(
6450 err,
6451 SignatureVerificationPipelineError::InvalidStructure {
6452 reason: "Signature must appear exactly once in document",
6453 }
6454 ));
6455 }
6456
6457 #[test]
6458 fn pipeline_start_node_limits_signature_cardinality_to_its_subtree() {
6459 let xml = r#"
6462<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6463 <scope Id="selected"><ds:Signature/></scope>
6464 <scope Id="other"><ds:Signature/></scope>
6465</root>
6466"#;
6467 let err = VerifyContext::new()
6468 .start_node_id("selected")
6469 .verify(xml)
6470 .expect_err("the selected Signature remains structurally incomplete");
6471 assert!(matches!(
6472 err,
6473 SignatureVerificationPipelineError::MissingElement {
6474 element: "SignedInfo"
6475 }
6476 ));
6477 }
6478
6479 #[test]
6480 fn pipeline_reports_keyinfo_parse_error() {
6481 let xml = r#"
6482<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
6483 xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
6484 <ds:SignedInfo>
6485 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6486 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6487 <ds:Reference URI="">
6488 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
6489 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6490 </ds:Reference>
6491 </ds:SignedInfo>
6492 <ds:SignatureValue>AA==</ds:SignatureValue>
6493 <ds:KeyInfo>
6494 <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
6495 </ds:KeyInfo>
6496</ds:Signature>
6497"#;
6498
6499 let err = VerifyContext::new().verify(xml).expect_err(
6500 "invalid KeyInfo must map to ParseKeyInfo when no explicit key is supplied",
6501 );
6502 assert!(matches!(
6503 err,
6504 SignatureVerificationPipelineError::ParseKeyInfo(_)
6505 ));
6506 }
6507
6508 #[test]
6509 fn pipeline_ignores_malformed_keyinfo_when_explicit_key_is_supplied() {
6510 let base_xml = signature_with_target_reference("AQ==");
6511 let xml = base_xml
6512 .replace(
6513 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
6514 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
6515 )
6516 .replace(
6517 "</ds:SignatureValue>\n </ds:Signature>",
6518 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
6519 );
6520
6521 let result = VerifyContext::new()
6522 .key(&RejectingKey)
6523 .verify(&xml)
6524 .expect("explicit key path should not fail on malformed KeyInfo");
6525 assert!(matches!(
6526 result.status,
6527 DsigStatus::Invalid(FailureReason::SignatureMismatch)
6528 ));
6529 }
6530
6531 #[test]
6532 fn pipeline_rejects_foreign_element_children_under_signature() {
6533 let base_xml = signature_with_target_reference("AQ==");
6534 let xml = base_xml
6535 .replace(
6536 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
6537 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:foo="urn:example:foo">"#,
6538 )
6539 .replace(
6540 "</ds:SignedInfo>\n <ds:SignatureValue>",
6541 "</ds:SignedInfo>\n <foo:Bar/>\n <ds:SignatureValue>",
6542 );
6543
6544 let err = VerifyContext::new()
6545 .key(&RejectingKey)
6546 .verify(&xml)
6547 .expect_err("foreign element children under Signature must fail closed");
6548 assert!(matches!(
6549 err,
6550 SignatureVerificationPipelineError::InvalidStructure {
6551 reason: "Signature must contain only XMLDSIG element children",
6552 }
6553 ));
6554 }
6555
6556 #[test]
6557 fn pipeline_rejects_non_whitespace_mixed_content_under_signature() {
6558 let base_xml = signature_with_target_reference("AQ==");
6559 let xml = base_xml.replace(
6560 "</ds:SignedInfo>\n <ds:SignatureValue>",
6561 "</ds:SignedInfo>\n oops\n <ds:SignatureValue>",
6562 );
6563
6564 let err = VerifyContext::new()
6565 .key(&RejectingKey)
6566 .verify(&xml)
6567 .expect_err("non-whitespace mixed content under Signature must fail closed");
6568 assert!(matches!(
6569 err,
6570 SignatureVerificationPipelineError::InvalidStructure {
6571 reason: "Signature must not contain non-whitespace mixed content",
6572 }
6573 ));
6574 }
6575
6576 #[test]
6577 fn pipeline_rejects_keyinfo_out_of_order() {
6578 let base_xml = signature_with_target_reference("AQ==");
6579 let xml = base_xml.replace(
6580 "</ds:SignatureValue>\n </ds:Signature>",
6581 "</ds:SignatureValue>\n <ds:Object/>\n <ds:KeyInfo><ds:KeyName>late</ds:KeyName></ds:KeyInfo>\n </ds:Signature>",
6582 );
6583
6584 let err = VerifyContext::new()
6585 .key(&RejectingKey)
6586 .verify(&xml)
6587 .expect_err("KeyInfo after Object must be rejected by Signature child order checks");
6588 assert!(matches!(
6589 err,
6590 SignatureVerificationPipelineError::InvalidStructure {
6591 reason: "KeyInfo must be the third element child of Signature when present"
6592 }
6593 ));
6594 }
6595
6596 #[test]
6597 fn pipeline_accepts_comments_and_processing_instructions_under_signature() {
6598 let xml = r#"
6599<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6600 <?dbg keep ?>
6601 <!-- signature metadata -->
6602 <ds:SignedInfo>
6603 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6604 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6605 <ds:Reference URI="">
6606 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
6607 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6608 </ds:Reference>
6609 </ds:SignedInfo>
6610 <!-- between required children -->
6611 <ds:SignatureValue>AA==</ds:SignatureValue>
6612</ds:Signature>
6613"#;
6614
6615 let doc = Document::parse(xml).expect("test XML must parse");
6616 let signature_node = doc.root_element();
6617 let parsed = parse_signature_children(signature_node)
6618 .expect("comment/PI nodes under Signature must be ignored");
6619
6620 assert_eq!(parsed.signed_info_node.tag_name().name(), "SignedInfo");
6621 assert_eq!(
6622 parsed.signature_value_node.tag_name().name(),
6623 "SignatureValue"
6624 );
6625 assert!(parsed.key_info_node.is_none());
6626 }
6627}