1use crate::xml::dom::{Node, NodeId};
14use base64::Engine;
15use std::cell::Cell;
16use std::collections::{HashMap, HashSet};
17
18use crate::c14n::canonicalize_bounded_with_xml_base_budget;
19use crate::document::{DocumentParseSettings, DocumentView, XmlDocument, XmlDocumentError};
20use crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING;
21
22#[cfg(test)]
23use super::digest::compute_digest;
24use super::digest::{DigestAlgorithm, constant_time_eq};
25#[cfg(test)]
26use super::parse::MAX_REFERENCES_PER_SIGNATURE;
27#[cfg(test)]
28use super::parse::parse_key_info;
29use super::parse::{
30 KeyInfo, MAX_X509_DATA_TOTAL_BINARY_LEN, MAX_X509_DECODED_BINARY_LEN, ParseError, Reference,
31 RetrievalMethodTransforms, SignatureAlgorithm, XMLDSIG_NS,
32};
33use super::parse::{
34 parse_key_info_with_policy_budgets, parse_key_info_with_policy_budgets_and_document_base,
35 parse_reference_with_xpath_budget, parse_signed_info_with_xpath_budget, parse_x509_certificate,
36 parse_x509_data_dispatch_with_budget_and_provider, reference_digest_method,
37};
38use super::signature::{
39 SignatureVerificationError, verify_dsa_signature_spki, verify_ecdsa_signature_pem,
40 verify_rsa_signature_pem,
41};
42#[cfg(test)]
43use super::transforms::BASE64_TRANSFORM_URI;
44use super::transforms::{
45 DEFAULT_IMPLICIT_C14N_URI, Transform, TransformExecutionBudget, TransformOptions,
46 XPATH_TRANSFORM_URI, XPathHereSemantics, XPathSignatureParseBudget,
47 execute_transforms_with_options_and_budget, map_c14n_resource_policy_violation,
48 transform_chain_produces_binary,
49};
50use super::types::{NodeSet, TransformError};
51use super::uri::UriReferenceResolver;
52use super::whitespace::{is_xml_whitespace_only, normalize_xml_base64_bytes};
53
54const MAX_SIGNATURE_VALUE_LEN: usize = 8192;
55const MAX_SIGNATURE_VALUE_TEXT_LEN: usize = 65_536;
56const MAX_RETRIEVAL_METHOD_COUNT: usize = 64;
57pub trait VerifyingKey {
62 fn validate_policy(
68 &self,
69 _policy: &crate::policy::VerificationPolicy,
70 ) -> Result<(), DsigError> {
71 Ok(())
72 }
73
74 fn validate_signature_value(
80 &self,
81 algorithm: SignatureAlgorithm,
82 signature_value: &[u8],
83 ) -> Result<bool, DsigError> {
84 Ok(super::signature::signature_value_matches_algorithm(
85 algorithm,
86 signature_value,
87 ))
88 }
89
90 fn validate_signature_value_with_policy(
98 &self,
99 policy: &crate::policy::VerificationPolicy,
100 algorithm: SignatureAlgorithm,
101 signature_value: &[u8],
102 ) -> Result<bool, DsigError> {
103 let _ = policy;
104 self.validate_signature_value(algorithm, signature_value)
105 }
106
107 fn verify(
109 &self,
110 algorithm: SignatureAlgorithm,
111 signed_data: &[u8],
112 signature_value: &[u8],
113 ) -> Result<bool, DsigError>;
114
115 fn verify_with_policy(
121 &self,
122 policy: &crate::policy::VerificationPolicy,
123 algorithm: SignatureAlgorithm,
124 signed_data: &[u8],
125 signature_value: &[u8],
126 ) -> Result<bool, DsigError> {
127 let _ = policy;
128 self.verify(algorithm, signed_data, signature_value)
129 }
130}
131
132pub trait KeyResolver {
137 fn resolve<'a>(
144 &'a self,
145 key_info: Option<&KeyInfo>,
146 algorithm: SignatureAlgorithm,
147 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError>;
148
149 fn resolve_with_policy<'a>(
159 &'a self,
160 key_info: Option<&KeyInfo>,
161 algorithm: SignatureAlgorithm,
162 _policy: &crate::policy::VerificationPolicy,
163 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
164 self.resolve(key_info, algorithm)
165 }
166
167 fn resolve_with_policy_and_provider<'a>(
173 &'a self,
174 key_info: Option<&KeyInfo>,
175 algorithm: SignatureAlgorithm,
176 policy: &crate::policy::VerificationPolicy,
177 _provider: &dyn crate::provider::CryptoProvider,
178 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
179 self.resolve_with_policy(key_info, algorithm, policy)
180 }
181
182 fn consumes_document_key_info(&self) -> bool {
189 false
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198#[must_use = "pass the policy to VerifyContext::allowed_uri_types(), or store it for reuse"]
199pub struct UriTypeSet {
200 allow_empty: bool,
201 allow_same_document: bool,
202 allow_external: bool,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206enum UriClass {
207 Empty,
208 SameDocument,
209 External,
210}
211
212fn classify_uri(uri: &str) -> UriClass {
213 if uri.is_empty() {
214 UriClass::Empty
215 } else if uri.starts_with('#') {
216 UriClass::SameDocument
217 } else {
218 UriClass::External
219 }
220}
221
222impl UriTypeSet {
223 pub const fn new(allow_empty: bool, allow_same_document: bool, allow_external: bool) -> Self {
225 Self {
226 allow_empty,
227 allow_same_document,
228 allow_external,
229 }
230 }
231
232 pub const SAME_DOCUMENT: Self = Self {
234 allow_empty: true,
235 allow_same_document: true,
236 allow_external: false,
237 };
238
239 pub const ALL: Self = Self {
243 allow_empty: true,
244 allow_same_document: true,
245 allow_external: true,
246 };
247
248 pub fn allows(self, uri: &str) -> bool {
250 match classify_uri(uri) {
251 UriClass::Empty => self.allow_empty,
252 UriClass::SameDocument => self.allow_same_document,
253 UriClass::External => self.allow_external,
254 }
255 }
256}
257
258impl Default for UriTypeSet {
259 fn default() -> Self {
260 Self::SAME_DOCUMENT
261 }
262}
263
264#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
266pub enum SignatureSelection<'a> {
267 #[default]
269 UniqueDocumentSignature,
270 FirstDocumentSignature,
272 FirstSignatureUnderId(&'a str),
274}
275
276#[must_use = "configure the context and call verify(), or store it for reuse"]
278pub struct VerifyContext<'a> {
279 key: Option<&'a dyn VerifyingKey>,
280 key_resolver: Option<&'a dyn KeyResolver>,
281 policy: crate::policy::VerificationPolicy,
282 provider: &'a dyn crate::provider::CryptoProvider,
283 xml_backend: crate::XmlBackend,
284 store_pre_digest: bool,
285 external_resources: Option<&'a HashMap<String, Vec<u8>>>,
286 signature_selection: SignatureSelection<'a>,
287 id_attributes: &'a [crate::IdAttributeRegistration],
288}
289
290impl<'a> VerifyContext<'a> {
291 pub fn new() -> Self {
300 Self {
301 key: None,
302 key_resolver: None,
303 policy: crate::policy::VerificationPolicy::default(),
304 provider: crate::provider::default_provider(),
305 xml_backend: crate::XmlBackend::default(),
306 store_pre_digest: false,
307 external_resources: None,
308 signature_selection: SignatureSelection::UniqueDocumentSignature,
309 id_attributes: &[],
310 }
311 }
312
313 pub fn key(mut self, key: &'a dyn VerifyingKey) -> Self {
320 self.key = Some(key);
321 self
322 }
323
324 pub fn key_resolver(mut self, resolver: &'a dyn KeyResolver) -> Self {
326 self.key_resolver = Some(resolver);
327 self
328 }
329
330 pub fn policy(mut self, policy: crate::policy::VerificationPolicy) -> Self {
332 self.policy = policy;
333 self
334 }
335
336 pub fn provider(mut self, provider: &'a dyn crate::provider::CryptoProvider) -> Self {
338 self.provider = provider;
339 self
340 }
341
342 pub fn xml_backend(mut self, backend: crate::XmlBackend) -> Self {
344 self.xml_backend = backend;
345 self
346 }
347
348 pub fn process_manifests(mut self, enabled: bool) -> Self {
379 self.policy.manifest_processing = if enabled {
380 crate::policy::ManifestProcessing::Process
381 } else {
382 crate::policy::ManifestProcessing::Ignore
383 };
384 self
385 }
386
387 pub fn allowed_uri_types(mut self, types: UriTypeSet) -> Self {
389 self.policy.uris.references = types;
390 self
391 }
392
393 pub fn allowed_retrieval_method_uri_types(mut self, types: UriTypeSet) -> Self {
400 self.policy.uris.retrieval_methods = types;
401 self
402 }
403
404 pub fn external_resources(mut self, resources: &'a HashMap<String, Vec<u8>>) -> Self {
412 self.external_resources = Some(resources);
413 self
414 }
415
416 pub fn start_node_id(mut self, id: &'a str) -> Self {
422 self.signature_selection = SignatureSelection::FirstSignatureUnderId(id);
423 self
424 }
425
426 pub fn first_document_signature(mut self) -> Self {
431 self.signature_selection = SignatureSelection::FirstDocumentSignature;
432 self
433 }
434
435 pub fn id_attributes(mut self, registrations: &'a [crate::IdAttributeRegistration]) -> Self {
437 self.id_attributes = registrations;
438 self
439 }
440
441 pub fn allow_internal_dtd(mut self, enabled: bool) -> Self {
444 self.policy.xml.allow_internal_dtd = enabled;
445 self
446 }
447
448 pub fn allowed_transforms<I, S>(mut self, transforms: I) -> Self
459 where
460 I: IntoIterator<Item = S>,
461 S: Into<String>,
462 {
463 self.policy.transforms.allowed_algorithms =
464 Some(transforms.into_iter().map(Into::into).collect());
465 self
466 }
467
468 pub fn store_pre_digest(mut self, enabled: bool) -> Self {
476 self.store_pre_digest = enabled;
477 self
478 }
479
480 pub fn xpath_here_semantics(mut self, semantics: XPathHereSemantics) -> Self {
486 self.policy.transforms.xpath_here_semantics = semantics;
487 self
488 }
489
490 fn allowed_transform_uris(&self) -> Option<&HashSet<String>> {
491 self.policy.transforms.allowed_algorithms.as_ref()
492 }
493
494 fn transform_options(&self) -> TransformOptions {
495 TransformOptions::default()
496 .allow_internal_dtd(self.policy.xml.allow_internal_dtd)
497 .xpath_here_semantics(self.policy.transforms.xpath_here_semantics)
498 }
499
500 pub fn verify(&self, xml: &str) -> Result<VerifyResult, DsigError> {
508 verify_signature_with_context(xml, self)
509 }
510
511 pub fn verify_document(&self, document: &XmlDocument) -> Result<VerifyResult, DsigError> {
517 verify_signature_document_with_context(document, self)
518 }
519}
520
521impl Default for VerifyContext<'_> {
522 fn default() -> Self {
523 Self::new()
524 }
525}
526
527#[derive(Debug)]
529#[non_exhaustive]
530#[must_use = "inspect status before accepting the reference result"]
531pub struct ReferenceResult {
532 pub reference_set: ReferenceSet,
534 pub reference_index: usize,
536 pub uri: String,
538 pub digest_algorithm: DigestAlgorithm,
540 pub status: DsigStatus,
542 pub pre_digest_data: Option<Vec<u8>>,
544}
545
546#[derive(Debug, Clone, Copy, PartialEq, Eq)]
548#[non_exhaustive]
549pub enum ReferenceSet {
550 SignedInfo,
552 Manifest,
554}
555
556#[derive(Debug, Clone, Copy, PartialEq, Eq)]
558#[non_exhaustive]
559pub enum DsigStatus {
560 Valid,
562 Invalid(FailureReason),
564}
565
566#[derive(Debug, Clone, Copy, PartialEq, Eq)]
568#[non_exhaustive]
569pub enum FailureReason {
570 ReferenceDigestMismatch {
572 ref_index: usize,
582 },
583 ReferencePolicyViolation {
585 ref_index: usize,
587 },
588 ReferenceProcessingFailure {
590 ref_index: usize,
592 },
593 SignatureMismatch,
595 KeyNotFound,
597}
598
599#[derive(Debug)]
601#[non_exhaustive]
602#[must_use = "check first_failure/results before accepting the reference set"]
603pub struct ReferencesResult {
604 pub results: Vec<ReferenceResult>,
607 pub first_failure: Option<usize>,
609}
610
611impl ReferencesResult {
612 #[must_use]
614 pub fn all_valid(&self) -> bool {
615 self.results
616 .iter()
617 .all(|result| matches!(result.status, DsigStatus::Valid))
618 }
619}
620
621pub fn process_reference(
640 reference: &Reference,
641 resolver: &UriReferenceResolver<'_>,
642 signature_node: Node<'_, '_>,
643 reference_set: ReferenceSet,
644 reference_index: usize,
645 store_pre_digest: bool,
646) -> Result<ReferenceResult, ReferenceProcessingError> {
647 let execution_budget = TransformExecutionBudget::default();
648 let canonicalized_data_budget = CanonicalizedDataBudget::default();
649 let execution = ReferenceExecutionContext {
650 store_pre_digest,
651 transform_options: TransformOptions::default(),
652 transform_budget: &execution_budget,
653 canonicalized_data_budget: &canonicalized_data_budget,
654 provider: crate::provider::default_provider(),
655 };
656 process_reference_with_options(
657 reference,
658 resolver,
659 signature_node,
660 reference_set,
661 reference_index,
662 reference_origin_node(signature_node, reference_set, reference_index),
663 &execution,
664 )
665}
666
667fn reference_origin_node<'a, 'input>(
668 signature_node: Node<'a, 'input>,
669 reference_set: ReferenceSet,
670 reference_index: usize,
671) -> Option<Node<'a, 'input>> {
672 let is_reference = |node: &Node<'_, '_>| {
673 node.is_element()
674 && node.tag_name().namespace() == Some(XMLDSIG_NS)
675 && node.tag_name().name() == "Reference"
676 };
677 match reference_set {
678 ReferenceSet::SignedInfo => signature_node
679 .children()
680 .find(|node| {
681 node.is_element()
682 && node.tag_name().namespace() == Some(XMLDSIG_NS)
683 && node.tag_name().name() == "SignedInfo"
684 })?
685 .children()
686 .filter(is_reference)
687 .nth(reference_index),
688 ReferenceSet::Manifest => signature_node
689 .children()
690 .filter(|node| {
691 node.is_element()
692 && node.tag_name().namespace() == Some(XMLDSIG_NS)
693 && node.tag_name().name() == "Object"
694 })
695 .flat_map(|object| {
696 object.children().filter(|node| {
697 node.is_element()
698 && node.tag_name().namespace() == Some(XMLDSIG_NS)
699 && node.tag_name().name() == "Manifest"
700 })
701 })
702 .flat_map(|manifest| manifest.children().filter(is_reference))
703 .nth(reference_index),
704 }
705}
706
707struct ReferenceExecutionContext<'a> {
708 store_pre_digest: bool,
709 transform_options: TransformOptions,
710 transform_budget: &'a TransformExecutionBudget,
711 canonicalized_data_budget: &'a CanonicalizedDataBudget,
712 provider: &'a dyn crate::provider::CryptoProvider,
713}
714
715struct CanonicalizedDataBudget {
716 remaining: Cell<usize>,
717 max_bytes: usize,
718}
719
720impl Default for CanonicalizedDataBudget {
721 fn default() -> Self {
722 Self {
723 remaining: Cell::new(CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING),
724 max_bytes: CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
725 }
726 }
727}
728
729impl CanonicalizedDataBudget {
730 fn remaining(&self) -> usize {
731 self.remaining.get()
732 }
733
734 fn charge(&self, bytes: usize) -> Result<(), ReferenceProcessingError> {
735 let available = self.remaining.get();
736 let Some(remaining) = available.checked_sub(bytes) else {
737 self.remaining.set(0);
738 return Err(crate::policy::PolicyViolation::ResourceLimit {
739 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
740 maximum: self.max_bytes,
741 actual: self
742 .max_bytes
743 .saturating_add(bytes.saturating_sub(available)),
744 }
745 .into());
746 };
747 self.remaining.set(remaining);
748 Ok(())
749 }
750
751 fn with_limit(max_bytes: usize) -> Self {
752 Self {
753 remaining: Cell::new(max_bytes),
754 max_bytes,
755 }
756 }
757}
758
759fn process_reference_with_options(
760 reference: &Reference,
761 resolver: &UriReferenceResolver<'_>,
762 signature_node: Node<'_, '_>,
763 reference_set: ReferenceSet,
764 reference_index: usize,
765 reference_node: Option<Node<'_, '_>>,
766 execution: &ReferenceExecutionContext<'_>,
767) -> Result<ReferenceResult, ReferenceProcessingError> {
768 let uri = reference
771 .uri
772 .as_deref()
773 .ok_or(ReferenceProcessingError::MissingUri)?;
774 let initial_data = reference_node
775 .map_or_else(
776 || {
777 resolver.dereference_with_budget(
778 uri,
779 execution.transform_budget.node_set_materialization(),
780 )
781 },
782 |node| {
783 resolver.dereference_from_with_budget(
784 uri,
785 node,
786 execution.transform_budget.node_set_materialization(),
787 execution.transform_budget.xml_base_resolution(),
788 )
789 },
790 )
791 .map_err(ReferenceProcessingError::UriDereference)?;
792
793 let pre_digest_bytes = execute_transforms_with_options_and_budget(
795 signature_node,
796 initial_data,
797 &reference.transforms,
798 execution.transform_options,
799 execution.transform_budget,
800 )
801 .map_err(ReferenceProcessingError::Transform)?;
802
803 let computed_digest = super::compute_digest_with_provider(
805 execution.provider,
806 reference.digest_method,
807 &pre_digest_bytes,
808 )?;
809
810 let status = if constant_time_eq(&computed_digest, &reference.digest_value) {
812 DsigStatus::Valid
813 } else {
814 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch {
815 ref_index: reference_index,
816 })
817 };
818
819 let pre_digest_data = if execution.store_pre_digest {
820 execution
821 .canonicalized_data_budget
822 .charge(pre_digest_bytes.len())?;
823 Some(pre_digest_bytes)
824 } else {
825 None
826 };
827
828 Ok(ReferenceResult {
829 reference_set,
830 reference_index,
831 uri: uri.to_owned(),
832 digest_algorithm: reference.digest_method,
833 status,
834 pre_digest_data,
835 })
836}
837
838pub fn process_all_references(
850 references: &[Reference],
851 resolver: &UriReferenceResolver<'_>,
852 signature_node: Node<'_, '_>,
853 store_pre_digest: bool,
854) -> Result<ReferencesResult, ReferenceProcessingError> {
855 let execution_budget = TransformExecutionBudget::default();
856 let canonicalized_data_budget = CanonicalizedDataBudget::default();
857 let execution = ReferenceExecutionContext {
858 store_pre_digest,
859 transform_options: TransformOptions::default(),
860 transform_budget: &execution_budget,
861 canonicalized_data_budget: &canonicalized_data_budget,
862 provider: crate::provider::default_provider(),
863 };
864 process_all_references_with_options(references, resolver, signature_node, &execution)
865}
866
867fn process_all_references_with_options(
868 references: &[Reference],
869 resolver: &UriReferenceResolver<'_>,
870 signature_node: Node<'_, '_>,
871 execution: &ReferenceExecutionContext<'_>,
872) -> Result<ReferencesResult, ReferenceProcessingError> {
873 let mut results = Vec::with_capacity(references.len());
874
875 for (i, reference) in references.iter().enumerate() {
876 let result = process_reference_with_options(
877 reference,
878 resolver,
879 signature_node,
880 ReferenceSet::SignedInfo,
881 i,
882 reference_origin_node(signature_node, ReferenceSet::SignedInfo, i),
883 execution,
884 )?;
885 let failed = matches!(result.status, DsigStatus::Invalid(_));
886 results.push(result);
887
888 if failed {
889 return Ok(ReferencesResult {
890 results,
891 first_failure: Some(i),
892 });
893 }
894 }
895
896 Ok(ReferencesResult {
897 results,
898 first_failure: None,
899 })
900}
901
902#[derive(Debug, thiserror::Error)]
906#[non_exhaustive]
907pub enum ReferenceProcessingError {
908 #[error("verification policy violation: {0}")]
910 Policy(#[from] crate::policy::PolicyViolation),
911
912 #[error("cryptographic provider error: {0}")]
914 Provider(#[from] crate::provider::ProviderError),
915
916 #[error("reference URI is required; omitted URI references are not supported")]
918 MissingUri,
919
920 #[error("URI dereference failed: {0}")]
922 UriDereference(#[source] super::types::TransformError),
923
924 #[error("transform failed: {0}")]
926 Transform(#[source] super::types::TransformError),
927}
928
929impl ReferenceProcessingError {
930 fn into_policy_violation(self) -> Result<crate::policy::PolicyViolation, Self> {
931 match self {
932 Self::Policy(error)
933 | Self::UriDereference(TransformError::Policy(error))
934 | Self::Transform(TransformError::Policy(error)) => Ok(error),
935 error => Err(error),
936 }
937 }
938}
939
940#[derive(Debug)]
942#[non_exhaustive]
943#[must_use = "inspect status before accepting the document"]
944pub struct VerifyResult {
945 pub status: DsigStatus,
950 pub signed_info_references: Vec<ReferenceResult>,
954 pub manifest_references: Vec<ReferenceResult>,
964 pub canonicalized_signed_info: Option<Vec<u8>>,
967}
968
969#[derive(Debug, thiserror::Error)]
971#[non_exhaustive]
972pub enum DsigError {
973 #[error("verification policy violation: {0}")]
975 Policy(#[from] crate::policy::PolicyViolation),
976
977 #[error("cryptographic provider error: {0}")]
979 Provider(#[from] crate::provider::ProviderError),
980
981 #[error("XML parse error: {0}")]
983 XmlParse(#[from] crate::xml::dom::ParseError),
984
985 #[error("XML document error: {0}")]
987 Document(#[from] crate::document::XmlDocumentError),
988
989 #[error("missing required element: <{element}>")]
991 MissingElement {
992 element: &'static str,
994 },
995
996 #[error("invalid Signature structure: {reason}")]
998 InvalidStructure {
999 reason: &'static str,
1001 },
1002
1003 #[error("selected node ID is missing or ambiguous: {id}")]
1005 SelectedNodeUnavailable {
1006 id: String,
1008 },
1009
1010 #[error("failed to parse SignedInfo: {0}")]
1012 ParseSignedInfo(super::parse::ParseError),
1013
1014 #[error("failed to parse KeyInfo: {0}")]
1016 ParseKeyInfo(#[source] super::parse::ParseError),
1017
1018 #[error("key resolution failed: {0}")]
1020 KeyResolution(#[from] super::keys::KeyResolutionError),
1021
1022 #[error("failed to parse Manifest reference: {0}")]
1024 ParseManifestReference(#[source] ParseError),
1025
1026 #[error("reference processing failed: {0}")]
1028 Reference(ReferenceProcessingError),
1029
1030 #[error("SignedInfo canonicalization failed: {0}")]
1032 Canonicalization(#[from] crate::c14n::C14nError),
1033
1034 #[error("invalid SignatureValue base64: {0}")]
1036 SignatureValueBase64(#[from] base64::DecodeError),
1037
1038 #[error("signature verification failed: {0}")]
1040 Crypto(#[from] SignatureVerificationError),
1041}
1042
1043impl From<super::parse::ParseError> for DsigError {
1044 fn from(error: super::parse::ParseError) -> Self {
1045 match error {
1046 super::parse::ParseError::Policy(error) => Self::Policy(error),
1047 super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1048 Self::Policy(error)
1049 }
1050 error => Self::ParseSignedInfo(error),
1051 }
1052 }
1053}
1054
1055fn map_key_info_parse_error(error: super::parse::ParseError) -> DsigError {
1056 match error {
1057 super::parse::ParseError::Policy(error)
1058 | super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1059 DsigError::Policy(error)
1060 }
1061 error => DsigError::ParseKeyInfo(error),
1062 }
1063}
1064
1065fn map_manifest_parse_error(error: super::parse::ParseError) -> DsigError {
1066 match error {
1067 super::parse::ParseError::Policy(error)
1068 | super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1069 DsigError::Policy(error)
1070 }
1071 error => DsigError::ParseManifestReference(error),
1072 }
1073}
1074
1075impl From<ReferenceProcessingError> for DsigError {
1076 fn from(error: ReferenceProcessingError) -> Self {
1077 match error.into_policy_violation() {
1078 Ok(error) => Self::Policy(error),
1079 Err(error) => Self::Reference(error),
1080 }
1081 }
1082}
1083
1084type SignatureVerificationPipelineError = DsigError;
1085
1086pub fn verify_signature_with_pem_key(
1114 xml: &str,
1115 public_key_pem: &str,
1116 store_pre_digest: bool,
1117) -> Result<VerifyResult, DsigError> {
1118 struct PemVerifyingKey<'a> {
1119 public_key_pem: &'a str,
1120 }
1121
1122 impl VerifyingKey for PemVerifyingKey<'_> {
1123 fn verify(
1124 &self,
1125 algorithm: SignatureAlgorithm,
1126 signed_data: &[u8],
1127 signature_value: &[u8],
1128 ) -> Result<bool, DsigError> {
1129 verify_with_algorithm(algorithm, self.public_key_pem, signed_data, signature_value)
1130 }
1131 }
1132
1133 let key = PemVerifyingKey { public_key_pem };
1134 VerifyContext::new()
1135 .key(&key)
1136 .store_pre_digest(store_pre_digest)
1137 .verify(xml)
1138}
1139
1140fn verify_signature_with_context(
1141 xml: &str,
1142 ctx: &VerifyContext<'_>,
1143) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1144 ctx.policy.validate()?;
1145 ctx.policy.resources.validate_xml_document_len(xml.len())?;
1146 let execution_budget = TransformExecutionBudget::from_resources(&ctx.policy.resources)
1147 .with_xml_backend(ctx.xml_backend);
1148 let settings = DocumentParseSettings::from_policy(&ctx.policy.xml, &ctx.policy.resources)
1149 .with_backend(ctx.xml_backend);
1150 let document = XmlDocument::parse_with_settings_and_budget(
1151 xml.to_owned(),
1152 settings,
1153 execution_budget.xml_parse_work(),
1154 )
1155 .map_err(|error| map_document_parse_error(error, settings))?;
1156 verify_signature_document_with_context_and_budget(&document, ctx, &execution_budget)
1157}
1158
1159fn map_document_parse_error(error: XmlDocumentError, settings: DocumentParseSettings) -> DsigError {
1160 match error.into_policy_violation(settings) {
1161 Ok(error) => DsigError::Policy(error),
1162 Err(XmlDocumentError::Parse(error)) => DsigError::XmlParse(error),
1163 Err(error) => DsigError::Document(error),
1164 }
1165}
1166
1167#[cfg(test)]
1168mod xml_parse_budget_tests {
1169 use super::*;
1170
1171 #[test]
1172 fn verification_initial_parse_uses_the_policy_work_budget() {
1173 let xml = "<root/>";
1176 let mut policy = crate::policy::VerificationPolicy::default();
1177 policy.resources.max_xml_parse_work_bytes = 0;
1178
1179 let error = VerifyContext::new()
1180 .policy(policy)
1181 .verify(xml)
1182 .expect_err("a zero parse-work budget must reject the input parse");
1183
1184 assert!(matches!(
1185 error,
1186 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1187 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
1188 maximum: 0,
1189 actual,
1190 }) if actual == xml.len()
1191 ));
1192 }
1193}
1194
1195fn verify_signature_document_with_context(
1196 document: &XmlDocument,
1197 ctx: &VerifyContext<'_>,
1198) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1199 let execution_budget = TransformExecutionBudget::from_resources(&ctx.policy.resources)
1200 .with_xml_backend(ctx.xml_backend);
1201 verify_signature_document_with_context_and_budget(document, ctx, &execution_budget)
1202}
1203
1204fn verify_signature_document_with_context_and_budget(
1205 document: &XmlDocument,
1206 ctx: &VerifyContext<'_>,
1207 execution_budget: &TransformExecutionBudget,
1208) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1209 document.validate_operation_policy(&ctx.policy.xml, &ctx.policy.resources)?;
1210 document.with_view(|view| verify_signature_view(view, ctx, execution_budget))
1211}
1212
1213fn verify_signature_view<'a>(
1214 view: DocumentView<'a>,
1215 ctx: &VerifyContext<'_>,
1216 execution_budget: &TransformExecutionBudget,
1217) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1218 ctx.policy.validate()?;
1219 let doc = view.document();
1220 let resolver = UriReferenceResolver::with_document_view(view, ctx.id_attributes)
1221 .with_same_document_id_semantics(ctx.policy.transforms.same_document_id_semantics)
1222 .with_external_resource_limits(
1223 ctx.policy.resources.max_external_resource_bytes,
1224 ctx.policy.resources.max_external_resource_total_bytes,
1225 );
1226 let resolver = match ctx.external_resources {
1227 Some(resources) => resolver.with_external_resources(resources),
1228 None => resolver,
1229 };
1230 let start_node = match ctx.signature_selection {
1231 SignatureSelection::FirstSignatureUnderId(id) => {
1232 resolver.node_for_id(id).ok_or_else(|| {
1233 SignatureVerificationPipelineError::SelectedNodeUnavailable { id: id.to_owned() }
1234 })?
1235 }
1236 SignatureSelection::UniqueDocumentSignature
1237 | SignatureSelection::FirstDocumentSignature => doc.root(),
1238 };
1239 let mut signatures = start_node.descendants().filter(|node| {
1240 node.is_element()
1241 && node.tag_name().name() == "Signature"
1242 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1243 });
1244 let signature_node = match (signatures.next(), ctx.signature_selection) {
1245 (None, _) => {
1246 return Err(SignatureVerificationPipelineError::MissingElement {
1247 element: "Signature",
1248 });
1249 }
1250 (
1254 Some(node),
1255 SignatureSelection::FirstDocumentSignature
1256 | SignatureSelection::FirstSignatureUnderId(_),
1257 ) => node,
1258 (Some(node), SignatureSelection::UniqueDocumentSignature)
1259 if signatures.next().is_none() =>
1260 {
1261 node
1262 }
1263 (Some(_), SignatureSelection::UniqueDocumentSignature) => {
1264 return Err(SignatureVerificationPipelineError::InvalidStructure {
1265 reason: "Signature must appear exactly once in document",
1266 });
1267 }
1268 };
1269
1270 let signature_children = parse_signature_children(signature_node)?;
1271 let signed_info_node = signature_children.signed_info_node;
1272 let should_parse_key_info = match (ctx.key, ctx.key_resolver) {
1273 (Some(_), _) => false,
1274 (None, Some(resolver)) => resolver.consumes_document_key_info(),
1275 (None, None) => true,
1276 };
1277 let mut key_info = if should_parse_key_info {
1278 signature_children
1279 .key_info_node
1280 .map(|node| {
1281 parse_key_info_with_policy_budgets(
1282 node,
1283 ctx.provider,
1284 execution_budget.xml_base_resolution(),
1285 &ctx.policy.resources,
1286 )
1287 })
1288 .transpose()
1289 .map_err(map_key_info_parse_error)?
1290 } else {
1291 None
1292 };
1293
1294 let mut xpath_parse_budget = XPathSignatureParseBudget::from_resources(&ctx.policy.resources);
1295 let signed_info =
1296 parse_signed_info_with_xpath_budget(signed_info_node, &mut xpath_parse_budget)?;
1297 if signed_info.references.len() > ctx.policy.resources.max_references {
1298 return Err(crate::policy::PolicyViolation::ResourceLimit {
1299 resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
1300 maximum: ctx.policy.resources.max_references,
1301 actual: signed_info.references.len(),
1302 }
1303 .into());
1304 }
1305 for reference in &signed_info.references {
1306 if reference.transforms.len() > ctx.policy.resources.max_transforms_per_reference {
1307 return Err(crate::policy::PolicyViolation::ResourceLimit {
1308 resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
1309 maximum: ctx.policy.resources.max_transforms_per_reference,
1310 actual: reference.transforms.len(),
1311 }
1312 .into());
1313 }
1314 }
1315 ctx.policy
1316 .check_signature_algorithm(signed_info.signature_method)?;
1317 for reference in &signed_info.references {
1318 if ctx
1319 .policy
1320 .digest_algorithms
1321 .as_ref()
1322 .is_some_and(|allowed| !allowed.contains(&reference.digest_method))
1323 {
1324 return Err(crate::policy::PolicyViolation::Algorithm {
1325 operation: "verification",
1326 algorithm: reference.digest_method.uri().to_string(),
1327 }
1328 .into());
1329 }
1330 }
1331 enforce_reference_policies(
1332 &signed_info.references,
1333 ctx.policy.uris.references,
1334 ctx.allowed_transform_uris(),
1335 )?;
1336 enforce_transform_allowed(ctx.allowed_transform_uris(), signed_info.c14n_method.uri())?;
1337
1338 if let Some(resources) = ctx.external_resources {
1339 let mut total = 0usize;
1340 for bytes in resources.values() {
1341 if bytes.len() > ctx.policy.resources.max_external_resource_bytes {
1342 return Err(crate::policy::PolicyViolation::ResourceLimit {
1343 resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
1344 maximum: ctx.policy.resources.max_external_resource_bytes,
1345 actual: bytes.len(),
1346 }
1347 .into());
1348 }
1349 total = total.checked_add(bytes.len()).ok_or(
1350 SignatureVerificationPipelineError::InvalidStructure {
1351 reason: "external resource total length overflow",
1352 },
1353 )?;
1354 }
1355 if total > ctx.policy.resources.max_external_resource_total_bytes {
1356 return Err(crate::policy::PolicyViolation::ResourceLimit {
1357 resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
1358 maximum: ctx.policy.resources.max_external_resource_total_bytes,
1359 actual: total,
1360 }
1361 .into());
1362 }
1363 }
1364 let retrieval_materialization = if let Some(info) = key_info.as_mut() {
1365 let mut retrieval_budgets = RetrievalMaterializationBudgets {
1366 xpath_parse: &mut xpath_parse_budget,
1367 execution: execution_budget,
1368 resources: &ctx.policy.resources,
1369 xml_backend: ctx.xml_backend,
1370 };
1371 let mut materialization = KeyInfoMaterializationState::default();
1372 let mut outcome = materialize_key_info_references_with_budgets(
1373 info,
1374 &resolver,
1375 &ctx.policy,
1376 ctx.provider,
1377 &mut retrieval_budgets,
1378 &mut materialization,
1379 )?;
1380 outcome.merge(materialize_retrieval_methods_with_budgets(
1381 info,
1382 &resolver,
1383 ctx.policy.uris.retrieval_methods,
1384 ctx.allowed_transform_uris(),
1385 ctx.provider,
1386 &mut retrieval_budgets,
1387 &mut materialization.candidate_work,
1388 )?);
1389 outcome
1390 } else {
1391 RetrievalMaterialization::default()
1392 };
1393 let canonicalized_data_budget =
1394 CanonicalizedDataBudget::with_limit(ctx.policy.resources.effective_canonicalized_bytes());
1395 let execution = ReferenceExecutionContext {
1396 store_pre_digest: ctx.store_pre_digest,
1397 transform_options: ctx.transform_options(),
1398 transform_budget: execution_budget,
1399 canonicalized_data_budget: &canonicalized_data_budget,
1400 provider: ctx.provider,
1401 };
1402 let references = process_all_references_with_options(
1403 &signed_info.references,
1404 &resolver,
1405 signature_node,
1406 &execution,
1407 )?;
1408
1409 if let Some(first_failure) = references.first_failure {
1410 let status = references.results[first_failure].status;
1411 return Ok(VerifyResult {
1412 status,
1413 signed_info_references: references.results,
1414 manifest_references: Vec::new(),
1415 canonicalized_signed_info: None,
1416 });
1417 }
1418
1419 let signed_info_subtree: HashSet<_> = signed_info_node
1420 .descendants()
1421 .map(|node: Node<'_, '_>| node.id())
1422 .collect();
1423 let mut canonical_signed_info = Vec::new();
1424 let signed_info_limit = canonicalized_data_budget
1425 .remaining()
1426 .min(execution_budget.remaining_c14n_output());
1427 canonicalize_bounded_with_xml_base_budget(
1428 doc,
1429 Some(&|node| signed_info_subtree.contains(&node.id())),
1430 &signed_info.c14n_method,
1431 signed_info_limit,
1432 execution_budget.xml_base_resolution(),
1433 &mut canonical_signed_info,
1434 )
1435 .map_err(|error| {
1436 if let Some(violation) = map_c14n_resource_policy_violation(
1437 &error,
1438 crate::policy::resource_name::CANONICALIZED_BYTES,
1439 canonicalized_data_budget.max_bytes,
1440 ) {
1441 SignatureVerificationPipelineError::Policy(violation)
1442 } else {
1443 SignatureVerificationPipelineError::Canonicalization(error)
1444 }
1445 })?;
1446 execution_budget
1447 .charge_c14n_output(canonical_signed_info.len())
1448 .map_err(ReferenceProcessingError::Transform)?;
1449 canonicalized_data_budget.charge(canonical_signed_info.len())?;
1450
1451 let signature_value = decode_signature_value(signature_children.signature_value_node)?;
1452 if let Some(full_output_bits) = signed_info.signature_method.hmac_output_bits() {
1453 let expected_bits = signed_info
1454 .hmac_output_length_bits
1455 .unwrap_or(full_output_bits);
1456 ctx.policy
1457 .hmac
1458 .validate_output(signed_info.signature_method, expected_bits)?;
1459 if signature_value.len() != expected_bits / 8 {
1460 return Err(SignatureVerificationPipelineError::InvalidStructure {
1461 reason: "SignatureValue length does not match HMACOutputLength",
1462 });
1463 }
1464 }
1465 let Some(resolved_key) =
1466 resolve_verifying_key(ctx, key_info.as_ref(), signed_info.signature_method)?
1467 else {
1468 if let Some(error) = retrieval_materialization.deferred_error {
1469 return Err(error);
1470 }
1471 return Ok(VerifyResult {
1472 status: DsigStatus::Invalid(FailureReason::KeyNotFound),
1473 signed_info_references: references.results,
1474 manifest_references: Vec::new(),
1475 canonicalized_signed_info: if ctx.store_pre_digest {
1476 Some(canonical_signed_info)
1477 } else {
1478 None
1479 },
1480 });
1481 };
1482 let verifier = resolved_key.as_ref();
1483 verifier.validate_policy(&ctx.policy)?;
1484 if !verifier.validate_signature_value_with_policy(
1485 &ctx.policy,
1486 signed_info.signature_method,
1487 &signature_value,
1488 )? {
1489 return Ok(VerifyResult {
1490 status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
1491 signed_info_references: references.results,
1492 manifest_references: Vec::new(),
1493 canonicalized_signed_info: if ctx.store_pre_digest {
1494 Some(canonical_signed_info)
1495 } else {
1496 None
1497 },
1498 });
1499 }
1500 ctx.provider
1501 .require_capability(crate::provider::ProviderCapability::Verify(
1502 signed_info.signature_method,
1503 ))?;
1504 let policy_verifier = PolicyVerifyingKey {
1505 key: verifier,
1506 policy: &ctx.policy,
1507 };
1508 let signature_valid = ctx.provider.verify(
1509 &policy_verifier,
1510 signed_info.signature_method,
1511 &canonical_signed_info,
1512 &signature_value,
1513 )?;
1514
1515 if !signature_valid {
1516 return Ok(VerifyResult {
1517 status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
1518 signed_info_references: references.results,
1519 manifest_references: Vec::new(),
1520 canonicalized_signed_info: if ctx.store_pre_digest {
1521 Some(canonical_signed_info)
1522 } else {
1523 None
1524 },
1525 });
1526 }
1527
1528 let manifest_references = if ctx.policy.manifest_processing
1529 == crate::policy::ManifestProcessing::Process
1530 {
1531 let signed_info_reference_nodes =
1532 collect_authenticated_signed_info_reference_nodes(&signed_info.references, &resolver);
1533 let remaining_reference_capacity = ctx
1534 .policy
1535 .resources
1536 .max_references
1537 .checked_sub(signed_info.references.len())
1538 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
1539 reason: "SignedInfo exceeds the per-signature Reference limit",
1540 })?;
1541 process_manifest_references(
1542 signature_node,
1543 &resolver,
1544 ctx,
1545 &signed_info_reference_nodes,
1546 remaining_reference_capacity,
1547 &execution,
1548 &mut xpath_parse_budget,
1549 )?
1550 } else {
1551 Vec::new()
1552 };
1553
1554 Ok(VerifyResult {
1555 status: DsigStatus::Valid,
1556 signed_info_references: references.results,
1557 manifest_references,
1558 canonicalized_signed_info: if ctx.store_pre_digest {
1559 Some(canonical_signed_info)
1560 } else {
1561 None
1562 },
1563 })
1564}
1565
1566struct PolicyVerifyingKey<'a> {
1567 key: &'a dyn VerifyingKey,
1568 policy: &'a crate::policy::VerificationPolicy,
1569}
1570
1571impl VerifyingKey for PolicyVerifyingKey<'_> {
1572 fn validate_policy(&self, policy: &crate::policy::VerificationPolicy) -> Result<(), DsigError> {
1573 self.key.validate_policy(policy)
1574 }
1575
1576 fn validate_signature_value(
1577 &self,
1578 algorithm: SignatureAlgorithm,
1579 signature_value: &[u8],
1580 ) -> Result<bool, DsigError> {
1581 self.key
1582 .validate_signature_value_with_policy(self.policy, algorithm, signature_value)
1583 }
1584
1585 fn verify(
1586 &self,
1587 algorithm: SignatureAlgorithm,
1588 signed_data: &[u8],
1589 signature_value: &[u8],
1590 ) -> Result<bool, DsigError> {
1591 self.key
1592 .verify_with_policy(self.policy, algorithm, signed_data, signature_value)
1593 }
1594}
1595
1596#[derive(Debug, Default)]
1597struct RetrievalMaterialization {
1598 deferred_error: Option<SignatureVerificationPipelineError>,
1599}
1600
1601impl RetrievalMaterialization {
1602 fn merge(&mut self, other: Self) {
1603 if self.deferred_error.is_none() {
1604 self.deferred_error = other.deferred_error;
1605 }
1606 }
1607}
1608
1609struct RetrievalMaterializationBudgets<'a> {
1610 xpath_parse: &'a mut XPathSignatureParseBudget,
1611 execution: &'a TransformExecutionBudget,
1612 resources: &'a crate::policy::ResourcePolicy,
1613 xml_backend: crate::XmlBackend,
1614}
1615
1616#[derive(Default)]
1617struct KeyInfoMaterializationState {
1618 active: HashSet<(super::uri::TraversalDocumentIdentity, String)>,
1619 candidate_work: usize,
1620}
1621
1622trait KeyInfoReferencePolicy {
1623 fn resources(&self) -> &crate::policy::ResourcePolicy;
1624 fn xml(&self) -> &crate::policy::XmlInputPolicy;
1625 fn key_info_reference_uris(&self) -> UriTypeSet;
1626 fn retrieval_method_uris(&self) -> UriTypeSet;
1627 fn key_info_reference_source_enabled(&self) -> bool;
1628 fn allowed_transforms(&self) -> Option<&HashSet<String>>;
1629}
1630
1631impl KeyInfoReferencePolicy for crate::policy::SigningPolicy {
1632 fn resources(&self) -> &crate::policy::ResourcePolicy {
1633 &self.resources
1634 }
1635
1636 fn xml(&self) -> &crate::policy::XmlInputPolicy {
1637 &self.xml
1638 }
1639
1640 fn key_info_reference_uris(&self) -> UriTypeSet {
1641 self.uris.key_info_references
1642 }
1643
1644 fn retrieval_method_uris(&self) -> UriTypeSet {
1645 self.uris.retrieval_methods
1646 }
1647
1648 fn key_info_reference_source_enabled(&self) -> bool {
1649 true
1650 }
1651
1652 fn allowed_transforms(&self) -> Option<&HashSet<String>> {
1653 self.transforms.allowed_algorithms.as_ref()
1654 }
1655}
1656
1657impl KeyInfoReferencePolicy for crate::policy::VerificationPolicy {
1658 fn resources(&self) -> &crate::policy::ResourcePolicy {
1659 &self.resources
1660 }
1661
1662 fn xml(&self) -> &crate::policy::XmlInputPolicy {
1663 &self.xml
1664 }
1665
1666 fn key_info_reference_uris(&self) -> UriTypeSet {
1667 self.uris.key_info_references
1668 }
1669
1670 fn retrieval_method_uris(&self) -> UriTypeSet {
1671 self.uris.retrieval_methods
1672 }
1673
1674 fn key_info_reference_source_enabled(&self) -> bool {
1675 self.key_sources.key_info_reference
1676 }
1677
1678 fn allowed_transforms(&self) -> Option<&HashSet<String>> {
1679 self.transforms.allowed_algorithms.as_ref()
1680 }
1681}
1682
1683struct KeyInfoReferenceMaterializationContext<'a, 'budget, P> {
1684 policy: &'a P,
1685 provider: &'a dyn crate::provider::CryptoProvider,
1686 budgets: &'a mut RetrievalMaterializationBudgets<'budget>,
1687}
1688
1689fn materialize_key_info_references_with_budgets<P: KeyInfoReferencePolicy>(
1690 key_info: &mut KeyInfo,
1691 resolver: &UriReferenceResolver<'_>,
1692 policy: &P,
1693 provider: &dyn crate::provider::CryptoProvider,
1694 budgets: &mut RetrievalMaterializationBudgets<'_>,
1695 materialization: &mut KeyInfoMaterializationState,
1696) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
1697 fn visit<P: KeyInfoReferencePolicy>(
1698 key_info: &mut KeyInfo,
1699 resolver: &UriReferenceResolver<'_>,
1700 context: &mut KeyInfoReferenceMaterializationContext<'_, '_, P>,
1701 materialization: &mut KeyInfoMaterializationState,
1702 depth: usize,
1703 ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
1704 let mut materialized = Vec::new();
1705 let mut outcome = RetrievalMaterialization::default();
1706 for source in std::mem::take(&mut key_info.sources) {
1707 let source_work = match &source {
1708 super::parse::KeyInfoSource::X509Data(info) => info.certificates.len().max(1),
1709 _ => 1,
1710 };
1711 materialization.candidate_work =
1712 materialization.candidate_work.saturating_add(source_work);
1713 context
1714 .policy
1715 .resources()
1716 .validate_key_candidates(materialization.candidate_work)?;
1717
1718 let super::parse::KeyInfoSource::KeyInfoReference { uri } = source else {
1719 materialized.push(source);
1720 continue;
1721 };
1722 if !context.policy.key_info_reference_source_enabled() {
1723 return Err(crate::policy::PolicyViolation::KeyTrust {
1724 reason: "KeyInfoReference key sources are disabled",
1725 }
1726 .into());
1727 }
1728 if !context.policy.key_info_reference_uris().allows(&uri) {
1729 return Err(crate::policy::PolicyViolation::Uri {
1730 operation: "KeyInfoReference",
1731 reason: "URI class is disabled",
1732 }
1733 .into());
1734 }
1735 let next_depth = depth.saturating_add(1);
1736 context
1737 .policy
1738 .resources()
1739 .validate_key_info_reference_depth(next_depth)?;
1740 let cycle_key = (resolver.traversal_document_identity(), uri.clone());
1741 if !materialization.active.insert(cycle_key.clone()) {
1742 return Err(SignatureVerificationPipelineError::InvalidStructure {
1743 reason: "KeyInfoReference cycle detected",
1744 });
1745 }
1746
1747 let is_same_document = uri.is_empty() || uri.starts_with('#');
1748 let (mut referenced, mut nested_outcome) = if is_same_document {
1749 let node = resolver
1750 .node_for_same_document_reference(&uri)
1751 .map_err(|error| {
1752 SignatureVerificationPipelineError::from(
1753 ReferenceProcessingError::UriDereference(error),
1754 )
1755 })?
1756 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
1757 reason: "KeyInfoReference target is missing or ambiguous",
1758 })?;
1759 if !node.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
1760 return Err(SignatureVerificationPipelineError::InvalidStructure {
1761 reason: "KeyInfoReference target must be KeyInfo",
1762 });
1763 }
1764 (
1765 parse_key_info_with_policy_budgets(
1766 node,
1767 context.provider,
1768 context.budgets.execution.xml_base_resolution(),
1769 context.policy.resources(),
1770 )
1771 .map_err(map_key_info_parse_error)?,
1772 RetrievalMaterialization::default(),
1773 )
1774 } else {
1775 let (resource_uri, fragment) = uri
1779 .split_once('#')
1780 .map_or((uri.as_str(), None), |(resource, fragment)| {
1781 (resource, (!fragment.is_empty()).then_some(fragment))
1782 });
1783 let bytes = resolver
1784 .external_resource(resource_uri)
1785 .map_err(|error| {
1786 SignatureVerificationPipelineError::from(
1787 ReferenceProcessingError::UriDereference(error),
1788 )
1789 })?
1790 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
1791 reason: "KeyInfoReference external resource is unavailable",
1792 })?;
1793 let xml = crate::encoding::decode_xml_octets(bytes).map_err(|_| {
1794 SignatureVerificationPipelineError::InvalidStructure {
1795 reason: "KeyInfoReference external resource has an invalid XML encoding",
1796 }
1797 })?;
1798 let settings = DocumentParseSettings::from_policy(
1799 context.policy.xml(),
1800 context.policy.resources(),
1801 )
1802 .with_backend(context.budgets.xml_backend);
1803 let document = XmlDocument::parse_with_settings_and_budget(
1804 xml.into_owned(),
1805 settings,
1806 context.budgets.execution.xml_parse_work(),
1807 )
1808 .map_err(|error| map_document_parse_error(error, settings))?;
1809 document.with_view(|view| {
1810 let external_resolver = resolver.for_external_document_view(view, resource_uri);
1811 let target = match fragment {
1812 Some(fragment) => external_resolver
1813 .node_for_same_document_reference(&format!("#{fragment}"))
1814 .map_err(|error| {
1815 SignatureVerificationPipelineError::from(
1816 ReferenceProcessingError::UriDereference(error),
1817 )
1818 })?
1819 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
1820 reason: "KeyInfoReference external target is missing or ambiguous",
1821 })?,
1822 _ => view.document().root_element(),
1823 };
1824 if !target.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
1825 return Err(SignatureVerificationPipelineError::InvalidStructure {
1826 reason: "KeyInfoReference external target must be KeyInfo",
1827 });
1828 }
1829 let mut referenced = parse_key_info_with_policy_budgets_and_document_base(
1830 target,
1831 context.provider,
1832 context.budgets.execution.xml_base_resolution(),
1833 context.policy.resources(),
1834 Some(resource_uri),
1835 )
1836 .map_err(map_key_info_parse_error)?;
1837 let mut nested_outcome = visit(
1838 &mut referenced,
1839 &external_resolver,
1840 context,
1841 materialization,
1842 next_depth,
1843 )?;
1844 nested_outcome.merge(materialize_retrieval_methods_with_budgets(
1845 &mut referenced,
1846 &external_resolver,
1847 context.policy.retrieval_method_uris(),
1848 context.policy.allowed_transforms(),
1849 context.provider,
1850 context.budgets,
1851 &mut materialization.candidate_work,
1852 )?);
1853 referenced.sources.retain(|source| {
1856 !matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. })
1857 });
1858 Ok((referenced, nested_outcome))
1859 })?
1860 };
1861 if is_same_document {
1862 nested_outcome.merge(visit(
1863 &mut referenced,
1864 resolver,
1865 context,
1866 materialization,
1867 next_depth,
1868 )?);
1869 }
1870 outcome.merge(nested_outcome);
1871 materialization.active.remove(&cycle_key);
1872 materialized.extend(referenced.sources);
1873 }
1874 key_info.sources = materialized;
1875 context
1876 .policy
1877 .resources()
1878 .validate_key_candidates(key_info.embedded_candidate_count())?;
1879 Ok(outcome)
1880 }
1881
1882 let mut context = KeyInfoReferenceMaterializationContext {
1883 policy,
1884 provider,
1885 budgets,
1886 };
1887 visit(key_info, resolver, &mut context, materialization, 0)
1888}
1889
1890fn materialize_key_info_references_for_policy<P: KeyInfoReferencePolicy>(
1891 key_info: &mut KeyInfo,
1892 resolver: UriReferenceResolver<'_>,
1893 policy: &P,
1894 provider: &dyn crate::provider::CryptoProvider,
1895 xml_backend: crate::XmlBackend,
1896) -> Result<(), DsigError> {
1897 let resolver = resolver.with_external_resource_limits(
1898 policy.resources().max_external_resource_bytes,
1899 policy.resources().max_external_resource_total_bytes,
1900 );
1901 let mut xpath_parse_budget = XPathSignatureParseBudget::from_resources(policy.resources());
1902 let execution_budget = TransformExecutionBudget::from_resources(policy.resources());
1903 let mut budgets = RetrievalMaterializationBudgets {
1904 xpath_parse: &mut xpath_parse_budget,
1905 execution: &execution_budget,
1906 resources: policy.resources(),
1907 xml_backend,
1908 };
1909 let mut materialization = KeyInfoMaterializationState::default();
1910 materialize_key_info_references_with_budgets(
1913 key_info,
1914 &resolver,
1915 policy,
1916 provider,
1917 &mut budgets,
1918 &mut materialization,
1919 )?;
1920 Ok(())
1921}
1922
1923pub fn materialize_signing_key_info_references(
1940 key_info: &mut KeyInfo,
1941 resolver: UriReferenceResolver<'_>,
1942 policy: &crate::policy::SigningPolicy,
1943 provider: &dyn crate::provider::CryptoProvider,
1944 xml_backend: crate::XmlBackend,
1945) -> Result<(), DsigError> {
1946 policy.validate()?;
1947 materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
1948}
1949
1950pub fn materialize_verification_key_info_references(
1965 key_info: &mut KeyInfo,
1966 resolver: UriReferenceResolver<'_>,
1967 policy: &crate::policy::VerificationPolicy,
1968 provider: &dyn crate::provider::CryptoProvider,
1969 xml_backend: crate::XmlBackend,
1970) -> Result<(), DsigError> {
1971 policy.validate()?;
1972 materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
1973}
1974
1975fn materialize_retrieval_methods_with_budgets(
1976 key_info: &mut KeyInfo,
1977 resolver: &UriReferenceResolver<'_>,
1978 allowed_uri_types: UriTypeSet,
1979 allowed_transforms: Option<&HashSet<String>>,
1980 provider: &dyn crate::provider::CryptoProvider,
1981 budgets: &mut RetrievalMaterializationBudgets<'_>,
1982 candidate_work: &mut usize,
1983) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
1984 let retrieval_count = key_info
1985 .sources
1986 .iter()
1987 .filter(|source| matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. }))
1988 .count();
1989 if retrieval_count > MAX_RETRIEVAL_METHOD_COUNT {
1990 return Err(SignatureVerificationPipelineError::InvalidStructure {
1991 reason: "KeyInfo contains too many RetrievalMethod elements",
1992 });
1993 }
1994
1995 let mut total_binary_len = existing_x509_binary_len(key_info)?;
1996 let mut seen = HashSet::new();
1997 let mut materialized = Vec::with_capacity(key_info.sources.len());
1998 let mut outcome = RetrievalMaterialization::default();
1999 for source in std::mem::take(&mut key_info.sources) {
2000 let super::parse::KeyInfoSource::RetrievalMethod {
2001 uri: resolved_uri,
2002 resource_type,
2003 transforms,
2004 } = source
2005 else {
2006 materialized.push(source);
2007 continue;
2008 };
2009
2010 let identity = (
2011 resolved_uri.clone(),
2012 resource_type.clone(),
2013 transforms.clone(),
2014 );
2015 if !seen.insert(identity) {
2016 continue;
2017 }
2018
2019 if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#rawX509Certificate")
2020 {
2021 if transforms != RetrievalMethodTransforms::None
2022 || classify_uri(&resolved_uri) != UriClass::External
2023 {
2024 return Err(SignatureVerificationPipelineError::InvalidStructure {
2025 reason: "raw X509 RetrievalMethod requires an untransformed external URI",
2026 });
2027 }
2028 if !allowed_uri_types.allows(&resolved_uri) {
2029 return Err(crate::policy::PolicyViolation::Uri {
2030 operation: "verification",
2031 reason: "retrieval method URI class is not permitted",
2032 }
2033 .into());
2034 }
2035 let certificate = resolver.external_resource(&resolved_uri).map_err(|error| {
2036 SignatureVerificationPipelineError::from(ReferenceProcessingError::Transform(error))
2037 })?;
2038 let Some(certificate) = certificate else {
2039 outcome.deferred_error.get_or_insert_with(|| {
2040 SignatureVerificationPipelineError::Reference(
2041 ReferenceProcessingError::Transform(super::TransformError::UnsupportedUri(
2042 resolved_uri.clone(),
2043 )),
2044 )
2045 });
2046 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
2047 uri: resolved_uri,
2048 resource_type,
2049 transforms,
2050 });
2051 continue;
2052 };
2053 *candidate_work = candidate_work.saturating_add(1);
2054 budgets.resources.validate_key_candidates(*candidate_work)?;
2055 if certificate.len() > MAX_X509_DECODED_BINARY_LEN {
2056 return Err(SignatureVerificationPipelineError::InvalidStructure {
2057 reason: "raw X509 RetrievalMethod certificate exceeds maximum allowed length",
2058 });
2059 }
2060 add_retrieval_binary_usage(&mut total_binary_len, certificate.len())?;
2061 let parsed = match parse_x509_certificate(certificate) {
2062 Ok(parsed) => parsed,
2063 Err(error) => {
2064 let error = map_key_info_parse_error(error);
2065 if matches!(error, SignatureVerificationPipelineError::Policy(_)) {
2066 return Err(error);
2067 }
2068 outcome.deferred_error.get_or_insert(error);
2069 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
2070 uri: resolved_uri,
2071 resource_type,
2072 transforms,
2073 });
2074 continue;
2075 }
2076 };
2077 materialized.push(super::parse::KeyInfoSource::X509Data(
2078 super::parse::X509DataInfo {
2079 certificates: vec![certificate.to_vec()],
2080 parsed_certificates: vec![parsed],
2081 certificate_chain: vec![0],
2082 ..super::parse::X509DataInfo::default()
2083 },
2084 ));
2085 } else if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#X509Data") {
2086 if !allowed_uri_types.allows(&resolved_uri) {
2087 return Err(crate::policy::PolicyViolation::Uri {
2088 operation: "verification",
2089 reason: "retrieval method URI class is not permitted",
2090 }
2091 .into());
2092 }
2093 let target = resolver
2094 .node_for_same_document_reference(&resolved_uri)
2095 .map_err(ReferenceProcessingError::Transform)?;
2096 let Some(target) = target else {
2097 outcome.deferred_error.get_or_insert(
2098 SignatureVerificationPipelineError::InvalidStructure {
2099 reason: "X509Data RetrievalMethod target is missing or ambiguous",
2100 },
2101 );
2102 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
2103 uri: resolved_uri,
2104 resource_type,
2105 transforms,
2106 });
2107 continue;
2108 };
2109 let node = match transforms {
2110 RetrievalMethodTransforms::None
2111 if target.has_tag_name((XMLDSIG_NS, "X509Data")) =>
2112 {
2113 target
2114 }
2115 RetrievalMethodTransforms::None => {
2116 return Err(SignatureVerificationPipelineError::InvalidStructure {
2117 reason: "untransformed X509Data RetrievalMethod must target X509Data directly",
2118 });
2119 }
2120 RetrievalMethodTransforms::X509DataNodeSetFilter {
2121 expression,
2122 namespaces,
2123 } => {
2124 enforce_transform_allowed(allowed_transforms, XPATH_TRANSFORM_URI)?;
2125 budgets
2126 .xpath_parse
2127 .validate_expression(&expression)
2128 .map_err(ReferenceProcessingError::Transform)?;
2129 budgets
2130 .xpath_parse
2131 .validate_namespaces(&namespaces)
2132 .map_err(ReferenceProcessingError::Transform)?;
2133 select_retrieved_x509_data_root(target, budgets.execution)?
2134 }
2135 RetrievalMethodTransforms::Unsupported => {
2136 return Err(SignatureVerificationPipelineError::InvalidStructure {
2137 reason: "X509Data RetrievalMethod contains unsupported transforms",
2138 });
2139 }
2140 };
2141 let data = parse_x509_data_dispatch_with_budget_and_provider(
2142 node,
2143 &mut total_binary_len,
2144 candidate_work,
2145 provider,
2146 budgets.resources,
2147 )
2148 .map_err(map_key_info_parse_error)?;
2149 materialized.push(super::parse::KeyInfoSource::X509Data(data));
2150 } else {
2151 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
2152 uri: resolved_uri,
2153 resource_type,
2154 transforms,
2155 });
2156 }
2157 }
2158 key_info.sources = materialized;
2159 Ok(outcome)
2160}
2161
2162fn select_retrieved_x509_data_root<'a, 'input>(
2163 target: Node<'a, 'input>,
2164 execution_budget: &TransformExecutionBudget,
2165) -> Result<Node<'a, 'input>, SignatureVerificationPipelineError> {
2166 let context_nodes = NodeSet::ensure_subtree_materialization_fits_with_budget(
2171 target,
2172 false,
2173 execution_budget.node_set_materialization(),
2174 )
2175 .map_err(ReferenceProcessingError::Transform)?;
2176 execution_budget
2177 .validate_xpath_context_evaluations(context_nodes)
2178 .map_err(ReferenceProcessingError::Transform)?;
2179 execution_budget
2180 .charge_xpath_work(context_nodes)
2181 .map_err(ReferenceProcessingError::Transform)?;
2182 execution_budget
2183 .charge_node_filter_work(context_nodes)
2184 .map_err(ReferenceProcessingError::Transform)?;
2185 let mut root = None;
2186 for candidate in target.descendants() {
2187 if !candidate.is_element()
2188 || candidate.tag_name().namespace() != Some(XMLDSIG_NS)
2189 || candidate.tag_name().name() != "X509Data"
2190 {
2191 continue;
2192 }
2193 if root.replace(candidate).is_some() {
2194 return Err(SignatureVerificationPipelineError::InvalidStructure {
2195 reason: "X509Data RetrievalMethod selected multiple X509Data elements",
2196 });
2197 }
2198 }
2199 root.ok_or(SignatureVerificationPipelineError::InvalidStructure {
2200 reason: "X509Data RetrievalMethod selected no X509Data element",
2201 })
2202}
2203
2204#[cfg(test)]
2205fn materialize_retrieval_methods(
2206 key_info: &mut KeyInfo,
2207 resolver: &UriReferenceResolver<'_>,
2208 allowed_uri_types: UriTypeSet,
2209 allowed_transforms: Option<&HashSet<String>>,
2210 provider: &dyn crate::provider::CryptoProvider,
2211) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
2212 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
2213 let execution_budget = TransformExecutionBudget::default();
2214 let resources = crate::policy::ResourcePolicy::default();
2215 let mut budgets = RetrievalMaterializationBudgets {
2216 xpath_parse: &mut xpath_parse_budget,
2217 execution: &execution_budget,
2218 resources: &resources,
2219 xml_backend: crate::XmlBackend::default(),
2220 };
2221 let mut candidate_work = key_info.embedded_candidate_count();
2222 materialize_retrieval_methods_with_budgets(
2223 key_info,
2224 resolver,
2225 allowed_uri_types,
2226 allowed_transforms,
2227 provider,
2228 &mut budgets,
2229 &mut candidate_work,
2230 )
2231}
2232
2233fn existing_x509_binary_len(
2234 key_info: &KeyInfo,
2235) -> Result<usize, SignatureVerificationPipelineError> {
2236 let mut total = 0usize;
2237 for source in &key_info.sources {
2238 if let super::parse::KeyInfoSource::X509Data(info) = source {
2239 for len in info
2240 .certificates
2241 .iter()
2242 .chain(&info.skis)
2243 .chain(&info.crls)
2244 .map(Vec::len)
2245 .chain(info.digests.iter().map(|(_, digest)| digest.len()))
2246 {
2247 add_retrieval_binary_usage(&mut total, len)?;
2248 }
2249 }
2250 }
2251 Ok(total)
2252}
2253
2254fn add_retrieval_binary_usage(
2255 total: &mut usize,
2256 delta: usize,
2257) -> Result<(), SignatureVerificationPipelineError> {
2258 *total =
2259 total
2260 .checked_add(delta)
2261 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2262 reason: "RetrievalMethod X509Data binary length overflow",
2263 })?;
2264 if *total > MAX_X509_DATA_TOTAL_BINARY_LEN {
2265 return Err(SignatureVerificationPipelineError::InvalidStructure {
2266 reason: "RetrievalMethod X509Data exceeds maximum aggregate binary length",
2267 });
2268 }
2269 Ok(())
2270}
2271
2272fn manifest_reference_failure_reason(
2273 error: ReferenceProcessingError,
2274 ref_index: usize,
2275) -> FailureReason {
2276 match error.into_policy_violation() {
2277 Ok(_) => FailureReason::ReferencePolicyViolation { ref_index },
2278 Err(_) => FailureReason::ReferenceProcessingFailure { ref_index },
2279 }
2280}
2281
2282fn process_manifest_references(
2283 signature_node: Node<'_, '_>,
2284 resolver: &UriReferenceResolver<'_>,
2285 ctx: &VerifyContext<'_>,
2286 signed_info_reference_nodes: &HashSet<NodeId>,
2287 remaining_reference_capacity: usize,
2288 execution: &ReferenceExecutionContext<'_>,
2289 xpath_parse_budget: &mut XPathSignatureParseBudget,
2290) -> Result<Vec<ReferenceResult>, SignatureVerificationPipelineError> {
2291 let mut authenticated_nodes = signed_info_reference_nodes.clone();
2292 let mut processed_manifests = HashSet::new();
2293 let mut remaining_reference_capacity = remaining_reference_capacity;
2294 let mut next_reference_index = 0usize;
2295 let mut results = Vec::new();
2296 loop {
2297 let parsed = parse_manifest_references(
2298 signature_node,
2299 &authenticated_nodes,
2300 &mut processed_manifests,
2301 &mut remaining_reference_capacity,
2302 &mut next_reference_index,
2303 xpath_parse_budget,
2304 ctx.allowed_transform_uris(),
2305 )?;
2306 let manifest_references = parsed.references;
2307 results.extend(parsed.invalid_results);
2308 if manifest_references.is_empty() {
2309 break;
2310 }
2311 results.reserve(manifest_references.len());
2312 for (index, reference, reference_node_id) in &manifest_references {
2313 let result = if execution.transform_budget.remaining_c14n_output() == 0
2314 || reference.transforms.len() > ctx.policy.resources.max_transforms_per_reference
2315 || ctx
2316 .policy
2317 .digest_algorithms
2318 .as_ref()
2319 .is_some_and(|allowed| !allowed.contains(&reference.digest_method))
2320 {
2321 manifest_reference_invalid_result(
2322 reference,
2323 *index,
2324 FailureReason::ReferencePolicyViolation { ref_index: *index },
2325 )
2326 } else {
2327 match enforce_reference_policies(
2328 std::slice::from_ref(reference),
2329 ctx.policy.uris.references,
2330 ctx.allowed_transform_uris(),
2331 ) {
2332 Ok(()) => process_reference_with_options(
2333 reference,
2334 resolver,
2335 signature_node,
2336 ReferenceSet::Manifest,
2337 *index,
2338 resolver.node_for_node_id(*reference_node_id),
2339 execution,
2340 )
2341 .unwrap_or_else(|error| {
2342 manifest_reference_invalid_result(
2343 reference,
2344 *index,
2345 manifest_reference_failure_reason(error, *index),
2346 )
2347 }),
2348 Err(SignatureVerificationPipelineError::Policy(_)) => {
2349 manifest_reference_invalid_result(
2350 reference,
2351 *index,
2352 FailureReason::ReferencePolicyViolation { ref_index: *index },
2353 )
2354 }
2355 Err(_) => manifest_reference_invalid_result(
2356 reference,
2357 *index,
2358 FailureReason::ReferenceProcessingFailure { ref_index: *index },
2359 ),
2360 }
2361 };
2362 if result.status == DsigStatus::Valid
2363 && reference
2364 .transforms
2365 .iter()
2366 .all(transform_preserves_manifest_structure)
2367 && let Ok(Some(target_id)) = reference.uri.as_deref().map_or_else(
2368 || Ok(None),
2369 |uri| resolver.node_id_for_same_document_reference(uri),
2370 )
2371 {
2372 authenticated_nodes.insert(target_id);
2375 }
2376 results.push(result);
2377 }
2378 }
2379 results.sort_by_key(|result| result.reference_index);
2380 Ok(results)
2381}
2382
2383fn manifest_reference_invalid_result(
2384 reference: &Reference,
2385 index: usize,
2386 reason: FailureReason,
2387) -> ReferenceResult {
2388 ReferenceResult {
2389 reference_set: ReferenceSet::Manifest,
2390 reference_index: index,
2391 uri: reference
2392 .uri
2393 .clone()
2394 .unwrap_or_else(|| "<omitted>".to_owned()),
2395 digest_algorithm: reference.digest_method,
2396 status: DsigStatus::Invalid(reason),
2397 pre_digest_data: None,
2398 }
2399}
2400
2401fn parse_manifest_references(
2402 signature_node: Node<'_, '_>,
2403 authenticated_nodes: &HashSet<NodeId>,
2404 processed_manifests: &mut HashSet<NodeId>,
2405 remaining_reference_capacity: &mut usize,
2406 next_reference_index: &mut usize,
2407 xpath_parse_budget: &mut XPathSignatureParseBudget,
2408 allowed_transforms: Option<&HashSet<String>>,
2409) -> Result<ParsedManifestReferences, SignatureVerificationPipelineError> {
2410 let mut references = Vec::new();
2411 let mut invalid = Vec::new();
2412 for object_node in signature_node.children().filter(|node| {
2413 node.is_element()
2414 && node.tag_name().namespace() == Some(XMLDSIG_NS)
2415 && node.tag_name().name() == "Object"
2416 }) {
2417 let object_is_signed = authenticated_nodes.contains(&object_node.id());
2418 for manifest_node in object_node.children().filter(|node| {
2419 node.is_element()
2420 && node.tag_name().namespace() == Some(XMLDSIG_NS)
2421 && node.tag_name().name() == "Manifest"
2422 }) {
2423 let manifest_is_signed = authenticated_nodes.contains(&manifest_node.id());
2424 if !object_is_signed && !manifest_is_signed {
2427 continue;
2428 }
2429 if !processed_manifests.insert(manifest_node.id()) {
2430 continue;
2431 }
2432 let mut manifest_children = Vec::new();
2433 for child in manifest_node.children() {
2434 if child.is_text()
2435 && child.text().is_some_and(|text| {
2436 text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
2437 })
2438 {
2439 return Err(SignatureVerificationPipelineError::InvalidStructure {
2440 reason: "Manifest contains non-whitespace mixed content",
2441 });
2442 }
2443 if child.is_element() {
2444 manifest_children.push(child);
2445 }
2446 }
2447 if manifest_children.is_empty() {
2448 return Err(SignatureVerificationPipelineError::InvalidStructure {
2449 reason: "Manifest must contain at least one ds:Reference element child",
2450 });
2451 }
2452 for child in manifest_children {
2453 if child.tag_name().namespace() != Some(XMLDSIG_NS)
2454 || child.tag_name().name() != "Reference"
2455 {
2456 return Err(SignatureVerificationPipelineError::InvalidStructure {
2457 reason: "Manifest must contain only ds:Reference element children",
2458 });
2459 }
2460 if *remaining_reference_capacity == 0 {
2461 return Err(SignatureVerificationPipelineError::InvalidStructure {
2462 reason: "signed Manifests exceed the per-signature Reference limit",
2463 });
2464 }
2465 *remaining_reference_capacity -= 1;
2466 let reference_index = *next_reference_index;
2467 *next_reference_index += 1;
2468 match parse_reference_with_xpath_budget(child, xpath_parse_budget) {
2469 Ok(reference) => references.push((reference_index, reference, child.id())),
2470 Err(ParseError::Transform(super::TransformError::UnsupportedTransform(
2471 uri,
2472 ))) => {
2473 let digest_algorithm =
2474 reference_digest_method(child).map_err(map_manifest_parse_error)?;
2475 let reason =
2476 if allowed_transforms.is_some_and(|allowed| !allowed.contains(&uri)) {
2477 FailureReason::ReferencePolicyViolation {
2478 ref_index: reference_index,
2479 }
2480 } else {
2481 FailureReason::ReferenceProcessingFailure {
2482 ref_index: reference_index,
2483 }
2484 };
2485 invalid.push(ReferenceResult {
2486 reference_set: ReferenceSet::Manifest,
2487 reference_index,
2488 uri: child.attribute("URI").unwrap_or("<omitted>").to_owned(),
2489 digest_algorithm,
2490 status: DsigStatus::Invalid(reason),
2491 pre_digest_data: None,
2492 });
2493 }
2494 Err(error) => return Err(map_manifest_parse_error(error)),
2495 }
2496 }
2497 }
2498 }
2499 Ok(ParsedManifestReferences {
2500 references,
2501 invalid_results: invalid,
2502 })
2503}
2504
2505struct ParsedManifestReferences {
2506 references: Vec<(usize, Reference, NodeId)>,
2507 invalid_results: Vec<ReferenceResult>,
2508}
2509
2510fn collect_authenticated_signed_info_reference_nodes(
2511 references: &[Reference],
2512 resolver: &UriReferenceResolver<'_>,
2513) -> HashSet<NodeId> {
2514 references
2515 .iter()
2516 .filter(|reference| {
2520 reference
2521 .transforms
2522 .iter()
2523 .all(transform_preserves_manifest_structure)
2524 })
2525 .filter_map(|reference| reference.uri.as_deref())
2526 .filter_map(|uri| {
2527 resolver
2528 .node_id_for_same_document_reference(uri)
2529 .ok()
2530 .flatten()
2531 })
2532 .collect()
2533}
2534
2535fn transform_preserves_manifest_structure(transform: &Transform) -> bool {
2536 match transform {
2537 Transform::C14n(_) => true,
2538 Transform::Enveloped
2543 | Transform::XpathExcludeAllSignatures
2544 | Transform::XPath(_)
2545 | Transform::XPathFilter2(_)
2546 | Transform::Base64Decode => false,
2547 }
2548}
2549
2550enum ResolvedVerifyingKey<'a> {
2551 Borrowed(&'a dyn VerifyingKey),
2552 Owned(Box<dyn VerifyingKey + 'a>),
2553}
2554
2555impl ResolvedVerifyingKey<'_> {
2556 fn as_ref(&self) -> &dyn VerifyingKey {
2557 match self {
2558 Self::Borrowed(key) => *key,
2559 Self::Owned(key) => key.as_ref(),
2560 }
2561 }
2562}
2563
2564fn resolve_verifying_key<'k>(
2565 ctx: &VerifyContext<'k>,
2566 key_info: Option<&KeyInfo>,
2567 algorithm: SignatureAlgorithm,
2568) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
2569 if let Some(key) = ctx.key {
2570 if !ctx.policy.key_sources.preset_key {
2571 return Err(crate::policy::PolicyViolation::KeyTrust {
2572 reason: "pre-resolved verification keys are disabled",
2573 }
2574 .into());
2575 }
2576 require_verifying_key_candidate_capacity(&ctx.policy)?;
2577 return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
2578 }
2579 if let Some(resolver) = ctx.key_resolver {
2580 require_verifying_key_candidate_capacity(&ctx.policy)?;
2581 let resolved = resolver.resolve_with_policy_and_provider(
2582 key_info,
2583 algorithm,
2584 &ctx.policy,
2585 ctx.provider,
2586 )?;
2587 return Ok(resolved.map(ResolvedVerifyingKey::Owned));
2588 }
2589 Ok(None)
2590}
2591
2592fn require_verifying_key_candidate_capacity(
2593 policy: &crate::policy::VerificationPolicy,
2594) -> Result<(), SignatureVerificationPipelineError> {
2595 policy
2596 .resources
2597 .validate_key_candidates(1)
2598 .map_err(Into::into)
2599}
2600
2601fn enforce_reference_policies(
2602 references: &[Reference],
2603 allowed_uri_types: UriTypeSet,
2604 allowed_transforms: Option<&HashSet<String>>,
2605) -> Result<(), SignatureVerificationPipelineError> {
2606 for reference in references {
2607 let uri = reference
2608 .uri
2609 .as_deref()
2610 .ok_or(SignatureVerificationPipelineError::Reference(
2611 ReferenceProcessingError::MissingUri,
2612 ))?;
2613 if !allowed_uri_types.allows(uri) {
2614 return Err(crate::policy::PolicyViolation::Uri {
2615 operation: "verification",
2616 reason: "reference URI class is not permitted",
2617 }
2618 .into());
2619 }
2620
2621 if let Some(allowed) = allowed_transforms {
2622 for transform in &reference.transforms {
2623 let transform_uri = transform.algorithm_uri();
2624 enforce_transform_allowed(Some(allowed), transform_uri)?;
2625 }
2626
2627 let produces_binary = transform_chain_produces_binary(
2632 classify_uri(uri) == UriClass::External,
2633 &reference.transforms,
2634 );
2635 if !produces_binary {
2636 enforce_transform_allowed(Some(allowed), DEFAULT_IMPLICIT_C14N_URI)?;
2637 }
2638 }
2639 }
2640 Ok(())
2641}
2642
2643fn enforce_transform_allowed(
2644 allowed_transforms: Option<&HashSet<String>>,
2645 algorithm: &str,
2646) -> Result<(), SignatureVerificationPipelineError> {
2647 if allowed_transforms.is_some_and(|allowed| !allowed.contains(algorithm)) {
2648 return Err(crate::policy::PolicyViolation::Algorithm {
2649 operation: "verification transform",
2650 algorithm: algorithm.to_owned(),
2651 }
2652 .into());
2653 }
2654 Ok(())
2655}
2656
2657#[derive(Debug, Clone, Copy)]
2658pub(super) struct SignatureChildNodes<'a, 'input> {
2659 signed_info_node: Node<'a, 'input>,
2660 signature_value_node: Node<'a, 'input>,
2661 key_info_node: Option<Node<'a, 'input>>,
2662}
2663
2664pub(super) fn parse_signature_children<'a, 'input>(
2665 signature_node: Node<'a, 'input>,
2666) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
2667 let mut signed_info_node: Option<Node<'_, '_>> = None;
2668 let mut signature_value_node: Option<Node<'_, '_>> = None;
2669 let mut key_info_node: Option<Node<'_, '_>> = None;
2670 let mut signed_info_index: Option<usize> = None;
2671 let mut signature_value_index: Option<usize> = None;
2672 let mut key_info_index: Option<usize> = None;
2673 let mut first_unexpected_dsig_index: Option<usize> = None;
2674
2675 let mut element_index = 0usize;
2676 for child in signature_node.children() {
2677 if child.is_text() {
2678 if child
2679 .text()
2680 .is_some_and(|text| !is_xml_whitespace_only(text))
2681 {
2682 return Err(SignatureVerificationPipelineError::InvalidStructure {
2683 reason: "Signature must not contain non-whitespace mixed content",
2684 });
2685 }
2686 continue;
2687 }
2688 if !child.is_element() {
2689 continue;
2690 }
2691
2692 element_index += 1;
2693 if child.tag_name().namespace() != Some(XMLDSIG_NS) {
2694 return Err(SignatureVerificationPipelineError::InvalidStructure {
2695 reason: "Signature must contain only XMLDSIG element children",
2696 });
2697 }
2698 match child.tag_name().name() {
2699 "SignedInfo" => {
2700 if signed_info_node.is_some() {
2701 return Err(SignatureVerificationPipelineError::InvalidStructure {
2702 reason: "SignedInfo must appear exactly once under Signature",
2703 });
2704 }
2705 signed_info_node = Some(child);
2706 signed_info_index = Some(element_index);
2707 }
2708 "SignatureValue" => {
2709 if signature_value_node.is_some() {
2710 return Err(SignatureVerificationPipelineError::InvalidStructure {
2711 reason: "SignatureValue must appear exactly once under Signature",
2712 });
2713 }
2714 signature_value_node = Some(child);
2715 signature_value_index = Some(element_index);
2716 }
2717 "KeyInfo" => {
2718 if key_info_node.is_some() {
2719 return Err(SignatureVerificationPipelineError::InvalidStructure {
2720 reason: "KeyInfo must appear at most once under Signature",
2721 });
2722 }
2723 key_info_node = Some(child);
2724 key_info_index = Some(element_index);
2725 }
2726 "Object" => {
2727 }
2730 _ => {
2731 if first_unexpected_dsig_index.is_none() {
2732 first_unexpected_dsig_index = Some(element_index);
2733 }
2734 }
2735 }
2736 }
2737
2738 let signed_info_node =
2739 signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
2740 element: "SignedInfo",
2741 })?;
2742 let signature_value_node =
2743 signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
2744 element: "SignatureValue",
2745 })?;
2746 if signed_info_index != Some(1) {
2747 return Err(SignatureVerificationPipelineError::InvalidStructure {
2748 reason: "SignedInfo must be the first element child of Signature",
2749 });
2750 }
2751 if signature_value_index != Some(2) {
2752 return Err(SignatureVerificationPipelineError::InvalidStructure {
2753 reason: "SignatureValue must be the second element child of Signature",
2754 });
2755 }
2756 if let Some(index) = key_info_index
2757 && index != 3
2758 {
2759 return Err(SignatureVerificationPipelineError::InvalidStructure {
2760 reason: "KeyInfo must be the third element child of Signature when present",
2761 });
2762 }
2763
2764 let allowed_prefix_end = key_info_index.unwrap_or(2);
2765 if let Some(unexpected_index) = first_unexpected_dsig_index {
2766 return Err(SignatureVerificationPipelineError::InvalidStructure {
2767 reason: if unexpected_index > allowed_prefix_end {
2768 "After SignedInfo, SignatureValue, and optional KeyInfo, Signature may contain only Object elements"
2769 } else {
2770 "Signature may contain SignedInfo first, SignatureValue second, optional KeyInfo third, and Object elements thereafter"
2771 },
2772 });
2773 }
2774
2775 Ok(SignatureChildNodes {
2776 signed_info_node,
2777 signature_value_node,
2778 key_info_node,
2779 })
2780}
2781
2782fn decode_signature_value(
2783 signature_value_node: Node<'_, '_>,
2784) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
2785 if signature_value_node
2786 .children()
2787 .any(|child| child.is_element())
2788 {
2789 return Err(SignatureVerificationPipelineError::InvalidStructure {
2790 reason: "SignatureValue must not contain element children",
2791 });
2792 }
2793
2794 let mut normalized = Vec::new();
2795 let mut raw_text_len = 0usize;
2796 for child in signature_value_node
2797 .children()
2798 .filter(|child| child.is_text())
2799 {
2800 if let Some(text) = child.text() {
2801 push_normalized_signature_text(text, &mut raw_text_len, &mut normalized)?;
2802 }
2803 }
2804
2805 Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
2806}
2807
2808fn push_normalized_signature_text(
2809 text: &str,
2810 raw_text_len: &mut usize,
2811 normalized: &mut Vec<u8>,
2812) -> Result<(), SignatureVerificationPipelineError> {
2813 if raw_text_len.saturating_add(text.len()) > MAX_SIGNATURE_VALUE_TEXT_LEN {
2814 return Err(SignatureVerificationPipelineError::InvalidStructure {
2815 reason: "SignatureValue exceeds maximum allowed text length",
2816 });
2817 }
2818 *raw_text_len = raw_text_len.saturating_add(text.len());
2819
2820 normalize_xml_base64_bytes(text.as_bytes(), normalized, |_| true).map_err(|err| {
2821 SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(
2822 err.normalized_offset,
2823 err.invalid_byte,
2824 ))
2825 })?;
2826 if normalized.len() > MAX_SIGNATURE_VALUE_LEN {
2827 return Err(SignatureVerificationPipelineError::InvalidStructure {
2828 reason: "SignatureValue exceeds maximum allowed length",
2829 });
2830 }
2831
2832 Ok(())
2833}
2834
2835fn verify_with_algorithm(
2836 algorithm: SignatureAlgorithm,
2837 public_key_pem: &str,
2838 signed_data: &[u8],
2839 signature_value: &[u8],
2840) -> Result<bool, SignatureVerificationPipelineError> {
2841 match algorithm {
2842 SignatureAlgorithm::DsaSha1 | SignatureAlgorithm::DsaSha256 => {
2843 let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
2844 .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
2845 if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
2846 return Err(SignatureVerificationError::InvalidKeyPem.into());
2847 }
2848 Ok(verify_dsa_signature_spki(
2849 algorithm,
2850 &pem.contents,
2851 signed_data,
2852 signature_value,
2853 )?)
2854 }
2855 SignatureAlgorithm::HmacSha1
2856 | SignatureAlgorithm::HmacSha224
2857 | SignatureAlgorithm::HmacSha256
2858 | SignatureAlgorithm::HmacSha384
2859 | SignatureAlgorithm::HmacSha512 => Err(SignatureVerificationError::UnsupportedAlgorithm {
2860 uri: algorithm.uri().to_string(),
2861 }
2862 .into()),
2863 SignatureAlgorithm::RsaSha1
2864 | SignatureAlgorithm::RsaSha224
2865 | SignatureAlgorithm::RsaSha256
2866 | SignatureAlgorithm::RsaSha384
2867 | SignatureAlgorithm::RsaSha512 => Ok(verify_rsa_signature_pem(
2868 algorithm,
2869 public_key_pem,
2870 signed_data,
2871 signature_value,
2872 )?),
2873 SignatureAlgorithm::EcdsaSha1
2874 | SignatureAlgorithm::EcdsaSha224
2875 | SignatureAlgorithm::EcdsaSha256
2876 | SignatureAlgorithm::EcdsaSha384
2877 | SignatureAlgorithm::EcdsaSha512 => {
2878 match verify_ecdsa_signature_pem(
2882 algorithm,
2883 public_key_pem,
2884 signed_data,
2885 signature_value,
2886 ) {
2887 Ok(valid) => Ok(valid),
2888 Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
2889 Err(error) => Err(error.into()),
2890 }
2891 }
2892 }
2893}
2894
2895#[cfg(test)]
2896#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
2897mod tests {
2898 use super::*;
2899 use crate::c14n::C14nAlgorithm;
2900 use crate::xml::dom::Document;
2901 use crate::xmldsig::TransformError;
2902 use crate::xmldsig::digest::DigestAlgorithm;
2903 use crate::xmldsig::parse::{Reference, parse_signed_info};
2904 use crate::xmldsig::transforms::Transform;
2905 use crate::xmldsig::uri::UriReferenceResolver;
2906 use base64::Engine;
2907
2908 fn make_reference(
2912 uri: &str,
2913 transforms: Vec<Transform>,
2914 digest_method: DigestAlgorithm,
2915 digest_value: Vec<u8>,
2916 ) -> Reference {
2917 Reference {
2918 uri: Some(uri.to_string()),
2919 id: None,
2920 ref_type: None,
2921 transforms,
2922 digest_method,
2923 digest_value,
2924 }
2925 }
2926
2927 #[test]
2928 fn reference_resolution_uses_each_elements_effective_xml_base() {
2929 let first = b"first payload";
2932 let second = b"second payload";
2933 let first_digest = base64::engine::general_purpose::STANDARD
2934 .encode(compute_digest(DigestAlgorithm::Sha256, first));
2935 let second_digest = base64::engine::general_purpose::STANDARD
2936 .encode(compute_digest(DigestAlgorithm::Sha256, second));
2937 let xml = format!(
2938 r#"<root xml:base="https://example.test/base/" xmlns:ds="{XMLDSIG_NS}">
2939 <ds:Signature><ds:SignedInfo>
2940 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2941 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2942 <ds:Reference xml:base="one/" URI="payload.bin">
2943 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2944 <ds:DigestValue>{first_digest}</ds:DigestValue>
2945 </ds:Reference>
2946 <ds:Reference xml:base="../two/" URI="payload.bin">
2947 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2948 <ds:DigestValue>{second_digest}</ds:DigestValue>
2949 </ds:Reference>
2950 </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>
2951 </root>"#
2952 );
2953 let document = Document::parse(&xml).unwrap();
2954 let signature = document
2955 .descendants()
2956 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
2957 .unwrap();
2958 let signed_info_node = signature
2959 .children()
2960 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
2961 .unwrap();
2962 let signed_info = parse_signed_info(signed_info_node).unwrap();
2963 let resources = HashMap::from([
2964 (
2965 "https://example.test/base/one/payload.bin".into(),
2966 first.to_vec(),
2967 ),
2968 (
2969 "https://example.test/two/payload.bin".into(),
2970 second.to_vec(),
2971 ),
2972 ]);
2973 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
2974
2975 let result = process_all_references(&signed_info.references, &resolver, signature, false)
2976 .expect("each Reference should resolve against its own effective base");
2977
2978 assert!(result.all_valid());
2979 }
2980
2981 #[test]
2982 fn internal_dtd_opt_in_applies_to_detached_xml_transforms() {
2983 let detached = b"<!DOCTYPE payload [<!ELEMENT payload (#PCDATA)>]><payload>ok</payload>";
2986 let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
2987 DigestAlgorithm::Sha256,
2988 b"<payload>ok</payload>",
2989 ));
2990 let xml = format!(
2991 r#"<root xmlns:ds="{XMLDSIG_NS}">
2992 <ds:Signature>
2993 <ds:SignedInfo>
2994 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2995 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2996 <ds:Reference URI="urn:detached-dtd">
2997 <ds:Transforms>
2998 <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
2999 </ds:Transforms>
3000 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3001 <ds:DigestValue>{digest}</ds:DigestValue>
3002 </ds:Reference>
3003 </ds:SignedInfo>
3004 <ds:SignatureValue>AQ==</ds:SignatureValue>
3005 </ds:Signature>
3006</root>"#
3007 );
3008 let resources = HashMap::from([("urn:detached-dtd".to_owned(), detached.to_vec())]);
3009 let key = AcceptingKey;
3010
3011 let default_error = VerifyContext::new()
3012 .key(&key)
3013 .allowed_uri_types(UriTypeSet::ALL)
3014 .external_resources(&resources)
3015 .verify(&xml)
3016 .expect_err("internal DTD parsing must remain disabled by default");
3017 assert!(matches!(
3018 default_error,
3019 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
3020 crate::xmldsig::TransformError::XmlParse(_)
3021 ))
3022 ));
3023
3024 let result = VerifyContext::new()
3025 .key(&key)
3026 .allowed_uri_types(UriTypeSet::ALL)
3027 .external_resources(&resources)
3028 .allow_internal_dtd(true)
3029 .verify(&xml)
3030 .expect("the explicit DTD opt-in must cover detached XML transforms");
3031
3032 assert_eq!(result.status, DsigStatus::Valid);
3033
3034 let external_entity = br#"<!DOCTYPE payload [
3035 <!ENTITY ext SYSTEM "file:///etc/passwd">
3036 ]><payload>&ext;</payload>"#;
3037 let external_entity_resources =
3038 HashMap::from([("urn:detached-dtd".to_owned(), external_entity.to_vec())]);
3039 let external_entity_error = VerifyContext::new()
3040 .key(&key)
3041 .allowed_uri_types(UriTypeSet::ALL)
3042 .external_resources(&external_entity_resources)
3043 .allow_internal_dtd(true)
3044 .verify(&xml)
3045 .expect_err("the internal-DTD opt-in must not resolve external entities");
3046 assert!(matches!(
3047 external_entity_error,
3048 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
3049 crate::xmldsig::TransformError::XmlParse(_)
3050 ))
3051 ));
3052 }
3053
3054 #[test]
3055 fn owned_verification_revalidates_internal_dtd_provenance() {
3056 let document = XmlDocument::parse_with_settings(
3059 "<!DOCTYPE root [<!ENTITY value \"ok\">]><root>&value;</root>".into(),
3060 DocumentParseSettings::new(
3061 true,
3062 crate::hard_limits::XML_DOCUMENT_NODE_CEILING,
3063 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
3064 ),
3065 )
3066 .expect("explicitly permitted DTD fixture must parse");
3067
3068 assert!(matches!(
3069 VerifyContext::new().verify_document(&document),
3070 Err(DsigError::Policy(
3071 crate::policy::PolicyViolation::XmlInput {
3072 reason: "owned document requires internal DTD support"
3073 }
3074 ))
3075 ));
3076 assert!(!matches!(
3077 VerifyContext::new()
3078 .allow_internal_dtd(true)
3079 .verify_document(&document),
3080 Err(DsigError::Policy(
3081 crate::policy::PolicyViolation::XmlInput { .. }
3082 ))
3083 ));
3084 }
3085
3086 #[test]
3087 fn verification_policy_bounds_reference_canonicalization() {
3088 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
3091 let xml = format!(
3092 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>"#,
3093 "payload".repeat(16)
3094 );
3095 let policy = crate::policy::VerificationPolicy {
3096 resources: crate::policy::ResourcePolicy {
3097 max_canonicalized_bytes: 64,
3098 ..crate::policy::ResourcePolicy::default()
3099 },
3100 ..crate::policy::VerificationPolicy::default()
3101 };
3102
3103 let error = VerifyContext::new()
3104 .key(&AcceptingKey)
3105 .policy(policy)
3106 .verify(&xml)
3107 .expect_err("reference canonicalization must consume the policy budget");
3108
3109 assert!(
3110 matches!(
3111 error,
3112 SignatureVerificationPipelineError::Policy(
3113 crate::policy::PolicyViolation::ResourceLimit {
3114 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
3115 maximum: 64,
3116 ..
3117 }
3118 )
3119 ),
3120 "unexpected error: {error:?}"
3121 );
3122 }
3123
3124 #[test]
3125 fn verification_policy_bounds_document_bytes_before_parsing() {
3126 let xml = format!("<root>{}</root>", "x".repeat(1_024));
3129 let policy = crate::policy::VerificationPolicy {
3130 resources: crate::policy::ResourcePolicy {
3131 max_xml_document_bytes: xml.len() - 1,
3132 ..crate::policy::ResourcePolicy::default()
3133 },
3134 ..crate::policy::VerificationPolicy::default()
3135 };
3136
3137 assert!(matches!(
3138 VerifyContext::new().policy(policy).verify(&xml),
3139 Err(SignatureVerificationPipelineError::Policy(
3140 crate::policy::PolicyViolation::ResourceLimit {
3141 resource: crate::policy::resource_name::XML_DOCUMENT,
3142 maximum,
3143 actual,
3144 }
3145 )) if maximum == xml.len() - 1 && actual == xml.len()
3146 ));
3147 }
3148
3149 #[test]
3150 fn verification_entry_points_enforce_policy_depth() {
3151 let xml = "<root><child><leaf/></child></root>";
3154 let policy = crate::policy::VerificationPolicy {
3155 resources: crate::policy::ResourcePolicy {
3156 max_xml_depth: 2,
3157 ..crate::policy::ResourcePolicy::default()
3158 },
3159 ..crate::policy::VerificationPolicy::default()
3160 };
3161 let document = XmlDocument::parse(xml).expect("wide retained fixture must parse");
3162
3163 assert!(matches!(
3164 VerifyContext::new().policy(policy.clone()).verify(xml),
3165 Err(DsigError::Policy(
3166 crate::policy::PolicyViolation::ResourceLimit {
3167 resource: crate::policy::resource_name::XML_DEPTH,
3168 maximum: 2,
3169 actual: 3,
3170 }
3171 ))
3172 ));
3173 assert!(matches!(
3174 VerifyContext::new()
3175 .policy(policy)
3176 .verify_document(&document),
3177 Err(DsigError::Policy(
3178 crate::policy::PolicyViolation::ResourceLimit {
3179 resource: crate::policy::resource_name::XML_DEPTH,
3180 maximum: 2,
3181 actual: 3,
3182 }
3183 ))
3184 ));
3185 }
3186
3187 #[test]
3188 fn verification_policy_bounds_base64_transform_input() {
3189 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
3192 let xml = format!(
3193 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>"##
3194 );
3195 let policy = crate::policy::VerificationPolicy {
3196 resources: crate::policy::ResourcePolicy {
3197 max_base64_transform_input_bytes: 4,
3198 ..crate::policy::ResourcePolicy::default()
3199 },
3200 ..crate::policy::VerificationPolicy::default()
3201 };
3202
3203 let error = VerifyContext::new()
3204 .key(&AcceptingKey)
3205 .policy(policy)
3206 .verify(&xml)
3207 .expect_err("Base64 input must use the operation policy ceiling");
3208
3209 assert!(matches!(
3210 error,
3211 SignatureVerificationPipelineError::Policy(
3212 crate::policy::PolicyViolation::ResourceLimit {
3213 resource: crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
3214 maximum: 4,
3215 ..
3216 }
3217 )
3218 ));
3219 }
3220
3221 #[test]
3222 fn verification_policy_bounds_cumulative_base64_transform_output() {
3223 let first_digest = base64::engine::general_purpose::STANDARD
3226 .encode(compute_digest(DigestAlgorithm::Sha256, b"a"));
3227 let second_digest = base64::engine::general_purpose::STANDARD
3228 .encode(compute_digest(DigestAlgorithm::Sha256, b"b"));
3229 let xml = format!(
3230 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>"##
3231 );
3232 let policy = crate::policy::VerificationPolicy {
3233 resources: crate::policy::ResourcePolicy {
3234 max_base64_transform_input_bytes: 8,
3235 max_base64_transform_output_bytes: 1,
3236 ..crate::policy::ResourcePolicy::default()
3237 },
3238 ..crate::policy::VerificationPolicy::default()
3239 };
3240
3241 let error = VerifyContext::new()
3242 .key(&AcceptingKey)
3243 .policy(policy)
3244 .verify(&xml)
3245 .expect_err("references must share the Base64 output allowance");
3246
3247 assert!(matches!(
3248 error,
3249 SignatureVerificationPipelineError::Policy(
3250 crate::policy::PolicyViolation::ResourceLimit {
3251 resource: crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
3252 maximum: 1,
3253 actual: 2,
3254 }
3255 )
3256 ));
3257 }
3258
3259 #[test]
3260 fn verification_policy_bounds_xpath_source_before_compilation() {
3261 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
3264 let xml = format!(
3265 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>"#
3266 );
3267 let policy = crate::policy::VerificationPolicy {
3268 resources: crate::policy::ResourcePolicy {
3269 max_xpath_expression_bytes: 4,
3270 ..crate::policy::ResourcePolicy::default()
3271 },
3272 ..crate::policy::VerificationPolicy::default()
3273 };
3274
3275 let error = VerifyContext::new()
3276 .key(&AcceptingKey)
3277 .policy(policy)
3278 .verify(&xml)
3279 .expect_err("XPath source must use the operation policy ceiling");
3280
3281 assert!(matches!(
3282 error,
3283 SignatureVerificationPipelineError::Policy(
3284 crate::policy::PolicyViolation::ResourceLimit {
3285 resource: crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
3286 maximum: 4,
3287 ..
3288 }
3289 )
3290 ));
3291 }
3292
3293 #[test]
3294 fn verification_policy_shares_canonicalization_budget_with_signed_info() {
3295 let payload_text = "x".repeat(700);
3299 let canonical_payload = format!("<payload ID=\"payload\">{payload_text}</payload>");
3300 let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
3301 DigestAlgorithm::Sha256,
3302 canonical_payload.as_bytes(),
3303 ));
3304 let xml = format!(
3305 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>"##
3306 );
3307 let policy = crate::policy::VerificationPolicy {
3308 resources: crate::policy::ResourcePolicy {
3309 max_canonicalized_bytes: 1_024,
3310 ..crate::policy::ResourcePolicy::default()
3311 },
3312 ..crate::policy::VerificationPolicy::default()
3313 };
3314
3315 let error = VerifyContext::new()
3316 .key(&AcceptingKey)
3317 .policy(policy)
3318 .verify(&xml)
3319 .expect_err("SignedInfo must consume the remaining operation C14N budget");
3320
3321 assert!(
3322 matches!(
3323 &error,
3324 SignatureVerificationPipelineError::Policy(
3325 crate::policy::PolicyViolation::ResourceLimit {
3326 resource: "canonicalized bytes",
3327 maximum: 1_024,
3328 ..
3329 }
3330 )
3331 ),
3332 "unexpected error: {error:?}"
3333 );
3334 }
3335
3336 #[test]
3337 fn manifest_processing_stops_after_c14n_budget_exhaustion() {
3338 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
3341 let xml = format!(
3342 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>"##
3343 );
3344 let document = Document::parse(&xml).expect("test signature must parse");
3345 let signature = document
3346 .descendants()
3347 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
3348 .expect("test signature must contain Signature");
3349 let object = signature
3350 .children()
3351 .find(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
3352 .expect("test signature must contain Object");
3353 let resources = HashMap::from([("urn:small".to_owned(), b"small".to_vec())]);
3354 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
3355 let transform_budget = TransformExecutionBudget::with_c14n_limit(8);
3356 let canonicalized_data_budget = CanonicalizedDataBudget::default();
3357 let execution = ReferenceExecutionContext {
3358 store_pre_digest: false,
3359 transform_options: TransformOptions::default(),
3360 transform_budget: &transform_budget,
3361 canonicalized_data_budget: &canonicalized_data_budget,
3362 provider: crate::provider::default_provider(),
3363 };
3364 let ctx = VerifyContext::new()
3365 .allowed_uri_types(UriTypeSet::ALL)
3366 .external_resources(&resources);
3367 let authenticated = HashSet::from([object.id()]);
3368 let mut xpath_budget = XPathSignatureParseBudget::default();
3369
3370 let results = process_manifest_references(
3371 signature,
3372 &resolver,
3373 &ctx,
3374 &authenticated,
3375 2,
3376 &execution,
3377 &mut xpath_budget,
3378 )
3379 .expect("resource exhaustion is reported per Manifest reference");
3380
3381 assert_eq!(results.len(), 2);
3382 assert!(results.iter().all(|result| {
3383 matches!(
3384 result.status,
3385 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { .. })
3386 )
3387 }));
3388 }
3389
3390 #[test]
3391 fn verification_policy_bounds_detached_xml_nodes() {
3392 let detached = format!("<payload>{}</payload>", "<n/>".repeat(32));
3395 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
3396 let xml = format!(
3397 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>"#
3398 );
3399 let resources = HashMap::from([("urn:detached-nodes".to_owned(), detached.into_bytes())]);
3400 let policy = crate::policy::VerificationPolicy {
3401 uris: crate::policy::UriPolicy {
3402 references: UriTypeSet::ALL,
3403 ..crate::policy::UriPolicy::default()
3404 },
3405 resources: crate::policy::ResourcePolicy {
3406 max_xml_nodes: 24,
3407 ..crate::policy::ResourcePolicy::default()
3408 },
3409 ..crate::policy::VerificationPolicy::default()
3410 };
3411
3412 let error = VerifyContext::new()
3413 .key(&AcceptingKey)
3414 .policy(policy)
3415 .external_resources(&resources)
3416 .verify(&xml)
3417 .expect_err("detached XML must inherit the policy node ceiling");
3418
3419 assert!(
3420 matches!(
3421 error,
3422 SignatureVerificationPipelineError::Policy(
3423 crate::policy::PolicyViolation::ResourceLimit {
3424 resource: crate::policy::resource_name::XML_NODES,
3425 maximum: 24,
3426 ..
3427 }
3428 )
3429 ),
3430 "unexpected error: {error:?}"
3431 );
3432 }
3433
3434 #[test]
3435 fn query_only_reference_resolves_against_relative_xml_base() {
3436 let payload = b"query-selected payload";
3439 let digest = base64::engine::general_purpose::STANDARD
3440 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
3441 let xml = format!(
3442 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo>
3443 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3444 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3445 <ds:Reference xml:base="a/b?old" URI="?new">
3446 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3447 <ds:DigestValue>{digest}</ds:DigestValue>
3448 </ds:Reference>
3449 </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>"#
3450 );
3451 let document = Document::parse(&xml).unwrap();
3452 let signature = document.root_element();
3453 let signed_info_node = signature
3454 .children()
3455 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
3456 .unwrap();
3457 let signed_info = parse_signed_info(signed_info_node).unwrap();
3458 let resources = HashMap::from([("a/b?new".to_string(), payload.to_vec())]);
3459 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
3460
3461 let result = process_all_references(&signed_info.references, &resolver, signature, false)
3462 .expect("query-only URI must resolve against the complete relative base path");
3463
3464 assert!(result.all_valid());
3465 }
3466
3467 #[test]
3468 fn manifest_reference_resolution_uses_its_effective_xml_base() {
3469 let payload = b"manifest payload";
3472 let digest = base64::engine::general_purpose::STANDARD
3473 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
3474 let xml = format!(
3475 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
3476 <ds:Object><ds:Manifest xml:base="manifests/">
3477 <ds:Reference URI="payload.bin">
3478 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3479 <ds:DigestValue>{digest}</ds:DigestValue>
3480 </ds:Reference>
3481 </ds:Manifest></ds:Object>
3482 </ds:Signature>"#
3483 );
3484 let document = Document::parse(&xml).unwrap();
3485 let signature = document.root_element();
3486 let reference_node = signature
3487 .descendants()
3488 .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
3489 .unwrap();
3490 let reference = super::super::parse::parse_reference(reference_node).unwrap();
3491 let resources = HashMap::from([(
3492 "https://example.test/manifests/payload.bin".to_string(),
3493 payload.to_vec(),
3494 )]);
3495 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
3496
3497 let result = process_reference(
3498 &reference,
3499 &resolver,
3500 signature,
3501 ReferenceSet::Manifest,
3502 0,
3503 false,
3504 )
3505 .expect("Manifest Reference should inherit its own XML Base context");
3506
3507 assert_eq!(result.status, DsigStatus::Valid);
3508 }
3509
3510 #[test]
3511 fn manifest_reference_index_ignores_nested_manifest_descendants() {
3512 let payload = b"direct manifest payload";
3515 let digest = base64::engine::general_purpose::STANDARD
3516 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
3517 let xml = format!(
3518 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
3519 <ds:Object><wrapper><ds:Manifest xml:base="nested/">
3520 <ds:Reference URI="payload.bin">
3521 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3522 <ds:DigestValue>{digest}</ds:DigestValue>
3523 </ds:Reference>
3524 </ds:Manifest></wrapper></ds:Object>
3525 <ds:Object><ds:Manifest xml:base="direct/">
3526 <ds:Reference URI="payload.bin">
3527 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3528 <ds:DigestValue>{digest}</ds:DigestValue>
3529 </ds:Reference>
3530 </ds:Manifest></ds:Object>
3531 </ds:Signature>"#
3532 );
3533 let document = Document::parse(&xml).unwrap();
3534 let signature = document.root_element();
3535 let direct_reference_node = signature
3536 .children()
3537 .filter(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
3538 .nth(1)
3539 .unwrap()
3540 .children()
3541 .find(|node| node.has_tag_name((XMLDSIG_NS, "Manifest")))
3542 .unwrap()
3543 .children()
3544 .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
3545 .unwrap();
3546 let reference = super::super::parse::parse_reference(direct_reference_node).unwrap();
3547 let resources = HashMap::from([(
3548 "https://example.test/direct/payload.bin".to_string(),
3549 payload.to_vec(),
3550 )]);
3551 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
3552
3553 let result = process_reference(
3554 &reference,
3555 &resolver,
3556 signature,
3557 ReferenceSet::Manifest,
3558 0,
3559 false,
3560 )
3561 .expect("Manifest index must select the direct Object/Manifest reference");
3562
3563 assert_eq!(result.status, DsigStatus::Valid);
3564 }
3565
3566 struct RejectingKey;
3567
3568 impl VerifyingKey for RejectingKey {
3569 fn verify(
3570 &self,
3571 _algorithm: SignatureAlgorithm,
3572 _signed_data: &[u8],
3573 _signature_value: &[u8],
3574 ) -> Result<bool, SignatureVerificationPipelineError> {
3575 Ok(false)
3576 }
3577 }
3578
3579 struct AcceptingKey;
3580
3581 impl VerifyingKey for AcceptingKey {
3582 fn verify(
3583 &self,
3584 _algorithm: SignatureAlgorithm,
3585 _signed_data: &[u8],
3586 _signature_value: &[u8],
3587 ) -> Result<bool, SignatureVerificationPipelineError> {
3588 Ok(true)
3589 }
3590 }
3591
3592 struct PanicResolver;
3593
3594 impl KeyResolver for PanicResolver {
3595 fn resolve<'a>(
3596 &'a self,
3597 _key_info: Option<&KeyInfo>,
3598 _algorithm: SignatureAlgorithm,
3599 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
3600 {
3601 panic!("resolver should not be called when references already fail");
3602 }
3603 }
3604
3605 struct MissingKeyResolver;
3606
3607 impl KeyResolver for MissingKeyResolver {
3608 fn resolve<'a>(
3609 &'a self,
3610 _key_info: Option<&KeyInfo>,
3611 _algorithm: SignatureAlgorithm,
3612 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
3613 {
3614 Ok(None)
3615 }
3616 }
3617
3618 struct ConsumingKeyInfoResolver;
3619
3620 impl KeyResolver for ConsumingKeyInfoResolver {
3621 fn resolve<'a>(
3622 &'a self,
3623 _key_info: Option<&KeyInfo>,
3624 _algorithm: SignatureAlgorithm,
3625 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
3626 {
3627 Ok(None)
3628 }
3629
3630 fn consumes_document_key_info(&self) -> bool {
3631 true
3632 }
3633 }
3634
3635 struct FallbackKeyInfoResolver;
3636
3637 impl KeyResolver for FallbackKeyInfoResolver {
3638 fn resolve<'a>(
3639 &'a self,
3640 key_info: Option<&KeyInfo>,
3641 _algorithm: SignatureAlgorithm,
3642 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
3643 {
3644 let sources = &key_info.expect("KeyInfo must be parsed").sources;
3645 assert!(matches!(
3646 sources.as_slice(),
3647 [
3648 super::super::parse::KeyInfoSource::RetrievalMethod { .. },
3649 super::super::parse::KeyInfoSource::KeyName(name),
3650 ] if name == "fallback"
3651 ));
3652 Ok(Some(Box::new(AcceptingKey)))
3653 }
3654
3655 fn consumes_document_key_info(&self) -> bool {
3656 true
3657 }
3658 }
3659
3660 struct EarlyKeyInfoResolver;
3661
3662 impl KeyResolver for EarlyKeyInfoResolver {
3663 fn resolve<'a>(
3664 &'a self,
3665 key_info: Option<&KeyInfo>,
3666 _algorithm: SignatureAlgorithm,
3667 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
3668 {
3669 let sources = &key_info.expect("KeyInfo must be parsed").sources;
3670 assert!(matches!(
3671 sources.as_slice(),
3672 [
3673 super::super::parse::KeyInfoSource::KeyName(name),
3674 super::super::parse::KeyInfoSource::RetrievalMethod { .. },
3675 ] if name == "primary"
3676 ));
3677 Ok(Some(Box::new(AcceptingKey)))
3678 }
3679
3680 fn consumes_document_key_info(&self) -> bool {
3681 true
3682 }
3683 }
3684
3685 fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
3686 format!(
3687 r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3688 <ds:SignedInfo>
3689 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3690 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3691 <ds:Reference URI="{reference_uri}">
3692 {transforms_xml}
3693 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3694 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
3695 </ds:Reference>
3696 </ds:SignedInfo>
3697 <ds:SignatureValue>AQ==</ds:SignatureValue>
3698</ds:Signature>"#
3699 )
3700 }
3701
3702 fn signature_with_target_reference(signature_value_b64: &str) -> String {
3703 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3704 <target ID="target">payload</target>
3705 <ds:Signature>
3706 <ds:SignedInfo>
3707 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3708 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3709 <ds:Reference URI="#target">
3710 <ds:Transforms>
3711 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3712 </ds:Transforms>
3713 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3714 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
3715 </ds:Reference>
3716 </ds:SignedInfo>
3717 <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
3718 </ds:Signature>
3719</root>"##;
3720
3721 let doc = Document::parse(xml_template).unwrap();
3722 let sig_node = doc
3723 .descendants()
3724 .find(|node| node.is_element() && node.tag_name().name() == "Signature")
3725 .unwrap();
3726 let signed_info_node = sig_node
3727 .children()
3728 .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
3729 .unwrap();
3730 let signed_info = parse_signed_info(signed_info_node).unwrap();
3731 let reference = &signed_info.references[0];
3732 let resolver = UriReferenceResolver::new(&doc);
3733 let initial_data = resolver
3734 .dereference(reference.uri.as_deref().unwrap())
3735 .unwrap();
3736 let pre_digest =
3737 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
3738 .unwrap();
3739 let digest = compute_digest(reference.digest_method, &pre_digest);
3740 let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
3741 xml_template
3742 .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
3743 .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
3744 }
3745
3746 #[test]
3747 fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
3748 let xml = signature_with_target_reference("AQ==");
3749
3750 let result = VerifyContext::new()
3751 .verify(&xml)
3752 .expect("missing key config must be reported as verification status");
3753 assert!(
3754 matches!(
3755 result.status,
3756 DsigStatus::Invalid(FailureReason::KeyNotFound)
3757 ),
3758 "unexpected status: {:?}",
3759 result.status
3760 );
3761 }
3762
3763 #[test]
3764 fn verify_context_rejects_disallowed_uri() {
3765 let xml = minimal_signature_xml("http://example.com/external", "");
3766 let err = VerifyContext::new()
3767 .key(&RejectingKey)
3768 .verify(&xml)
3769 .expect_err("external URI should be rejected by default policy");
3770 assert!(matches!(
3771 err,
3772 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
3773 operation: "verification",
3774 ..
3775 })
3776 ));
3777 }
3778
3779 #[test]
3780 fn verify_context_bounds_effective_xml_base_components() {
3781 let mut xml = minimal_signature_xml("payload", "");
3784 for _ in 0..65 {
3785 xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
3786 }
3787 let resources = HashMap::new();
3788 let error = VerifyContext::new()
3789 .key(&AcceptingKey)
3790 .allowed_uri_types(UriTypeSet::ALL)
3791 .external_resources(&resources)
3792 .verify(&xml)
3793 .expect_err("XML Base component work must be bounded before lookup");
3794
3795 assert!(matches!(
3796 error,
3797 SignatureVerificationPipelineError::Policy(
3798 crate::policy::PolicyViolation::ResourceLimit {
3799 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
3800 maximum: 64,
3801 actual: 65,
3802 }
3803 )
3804 ));
3805 }
3806
3807 #[test]
3808 fn verify_context_bounds_cumulative_xml_base_resolution_bytes() {
3809 let mut xml = minimal_signature_xml("payload", "");
3812 for _ in 0..2 {
3813 xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
3814 }
3815 let resources = HashMap::new();
3816 let mut policy = crate::policy::VerificationPolicy::default();
3817 policy.resources.max_xml_base_resolution_bytes = 32;
3818 let error = VerifyContext::new()
3819 .policy(policy)
3820 .key(&AcceptingKey)
3821 .allowed_uri_types(UriTypeSet::ALL)
3822 .external_resources(&resources)
3823 .verify(&xml)
3824 .expect_err("cumulative XML Base copies must obey the operation budget");
3825
3826 assert!(matches!(
3827 error,
3828 SignatureVerificationPipelineError::Policy(
3829 crate::policy::PolicyViolation::ResourceLimit {
3830 resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
3831 maximum: 32,
3832 ..
3833 }
3834 )
3835 ));
3836 }
3837
3838 #[test]
3839 fn verify_context_applies_xml_base_policy_to_signed_info_c14n() {
3840 let xml = signature_with_target_reference("AQ==")
3844 .replacen(
3845 "http://www.w3.org/2001/10/xml-exc-c14n#",
3846 "http://www.w3.org/2006/12/xml-c14n11",
3847 1,
3848 )
3849 .replace(
3850 " <ds:Signature>",
3851 " <outer xml:base=\"one/\"><inner xml:base=\"two/\"><ds:Signature>",
3852 )
3853 .replace(" </ds:Signature>", " </ds:Signature></inner></outer>");
3854 let policy = crate::policy::VerificationPolicy {
3855 resources: crate::policy::ResourcePolicy {
3856 max_xml_base_components: 1,
3857 ..crate::policy::ResourcePolicy::default()
3858 },
3859 ..crate::policy::VerificationPolicy::default()
3860 };
3861
3862 let error = VerifyContext::new()
3863 .key(&AcceptingKey)
3864 .policy(policy)
3865 .verify(&xml)
3866 .expect_err("SignedInfo C14N must use the operation XML Base budget");
3867
3868 assert!(matches!(
3869 error,
3870 SignatureVerificationPipelineError::Policy(
3871 crate::policy::PolicyViolation::ResourceLimit {
3872 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
3873 maximum: 1,
3874 actual: 2,
3875 }
3876 )
3877 ));
3878 }
3879
3880 #[test]
3881 fn verify_context_classifies_signed_info_xml_base_byte_limit_as_policy() {
3882 let xml = signature_with_target_reference("AQ==")
3885 .replacen(
3886 "http://www.w3.org/2001/10/xml-exc-c14n#",
3887 "http://www.w3.org/2006/12/xml-c14n11",
3888 1,
3889 )
3890 .replace(
3891 " <ds:Signature>",
3892 " <outer xml:base=\"segment/\"><ds:Signature>",
3893 )
3894 .replace(" </ds:Signature>", " </ds:Signature></outer>");
3895 let policy = crate::policy::VerificationPolicy {
3896 resources: crate::policy::ResourcePolicy {
3897 max_xml_base_resolution_bytes: 1,
3898 ..crate::policy::ResourcePolicy::default()
3899 },
3900 ..crate::policy::VerificationPolicy::default()
3901 };
3902
3903 let error = VerifyContext::new()
3904 .key(&AcceptingKey)
3905 .policy(policy)
3906 .verify(&xml)
3907 .expect_err("SignedInfo XML Base byte exhaustion must be a policy error");
3908
3909 assert!(matches!(
3910 error,
3911 SignatureVerificationPipelineError::Policy(
3912 crate::policy::PolicyViolation::ResourceLimit {
3913 resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
3914 maximum: 1,
3915 actual,
3916 }
3917 ) if actual > 1
3918 ));
3919 }
3920
3921 #[test]
3922 fn verify_context_meters_repeated_external_dereferences() {
3923 let payload = b"payload";
3926 let digest = base64::engine::general_purpose::STANDARD.encode(
3927 crate::xmldsig::compute_digest(DigestAlgorithm::Sha1, payload),
3928 );
3929 let reference = format!(
3930 r#"<ds:Reference URI="urn:payload"><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference>"#
3931 );
3932 let xml = format!(
3933 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>"#
3934 );
3935 let resources = HashMap::from([("urn:payload".to_owned(), payload.to_vec())]);
3936 let policy = crate::policy::VerificationPolicy {
3937 uris: crate::policy::UriPolicy {
3938 references: UriTypeSet::ALL,
3939 ..crate::policy::UriPolicy::default()
3940 },
3941 resources: crate::policy::ResourcePolicy {
3942 max_external_resource_bytes: payload.len(),
3943 max_external_resource_total_bytes: payload.len(),
3944 ..crate::policy::ResourcePolicy::default()
3945 },
3946 ..crate::policy::VerificationPolicy::default()
3947 };
3948
3949 let error = VerifyContext::new()
3950 .key(&AcceptingKey)
3951 .policy(policy)
3952 .external_resources(&resources)
3953 .verify(&xml)
3954 .expect_err("the second dereference must exhaust the aggregate byte ceiling");
3955
3956 assert!(
3957 error
3958 .to_string()
3959 .contains("aggregate external resource bytes")
3960 );
3961 }
3962
3963 #[test]
3964 fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
3965 let xml = minimal_signature_xml("", "");
3966 let err = VerifyContext::new()
3967 .key(&RejectingKey)
3968 .allowed_uri_types(UriTypeSet::new(false, true, false))
3969 .verify(&xml)
3970 .expect_err("empty URI must be rejected when empty references are disabled");
3971 assert!(matches!(
3972 err,
3973 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
3974 operation: "verification",
3975 ..
3976 })
3977 ));
3978 }
3979
3980 #[test]
3981 fn verify_context_rejects_disallowed_transform() {
3982 let xml = minimal_signature_xml(
3983 "",
3984 r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
3985 );
3986 let err = VerifyContext::new()
3987 .key(&RejectingKey)
3988 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
3989 .verify(&xml)
3990 .expect_err("enveloped transform should be rejected by allowlist");
3991 assert!(matches!(
3992 err,
3993 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
3994 operation: "verification transform",
3995 ..
3996 })
3997 ));
3998 }
3999
4000 #[test]
4001 fn verify_context_applies_transform_allowlist_to_signed_info_c14n() {
4002 let xml = signature_with_target_reference("AQ==").replacen(
4005 "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
4006 "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"/>",
4007 1,
4008 );
4009 let error = VerifyContext::new()
4010 .key(&AcceptingKey)
4011 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
4012 .verify(&xml)
4013 .expect_err("SignedInfo C14N must obey the operation transform allowlist");
4014
4015 assert!(matches!(
4016 error,
4017 SignatureVerificationPipelineError::Policy(
4018 crate::policy::PolicyViolation::Algorithm {
4019 operation: "verification transform",
4020 ref algorithm,
4021 }
4022 )
4023 if algorithm == "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
4024 ));
4025 }
4026
4027 #[test]
4028 fn verify_context_applies_transform_allowlist_to_key_retrieval() {
4029 let xml = signature_with_target_reference("AQ==")
4032 .replacen(
4033 "</ds:Signature>",
4034 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>"##,
4035 1,
4036 )
4037 .replacen(
4038 "</root>",
4039 r#"<holder ID="keys"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder></root>"#,
4040 1,
4041 );
4042 let error = VerifyContext::new()
4043 .key_resolver(&ConsumingKeyInfoResolver)
4044 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
4045 .verify(&xml)
4046 .expect_err("RetrievalMethod XPath must obey the operation transform allowlist");
4047
4048 assert!(matches!(
4049 error,
4050 SignatureVerificationPipelineError::Policy(
4051 crate::policy::PolicyViolation::Algorithm {
4052 operation: "verification transform",
4053 ref algorithm,
4054 }
4055 )
4056 if algorithm == XPATH_TRANSFORM_URI
4057 ));
4058 }
4059
4060 fn signature_with_manifest_xml(valid_manifest_digest: bool) -> String {
4061 signature_with_manifest_xml_with_manifest_mutation(valid_manifest_digest, |xml| xml)
4062 }
4063
4064 fn signature_with_manifest_xml_with_manifest_mutation<F>(
4065 valid_manifest_digest: bool,
4066 mutate_manifest: F,
4067 ) -> String
4068 where
4069 F: FnOnce(String) -> String,
4070 {
4071 const TMP_SIGNED_INFO_DIGEST: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
4072 const INVALID_MANIFEST_DIGEST: &str = "//////////////////////////8=";
4073 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4074 <target ID="target">payload</target>
4075 <ds:Signature>
4076 <ds:SignedInfo>
4077 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4078 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4079 <ds:Reference URI="#manifest">
4080 <ds:Transforms>
4081 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4082 </ds:Transforms>
4083 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
4084 <ds:DigestValue>SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER</ds:DigestValue>
4085 </ds:Reference>
4086 </ds:SignedInfo>
4087 <ds:SignatureValue>AQ==</ds:SignatureValue>
4088 <ds:Object>
4089 <ds:Manifest ID="manifest">
4090 <ds:Reference URI="#target">
4091 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
4092 <ds:DigestValue>MANIFEST_DIGEST_PLACEHOLDER</ds:DigestValue>
4093 </ds:Reference>
4094 </ds:Manifest>
4095 </ds:Object>
4096 </ds:Signature>
4097</root>"##;
4098 let seed_xml = xml_template.replace(
4099 "SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER",
4100 TMP_SIGNED_INFO_DIGEST,
4101 );
4102 let doc = Document::parse(&seed_xml).unwrap();
4103 let signature_node = doc
4104 .descendants()
4105 .find(|node| {
4106 node.is_element()
4107 && node.tag_name().namespace() == Some(XMLDSIG_NS)
4108 && node.tag_name().name() == "Signature"
4109 })
4110 .unwrap();
4111 let resolver = UriReferenceResolver::new(&doc);
4112 let initial_data = resolver.dereference("#target").unwrap();
4113 let manifest_pre_digest =
4114 crate::xmldsig::execute_transforms(signature_node, initial_data, &[]).unwrap();
4115 let computed_manifest_digest_b64 = base64::engine::general_purpose::STANDARD
4116 .encode(compute_digest(DigestAlgorithm::Sha1, &manifest_pre_digest));
4117 let final_manifest_digest_b64 = if valid_manifest_digest {
4118 computed_manifest_digest_b64.as_str()
4119 } else {
4120 INVALID_MANIFEST_DIGEST
4121 };
4122 let xml_with_manifest_digest = mutate_manifest(
4123 seed_xml.replace("MANIFEST_DIGEST_PLACEHOLDER", final_manifest_digest_b64),
4124 );
4125 let signed_doc = Document::parse(&xml_with_manifest_digest).unwrap();
4126 let signed_signature_node = signed_doc
4127 .descendants()
4128 .find(|node| {
4129 node.is_element()
4130 && node.tag_name().namespace() == Some(XMLDSIG_NS)
4131 && node.tag_name().name() == "Signature"
4132 })
4133 .unwrap();
4134 let signed_info_node = signed_signature_node
4135 .children()
4136 .find(|node| {
4137 node.is_element()
4138 && node.tag_name().namespace() == Some(XMLDSIG_NS)
4139 && node.tag_name().name() == "SignedInfo"
4140 })
4141 .unwrap();
4142 let signed_info = parse_signed_info(signed_info_node).unwrap();
4143 let object_reference = &signed_info.references[0];
4144 let signed_resolver = UriReferenceResolver::new(&signed_doc);
4145 let signed_initial_data = signed_resolver
4146 .dereference(object_reference.uri.as_deref().unwrap())
4147 .unwrap();
4148 let signed_pre_digest = crate::xmldsig::execute_transforms(
4149 signed_signature_node,
4150 signed_initial_data,
4151 &object_reference.transforms,
4152 )
4153 .unwrap();
4154 let signed_digest_b64 = base64::engine::general_purpose::STANDARD.encode(compute_digest(
4155 object_reference.digest_method,
4156 &signed_pre_digest,
4157 ));
4158
4159 xml_with_manifest_digest.replacen(TMP_SIGNED_INFO_DIGEST, &signed_digest_b64, 1)
4160 }
4161
4162 fn replace_fixture_manifest_digest(xml: &str, replacement: &str) -> String {
4163 let object_marker = "<ds:Object>";
4164 let object_start = xml
4165 .find(object_marker)
4166 .expect("fixture should contain ds:Object")
4167 + object_marker.len();
4168 let open = "<ds:DigestValue>";
4169 let close = "</ds:DigestValue>";
4170 let value_start = xml[object_start..]
4171 .find(open)
4172 .map(|offset| object_start + offset + open.len())
4173 .expect("Manifest should contain DigestValue");
4174 let value_end = xml[value_start..]
4175 .find(close)
4176 .map(|offset| value_start + offset)
4177 .expect("Manifest DigestValue must be closed");
4178
4179 format!("{}{replacement}{}", &xml[..value_start], &xml[value_end..])
4180 }
4181
4182 #[test]
4183 fn verify_context_processes_manifest_references_when_enabled() {
4184 let xml = signature_with_manifest_xml(true);
4185
4186 let result_without_manifests = VerifyContext::new()
4187 .key(&RejectingKey)
4188 .verify(&xml)
4189 .expect("manifest processing disabled should still verify SignedInfo");
4190 assert!(
4191 result_without_manifests.manifest_references.is_empty(),
4192 "manifest results must stay empty when manifest processing is disabled",
4193 );
4194 assert!(matches!(
4195 result_without_manifests.status,
4196 DsigStatus::Invalid(FailureReason::SignatureMismatch)
4197 ));
4198
4199 let malformed_manifest_xml = signature_with_manifest_xml(true).replacen(
4200 "</ds:Object>",
4201 "</ds:Object><ds:Object><ds:Manifest><ds:Foo/></ds:Manifest></ds:Object>",
4202 1,
4203 );
4204 let malformed_with_manifests_disabled = VerifyContext::new()
4205 .key(&RejectingKey)
4206 .verify(&malformed_manifest_xml)
4207 .expect("malformed Manifest must be ignored when manifest processing is disabled");
4208 assert!(
4209 malformed_with_manifests_disabled
4210 .manifest_references
4211 .is_empty(),
4212 "manifest parser must not run when process_manifests is disabled",
4213 );
4214 assert!(matches!(
4215 malformed_with_manifests_disabled.status,
4216 DsigStatus::Invalid(FailureReason::SignatureMismatch)
4217 ));
4218
4219 let result_with_manifests = VerifyContext::new()
4220 .key(&AcceptingKey)
4221 .process_manifests(true)
4222 .verify(&xml)
4223 .expect("manifest references should be processed when enabled");
4224 assert_eq!(result_with_manifests.manifest_references.len(), 1);
4225 assert_eq!(
4226 result_with_manifests.manifest_references[0].reference_set,
4227 ReferenceSet::Manifest
4228 );
4229 assert_eq!(
4230 result_with_manifests.manifest_references[0].reference_index,
4231 0
4232 );
4233 assert!(matches!(
4234 result_with_manifests.manifest_references[0].status,
4235 DsigStatus::Valid
4236 ));
4237 assert!(matches!(result_with_manifests.status, DsigStatus::Valid));
4238 }
4239
4240 #[test]
4241 fn verify_context_skips_manifest_work_when_signature_value_is_invalid() {
4242 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4246 replace_fixture_manifest_digest(&xml, "!!!")
4247 });
4248 assert!(
4249 xml.split_once("<ds:Object>")
4250 .is_some_and(|(_, object)| object.contains("<ds:DigestValue>!!!</ds:DigestValue>")),
4251 "fixture mutation must corrupt the nested Manifest DigestValue",
4252 );
4253
4254 let result = VerifyContext::new()
4255 .key(&RejectingKey)
4256 .process_manifests(true)
4257 .verify(&xml)
4258 .expect("invalid SignatureValue must short-circuit Manifest parsing");
4259
4260 assert!(matches!(
4261 result.status,
4262 DsigStatus::Invalid(FailureReason::SignatureMismatch)
4263 ));
4264 assert!(result.manifest_references.is_empty());
4265 }
4266
4267 #[test]
4268 fn verify_context_shares_xpath_parse_budget_with_manifest_references() {
4269 let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
4272 .repeat(64);
4273 let transform = format!(
4274 r#"<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</ds:Transform>"#
4275 );
4276 let max_transforms = transform.repeat(16);
4277 let max_manifest_reference = format!(
4278 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>"##
4279 );
4280 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4281 xml.replacen(
4282 r##"<ds:Reference URI="#target">"##,
4283 &format!(
4284 r##"<ds:Reference URI="#target"><ds:Transforms>{}</ds:Transforms>"##,
4285 max_transforms
4286 ),
4287 1,
4288 )
4289 .replacen(
4290 "</ds:SignedInfo>",
4291 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>"##,
4292 1,
4293 )
4294 .replacen(
4295 "</ds:Manifest>",
4296 &format!("{}</ds:Manifest>", max_manifest_reference.repeat(3)),
4297 1,
4298 )
4299 });
4300
4301 let error = VerifyContext::new()
4302 .key(&AcceptingKey)
4303 .process_manifests(true)
4304 .verify(&xml)
4305 .expect_err("SignedInfo and Manifest References must share one XPath parse budget");
4306
4307 assert!(
4308 matches!(
4309 &error,
4310 SignatureVerificationPipelineError::Policy(
4311 crate::policy::PolicyViolation::ResourceLimit {
4312 resource: "XPath expressions",
4313 ..
4314 }
4315 )
4316 ),
4317 "unexpected error: {error:?}"
4318 );
4319 }
4320
4321 #[test]
4322 fn verify_context_processes_manifest_when_signedinfo_references_object() {
4323 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4324 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
4325 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
4326 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
4327 });
4328
4329 let result = VerifyContext::new()
4330 .key(&AcceptingKey)
4331 .process_manifests(true)
4332 .verify(&xml)
4333 .expect("manifest references should be processed when SignedInfo references ds:Object");
4334 assert_eq!(
4335 result.manifest_references.len(),
4336 1,
4337 "signed ds:Object should enable processing of its direct-child ds:Manifest",
4338 );
4339 assert_eq!(
4340 result.manifest_references[0].reference_set,
4341 ReferenceSet::Manifest
4342 );
4343 assert_eq!(result.manifest_references[0].reference_index, 0);
4344 assert!(matches!(
4345 result.manifest_references[0].status,
4346 DsigStatus::Valid
4347 ));
4348 }
4349
4350 #[test]
4351 fn verify_context_skips_manifest_removed_by_enveloped_transform() {
4352 for target_object in [false, true] {
4356 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4357 let xml = xml.replacen(
4358 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
4359 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#"/>"#,
4360 1,
4361 );
4362 if target_object {
4363 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
4364 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
4365 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
4366 } else {
4367 xml
4368 }
4369 });
4370
4371 let result = VerifyContext::new()
4372 .key(&AcceptingKey)
4373 .process_manifests(true)
4374 .store_pre_digest(true)
4375 .verify(&xml)
4376 .expect("an emptied reference remains a valid core digest input");
4377
4378 assert!(matches!(result.status, DsigStatus::Valid));
4379 assert_eq!(
4380 result.signed_info_references[0].pre_digest_data.as_deref(),
4381 Some([].as_slice()),
4382 "target_object={target_object} must have empty transformed bytes",
4383 );
4384 assert!(
4385 result.manifest_references.is_empty(),
4386 "target_object={target_object} must not authenticate the Manifest",
4387 );
4388 }
4389 }
4390
4391 #[test]
4392 fn verify_context_ignores_manifest_excluded_from_signed_object() {
4393 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4397 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
4398 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
4399 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
4400 .replacen(
4401 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
4402 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#"/>"#,
4403 1,
4404 )
4405 });
4406
4407 let result = VerifyContext::new()
4408 .key(&AcceptingKey)
4409 .process_manifests(true)
4410 .verify(&xml)
4411 .expect("excluded Manifest content must be ignored, not parsed");
4412
4413 assert!(matches!(result.status, DsigStatus::Valid));
4414 assert!(
4415 result.manifest_references.is_empty(),
4416 "a transform-excluded Manifest is not authenticated by SignedInfo",
4417 );
4418 }
4419
4420 #[test]
4421 fn verify_context_skips_manifest_digest_work_when_signature_is_invalid() {
4422 let xml = signature_with_manifest_xml(false);
4423 let result = VerifyContext::new()
4424 .key(&RejectingKey)
4425 .process_manifests(true)
4426 .verify(&xml)
4427 .expect("invalid SignatureValue must short-circuit Manifest digest work");
4428 assert!(result.manifest_references.is_empty());
4429 assert!(matches!(
4430 result.status,
4431 DsigStatus::Invalid(FailureReason::SignatureMismatch)
4432 ));
4433 }
4434
4435 #[test]
4436 fn verify_context_manifest_digest_mismatch_is_non_fatal_with_accepting_key() {
4437 let xml = signature_with_manifest_xml(false);
4438 let result = VerifyContext::new()
4439 .key(&AcceptingKey)
4440 .process_manifests(true)
4441 .verify(&xml)
4442 .expect("manifest digest mismatches should be recorded while signature stays valid");
4443 assert_eq!(result.manifest_references.len(), 1);
4444 assert!(matches!(
4445 result.manifest_references[0].status,
4446 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
4447 ));
4448 assert!(matches!(result.status, DsigStatus::Valid));
4449 }
4450
4451 #[test]
4452 fn verify_context_skips_manifest_parsing_when_signedinfo_reference_fails() {
4453 let xml = signature_with_manifest_xml(true);
4456 let (signed_info_prefix, object_suffix) = xml
4457 .split_once("<ds:Object>")
4458 .expect("fixture should contain ds:Object");
4459 let open = "<ds:DigestValue>";
4460 let close = "</ds:DigestValue>";
4461 let digest_start = signed_info_prefix
4462 .find(open)
4463 .expect("SignedInfo should contain DigestValue");
4464 let digest_end = signed_info_prefix[digest_start + open.len()..]
4465 .find(close)
4466 .map(|offset| digest_start + open.len() + offset)
4467 .expect("SignedInfo DigestValue must be closed");
4468 let broken_signed_info_prefix = format!(
4469 "{}{}AAAAAAAAAAAAAAAAAAAAAAAAAAA={}{}",
4470 &signed_info_prefix[..digest_start],
4471 open,
4472 close,
4473 &signed_info_prefix[digest_end + close.len()..],
4474 );
4475 let broken_xml = format!("{broken_signed_info_prefix}<ds:Object>{object_suffix}");
4476 let result = VerifyContext::new()
4477 .key(&RejectingKey)
4478 .process_manifests(true)
4479 .verify(&broken_xml)
4480 .expect("SignedInfo digest failure should return without parsing Manifests");
4481 assert!(matches!(
4482 result.status,
4483 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
4484 ));
4485 assert!(
4486 result.manifest_references.is_empty(),
4487 "unauthenticated Manifest content must not be parsed",
4488 );
4489 }
4490
4491 #[test]
4492 fn verify_context_skips_manifest_policy_work_when_signature_is_invalid() {
4493 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4496 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
4497 });
4498 let result = VerifyContext::new()
4499 .key(&RejectingKey)
4500 .process_manifests(true)
4501 .verify(&broken_xml)
4502 .expect("invalid SignatureValue must short-circuit Manifest policy work");
4503 assert!(result.manifest_references.is_empty());
4504 assert!(matches!(
4505 result.status,
4506 DsigStatus::Invalid(FailureReason::SignatureMismatch)
4507 ));
4508 }
4509
4510 #[test]
4511 fn verify_context_records_manifest_policy_violations_with_accepting_key() {
4512 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4513 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
4514 });
4515 let result = VerifyContext::new()
4516 .key(&AcceptingKey)
4517 .process_manifests(true)
4518 .verify(&broken_xml)
4519 .expect("manifest policy violations should be recorded while signature stays valid");
4520 assert_eq!(result.manifest_references.len(), 1);
4521 assert!(matches!(
4522 result.manifest_references[0].status,
4523 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
4524 ));
4525 assert!(matches!(result.status, DsigStatus::Valid));
4526 }
4527
4528 #[test]
4529 fn verify_context_applies_digest_policy_to_manifest_references() {
4530 let policy = crate::policy::VerificationPolicy {
4533 manifest_processing: crate::policy::ManifestProcessing::Process,
4534 digest_algorithms: Some(HashSet::from([DigestAlgorithm::Sha1])),
4535 ..crate::policy::VerificationPolicy::default()
4536 };
4537 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
4538 let legacy = "http://www.w3.org/2000/09/xmldsig#sha1";
4539 let offset = xml
4540 .rfind(legacy)
4541 .expect("Manifest DigestMethod must be present");
4542 xml.replace_range(offset..offset + legacy.len(), DigestAlgorithm::Sha256.uri());
4543 let value_start = xml[offset..]
4544 .find("<ds:DigestValue>")
4545 .map(|relative| offset + relative + "<ds:DigestValue>".len())
4546 .expect("Manifest DigestValue must be present");
4547 let value_end = xml[value_start..]
4548 .find("</ds:DigestValue>")
4549 .map(|relative| value_start + relative)
4550 .expect("Manifest DigestValue must be closed");
4551 xml.replace_range(
4552 value_start..value_end,
4553 &base64::engine::general_purpose::STANDARD.encode([0_u8; 32]),
4554 );
4555 xml
4556 });
4557 let result = VerifyContext::new()
4558 .key(&AcceptingKey)
4559 .policy(policy)
4560 .verify(&xml)
4561 .expect("a disallowed Manifest digest is a per-reference result");
4562
4563 assert!(matches!(result.status, DsigStatus::Valid));
4564 assert!(matches!(
4565 result.manifest_references[0].status,
4566 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
4567 ));
4568 }
4569
4570 #[test]
4571 fn verify_context_applies_transform_count_policy_to_manifest_references() {
4572 let policy = crate::policy::VerificationPolicy {
4575 manifest_processing: crate::policy::ManifestProcessing::Process,
4576 resources: crate::policy::ResourcePolicy {
4577 max_transforms_per_reference: 1,
4578 ..crate::policy::ResourcePolicy::default()
4579 },
4580 ..crate::policy::VerificationPolicy::default()
4581 };
4582 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
4583 let manifest_start = xml
4584 .find("<ds:Manifest")
4585 .expect("fixture must contain a Manifest");
4586 let manifest = xml[manifest_start..].replacen(
4587 "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
4588 concat!(
4589 "<ds:Transforms>",
4590 "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
4591 "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
4592 "</ds:Transforms>",
4593 "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>"
4594 ),
4595 1,
4596 );
4597 xml.replace_range(manifest_start.., &manifest);
4598 xml
4599 });
4600 let result = VerifyContext::new()
4601 .key(&AcceptingKey)
4602 .policy(policy)
4603 .verify(&xml)
4604 .expect("Manifest transform policy is a per-reference result");
4605
4606 assert!(matches!(result.status, DsigStatus::Valid));
4607 assert!(matches!(
4608 result.manifest_references[0].status,
4609 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
4610 ));
4611 }
4612
4613 #[test]
4614 fn verify_context_skips_manifest_uri_work_when_signature_is_invalid() {
4615 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4618 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
4619 });
4620
4621 let result = VerifyContext::new()
4622 .key(&RejectingKey)
4623 .process_manifests(true)
4624 .verify(&broken_xml)
4625 .expect("invalid SignatureValue must short-circuit Manifest URI processing");
4626 assert!(result.manifest_references.is_empty());
4627 assert!(matches!(
4628 result.status,
4629 DsigStatus::Invalid(FailureReason::SignatureMismatch)
4630 ));
4631 }
4632
4633 #[test]
4634 fn verify_context_records_manifest_missing_uri_with_accepting_key() {
4635 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4636 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
4637 });
4638
4639 let result = VerifyContext::new()
4640 .key(&AcceptingKey)
4641 .process_manifests(true)
4642 .verify(&broken_xml)
4643 .expect("manifest missing URI should be recorded while signature stays valid");
4644 assert_eq!(result.manifest_references.len(), 1);
4645 assert_eq!(result.manifest_references[0].uri, "<omitted>");
4646 assert!(matches!(
4647 result.manifest_references[0].status,
4648 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
4649 ));
4650 assert!(matches!(result.status, DsigStatus::Valid));
4651 }
4652
4653 #[test]
4654 fn verify_context_ignores_nested_manifests_in_object() {
4655 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4658 xml.replacen(
4659 "<ds:Manifest ID=\"manifest\">",
4660 "<wrapper><ds:Manifest ID=\"manifest\">",
4661 1,
4662 )
4663 .replacen("</ds:Manifest>", "</ds:Manifest></wrapper>", 1)
4664 });
4665
4666 let result = VerifyContext::new()
4667 .key(&AcceptingKey)
4668 .process_manifests(true)
4669 .verify(&xml)
4670 .expect("nested Manifest nodes are ignored in strict mode");
4671 assert!(
4672 result.manifest_references.is_empty(),
4673 "only direct ds:Manifest children of ds:Object must be processed"
4674 );
4675 assert!(matches!(result.status, DsigStatus::Valid));
4676 }
4677
4678 #[test]
4679 fn verify_context_reports_manifest_reference_parse_errors_explicitly() {
4680 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4683 replace_fixture_manifest_digest(&xml, "!!!")
4684 });
4685
4686 let err = VerifyContext::new()
4687 .key(&AcceptingKey)
4688 .process_manifests(true)
4689 .verify(&broken_xml)
4690 .expect_err("invalid Manifest DigestValue must map to ParseManifestReference");
4691 assert!(matches!(
4692 err,
4693 SignatureVerificationPipelineError::ParseManifestReference(_)
4694 ));
4695 }
4696
4697 #[test]
4698 fn verify_context_reports_unsupported_manifest_transform_with_declared_digest() {
4699 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4702 let xml = xml.replacen(
4703 "<ds:Reference URI=\"#target\">",
4704 "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
4705 1,
4706 );
4707 let xml = xml.replacen(
4708 "</ds:Transforms>\n <ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
4709 "</ds:Transforms>\n <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>",
4710 1,
4711 );
4712 replace_fixture_manifest_digest(&xml, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
4713 });
4714 assert!(xml.contains("urn:unsupported"));
4715 assert!(xml.contains("http://www.w3.org/2001/04/xmlenc#sha256"));
4716
4717 let result = VerifyContext::new()
4718 .key(&AcceptingKey)
4719 .process_manifests(true)
4720 .verify(&xml)
4721 .expect("unsupported Manifest transform is a per-reference result");
4722 assert_eq!(result.status, DsigStatus::Valid);
4723 assert_eq!(result.manifest_references.len(), 1);
4724 assert_eq!(
4725 result.manifest_references[0].digest_algorithm,
4726 DigestAlgorithm::Sha256
4727 );
4728 assert!(matches!(
4729 result.manifest_references[0].status,
4730 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
4731 ));
4732
4733 let restricted = VerifyContext::new()
4734 .key(&AcceptingKey)
4735 .process_manifests(true)
4736 .allowed_transforms([
4737 DEFAULT_IMPLICIT_C14N_URI,
4738 "http://www.w3.org/2001/10/xml-exc-c14n#",
4739 ])
4740 .verify(&xml)
4741 .expect("a disallowed Manifest transform is a per-reference policy result");
4742 assert!(matches!(
4743 restricted.manifest_references[0].status,
4744 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
4745 ));
4746 }
4747
4748 #[test]
4749 fn manifest_reference_limit_counts_unsupported_entries() {
4750 let references = (0..=MAX_REFERENCES_PER_SIGNATURE)
4751 .map(|index| {
4752 format!(
4753 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>"##
4754 )
4755 })
4756 .collect::<String>();
4757 let xml = format!(
4758 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>"#
4759 );
4760 let document = Document::parse(&xml).unwrap();
4761 let signature = document.root_element();
4762 let object = signature.children().find(|node| node.is_element()).unwrap();
4763 let authenticated = HashSet::from([object.id()]);
4764 let mut processed = HashSet::new();
4765 let mut remaining = MAX_REFERENCES_PER_SIGNATURE;
4766 let mut next_index = 0;
4767
4768 let error = match parse_manifest_references(
4769 signature,
4770 &authenticated,
4771 &mut processed,
4772 &mut remaining,
4773 &mut next_index,
4774 &mut XPathSignatureParseBudget::default(),
4775 None,
4776 ) {
4777 Ok(_) => panic!("unsupported references must consume the same aggregate limit"),
4778 Err(error) => error,
4779 };
4780 assert!(matches!(
4781 error,
4782 SignatureVerificationPipelineError::InvalidStructure {
4783 reason: "signed Manifests exceed the per-signature Reference limit"
4784 }
4785 ));
4786 }
4787
4788 #[test]
4789 fn unsigned_manifest_remains_eligible_after_trust_expands() {
4790 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4794 let xml = format!(
4795 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>"##
4796 );
4797 let document = Document::parse(&xml).expect("nested Manifest fixture must parse");
4798 let signature = document.root_element();
4799 let mut objects = signature.children().filter(|node| node.is_element());
4800 let outer = objects.next().expect("outer Object");
4801 let inner = objects.next().expect("inner Object");
4802 let mut authenticated = HashSet::from([outer.id()]);
4803 let mut processed = HashSet::new();
4804 let mut remaining = 2;
4805 let mut next_index = 0;
4806 let mut xpath_budget = XPathSignatureParseBudget::default();
4807
4808 let first = parse_manifest_references(
4809 signature,
4810 &authenticated,
4811 &mut processed,
4812 &mut remaining,
4813 &mut next_index,
4814 &mut xpath_budget,
4815 None,
4816 )
4817 .expect("outer Manifest must be discovered");
4818 assert_eq!(first.references.len(), 1);
4819 assert_eq!(first.references[0].1.uri.as_deref(), Some("#inner"));
4820
4821 authenticated.insert(inner.id());
4822 let second = parse_manifest_references(
4823 signature,
4824 &authenticated,
4825 &mut processed,
4826 &mut remaining,
4827 &mut next_index,
4828 &mut xpath_budget,
4829 None,
4830 )
4831 .expect("newly authenticated sibling Manifest must remain eligible");
4832 assert_eq!(second.references.len(), 1);
4833 assert_eq!(second.references[0].1.uri.as_deref(), Some("#payload"));
4834 }
4835
4836 #[test]
4837 fn manifest_reference_limit_includes_signed_info_references() {
4838 let xml = signature_with_manifest_xml(true);
4841 let reference_start = xml
4842 .find(r##"<ds:Reference URI="#manifest">"##)
4843 .expect("fixture SignedInfo must reference the Manifest");
4844 let reference_end = xml[reference_start..]
4845 .find("</ds:Reference>")
4846 .map(|offset| reference_start + offset + "</ds:Reference>".len())
4847 .expect("fixture SignedInfo Reference must be closed");
4848 let repeated = xml[reference_start..reference_end].repeat(MAX_REFERENCES_PER_SIGNATURE);
4849 let xml = format!(
4850 "{}{repeated}{}",
4851 &xml[..reference_start],
4852 &xml[reference_end..]
4853 );
4854
4855 let error = VerifyContext::new()
4856 .key(&AcceptingKey)
4857 .process_manifests(true)
4858 .verify(&xml)
4859 .expect_err("one Manifest Reference must exceed the exhausted signature-wide limit");
4860
4861 assert!(matches!(
4862 error,
4863 SignatureVerificationPipelineError::InvalidStructure {
4864 reason: "signed Manifests exceed the per-signature Reference limit"
4865 }
4866 ));
4867 }
4868
4869 #[test]
4870 fn configured_reference_limit_is_shared_with_manifests() {
4871 let policy = crate::policy::VerificationPolicy {
4874 manifest_processing: crate::policy::ManifestProcessing::Process,
4875 resources: crate::policy::ResourcePolicy {
4876 max_references: 1,
4877 ..crate::policy::ResourcePolicy::default()
4878 },
4879 ..crate::policy::VerificationPolicy::default()
4880 };
4881
4882 let error = VerifyContext::new()
4883 .key(&AcceptingKey)
4884 .policy(policy)
4885 .verify(&signature_with_manifest_xml(true))
4886 .expect_err("Manifest must exceed the caller-selected aggregate limit");
4887 assert!(matches!(
4888 error,
4889 SignatureVerificationPipelineError::InvalidStructure {
4890 reason: "signed Manifests exceed the per-signature Reference limit"
4891 }
4892 ));
4893 }
4894
4895 #[test]
4896 fn retrieval_method_materializes_single_x509_data_subtree() {
4897 for uri in [
4898 "#target",
4899 "#xpointer(id('target'))",
4900 "#xpointer(id("target"))",
4901 ] {
4902 for target_xml in [
4903 r#"<ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>"#,
4904 r#"<holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>"#,
4905 ] {
4906 let xml = format!(
4907 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>"#
4908 );
4909 let document = Document::parse(&xml).unwrap();
4910 let key_info_node = document
4911 .descendants()
4912 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
4913 .unwrap();
4914 let mut key_info = parse_key_info(key_info_node).unwrap();
4915 let resolver = UriReferenceResolver::new(&document);
4916
4917 materialize_retrieval_methods(
4918 &mut key_info,
4919 &resolver,
4920 UriTypeSet::SAME_DOCUMENT,
4921 None,
4922 crate::provider::default_provider(),
4923 )
4924 .expect("XPath filter must produce one X509Data-rooted node-set");
4925 assert!(matches!(
4926 key_info.sources.as_slice(),
4927 [super::super::parse::KeyInfoSource::X509Data(info)]
4928 if info.subject_names == ["CN=leaf"]
4929 ));
4930 }
4931 }
4932 }
4933
4934 fn retrieval_method_xpath_signature() -> String {
4935 format!(
4936 r##"<root xmlns:ds="{XMLDSIG_NS}">
4937 <payload Id="payload">ok</payload>
4938 <ds:Signature>
4939 <ds:SignedInfo>
4940 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4941 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4942 <ds:Reference URI="#payload">
4943 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4944 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
4945 </ds:Reference>
4946 </ds:SignedInfo>
4947 <ds:SignatureValue>AQ==</ds:SignatureValue>
4948 <ds:KeyInfo>
4949 <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
4950 <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
4951 <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
4952 </ds:Transform></ds:Transforms>
4953 </ds:RetrievalMethod>
4954 </ds:KeyInfo>
4955 </ds:Signature>
4956 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
4957 </root>"##
4958 )
4959 }
4960
4961 #[test]
4962 fn retrieval_method_xpath_uses_signature_expression_budget() {
4963 let mut policy = crate::policy::VerificationPolicy::default();
4966 policy.resources.max_xpath_expressions = 0;
4967
4968 let error = VerifyContext::new()
4969 .policy(policy)
4970 .verify(&retrieval_method_xpath_signature())
4971 .expect_err("RetrievalMethod XPath must consume the signature parse budget");
4972
4973 assert!(
4974 matches!(
4975 &error,
4976 SignatureVerificationPipelineError::Policy(
4977 crate::policy::PolicyViolation::ResourceLimit {
4978 resource: "XPath expressions",
4979 maximum: 0,
4980 actual: 1,
4981 }
4982 )
4983 ),
4984 "unexpected error: {error:?}"
4985 );
4986 }
4987
4988 #[test]
4989 fn retrieval_method_xpath_obeys_expression_byte_limit() {
4990 let expression = "ancestor-or-self::ds:X509Data";
4993 let padded_expression = format!(" {expression} ");
4994 let xml = retrieval_method_xpath_signature().replace(expression, &padded_expression);
4995 let mut policy = crate::policy::VerificationPolicy::default();
4996 policy.resources.max_xpath_expression_bytes = expression.len();
4997
4998 let error = VerifyContext::new()
4999 .policy(policy)
5000 .verify(&xml)
5001 .expect_err("RetrievalMethod XPath must obey the expression byte limit");
5002
5003 assert!(
5004 matches!(
5005 &error,
5006 SignatureVerificationPipelineError::Policy(
5007 crate::policy::PolicyViolation::ResourceLimit {
5008 resource: "XPath expression bytes",
5009 maximum,
5010 actual,
5011 }
5012 ) if *maximum == expression.len() && *actual == padded_expression.len()
5013 ),
5014 "unexpected error: {error:?}"
5015 );
5016 }
5017
5018 #[test]
5019 fn retrieval_method_xpath_obeys_expression_complexity_limit() {
5020 let mut policy = crate::policy::VerificationPolicy::default();
5023 policy.resources.max_xpath_expression_complexity = 0;
5024
5025 let error = VerifyContext::new()
5026 .policy(policy)
5027 .verify(&retrieval_method_xpath_signature())
5028 .expect_err("RetrievalMethod XPath must obey the complexity limit");
5029
5030 assert!(
5031 matches!(
5032 &error,
5033 SignatureVerificationPipelineError::Policy(
5034 crate::policy::PolicyViolation::ResourceLimit {
5035 resource: "XPath expression complexity",
5036 maximum: 0,
5037 actual,
5038 }
5039 ) if *actual > 0
5040 ),
5041 "unexpected error: {error:?}"
5042 );
5043 }
5044
5045 #[test]
5046 fn retrieval_method_xpath_uses_node_filter_work_budget() {
5047 let mut policy = crate::policy::VerificationPolicy::default();
5050 policy.resources.max_node_set_filter_work = 4;
5051 let xml = retrieval_method_xpath_signature().replace(
5052 "<holder Id=\"target\">",
5053 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
5054 );
5055
5056 let error = VerifyContext::new()
5057 .policy(policy)
5058 .verify(&xml)
5059 .expect_err("RetrievalMethod XPath must consume node-filter work");
5060
5061 assert!(
5062 matches!(
5063 &error,
5064 SignatureVerificationPipelineError::Policy(
5065 crate::policy::PolicyViolation::ResourceLimit {
5066 resource: crate::policy::resource_name::NODE_SET_FILTER_WORK,
5067 maximum: 4,
5068 actual,
5069 }
5070 ) if *actual > 4
5071 ),
5072 "unexpected error: {error:?}"
5073 );
5074 }
5075
5076 #[test]
5077 fn retrieval_method_xpath_charges_every_context_to_evaluation_work() {
5078 let mut policy = crate::policy::VerificationPolicy::default();
5081 policy.resources.max_xpath_evaluation_work = 4;
5082 let xml = retrieval_method_xpath_signature().replace(
5083 "<holder Id=\"target\">",
5084 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
5085 );
5086
5087 let error = VerifyContext::new()
5088 .policy(policy)
5089 .verify(&xml)
5090 .expect_err("RetrievalMethod XPath must charge every evaluation context");
5091
5092 assert!(matches!(
5093 error,
5094 SignatureVerificationPipelineError::Policy(
5095 crate::policy::PolicyViolation::ResourceLimit {
5096 resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
5097 maximum: 4,
5098 actual,
5099 }
5100 ) if actual > 4
5101 ));
5102 }
5103
5104 #[test]
5105 fn retrieval_method_xpath_obeys_namespace_binding_limit() {
5106 let mut policy = crate::policy::VerificationPolicy::default();
5109 policy.resources.max_xpath_namespace_bindings = 0;
5110
5111 let error = VerifyContext::new()
5112 .policy(policy)
5113 .verify(&retrieval_method_xpath_signature())
5114 .expect_err("RetrievalMethod XPath namespaces must obey the binding limit");
5115
5116 assert!(matches!(
5117 error,
5118 SignatureVerificationPipelineError::Policy(
5119 crate::policy::PolicyViolation::ResourceLimit {
5120 resource: crate::policy::resource_name::XPATH_NAMESPACE_BINDINGS,
5121 maximum: 0,
5122 actual,
5123 }
5124 ) if actual > 0
5125 ));
5126 }
5127
5128 #[test]
5129 fn retrieval_method_xpath_obeys_namespace_byte_limit() {
5130 let mut policy = crate::policy::VerificationPolicy::default();
5133 policy.resources.max_xpath_namespace_bytes = 0;
5134
5135 let error = VerifyContext::new()
5136 .policy(policy)
5137 .verify(&retrieval_method_xpath_signature())
5138 .expect_err("RetrievalMethod XPath namespaces must obey the byte limit");
5139
5140 assert!(matches!(
5141 error,
5142 SignatureVerificationPipelineError::Policy(
5143 crate::policy::PolicyViolation::ResourceLimit {
5144 resource: crate::policy::resource_name::XPATH_NAMESPACE_BYTES,
5145 maximum: 0,
5146 actual,
5147 }
5148 ) if actual > 0
5149 ));
5150 }
5151
5152 #[test]
5153 fn retrieval_method_xpath_obeys_context_evaluation_limit() {
5154 let mut policy = crate::policy::VerificationPolicy::default();
5158 policy.resources.max_xpath_context_evaluations = 4;
5159 let xml = retrieval_method_xpath_signature().replace(
5160 "<holder Id=\"target\">",
5161 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
5162 );
5163
5164 let error = VerifyContext::new()
5165 .policy(policy)
5166 .verify(&xml)
5167 .expect_err("RetrievalMethod XPath contexts must obey the evaluation limit");
5168
5169 assert!(matches!(
5170 error,
5171 SignatureVerificationPipelineError::Policy(
5172 crate::policy::PolicyViolation::ResourceLimit {
5173 resource: crate::policy::resource_name::XPATH_CONTEXT_EVALUATIONS,
5174 maximum: 4,
5175 actual,
5176 }
5177 ) if actual > 4
5178 ));
5179 }
5180
5181 #[test]
5182 fn retrieval_method_materializes_direct_untransformed_x509_data() {
5183 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5186 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
5187 <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
5188 </root>"##;
5189 let document = Document::parse(xml).unwrap();
5190 let key_info_node = document
5191 .descendants()
5192 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5193 .unwrap();
5194 let mut key_info = parse_key_info(key_info_node).unwrap();
5195
5196 materialize_retrieval_methods(
5197 &mut key_info,
5198 &UriReferenceResolver::new(&document),
5199 UriTypeSet::SAME_DOCUMENT,
5200 None,
5201 crate::provider::default_provider(),
5202 )
5203 .expect("a direct X509Data target needs no transform");
5204 assert!(matches!(
5205 key_info.sources.as_slice(),
5206 [super::super::parse::KeyInfoSource::X509Data(info)]
5207 if info.subject_names == ["CN=leaf"]
5208 ));
5209 }
5210
5211 #[test]
5212 fn retrieval_method_respects_configured_same_document_id_semantics() {
5213 fn materialize(
5216 uri: &str,
5217 id: &str,
5218 semantics: crate::policy::SameDocumentIdSemantics,
5219 ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
5220 let xml = format!(
5221 r#"<root xmlns:ds="{XMLDSIG_NS}">
5222 <ds:KeyInfo><ds:RetrievalMethod URI="{uri}" Type="{XMLDSIG_NS}X509Data"/></ds:KeyInfo>
5223 <ds:X509Data Id="{id}"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
5224 </root>"#
5225 );
5226 let document = Document::parse(&xml).unwrap();
5227 let key_info_node = document
5228 .descendants()
5229 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5230 .unwrap();
5231 let mut key_info = parse_key_info(key_info_node).unwrap();
5232 let resolver =
5233 UriReferenceResolver::new(&document).with_same_document_id_semantics(semantics);
5234
5235 materialize_retrieval_methods(
5236 &mut key_info,
5237 &resolver,
5238 UriTypeSet::SAME_DOCUMENT,
5239 None,
5240 crate::provider::default_provider(),
5241 )
5242 }
5243
5244 assert!(
5245 materialize(
5246 "#12345",
5247 "12345",
5248 crate::policy::SameDocumentIdSemantics::Specification,
5249 )
5250 .is_err(),
5251 "the standards mode must reject a non-NCName bare fragment"
5252 );
5253 assert!(
5254 materialize(
5255 "#visa'3d",
5256 "visa'3d",
5257 crate::policy::SameDocumentIdSemantics::XmlSecBarename,
5258 )
5259 .is_err(),
5260 "the donor barename wrapper cannot represent an apostrophe"
5261 );
5262 assert!(
5263 materialize(
5264 "#visa'3d",
5265 "visa'3d",
5266 crate::policy::SameDocumentIdSemantics::XmlSecVisa3d,
5267 )
5268 .is_ok(),
5269 "Visa3D mode resolves the registered ID without an XPointer literal"
5270 );
5271 }
5272
5273 #[test]
5274 fn raw_x509_retrieval_method_uses_inherited_xml_base() {
5275 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
5278 let xml = format!(
5279 r#"<root xml:base="https://example.test/keys/nested/" xmlns:ds="{XMLDSIG_NS}">
5280 <ds:KeyInfo><ds:RetrievalMethod URI="../signer.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>
5281 </root>"#
5282 );
5283 let document = Document::parse(&xml).unwrap();
5284 let key_info_node = document
5285 .descendants()
5286 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5287 .unwrap();
5288 let mut key_info = parse_key_info(key_info_node).unwrap();
5289 let certificate = include_bytes!(
5290 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
5291 )
5292 .to_vec();
5293 let resources = HashMap::from([(
5294 "https://example.test/keys/signer.der".to_string(),
5295 certificate,
5296 )]);
5297 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5298
5299 materialize_retrieval_methods(
5300 &mut key_info,
5301 &resolver,
5302 UriTypeSet::ALL,
5303 None,
5304 crate::provider::default_provider(),
5305 )
5306 .expect("RetrievalMethod should resolve against inherited xml:base");
5307
5308 assert!(matches!(
5309 key_info.sources.as_slice(),
5310 [super::super::parse::KeyInfoSource::X509Data(info)]
5311 if info.certificates.len() == 1
5312 ));
5313 }
5314
5315 #[test]
5316 fn retrieval_method_requires_xpath_for_x509_data_below_uri_root() {
5317 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5320 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
5321 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
5322 </root>"##;
5323 let document = Document::parse(xml).unwrap();
5324 let key_info_node = document
5325 .descendants()
5326 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5327 .unwrap();
5328 let mut key_info = parse_key_info(key_info_node).unwrap();
5329
5330 let error = materialize_retrieval_methods(
5331 &mut key_info,
5332 &UriReferenceResolver::new(&document),
5333 UriTypeSet::SAME_DOCUMENT,
5334 None,
5335 crate::provider::default_provider(),
5336 )
5337 .expect_err("a wrapper target requires an explicit selection transform");
5338 assert!(matches!(
5339 error,
5340 SignatureVerificationPipelineError::InvalidStructure {
5341 reason: "untransformed X509Data RetrievalMethod must target X509Data directly"
5342 }
5343 ));
5344 }
5345
5346 #[test]
5347 fn retrieval_method_rejects_target_inside_external_x509_data_ancestor() {
5348 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5351 <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>
5352 <ds:X509Data><ds:X509SubjectName Id="target">CN=leaf</ds:X509SubjectName></ds:X509Data>
5353 </root>"##;
5354 let document = Document::parse(xml).unwrap();
5355 let key_info_node = document
5356 .descendants()
5357 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5358 .unwrap();
5359 let mut key_info = parse_key_info(key_info_node).unwrap();
5360
5361 let error = materialize_retrieval_methods(
5362 &mut key_info,
5363 &UriReferenceResolver::new(&document),
5364 UriTypeSet::SAME_DOCUMENT,
5365 None,
5366 crate::provider::default_provider(),
5367 )
5368 .expect_err("filter output without an X509Data root must be rejected");
5369 assert!(matches!(
5370 error,
5371 SignatureVerificationPipelineError::InvalidStructure {
5372 reason: "X509Data RetrievalMethod selected no X509Data element"
5373 }
5374 ));
5375 }
5376
5377 #[test]
5378 fn retrieval_method_rejects_ambiguous_x509_data_relation() {
5379 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5381 <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>
5382 <holder Id="target"><ds:X509Data/><ds:X509Data/></holder>
5383 </root>"##;
5384 let document = Document::parse(xml).unwrap();
5385 let key_info_node = document
5386 .descendants()
5387 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5388 .unwrap();
5389 let mut key_info = parse_key_info(key_info_node).unwrap();
5390
5391 let error = materialize_retrieval_methods(
5392 &mut key_info,
5393 &UriReferenceResolver::new(&document),
5394 UriTypeSet::SAME_DOCUMENT,
5395 None,
5396 crate::provider::default_provider(),
5397 )
5398 .expect_err("multiple transformed X509Data roots must be rejected");
5399 assert!(matches!(
5400 error,
5401 SignatureVerificationPipelineError::InvalidStructure {
5402 reason: "X509Data RetrievalMethod selected multiple X509Data elements"
5403 }
5404 ));
5405 }
5406
5407 #[test]
5408 fn retrieval_method_materialization_preserves_key_info_order() {
5409 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5412 <ds:KeyInfo>
5413 <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>
5414 <ds:KeyName>fallback</ds:KeyName>
5415 </ds:KeyInfo>
5416 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
5417 </root>"##;
5418 let document = Document::parse(xml).unwrap();
5419 let key_info_node = document
5420 .descendants()
5421 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5422 .unwrap();
5423 let mut key_info = parse_key_info(key_info_node).unwrap();
5424
5425 materialize_retrieval_methods(
5426 &mut key_info,
5427 &UriReferenceResolver::new(&document),
5428 UriTypeSet::SAME_DOCUMENT,
5429 None,
5430 crate::provider::default_provider(),
5431 )
5432 .unwrap();
5433 assert!(matches!(
5434 key_info.sources.as_slice(),
5435 [
5436 super::super::parse::KeyInfoSource::X509Data(_),
5437 super::super::parse::KeyInfoSource::KeyName(name)
5438 ] if name == "fallback"
5439 ));
5440 }
5441
5442 #[test]
5443 fn retrieval_method_materialization_bounds_repeated_sources() {
5444 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
5447 let certificate = include_bytes!(
5448 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
5449 )
5450 .to_vec();
5451 let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
5452 let mut key_info = KeyInfo {
5453 sources: (0..=64)
5454 .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
5455 uri: "urn:certificate".into(),
5456 resource_type: Some(RAW_X509_TYPE.into()),
5457 transforms: RetrievalMethodTransforms::None,
5458 })
5459 .collect(),
5460 };
5461 let document = Document::parse("<root/>").unwrap();
5462 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5463
5464 let error = materialize_retrieval_methods(
5465 &mut key_info,
5466 &resolver,
5467 UriTypeSet::ALL,
5468 None,
5469 crate::provider::default_provider(),
5470 )
5471 .expect_err("retrieval count must be bounded before materialization");
5472 assert!(matches!(
5473 error,
5474 SignatureVerificationPipelineError::InvalidStructure {
5475 reason: "KeyInfo contains too many RetrievalMethod elements"
5476 }
5477 ));
5478 }
5479
5480 #[test]
5481 fn key_info_reference_materializes_an_allowed_empty_uri() {
5482 let document = XmlDocument::parse(format!(
5486 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>root-key</ds:KeyName></ds:KeyInfo>"#
5487 ))
5488 .unwrap();
5489 let mut key_info = KeyInfo {
5490 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
5491 uri: String::new(),
5492 }],
5493 };
5494 let mut policy = crate::policy::VerificationPolicy::default();
5495 policy.key_sources.key_info_reference = true;
5496 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
5497 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
5498 let mut budgets = RetrievalMaterializationBudgets {
5499 xpath_parse: &mut xpath_parse_budget,
5500 execution: &execution_budget,
5501 resources: &policy.resources,
5502 xml_backend: crate::XmlBackend::default(),
5503 };
5504 let mut materialization = KeyInfoMaterializationState::default();
5505
5506 document
5507 .with_view(|view| {
5508 materialize_key_info_references_with_budgets(
5509 &mut key_info,
5510 &UriReferenceResolver::with_document_view(view, &[]),
5511 &policy,
5512 crate::provider::default_provider(),
5513 &mut budgets,
5514 &mut materialization,
5515 )?;
5516 Ok::<_, SignatureVerificationPipelineError>(())
5517 })
5518 .unwrap();
5519
5520 assert!(matches!(
5521 key_info.sources.as_slice(),
5522 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "root-key"
5523 ));
5524 }
5525
5526 #[test]
5527 fn empty_key_info_reference_participates_in_cycle_detection() {
5528 let document = XmlDocument::parse(format!(
5532 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#"><dsig11:KeyInfoReference URI=""/></ds:KeyInfo>"#
5533 ))
5534 .unwrap();
5535 let mut key_info = KeyInfo {
5536 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
5537 uri: String::new(),
5538 }],
5539 };
5540 let mut policy = crate::policy::VerificationPolicy::default();
5541 policy.key_sources.key_info_reference = true;
5542 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
5543 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
5544 let mut budgets = RetrievalMaterializationBudgets {
5545 xpath_parse: &mut xpath_parse_budget,
5546 execution: &execution_budget,
5547 resources: &policy.resources,
5548 xml_backend: crate::XmlBackend::default(),
5549 };
5550 let mut materialization = KeyInfoMaterializationState::default();
5551
5552 let error = document
5553 .with_view(|view| {
5554 materialize_key_info_references_with_budgets(
5555 &mut key_info,
5556 &UriReferenceResolver::with_document_view(view, &[]),
5557 &policy,
5558 crate::provider::default_provider(),
5559 &mut budgets,
5560 &mut materialization,
5561 )
5562 })
5563 .expect_err("empty-URI self-reference must be rejected as a cycle");
5564
5565 assert!(matches!(
5566 error,
5567 SignatureVerificationPipelineError::InvalidStructure {
5568 reason: "KeyInfoReference cycle detected"
5569 }
5570 ));
5571 }
5572
5573 #[test]
5574 fn key_info_materialization_shares_candidate_work_across_source_kinds() {
5575 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
5578 let document = XmlDocument::parse(format!(
5579 r##"<root xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#"><ds:KeyInfo ID="target"><ds:RetrievalMethod URI="signer.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo></root>"##
5580 ))
5581 .unwrap();
5582 let certificate = include_bytes!(
5583 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
5584 )
5585 .to_vec();
5586 let resources = HashMap::from([("signer.der".to_owned(), certificate)]);
5587 let mut key_info = KeyInfo {
5588 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
5589 uri: "#target".into(),
5590 }],
5591 };
5592 let mut policy = crate::policy::VerificationPolicy::default();
5593 policy.key_sources.key_info_reference = true;
5594 policy.uris.retrieval_methods = UriTypeSet::ALL;
5595 policy.resources.max_key_candidates = 2;
5596 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
5597 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
5598 let mut budgets = RetrievalMaterializationBudgets {
5599 xpath_parse: &mut xpath_parse_budget,
5600 execution: &execution_budget,
5601 resources: &policy.resources,
5602 xml_backend: crate::XmlBackend::default(),
5603 };
5604 let mut materialization = KeyInfoMaterializationState::default();
5605
5606 let error = document
5607 .with_view(|view| {
5608 let resolver = UriReferenceResolver::with_document_view(view, &[])
5609 .with_external_resources(&resources);
5610 let mut outcome = materialize_key_info_references_with_budgets(
5611 &mut key_info,
5612 &resolver,
5613 &policy,
5614 crate::provider::default_provider(),
5615 &mut budgets,
5616 &mut materialization,
5617 )?;
5618 outcome.merge(materialize_retrieval_methods_with_budgets(
5619 &mut key_info,
5620 &resolver,
5621 policy.uris.retrieval_methods,
5622 policy.transforms.allowed_algorithms.as_ref(),
5623 crate::provider::default_provider(),
5624 &mut budgets,
5625 &mut materialization.candidate_work,
5626 )?);
5627 Ok::<_, DsigError>(outcome)
5628 })
5629 .expect_err("all materializers must share the candidate-work limit");
5630
5631 assert!(matches!(
5632 error,
5633 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
5634 resource: crate::policy::resource_name::KEY_CANDIDATES,
5635 maximum: 2,
5636 actual: 3,
5637 })
5638 ));
5639 }
5640
5641 #[test]
5642 fn public_key_info_materialization_binds_external_resource_policy() {
5643 let document = Document::parse("<root/>").unwrap();
5646 let external = format!(
5647 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
5648 );
5649 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
5650 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5651 let mut key_info = KeyInfo {
5652 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
5653 uri: "key.xml".into(),
5654 }],
5655 };
5656 let mut policy = crate::policy::VerificationPolicy::default();
5657 policy.key_sources.key_info_reference = true;
5658 policy.uris.key_info_references = UriTypeSet::ALL;
5659 policy.resources.max_external_resource_bytes = 0;
5660
5661 let error = materialize_verification_key_info_references(
5662 &mut key_info,
5663 resolver,
5664 &policy,
5665 crate::provider::default_provider(),
5666 crate::XmlBackend::default(),
5667 )
5668 .expect_err("policy must reject non-empty external KeyInfo bytes");
5669
5670 assert!(matches!(
5671 error,
5672 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
5673 resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
5674 maximum: 0,
5675 actual,
5676 }) if actual == resources["key.xml"].len()
5677 ));
5678 }
5679
5680 #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
5681 #[test]
5682 fn public_key_info_materialization_uses_the_selected_backend() {
5683 let external = format!(
5686 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
5687 );
5688 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
5689 let document = Document::parse("<root/>").unwrap();
5690
5691 for backend in [crate::XmlBackend::Xmloxide, crate::XmlBackend::Roxmltree] {
5692 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5693 let mut key_info = KeyInfo {
5694 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
5695 uri: "key.xml".into(),
5696 }],
5697 };
5698 let mut policy = crate::policy::VerificationPolicy::default();
5699 policy.key_sources.key_info_reference = true;
5700 policy.uris.key_info_references = UriTypeSet::ALL;
5701
5702 materialize_verification_key_info_references(
5703 &mut key_info,
5704 resolver,
5705 &policy,
5706 crate::provider::default_provider(),
5707 backend,
5708 )
5709 .unwrap_or_else(|error| panic!("{backend:?} materialization failed: {error}"));
5710
5711 assert!(matches!(
5712 key_info.sources.as_slice(),
5713 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "external"
5714 ));
5715 }
5716 }
5717
5718 #[test]
5719 fn public_key_info_materialization_defers_nested_retrieval_failure() {
5720 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
5724 let external = format!(
5725 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>usable</ds:KeyName><ds:RetrievalMethod URI="missing.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>"#
5726 );
5727 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
5728 let document = Document::parse("<root/>").unwrap();
5729
5730 for operation in ["signing", "verification"] {
5731 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5732 let mut key_info = KeyInfo {
5733 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
5734 uri: "key.xml".into(),
5735 }],
5736 };
5737 match operation {
5738 "signing" => {
5739 let mut policy = crate::policy::SigningPolicy::default();
5740 policy.uris.key_info_references = UriTypeSet::ALL;
5741 policy.uris.retrieval_methods = UriTypeSet::ALL;
5742 materialize_signing_key_info_references(
5743 &mut key_info,
5744 resolver,
5745 &policy,
5746 crate::provider::default_provider(),
5747 crate::XmlBackend::default(),
5748 )
5749 }
5750 "verification" => {
5751 let mut policy = crate::policy::VerificationPolicy::default();
5752 policy.key_sources.key_info_reference = true;
5753 policy.uris.key_info_references = UriTypeSet::ALL;
5754 policy.uris.retrieval_methods = UriTypeSet::ALL;
5755 materialize_verification_key_info_references(
5756 &mut key_info,
5757 resolver,
5758 &policy,
5759 crate::provider::default_provider(),
5760 crate::XmlBackend::default(),
5761 )
5762 }
5763 _ => unreachable!(),
5764 }
5765 .unwrap_or_else(|error| panic!("{operation} materialization failed: {error}"));
5766
5767 assert!(matches!(
5768 key_info.sources.as_slice(),
5769 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "usable"
5770 ));
5771 }
5772 }
5773
5774 #[test]
5775 fn public_key_info_materialization_binds_xpath_policy() {
5776 let external = format!(
5779 r##"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}">
5780 <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
5781 <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
5782 <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
5783 </ds:Transform></ds:Transforms>
5784 </ds:RetrievalMethod>
5785 <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
5786 </ds:KeyInfo>"##
5787 );
5788 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
5789 let document = Document::parse("<root/>").unwrap();
5790
5791 for operation in ["signing", "verification"] {
5792 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5793 let mut key_info = KeyInfo {
5794 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
5795 uri: "key.xml".into(),
5796 }],
5797 };
5798 let error = match operation {
5799 "signing" => {
5800 let mut policy = crate::policy::SigningPolicy::default();
5801 policy.uris.key_info_references = UriTypeSet::ALL;
5802 policy.resources.max_xpath_expressions = 0;
5803 materialize_signing_key_info_references(
5804 &mut key_info,
5805 resolver,
5806 &policy,
5807 crate::provider::default_provider(),
5808 crate::XmlBackend::default(),
5809 )
5810 }
5811 "verification" => {
5812 let mut policy = crate::policy::VerificationPolicy::default();
5813 policy.key_sources.key_info_reference = true;
5814 policy.uris.key_info_references = UriTypeSet::ALL;
5815 policy.resources.max_xpath_expressions = 0;
5816 materialize_verification_key_info_references(
5817 &mut key_info,
5818 resolver,
5819 &policy,
5820 crate::provider::default_provider(),
5821 crate::XmlBackend::default(),
5822 )
5823 }
5824 _ => unreachable!(),
5825 }
5826 .expect_err("the public helper must enforce the supplied XPath expression limit");
5827
5828 assert!(
5829 matches!(
5830 error,
5831 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
5832 resource: crate::policy::resource_name::XPATH_EXPRESSIONS,
5833 maximum: 0,
5834 actual: 1,
5835 })
5836 ),
5837 "unexpected {operation} error: {error:?}"
5838 );
5839 }
5840 }
5841
5842 fn materialize_external_key_info_chain(
5843 terminal_reference: &str,
5844 ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
5845 let a = format!(
5846 r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
5847 <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
5848 <ds:KeyInfo ID="next"><dsig11:KeyInfoReference URI="b.xml#root"/></ds:KeyInfo>
5849 </doc>"##
5850 );
5851 let b = format!(
5852 r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
5853 <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
5854 <ds:KeyInfo ID="next">{terminal_reference}</ds:KeyInfo>
5855 </doc>"##
5856 );
5857 let resources = HashMap::from([
5858 ("a.xml".to_owned(), a.into_bytes()),
5859 ("b.xml".to_owned(), b.into_bytes()),
5860 ]);
5861 let document = XmlDocument::parse("<root/>").unwrap();
5862 let mut key_info = KeyInfo {
5863 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
5864 uri: "a.xml#root".into(),
5865 }],
5866 };
5867 let mut policy = crate::policy::VerificationPolicy::default();
5868 policy.key_sources.key_info_reference = true;
5869 policy.uris.key_info_references = UriTypeSet::ALL;
5870 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
5871 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
5872 let mut budgets = RetrievalMaterializationBudgets {
5873 xpath_parse: &mut xpath_parse_budget,
5874 execution: &execution_budget,
5875 resources: &policy.resources,
5876 xml_backend: crate::XmlBackend::default(),
5877 };
5878 let mut materialization = KeyInfoMaterializationState::default();
5879
5880 document.with_view(|view| {
5881 let resolver = UriReferenceResolver::with_document_view(view, &[])
5882 .with_external_resources(&resources);
5883 materialize_key_info_references_with_budgets(
5884 &mut key_info,
5885 &resolver,
5886 &policy,
5887 crate::provider::default_provider(),
5888 &mut budgets,
5889 &mut materialization,
5890 )?;
5891 Ok::<_, SignatureVerificationPipelineError>(())
5892 })?;
5893 Ok(key_info)
5894 }
5895
5896 fn materialize_external_key_info_bytes(
5897 encoded: Vec<u8>,
5898 ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
5899 let resources = HashMap::from([("key.xml".to_owned(), encoded)]);
5900 let document = XmlDocument::parse("<root/>").unwrap();
5901 let mut key_info = KeyInfo {
5902 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
5903 uri: "key.xml".into(),
5904 }],
5905 };
5906 let mut policy = crate::policy::VerificationPolicy::default();
5907 policy.key_sources.key_info_reference = true;
5908 policy.uris.key_info_references = UriTypeSet::ALL;
5909 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
5910 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
5911 let mut budgets = RetrievalMaterializationBudgets {
5912 xpath_parse: &mut xpath_parse_budget,
5913 execution: &execution_budget,
5914 resources: &policy.resources,
5915 xml_backend: crate::XmlBackend::default(),
5916 };
5917 let mut materialization = KeyInfoMaterializationState::default();
5918
5919 document.with_view(|view| {
5920 let resolver = UriReferenceResolver::with_document_view(view, &[])
5921 .with_external_resources(&resources);
5922 materialize_key_info_references_with_budgets(
5923 &mut key_info,
5924 &resolver,
5925 &policy,
5926 crate::provider::default_provider(),
5927 &mut budgets,
5928 &mut materialization,
5929 )?;
5930 Ok::<_, SignatureVerificationPipelineError>(())
5931 })?;
5932 Ok(key_info)
5933 }
5934
5935 #[test]
5936 fn external_key_info_reference_decodes_utf16_xml_octets() {
5937 let xml = format!(
5940 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>utf16-key</ds:KeyName></ds:KeyInfo>"#
5941 );
5942 let mut encoded = vec![0xff, 0xfe];
5943 encoded.extend(xml.encode_utf16().flat_map(u16::to_le_bytes));
5944 let key_info = materialize_external_key_info_bytes(encoded)
5945 .expect("UTF-16 external KeyInfo must materialize");
5946 assert!(matches!(
5947 key_info.sources.as_slice(),
5948 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "utf16-key"
5949 ));
5950 }
5951
5952 #[test]
5953 fn external_key_info_reference_rejects_malformed_xml_encoding() {
5954 let error = materialize_external_key_info_bytes(vec![0xff, 0xfe, b'<'])
5957 .expect_err("truncated UTF-16 must be rejected during octet decoding");
5958
5959 assert!(matches!(
5960 error,
5961 SignatureVerificationPipelineError::InvalidStructure {
5962 reason: "KeyInfoReference external resource has an invalid XML encoding"
5963 }
5964 ));
5965 }
5966
5967 #[test]
5968 fn key_info_reference_cycle_identity_includes_the_owning_resource() {
5969 let key_info = materialize_external_key_info_chain("<ds:KeyName>terminal</ds:KeyName>")
5972 .expect("cross-document duplicate fragments must materialize");
5973 assert!(matches!(
5974 key_info.sources.as_slice(),
5975 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "terminal"
5976 ));
5977 }
5978
5979 #[test]
5980 fn key_info_reference_cycle_identity_survives_external_reparse() {
5981 let error =
5984 materialize_external_key_info_chain("<dsig11:KeyInfoReference URI=\"a.xml#root\"/>")
5985 .expect_err("a resource cycle must fail before exhausting depth");
5986 assert!(matches!(
5987 error,
5988 SignatureVerificationPipelineError::InvalidStructure {
5989 reason: "KeyInfoReference cycle detected"
5990 }
5991 ));
5992 }
5993
5994 #[test]
5995 fn retrieval_method_materialization_deduplicates_within_count_limit() {
5996 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
5999 let certificate = include_bytes!(
6000 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
6001 )
6002 .to_vec();
6003 let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
6004 let mut key_info = KeyInfo {
6005 sources: (0..MAX_RETRIEVAL_METHOD_COUNT)
6006 .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
6007 uri: "urn:certificate".into(),
6008 resource_type: Some(RAW_X509_TYPE.into()),
6009 transforms: RetrievalMethodTransforms::None,
6010 })
6011 .collect(),
6012 };
6013 let document = Document::parse("<root/>").unwrap();
6014 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6015
6016 materialize_retrieval_methods(
6017 &mut key_info,
6018 &resolver,
6019 UriTypeSet::ALL,
6020 None,
6021 crate::provider::default_provider(),
6022 )
6023 .unwrap();
6024 assert!(matches!(
6025 key_info.sources.as_slice(),
6026 [super::super::parse::KeyInfoSource::X509Data(info)]
6027 if info.certificates.len() == 1
6028 ));
6029 }
6030
6031 #[test]
6032 fn retrieval_method_candidate_budget_includes_embedded_key_values() {
6033 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
6036 let resources = HashMap::from([("urn:certificate".to_string(), vec![1, 2, 3])]);
6037 let mut key_info = KeyInfo {
6038 sources: vec![
6039 super::super::parse::KeyInfoSource::KeyValue(
6040 super::super::parse::KeyValueInfo::Unsupported {
6041 namespace: Some(XMLDSIG_NS.into()),
6042 local_name: "FutureKeyValue".into(),
6043 },
6044 ),
6045 super::super::parse::KeyInfoSource::RetrievalMethod {
6046 uri: "urn:certificate".into(),
6047 resource_type: Some(RAW_X509_TYPE.into()),
6048 transforms: RetrievalMethodTransforms::None,
6049 },
6050 ],
6051 };
6052 let document = Document::parse("<root/>").unwrap();
6053 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6054 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
6055 let execution_budget = TransformExecutionBudget::default();
6056 let resource_policy = crate::policy::ResourcePolicy {
6057 max_key_candidates: 1,
6058 ..crate::policy::ResourcePolicy::default()
6059 };
6060 let mut budgets = RetrievalMaterializationBudgets {
6061 xpath_parse: &mut xpath_parse_budget,
6062 execution: &execution_budget,
6063 resources: &resource_policy,
6064 xml_backend: crate::XmlBackend::default(),
6065 };
6066 let mut candidate_work = key_info.embedded_candidate_count();
6067
6068 let error = materialize_retrieval_methods_with_budgets(
6069 &mut key_info,
6070 &resolver,
6071 UriTypeSet::ALL,
6072 None,
6073 crate::provider::default_provider(),
6074 &mut budgets,
6075 &mut candidate_work,
6076 )
6077 .expect_err("the retrieved certificate must exceed the aggregate candidate limit");
6078
6079 assert!(matches!(
6080 error,
6081 SignatureVerificationPipelineError::Policy(
6082 crate::policy::PolicyViolation::ResourceLimit {
6083 resource: crate::policy::resource_name::KEY_CANDIDATES,
6084 maximum: 1,
6085 actual: 2,
6086 }
6087 )
6088 ));
6089 }
6090
6091 #[test]
6092 fn raw_x509_retrieval_rejects_empty_same_document_uri() {
6093 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
6096 let certificate = include_bytes!(
6097 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
6098 )
6099 .to_vec();
6100 let resources = HashMap::from([(String::new(), certificate)]);
6101 let mut key_info = KeyInfo {
6102 sources: vec![super::super::parse::KeyInfoSource::RetrievalMethod {
6103 uri: String::new(),
6104 resource_type: Some(RAW_X509_TYPE.into()),
6105 transforms: RetrievalMethodTransforms::None,
6106 }],
6107 };
6108 let document = Document::parse("<root/>").unwrap();
6109 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6110
6111 let error = materialize_retrieval_methods(
6112 &mut key_info,
6113 &resolver,
6114 UriTypeSet::ALL,
6115 None,
6116 crate::provider::default_provider(),
6117 )
6118 .expect_err("empty URI must retain same-document semantics");
6119 assert!(matches!(
6120 error,
6121 SignatureVerificationPipelineError::InvalidStructure {
6122 reason: "raw X509 RetrievalMethod requires an untransformed external URI"
6123 }
6124 ));
6125 }
6126
6127 #[test]
6128 fn verify_context_does_not_hide_malformed_digest_behind_unsupported_transform() {
6129 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6131 let xml = xml.replacen(
6132 "<ds:Reference URI=\"#target\">",
6133 "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
6134 1,
6135 );
6136 replace_fixture_manifest_digest(&xml, "!!!")
6137 });
6138
6139 let error = VerifyContext::new()
6140 .key(&AcceptingKey)
6141 .process_manifests(true)
6142 .verify(&broken_xml)
6143 .expect_err("malformed Manifest digest must not become a validity result");
6144 assert!(matches!(
6145 error,
6146 SignatureVerificationPipelineError::ParseManifestReference(_)
6147 ));
6148 }
6149
6150 #[test]
6151 fn verify_context_rejects_manifest_non_whitespace_mixed_content() {
6152 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6155 xml.replacen(
6156 "<ds:Manifest ID=\"manifest\">",
6157 "<ds:Manifest ID=\"manifest\">junk",
6158 1,
6159 )
6160 });
6161
6162 let err = VerifyContext::new()
6163 .key(&AcceptingKey)
6164 .process_manifests(true)
6165 .verify(&xml)
6166 .expect_err("Manifest mixed content must fail verification");
6167 assert!(matches!(
6168 err,
6169 SignatureVerificationPipelineError::InvalidStructure {
6170 reason: "Manifest contains non-whitespace mixed content"
6171 }
6172 ));
6173 }
6174
6175 #[test]
6176 fn verify_context_rejects_empty_manifest_children() {
6177 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6180 let (prefix, rest) = xml
6181 .split_once("<ds:Manifest ID=\"manifest\">")
6182 .expect("fixture should contain Manifest");
6183 let (_, suffix) = rest
6184 .split_once("</ds:Manifest>")
6185 .expect("fixture should contain closing Manifest");
6186 format!("{prefix}<ds:Manifest ID=\"manifest\"></ds:Manifest>{suffix}")
6187 });
6188
6189 let err = VerifyContext::new()
6190 .key(&AcceptingKey)
6191 .process_manifests(true)
6192 .verify(&xml)
6193 .expect_err("empty Manifest must fail verification");
6194 assert!(matches!(
6195 err,
6196 SignatureVerificationPipelineError::InvalidStructure {
6197 reason: "Manifest must contain at least one ds:Reference element child"
6198 }
6199 ));
6200 }
6201
6202 #[test]
6203 fn verify_context_ignores_unsigned_malformed_manifest_blocks() {
6204 let xml = signature_with_manifest_xml(true).replacen(
6205 "</ds:Object>",
6206 "</ds:Object><ds:Object><ds:Manifest>junk<ds:Foo/></ds:Manifest></ds:Object>",
6207 1,
6208 );
6209 let result = VerifyContext::new()
6210 .key(&AcceptingKey)
6211 .process_manifests(true)
6212 .verify(&xml)
6213 .expect("unsigned malformed Manifest must be ignored");
6214 assert_eq!(
6215 result.manifest_references.len(),
6216 1,
6217 "only signed Manifest references must be reported",
6218 );
6219 assert!(matches!(result.status, DsigStatus::Valid));
6220 }
6221
6222 #[test]
6223 fn verify_context_skips_ambiguous_manifest_id_blocks() {
6224 let xml = signature_with_manifest_xml(true).replacen(
6225 "</ds:Object>",
6226 "</ds:Object><ds:Object><ds:Manifest ID=\"manifest\">junk<ds:Foo/></ds:Manifest></ds:Object>",
6227 1,
6228 );
6229 let err = VerifyContext::new()
6230 .key(&RejectingKey)
6231 .process_manifests(true)
6232 .verify(&xml)
6233 .expect_err("ambiguous manifest IDs should make SignedInfo #manifest dereference fail");
6234 assert!(matches!(
6235 err,
6236 SignatureVerificationPipelineError::Reference(
6237 ReferenceProcessingError::UriDereference(
6238 crate::xmldsig::types::TransformError::ElementNotFound(id)
6239 )
6240 ) if id == "manifest"
6241 ));
6242 }
6243
6244 #[test]
6245 fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
6246 let xml = minimal_signature_xml("", "");
6247 let err = VerifyContext::new()
6248 .key(&RejectingKey)
6249 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
6250 .verify(&xml)
6251 .expect_err("implicit default C14N must be checked against allowlist");
6252 assert!(matches!(
6253 err,
6254 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
6255 operation: "verification transform",
6256 ..
6257 })
6258 ));
6259 }
6260
6261 #[test]
6262 fn verify_context_skips_resolver_when_reference_processing_fails() {
6263 let xml = minimal_signature_xml("", "");
6264 let result = VerifyContext::new()
6265 .key_resolver(&PanicResolver)
6266 .verify(&xml)
6267 .expect("reference digest mismatch should short-circuit before resolver");
6268 assert!(matches!(
6269 result.status,
6270 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
6271 ));
6272 }
6273
6274 #[test]
6275 fn verify_context_reports_key_not_found_when_resolver_misses() {
6276 let xml = signature_with_target_reference("AQ==");
6277 let result = VerifyContext::new()
6278 .key_resolver(&MissingKeyResolver)
6279 .verify(&xml)
6280 .expect("resolver miss should report status, not pipeline error");
6281 assert!(matches!(
6282 result.status,
6283 DsigStatus::Invalid(FailureReason::KeyNotFound)
6284 ));
6285 assert_eq!(
6286 result.signed_info_references.len(),
6287 1,
6288 "KeyNotFound path must preserve SignedInfo reference diagnostics",
6289 );
6290 assert!(matches!(
6291 result.signed_info_references[0].status,
6292 DsigStatus::Valid
6293 ));
6294 }
6295
6296 #[test]
6297 fn verification_candidate_budget_covers_preset_and_custom_resolver_paths() {
6298 let xml = signature_with_target_reference("AQ==");
6301 let mut policy = crate::policy::VerificationPolicy::default();
6302 policy.resources.max_key_candidates = 0;
6303
6304 let preset_error = VerifyContext::new()
6305 .key(&RejectingKey)
6306 .policy(policy.clone())
6307 .verify(&xml)
6308 .expect_err("a preset key consumes one candidate");
6309 assert!(matches!(
6310 preset_error,
6311 SignatureVerificationPipelineError::Policy(
6312 crate::policy::PolicyViolation::ResourceLimit {
6313 resource: crate::policy::resource_name::KEY_CANDIDATES,
6314 maximum: 0,
6315 actual: 1,
6316 }
6317 )
6318 ));
6319
6320 let resolver_error = VerifyContext::new()
6321 .key_resolver(&PanicResolver)
6322 .policy(policy)
6323 .verify(&xml)
6324 .expect_err("a custom resolver requires candidate capacity before dispatch");
6325 assert!(matches!(
6326 resolver_error,
6327 SignatureVerificationPipelineError::Policy(
6328 crate::policy::PolicyViolation::ResourceLimit {
6329 resource: crate::policy::resource_name::KEY_CANDIDATES,
6330 maximum: 0,
6331 actual: 1,
6332 }
6333 )
6334 ));
6335 }
6336
6337 #[test]
6338 fn verification_candidate_budget_precedes_embedded_x509_parsing() {
6339 let first_certificate = base64::engine::general_purpose::STANDARD.encode(include_bytes!(
6342 "../../tests/fixtures/xmldsig/phaos-xmldsig-three/certs/rsa-cert.der"
6343 ));
6344 let xml = signature_with_target_reference("AQ==").replace(
6345 "</ds:SignatureValue>\n </ds:Signature>",
6346 &format!(
6347 "</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>"
6348 ),
6349 );
6350 let mut policy = crate::policy::VerificationPolicy::default();
6351 policy.resources.max_key_candidates = 1;
6352
6353 let error = VerifyContext::new()
6354 .policy(policy)
6355 .verify(&xml)
6356 .expect_err("candidate policy must run before embedded certificate parsing");
6357
6358 assert!(matches!(
6359 error,
6360 SignatureVerificationPipelineError::Policy(
6361 crate::policy::PolicyViolation::ResourceLimit {
6362 resource: crate::policy::resource_name::KEY_CANDIDATES,
6363 maximum: 1,
6364 actual: 2,
6365 }
6366 )
6367 ));
6368 }
6369
6370 #[test]
6371 fn verify_context_resolver_can_ignore_malformed_keyinfo_by_default() {
6372 let base_xml = signature_with_target_reference("AQ==");
6373 let xml = base_xml
6374 .replace(
6375 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
6376 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
6377 )
6378 .replace(
6379 "</ds:SignatureValue>\n </ds:Signature>",
6380 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
6381 );
6382
6383 let result = VerifyContext::new()
6384 .key_resolver(&MissingKeyResolver)
6385 .verify(&xml)
6386 .expect("resolver path should not hard-fail on advisory malformed KeyInfo by default");
6387 assert!(matches!(
6388 result.status,
6389 DsigStatus::Invalid(FailureReason::KeyNotFound)
6390 ));
6391 }
6392
6393 #[test]
6394 fn verify_context_resolver_can_opt_in_to_keyinfo_parse_failures() {
6395 let base_xml = signature_with_target_reference("AQ==");
6396 let xml = base_xml
6397 .replace(
6398 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
6399 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
6400 )
6401 .replace(
6402 "</ds:SignatureValue>\n </ds:Signature>",
6403 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
6404 );
6405
6406 let err = VerifyContext::new()
6407 .key_resolver(&ConsumingKeyInfoResolver)
6408 .verify(&xml)
6409 .expect_err("resolver opted into KeyInfo parsing, malformed KeyInfo must fail");
6410 assert!(matches!(
6411 err,
6412 SignatureVerificationPipelineError::ParseKeyInfo(_)
6413 ));
6414 }
6415
6416 #[test]
6417 fn verify_context_ignores_unsupported_retrieval_before_valid_key_source() {
6418 let xml = signature_with_target_reference("AQ==").replace(
6421 "</ds:SignatureValue>\n </ds:Signature>",
6422 r##"</ds:SignatureValue>
6423 <ds:KeyInfo>
6424 <ds:RetrievalMethod URI="#vendor" Type="urn:vendor:key">
6425 <ds:Transforms><ds:Transform Algorithm="urn:vendor:transform"/></ds:Transforms>
6426 </ds:RetrievalMethod>
6427 <ds:KeyName>fallback</ds:KeyName>
6428 </ds:KeyInfo>
6429 </ds:Signature>"##,
6430 );
6431
6432 let result = VerifyContext::new()
6433 .key_resolver(&FallbackKeyInfoResolver)
6434 .verify(&xml)
6435 .expect("unsupported advisory retrieval must not abort key resolution");
6436 assert_eq!(result.status, DsigStatus::Valid);
6437 }
6438
6439 #[test]
6440 fn verify_context_does_not_eagerly_fail_unused_retrieval_fallback() {
6441 let xml = signature_with_target_reference("AQ==").replace(
6444 "</ds:SignatureValue>\n </ds:Signature>",
6445 r#"</ds:SignatureValue>
6446 <ds:KeyInfo>
6447 <ds:KeyName>primary</ds:KeyName>
6448 <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
6449 </ds:KeyInfo>
6450 </ds:Signature>"#,
6451 );
6452
6453 let result = VerifyContext::new()
6454 .key_resolver(&EarlyKeyInfoResolver)
6455 .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
6456 .verify(&xml)
6457 .expect("an unused missing retrieval fallback must not abort verification");
6458
6459 assert_eq!(result.status, DsigStatus::Valid);
6460 }
6461
6462 #[test]
6463 fn verify_context_does_not_eagerly_fail_unused_same_document_x509_retrieval() {
6464 let xml = signature_with_target_reference("AQ==").replace(
6467 "</ds:SignatureValue>\n </ds:Signature>",
6468 r##"</ds:SignatureValue>
6469 <ds:KeyInfo>
6470 <ds:KeyName>primary</ds:KeyName>
6471 <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
6472 </ds:KeyInfo>
6473 </ds:Signature>"##,
6474 );
6475
6476 let result = VerifyContext::new()
6477 .key_resolver(&EarlyKeyInfoResolver)
6478 .verify(&xml)
6479 .expect("an unused missing X509Data retrieval fallback must not abort verification");
6480
6481 assert_eq!(result.status, DsigStatus::Valid);
6482 }
6483
6484 #[test]
6485 fn verify_context_reports_missing_same_document_x509_retrieval_without_fallback() {
6486 let xml = signature_with_target_reference("AQ==").replace(
6489 "</ds:SignatureValue>\n </ds:Signature>",
6490 r##"</ds:SignatureValue>
6491 <ds:KeyInfo>
6492 <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
6493 </ds:KeyInfo>
6494 </ds:Signature>"##,
6495 );
6496
6497 let error = VerifyContext::new()
6498 .key_resolver(&ConsumingKeyInfoResolver)
6499 .verify(&xml)
6500 .expect_err("a sole missing X509Data retrieval must remain an explicit error");
6501
6502 assert!(matches!(
6503 error,
6504 SignatureVerificationPipelineError::InvalidStructure {
6505 reason: "X509Data RetrievalMethod target is missing or ambiguous"
6506 }
6507 ));
6508 }
6509
6510 #[test]
6511 fn verify_context_does_not_eagerly_parse_unused_retrieval_fallback() {
6512 let xml = signature_with_target_reference("AQ==").replace(
6515 "</ds:SignatureValue>\n </ds:Signature>",
6516 r#"</ds:SignatureValue>
6517 <ds:KeyInfo>
6518 <ds:KeyName>primary</ds:KeyName>
6519 <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
6520 </ds:KeyInfo>
6521 </ds:Signature>"#,
6522 );
6523 let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
6524
6525 let result = VerifyContext::new()
6526 .key_resolver(&EarlyKeyInfoResolver)
6527 .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
6528 .external_resources(&resources)
6529 .verify(&xml)
6530 .expect("an unused malformed retrieval fallback must not abort verification");
6531
6532 assert_eq!(result.status, DsigStatus::Valid);
6533 }
6534
6535 #[test]
6536 fn verify_context_reports_missing_retrieval_when_no_key_source_resolves() {
6537 let xml = signature_with_target_reference("AQ==").replace(
6540 "</ds:SignatureValue>\n </ds:Signature>",
6541 r#"</ds:SignatureValue>
6542 <ds:KeyInfo>
6543 <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
6544 </ds:KeyInfo>
6545 </ds:Signature>"#,
6546 );
6547
6548 let error = VerifyContext::new()
6549 .key_resolver(&ConsumingKeyInfoResolver)
6550 .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
6551 .verify(&xml)
6552 .expect_err("a missing sole RetrievalMethod must remain an explicit error");
6553
6554 assert!(matches!(
6555 error,
6556 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
6557 crate::xmldsig::TransformError::UnsupportedUri(uri)
6558 )) if uri == "missing.der"
6559 ));
6560 }
6561
6562 #[test]
6563 fn verify_context_reports_malformed_retrieval_when_no_key_source_resolves() {
6564 let xml = signature_with_target_reference("AQ==").replace(
6567 "</ds:SignatureValue>\n </ds:Signature>",
6568 r#"</ds:SignatureValue>
6569 <ds:KeyInfo>
6570 <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
6571 </ds:KeyInfo>
6572 </ds:Signature>"#,
6573 );
6574 let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
6575
6576 let error = VerifyContext::new()
6577 .key_resolver(&ConsumingKeyInfoResolver)
6578 .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
6579 .external_resources(&resources)
6580 .verify(&xml)
6581 .expect_err("a malformed sole RetrievalMethod must remain a parse error");
6582
6583 assert!(matches!(
6584 error,
6585 SignatureVerificationPipelineError::ParseKeyInfo(_)
6586 ));
6587 }
6588
6589 #[test]
6590 fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
6591 let xml = signature_with_target_reference("@@@");
6592
6593 let err = VerifyContext::new()
6594 .key_resolver(&MissingKeyResolver)
6595 .verify(&xml)
6596 .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
6597 assert!(matches!(
6598 err,
6599 SignatureVerificationPipelineError::SignatureValueBase64(_)
6600 ));
6601 }
6602
6603 #[test]
6604 fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
6605 let xml = signature_with_target_reference("@@@");
6606
6607 let err = VerifyContext::new()
6608 .verify(&xml)
6609 .expect_err("invalid SignatureValue must remain a decode error");
6610 assert!(matches!(
6611 err,
6612 SignatureVerificationPipelineError::SignatureValueBase64(_)
6613 ));
6614 }
6615
6616 #[test]
6617 fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
6618 let references = vec![Reference {
6619 uri: None,
6620 id: None,
6621 ref_type: None,
6622 transforms: vec![],
6623 digest_method: DigestAlgorithm::Sha256,
6624 digest_value: vec![0; 32],
6625 }];
6626 let uri_types = UriTypeSet {
6627 allow_empty: false,
6628 allow_same_document: true,
6629 allow_external: false,
6630 };
6631
6632 let err = enforce_reference_policies(&references, uri_types, None)
6633 .expect_err("missing URI must fail before allow_empty policy is evaluated");
6634 assert!(matches!(
6635 err,
6636 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
6637 ));
6638 }
6639
6640 #[test]
6641 fn enforce_reference_policies_checks_only_terminal_binary_output() {
6642 let c14n = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI).unwrap();
6643 let allowed = HashSet::from([
6644 BASE64_TRANSFORM_URI.to_owned(),
6645 DEFAULT_IMPLICIT_C14N_URI.to_owned(),
6646 ]);
6647 let without_implicit_c14n = HashSet::from([BASE64_TRANSFORM_URI.to_owned()]);
6648
6649 for transforms in [
6650 vec![Transform::Base64Decode, Transform::C14n(c14n)],
6651 vec![Transform::Base64Decode, Transform::Base64Decode],
6652 ] {
6653 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, vec![0; 32]);
6654 enforce_reference_policies(
6655 std::slice::from_ref(&reference),
6656 UriTypeSet::default(),
6657 Some(&allowed),
6658 )
6659 .expect("terminal binary output must not require implicit C14N");
6660 }
6661
6662 let terminal_base64 = make_reference(
6663 "",
6664 vec![Transform::Base64Decode, Transform::Base64Decode],
6665 DigestAlgorithm::Sha256,
6666 vec![0; 32],
6667 );
6668 enforce_reference_policies(
6669 std::slice::from_ref(&terminal_base64),
6670 UriTypeSet::default(),
6671 Some(&without_implicit_c14n),
6672 )
6673 .expect("terminal Base64 output must not require implicit C14N");
6674
6675 let no_transforms = make_reference("", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
6676 let error = enforce_reference_policies(
6677 std::slice::from_ref(&no_transforms),
6678 UriTypeSet::default(),
6679 Some(&without_implicit_c14n),
6680 )
6681 .expect_err("a node-set result must require allowlisted implicit C14N");
6682 assert!(matches!(
6683 error,
6684 SignatureVerificationPipelineError::Policy(
6685 crate::policy::PolicyViolation::Algorithm {
6686 operation: "verification transform",
6687 ref algorithm,
6688 }
6689 )
6690 if algorithm == DEFAULT_IMPLICIT_C14N_URI
6691 ));
6692
6693 let detached = make_reference("urn:payload", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
6694 enforce_reference_policies(
6695 std::slice::from_ref(&detached),
6696 UriTypeSet::ALL,
6697 Some(&without_implicit_c14n),
6698 )
6699 .expect("external octets without transforms must not require implicit C14N");
6700
6701 let external_xpath = make_reference(
6702 "urn:payload",
6703 vec![Transform::XPath(
6704 super::super::transforms::XPathExpression::new("true()"),
6705 )],
6706 DigestAlgorithm::Sha256,
6707 vec![0; 32],
6708 );
6709 let error = enforce_reference_policies(
6710 std::slice::from_ref(&external_xpath),
6711 UriTypeSet::ALL,
6712 Some(&HashSet::from([XPATH_TRANSFORM_URI.to_owned()])),
6713 )
6714 .expect_err("external XML converted to a node-set must require implicit C14N");
6715 assert!(matches!(
6716 error,
6717 SignatureVerificationPipelineError::Policy(
6718 crate::policy::PolicyViolation::Algorithm {
6719 operation: "verification transform",
6720 ref algorithm,
6721 }
6722 )
6723 if algorithm == DEFAULT_IMPLICIT_C14N_URI
6724 ));
6725 }
6726
6727 #[test]
6728 fn stored_pre_digest_budget_counts_repeated_external_references() {
6729 let document =
6732 Document::parse("<ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"/>")
6733 .unwrap();
6734 let payload = vec![b'x'; 7];
6735 let digest = compute_digest(DigestAlgorithm::Sha256, &payload);
6736 let references = (0..5)
6737 .map(|_| {
6738 make_reference(
6739 "urn:repeated",
6740 Vec::new(),
6741 DigestAlgorithm::Sha256,
6742 digest.clone(),
6743 )
6744 })
6745 .collect::<Vec<_>>();
6746 let resources = HashMap::from([("urn:repeated".to_owned(), payload)]);
6747 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6748 let transform_budget = TransformExecutionBudget::default();
6749 let canonicalized_data_budget = CanonicalizedDataBudget::with_limit(32);
6750 let execution = ReferenceExecutionContext {
6751 store_pre_digest: true,
6752 transform_options: TransformOptions::default(),
6753 transform_budget: &transform_budget,
6754 canonicalized_data_budget: &canonicalized_data_budget,
6755 provider: crate::provider::default_provider(),
6756 };
6757
6758 let error = process_all_references_with_options(
6759 &references,
6760 &resolver,
6761 document.root_element(),
6762 &execution,
6763 )
6764 .expect_err(
6765 "retained diagnostics must not multiply one external allocation past the aggregate cap",
6766 );
6767 assert!(matches!(
6768 error,
6769 ReferenceProcessingError::Policy(crate::policy::PolicyViolation::ResourceLimit {
6770 resource: "canonicalized bytes",
6771 maximum: 32,
6772 ..
6773 })
6774 ));
6775 }
6776
6777 #[test]
6778 fn canonical_signed_info_obeys_policy_without_diagnostic_retention() {
6779 let xml = signature_with_target_reference("AQ==");
6783 let marker = "<ds:SignatureMethod";
6784 let padding = " ".repeat(1_025);
6785 let xml = xml.replacen(marker, &format!("{padding}{marker}"), 1);
6786 let policy = crate::policy::VerificationPolicy {
6787 resources: crate::policy::ResourcePolicy {
6788 max_canonicalized_bytes: 1_024,
6789 ..crate::policy::ResourcePolicy::default()
6790 },
6791 ..crate::policy::VerificationPolicy::default()
6792 };
6793
6794 let error = VerifyContext::new()
6795 .key(&AcceptingKey)
6796 .policy(policy)
6797 .verify(&xml)
6798 .expect_err("canonicalized SignedInfo must remain policy-bounded");
6799
6800 assert!(matches!(
6801 error,
6802 SignatureVerificationPipelineError::Policy(
6803 crate::policy::PolicyViolation::ResourceLimit {
6804 resource: "canonicalized bytes",
6805 ..
6806 }
6807 )
6808 ));
6809 }
6810
6811 #[test]
6812 fn push_normalized_signature_text_rejects_form_feed() {
6813 let mut normalized = Vec::new();
6814 let mut raw_text_len = 0usize;
6815 let err =
6816 push_normalized_signature_text("ab\u{000C}cd", &mut raw_text_len, &mut normalized)
6817 .expect_err("form-feed must not be treated as XML base64 whitespace");
6818 assert!(matches!(
6819 err,
6820 SignatureVerificationPipelineError::SignatureValueBase64(
6821 base64::DecodeError::InvalidByte(_, 0x0C)
6822 )
6823 ));
6824 }
6825
6826 #[test]
6827 fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
6828 let mut normalized = vec![b'A'; MAX_SIGNATURE_VALUE_LEN - 1];
6829 let mut raw_text_len = normalized.len();
6830 let err = push_normalized_signature_text("é", &mut raw_text_len, &mut normalized)
6831 .expect_err("multibyte characters must not bypass byte-size limit");
6832 assert!(matches!(
6833 err,
6834 SignatureVerificationPipelineError::InvalidStructure {
6835 reason: "SignatureValue exceeds maximum allowed length"
6836 }
6837 ));
6838 }
6839
6840 #[test]
6843 fn reference_with_correct_digest_passes() {
6844 let xml = r##"<root>
6847 <data>hello world</data>
6848 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
6849 <ds:SignedInfo/>
6850 </ds:Signature>
6851 </root>"##;
6852 let doc = Document::parse(xml).unwrap();
6853 let resolver = UriReferenceResolver::new(&doc);
6854 let sig_node = doc
6855 .descendants()
6856 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
6857 .unwrap();
6858
6859 let initial_data = resolver.dereference("").unwrap();
6861 let transforms = vec![
6862 Transform::Enveloped,
6863 Transform::C14n(
6864 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
6865 .unwrap(),
6866 ),
6867 ];
6868 let pre_digest_bytes =
6869 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
6870 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
6871
6872 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
6874
6875 let result = process_reference(
6876 &reference,
6877 &resolver,
6878 sig_node,
6879 ReferenceSet::SignedInfo,
6880 0,
6881 false,
6882 )
6883 .unwrap();
6884 assert!(
6885 matches!(result.status, DsigStatus::Valid),
6886 "digest should match"
6887 );
6888 assert!(result.pre_digest_data.is_none());
6889 }
6890
6891 #[test]
6892 fn reference_with_wrong_digest_fails() {
6893 let xml = r##"<root>
6894 <data>hello</data>
6895 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6896 <ds:SignedInfo/>
6897 </ds:Signature>
6898 </root>"##;
6899 let doc = Document::parse(xml).unwrap();
6900 let resolver = UriReferenceResolver::new(&doc);
6901 let sig_node = doc
6902 .descendants()
6903 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
6904 .unwrap();
6905
6906 let transforms = vec![Transform::Enveloped];
6907 let wrong_digest = vec![0u8; 32];
6909 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
6910
6911 let result = process_reference(
6912 &reference,
6913 &resolver,
6914 sig_node,
6915 ReferenceSet::SignedInfo,
6916 0,
6917 false,
6918 )
6919 .unwrap();
6920 assert!(matches!(
6921 result.status,
6922 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
6923 ));
6924 }
6925
6926 #[test]
6927 fn reference_with_wrong_digest_preserves_supplied_ref_index() {
6928 let xml = r##"<root>
6929 <data>hello</data>
6930 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6931 <ds:SignedInfo/>
6932 </ds:Signature>
6933 </root>"##;
6934 let doc = Document::parse(xml).unwrap();
6935 let resolver = UriReferenceResolver::new(&doc);
6936 let sig_node = doc
6937 .descendants()
6938 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
6939 .unwrap();
6940
6941 let reference = make_reference(
6942 "",
6943 vec![Transform::Enveloped],
6944 DigestAlgorithm::Sha256,
6945 vec![0u8; 32],
6946 );
6947 let result = process_reference(
6948 &reference,
6949 &resolver,
6950 sig_node,
6951 ReferenceSet::SignedInfo,
6952 7,
6953 false,
6954 )
6955 .unwrap();
6956 assert!(matches!(
6957 result.status,
6958 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
6959 ));
6960 }
6961
6962 #[test]
6963 fn reference_stores_pre_digest_data() {
6964 let xml = "<root><child>text</child></root>";
6965 let doc = Document::parse(xml).unwrap();
6966 let resolver = UriReferenceResolver::new(&doc);
6967
6968 let initial_data = resolver.dereference("").unwrap();
6970 let pre_digest =
6971 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
6972 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
6973
6974 let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
6975 let result = process_reference(
6976 &reference,
6977 &resolver,
6978 doc.root_element(),
6979 ReferenceSet::SignedInfo,
6980 0,
6981 true,
6982 )
6983 .unwrap();
6984
6985 assert!(matches!(result.status, DsigStatus::Valid));
6986 assert!(result.pre_digest_data.is_some());
6987 assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
6988 }
6989
6990 #[test]
6993 fn reference_with_id_uri() {
6994 let xml = r##"<root>
6995 <item ID="target">specific content</item>
6996 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6997 <ds:SignedInfo/>
6998 </ds:Signature>
6999 </root>"##;
7000 let doc = Document::parse(xml).unwrap();
7001 let resolver = UriReferenceResolver::new(&doc);
7002 let sig_node = doc
7003 .descendants()
7004 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
7005 .unwrap();
7006
7007 let initial_data = resolver.dereference("#target").unwrap();
7009 let transforms = vec![Transform::C14n(
7010 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
7011 .unwrap(),
7012 )];
7013 let pre_digest =
7014 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
7015 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
7016
7017 let reference = make_reference(
7018 "#target",
7019 transforms,
7020 DigestAlgorithm::Sha256,
7021 expected_digest,
7022 );
7023 let result = process_reference(
7024 &reference,
7025 &resolver,
7026 sig_node,
7027 ReferenceSet::SignedInfo,
7028 0,
7029 false,
7030 )
7031 .unwrap();
7032 assert!(matches!(result.status, DsigStatus::Valid));
7033 }
7034
7035 #[test]
7036 fn reference_with_nonexistent_id_fails() {
7037 let xml = "<root><child/></root>";
7038 let doc = Document::parse(xml).unwrap();
7039 let resolver = UriReferenceResolver::new(&doc);
7040
7041 let reference =
7042 make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
7043 let result = process_reference(
7044 &reference,
7045 &resolver,
7046 doc.root_element(),
7047 ReferenceSet::SignedInfo,
7048 0,
7049 false,
7050 );
7051 assert!(result.is_err());
7052 }
7053
7054 #[test]
7055 fn reference_with_absent_uri_fails_closed() {
7056 let xml = "<root><child>text</child></root>";
7057 let doc = Document::parse(xml).unwrap();
7058 let resolver = UriReferenceResolver::new(&doc);
7059
7060 let reference = Reference {
7061 uri: None, id: None,
7063 ref_type: None,
7064 transforms: vec![],
7065 digest_method: DigestAlgorithm::Sha256,
7066 digest_value: vec![0; 32],
7067 };
7068
7069 let result = process_reference(
7070 &reference,
7071 &resolver,
7072 doc.root_element(),
7073 ReferenceSet::SignedInfo,
7074 0,
7075 false,
7076 );
7077 assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
7078 }
7079
7080 #[test]
7083 fn all_references_pass() {
7084 let xml = "<root><child>text</child></root>";
7085 let doc = Document::parse(xml).unwrap();
7086 let resolver = UriReferenceResolver::new(&doc);
7087
7088 let initial_data = resolver.dereference("").unwrap();
7090 let pre_digest =
7091 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
7092 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
7093
7094 let refs = vec![
7095 make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
7096 make_reference("", vec![], DigestAlgorithm::Sha256, digest),
7097 ];
7098
7099 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
7100 assert!(result.all_valid());
7101 assert_eq!(result.results.len(), 2);
7102 assert!(result.first_failure.is_none());
7103 }
7104
7105 #[test]
7106 fn reference_processing_shares_xpath_work_across_references() {
7107 let document = Document::parse("<root/>").unwrap();
7110 let resolver = UriReferenceResolver::new(&document);
7111 let transform = Transform::XPath(super::super::transforms::XPathExpression::new("true()"));
7112 let initial_data = resolver.dereference("").unwrap();
7113 let pre_digest = crate::xmldsig::execute_transforms(
7114 document.root_element(),
7115 initial_data,
7116 std::slice::from_ref(&transform),
7117 )
7118 .unwrap();
7119 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
7120 let references = vec![
7121 make_reference(
7122 "",
7123 vec![transform.clone()],
7124 DigestAlgorithm::Sha256,
7125 digest.clone(),
7126 ),
7127 make_reference("", vec![transform], DigestAlgorithm::Sha256, digest),
7128 ];
7129 let budget = TransformExecutionBudget::with_xpath_limit(12);
7130 let canonicalized_data_budget = CanonicalizedDataBudget::default();
7131 let execution = ReferenceExecutionContext {
7132 store_pre_digest: false,
7133 transform_options: TransformOptions::default(),
7134 transform_budget: &budget,
7135 canonicalized_data_budget: &canonicalized_data_budget,
7136 provider: crate::provider::default_provider(),
7137 };
7138
7139 let error = process_all_references_with_options(
7140 &references,
7141 &resolver,
7142 document.root_element(),
7143 &execution,
7144 )
7145 .expect_err("the second Reference must consume the first Reference's XPath work");
7146
7147 assert!(matches!(
7148 error,
7149 ReferenceProcessingError::Transform(TransformError::Policy(
7150 crate::policy::PolicyViolation::ResourceLimit {
7151 resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
7152 ..
7153 }
7154 ))
7155 ));
7156 }
7157
7158 #[test]
7159 fn reference_processing_shares_node_set_materialization_across_references() {
7160 let document = Document::parse(
7164 r#"<root xmlns:n="urn:0123456789"><target Id="selected">payload</target></root>"#,
7165 )
7166 .unwrap();
7167 let resolver = UriReferenceResolver::new(&document);
7168 let initial_data = resolver.dereference("#selected").unwrap();
7169 let pre_digest =
7170 crate::xmldsig::execute_transforms(document.root_element(), initial_data, &[]).unwrap();
7171 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
7172 let references = vec![
7173 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest.clone()),
7174 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest),
7175 ];
7176 let budget = TransformExecutionBudget::with_node_set_materialization_limit(30);
7177 let canonicalized_data_budget = CanonicalizedDataBudget::default();
7178 let execution = ReferenceExecutionContext {
7179 store_pre_digest: false,
7180 transform_options: TransformOptions::default(),
7181 transform_budget: &budget,
7182 canonicalized_data_budget: &canonicalized_data_budget,
7183 provider: crate::provider::default_provider(),
7184 };
7185
7186 let error = process_all_references_with_options(
7187 &references,
7188 &resolver,
7189 document.root_element(),
7190 &execution,
7191 )
7192 .expect_err("the second Reference must consume the first Reference's materialization work");
7193
7194 assert!(matches!(
7195 error,
7196 ReferenceProcessingError::UriDereference(TransformError::Policy(
7197 crate::policy::PolicyViolation::ResourceLimit {
7198 resource: crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
7199 ..
7200 }
7201 ))
7202 ));
7203 }
7204
7205 #[test]
7206 fn fail_fast_on_first_mismatch() {
7207 let xml = "<root><child>text</child></root>";
7208 let doc = Document::parse(xml).unwrap();
7209 let resolver = UriReferenceResolver::new(&doc);
7210
7211 let wrong_digest = vec![0u8; 32];
7212 let refs = vec![
7213 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
7214 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
7216 ];
7217
7218 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
7219 assert!(!result.all_valid());
7220 assert_eq!(result.first_failure, Some(0));
7221 assert_eq!(result.results.len(), 1);
7223 assert!(matches!(
7224 result.results[0].status,
7225 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
7226 ));
7227 }
7228
7229 #[test]
7230 fn fail_fast_second_reference() {
7231 let xml = "<root><child>text</child></root>";
7232 let doc = Document::parse(xml).unwrap();
7233 let resolver = UriReferenceResolver::new(&doc);
7234
7235 let initial_data = resolver.dereference("").unwrap();
7237 let pre_digest =
7238 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
7239 let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
7240 let wrong_digest = vec![0u8; 32];
7241
7242 let refs = vec![
7243 make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
7244 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
7245 ];
7246
7247 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
7248 assert!(!result.all_valid());
7249 assert_eq!(result.first_failure, Some(1));
7250 assert_eq!(result.results.len(), 2);
7252 assert!(matches!(result.results[0].status, DsigStatus::Valid));
7253 assert!(matches!(
7254 result.results[1].status,
7255 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
7256 ));
7257 }
7258
7259 #[test]
7260 fn empty_references_list() {
7261 let xml = "<root/>";
7262 let doc = Document::parse(xml).unwrap();
7263 let resolver = UriReferenceResolver::new(&doc);
7264
7265 let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
7266 assert!(result.all_valid());
7267 assert!(result.results.is_empty());
7268 }
7269
7270 #[test]
7273 fn reference_sha1_digest() {
7274 let xml = "<root>content</root>";
7275 let doc = Document::parse(xml).unwrap();
7276 let resolver = UriReferenceResolver::new(&doc);
7277
7278 let initial_data = resolver.dereference("").unwrap();
7279 let pre_digest =
7280 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
7281 let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
7282
7283 let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
7284 let result = process_reference(
7285 &reference,
7286 &resolver,
7287 doc.root_element(),
7288 ReferenceSet::SignedInfo,
7289 0,
7290 false,
7291 )
7292 .unwrap();
7293 assert!(matches!(result.status, DsigStatus::Valid));
7294 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
7295 }
7296
7297 #[test]
7298 fn reference_sha512_digest() {
7299 let xml = "<root>content</root>";
7300 let doc = Document::parse(xml).unwrap();
7301 let resolver = UriReferenceResolver::new(&doc);
7302
7303 let initial_data = resolver.dereference("").unwrap();
7304 let pre_digest =
7305 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
7306 let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
7307
7308 let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
7309 let result = process_reference(
7310 &reference,
7311 &resolver,
7312 doc.root_element(),
7313 ReferenceSet::SignedInfo,
7314 0,
7315 false,
7316 )
7317 .unwrap();
7318 assert!(matches!(result.status, DsigStatus::Valid));
7319 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
7320 }
7321
7322 #[test]
7325 fn saml_enveloped_reference_processing() {
7326 let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
7328 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
7329 ID="_resp1">
7330 <saml:Assertion ID="_assert1">
7331 <saml:Subject>user@example.com</saml:Subject>
7332 </saml:Assertion>
7333 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7334 <ds:SignedInfo>
7335 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7336 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
7337 <ds:Reference URI="">
7338 <ds:Transforms>
7339 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
7340 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7341 </ds:Transforms>
7342 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
7343 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
7344 </ds:Reference>
7345 </ds:SignedInfo>
7346 <ds:SignatureValue>fakesig==</ds:SignatureValue>
7347 </ds:Signature>
7348 </samlp:Response>"##;
7349 let doc = Document::parse(xml).unwrap();
7350 let resolver = UriReferenceResolver::new(&doc);
7351 let sig_node = doc
7352 .descendants()
7353 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
7354 .unwrap();
7355
7356 let signed_info_node = sig_node
7358 .children()
7359 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
7360 .unwrap();
7361 let signed_info = parse_signed_info(signed_info_node).unwrap();
7362 let reference = &signed_info.references[0];
7363
7364 let initial_data = resolver.dereference("").unwrap();
7366 let pre_digest =
7367 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
7368 .unwrap();
7369 let correct_digest = compute_digest(reference.digest_method, &pre_digest);
7370
7371 let corrected_ref = make_reference(
7373 "",
7374 reference.transforms.clone(),
7375 reference.digest_method,
7376 correct_digest,
7377 );
7378
7379 let result = process_reference(
7381 &corrected_ref,
7382 &resolver,
7383 sig_node,
7384 ReferenceSet::SignedInfo,
7385 0,
7386 true,
7387 )
7388 .unwrap();
7389 assert!(
7390 matches!(result.status, DsigStatus::Valid),
7391 "SAML reference should verify"
7392 );
7393 assert!(result.pre_digest_data.is_some());
7394
7395 let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
7397 assert!(
7398 pre_digest_str.contains("samlp:Response"),
7399 "pre-digest should contain Response"
7400 );
7401 assert!(
7402 !pre_digest_str.contains("SignatureValue"),
7403 "pre-digest should NOT contain Signature"
7404 );
7405 }
7406
7407 #[test]
7408 fn pipeline_missing_signed_info_returns_missing_element() {
7409 let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
7410
7411 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
7412 .expect_err("missing SignedInfo must fail before crypto stage");
7413 assert!(matches!(
7414 err,
7415 SignatureVerificationPipelineError::MissingElement {
7416 element: "SignedInfo"
7417 }
7418 ));
7419 }
7420
7421 #[test]
7422 fn pipeline_multiple_signature_elements_are_rejected() {
7423 let xml = r#"
7424<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7425 <ds:Signature>
7426 <ds:SignedInfo/>
7427 </ds:Signature>
7428 <ds:Signature/>
7429</root>
7430"#;
7431
7432 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
7433 .expect_err("multiple signatures must fail closed");
7434 assert!(matches!(
7435 err,
7436 SignatureVerificationPipelineError::InvalidStructure {
7437 reason: "Signature must appear exactly once in document",
7438 }
7439 ));
7440 }
7441
7442 #[test]
7443 fn pipeline_start_node_limits_signature_cardinality_to_its_subtree() {
7444 let xml = r#"
7447<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7448 <scope Id="selected"><ds:Signature/></scope>
7449 <scope Id="other"><ds:Signature/></scope>
7450</root>
7451"#;
7452 let err = VerifyContext::new()
7453 .start_node_id("selected")
7454 .verify(xml)
7455 .expect_err("the selected Signature remains structurally incomplete");
7456 assert!(matches!(
7457 err,
7458 SignatureVerificationPipelineError::MissingElement {
7459 element: "SignedInfo"
7460 }
7461 ));
7462 }
7463
7464 #[test]
7465 fn pipeline_reports_keyinfo_parse_error() {
7466 let xml = r#"
7467<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
7468 xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
7469 <ds:SignedInfo>
7470 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7471 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
7472 <ds:Reference URI="">
7473 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
7474 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
7475 </ds:Reference>
7476 </ds:SignedInfo>
7477 <ds:SignatureValue>AA==</ds:SignatureValue>
7478 <ds:KeyInfo>
7479 <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
7480 </ds:KeyInfo>
7481</ds:Signature>
7482"#;
7483
7484 let err = VerifyContext::new().verify(xml).expect_err(
7485 "invalid KeyInfo must map to ParseKeyInfo when no explicit key is supplied",
7486 );
7487 assert!(matches!(
7488 err,
7489 SignatureVerificationPipelineError::ParseKeyInfo(_)
7490 ));
7491 }
7492
7493 #[test]
7494 fn pipeline_ignores_malformed_keyinfo_when_explicit_key_is_supplied() {
7495 let base_xml = signature_with_target_reference("AQ==");
7496 let xml = base_xml
7497 .replace(
7498 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
7499 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
7500 )
7501 .replace(
7502 "</ds:SignatureValue>\n </ds:Signature>",
7503 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
7504 );
7505
7506 let result = VerifyContext::new()
7507 .key(&RejectingKey)
7508 .verify(&xml)
7509 .expect("explicit key path should not fail on malformed KeyInfo");
7510 assert!(matches!(
7511 result.status,
7512 DsigStatus::Invalid(FailureReason::SignatureMismatch)
7513 ));
7514 }
7515
7516 #[test]
7517 fn pipeline_rejects_foreign_element_children_under_signature() {
7518 let base_xml = signature_with_target_reference("AQ==");
7519 let xml = base_xml
7520 .replace(
7521 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
7522 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:foo="urn:example:foo">"#,
7523 )
7524 .replace(
7525 "</ds:SignedInfo>\n <ds:SignatureValue>",
7526 "</ds:SignedInfo>\n <foo:Bar/>\n <ds:SignatureValue>",
7527 );
7528
7529 let err = VerifyContext::new()
7530 .key(&RejectingKey)
7531 .verify(&xml)
7532 .expect_err("foreign element children under Signature must fail closed");
7533 assert!(matches!(
7534 err,
7535 SignatureVerificationPipelineError::InvalidStructure {
7536 reason: "Signature must contain only XMLDSIG element children",
7537 }
7538 ));
7539 }
7540
7541 #[test]
7542 fn pipeline_rejects_non_whitespace_mixed_content_under_signature() {
7543 let base_xml = signature_with_target_reference("AQ==");
7544 let xml = base_xml.replace(
7545 "</ds:SignedInfo>\n <ds:SignatureValue>",
7546 "</ds:SignedInfo>\n oops\n <ds:SignatureValue>",
7547 );
7548
7549 let err = VerifyContext::new()
7550 .key(&RejectingKey)
7551 .verify(&xml)
7552 .expect_err("non-whitespace mixed content under Signature must fail closed");
7553 assert!(matches!(
7554 err,
7555 SignatureVerificationPipelineError::InvalidStructure {
7556 reason: "Signature must not contain non-whitespace mixed content",
7557 }
7558 ));
7559 }
7560
7561 #[test]
7562 fn pipeline_rejects_keyinfo_out_of_order() {
7563 let base_xml = signature_with_target_reference("AQ==");
7564 let xml = base_xml.replace(
7565 "</ds:SignatureValue>\n </ds:Signature>",
7566 "</ds:SignatureValue>\n <ds:Object/>\n <ds:KeyInfo><ds:KeyName>late</ds:KeyName></ds:KeyInfo>\n </ds:Signature>",
7567 );
7568
7569 let err = VerifyContext::new()
7570 .key(&RejectingKey)
7571 .verify(&xml)
7572 .expect_err("KeyInfo after Object must be rejected by Signature child order checks");
7573 assert!(matches!(
7574 err,
7575 SignatureVerificationPipelineError::InvalidStructure {
7576 reason: "KeyInfo must be the third element child of Signature when present"
7577 }
7578 ));
7579 }
7580
7581 #[test]
7582 fn pipeline_accepts_comments_and_processing_instructions_under_signature() {
7583 let xml = r#"
7584<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7585 <?dbg keep ?>
7586 <!-- signature metadata -->
7587 <ds:SignedInfo>
7588 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7589 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
7590 <ds:Reference URI="">
7591 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
7592 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
7593 </ds:Reference>
7594 </ds:SignedInfo>
7595 <!-- between required children -->
7596 <ds:SignatureValue>AA==</ds:SignatureValue>
7597</ds:Signature>
7598"#;
7599
7600 let doc = Document::parse(xml).expect("test XML must parse");
7601 let signature_node = doc.root_element();
7602 let parsed = parse_signature_children(signature_node)
7603 .expect("comment/PI nodes under Signature must be ignored");
7604
7605 assert_eq!(parsed.signed_info_node.tag_name().name(), "SignedInfo");
7606 assert_eq!(
7607 parsed.signature_value_node.tag_name().name(),
7608 "SignatureValue"
7609 );
7610 assert!(parsed.key_info_node.is_none());
7611 }
7612}