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