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