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 let xml = crate::encoding::decode_xml_octets(bytes).map_err(|_| {
2074 SignatureVerificationPipelineError::InvalidStructure {
2075 reason: "KeyInfoReference external resource has an invalid XML encoding",
2076 }
2077 })?;
2078 let settings = DocumentParseSettings::from_policy(
2079 context.policy.xml(),
2080 context.policy.resources(),
2081 )
2082 .with_backend(context.budgets.xml_backend);
2083 let document = XmlDocument::parse_with_settings_and_budget(
2084 xml.into_owned(),
2085 settings,
2086 context.budgets.execution.xml_parse_work(),
2087 )
2088 .map_err(|error| map_document_parse_error(error, settings))?;
2089 document.with_view(|view| {
2090 let external_resolver = resolver.for_external_document_view(view, resource_uri);
2091 let target = match fragment {
2092 Some(fragment) => external_resolver
2093 .node_for_same_document_reference(&format!("#{fragment}"))
2094 .map_err(|error| {
2095 SignatureVerificationPipelineError::from(
2096 ReferenceProcessingError::UriDereference(error),
2097 )
2098 })?
2099 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2100 reason: "KeyInfoReference external target is missing or ambiguous",
2101 })?,
2102 _ => view.document().root_element(),
2103 };
2104 if !target.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
2105 return Err(SignatureVerificationPipelineError::InvalidStructure {
2106 reason: "KeyInfoReference external target must be KeyInfo",
2107 });
2108 }
2109 let mut referenced = parse_key_info_with_policy_budgets_and_document_base(
2110 target,
2111 context.provider,
2112 context.budgets.execution.xml_base_resolution(),
2113 context.policy.resources(),
2114 Some(resource_uri),
2115 )
2116 .map_err(map_key_info_parse_error)?;
2117 let mut nested_outcome = visit(
2118 &mut referenced,
2119 &external_resolver,
2120 context,
2121 materialization,
2122 next_depth,
2123 )?;
2124 nested_outcome.merge(materialize_retrieval_methods_with_budgets(
2125 &mut referenced,
2126 &external_resolver,
2127 context.policy.retrieval_method_uris(),
2128 context.policy.allowed_transforms(),
2129 context.provider,
2130 context.budgets,
2131 &mut materialization.candidate_work,
2132 )?);
2133 referenced.sources.retain(|source| {
2136 !matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. })
2137 });
2138 Ok((referenced, nested_outcome))
2139 })?
2140 };
2141 if is_same_document {
2142 nested_outcome.merge(visit(
2143 &mut referenced,
2144 resolver,
2145 context,
2146 materialization,
2147 next_depth,
2148 )?);
2149 }
2150 outcome.merge(nested_outcome);
2151 materialization.active.remove(&cycle_key);
2152 materialized.extend(referenced.sources);
2153 }
2154 key_info.sources = materialized;
2155 context
2156 .policy
2157 .resources()
2158 .validate_key_candidates(key_info.embedded_candidate_count())?;
2159 Ok(outcome)
2160 }
2161
2162 let mut context = KeyInfoReferenceMaterializationContext {
2163 policy,
2164 provider,
2165 budgets,
2166 };
2167 visit(key_info, resolver, &mut context, materialization, 0)
2168}
2169
2170fn materialize_key_info_references_for_policy<P: KeyInfoReferencePolicy>(
2171 key_info: &mut KeyInfo,
2172 resolver: UriReferenceResolver<'_>,
2173 policy: &P,
2174 provider: &dyn crate::provider::CryptoProvider,
2175 xml_backend: crate::XmlBackend,
2176) -> Result<(), DsigError> {
2177 let resolver = resolver.with_external_resource_limits(
2178 policy.resources().max_external_resource_bytes,
2179 policy.resources().max_external_resource_total_bytes,
2180 );
2181 let mut xpath_parse_budget = XPathSignatureParseBudget::from_resources(policy.resources());
2182 let execution_budget = TransformExecutionBudget::from_resources(policy.resources());
2183 let mut budgets = RetrievalMaterializationBudgets {
2184 xpath_parse: &mut xpath_parse_budget,
2185 execution: &execution_budget,
2186 resources: policy.resources(),
2187 xml_backend,
2188 };
2189 let mut materialization = KeyInfoMaterializationState::default();
2190 materialize_key_info_references_with_budgets(
2193 key_info,
2194 &resolver,
2195 policy,
2196 provider,
2197 &mut budgets,
2198 &mut materialization,
2199 )?;
2200 Ok(())
2201}
2202
2203pub fn materialize_signing_key_info_references(
2220 key_info: &mut KeyInfo,
2221 resolver: UriReferenceResolver<'_>,
2222 policy: &crate::policy::SigningPolicy,
2223 provider: &dyn crate::provider::CryptoProvider,
2224 xml_backend: crate::XmlBackend,
2225) -> Result<(), DsigError> {
2226 policy.validate()?;
2227 materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
2228}
2229
2230pub fn materialize_verification_key_info_references(
2245 key_info: &mut KeyInfo,
2246 resolver: UriReferenceResolver<'_>,
2247 policy: &crate::policy::VerificationPolicy,
2248 provider: &dyn crate::provider::CryptoProvider,
2249 xml_backend: crate::XmlBackend,
2250) -> Result<(), DsigError> {
2251 policy.validate()?;
2252 materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
2253}
2254
2255fn materialize_retrieval_methods_with_budgets(
2256 key_info: &mut KeyInfo,
2257 resolver: &UriReferenceResolver<'_>,
2258 allowed_uri_types: UriTypeSet,
2259 allowed_transforms: Option<&HashSet<String>>,
2260 provider: &dyn crate::provider::CryptoProvider,
2261 budgets: &mut RetrievalMaterializationBudgets<'_>,
2262 candidate_work: &mut usize,
2263) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
2264 let retrieval_count = key_info
2265 .sources
2266 .iter()
2267 .filter(|source| matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. }))
2268 .count();
2269 if retrieval_count > MAX_RETRIEVAL_METHOD_COUNT {
2270 return Err(SignatureVerificationPipelineError::InvalidStructure {
2271 reason: "KeyInfo contains too many RetrievalMethod elements",
2272 });
2273 }
2274
2275 let mut total_binary_len = existing_x509_binary_len(key_info)?;
2276 let mut seen = HashSet::new();
2277 let mut materialized = Vec::with_capacity(key_info.sources.len());
2278 let mut outcome = RetrievalMaterialization::default();
2279 for source in std::mem::take(&mut key_info.sources) {
2280 let super::parse::KeyInfoSource::RetrievalMethod {
2281 uri: resolved_uri,
2282 resource_type,
2283 transforms,
2284 } = source
2285 else {
2286 materialized.push(source);
2287 continue;
2288 };
2289
2290 let identity = (
2291 resolved_uri.clone(),
2292 resource_type.clone(),
2293 transforms.clone(),
2294 );
2295 if !seen.insert(identity) {
2296 continue;
2297 }
2298
2299 if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#rawX509Certificate")
2300 {
2301 if transforms != RetrievalMethodTransforms::None
2302 || classify_uri(&resolved_uri) != UriClass::External
2303 {
2304 return Err(SignatureVerificationPipelineError::InvalidStructure {
2305 reason: "raw X509 RetrievalMethod requires an untransformed external URI",
2306 });
2307 }
2308 if !allowed_uri_types.allows(&resolved_uri) {
2309 return Err(crate::policy::PolicyViolation::Uri {
2310 operation: "verification",
2311 reason: "retrieval method URI class is not permitted",
2312 }
2313 .into());
2314 }
2315 let certificate = resolver.external_resource(&resolved_uri).map_err(|error| {
2316 SignatureVerificationPipelineError::from(ReferenceProcessingError::Transform(error))
2317 })?;
2318 let Some(certificate) = certificate else {
2319 outcome.deferred_error.get_or_insert_with(|| {
2320 SignatureVerificationPipelineError::Reference(
2321 ReferenceProcessingError::Transform(super::TransformError::UnsupportedUri(
2322 resolved_uri.clone(),
2323 )),
2324 )
2325 });
2326 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
2327 uri: resolved_uri,
2328 resource_type,
2329 transforms,
2330 });
2331 continue;
2332 };
2333 *candidate_work = candidate_work.saturating_add(1);
2334 budgets.resources.validate_key_candidates(*candidate_work)?;
2335 if certificate.len() > MAX_X509_DECODED_BINARY_LEN {
2336 return Err(SignatureVerificationPipelineError::InvalidStructure {
2337 reason: "raw X509 RetrievalMethod certificate exceeds maximum allowed length",
2338 });
2339 }
2340 add_retrieval_binary_usage(&mut total_binary_len, certificate.len())?;
2341 let parsed = match parse_x509_certificate(certificate) {
2342 Ok(parsed) => parsed,
2343 Err(error) => {
2344 let error = map_key_info_parse_error(error);
2345 if matches!(error, SignatureVerificationPipelineError::Policy(_)) {
2346 return Err(error);
2347 }
2348 outcome.deferred_error.get_or_insert(error);
2349 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
2350 uri: resolved_uri,
2351 resource_type,
2352 transforms,
2353 });
2354 continue;
2355 }
2356 };
2357 materialized.push(super::parse::KeyInfoSource::X509Data(
2358 super::parse::X509DataInfo {
2359 certificates: vec![certificate.to_vec()],
2360 parsed_certificates: vec![parsed],
2361 certificate_chain: vec![0],
2362 ..super::parse::X509DataInfo::default()
2363 },
2364 ));
2365 } else if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#X509Data") {
2366 if !allowed_uri_types.allows(&resolved_uri) {
2367 return Err(crate::policy::PolicyViolation::Uri {
2368 operation: "verification",
2369 reason: "retrieval method URI class is not permitted",
2370 }
2371 .into());
2372 }
2373 let target = resolver
2374 .node_for_same_document_reference(&resolved_uri)
2375 .map_err(ReferenceProcessingError::Transform)?;
2376 let Some(target) = target else {
2377 outcome.deferred_error.get_or_insert(
2378 SignatureVerificationPipelineError::InvalidStructure {
2379 reason: "X509Data RetrievalMethod target is missing or ambiguous",
2380 },
2381 );
2382 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
2383 uri: resolved_uri,
2384 resource_type,
2385 transforms,
2386 });
2387 continue;
2388 };
2389 let node = match transforms {
2390 RetrievalMethodTransforms::None
2391 if target.has_tag_name((XMLDSIG_NS, "X509Data")) =>
2392 {
2393 target
2394 }
2395 RetrievalMethodTransforms::None => {
2396 return Err(SignatureVerificationPipelineError::InvalidStructure {
2397 reason: "untransformed X509Data RetrievalMethod must target X509Data directly",
2398 });
2399 }
2400 RetrievalMethodTransforms::X509DataNodeSetFilter {
2401 expression,
2402 namespaces,
2403 } => {
2404 enforce_transform_allowed(allowed_transforms, XPATH_TRANSFORM_URI)?;
2405 budgets
2406 .xpath_parse
2407 .validate_expression(&expression)
2408 .map_err(ReferenceProcessingError::Transform)?;
2409 budgets
2410 .xpath_parse
2411 .validate_namespaces(&namespaces)
2412 .map_err(ReferenceProcessingError::Transform)?;
2413 select_retrieved_x509_data_root(target, budgets.execution)?
2414 }
2415 RetrievalMethodTransforms::Unsupported => {
2416 return Err(SignatureVerificationPipelineError::InvalidStructure {
2417 reason: "X509Data RetrievalMethod contains unsupported transforms",
2418 });
2419 }
2420 };
2421 let data = parse_x509_data_dispatch_with_budget_and_provider(
2422 node,
2423 &mut total_binary_len,
2424 candidate_work,
2425 provider,
2426 budgets.resources,
2427 )
2428 .map_err(map_key_info_parse_error)?;
2429 materialized.push(super::parse::KeyInfoSource::X509Data(data));
2430 } else {
2431 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
2432 uri: resolved_uri,
2433 resource_type,
2434 transforms,
2435 });
2436 }
2437 }
2438 key_info.sources = materialized;
2439 Ok(outcome)
2440}
2441
2442fn select_retrieved_x509_data_root<'a, 'input>(
2443 target: Node<'a, 'input>,
2444 execution_budget: &TransformExecutionBudget,
2445) -> Result<Node<'a, 'input>, SignatureVerificationPipelineError> {
2446 let context_nodes = NodeSet::ensure_subtree_materialization_fits_with_budget(
2451 target,
2452 false,
2453 execution_budget.node_set_materialization(),
2454 )
2455 .map_err(ReferenceProcessingError::Transform)?;
2456 execution_budget
2457 .validate_xpath_context_evaluations(context_nodes)
2458 .map_err(ReferenceProcessingError::Transform)?;
2459 execution_budget
2460 .charge_xpath_work(context_nodes)
2461 .map_err(ReferenceProcessingError::Transform)?;
2462 execution_budget
2463 .charge_node_filter_work(context_nodes)
2464 .map_err(ReferenceProcessingError::Transform)?;
2465 let mut root = None;
2466 for candidate in target.descendants() {
2467 if !candidate.is_element()
2468 || candidate.tag_name().namespace() != Some(XMLDSIG_NS)
2469 || candidate.tag_name().name() != "X509Data"
2470 {
2471 continue;
2472 }
2473 if root.replace(candidate).is_some() {
2474 return Err(SignatureVerificationPipelineError::InvalidStructure {
2475 reason: "X509Data RetrievalMethod selected multiple X509Data elements",
2476 });
2477 }
2478 }
2479 root.ok_or(SignatureVerificationPipelineError::InvalidStructure {
2480 reason: "X509Data RetrievalMethod selected no X509Data element",
2481 })
2482}
2483
2484#[cfg(test)]
2485fn materialize_retrieval_methods(
2486 key_info: &mut KeyInfo,
2487 resolver: &UriReferenceResolver<'_>,
2488 allowed_uri_types: UriTypeSet,
2489 allowed_transforms: Option<&HashSet<String>>,
2490 provider: &dyn crate::provider::CryptoProvider,
2491) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
2492 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
2493 let execution_budget = TransformExecutionBudget::default();
2494 let resources = crate::policy::ResourcePolicy::default();
2495 let mut budgets = RetrievalMaterializationBudgets {
2496 xpath_parse: &mut xpath_parse_budget,
2497 execution: &execution_budget,
2498 resources: &resources,
2499 xml_backend: crate::XmlBackend::default(),
2500 };
2501 let mut candidate_work = key_info.embedded_candidate_count();
2502 materialize_retrieval_methods_with_budgets(
2503 key_info,
2504 resolver,
2505 allowed_uri_types,
2506 allowed_transforms,
2507 provider,
2508 &mut budgets,
2509 &mut candidate_work,
2510 )
2511}
2512
2513fn existing_x509_binary_len(
2514 key_info: &KeyInfo,
2515) -> Result<usize, SignatureVerificationPipelineError> {
2516 let mut total = 0usize;
2517 for source in &key_info.sources {
2518 if let super::parse::KeyInfoSource::X509Data(info) = source {
2519 for len in info
2520 .certificates
2521 .iter()
2522 .chain(&info.skis)
2523 .chain(&info.crls)
2524 .map(Vec::len)
2525 .chain(info.digests.iter().map(|(_, digest)| digest.len()))
2526 {
2527 add_retrieval_binary_usage(&mut total, len)?;
2528 }
2529 }
2530 }
2531 Ok(total)
2532}
2533
2534fn add_retrieval_binary_usage(
2535 total: &mut usize,
2536 delta: usize,
2537) -> Result<(), SignatureVerificationPipelineError> {
2538 *total =
2539 total
2540 .checked_add(delta)
2541 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2542 reason: "RetrievalMethod X509Data binary length overflow",
2543 })?;
2544 if *total > MAX_X509_DATA_TOTAL_BINARY_LEN {
2545 return Err(SignatureVerificationPipelineError::InvalidStructure {
2546 reason: "RetrievalMethod X509Data exceeds maximum aggregate binary length",
2547 });
2548 }
2549 Ok(())
2550}
2551
2552fn manifest_reference_failure_reason(
2553 error: ReferenceProcessingError,
2554 ref_index: usize,
2555) -> FailureReason {
2556 match error.into_policy_violation() {
2557 Ok(_) => FailureReason::ReferencePolicyViolation { ref_index },
2558 Err(_) => FailureReason::ReferenceProcessingFailure { ref_index },
2559 }
2560}
2561
2562struct CompiledManifestReference {
2563 index: usize,
2564 reference: Reference,
2565 reference_node_id: NodeId,
2566}
2567
2568struct CompiledManifestInvalid {
2569 result: ReferenceResult,
2570}
2571
2572enum ScheduledManifestReference {
2573 Parsed {
2574 node: OperationNodeId,
2575 reference: CompiledManifestReference,
2576 },
2577 Invalid {
2578 node: OperationNodeId,
2579 invalid: CompiledManifestInvalid,
2580 },
2581}
2582
2583impl ScheduledManifestReference {
2584 fn index(&self) -> usize {
2585 match self {
2586 Self::Parsed { reference, .. } => reference.index,
2587 Self::Invalid { invalid, .. } => invalid.result.reference_index,
2588 }
2589 }
2590}
2591
2592fn process_authenticated_manifest_references(
2593 operation: &mut OperationExecutionContext<
2594 crate::policy::VerificationPolicy,
2595 VerificationOperationBudgets,
2596 >,
2597 view: DocumentView<'_>,
2598 signature_node: Node<'_, '_>,
2599 resolver: &UriReferenceResolver<'_>,
2600 ctx: &VerifyContext<'_>,
2601 mut remaining_reference_capacity: usize,
2602 crypto: OperationNodeId,
2603) -> Result<(Vec<ReferenceResult>, Vec<OperationNodeId>), SignatureVerificationPipelineError> {
2604 let mut processed_manifests = HashSet::new();
2605 let mut next_reference_index = 0usize;
2606 let mut frontier_index = 0usize;
2607 let mut completed_digests = Vec::new();
2608 let mut results = Vec::new();
2609 loop {
2610 operation.extend();
2611 let discovery = operation.add_node(
2612 OperationNodeKind::Manifest {
2613 index: frontier_index,
2614 },
2615 OperationStage::AuthenticatedDependency,
2616 None,
2617 );
2618 operation
2619 .add_dependency(discovery, crypto)
2620 .map_err(map_verification_plan_error)?;
2621 for digest in &completed_digests {
2622 operation
2623 .add_dependency(discovery, *digest)
2624 .map_err(map_verification_plan_error)?;
2625 }
2626 operation.compile().map_err(map_verification_plan_error)?;
2627 let parsed = operation.run(discovery, || {
2628 let mut xpath_parse = operation.budgets().xpath_parse.borrow_mut();
2629 let mut state = ManifestDiscoveryState {
2630 processed: &mut processed_manifests,
2631 remaining_capacity: &mut remaining_reference_capacity,
2632 next_reference_index: &mut next_reference_index,
2633 xpath_parse: &mut xpath_parse,
2634 };
2635 parse_manifest_references(
2636 signature_node,
2637 operation,
2638 view,
2639 &mut state,
2640 ctx.allowed_transform_uris(),
2641 )
2642 })?;
2643 let discovered = parsed.references.len() + parsed.invalid_results.len();
2644 if discovered == 0 {
2645 break;
2646 }
2647
2648 operation.extend();
2649 let mut scheduled = Vec::with_capacity(discovered);
2650 for item in parsed.references {
2651 let resource = operation.budgets().resource_identity_for_reference(
2652 &item.reference,
2653 item.index,
2654 resolver,
2655 view,
2656 );
2657 let node = operation.add_node(
2658 OperationNodeKind::Digest { index: item.index },
2659 OperationStage::AuthenticatedDependency,
2660 Some(resource.clone()),
2661 );
2662 operation
2663 .add_dependency(node, discovery)
2664 .map_err(map_verification_plan_error)?;
2665 scheduled.push(ScheduledManifestReference::Parsed {
2666 node,
2667 reference: item,
2668 });
2669 }
2670 for item in parsed.invalid_results {
2671 let node = operation.add_node(
2672 OperationNodeKind::Digest {
2673 index: item.result.reference_index,
2674 },
2675 OperationStage::AuthenticatedDependency,
2676 None,
2677 );
2678 operation
2679 .add_dependency(node, discovery)
2680 .map_err(map_verification_plan_error)?;
2681 scheduled.push(ScheduledManifestReference::Invalid {
2682 node,
2683 invalid: item,
2684 });
2685 }
2686 scheduled.sort_by_key(ScheduledManifestReference::index);
2687 operation.compile().map_err(map_verification_plan_error)?;
2688
2689 for scheduled_reference in scheduled {
2690 let (node, result) = match scheduled_reference {
2691 ScheduledManifestReference::Invalid { node, invalid } => {
2692 let result = operation.run(node, || {
2693 Ok::<_, SignatureVerificationPipelineError>(invalid.result)
2694 })?;
2695 (node, result)
2696 }
2697 ScheduledManifestReference::Parsed {
2698 node,
2699 reference: compiled,
2700 } => {
2701 let observed = operation.budgets().resource_identity_for_reference(
2702 &compiled.reference,
2703 compiled.index,
2704 resolver,
2705 view,
2706 );
2707 let result = operation.run_with_resource(node, &observed, || {
2708 let reference = &compiled.reference;
2709 let budgets = operation.budgets();
2710 let execution = ReferenceExecutionContext {
2711 store_pre_digest: ctx.store_pre_digest,
2712 transform_options: ctx.transform_options(),
2713 transform_budget: &budgets.transforms,
2714 canonicalized_data_budget: &budgets.canonicalized,
2715 provider: ctx.provider,
2716 };
2717 let result = if execution.transform_budget.remaining_c14n_output() == 0
2718 || reference.transforms.len()
2719 > ctx.policy.resources.max_transforms_per_reference
2720 || ctx
2721 .policy
2722 .digest_algorithms
2723 .as_ref()
2724 .is_some_and(|allowed| !allowed.contains(&reference.digest_method))
2725 {
2726 manifest_reference_invalid_result(
2727 reference,
2728 compiled.index,
2729 FailureReason::ReferencePolicyViolation {
2730 ref_index: compiled.index,
2731 },
2732 )
2733 } else {
2734 match enforce_reference_policies(
2735 std::slice::from_ref(reference),
2736 ctx.policy.uris.references,
2737 ctx.allowed_transform_uris(),
2738 ) {
2739 Ok(()) => process_reference_with_options(
2740 reference,
2741 resolver,
2742 signature_node,
2743 ReferenceSet::Manifest,
2744 compiled.index,
2745 resolver.node_for_node_id(compiled.reference_node_id),
2746 &execution,
2747 )
2748 .unwrap_or_else(|error| {
2749 manifest_reference_invalid_result(
2750 reference,
2751 compiled.index,
2752 manifest_reference_failure_reason(error, compiled.index),
2753 )
2754 }),
2755 Err(SignatureVerificationPipelineError::Policy(_)) => {
2756 manifest_reference_invalid_result(
2757 reference,
2758 compiled.index,
2759 FailureReason::ReferencePolicyViolation {
2760 ref_index: compiled.index,
2761 },
2762 )
2763 }
2764 Err(_) => manifest_reference_invalid_result(
2765 reference,
2766 compiled.index,
2767 FailureReason::ReferenceProcessingFailure {
2768 ref_index: compiled.index,
2769 },
2770 ),
2771 }
2772 };
2773 if result.status == DsigStatus::Valid
2774 && reference
2775 .transforms
2776 .iter()
2777 .all(transform_preserves_manifest_structure)
2778 && let Some(uri) = reference.uri.as_deref()
2779 && let Ok(Some(target)) =
2780 resolver.node_id_for_same_document_reference(uri)
2781 {
2782 operation.authenticate(view.node_identity_by_id(target));
2783 }
2784 Ok::<_, SignatureVerificationPipelineError>(result)
2785 })?;
2786 (node, result)
2787 }
2788 };
2789 let accepted = result.status == DsigStatus::Valid;
2790 operation.set_outcome(
2791 node,
2792 accepted,
2793 if accepted {
2794 OperationDecisionReason::ReferenceDigestVerified
2795 } else {
2796 OperationDecisionReason::ReferenceDigestRejected
2797 },
2798 );
2799 completed_digests.push(node);
2800 results.push(result);
2801 }
2802 frontier_index += 1;
2803 }
2804 results.sort_by_key(|result| result.reference_index);
2805 Ok((results, completed_digests))
2806}
2807
2808fn manifest_reference_invalid_result(
2809 reference: &Reference,
2810 index: usize,
2811 reason: FailureReason,
2812) -> ReferenceResult {
2813 ReferenceResult {
2814 reference_set: ReferenceSet::Manifest,
2815 reference_index: index,
2816 uri: reference
2817 .uri
2818 .clone()
2819 .unwrap_or_else(|| "<omitted>".to_owned()),
2820 digest_algorithm: reference.digest_method,
2821 status: DsigStatus::Invalid(reason),
2822 pre_digest_data: None,
2823 }
2824}
2825
2826fn parse_manifest_references(
2827 signature_node: Node<'_, '_>,
2828 operation: &OperationExecutionContext<
2829 crate::policy::VerificationPolicy,
2830 VerificationOperationBudgets,
2831 >,
2832 view: DocumentView<'_>,
2833 state: &mut ManifestDiscoveryState<'_>,
2834 allowed_transforms: Option<&HashSet<String>>,
2835) -> Result<ParsedManifestReferences, SignatureVerificationPipelineError> {
2836 let mut references = Vec::new();
2837 let mut invalid = Vec::new();
2838 for object_node in signature_node.children().filter(|node| {
2839 node.is_element()
2840 && node.tag_name().namespace() == Some(XMLDSIG_NS)
2841 && node.tag_name().name() == "Object"
2842 }) {
2843 let object_is_signed = operation.is_authenticated(view.node_identity(object_node));
2844 for manifest_node in object_node.children().filter(|node| {
2845 node.is_element()
2846 && node.tag_name().namespace() == Some(XMLDSIG_NS)
2847 && node.tag_name().name() == "Manifest"
2848 }) {
2849 let manifest_is_signed = operation.is_authenticated(view.node_identity(manifest_node));
2850 if !object_is_signed && !manifest_is_signed {
2853 continue;
2854 }
2855 if !state.processed.insert(manifest_node.id()) {
2856 continue;
2857 }
2858 let mut manifest_children = Vec::new();
2859 for child in manifest_node.children() {
2860 if child.is_text()
2861 && child.text().is_some_and(|text| {
2862 text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
2863 })
2864 {
2865 return Err(SignatureVerificationPipelineError::InvalidStructure {
2866 reason: "Manifest contains non-whitespace mixed content",
2867 });
2868 }
2869 if child.is_element() {
2870 manifest_children.push(child);
2871 }
2872 }
2873 if manifest_children.is_empty() {
2874 return Err(SignatureVerificationPipelineError::InvalidStructure {
2875 reason: "Manifest must contain at least one ds:Reference element child",
2876 });
2877 }
2878 for child in manifest_children {
2879 if child.tag_name().namespace() != Some(XMLDSIG_NS)
2880 || child.tag_name().name() != "Reference"
2881 {
2882 return Err(SignatureVerificationPipelineError::InvalidStructure {
2883 reason: "Manifest must contain only ds:Reference element children",
2884 });
2885 }
2886 if *state.remaining_capacity == 0 {
2887 return Err(SignatureVerificationPipelineError::InvalidStructure {
2888 reason: "signed Manifests exceed the per-signature Reference limit",
2889 });
2890 }
2891 *state.remaining_capacity -= 1;
2892 let reference_index = *state.next_reference_index;
2893 *state.next_reference_index += 1;
2894 match parse_reference_with_xpath_budget(child, state.xpath_parse) {
2895 Ok(reference) => references.push(CompiledManifestReference {
2896 index: reference_index,
2897 reference,
2898 reference_node_id: child.id(),
2899 }),
2900 Err(ParseError::Transform(super::TransformError::UnsupportedTransform(
2901 uri,
2902 ))) => {
2903 let digest_algorithm =
2904 reference_digest_method(child).map_err(map_manifest_parse_error)?;
2905 let reason =
2906 if allowed_transforms.is_some_and(|allowed| !allowed.contains(&uri)) {
2907 FailureReason::ReferencePolicyViolation {
2908 ref_index: reference_index,
2909 }
2910 } else {
2911 FailureReason::ReferenceProcessingFailure {
2912 ref_index: reference_index,
2913 }
2914 };
2915 invalid.push(CompiledManifestInvalid {
2916 result: ReferenceResult {
2917 reference_set: ReferenceSet::Manifest,
2918 reference_index,
2919 uri: child.attribute("URI").unwrap_or("<omitted>").to_owned(),
2920 digest_algorithm,
2921 status: DsigStatus::Invalid(reason),
2922 pre_digest_data: None,
2923 },
2924 });
2925 }
2926 Err(error) => return Err(map_manifest_parse_error(error)),
2927 }
2928 }
2929 }
2930 }
2931 Ok(ParsedManifestReferences {
2932 references,
2933 invalid_results: invalid,
2934 })
2935}
2936
2937struct ParsedManifestReferences {
2938 references: Vec<CompiledManifestReference>,
2939 invalid_results: Vec<CompiledManifestInvalid>,
2940}
2941
2942struct ManifestDiscoveryState<'a> {
2943 processed: &'a mut HashSet<NodeId>,
2944 remaining_capacity: &'a mut usize,
2945 next_reference_index: &'a mut usize,
2946 xpath_parse: &'a mut XPathSignatureParseBudget,
2947}
2948
2949fn transform_preserves_manifest_structure(transform: &Transform) -> bool {
2950 match transform {
2951 Transform::C14n(_) => true,
2952 Transform::Enveloped
2957 | Transform::XpathExcludeAllSignatures
2958 | Transform::XPath(_)
2959 | Transform::XPathFilter2(_)
2960 | Transform::Base64Decode => false,
2961 }
2962}
2963
2964enum ResolvedVerifyingKey<'a> {
2965 Borrowed(&'a dyn VerifyingKey),
2966 Owned(Box<dyn VerifyingKey + 'a>),
2967}
2968
2969impl ResolvedVerifyingKey<'_> {
2970 fn as_ref(&self) -> &dyn VerifyingKey {
2971 match self {
2972 Self::Borrowed(key) => *key,
2973 Self::Owned(key) => key.as_ref(),
2974 }
2975 }
2976}
2977
2978fn resolve_verifying_key<'k>(
2979 ctx: &VerifyContext<'k>,
2980 key_info: Option<&KeyInfo>,
2981 algorithm: SignatureAlgorithm,
2982) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
2983 if let Some(key) = ctx.key {
2984 if !ctx.policy.key_sources.preset_key {
2985 return Err(crate::policy::PolicyViolation::KeyTrust {
2986 reason: "pre-resolved verification keys are disabled",
2987 }
2988 .into());
2989 }
2990 require_verifying_key_candidate_capacity(&ctx.policy)?;
2991 return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
2992 }
2993 if let Some(resolver) = ctx.key_resolver {
2994 require_verifying_key_candidate_capacity(&ctx.policy)?;
2995 let resolved = resolver.resolve_with_policy_and_provider(
2996 key_info,
2997 algorithm,
2998 &ctx.policy,
2999 ctx.provider,
3000 )?;
3001 return Ok(resolved.map(ResolvedVerifyingKey::Owned));
3002 }
3003 Ok(None)
3004}
3005
3006fn require_verifying_key_candidate_capacity(
3007 policy: &crate::policy::VerificationPolicy,
3008) -> Result<(), SignatureVerificationPipelineError> {
3009 policy
3010 .resources
3011 .validate_key_candidates(1)
3012 .map_err(Into::into)
3013}
3014
3015fn enforce_reference_policies(
3016 references: &[Reference],
3017 allowed_uri_types: UriTypeSet,
3018 allowed_transforms: Option<&HashSet<String>>,
3019) -> Result<(), SignatureVerificationPipelineError> {
3020 for reference in references {
3021 let uri = reference
3022 .uri
3023 .as_deref()
3024 .ok_or(SignatureVerificationPipelineError::Reference(
3025 ReferenceProcessingError::MissingUri,
3026 ))?;
3027 if !allowed_uri_types.allows(uri) {
3028 return Err(crate::policy::PolicyViolation::Uri {
3029 operation: "verification",
3030 reason: "reference URI class is not permitted",
3031 }
3032 .into());
3033 }
3034
3035 if let Some(allowed) = allowed_transforms {
3036 for transform in &reference.transforms {
3037 let transform_uri = transform.algorithm_uri();
3038 enforce_transform_allowed(Some(allowed), transform_uri)?;
3039 }
3040
3041 let produces_binary = transform_chain_produces_binary(
3046 classify_uri(uri) == UriClass::External,
3047 &reference.transforms,
3048 );
3049 if !produces_binary {
3050 enforce_transform_allowed(Some(allowed), DEFAULT_IMPLICIT_C14N_URI)?;
3051 }
3052 }
3053 }
3054 Ok(())
3055}
3056
3057fn enforce_transform_allowed(
3058 allowed_transforms: Option<&HashSet<String>>,
3059 algorithm: &str,
3060) -> Result<(), SignatureVerificationPipelineError> {
3061 if allowed_transforms.is_some_and(|allowed| !allowed.contains(algorithm)) {
3062 return Err(crate::policy::PolicyViolation::Algorithm {
3063 operation: "verification transform",
3064 algorithm: algorithm.to_owned(),
3065 }
3066 .into());
3067 }
3068 Ok(())
3069}
3070
3071#[derive(Debug, Clone, Copy)]
3072pub(super) struct SignatureChildNodes<'a, 'input> {
3073 signed_info_node: Node<'a, 'input>,
3074 signature_value_node: Node<'a, 'input>,
3075 key_info_node: Option<Node<'a, 'input>>,
3076}
3077
3078pub(super) fn parse_signature_children<'a, 'input>(
3079 signature_node: Node<'a, 'input>,
3080) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
3081 let mut signed_info_node: Option<Node<'_, '_>> = None;
3082 let mut signature_value_node: Option<Node<'_, '_>> = None;
3083 let mut key_info_node: Option<Node<'_, '_>> = None;
3084 let mut signed_info_index: Option<usize> = None;
3085 let mut signature_value_index: Option<usize> = None;
3086 let mut key_info_index: Option<usize> = None;
3087 let mut first_unexpected_dsig_index: Option<usize> = None;
3088
3089 let mut element_index = 0usize;
3090 for child in signature_node.children() {
3091 if child.is_text() {
3092 if child
3093 .text()
3094 .is_some_and(|text| !is_xml_whitespace_only(text))
3095 {
3096 return Err(SignatureVerificationPipelineError::InvalidStructure {
3097 reason: "Signature must not contain non-whitespace mixed content",
3098 });
3099 }
3100 continue;
3101 }
3102 if !child.is_element() {
3103 continue;
3104 }
3105
3106 element_index += 1;
3107 if child.tag_name().namespace() != Some(XMLDSIG_NS) {
3108 return Err(SignatureVerificationPipelineError::InvalidStructure {
3109 reason: "Signature must contain only XMLDSIG element children",
3110 });
3111 }
3112 match child.tag_name().name() {
3113 "SignedInfo" => {
3114 if signed_info_node.is_some() {
3115 return Err(SignatureVerificationPipelineError::InvalidStructure {
3116 reason: "SignedInfo must appear exactly once under Signature",
3117 });
3118 }
3119 signed_info_node = Some(child);
3120 signed_info_index = Some(element_index);
3121 }
3122 "SignatureValue" => {
3123 if signature_value_node.is_some() {
3124 return Err(SignatureVerificationPipelineError::InvalidStructure {
3125 reason: "SignatureValue must appear exactly once under Signature",
3126 });
3127 }
3128 signature_value_node = Some(child);
3129 signature_value_index = Some(element_index);
3130 }
3131 "KeyInfo" => {
3132 if key_info_node.is_some() {
3133 return Err(SignatureVerificationPipelineError::InvalidStructure {
3134 reason: "KeyInfo must appear at most once under Signature",
3135 });
3136 }
3137 key_info_node = Some(child);
3138 key_info_index = Some(element_index);
3139 }
3140 "Object" => {
3141 }
3144 _ => {
3145 if first_unexpected_dsig_index.is_none() {
3146 first_unexpected_dsig_index = Some(element_index);
3147 }
3148 }
3149 }
3150 }
3151
3152 let signed_info_node =
3153 signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
3154 element: "SignedInfo",
3155 })?;
3156 let signature_value_node =
3157 signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
3158 element: "SignatureValue",
3159 })?;
3160 if signed_info_index != Some(1) {
3161 return Err(SignatureVerificationPipelineError::InvalidStructure {
3162 reason: "SignedInfo must be the first element child of Signature",
3163 });
3164 }
3165 if signature_value_index != Some(2) {
3166 return Err(SignatureVerificationPipelineError::InvalidStructure {
3167 reason: "SignatureValue must be the second element child of Signature",
3168 });
3169 }
3170 if let Some(index) = key_info_index
3171 && index != 3
3172 {
3173 return Err(SignatureVerificationPipelineError::InvalidStructure {
3174 reason: "KeyInfo must be the third element child of Signature when present",
3175 });
3176 }
3177
3178 let allowed_prefix_end = key_info_index.unwrap_or(2);
3179 if let Some(unexpected_index) = first_unexpected_dsig_index {
3180 return Err(SignatureVerificationPipelineError::InvalidStructure {
3181 reason: if unexpected_index > allowed_prefix_end {
3182 "After SignedInfo, SignatureValue, and optional KeyInfo, Signature may contain only Object elements"
3183 } else {
3184 "Signature may contain SignedInfo first, SignatureValue second, optional KeyInfo third, and Object elements thereafter"
3185 },
3186 });
3187 }
3188
3189 Ok(SignatureChildNodes {
3190 signed_info_node,
3191 signature_value_node,
3192 key_info_node,
3193 })
3194}
3195
3196fn decode_signature_value(
3197 signature_value_node: Node<'_, '_>,
3198) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
3199 if signature_value_node
3200 .children()
3201 .any(|child| child.is_element())
3202 {
3203 return Err(SignatureVerificationPipelineError::InvalidStructure {
3204 reason: "SignatureValue must not contain element children",
3205 });
3206 }
3207
3208 let mut normalized = Vec::new();
3209 let mut raw_text_len = 0usize;
3210 for child in signature_value_node
3211 .children()
3212 .filter(|child| child.is_text())
3213 {
3214 if let Some(text) = child.text() {
3215 push_normalized_signature_text(text, &mut raw_text_len, &mut normalized)?;
3216 }
3217 }
3218
3219 Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
3220}
3221
3222fn push_normalized_signature_text(
3223 text: &str,
3224 raw_text_len: &mut usize,
3225 normalized: &mut Vec<u8>,
3226) -> Result<(), SignatureVerificationPipelineError> {
3227 if raw_text_len.saturating_add(text.len()) > MAX_SIGNATURE_VALUE_TEXT_LEN {
3228 return Err(SignatureVerificationPipelineError::InvalidStructure {
3229 reason: "SignatureValue exceeds maximum allowed text length",
3230 });
3231 }
3232 *raw_text_len = raw_text_len.saturating_add(text.len());
3233
3234 normalize_xml_base64_bytes(text.as_bytes(), normalized, |_| true).map_err(|err| {
3235 SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(
3236 err.normalized_offset,
3237 err.invalid_byte,
3238 ))
3239 })?;
3240 if normalized.len() > MAX_SIGNATURE_VALUE_LEN {
3241 return Err(SignatureVerificationPipelineError::InvalidStructure {
3242 reason: "SignatureValue exceeds maximum allowed length",
3243 });
3244 }
3245
3246 Ok(())
3247}
3248
3249fn verify_with_algorithm(
3250 algorithm: SignatureAlgorithm,
3251 public_key_pem: &str,
3252 signed_data: &[u8],
3253 signature_value: &[u8],
3254) -> Result<bool, SignatureVerificationPipelineError> {
3255 match algorithm {
3256 SignatureAlgorithm::DsaSha1 | SignatureAlgorithm::DsaSha256 => {
3257 let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
3258 .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
3259 if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
3260 return Err(SignatureVerificationError::InvalidKeyPem.into());
3261 }
3262 Ok(verify_dsa_signature_spki(
3263 algorithm,
3264 &pem.contents,
3265 signed_data,
3266 signature_value,
3267 )?)
3268 }
3269 SignatureAlgorithm::HmacSha1
3270 | SignatureAlgorithm::HmacSha224
3271 | SignatureAlgorithm::HmacSha256
3272 | SignatureAlgorithm::HmacSha384
3273 | SignatureAlgorithm::HmacSha512 => Err(SignatureVerificationError::UnsupportedAlgorithm {
3274 uri: algorithm.uri().to_string(),
3275 }
3276 .into()),
3277 SignatureAlgorithm::RsaSha1
3278 | SignatureAlgorithm::RsaSha224
3279 | SignatureAlgorithm::RsaSha256
3280 | SignatureAlgorithm::RsaSha384
3281 | SignatureAlgorithm::RsaSha512 => Ok(verify_rsa_signature_pem(
3282 algorithm,
3283 public_key_pem,
3284 signed_data,
3285 signature_value,
3286 )?),
3287 SignatureAlgorithm::EcdsaSha1
3288 | SignatureAlgorithm::EcdsaSha224
3289 | SignatureAlgorithm::EcdsaSha256
3290 | SignatureAlgorithm::EcdsaSha384
3291 | SignatureAlgorithm::EcdsaSha512 => {
3292 match verify_ecdsa_signature_pem(
3296 algorithm,
3297 public_key_pem,
3298 signed_data,
3299 signature_value,
3300 ) {
3301 Ok(valid) => Ok(valid),
3302 Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
3303 Err(error) => Err(error.into()),
3304 }
3305 }
3306 }
3307}
3308
3309#[cfg(test)]
3310#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
3311mod tests {
3312 use super::*;
3313 use crate::c14n::C14nAlgorithm;
3314 use crate::xml::dom::Document;
3315 use crate::xmldsig::TransformError;
3316 use crate::xmldsig::digest::DigestAlgorithm;
3317 use crate::xmldsig::parse::{Reference, parse_signed_info};
3318 use crate::xmldsig::transforms::Transform;
3319 use crate::xmldsig::uri::UriReferenceResolver;
3320 use base64::Engine;
3321
3322 fn make_reference(
3326 uri: &str,
3327 transforms: Vec<Transform>,
3328 digest_method: DigestAlgorithm,
3329 digest_value: Vec<u8>,
3330 ) -> Reference {
3331 Reference {
3332 uri: Some(uri.to_string()),
3333 id: None,
3334 ref_type: None,
3335 transforms,
3336 digest_method,
3337 digest_value,
3338 }
3339 }
3340
3341 #[test]
3342 fn reference_resolution_uses_each_elements_effective_xml_base() {
3343 let first = b"first payload";
3346 let second = b"second payload";
3347 let first_digest = base64::engine::general_purpose::STANDARD
3348 .encode(compute_digest(DigestAlgorithm::Sha256, first));
3349 let second_digest = base64::engine::general_purpose::STANDARD
3350 .encode(compute_digest(DigestAlgorithm::Sha256, second));
3351 let xml = format!(
3352 r#"<root xml:base="https://example.test/base/" xmlns:ds="{XMLDSIG_NS}">
3353 <ds:Signature><ds:SignedInfo>
3354 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3355 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3356 <ds:Reference xml:base="one/" URI="payload.bin">
3357 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3358 <ds:DigestValue>{first_digest}</ds:DigestValue>
3359 </ds:Reference>
3360 <ds:Reference xml:base="../two/" URI="payload.bin">
3361 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3362 <ds:DigestValue>{second_digest}</ds:DigestValue>
3363 </ds:Reference>
3364 </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>
3365 </root>"#
3366 );
3367 let document = Document::parse(&xml).unwrap();
3368 let signature = document
3369 .descendants()
3370 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
3371 .unwrap();
3372 let signed_info_node = signature
3373 .children()
3374 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
3375 .unwrap();
3376 let signed_info = parse_signed_info(signed_info_node).unwrap();
3377 let resources = HashMap::from([
3378 (
3379 "https://example.test/base/one/payload.bin".into(),
3380 first.to_vec(),
3381 ),
3382 (
3383 "https://example.test/two/payload.bin".into(),
3384 second.to_vec(),
3385 ),
3386 ]);
3387 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
3388
3389 let result = process_all_references(&signed_info.references, &resolver, signature, false)
3390 .expect("each Reference should resolve against its own effective base");
3391
3392 assert!(result.all_valid());
3393 }
3394
3395 #[test]
3396 fn internal_dtd_opt_in_applies_to_detached_xml_transforms() {
3397 let detached = b"<!DOCTYPE payload [<!ELEMENT payload (#PCDATA)>]><payload>ok</payload>";
3400 let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
3401 DigestAlgorithm::Sha256,
3402 b"<payload>ok</payload>",
3403 ));
3404 let xml = format!(
3405 r#"<root xmlns:ds="{XMLDSIG_NS}">
3406 <ds:Signature>
3407 <ds:SignedInfo>
3408 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3409 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3410 <ds:Reference URI="urn:detached-dtd">
3411 <ds:Transforms>
3412 <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
3413 </ds:Transforms>
3414 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3415 <ds:DigestValue>{digest}</ds:DigestValue>
3416 </ds:Reference>
3417 </ds:SignedInfo>
3418 <ds:SignatureValue>AQ==</ds:SignatureValue>
3419 </ds:Signature>
3420</root>"#
3421 );
3422 let resources = HashMap::from([("urn:detached-dtd".to_owned(), detached.to_vec())]);
3423 let key = AcceptingKey;
3424
3425 let default_error = VerifyContext::new()
3426 .key(&key)
3427 .allowed_uri_types(UriTypeSet::ALL)
3428 .external_resources(&resources)
3429 .verify(&xml)
3430 .expect_err("internal DTD parsing must remain disabled by default");
3431 assert!(matches!(
3432 default_error,
3433 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
3434 crate::xmldsig::TransformError::XmlParse(_)
3435 ))
3436 ));
3437
3438 let result = VerifyContext::new()
3439 .key(&key)
3440 .allowed_uri_types(UriTypeSet::ALL)
3441 .external_resources(&resources)
3442 .allow_internal_dtd(true)
3443 .verify(&xml)
3444 .expect("the explicit DTD opt-in must cover detached XML transforms");
3445
3446 assert_eq!(result.status, DsigStatus::Valid);
3447
3448 let external_entity = br#"<!DOCTYPE payload [
3449 <!ENTITY ext SYSTEM "file:///etc/passwd">
3450 ]><payload>&ext;</payload>"#;
3451 let external_entity_resources =
3452 HashMap::from([("urn:detached-dtd".to_owned(), external_entity.to_vec())]);
3453 let external_entity_error = VerifyContext::new()
3454 .key(&key)
3455 .allowed_uri_types(UriTypeSet::ALL)
3456 .external_resources(&external_entity_resources)
3457 .allow_internal_dtd(true)
3458 .verify(&xml)
3459 .expect_err("the internal-DTD opt-in must not resolve external entities");
3460 assert!(matches!(
3461 external_entity_error,
3462 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
3463 crate::xmldsig::TransformError::XmlParse(_)
3464 ))
3465 ));
3466 }
3467
3468 #[test]
3469 fn owned_verification_revalidates_internal_dtd_provenance() {
3470 let document = XmlDocument::parse_with_settings(
3473 "<!DOCTYPE root [<!ENTITY value \"ok\">]><root>&value;</root>".into(),
3474 DocumentParseSettings::new(
3475 true,
3476 crate::hard_limits::XML_DOCUMENT_NODE_CEILING,
3477 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
3478 ),
3479 )
3480 .expect("explicitly permitted DTD fixture must parse");
3481
3482 assert!(matches!(
3483 VerifyContext::new().verify_document(&document),
3484 Err(DsigError::Policy(
3485 crate::policy::PolicyViolation::XmlInput {
3486 reason: "owned document requires internal DTD support"
3487 }
3488 ))
3489 ));
3490 assert!(!matches!(
3491 VerifyContext::new()
3492 .allow_internal_dtd(true)
3493 .verify_document(&document),
3494 Err(DsigError::Policy(
3495 crate::policy::PolicyViolation::XmlInput { .. }
3496 ))
3497 ));
3498 }
3499
3500 #[test]
3501 fn verification_policy_bounds_reference_canonicalization() {
3502 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
3505 let xml = format!(
3506 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>"#,
3507 "payload".repeat(16)
3508 );
3509 let policy = crate::policy::VerificationPolicy {
3510 resources: crate::policy::ResourcePolicy {
3511 max_canonicalized_bytes: 64,
3512 ..crate::policy::ResourcePolicy::default()
3513 },
3514 ..crate::policy::VerificationPolicy::default()
3515 };
3516
3517 let error = VerifyContext::new()
3518 .key(&AcceptingKey)
3519 .policy(policy)
3520 .verify(&xml)
3521 .expect_err("reference canonicalization must consume the policy budget");
3522
3523 assert!(
3524 matches!(
3525 error,
3526 SignatureVerificationPipelineError::Policy(
3527 crate::policy::PolicyViolation::ResourceLimit {
3528 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
3529 maximum: 64,
3530 ..
3531 }
3532 )
3533 ),
3534 "unexpected error: {error:?}"
3535 );
3536 }
3537
3538 #[test]
3539 fn verification_policy_bounds_document_bytes_before_parsing() {
3540 let xml = format!("<root>{}</root>", "x".repeat(1_024));
3543 let policy = crate::policy::VerificationPolicy {
3544 resources: crate::policy::ResourcePolicy {
3545 max_xml_document_bytes: xml.len() - 1,
3546 ..crate::policy::ResourcePolicy::default()
3547 },
3548 ..crate::policy::VerificationPolicy::default()
3549 };
3550
3551 assert!(matches!(
3552 VerifyContext::new().policy(policy).verify(&xml),
3553 Err(SignatureVerificationPipelineError::Policy(
3554 crate::policy::PolicyViolation::ResourceLimit {
3555 resource: crate::policy::resource_name::XML_DOCUMENT,
3556 maximum,
3557 actual,
3558 }
3559 )) if maximum == xml.len() - 1 && actual == xml.len()
3560 ));
3561 }
3562
3563 #[test]
3564 fn verification_entry_points_enforce_policy_depth() {
3565 let xml = "<root><child><leaf/></child></root>";
3568 let policy = crate::policy::VerificationPolicy {
3569 resources: crate::policy::ResourcePolicy {
3570 max_xml_depth: 2,
3571 ..crate::policy::ResourcePolicy::default()
3572 },
3573 ..crate::policy::VerificationPolicy::default()
3574 };
3575 let document = XmlDocument::parse(xml).expect("wide retained fixture must parse");
3576
3577 assert!(matches!(
3578 VerifyContext::new().policy(policy.clone()).verify(xml),
3579 Err(DsigError::Policy(
3580 crate::policy::PolicyViolation::ResourceLimit {
3581 resource: crate::policy::resource_name::XML_DEPTH,
3582 maximum: 2,
3583 actual: 3,
3584 }
3585 ))
3586 ));
3587 assert!(matches!(
3588 VerifyContext::new()
3589 .policy(policy)
3590 .verify_document(&document),
3591 Err(DsigError::Policy(
3592 crate::policy::PolicyViolation::ResourceLimit {
3593 resource: crate::policy::resource_name::XML_DEPTH,
3594 maximum: 2,
3595 actual: 3,
3596 }
3597 ))
3598 ));
3599 }
3600
3601 #[test]
3602 fn verification_policy_bounds_base64_transform_input() {
3603 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
3606 let xml = format!(
3607 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>"##
3608 );
3609 let policy = crate::policy::VerificationPolicy {
3610 resources: crate::policy::ResourcePolicy {
3611 max_base64_transform_input_bytes: 4,
3612 ..crate::policy::ResourcePolicy::default()
3613 },
3614 ..crate::policy::VerificationPolicy::default()
3615 };
3616
3617 let error = VerifyContext::new()
3618 .key(&AcceptingKey)
3619 .policy(policy)
3620 .verify(&xml)
3621 .expect_err("Base64 input must use the operation policy ceiling");
3622
3623 assert!(matches!(
3624 error,
3625 SignatureVerificationPipelineError::Policy(
3626 crate::policy::PolicyViolation::ResourceLimit {
3627 resource: crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
3628 maximum: 4,
3629 ..
3630 }
3631 )
3632 ));
3633 }
3634
3635 #[test]
3636 fn verification_policy_bounds_cumulative_base64_transform_output() {
3637 let first_digest = base64::engine::general_purpose::STANDARD
3640 .encode(compute_digest(DigestAlgorithm::Sha256, b"a"));
3641 let second_digest = base64::engine::general_purpose::STANDARD
3642 .encode(compute_digest(DigestAlgorithm::Sha256, b"b"));
3643 let xml = format!(
3644 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>"##
3645 );
3646 let policy = crate::policy::VerificationPolicy {
3647 resources: crate::policy::ResourcePolicy {
3648 max_base64_transform_input_bytes: 8,
3649 max_base64_transform_output_bytes: 1,
3650 ..crate::policy::ResourcePolicy::default()
3651 },
3652 ..crate::policy::VerificationPolicy::default()
3653 };
3654
3655 let error = VerifyContext::new()
3656 .key(&AcceptingKey)
3657 .policy(policy)
3658 .verify(&xml)
3659 .expect_err("references must share the Base64 output allowance");
3660
3661 assert!(matches!(
3662 error,
3663 SignatureVerificationPipelineError::Policy(
3664 crate::policy::PolicyViolation::ResourceLimit {
3665 resource: crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
3666 maximum: 1,
3667 actual: 2,
3668 }
3669 )
3670 ));
3671 }
3672
3673 #[test]
3674 fn verification_policy_bounds_xpath_source_before_compilation() {
3675 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
3678 let xml = format!(
3679 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>"#
3680 );
3681 let policy = crate::policy::VerificationPolicy {
3682 resources: crate::policy::ResourcePolicy {
3683 max_xpath_expression_bytes: 4,
3684 ..crate::policy::ResourcePolicy::default()
3685 },
3686 ..crate::policy::VerificationPolicy::default()
3687 };
3688
3689 let error = VerifyContext::new()
3690 .key(&AcceptingKey)
3691 .policy(policy)
3692 .verify(&xml)
3693 .expect_err("XPath source must use the operation policy ceiling");
3694
3695 assert!(matches!(
3696 error,
3697 SignatureVerificationPipelineError::Policy(
3698 crate::policy::PolicyViolation::ResourceLimit {
3699 resource: crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
3700 maximum: 4,
3701 ..
3702 }
3703 )
3704 ));
3705 }
3706
3707 #[test]
3708 fn verification_policy_shares_canonicalization_budget_with_signed_info() {
3709 let payload_text = "x".repeat(700);
3713 let canonical_payload = format!("<payload ID=\"payload\">{payload_text}</payload>");
3714 let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
3715 DigestAlgorithm::Sha256,
3716 canonical_payload.as_bytes(),
3717 ));
3718 let xml = format!(
3719 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>"##
3720 );
3721 let policy = crate::policy::VerificationPolicy {
3722 resources: crate::policy::ResourcePolicy {
3723 max_canonicalized_bytes: 1_024,
3724 ..crate::policy::ResourcePolicy::default()
3725 },
3726 ..crate::policy::VerificationPolicy::default()
3727 };
3728
3729 let error = VerifyContext::new()
3730 .key(&AcceptingKey)
3731 .policy(policy)
3732 .verify(&xml)
3733 .expect_err("SignedInfo must consume the remaining operation C14N budget");
3734
3735 assert!(
3736 matches!(
3737 &error,
3738 SignatureVerificationPipelineError::Policy(
3739 crate::policy::PolicyViolation::ResourceLimit {
3740 resource: "canonicalized bytes",
3741 maximum: 1_024,
3742 ..
3743 }
3744 )
3745 ),
3746 "unexpected error: {error:?}"
3747 );
3748 }
3749
3750 #[test]
3751 fn manifest_processing_stops_after_c14n_budget_exhaustion() {
3752 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
3755 let xml = format!(
3756 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>"##
3757 );
3758 let resources = HashMap::from([("urn:small".to_owned(), b"small".to_vec())]);
3759 let transform_budget = TransformExecutionBudget::with_c14n_limit(8);
3760 let ctx = VerifyContext::new()
3761 .allowed_uri_types(UriTypeSet::ALL)
3762 .external_resources(&resources);
3763 let document = XmlDocument::parse(xml).expect("test signature must parse");
3764 let results = document
3765 .with_view(|view| {
3766 let signature = view
3767 .document()
3768 .descendants()
3769 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
3770 .expect("test signature must contain Signature");
3771 let object = signature
3772 .children()
3773 .find(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
3774 .expect("test signature must contain Object");
3775 let resolver = UriReferenceResolver::with_document_view(view, &[])
3776 .with_external_resources(&resources);
3777 let budgets =
3778 VerificationOperationBudgets::with_transforms(&ctx.policy, transform_budget);
3779 let mut operation = OperationExecutionContext::new(
3780 ctx.policy.clone(),
3781 budgets,
3782 Some((view.identity(), view.generation())),
3783 );
3784 let crypto =
3785 operation.add_node(OperationNodeKind::Crypto, OperationStage::Crypto, None);
3786 operation.compile().expect("base plan");
3787 operation
3788 .run(crypto, || Ok::<_, SignatureVerificationPipelineError>(()))
3789 .expect("crypto gate");
3790 operation.authenticate(view.node_identity(object));
3791 process_authenticated_manifest_references(
3792 &mut operation,
3793 view,
3794 signature,
3795 &resolver,
3796 &ctx,
3797 2,
3798 crypto,
3799 )
3800 .map(|(results, _)| results)
3801 })
3802 .expect("resource exhaustion is reported per Manifest reference");
3803
3804 assert_eq!(results.len(), 2);
3805 assert!(results.iter().all(|result| {
3806 matches!(
3807 result.status,
3808 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { .. })
3809 )
3810 }));
3811 }
3812
3813 #[test]
3814 fn verification_policy_bounds_detached_xml_nodes() {
3815 let detached = format!("<payload>{}</payload>", "<n/>".repeat(32));
3818 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
3819 let xml = format!(
3820 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>"#
3821 );
3822 let resources = HashMap::from([("urn:detached-nodes".to_owned(), detached.into_bytes())]);
3823 let policy = crate::policy::VerificationPolicy {
3824 uris: crate::policy::UriPolicy {
3825 references: UriTypeSet::ALL,
3826 ..crate::policy::UriPolicy::default()
3827 },
3828 resources: crate::policy::ResourcePolicy {
3829 max_xml_nodes: 24,
3830 ..crate::policy::ResourcePolicy::default()
3831 },
3832 ..crate::policy::VerificationPolicy::default()
3833 };
3834
3835 let error = VerifyContext::new()
3836 .key(&AcceptingKey)
3837 .policy(policy)
3838 .external_resources(&resources)
3839 .verify(&xml)
3840 .expect_err("detached XML must inherit the policy node ceiling");
3841
3842 assert!(
3843 matches!(
3844 error,
3845 SignatureVerificationPipelineError::Policy(
3846 crate::policy::PolicyViolation::ResourceLimit {
3847 resource: crate::policy::resource_name::XML_NODES,
3848 maximum: 24,
3849 ..
3850 }
3851 )
3852 ),
3853 "unexpected error: {error:?}"
3854 );
3855 }
3856
3857 #[test]
3858 fn query_only_reference_resolves_against_relative_xml_base() {
3859 let payload = b"query-selected payload";
3862 let digest = base64::engine::general_purpose::STANDARD
3863 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
3864 let xml = format!(
3865 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo>
3866 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3867 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3868 <ds:Reference xml:base="a/b?old" URI="?new">
3869 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3870 <ds:DigestValue>{digest}</ds:DigestValue>
3871 </ds:Reference>
3872 </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>"#
3873 );
3874 let document = Document::parse(&xml).unwrap();
3875 let signature = document.root_element();
3876 let signed_info_node = signature
3877 .children()
3878 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
3879 .unwrap();
3880 let signed_info = parse_signed_info(signed_info_node).unwrap();
3881 let resources = HashMap::from([("a/b?new".to_string(), payload.to_vec())]);
3882 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
3883
3884 let result = process_all_references(&signed_info.references, &resolver, signature, false)
3885 .expect("query-only URI must resolve against the complete relative base path");
3886
3887 assert!(result.all_valid());
3888 }
3889
3890 #[test]
3891 fn manifest_reference_resolution_uses_its_effective_xml_base() {
3892 let payload = b"manifest payload";
3895 let digest = base64::engine::general_purpose::STANDARD
3896 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
3897 let xml = format!(
3898 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
3899 <ds:Object><ds:Manifest xml:base="manifests/">
3900 <ds:Reference URI="payload.bin">
3901 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3902 <ds:DigestValue>{digest}</ds:DigestValue>
3903 </ds:Reference>
3904 </ds:Manifest></ds:Object>
3905 </ds:Signature>"#
3906 );
3907 let document = Document::parse(&xml).unwrap();
3908 let signature = document.root_element();
3909 let reference_node = signature
3910 .descendants()
3911 .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
3912 .unwrap();
3913 let reference = super::super::parse::parse_reference(reference_node).unwrap();
3914 let resources = HashMap::from([(
3915 "https://example.test/manifests/payload.bin".to_string(),
3916 payload.to_vec(),
3917 )]);
3918 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
3919
3920 let result = process_reference(
3921 &reference,
3922 &resolver,
3923 signature,
3924 ReferenceSet::Manifest,
3925 0,
3926 false,
3927 )
3928 .expect("Manifest Reference should inherit its own XML Base context");
3929
3930 assert_eq!(result.status, DsigStatus::Valid);
3931 }
3932
3933 #[test]
3934 fn manifest_reference_index_ignores_nested_manifest_descendants() {
3935 let payload = b"direct manifest payload";
3938 let digest = base64::engine::general_purpose::STANDARD
3939 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
3940 let xml = format!(
3941 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
3942 <ds:Object><wrapper><ds:Manifest xml:base="nested/">
3943 <ds:Reference URI="payload.bin">
3944 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3945 <ds:DigestValue>{digest}</ds:DigestValue>
3946 </ds:Reference>
3947 </ds:Manifest></wrapper></ds:Object>
3948 <ds:Object><ds:Manifest xml:base="direct/">
3949 <ds:Reference URI="payload.bin">
3950 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3951 <ds:DigestValue>{digest}</ds:DigestValue>
3952 </ds:Reference>
3953 </ds:Manifest></ds:Object>
3954 </ds:Signature>"#
3955 );
3956 let document = Document::parse(&xml).unwrap();
3957 let signature = document.root_element();
3958 let direct_reference_node = signature
3959 .children()
3960 .filter(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
3961 .nth(1)
3962 .unwrap()
3963 .children()
3964 .find(|node| node.has_tag_name((XMLDSIG_NS, "Manifest")))
3965 .unwrap()
3966 .children()
3967 .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
3968 .unwrap();
3969 let reference = super::super::parse::parse_reference(direct_reference_node).unwrap();
3970 let resources = HashMap::from([(
3971 "https://example.test/direct/payload.bin".to_string(),
3972 payload.to_vec(),
3973 )]);
3974 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
3975
3976 let result = process_reference(
3977 &reference,
3978 &resolver,
3979 signature,
3980 ReferenceSet::Manifest,
3981 0,
3982 false,
3983 )
3984 .expect("Manifest index must select the direct Object/Manifest reference");
3985
3986 assert_eq!(result.status, DsigStatus::Valid);
3987 }
3988
3989 struct RejectingKey;
3990
3991 impl VerifyingKey for RejectingKey {
3992 fn verify(
3993 &self,
3994 _algorithm: SignatureAlgorithm,
3995 _signed_data: &[u8],
3996 _signature_value: &[u8],
3997 ) -> Result<bool, SignatureVerificationPipelineError> {
3998 Ok(false)
3999 }
4000 }
4001
4002 struct AcceptingKey;
4003
4004 impl VerifyingKey for AcceptingKey {
4005 fn verify(
4006 &self,
4007 _algorithm: SignatureAlgorithm,
4008 _signed_data: &[u8],
4009 _signature_value: &[u8],
4010 ) -> Result<bool, SignatureVerificationPipelineError> {
4011 Ok(true)
4012 }
4013 }
4014
4015 struct PanicResolver;
4016
4017 impl KeyResolver for PanicResolver {
4018 fn resolve<'a>(
4019 &'a self,
4020 _key_info: Option<&KeyInfo>,
4021 _algorithm: SignatureAlgorithm,
4022 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
4023 {
4024 panic!("resolver should not be called when references already fail");
4025 }
4026 }
4027
4028 struct MissingKeyResolver;
4029
4030 impl KeyResolver for MissingKeyResolver {
4031 fn resolve<'a>(
4032 &'a self,
4033 _key_info: Option<&KeyInfo>,
4034 _algorithm: SignatureAlgorithm,
4035 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
4036 {
4037 Ok(None)
4038 }
4039 }
4040
4041 struct ConsumingKeyInfoResolver;
4042
4043 impl KeyResolver for ConsumingKeyInfoResolver {
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 Ok(None)
4051 }
4052
4053 fn consumes_document_key_info(&self) -> bool {
4054 true
4055 }
4056 }
4057
4058 struct FallbackKeyInfoResolver;
4059
4060 impl KeyResolver for FallbackKeyInfoResolver {
4061 fn resolve<'a>(
4062 &'a self,
4063 key_info: Option<&KeyInfo>,
4064 _algorithm: SignatureAlgorithm,
4065 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
4066 {
4067 let sources = &key_info.expect("KeyInfo must be parsed").sources;
4068 assert!(matches!(
4069 sources.as_slice(),
4070 [
4071 super::super::parse::KeyInfoSource::RetrievalMethod { .. },
4072 super::super::parse::KeyInfoSource::KeyName(name),
4073 ] if name == "fallback"
4074 ));
4075 Ok(Some(Box::new(AcceptingKey)))
4076 }
4077
4078 fn consumes_document_key_info(&self) -> bool {
4079 true
4080 }
4081 }
4082
4083 struct EarlyKeyInfoResolver;
4084
4085 impl KeyResolver for EarlyKeyInfoResolver {
4086 fn resolve<'a>(
4087 &'a self,
4088 key_info: Option<&KeyInfo>,
4089 _algorithm: SignatureAlgorithm,
4090 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
4091 {
4092 let sources = &key_info.expect("KeyInfo must be parsed").sources;
4093 assert!(matches!(
4094 sources.as_slice(),
4095 [
4096 super::super::parse::KeyInfoSource::KeyName(name),
4097 super::super::parse::KeyInfoSource::RetrievalMethod { .. },
4098 ] if name == "primary"
4099 ));
4100 Ok(Some(Box::new(AcceptingKey)))
4101 }
4102
4103 fn consumes_document_key_info(&self) -> bool {
4104 true
4105 }
4106 }
4107
4108 fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
4109 format!(
4110 r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4111 <ds:SignedInfo>
4112 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4113 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4114 <ds:Reference URI="{reference_uri}">
4115 {transforms_xml}
4116 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
4117 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
4118 </ds:Reference>
4119 </ds:SignedInfo>
4120 <ds:SignatureValue>AQ==</ds:SignatureValue>
4121</ds:Signature>"#
4122 )
4123 }
4124
4125 fn signature_with_target_reference(signature_value_b64: &str) -> String {
4126 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4127 <target ID="target">payload</target>
4128 <ds:Signature>
4129 <ds:SignedInfo>
4130 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4131 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4132 <ds:Reference URI="#target">
4133 <ds:Transforms>
4134 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4135 </ds:Transforms>
4136 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
4137 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
4138 </ds:Reference>
4139 </ds:SignedInfo>
4140 <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
4141 </ds:Signature>
4142</root>"##;
4143
4144 let doc = Document::parse(xml_template).unwrap();
4145 let sig_node = doc
4146 .descendants()
4147 .find(|node| node.is_element() && node.tag_name().name() == "Signature")
4148 .unwrap();
4149 let signed_info_node = sig_node
4150 .children()
4151 .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
4152 .unwrap();
4153 let signed_info = parse_signed_info(signed_info_node).unwrap();
4154 let reference = &signed_info.references[0];
4155 let resolver = UriReferenceResolver::new(&doc);
4156 let initial_data = resolver
4157 .dereference(reference.uri.as_deref().unwrap())
4158 .unwrap();
4159 let pre_digest =
4160 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
4161 .unwrap();
4162 let digest = compute_digest(reference.digest_method, &pre_digest);
4163 let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
4164 xml_template
4165 .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
4166 .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
4167 }
4168
4169 #[test]
4170 fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
4171 let xml = signature_with_target_reference("AQ==");
4172
4173 let result = VerifyContext::new()
4174 .verify(&xml)
4175 .expect("missing key config must be reported as verification status");
4176 assert!(
4177 matches!(
4178 result.status,
4179 DsigStatus::Invalid(FailureReason::KeyNotFound)
4180 ),
4181 "unexpected status: {:?}",
4182 result.status
4183 );
4184 }
4185
4186 #[test]
4187 fn verify_context_rejects_disallowed_uri() {
4188 let xml = minimal_signature_xml("http://example.com/external", "");
4189 let err = VerifyContext::new()
4190 .key(&RejectingKey)
4191 .verify(&xml)
4192 .expect_err("external URI should be rejected by default policy");
4193 assert!(matches!(
4194 err,
4195 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
4196 operation: "verification",
4197 ..
4198 })
4199 ));
4200 }
4201
4202 #[test]
4203 fn verify_context_bounds_effective_xml_base_components() {
4204 let mut xml = minimal_signature_xml("payload", "");
4207 for _ in 0..65 {
4208 xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
4209 }
4210 let resources = HashMap::new();
4211 let error = VerifyContext::new()
4212 .key(&AcceptingKey)
4213 .allowed_uri_types(UriTypeSet::ALL)
4214 .external_resources(&resources)
4215 .verify(&xml)
4216 .expect_err("XML Base component work must be bounded before lookup");
4217
4218 assert!(matches!(
4219 error,
4220 SignatureVerificationPipelineError::Policy(
4221 crate::policy::PolicyViolation::ResourceLimit {
4222 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
4223 maximum: 64,
4224 actual: 65,
4225 }
4226 )
4227 ));
4228 }
4229
4230 #[test]
4231 fn verify_context_bounds_cumulative_xml_base_resolution_bytes() {
4232 let mut xml = minimal_signature_xml("payload", "");
4235 for _ in 0..2 {
4236 xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
4237 }
4238 let resources = HashMap::new();
4239 let mut policy = crate::policy::VerificationPolicy::default();
4240 policy.resources.max_xml_base_resolution_bytes = 32;
4241 let error = VerifyContext::new()
4242 .policy(policy)
4243 .key(&AcceptingKey)
4244 .allowed_uri_types(UriTypeSet::ALL)
4245 .external_resources(&resources)
4246 .verify(&xml)
4247 .expect_err("cumulative XML Base copies must obey the operation budget");
4248
4249 assert!(matches!(
4250 error,
4251 SignatureVerificationPipelineError::Policy(
4252 crate::policy::PolicyViolation::ResourceLimit {
4253 resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
4254 maximum: 32,
4255 ..
4256 }
4257 )
4258 ));
4259 }
4260
4261 #[test]
4262 fn verify_context_applies_xml_base_policy_to_signed_info_c14n() {
4263 let xml = signature_with_target_reference("AQ==")
4267 .replacen(
4268 "http://www.w3.org/2001/10/xml-exc-c14n#",
4269 "http://www.w3.org/2006/12/xml-c14n11",
4270 1,
4271 )
4272 .replace(
4273 " <ds:Signature>",
4274 " <outer xml:base=\"one/\"><inner xml:base=\"two/\"><ds:Signature>",
4275 )
4276 .replace(" </ds:Signature>", " </ds:Signature></inner></outer>");
4277 let policy = crate::policy::VerificationPolicy {
4278 resources: crate::policy::ResourcePolicy {
4279 max_xml_base_components: 1,
4280 ..crate::policy::ResourcePolicy::default()
4281 },
4282 ..crate::policy::VerificationPolicy::default()
4283 };
4284
4285 let error = VerifyContext::new()
4286 .key(&AcceptingKey)
4287 .policy(policy)
4288 .verify(&xml)
4289 .expect_err("SignedInfo C14N must use the operation XML Base budget");
4290
4291 assert!(matches!(
4292 error,
4293 SignatureVerificationPipelineError::Policy(
4294 crate::policy::PolicyViolation::ResourceLimit {
4295 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
4296 maximum: 1,
4297 actual: 2,
4298 }
4299 )
4300 ));
4301 }
4302
4303 #[test]
4304 fn verify_context_classifies_signed_info_xml_base_byte_limit_as_policy() {
4305 let xml = signature_with_target_reference("AQ==")
4308 .replacen(
4309 "http://www.w3.org/2001/10/xml-exc-c14n#",
4310 "http://www.w3.org/2006/12/xml-c14n11",
4311 1,
4312 )
4313 .replace(
4314 " <ds:Signature>",
4315 " <outer xml:base=\"segment/\"><ds:Signature>",
4316 )
4317 .replace(" </ds:Signature>", " </ds:Signature></outer>");
4318 let policy = crate::policy::VerificationPolicy {
4319 resources: crate::policy::ResourcePolicy {
4320 max_xml_base_resolution_bytes: 1,
4321 ..crate::policy::ResourcePolicy::default()
4322 },
4323 ..crate::policy::VerificationPolicy::default()
4324 };
4325
4326 let error = VerifyContext::new()
4327 .key(&AcceptingKey)
4328 .policy(policy)
4329 .verify(&xml)
4330 .expect_err("SignedInfo XML Base byte exhaustion must be a policy error");
4331
4332 assert!(matches!(
4333 error,
4334 SignatureVerificationPipelineError::Policy(
4335 crate::policy::PolicyViolation::ResourceLimit {
4336 resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
4337 maximum: 1,
4338 actual,
4339 }
4340 ) if actual > 1
4341 ));
4342 }
4343
4344 #[test]
4345 fn verify_context_meters_repeated_external_dereferences() {
4346 let payload = b"payload";
4349 let digest = base64::engine::general_purpose::STANDARD.encode(
4350 crate::xmldsig::compute_digest(DigestAlgorithm::Sha1, payload),
4351 );
4352 let reference = format!(
4353 r#"<ds:Reference URI="urn:payload"><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference>"#
4354 );
4355 let xml = format!(
4356 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>"#
4357 );
4358 let resources = HashMap::from([("urn:payload".to_owned(), payload.to_vec())]);
4359 let policy = crate::policy::VerificationPolicy {
4360 uris: crate::policy::UriPolicy {
4361 references: UriTypeSet::ALL,
4362 ..crate::policy::UriPolicy::default()
4363 },
4364 resources: crate::policy::ResourcePolicy {
4365 max_external_resource_bytes: payload.len(),
4366 max_external_resource_total_bytes: payload.len(),
4367 ..crate::policy::ResourcePolicy::default()
4368 },
4369 ..crate::policy::VerificationPolicy::default()
4370 };
4371
4372 let error = VerifyContext::new()
4373 .key(&AcceptingKey)
4374 .policy(policy)
4375 .external_resources(&resources)
4376 .verify(&xml)
4377 .expect_err("the second dereference must exhaust the aggregate byte ceiling");
4378
4379 assert!(
4380 error
4381 .to_string()
4382 .contains("aggregate external resource bytes")
4383 );
4384 }
4385
4386 #[test]
4387 fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
4388 let xml = minimal_signature_xml("", "");
4389 let err = VerifyContext::new()
4390 .key(&RejectingKey)
4391 .allowed_uri_types(UriTypeSet::new(false, true, false))
4392 .verify(&xml)
4393 .expect_err("empty URI must be rejected when empty references are disabled");
4394 assert!(matches!(
4395 err,
4396 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
4397 operation: "verification",
4398 ..
4399 })
4400 ));
4401 }
4402
4403 #[test]
4404 fn verify_context_rejects_disallowed_transform() {
4405 let xml = minimal_signature_xml(
4406 "",
4407 r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
4408 );
4409 let err = VerifyContext::new()
4410 .key(&RejectingKey)
4411 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
4412 .verify(&xml)
4413 .expect_err("enveloped transform should be rejected by allowlist");
4414 assert!(matches!(
4415 err,
4416 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
4417 operation: "verification transform",
4418 ..
4419 })
4420 ));
4421 }
4422
4423 #[test]
4424 fn verify_context_applies_transform_allowlist_to_signed_info_c14n() {
4425 let xml = signature_with_target_reference("AQ==").replacen(
4428 "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
4429 "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"/>",
4430 1,
4431 );
4432 let error = VerifyContext::new()
4433 .key(&AcceptingKey)
4434 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
4435 .verify(&xml)
4436 .expect_err("SignedInfo C14N must obey the operation transform allowlist");
4437
4438 assert!(matches!(
4439 error,
4440 SignatureVerificationPipelineError::Policy(
4441 crate::policy::PolicyViolation::Algorithm {
4442 operation: "verification transform",
4443 ref algorithm,
4444 }
4445 )
4446 if algorithm == "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
4447 ));
4448 }
4449
4450 #[test]
4451 fn verify_context_applies_transform_allowlist_to_key_retrieval() {
4452 let xml = signature_with_target_reference("AQ==")
4455 .replacen(
4456 "</ds:Signature>",
4457 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>"##,
4458 1,
4459 )
4460 .replacen(
4461 "</root>",
4462 r#"<holder ID="keys"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder></root>"#,
4463 1,
4464 );
4465 let error = VerifyContext::new()
4466 .key_resolver(&ConsumingKeyInfoResolver)
4467 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
4468 .verify(&xml)
4469 .expect_err("RetrievalMethod XPath must obey the operation transform allowlist");
4470
4471 assert!(matches!(
4472 error,
4473 SignatureVerificationPipelineError::Policy(
4474 crate::policy::PolicyViolation::Algorithm {
4475 operation: "verification transform",
4476 ref algorithm,
4477 }
4478 )
4479 if algorithm == XPATH_TRANSFORM_URI
4480 ));
4481 }
4482
4483 fn signature_with_manifest_xml(valid_manifest_digest: bool) -> String {
4484 signature_with_manifest_xml_with_manifest_mutation(valid_manifest_digest, |xml| xml)
4485 }
4486
4487 fn signature_with_manifest_xml_with_manifest_mutation<F>(
4488 valid_manifest_digest: bool,
4489 mutate_manifest: F,
4490 ) -> String
4491 where
4492 F: FnOnce(String) -> String,
4493 {
4494 const TMP_SIGNED_INFO_DIGEST: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
4495 const INVALID_MANIFEST_DIGEST: &str = "//////////////////////////8=";
4496 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4497 <target ID="target">payload</target>
4498 <ds:Signature>
4499 <ds:SignedInfo>
4500 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4501 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4502 <ds:Reference URI="#manifest">
4503 <ds:Transforms>
4504 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4505 </ds:Transforms>
4506 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
4507 <ds:DigestValue>SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER</ds:DigestValue>
4508 </ds:Reference>
4509 </ds:SignedInfo>
4510 <ds:SignatureValue>AQ==</ds:SignatureValue>
4511 <ds:Object>
4512 <ds:Manifest ID="manifest">
4513 <ds:Reference URI="#target">
4514 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
4515 <ds:DigestValue>MANIFEST_DIGEST_PLACEHOLDER</ds:DigestValue>
4516 </ds:Reference>
4517 </ds:Manifest>
4518 </ds:Object>
4519 </ds:Signature>
4520</root>"##;
4521 let seed_xml = xml_template.replace(
4522 "SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER",
4523 TMP_SIGNED_INFO_DIGEST,
4524 );
4525 let doc = Document::parse(&seed_xml).unwrap();
4526 let signature_node = doc
4527 .descendants()
4528 .find(|node| {
4529 node.is_element()
4530 && node.tag_name().namespace() == Some(XMLDSIG_NS)
4531 && node.tag_name().name() == "Signature"
4532 })
4533 .unwrap();
4534 let resolver = UriReferenceResolver::new(&doc);
4535 let initial_data = resolver.dereference("#target").unwrap();
4536 let manifest_pre_digest =
4537 crate::xmldsig::execute_transforms(signature_node, initial_data, &[]).unwrap();
4538 let computed_manifest_digest_b64 = base64::engine::general_purpose::STANDARD
4539 .encode(compute_digest(DigestAlgorithm::Sha1, &manifest_pre_digest));
4540 let final_manifest_digest_b64 = if valid_manifest_digest {
4541 computed_manifest_digest_b64.as_str()
4542 } else {
4543 INVALID_MANIFEST_DIGEST
4544 };
4545 let xml_with_manifest_digest = mutate_manifest(
4546 seed_xml.replace("MANIFEST_DIGEST_PLACEHOLDER", final_manifest_digest_b64),
4547 );
4548 let signed_doc = Document::parse(&xml_with_manifest_digest).unwrap();
4549 let signed_signature_node = signed_doc
4550 .descendants()
4551 .find(|node| {
4552 node.is_element()
4553 && node.tag_name().namespace() == Some(XMLDSIG_NS)
4554 && node.tag_name().name() == "Signature"
4555 })
4556 .unwrap();
4557 let signed_info_node = signed_signature_node
4558 .children()
4559 .find(|node| {
4560 node.is_element()
4561 && node.tag_name().namespace() == Some(XMLDSIG_NS)
4562 && node.tag_name().name() == "SignedInfo"
4563 })
4564 .unwrap();
4565 let signed_info = parse_signed_info(signed_info_node).unwrap();
4566 let object_reference = &signed_info.references[0];
4567 let signed_resolver = UriReferenceResolver::new(&signed_doc);
4568 let signed_initial_data = signed_resolver
4569 .dereference(object_reference.uri.as_deref().unwrap())
4570 .unwrap();
4571 let signed_pre_digest = crate::xmldsig::execute_transforms(
4572 signed_signature_node,
4573 signed_initial_data,
4574 &object_reference.transforms,
4575 )
4576 .unwrap();
4577 let signed_digest_b64 = base64::engine::general_purpose::STANDARD.encode(compute_digest(
4578 object_reference.digest_method,
4579 &signed_pre_digest,
4580 ));
4581
4582 xml_with_manifest_digest.replacen(TMP_SIGNED_INFO_DIGEST, &signed_digest_b64, 1)
4583 }
4584
4585 fn replace_fixture_manifest_digest(xml: &str, replacement: &str) -> String {
4586 let object_marker = "<ds:Object>";
4587 let object_start = xml
4588 .find(object_marker)
4589 .expect("fixture should contain ds:Object")
4590 + object_marker.len();
4591 let open = "<ds:DigestValue>";
4592 let close = "</ds:DigestValue>";
4593 let value_start = xml[object_start..]
4594 .find(open)
4595 .map(|offset| object_start + offset + open.len())
4596 .expect("Manifest should contain DigestValue");
4597 let value_end = xml[value_start..]
4598 .find(close)
4599 .map(|offset| value_start + offset)
4600 .expect("Manifest DigestValue must be closed");
4601
4602 format!("{}{replacement}{}", &xml[..value_start], &xml[value_end..])
4603 }
4604
4605 #[test]
4606 fn verify_context_processes_manifest_references_when_enabled() {
4607 let xml = signature_with_manifest_xml(true);
4608
4609 let result_without_manifests = VerifyContext::new()
4610 .key(&RejectingKey)
4611 .verify(&xml)
4612 .expect("manifest processing disabled should still verify SignedInfo");
4613 assert!(
4614 result_without_manifests.manifest_references.is_empty(),
4615 "manifest results must stay empty when manifest processing is disabled",
4616 );
4617 assert!(matches!(
4618 result_without_manifests.status,
4619 DsigStatus::Invalid(FailureReason::SignatureMismatch)
4620 ));
4621
4622 let malformed_manifest_xml = signature_with_manifest_xml(true).replacen(
4623 "</ds:Object>",
4624 "</ds:Object><ds:Object><ds:Manifest><ds:Foo/></ds:Manifest></ds:Object>",
4625 1,
4626 );
4627 let malformed_with_manifests_disabled = VerifyContext::new()
4628 .key(&RejectingKey)
4629 .verify(&malformed_manifest_xml)
4630 .expect("malformed Manifest must be ignored when manifest processing is disabled");
4631 assert!(
4632 malformed_with_manifests_disabled
4633 .manifest_references
4634 .is_empty(),
4635 "manifest parser must not run when process_manifests is disabled",
4636 );
4637 assert!(matches!(
4638 malformed_with_manifests_disabled.status,
4639 DsigStatus::Invalid(FailureReason::SignatureMismatch)
4640 ));
4641
4642 let result_with_manifests = VerifyContext::new()
4643 .key(&AcceptingKey)
4644 .process_manifests(true)
4645 .verify(&xml)
4646 .expect("manifest references should be processed when enabled");
4647 assert_eq!(result_with_manifests.manifest_references.len(), 1);
4648 assert_eq!(
4649 result_with_manifests.manifest_references[0].reference_set,
4650 ReferenceSet::Manifest
4651 );
4652 assert_eq!(
4653 result_with_manifests.manifest_references[0].reference_index,
4654 0
4655 );
4656 assert!(matches!(
4657 result_with_manifests.manifest_references[0].status,
4658 DsigStatus::Valid
4659 ));
4660 assert!(matches!(result_with_manifests.status, DsigStatus::Valid));
4661 }
4662
4663 #[test]
4664 fn verify_context_skips_manifest_work_when_signature_value_is_invalid() {
4665 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4669 replace_fixture_manifest_digest(&xml, "!!!")
4670 });
4671 assert!(
4672 xml.split_once("<ds:Object>")
4673 .is_some_and(|(_, object)| object.contains("<ds:DigestValue>!!!</ds:DigestValue>")),
4674 "fixture mutation must corrupt the nested Manifest DigestValue",
4675 );
4676
4677 let result = VerifyContext::new()
4678 .key(&RejectingKey)
4679 .process_manifests(true)
4680 .verify(&xml)
4681 .expect("invalid SignatureValue must short-circuit Manifest parsing");
4682
4683 assert!(matches!(
4684 result.status,
4685 DsigStatus::Invalid(FailureReason::SignatureMismatch)
4686 ));
4687 assert!(result.manifest_references.is_empty());
4688 }
4689
4690 #[test]
4691 fn verify_context_shares_xpath_parse_budget_with_manifest_references() {
4692 let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
4695 .repeat(64);
4696 let transform = format!(
4697 r#"<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</ds:Transform>"#
4698 );
4699 let max_transforms = transform.repeat(16);
4700 let max_manifest_reference = format!(
4701 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>"##
4702 );
4703 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4704 xml.replacen(
4705 r##"<ds:Reference URI="#target">"##,
4706 &format!(
4707 r##"<ds:Reference URI="#target"><ds:Transforms>{}</ds:Transforms>"##,
4708 max_transforms
4709 ),
4710 1,
4711 )
4712 .replacen(
4713 "</ds:SignedInfo>",
4714 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>"##,
4715 1,
4716 )
4717 .replacen(
4718 "</ds:Manifest>",
4719 &format!("{}</ds:Manifest>", max_manifest_reference.repeat(3)),
4720 1,
4721 )
4722 });
4723
4724 let error = VerifyContext::new()
4725 .key(&AcceptingKey)
4726 .process_manifests(true)
4727 .verify(&xml)
4728 .expect_err("SignedInfo and Manifest References must share one XPath parse budget");
4729
4730 assert!(
4731 matches!(
4732 &error,
4733 SignatureVerificationPipelineError::Policy(
4734 crate::policy::PolicyViolation::ResourceLimit {
4735 resource: "XPath expressions",
4736 ..
4737 }
4738 )
4739 ),
4740 "unexpected error: {error:?}"
4741 );
4742 }
4743
4744 #[test]
4745 fn verify_context_processes_manifest_when_signedinfo_references_object() {
4746 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4747 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
4748 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
4749 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
4750 });
4751
4752 let result = VerifyContext::new()
4753 .key(&AcceptingKey)
4754 .process_manifests(true)
4755 .verify(&xml)
4756 .expect("manifest references should be processed when SignedInfo references ds:Object");
4757 assert_eq!(
4758 result.manifest_references.len(),
4759 1,
4760 "signed ds:Object should enable processing of its direct-child ds:Manifest",
4761 );
4762 assert_eq!(
4763 result.manifest_references[0].reference_set,
4764 ReferenceSet::Manifest
4765 );
4766 assert_eq!(result.manifest_references[0].reference_index, 0);
4767 assert!(matches!(
4768 result.manifest_references[0].status,
4769 DsigStatus::Valid
4770 ));
4771 }
4772
4773 #[test]
4774 fn verify_context_skips_manifest_removed_by_enveloped_transform() {
4775 for target_object in [false, true] {
4779 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4780 let xml = xml.replacen(
4781 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
4782 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#"/>"#,
4783 1,
4784 );
4785 if target_object {
4786 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
4787 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
4788 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
4789 } else {
4790 xml
4791 }
4792 });
4793
4794 let result = VerifyContext::new()
4795 .key(&AcceptingKey)
4796 .process_manifests(true)
4797 .store_pre_digest(true)
4798 .verify(&xml)
4799 .expect("an emptied reference remains a valid core digest input");
4800
4801 assert!(matches!(result.status, DsigStatus::Valid));
4802 assert_eq!(
4803 result.signed_info_references[0].pre_digest_data.as_deref(),
4804 Some([].as_slice()),
4805 "target_object={target_object} must have empty transformed bytes",
4806 );
4807 assert!(
4808 result.manifest_references.is_empty(),
4809 "target_object={target_object} must not authenticate the Manifest",
4810 );
4811 }
4812 }
4813
4814 #[test]
4815 fn verify_context_ignores_manifest_excluded_from_signed_object() {
4816 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4820 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
4821 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
4822 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
4823 .replacen(
4824 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
4825 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#"/>"#,
4826 1,
4827 )
4828 });
4829
4830 let result = VerifyContext::new()
4831 .key(&AcceptingKey)
4832 .process_manifests(true)
4833 .verify(&xml)
4834 .expect("excluded Manifest content must be ignored, not parsed");
4835
4836 assert!(matches!(result.status, DsigStatus::Valid));
4837 assert!(
4838 result.manifest_references.is_empty(),
4839 "a transform-excluded Manifest is not authenticated by SignedInfo",
4840 );
4841 }
4842
4843 #[test]
4844 fn verify_context_skips_manifest_digest_work_when_signature_is_invalid() {
4845 let xml = signature_with_manifest_xml(false);
4846 let result = VerifyContext::new()
4847 .key(&RejectingKey)
4848 .process_manifests(true)
4849 .verify(&xml)
4850 .expect("invalid SignatureValue must short-circuit Manifest digest work");
4851 assert!(result.manifest_references.is_empty());
4852 assert!(matches!(
4853 result.status,
4854 DsigStatus::Invalid(FailureReason::SignatureMismatch)
4855 ));
4856 }
4857
4858 #[test]
4859 fn verify_context_manifest_digest_mismatch_is_non_fatal_with_accepting_key() {
4860 let xml = signature_with_manifest_xml(false);
4861 let result = VerifyContext::new()
4862 .key(&AcceptingKey)
4863 .process_manifests(true)
4864 .verify(&xml)
4865 .expect("manifest digest mismatches should be recorded while signature stays valid");
4866 assert_eq!(result.manifest_references.len(), 1);
4867 assert!(matches!(
4868 result.manifest_references[0].status,
4869 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
4870 ));
4871 assert!(matches!(result.status, DsigStatus::Valid));
4872 }
4873
4874 #[test]
4875 fn invalid_outer_manifest_digest_does_not_parse_nested_manifest() {
4876 let xml = signature_with_manifest_xml_with_manifest_mutation(false, |xml| {
4880 xml.replacen("URI=\"#target\"", "URI=\"#inner-object\"", 1)
4881 .replacen(
4882 "</ds:Object>",
4883 "</ds:Object><ds:Object ID=\"inner-object\"><ds:Manifest>junk<ds:Unexpected/></ds:Manifest></ds:Object>",
4884 1,
4885 )
4886 });
4887
4888 let result = VerifyContext::new()
4889 .key(&AcceptingKey)
4890 .process_manifests(true)
4891 .verify(&xml)
4892 .expect("an unauthenticated nested Manifest must remain opaque");
4893
4894 assert_eq!(result.manifest_references.len(), 1);
4895 assert!(matches!(
4896 result.manifest_references[0].status,
4897 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
4898 ));
4899 }
4900
4901 #[test]
4902 fn verify_context_skips_manifest_parsing_when_signedinfo_reference_fails() {
4903 let xml = signature_with_manifest_xml(true);
4906 let (signed_info_prefix, object_suffix) = xml
4907 .split_once("<ds:Object>")
4908 .expect("fixture should contain ds:Object");
4909 let open = "<ds:DigestValue>";
4910 let close = "</ds:DigestValue>";
4911 let digest_start = signed_info_prefix
4912 .find(open)
4913 .expect("SignedInfo should contain DigestValue");
4914 let digest_end = signed_info_prefix[digest_start + open.len()..]
4915 .find(close)
4916 .map(|offset| digest_start + open.len() + offset)
4917 .expect("SignedInfo DigestValue must be closed");
4918 let broken_signed_info_prefix = format!(
4919 "{}{}AAAAAAAAAAAAAAAAAAAAAAAAAAA={}{}",
4920 &signed_info_prefix[..digest_start],
4921 open,
4922 close,
4923 &signed_info_prefix[digest_end + close.len()..],
4924 );
4925 let broken_xml = format!("{broken_signed_info_prefix}<ds:Object>{object_suffix}");
4926 let result = VerifyContext::new()
4927 .key(&RejectingKey)
4928 .process_manifests(true)
4929 .verify(&broken_xml)
4930 .expect("SignedInfo digest failure should return without parsing Manifests");
4931 assert!(matches!(
4932 result.status,
4933 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
4934 ));
4935 assert!(
4936 result.manifest_references.is_empty(),
4937 "unauthenticated Manifest content must not be parsed",
4938 );
4939 }
4940
4941 #[test]
4942 fn verify_context_skips_manifest_policy_work_when_signature_is_invalid() {
4943 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4946 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
4947 });
4948 let result = VerifyContext::new()
4949 .key(&RejectingKey)
4950 .process_manifests(true)
4951 .verify(&broken_xml)
4952 .expect("invalid SignatureValue must short-circuit Manifest policy work");
4953 assert!(result.manifest_references.is_empty());
4954 assert!(matches!(
4955 result.status,
4956 DsigStatus::Invalid(FailureReason::SignatureMismatch)
4957 ));
4958 }
4959
4960 #[test]
4961 fn verify_context_records_manifest_policy_violations_with_accepting_key() {
4962 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4963 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
4964 });
4965 let result = VerifyContext::new()
4966 .key(&AcceptingKey)
4967 .process_manifests(true)
4968 .verify(&broken_xml)
4969 .expect("manifest policy violations should be recorded while signature stays valid");
4970 assert_eq!(result.manifest_references.len(), 1);
4971 assert!(matches!(
4972 result.manifest_references[0].status,
4973 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
4974 ));
4975 assert!(matches!(result.status, DsigStatus::Valid));
4976 }
4977
4978 #[test]
4979 fn verify_context_applies_digest_policy_to_manifest_references() {
4980 let policy = crate::policy::VerificationPolicy {
4983 manifest_processing: crate::policy::ManifestProcessing::Process,
4984 digest_algorithms: Some(HashSet::from([DigestAlgorithm::Sha1])),
4985 ..crate::policy::VerificationPolicy::default()
4986 };
4987 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
4988 let legacy = "http://www.w3.org/2000/09/xmldsig#sha1";
4989 let offset = xml
4990 .rfind(legacy)
4991 .expect("Manifest DigestMethod must be present");
4992 xml.replace_range(offset..offset + legacy.len(), DigestAlgorithm::Sha256.uri());
4993 let value_start = xml[offset..]
4994 .find("<ds:DigestValue>")
4995 .map(|relative| offset + relative + "<ds:DigestValue>".len())
4996 .expect("Manifest DigestValue must be present");
4997 let value_end = xml[value_start..]
4998 .find("</ds:DigestValue>")
4999 .map(|relative| value_start + relative)
5000 .expect("Manifest DigestValue must be closed");
5001 xml.replace_range(
5002 value_start..value_end,
5003 &base64::engine::general_purpose::STANDARD.encode([0_u8; 32]),
5004 );
5005 xml
5006 });
5007 let result = VerifyContext::new()
5008 .key(&AcceptingKey)
5009 .policy(policy)
5010 .verify(&xml)
5011 .expect("a disallowed Manifest digest is a per-reference result");
5012
5013 assert!(matches!(result.status, DsigStatus::Valid));
5014 assert!(matches!(
5015 result.manifest_references[0].status,
5016 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
5017 ));
5018 }
5019
5020 #[test]
5021 fn verify_context_applies_transform_count_policy_to_manifest_references() {
5022 let policy = crate::policy::VerificationPolicy {
5025 manifest_processing: crate::policy::ManifestProcessing::Process,
5026 resources: crate::policy::ResourcePolicy {
5027 max_transforms_per_reference: 1,
5028 ..crate::policy::ResourcePolicy::default()
5029 },
5030 ..crate::policy::VerificationPolicy::default()
5031 };
5032 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
5033 let manifest_start = xml
5034 .find("<ds:Manifest")
5035 .expect("fixture must contain a Manifest");
5036 let manifest = xml[manifest_start..].replacen(
5037 "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
5038 concat!(
5039 "<ds:Transforms>",
5040 "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
5041 "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
5042 "</ds:Transforms>",
5043 "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>"
5044 ),
5045 1,
5046 );
5047 xml.replace_range(manifest_start.., &manifest);
5048 xml
5049 });
5050 let result = VerifyContext::new()
5051 .key(&AcceptingKey)
5052 .policy(policy)
5053 .verify(&xml)
5054 .expect("Manifest transform policy is a per-reference result");
5055
5056 assert!(matches!(result.status, DsigStatus::Valid));
5057 assert!(matches!(
5058 result.manifest_references[0].status,
5059 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
5060 ));
5061 }
5062
5063 #[test]
5064 fn verify_context_skips_manifest_uri_work_when_signature_is_invalid() {
5065 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5068 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
5069 });
5070
5071 let result = VerifyContext::new()
5072 .key(&RejectingKey)
5073 .process_manifests(true)
5074 .verify(&broken_xml)
5075 .expect("invalid SignatureValue must short-circuit Manifest URI processing");
5076 assert!(result.manifest_references.is_empty());
5077 assert!(matches!(
5078 result.status,
5079 DsigStatus::Invalid(FailureReason::SignatureMismatch)
5080 ));
5081 }
5082
5083 #[test]
5084 fn verify_context_records_manifest_missing_uri_with_accepting_key() {
5085 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5086 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
5087 });
5088
5089 let result = VerifyContext::new()
5090 .key(&AcceptingKey)
5091 .process_manifests(true)
5092 .verify(&broken_xml)
5093 .expect("manifest missing URI should be recorded while signature stays valid");
5094 assert_eq!(result.manifest_references.len(), 1);
5095 assert_eq!(result.manifest_references[0].uri, "<omitted>");
5096 assert!(matches!(
5097 result.manifest_references[0].status,
5098 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
5099 ));
5100 assert!(matches!(result.status, DsigStatus::Valid));
5101 }
5102
5103 #[test]
5104 fn verify_context_ignores_nested_manifests_in_object() {
5105 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5108 xml.replacen(
5109 "<ds:Manifest ID=\"manifest\">",
5110 "<wrapper><ds:Manifest ID=\"manifest\">",
5111 1,
5112 )
5113 .replacen("</ds:Manifest>", "</ds:Manifest></wrapper>", 1)
5114 });
5115
5116 let result = VerifyContext::new()
5117 .key(&AcceptingKey)
5118 .process_manifests(true)
5119 .verify(&xml)
5120 .expect("nested Manifest nodes are ignored in strict mode");
5121 assert!(
5122 result.manifest_references.is_empty(),
5123 "only direct ds:Manifest children of ds:Object must be processed"
5124 );
5125 assert!(matches!(result.status, DsigStatus::Valid));
5126 }
5127
5128 #[test]
5129 fn verify_context_reports_manifest_reference_parse_errors_explicitly() {
5130 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5133 replace_fixture_manifest_digest(&xml, "!!!")
5134 });
5135
5136 let err = VerifyContext::new()
5137 .key(&AcceptingKey)
5138 .process_manifests(true)
5139 .verify(&broken_xml)
5140 .expect_err("invalid Manifest DigestValue must map to ParseManifestReference");
5141 assert!(matches!(
5142 err,
5143 SignatureVerificationPipelineError::ParseManifestReference(_)
5144 ));
5145 }
5146
5147 #[test]
5148 fn verify_context_reports_unsupported_manifest_transform_with_declared_digest() {
5149 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5152 let xml = xml.replacen(
5153 "<ds:Reference URI=\"#target\">",
5154 "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
5155 1,
5156 );
5157 let xml = xml.replacen(
5158 "</ds:Transforms>\n <ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
5159 "</ds:Transforms>\n <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>",
5160 1,
5161 );
5162 replace_fixture_manifest_digest(&xml, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
5163 });
5164 assert!(xml.contains("urn:unsupported"));
5165 assert!(xml.contains("http://www.w3.org/2001/04/xmlenc#sha256"));
5166
5167 let result = VerifyContext::new()
5168 .key(&AcceptingKey)
5169 .process_manifests(true)
5170 .verify(&xml)
5171 .expect("unsupported Manifest transform is a per-reference result");
5172 assert_eq!(result.status, DsigStatus::Valid);
5173 assert_eq!(result.manifest_references.len(), 1);
5174 assert_eq!(
5175 result.manifest_references[0].digest_algorithm,
5176 DigestAlgorithm::Sha256
5177 );
5178 assert!(matches!(
5179 result.manifest_references[0].status,
5180 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
5181 ));
5182
5183 let restricted = VerifyContext::new()
5184 .key(&AcceptingKey)
5185 .process_manifests(true)
5186 .allowed_transforms([
5187 DEFAULT_IMPLICIT_C14N_URI,
5188 "http://www.w3.org/2001/10/xml-exc-c14n#",
5189 ])
5190 .verify(&xml)
5191 .expect("a disallowed Manifest transform is a per-reference policy result");
5192 assert!(matches!(
5193 restricted.manifest_references[0].status,
5194 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
5195 ));
5196 }
5197
5198 #[test]
5199 fn manifest_reference_limit_counts_unsupported_entries() {
5200 let references = (0..=MAX_REFERENCES_PER_SIGNATURE)
5201 .map(|index| {
5202 format!(
5203 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>"##
5204 )
5205 })
5206 .collect::<String>();
5207 let xml = format!(
5208 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>"#
5209 );
5210 let document = XmlDocument::parse(xml).unwrap();
5211 let error = document.with_view(|view| {
5212 let signature = view.document().root_element();
5213 let object = signature.children().find(|node| node.is_element()).unwrap();
5214 let policy = crate::policy::VerificationPolicy::default();
5215 let budgets = VerificationOperationBudgets::with_transforms(
5216 &policy,
5217 TransformExecutionBudget::from_resources(&policy.resources),
5218 );
5219 let operation = OperationExecutionContext::new(
5220 policy,
5221 budgets,
5222 Some((view.identity(), view.generation())),
5223 );
5224 operation.authenticate(view.node_identity(object));
5225 let mut processed = HashSet::new();
5226 let mut remaining = MAX_REFERENCES_PER_SIGNATURE;
5227 let mut next_index = 0;
5228 let mut xpath_parse = XPathSignatureParseBudget::default();
5229 let mut state = ManifestDiscoveryState {
5230 processed: &mut processed,
5231 remaining_capacity: &mut remaining,
5232 next_reference_index: &mut next_index,
5233 xpath_parse: &mut xpath_parse,
5234 };
5235 match parse_manifest_references(signature, &operation, view, &mut state, None) {
5236 Ok(_) => panic!("unsupported references must consume the same aggregate limit"),
5237 Err(error) => error,
5238 }
5239 });
5240 assert!(matches!(
5241 error,
5242 SignatureVerificationPipelineError::InvalidStructure {
5243 reason: "signed Manifests exceed the per-signature Reference limit"
5244 }
5245 ));
5246 }
5247
5248 #[test]
5249 fn unsigned_manifest_remains_eligible_after_trust_expands() {
5250 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
5254 let xml = format!(
5255 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>"##
5256 );
5257 let document = XmlDocument::parse(xml).expect("nested Manifest fixture must parse");
5258 document.with_view(|view| {
5259 let signature = view.document().root_element();
5260 let mut objects = signature.children().filter(|node| node.is_element());
5261 let outer = objects.next().expect("outer Object");
5262 let inner = objects.next().expect("inner Object");
5263 let policy = crate::policy::VerificationPolicy::default();
5264 let budgets = VerificationOperationBudgets::with_transforms(
5265 &policy,
5266 TransformExecutionBudget::from_resources(&policy.resources),
5267 );
5268 let operation = OperationExecutionContext::new(
5269 policy,
5270 budgets,
5271 Some((view.identity(), view.generation())),
5272 );
5273 operation.authenticate(view.node_identity(outer));
5274 let mut processed = HashSet::new();
5275 let mut remaining = 2;
5276 let mut next_index = 0;
5277 let mut xpath_budget = XPathSignatureParseBudget::default();
5278
5279 let first = parse_manifest_references(
5280 signature,
5281 &operation,
5282 view,
5283 &mut ManifestDiscoveryState {
5284 processed: &mut processed,
5285 remaining_capacity: &mut remaining,
5286 next_reference_index: &mut next_index,
5287 xpath_parse: &mut xpath_budget,
5288 },
5289 None,
5290 )
5291 .expect("outer Manifest must be discovered");
5292 assert_eq!(first.references.len(), 1);
5293 assert_eq!(first.references[0].reference.uri.as_deref(), Some("#inner"));
5294
5295 operation.authenticate(view.node_identity(inner));
5296 let second = parse_manifest_references(
5297 signature,
5298 &operation,
5299 view,
5300 &mut ManifestDiscoveryState {
5301 processed: &mut processed,
5302 remaining_capacity: &mut remaining,
5303 next_reference_index: &mut next_index,
5304 xpath_parse: &mut xpath_budget,
5305 },
5306 None,
5307 )
5308 .expect("newly authenticated sibling Manifest must remain eligible");
5309 assert_eq!(second.references.len(), 1);
5310 assert_eq!(
5311 second.references[0].reference.uri.as_deref(),
5312 Some("#payload")
5313 );
5314 });
5315 }
5316
5317 #[test]
5318 fn manifest_reference_limit_includes_signed_info_references() {
5319 let xml = signature_with_manifest_xml(true);
5322 let reference_start = xml
5323 .find(r##"<ds:Reference URI="#manifest">"##)
5324 .expect("fixture SignedInfo must reference the Manifest");
5325 let reference_end = xml[reference_start..]
5326 .find("</ds:Reference>")
5327 .map(|offset| reference_start + offset + "</ds:Reference>".len())
5328 .expect("fixture SignedInfo Reference must be closed");
5329 let repeated = xml[reference_start..reference_end].repeat(MAX_REFERENCES_PER_SIGNATURE);
5330 let xml = format!(
5331 "{}{repeated}{}",
5332 &xml[..reference_start],
5333 &xml[reference_end..]
5334 );
5335
5336 let error = VerifyContext::new()
5337 .key(&AcceptingKey)
5338 .process_manifests(true)
5339 .verify(&xml)
5340 .expect_err("one Manifest Reference must exceed the exhausted signature-wide limit");
5341
5342 assert!(matches!(
5343 error,
5344 SignatureVerificationPipelineError::InvalidStructure {
5345 reason: "signed Manifests exceed the per-signature Reference limit"
5346 }
5347 ));
5348 }
5349
5350 #[test]
5351 fn configured_reference_limit_is_shared_with_manifests() {
5352 let policy = crate::policy::VerificationPolicy {
5355 manifest_processing: crate::policy::ManifestProcessing::Process,
5356 resources: crate::policy::ResourcePolicy {
5357 max_references: 1,
5358 ..crate::policy::ResourcePolicy::default()
5359 },
5360 ..crate::policy::VerificationPolicy::default()
5361 };
5362
5363 let error = VerifyContext::new()
5364 .key(&AcceptingKey)
5365 .policy(policy)
5366 .verify(&signature_with_manifest_xml(true))
5367 .expect_err("Manifest must exceed the caller-selected aggregate limit");
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 retrieval_method_materializes_single_x509_data_subtree() {
5378 for uri in [
5379 "#target",
5380 "#xpointer(id('target'))",
5381 "#xpointer(id("target"))",
5382 ] {
5383 for target_xml in [
5384 r#"<ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>"#,
5385 r#"<holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>"#,
5386 ] {
5387 let xml = format!(
5388 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>"#
5389 );
5390 let document = Document::parse(&xml).unwrap();
5391 let key_info_node = document
5392 .descendants()
5393 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5394 .unwrap();
5395 let mut key_info = parse_key_info(key_info_node).unwrap();
5396 let resolver = UriReferenceResolver::new(&document);
5397
5398 materialize_retrieval_methods(
5399 &mut key_info,
5400 &resolver,
5401 UriTypeSet::SAME_DOCUMENT,
5402 None,
5403 crate::provider::default_provider(),
5404 )
5405 .expect("XPath filter must produce one X509Data-rooted node-set");
5406 assert!(matches!(
5407 key_info.sources.as_slice(),
5408 [super::super::parse::KeyInfoSource::X509Data(info)]
5409 if info.subject_names == ["CN=leaf"]
5410 ));
5411 }
5412 }
5413 }
5414
5415 fn retrieval_method_xpath_signature() -> String {
5416 format!(
5417 r##"<root xmlns:ds="{XMLDSIG_NS}">
5418 <payload Id="payload">ok</payload>
5419 <ds:Signature>
5420 <ds:SignedInfo>
5421 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5422 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5423 <ds:Reference URI="#payload">
5424 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5425 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
5426 </ds:Reference>
5427 </ds:SignedInfo>
5428 <ds:SignatureValue>AQ==</ds:SignatureValue>
5429 <ds:KeyInfo>
5430 <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
5431 <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
5432 <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
5433 </ds:Transform></ds:Transforms>
5434 </ds:RetrievalMethod>
5435 </ds:KeyInfo>
5436 </ds:Signature>
5437 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
5438 </root>"##
5439 )
5440 }
5441
5442 #[test]
5443 fn retrieval_method_xpath_uses_signature_expression_budget() {
5444 let mut policy = crate::policy::VerificationPolicy::default();
5447 policy.resources.max_xpath_expressions = 0;
5448
5449 let error = VerifyContext::new()
5450 .policy(policy)
5451 .verify(&retrieval_method_xpath_signature())
5452 .expect_err("RetrievalMethod XPath must consume the signature parse budget");
5453
5454 assert!(
5455 matches!(
5456 &error,
5457 SignatureVerificationPipelineError::Policy(
5458 crate::policy::PolicyViolation::ResourceLimit {
5459 resource: "XPath expressions",
5460 maximum: 0,
5461 actual: 1,
5462 }
5463 )
5464 ),
5465 "unexpected error: {error:?}"
5466 );
5467 }
5468
5469 #[test]
5470 fn retrieval_method_xpath_obeys_expression_byte_limit() {
5471 let expression = "ancestor-or-self::ds:X509Data";
5474 let padded_expression = format!(" {expression} ");
5475 let xml = retrieval_method_xpath_signature().replace(expression, &padded_expression);
5476 let mut policy = crate::policy::VerificationPolicy::default();
5477 policy.resources.max_xpath_expression_bytes = expression.len();
5478
5479 let error = VerifyContext::new()
5480 .policy(policy)
5481 .verify(&xml)
5482 .expect_err("RetrievalMethod XPath must obey the expression byte limit");
5483
5484 assert!(
5485 matches!(
5486 &error,
5487 SignatureVerificationPipelineError::Policy(
5488 crate::policy::PolicyViolation::ResourceLimit {
5489 resource: "XPath expression bytes",
5490 maximum,
5491 actual,
5492 }
5493 ) if *maximum == expression.len() && *actual == padded_expression.len()
5494 ),
5495 "unexpected error: {error:?}"
5496 );
5497 }
5498
5499 #[test]
5500 fn retrieval_method_xpath_obeys_expression_complexity_limit() {
5501 let mut policy = crate::policy::VerificationPolicy::default();
5504 policy.resources.max_xpath_expression_complexity = 0;
5505
5506 let error = VerifyContext::new()
5507 .policy(policy)
5508 .verify(&retrieval_method_xpath_signature())
5509 .expect_err("RetrievalMethod XPath must obey the complexity limit");
5510
5511 assert!(
5512 matches!(
5513 &error,
5514 SignatureVerificationPipelineError::Policy(
5515 crate::policy::PolicyViolation::ResourceLimit {
5516 resource: "XPath expression complexity",
5517 maximum: 0,
5518 actual,
5519 }
5520 ) if *actual > 0
5521 ),
5522 "unexpected error: {error:?}"
5523 );
5524 }
5525
5526 #[test]
5527 fn retrieval_method_xpath_uses_node_filter_work_budget() {
5528 let mut policy = crate::policy::VerificationPolicy::default();
5531 policy.resources.max_node_set_filter_work = 4;
5532 let xml = retrieval_method_xpath_signature().replace(
5533 "<holder Id=\"target\">",
5534 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
5535 );
5536
5537 let error = VerifyContext::new()
5538 .policy(policy)
5539 .verify(&xml)
5540 .expect_err("RetrievalMethod XPath must consume node-filter work");
5541
5542 assert!(
5543 matches!(
5544 &error,
5545 SignatureVerificationPipelineError::Policy(
5546 crate::policy::PolicyViolation::ResourceLimit {
5547 resource: crate::policy::resource_name::NODE_SET_FILTER_WORK,
5548 maximum: 4,
5549 actual,
5550 }
5551 ) if *actual > 4
5552 ),
5553 "unexpected error: {error:?}"
5554 );
5555 }
5556
5557 #[test]
5558 fn retrieval_method_xpath_charges_every_context_to_evaluation_work() {
5559 let mut policy = crate::policy::VerificationPolicy::default();
5562 policy.resources.max_xpath_evaluation_work = 4;
5563 let xml = retrieval_method_xpath_signature().replace(
5564 "<holder Id=\"target\">",
5565 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
5566 );
5567
5568 let error = VerifyContext::new()
5569 .policy(policy)
5570 .verify(&xml)
5571 .expect_err("RetrievalMethod XPath must charge every evaluation context");
5572
5573 assert!(matches!(
5574 error,
5575 SignatureVerificationPipelineError::Policy(
5576 crate::policy::PolicyViolation::ResourceLimit {
5577 resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
5578 maximum: 4,
5579 actual,
5580 }
5581 ) if actual > 4
5582 ));
5583 }
5584
5585 #[test]
5586 fn retrieval_method_xpath_obeys_namespace_binding_limit() {
5587 let mut policy = crate::policy::VerificationPolicy::default();
5590 policy.resources.max_xpath_namespace_bindings = 0;
5591
5592 let error = VerifyContext::new()
5593 .policy(policy)
5594 .verify(&retrieval_method_xpath_signature())
5595 .expect_err("RetrievalMethod XPath namespaces must obey the binding limit");
5596
5597 assert!(matches!(
5598 error,
5599 SignatureVerificationPipelineError::Policy(
5600 crate::policy::PolicyViolation::ResourceLimit {
5601 resource: crate::policy::resource_name::XPATH_NAMESPACE_BINDINGS,
5602 maximum: 0,
5603 actual,
5604 }
5605 ) if actual > 0
5606 ));
5607 }
5608
5609 #[test]
5610 fn retrieval_method_xpath_obeys_namespace_byte_limit() {
5611 let mut policy = crate::policy::VerificationPolicy::default();
5614 policy.resources.max_xpath_namespace_bytes = 0;
5615
5616 let error = VerifyContext::new()
5617 .policy(policy)
5618 .verify(&retrieval_method_xpath_signature())
5619 .expect_err("RetrievalMethod XPath namespaces must obey the byte limit");
5620
5621 assert!(matches!(
5622 error,
5623 SignatureVerificationPipelineError::Policy(
5624 crate::policy::PolicyViolation::ResourceLimit {
5625 resource: crate::policy::resource_name::XPATH_NAMESPACE_BYTES,
5626 maximum: 0,
5627 actual,
5628 }
5629 ) if actual > 0
5630 ));
5631 }
5632
5633 #[test]
5634 fn retrieval_method_xpath_obeys_context_evaluation_limit() {
5635 let mut policy = crate::policy::VerificationPolicy::default();
5639 policy.resources.max_xpath_context_evaluations = 4;
5640 let xml = retrieval_method_xpath_signature().replace(
5641 "<holder Id=\"target\">",
5642 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
5643 );
5644
5645 let error = VerifyContext::new()
5646 .policy(policy)
5647 .verify(&xml)
5648 .expect_err("RetrievalMethod XPath contexts must obey the evaluation limit");
5649
5650 assert!(matches!(
5651 error,
5652 SignatureVerificationPipelineError::Policy(
5653 crate::policy::PolicyViolation::ResourceLimit {
5654 resource: crate::policy::resource_name::XPATH_CONTEXT_EVALUATIONS,
5655 maximum: 4,
5656 actual,
5657 }
5658 ) if actual > 4
5659 ));
5660 }
5661
5662 #[test]
5663 fn retrieval_method_materializes_direct_untransformed_x509_data() {
5664 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5667 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
5668 <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
5669 </root>"##;
5670 let document = Document::parse(xml).unwrap();
5671 let key_info_node = document
5672 .descendants()
5673 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5674 .unwrap();
5675 let mut key_info = parse_key_info(key_info_node).unwrap();
5676
5677 materialize_retrieval_methods(
5678 &mut key_info,
5679 &UriReferenceResolver::new(&document),
5680 UriTypeSet::SAME_DOCUMENT,
5681 None,
5682 crate::provider::default_provider(),
5683 )
5684 .expect("a direct X509Data target needs no transform");
5685 assert!(matches!(
5686 key_info.sources.as_slice(),
5687 [super::super::parse::KeyInfoSource::X509Data(info)]
5688 if info.subject_names == ["CN=leaf"]
5689 ));
5690 }
5691
5692 #[test]
5693 fn retrieval_method_respects_configured_same_document_id_semantics() {
5694 fn materialize(
5697 uri: &str,
5698 id: &str,
5699 semantics: crate::policy::SameDocumentIdSemantics,
5700 ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
5701 let xml = format!(
5702 r#"<root xmlns:ds="{XMLDSIG_NS}">
5703 <ds:KeyInfo><ds:RetrievalMethod URI="{uri}" Type="{XMLDSIG_NS}X509Data"/></ds:KeyInfo>
5704 <ds:X509Data Id="{id}"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
5705 </root>"#
5706 );
5707 let document = Document::parse(&xml).unwrap();
5708 let key_info_node = document
5709 .descendants()
5710 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5711 .unwrap();
5712 let mut key_info = parse_key_info(key_info_node).unwrap();
5713 let resolver =
5714 UriReferenceResolver::new(&document).with_same_document_id_semantics(semantics);
5715
5716 materialize_retrieval_methods(
5717 &mut key_info,
5718 &resolver,
5719 UriTypeSet::SAME_DOCUMENT,
5720 None,
5721 crate::provider::default_provider(),
5722 )
5723 }
5724
5725 assert!(
5726 materialize(
5727 "#12345",
5728 "12345",
5729 crate::policy::SameDocumentIdSemantics::Specification,
5730 )
5731 .is_err(),
5732 "the standards mode must reject a non-NCName bare fragment"
5733 );
5734 assert!(
5735 materialize(
5736 "#visa'3d",
5737 "visa'3d",
5738 crate::policy::SameDocumentIdSemantics::XmlSecBarename,
5739 )
5740 .is_err(),
5741 "the donor barename wrapper cannot represent an apostrophe"
5742 );
5743 assert!(
5744 materialize(
5745 "#visa'3d",
5746 "visa'3d",
5747 crate::policy::SameDocumentIdSemantics::XmlSecVisa3d,
5748 )
5749 .is_ok(),
5750 "Visa3D mode resolves the registered ID without an XPointer literal"
5751 );
5752 }
5753
5754 #[test]
5755 fn raw_x509_retrieval_method_uses_inherited_xml_base() {
5756 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
5759 let xml = format!(
5760 r#"<root xml:base="https://example.test/keys/nested/" xmlns:ds="{XMLDSIG_NS}">
5761 <ds:KeyInfo><ds:RetrievalMethod URI="../signer.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>
5762 </root>"#
5763 );
5764 let document = Document::parse(&xml).unwrap();
5765 let key_info_node = document
5766 .descendants()
5767 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5768 .unwrap();
5769 let mut key_info = parse_key_info(key_info_node).unwrap();
5770 let certificate = include_bytes!(
5771 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
5772 )
5773 .to_vec();
5774 let resources = HashMap::from([(
5775 "https://example.test/keys/signer.der".to_string(),
5776 certificate,
5777 )]);
5778 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5779
5780 materialize_retrieval_methods(
5781 &mut key_info,
5782 &resolver,
5783 UriTypeSet::ALL,
5784 None,
5785 crate::provider::default_provider(),
5786 )
5787 .expect("RetrievalMethod should resolve against inherited xml:base");
5788
5789 assert!(matches!(
5790 key_info.sources.as_slice(),
5791 [super::super::parse::KeyInfoSource::X509Data(info)]
5792 if info.certificates.len() == 1
5793 ));
5794 }
5795
5796 #[test]
5797 fn retrieval_method_requires_xpath_for_x509_data_below_uri_root() {
5798 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5801 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
5802 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
5803 </root>"##;
5804 let document = Document::parse(xml).unwrap();
5805 let key_info_node = document
5806 .descendants()
5807 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5808 .unwrap();
5809 let mut key_info = parse_key_info(key_info_node).unwrap();
5810
5811 let error = materialize_retrieval_methods(
5812 &mut key_info,
5813 &UriReferenceResolver::new(&document),
5814 UriTypeSet::SAME_DOCUMENT,
5815 None,
5816 crate::provider::default_provider(),
5817 )
5818 .expect_err("a wrapper target requires an explicit selection transform");
5819 assert!(matches!(
5820 error,
5821 SignatureVerificationPipelineError::InvalidStructure {
5822 reason: "untransformed X509Data RetrievalMethod must target X509Data directly"
5823 }
5824 ));
5825 }
5826
5827 #[test]
5828 fn retrieval_method_rejects_target_inside_external_x509_data_ancestor() {
5829 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5832 <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>
5833 <ds:X509Data><ds:X509SubjectName Id="target">CN=leaf</ds:X509SubjectName></ds:X509Data>
5834 </root>"##;
5835 let document = Document::parse(xml).unwrap();
5836 let key_info_node = document
5837 .descendants()
5838 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5839 .unwrap();
5840 let mut key_info = parse_key_info(key_info_node).unwrap();
5841
5842 let error = materialize_retrieval_methods(
5843 &mut key_info,
5844 &UriReferenceResolver::new(&document),
5845 UriTypeSet::SAME_DOCUMENT,
5846 None,
5847 crate::provider::default_provider(),
5848 )
5849 .expect_err("filter output without an X509Data root must be rejected");
5850 assert!(matches!(
5851 error,
5852 SignatureVerificationPipelineError::InvalidStructure {
5853 reason: "X509Data RetrievalMethod selected no X509Data element"
5854 }
5855 ));
5856 }
5857
5858 #[test]
5859 fn retrieval_method_rejects_ambiguous_x509_data_relation() {
5860 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5862 <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>
5863 <holder Id="target"><ds:X509Data/><ds:X509Data/></holder>
5864 </root>"##;
5865 let document = Document::parse(xml).unwrap();
5866 let key_info_node = document
5867 .descendants()
5868 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5869 .unwrap();
5870 let mut key_info = parse_key_info(key_info_node).unwrap();
5871
5872 let error = materialize_retrieval_methods(
5873 &mut key_info,
5874 &UriReferenceResolver::new(&document),
5875 UriTypeSet::SAME_DOCUMENT,
5876 None,
5877 crate::provider::default_provider(),
5878 )
5879 .expect_err("multiple transformed X509Data roots must be rejected");
5880 assert!(matches!(
5881 error,
5882 SignatureVerificationPipelineError::InvalidStructure {
5883 reason: "X509Data RetrievalMethod selected multiple X509Data elements"
5884 }
5885 ));
5886 }
5887
5888 #[test]
5889 fn retrieval_method_materialization_preserves_key_info_order() {
5890 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5893 <ds:KeyInfo>
5894 <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>
5895 <ds:KeyName>fallback</ds:KeyName>
5896 </ds:KeyInfo>
5897 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
5898 </root>"##;
5899 let document = Document::parse(xml).unwrap();
5900 let key_info_node = document
5901 .descendants()
5902 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5903 .unwrap();
5904 let mut key_info = parse_key_info(key_info_node).unwrap();
5905
5906 materialize_retrieval_methods(
5907 &mut key_info,
5908 &UriReferenceResolver::new(&document),
5909 UriTypeSet::SAME_DOCUMENT,
5910 None,
5911 crate::provider::default_provider(),
5912 )
5913 .unwrap();
5914 assert!(matches!(
5915 key_info.sources.as_slice(),
5916 [
5917 super::super::parse::KeyInfoSource::X509Data(_),
5918 super::super::parse::KeyInfoSource::KeyName(name)
5919 ] if name == "fallback"
5920 ));
5921 }
5922
5923 #[test]
5924 fn retrieval_method_materialization_bounds_repeated_sources() {
5925 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
5928 let certificate = include_bytes!(
5929 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
5930 )
5931 .to_vec();
5932 let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
5933 let mut key_info = KeyInfo {
5934 sources: (0..=64)
5935 .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
5936 uri: "urn:certificate".into(),
5937 resource_type: Some(RAW_X509_TYPE.into()),
5938 transforms: RetrievalMethodTransforms::None,
5939 })
5940 .collect(),
5941 };
5942 let document = Document::parse("<root/>").unwrap();
5943 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5944
5945 let error = materialize_retrieval_methods(
5946 &mut key_info,
5947 &resolver,
5948 UriTypeSet::ALL,
5949 None,
5950 crate::provider::default_provider(),
5951 )
5952 .expect_err("retrieval count must be bounded before materialization");
5953 assert!(matches!(
5954 error,
5955 SignatureVerificationPipelineError::InvalidStructure {
5956 reason: "KeyInfo contains too many RetrievalMethod elements"
5957 }
5958 ));
5959 }
5960
5961 #[test]
5962 fn key_info_reference_materializes_an_allowed_empty_uri() {
5963 let document = XmlDocument::parse(format!(
5967 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>root-key</ds:KeyName></ds:KeyInfo>"#
5968 ))
5969 .unwrap();
5970 let mut key_info = KeyInfo {
5971 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
5972 uri: String::new(),
5973 }],
5974 };
5975 let mut policy = crate::policy::VerificationPolicy::default();
5976 policy.key_sources.key_info_reference = true;
5977 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
5978 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
5979 let mut budgets = RetrievalMaterializationBudgets {
5980 xpath_parse: &mut xpath_parse_budget,
5981 execution: &execution_budget,
5982 resources: &policy.resources,
5983 xml_backend: crate::XmlBackend::default(),
5984 };
5985 let mut materialization = KeyInfoMaterializationState::default();
5986
5987 document
5988 .with_view(|view| {
5989 materialize_key_info_references_with_budgets(
5990 &mut key_info,
5991 &UriReferenceResolver::with_document_view(view, &[]),
5992 &policy,
5993 crate::provider::default_provider(),
5994 &mut budgets,
5995 &mut materialization,
5996 )?;
5997 Ok::<_, SignatureVerificationPipelineError>(())
5998 })
5999 .unwrap();
6000
6001 assert!(matches!(
6002 key_info.sources.as_slice(),
6003 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "root-key"
6004 ));
6005 }
6006
6007 #[test]
6008 fn empty_key_info_reference_participates_in_cycle_detection() {
6009 let document = XmlDocument::parse(format!(
6013 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#"><dsig11:KeyInfoReference URI=""/></ds:KeyInfo>"#
6014 ))
6015 .unwrap();
6016 let mut key_info = KeyInfo {
6017 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
6018 uri: String::new(),
6019 }],
6020 };
6021 let mut policy = crate::policy::VerificationPolicy::default();
6022 policy.key_sources.key_info_reference = true;
6023 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
6024 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
6025 let mut budgets = RetrievalMaterializationBudgets {
6026 xpath_parse: &mut xpath_parse_budget,
6027 execution: &execution_budget,
6028 resources: &policy.resources,
6029 xml_backend: crate::XmlBackend::default(),
6030 };
6031 let mut materialization = KeyInfoMaterializationState::default();
6032
6033 let error = document
6034 .with_view(|view| {
6035 materialize_key_info_references_with_budgets(
6036 &mut key_info,
6037 &UriReferenceResolver::with_document_view(view, &[]),
6038 &policy,
6039 crate::provider::default_provider(),
6040 &mut budgets,
6041 &mut materialization,
6042 )
6043 })
6044 .expect_err("empty-URI self-reference must be rejected as a cycle");
6045
6046 assert!(matches!(
6047 error,
6048 SignatureVerificationPipelineError::InvalidStructure {
6049 reason: "KeyInfoReference cycle detected"
6050 }
6051 ));
6052 }
6053
6054 #[test]
6055 fn key_info_materialization_shares_candidate_work_across_source_kinds() {
6056 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
6059 let document = XmlDocument::parse(format!(
6060 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>"##
6061 ))
6062 .unwrap();
6063 let certificate = include_bytes!(
6064 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
6065 )
6066 .to_vec();
6067 let resources = HashMap::from([("signer.der".to_owned(), certificate)]);
6068 let mut key_info = KeyInfo {
6069 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
6070 uri: "#target".into(),
6071 }],
6072 };
6073 let mut policy = crate::policy::VerificationPolicy::default();
6074 policy.key_sources.key_info_reference = true;
6075 policy.uris.retrieval_methods = UriTypeSet::ALL;
6076 policy.resources.max_key_candidates = 2;
6077 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
6078 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
6079 let mut budgets = RetrievalMaterializationBudgets {
6080 xpath_parse: &mut xpath_parse_budget,
6081 execution: &execution_budget,
6082 resources: &policy.resources,
6083 xml_backend: crate::XmlBackend::default(),
6084 };
6085 let mut materialization = KeyInfoMaterializationState::default();
6086
6087 let error = document
6088 .with_view(|view| {
6089 let resolver = UriReferenceResolver::with_document_view(view, &[])
6090 .with_external_resources(&resources);
6091 let mut outcome = materialize_key_info_references_with_budgets(
6092 &mut key_info,
6093 &resolver,
6094 &policy,
6095 crate::provider::default_provider(),
6096 &mut budgets,
6097 &mut materialization,
6098 )?;
6099 outcome.merge(materialize_retrieval_methods_with_budgets(
6100 &mut key_info,
6101 &resolver,
6102 policy.uris.retrieval_methods,
6103 policy.transforms.allowed_algorithms.as_ref(),
6104 crate::provider::default_provider(),
6105 &mut budgets,
6106 &mut materialization.candidate_work,
6107 )?);
6108 Ok::<_, DsigError>(outcome)
6109 })
6110 .expect_err("all materializers must share the candidate-work limit");
6111
6112 assert!(matches!(
6113 error,
6114 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
6115 resource: crate::policy::resource_name::KEY_CANDIDATES,
6116 maximum: 2,
6117 actual: 3,
6118 })
6119 ));
6120 }
6121
6122 #[test]
6123 fn public_key_info_materialization_binds_external_resource_policy() {
6124 let document = Document::parse("<root/>").unwrap();
6127 let external = format!(
6128 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
6129 );
6130 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
6131 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6132 let mut key_info = KeyInfo {
6133 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
6134 uri: "key.xml".into(),
6135 }],
6136 };
6137 let mut policy = crate::policy::VerificationPolicy::default();
6138 policy.key_sources.key_info_reference = true;
6139 policy.uris.key_info_references = UriTypeSet::ALL;
6140 policy.resources.max_external_resource_bytes = 0;
6141
6142 let error = materialize_verification_key_info_references(
6143 &mut key_info,
6144 resolver,
6145 &policy,
6146 crate::provider::default_provider(),
6147 crate::XmlBackend::default(),
6148 )
6149 .expect_err("policy must reject non-empty external KeyInfo bytes");
6150
6151 assert!(matches!(
6152 error,
6153 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
6154 resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
6155 maximum: 0,
6156 actual,
6157 }) if actual == resources["key.xml"].len()
6158 ));
6159 }
6160
6161 #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
6162 #[test]
6163 fn public_key_info_materialization_uses_the_selected_backend() {
6164 let external = format!(
6167 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
6168 );
6169 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
6170 let document = Document::parse("<root/>").unwrap();
6171
6172 for backend in [crate::XmlBackend::Xmloxide, crate::XmlBackend::Roxmltree] {
6173 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6174 let mut key_info = KeyInfo {
6175 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
6176 uri: "key.xml".into(),
6177 }],
6178 };
6179 let mut policy = crate::policy::VerificationPolicy::default();
6180 policy.key_sources.key_info_reference = true;
6181 policy.uris.key_info_references = UriTypeSet::ALL;
6182
6183 materialize_verification_key_info_references(
6184 &mut key_info,
6185 resolver,
6186 &policy,
6187 crate::provider::default_provider(),
6188 backend,
6189 )
6190 .unwrap_or_else(|error| panic!("{backend:?} materialization failed: {error}"));
6191
6192 assert!(matches!(
6193 key_info.sources.as_slice(),
6194 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "external"
6195 ));
6196 }
6197 }
6198
6199 #[test]
6200 fn public_key_info_materialization_defers_nested_retrieval_failure() {
6201 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
6205 let external = format!(
6206 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>usable</ds:KeyName><ds:RetrievalMethod URI="missing.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>"#
6207 );
6208 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
6209 let document = Document::parse("<root/>").unwrap();
6210
6211 for operation in ["signing", "verification"] {
6212 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6213 let mut key_info = KeyInfo {
6214 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
6215 uri: "key.xml".into(),
6216 }],
6217 };
6218 match operation {
6219 "signing" => {
6220 let mut policy = crate::policy::SigningPolicy::default();
6221 policy.uris.key_info_references = UriTypeSet::ALL;
6222 policy.uris.retrieval_methods = UriTypeSet::ALL;
6223 materialize_signing_key_info_references(
6224 &mut key_info,
6225 resolver,
6226 &policy,
6227 crate::provider::default_provider(),
6228 crate::XmlBackend::default(),
6229 )
6230 }
6231 "verification" => {
6232 let mut policy = crate::policy::VerificationPolicy::default();
6233 policy.key_sources.key_info_reference = true;
6234 policy.uris.key_info_references = UriTypeSet::ALL;
6235 policy.uris.retrieval_methods = UriTypeSet::ALL;
6236 materialize_verification_key_info_references(
6237 &mut key_info,
6238 resolver,
6239 &policy,
6240 crate::provider::default_provider(),
6241 crate::XmlBackend::default(),
6242 )
6243 }
6244 _ => unreachable!(),
6245 }
6246 .unwrap_or_else(|error| panic!("{operation} materialization failed: {error}"));
6247
6248 assert!(matches!(
6249 key_info.sources.as_slice(),
6250 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "usable"
6251 ));
6252 }
6253 }
6254
6255 #[test]
6256 fn public_key_info_materialization_binds_xpath_policy() {
6257 let external = format!(
6260 r##"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}">
6261 <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
6262 <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
6263 <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
6264 </ds:Transform></ds:Transforms>
6265 </ds:RetrievalMethod>
6266 <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
6267 </ds:KeyInfo>"##
6268 );
6269 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
6270 let document = Document::parse("<root/>").unwrap();
6271
6272 for operation in ["signing", "verification"] {
6273 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6274 let mut key_info = KeyInfo {
6275 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
6276 uri: "key.xml".into(),
6277 }],
6278 };
6279 let error = match operation {
6280 "signing" => {
6281 let mut policy = crate::policy::SigningPolicy::default();
6282 policy.uris.key_info_references = UriTypeSet::ALL;
6283 policy.resources.max_xpath_expressions = 0;
6284 materialize_signing_key_info_references(
6285 &mut key_info,
6286 resolver,
6287 &policy,
6288 crate::provider::default_provider(),
6289 crate::XmlBackend::default(),
6290 )
6291 }
6292 "verification" => {
6293 let mut policy = crate::policy::VerificationPolicy::default();
6294 policy.key_sources.key_info_reference = true;
6295 policy.uris.key_info_references = UriTypeSet::ALL;
6296 policy.resources.max_xpath_expressions = 0;
6297 materialize_verification_key_info_references(
6298 &mut key_info,
6299 resolver,
6300 &policy,
6301 crate::provider::default_provider(),
6302 crate::XmlBackend::default(),
6303 )
6304 }
6305 _ => unreachable!(),
6306 }
6307 .expect_err("the public helper must enforce the supplied XPath expression limit");
6308
6309 assert!(
6310 matches!(
6311 error,
6312 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
6313 resource: crate::policy::resource_name::XPATH_EXPRESSIONS,
6314 maximum: 0,
6315 actual: 1,
6316 })
6317 ),
6318 "unexpected {operation} error: {error:?}"
6319 );
6320 }
6321 }
6322
6323 fn materialize_external_key_info_chain(
6324 terminal_reference: &str,
6325 ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
6326 let a = format!(
6327 r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
6328 <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
6329 <ds:KeyInfo ID="next"><dsig11:KeyInfoReference URI="b.xml#root"/></ds:KeyInfo>
6330 </doc>"##
6331 );
6332 let b = format!(
6333 r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
6334 <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
6335 <ds:KeyInfo ID="next">{terminal_reference}</ds:KeyInfo>
6336 </doc>"##
6337 );
6338 let resources = HashMap::from([
6339 ("a.xml".to_owned(), a.into_bytes()),
6340 ("b.xml".to_owned(), b.into_bytes()),
6341 ]);
6342 let document = XmlDocument::parse("<root/>").unwrap();
6343 let mut key_info = KeyInfo {
6344 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
6345 uri: "a.xml#root".into(),
6346 }],
6347 };
6348 let mut policy = crate::policy::VerificationPolicy::default();
6349 policy.key_sources.key_info_reference = true;
6350 policy.uris.key_info_references = UriTypeSet::ALL;
6351 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
6352 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
6353 let mut budgets = RetrievalMaterializationBudgets {
6354 xpath_parse: &mut xpath_parse_budget,
6355 execution: &execution_budget,
6356 resources: &policy.resources,
6357 xml_backend: crate::XmlBackend::default(),
6358 };
6359 let mut materialization = KeyInfoMaterializationState::default();
6360
6361 document.with_view(|view| {
6362 let resolver = UriReferenceResolver::with_document_view(view, &[])
6363 .with_external_resources(&resources);
6364 materialize_key_info_references_with_budgets(
6365 &mut key_info,
6366 &resolver,
6367 &policy,
6368 crate::provider::default_provider(),
6369 &mut budgets,
6370 &mut materialization,
6371 )?;
6372 Ok::<_, SignatureVerificationPipelineError>(())
6373 })?;
6374 Ok(key_info)
6375 }
6376
6377 fn materialize_external_key_info_bytes(
6378 encoded: Vec<u8>,
6379 ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
6380 let resources = HashMap::from([("key.xml".to_owned(), encoded)]);
6381 let document = XmlDocument::parse("<root/>").unwrap();
6382 let mut key_info = KeyInfo {
6383 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
6384 uri: "key.xml".into(),
6385 }],
6386 };
6387 let mut policy = crate::policy::VerificationPolicy::default();
6388 policy.key_sources.key_info_reference = true;
6389 policy.uris.key_info_references = UriTypeSet::ALL;
6390 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
6391 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
6392 let mut budgets = RetrievalMaterializationBudgets {
6393 xpath_parse: &mut xpath_parse_budget,
6394 execution: &execution_budget,
6395 resources: &policy.resources,
6396 xml_backend: crate::XmlBackend::default(),
6397 };
6398 let mut materialization = KeyInfoMaterializationState::default();
6399
6400 document.with_view(|view| {
6401 let resolver = UriReferenceResolver::with_document_view(view, &[])
6402 .with_external_resources(&resources);
6403 materialize_key_info_references_with_budgets(
6404 &mut key_info,
6405 &resolver,
6406 &policy,
6407 crate::provider::default_provider(),
6408 &mut budgets,
6409 &mut materialization,
6410 )?;
6411 Ok::<_, SignatureVerificationPipelineError>(())
6412 })?;
6413 Ok(key_info)
6414 }
6415
6416 #[test]
6417 fn external_key_info_reference_decodes_utf16_xml_octets() {
6418 let xml = format!(
6421 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>utf16-key</ds:KeyName></ds:KeyInfo>"#
6422 );
6423 let mut encoded = vec![0xff, 0xfe];
6424 encoded.extend(xml.encode_utf16().flat_map(u16::to_le_bytes));
6425 let key_info = materialize_external_key_info_bytes(encoded)
6426 .expect("UTF-16 external KeyInfo must materialize");
6427 assert!(matches!(
6428 key_info.sources.as_slice(),
6429 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "utf16-key"
6430 ));
6431 }
6432
6433 #[test]
6434 fn external_key_info_reference_rejects_malformed_xml_encoding() {
6435 let error = materialize_external_key_info_bytes(vec![0xff, 0xfe, b'<'])
6438 .expect_err("truncated UTF-16 must be rejected during octet decoding");
6439
6440 assert!(matches!(
6441 error,
6442 SignatureVerificationPipelineError::InvalidStructure {
6443 reason: "KeyInfoReference external resource has an invalid XML encoding"
6444 }
6445 ));
6446 }
6447
6448 #[test]
6449 fn key_info_reference_cycle_identity_includes_the_owning_resource() {
6450 let key_info = materialize_external_key_info_chain("<ds:KeyName>terminal</ds:KeyName>")
6453 .expect("cross-document duplicate fragments must materialize");
6454 assert!(matches!(
6455 key_info.sources.as_slice(),
6456 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "terminal"
6457 ));
6458 }
6459
6460 #[test]
6461 fn key_info_reference_cycle_identity_survives_external_reparse() {
6462 let error =
6465 materialize_external_key_info_chain("<dsig11:KeyInfoReference URI=\"a.xml#root\"/>")
6466 .expect_err("a resource cycle must fail before exhausting depth");
6467 assert!(matches!(
6468 error,
6469 SignatureVerificationPipelineError::InvalidStructure {
6470 reason: "KeyInfoReference cycle detected"
6471 }
6472 ));
6473 }
6474
6475 #[test]
6476 fn retrieval_method_materialization_deduplicates_within_count_limit() {
6477 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
6480 let certificate = include_bytes!(
6481 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
6482 )
6483 .to_vec();
6484 let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
6485 let mut key_info = KeyInfo {
6486 sources: (0..MAX_RETRIEVAL_METHOD_COUNT)
6487 .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
6488 uri: "urn:certificate".into(),
6489 resource_type: Some(RAW_X509_TYPE.into()),
6490 transforms: RetrievalMethodTransforms::None,
6491 })
6492 .collect(),
6493 };
6494 let document = Document::parse("<root/>").unwrap();
6495 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6496
6497 materialize_retrieval_methods(
6498 &mut key_info,
6499 &resolver,
6500 UriTypeSet::ALL,
6501 None,
6502 crate::provider::default_provider(),
6503 )
6504 .unwrap();
6505 assert!(matches!(
6506 key_info.sources.as_slice(),
6507 [super::super::parse::KeyInfoSource::X509Data(info)]
6508 if info.certificates.len() == 1
6509 ));
6510 }
6511
6512 #[test]
6513 fn retrieval_method_candidate_budget_includes_embedded_key_values() {
6514 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
6517 let resources = HashMap::from([("urn:certificate".to_string(), vec![1, 2, 3])]);
6518 let mut key_info = KeyInfo {
6519 sources: vec![
6520 super::super::parse::KeyInfoSource::KeyValue(
6521 super::super::parse::KeyValueInfo::Unsupported {
6522 namespace: Some(XMLDSIG_NS.into()),
6523 local_name: "FutureKeyValue".into(),
6524 },
6525 ),
6526 super::super::parse::KeyInfoSource::RetrievalMethod {
6527 uri: "urn:certificate".into(),
6528 resource_type: Some(RAW_X509_TYPE.into()),
6529 transforms: RetrievalMethodTransforms::None,
6530 },
6531 ],
6532 };
6533 let document = Document::parse("<root/>").unwrap();
6534 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6535 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
6536 let execution_budget = TransformExecutionBudget::default();
6537 let resource_policy = crate::policy::ResourcePolicy {
6538 max_key_candidates: 1,
6539 ..crate::policy::ResourcePolicy::default()
6540 };
6541 let mut budgets = RetrievalMaterializationBudgets {
6542 xpath_parse: &mut xpath_parse_budget,
6543 execution: &execution_budget,
6544 resources: &resource_policy,
6545 xml_backend: crate::XmlBackend::default(),
6546 };
6547 let mut candidate_work = key_info.embedded_candidate_count();
6548
6549 let error = materialize_retrieval_methods_with_budgets(
6550 &mut key_info,
6551 &resolver,
6552 UriTypeSet::ALL,
6553 None,
6554 crate::provider::default_provider(),
6555 &mut budgets,
6556 &mut candidate_work,
6557 )
6558 .expect_err("the retrieved certificate must exceed the aggregate candidate limit");
6559
6560 assert!(matches!(
6561 error,
6562 SignatureVerificationPipelineError::Policy(
6563 crate::policy::PolicyViolation::ResourceLimit {
6564 resource: crate::policy::resource_name::KEY_CANDIDATES,
6565 maximum: 1,
6566 actual: 2,
6567 }
6568 )
6569 ));
6570 }
6571
6572 #[test]
6573 fn raw_x509_retrieval_rejects_empty_same_document_uri() {
6574 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
6577 let certificate = include_bytes!(
6578 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
6579 )
6580 .to_vec();
6581 let resources = HashMap::from([(String::new(), certificate)]);
6582 let mut key_info = KeyInfo {
6583 sources: vec![super::super::parse::KeyInfoSource::RetrievalMethod {
6584 uri: String::new(),
6585 resource_type: Some(RAW_X509_TYPE.into()),
6586 transforms: RetrievalMethodTransforms::None,
6587 }],
6588 };
6589 let document = Document::parse("<root/>").unwrap();
6590 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6591
6592 let error = materialize_retrieval_methods(
6593 &mut key_info,
6594 &resolver,
6595 UriTypeSet::ALL,
6596 None,
6597 crate::provider::default_provider(),
6598 )
6599 .expect_err("empty URI must retain same-document semantics");
6600 assert!(matches!(
6601 error,
6602 SignatureVerificationPipelineError::InvalidStructure {
6603 reason: "raw X509 RetrievalMethod requires an untransformed external URI"
6604 }
6605 ));
6606 }
6607
6608 #[test]
6609 fn verify_context_does_not_hide_malformed_digest_behind_unsupported_transform() {
6610 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6612 let xml = xml.replacen(
6613 "<ds:Reference URI=\"#target\">",
6614 "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
6615 1,
6616 );
6617 replace_fixture_manifest_digest(&xml, "!!!")
6618 });
6619
6620 let error = VerifyContext::new()
6621 .key(&AcceptingKey)
6622 .process_manifests(true)
6623 .verify(&broken_xml)
6624 .expect_err("malformed Manifest digest must not become a validity result");
6625 assert!(matches!(
6626 error,
6627 SignatureVerificationPipelineError::ParseManifestReference(_)
6628 ));
6629 }
6630
6631 #[test]
6632 fn verify_context_rejects_manifest_non_whitespace_mixed_content() {
6633 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6636 xml.replacen(
6637 "<ds:Manifest ID=\"manifest\">",
6638 "<ds:Manifest ID=\"manifest\">junk",
6639 1,
6640 )
6641 });
6642
6643 let err = VerifyContext::new()
6644 .key(&AcceptingKey)
6645 .process_manifests(true)
6646 .verify(&xml)
6647 .expect_err("Manifest mixed content must fail verification");
6648 assert!(matches!(
6649 err,
6650 SignatureVerificationPipelineError::InvalidStructure {
6651 reason: "Manifest contains non-whitespace mixed content"
6652 }
6653 ));
6654 }
6655
6656 #[test]
6657 fn verify_context_rejects_empty_manifest_children() {
6658 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6661 let (prefix, rest) = xml
6662 .split_once("<ds:Manifest ID=\"manifest\">")
6663 .expect("fixture should contain Manifest");
6664 let (_, suffix) = rest
6665 .split_once("</ds:Manifest>")
6666 .expect("fixture should contain closing Manifest");
6667 format!("{prefix}<ds:Manifest ID=\"manifest\"></ds:Manifest>{suffix}")
6668 });
6669
6670 let err = VerifyContext::new()
6671 .key(&AcceptingKey)
6672 .process_manifests(true)
6673 .verify(&xml)
6674 .expect_err("empty Manifest must fail verification");
6675 assert!(matches!(
6676 err,
6677 SignatureVerificationPipelineError::InvalidStructure {
6678 reason: "Manifest must contain at least one ds:Reference element child"
6679 }
6680 ));
6681 }
6682
6683 #[test]
6684 fn verify_context_ignores_unsigned_malformed_manifest_blocks() {
6685 let xml = signature_with_manifest_xml(true).replacen(
6686 "</ds:Object>",
6687 "</ds:Object><ds:Object><ds:Manifest>junk<ds:Foo/></ds:Manifest></ds:Object>",
6688 1,
6689 );
6690 let result = VerifyContext::new()
6691 .key(&AcceptingKey)
6692 .process_manifests(true)
6693 .verify(&xml)
6694 .expect("unsigned malformed Manifest must be ignored");
6695 assert_eq!(
6696 result.manifest_references.len(),
6697 1,
6698 "only signed Manifest references must be reported",
6699 );
6700 assert!(matches!(result.status, DsigStatus::Valid));
6701 }
6702
6703 #[test]
6704 fn verify_context_skips_ambiguous_manifest_id_blocks() {
6705 let xml = signature_with_manifest_xml(true).replacen(
6706 "</ds:Object>",
6707 "</ds:Object><ds:Object><ds:Manifest ID=\"manifest\">junk<ds:Foo/></ds:Manifest></ds:Object>",
6708 1,
6709 );
6710 let err = VerifyContext::new()
6711 .key(&RejectingKey)
6712 .process_manifests(true)
6713 .verify(&xml)
6714 .expect_err("ambiguous manifest IDs should make SignedInfo #manifest dereference fail");
6715 assert!(matches!(
6716 err,
6717 SignatureVerificationPipelineError::Reference(
6718 ReferenceProcessingError::UriDereference(
6719 crate::xmldsig::types::TransformError::ElementNotFound(id)
6720 )
6721 ) if id == "manifest"
6722 ));
6723 }
6724
6725 #[test]
6726 fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
6727 let xml = minimal_signature_xml("", "");
6728 let err = VerifyContext::new()
6729 .key(&RejectingKey)
6730 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
6731 .verify(&xml)
6732 .expect_err("implicit default C14N must be checked against allowlist");
6733 assert!(matches!(
6734 err,
6735 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
6736 operation: "verification transform",
6737 ..
6738 })
6739 ));
6740 }
6741
6742 #[test]
6743 fn verify_context_skips_resolver_when_reference_processing_fails() {
6744 let xml = minimal_signature_xml("", "");
6745 let result = VerifyContext::new()
6746 .key_resolver(&PanicResolver)
6747 .verify(&xml)
6748 .expect("reference digest mismatch should short-circuit before resolver");
6749 assert!(matches!(
6750 result.status,
6751 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
6752 ));
6753 }
6754
6755 #[test]
6756 fn verify_context_leaves_fail_fast_reference_tail_unexecuted() {
6757 let second_reference = r#"
6760 <ds:Reference URI="">
6761 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
6762 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6763 </ds:Reference>"#;
6764 let xml = minimal_signature_xml("", "").replacen(
6765 " </ds:SignedInfo>",
6766 &format!("{second_reference}\n </ds:SignedInfo>"),
6767 1,
6768 );
6769
6770 let result = VerifyContext::new()
6771 .key(&AcceptingKey)
6772 .verify(&xml)
6773 .expect("digest mismatch must remain a verification result");
6774
6775 assert_eq!(result.signed_info_references.len(), 1);
6776 assert!(matches!(
6777 result.signed_info_references[0].status,
6778 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
6779 ));
6780 }
6781
6782 #[test]
6783 fn verify_context_reports_key_not_found_when_resolver_misses() {
6784 let xml = signature_with_target_reference("AQ==");
6785 let result = VerifyContext::new()
6786 .key_resolver(&MissingKeyResolver)
6787 .verify(&xml)
6788 .expect("resolver miss should report status, not pipeline error");
6789 assert!(matches!(
6790 result.status,
6791 DsigStatus::Invalid(FailureReason::KeyNotFound)
6792 ));
6793 assert_eq!(
6794 result.signed_info_references.len(),
6795 1,
6796 "KeyNotFound path must preserve SignedInfo reference diagnostics",
6797 );
6798 assert!(matches!(
6799 result.signed_info_references[0].status,
6800 DsigStatus::Valid
6801 ));
6802 }
6803
6804 #[test]
6805 fn verification_candidate_budget_covers_preset_and_custom_resolver_paths() {
6806 let xml = signature_with_target_reference("AQ==");
6809 let mut policy = crate::policy::VerificationPolicy::default();
6810 policy.resources.max_key_candidates = 0;
6811
6812 let preset_error = VerifyContext::new()
6813 .key(&RejectingKey)
6814 .policy(policy.clone())
6815 .verify(&xml)
6816 .expect_err("a preset key consumes one candidate");
6817 assert!(matches!(
6818 preset_error,
6819 SignatureVerificationPipelineError::Policy(
6820 crate::policy::PolicyViolation::ResourceLimit {
6821 resource: crate::policy::resource_name::KEY_CANDIDATES,
6822 maximum: 0,
6823 actual: 1,
6824 }
6825 )
6826 ));
6827
6828 let resolver_error = VerifyContext::new()
6829 .key_resolver(&PanicResolver)
6830 .policy(policy)
6831 .verify(&xml)
6832 .expect_err("a custom resolver requires candidate capacity before dispatch");
6833 assert!(matches!(
6834 resolver_error,
6835 SignatureVerificationPipelineError::Policy(
6836 crate::policy::PolicyViolation::ResourceLimit {
6837 resource: crate::policy::resource_name::KEY_CANDIDATES,
6838 maximum: 0,
6839 actual: 1,
6840 }
6841 )
6842 ));
6843 }
6844
6845 #[test]
6846 fn verification_candidate_budget_precedes_embedded_x509_parsing() {
6847 let first_certificate = base64::engine::general_purpose::STANDARD.encode(include_bytes!(
6850 "../../tests/fixtures/xmldsig/phaos-xmldsig-three/certs/rsa-cert.der"
6851 ));
6852 let xml = signature_with_target_reference("AQ==").replace(
6853 "</ds:SignatureValue>\n </ds:Signature>",
6854 &format!(
6855 "</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>"
6856 ),
6857 );
6858 let mut policy = crate::policy::VerificationPolicy::default();
6859 policy.resources.max_key_candidates = 1;
6860
6861 let error = VerifyContext::new()
6862 .policy(policy)
6863 .verify(&xml)
6864 .expect_err("candidate policy must run before embedded certificate parsing");
6865
6866 assert!(matches!(
6867 error,
6868 SignatureVerificationPipelineError::Policy(
6869 crate::policy::PolicyViolation::ResourceLimit {
6870 resource: crate::policy::resource_name::KEY_CANDIDATES,
6871 maximum: 1,
6872 actual: 2,
6873 }
6874 )
6875 ));
6876 }
6877
6878 #[test]
6879 fn verify_context_resolver_can_ignore_malformed_keyinfo_by_default() {
6880 let base_xml = signature_with_target_reference("AQ==");
6881 let xml = base_xml
6882 .replace(
6883 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
6884 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
6885 )
6886 .replace(
6887 "</ds:SignatureValue>\n </ds:Signature>",
6888 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
6889 );
6890
6891 let result = VerifyContext::new()
6892 .key_resolver(&MissingKeyResolver)
6893 .verify(&xml)
6894 .expect("resolver path should not hard-fail on advisory malformed KeyInfo by default");
6895 assert!(matches!(
6896 result.status,
6897 DsigStatus::Invalid(FailureReason::KeyNotFound)
6898 ));
6899 }
6900
6901 #[test]
6902 fn verify_context_resolver_can_opt_in_to_keyinfo_parse_failures() {
6903 let base_xml = signature_with_target_reference("AQ==");
6904 let xml = base_xml
6905 .replace(
6906 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
6907 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
6908 )
6909 .replace(
6910 "</ds:SignatureValue>\n </ds:Signature>",
6911 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
6912 );
6913
6914 let err = VerifyContext::new()
6915 .key_resolver(&ConsumingKeyInfoResolver)
6916 .verify(&xml)
6917 .expect_err("resolver opted into KeyInfo parsing, malformed KeyInfo must fail");
6918 assert!(matches!(
6919 err,
6920 SignatureVerificationPipelineError::ParseKeyInfo(_)
6921 ));
6922 }
6923
6924 #[test]
6925 fn verify_context_ignores_unsupported_retrieval_before_valid_key_source() {
6926 let xml = signature_with_target_reference("AQ==").replace(
6929 "</ds:SignatureValue>\n </ds:Signature>",
6930 r##"</ds:SignatureValue>
6931 <ds:KeyInfo>
6932 <ds:RetrievalMethod URI="#vendor" Type="urn:vendor:key">
6933 <ds:Transforms><ds:Transform Algorithm="urn:vendor:transform"/></ds:Transforms>
6934 </ds:RetrievalMethod>
6935 <ds:KeyName>fallback</ds:KeyName>
6936 </ds:KeyInfo>
6937 </ds:Signature>"##,
6938 );
6939
6940 let result = VerifyContext::new()
6941 .key_resolver(&FallbackKeyInfoResolver)
6942 .verify(&xml)
6943 .expect("unsupported advisory retrieval must not abort key resolution");
6944 assert_eq!(result.status, DsigStatus::Valid);
6945 }
6946
6947 #[test]
6948 fn verify_context_does_not_eagerly_fail_unused_retrieval_fallback() {
6949 let xml = signature_with_target_reference("AQ==").replace(
6952 "</ds:SignatureValue>\n </ds:Signature>",
6953 r#"</ds:SignatureValue>
6954 <ds:KeyInfo>
6955 <ds:KeyName>primary</ds:KeyName>
6956 <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
6957 </ds:KeyInfo>
6958 </ds:Signature>"#,
6959 );
6960
6961 let result = VerifyContext::new()
6962 .key_resolver(&EarlyKeyInfoResolver)
6963 .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
6964 .verify(&xml)
6965 .expect("an unused missing retrieval fallback must not abort verification");
6966
6967 assert_eq!(result.status, DsigStatus::Valid);
6968 }
6969
6970 #[test]
6971 fn verify_context_does_not_eagerly_fail_unused_same_document_x509_retrieval() {
6972 let xml = signature_with_target_reference("AQ==").replace(
6975 "</ds:SignatureValue>\n </ds:Signature>",
6976 r##"</ds:SignatureValue>
6977 <ds:KeyInfo>
6978 <ds:KeyName>primary</ds:KeyName>
6979 <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
6980 </ds:KeyInfo>
6981 </ds:Signature>"##,
6982 );
6983
6984 let result = VerifyContext::new()
6985 .key_resolver(&EarlyKeyInfoResolver)
6986 .verify(&xml)
6987 .expect("an unused missing X509Data retrieval fallback must not abort verification");
6988
6989 assert_eq!(result.status, DsigStatus::Valid);
6990 }
6991
6992 #[test]
6993 fn verify_context_reports_missing_same_document_x509_retrieval_without_fallback() {
6994 let xml = signature_with_target_reference("AQ==").replace(
6997 "</ds:SignatureValue>\n </ds:Signature>",
6998 r##"</ds:SignatureValue>
6999 <ds:KeyInfo>
7000 <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
7001 </ds:KeyInfo>
7002 </ds:Signature>"##,
7003 );
7004
7005 let error = VerifyContext::new()
7006 .key_resolver(&ConsumingKeyInfoResolver)
7007 .verify(&xml)
7008 .expect_err("a sole missing X509Data retrieval must remain an explicit error");
7009
7010 assert!(matches!(
7011 error,
7012 SignatureVerificationPipelineError::InvalidStructure {
7013 reason: "X509Data RetrievalMethod target is missing or ambiguous"
7014 }
7015 ));
7016 }
7017
7018 #[test]
7019 fn verify_context_does_not_eagerly_parse_unused_retrieval_fallback() {
7020 let xml = signature_with_target_reference("AQ==").replace(
7023 "</ds:SignatureValue>\n </ds:Signature>",
7024 r#"</ds:SignatureValue>
7025 <ds:KeyInfo>
7026 <ds:KeyName>primary</ds:KeyName>
7027 <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
7028 </ds:KeyInfo>
7029 </ds:Signature>"#,
7030 );
7031 let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
7032
7033 let result = VerifyContext::new()
7034 .key_resolver(&EarlyKeyInfoResolver)
7035 .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
7036 .external_resources(&resources)
7037 .verify(&xml)
7038 .expect("an unused malformed retrieval fallback must not abort verification");
7039
7040 assert_eq!(result.status, DsigStatus::Valid);
7041 }
7042
7043 #[test]
7044 fn verify_context_reports_missing_retrieval_when_no_key_source_resolves() {
7045 let xml = signature_with_target_reference("AQ==").replace(
7048 "</ds:SignatureValue>\n </ds:Signature>",
7049 r#"</ds:SignatureValue>
7050 <ds:KeyInfo>
7051 <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
7052 </ds:KeyInfo>
7053 </ds:Signature>"#,
7054 );
7055
7056 let error = VerifyContext::new()
7057 .key_resolver(&ConsumingKeyInfoResolver)
7058 .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
7059 .verify(&xml)
7060 .expect_err("a missing sole RetrievalMethod must remain an explicit error");
7061
7062 assert!(matches!(
7063 error,
7064 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
7065 crate::xmldsig::TransformError::UnsupportedUri(uri)
7066 )) if uri == "missing.der"
7067 ));
7068 }
7069
7070 #[test]
7071 fn verify_context_reports_malformed_retrieval_when_no_key_source_resolves() {
7072 let xml = signature_with_target_reference("AQ==").replace(
7075 "</ds:SignatureValue>\n </ds:Signature>",
7076 r#"</ds:SignatureValue>
7077 <ds:KeyInfo>
7078 <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
7079 </ds:KeyInfo>
7080 </ds:Signature>"#,
7081 );
7082 let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
7083
7084 let error = VerifyContext::new()
7085 .key_resolver(&ConsumingKeyInfoResolver)
7086 .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
7087 .external_resources(&resources)
7088 .verify(&xml)
7089 .expect_err("a malformed sole RetrievalMethod must remain a parse error");
7090
7091 assert!(matches!(
7092 error,
7093 SignatureVerificationPipelineError::ParseKeyInfo(_)
7094 ));
7095 }
7096
7097 #[test]
7098 fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
7099 let xml = signature_with_target_reference("@@@");
7100
7101 let err = VerifyContext::new()
7102 .key_resolver(&MissingKeyResolver)
7103 .verify(&xml)
7104 .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
7105 assert!(matches!(
7106 err,
7107 SignatureVerificationPipelineError::SignatureValueBase64(_)
7108 ));
7109 }
7110
7111 #[test]
7112 fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
7113 let xml = signature_with_target_reference("@@@");
7114
7115 let err = VerifyContext::new()
7116 .verify(&xml)
7117 .expect_err("invalid SignatureValue must remain a decode error");
7118 assert!(matches!(
7119 err,
7120 SignatureVerificationPipelineError::SignatureValueBase64(_)
7121 ));
7122 }
7123
7124 #[test]
7125 fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
7126 let references = vec![Reference {
7127 uri: None,
7128 id: None,
7129 ref_type: None,
7130 transforms: vec![],
7131 digest_method: DigestAlgorithm::Sha256,
7132 digest_value: vec![0; 32],
7133 }];
7134 let uri_types = UriTypeSet {
7135 allow_empty: false,
7136 allow_same_document: true,
7137 allow_external: false,
7138 };
7139
7140 let err = enforce_reference_policies(&references, uri_types, None)
7141 .expect_err("missing URI must fail before allow_empty policy is evaluated");
7142 assert!(matches!(
7143 err,
7144 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
7145 ));
7146 }
7147
7148 #[test]
7149 fn enforce_reference_policies_checks_only_terminal_binary_output() {
7150 let c14n = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI).unwrap();
7151 let allowed = HashSet::from([
7152 BASE64_TRANSFORM_URI.to_owned(),
7153 DEFAULT_IMPLICIT_C14N_URI.to_owned(),
7154 ]);
7155 let without_implicit_c14n = HashSet::from([BASE64_TRANSFORM_URI.to_owned()]);
7156
7157 for transforms in [
7158 vec![Transform::Base64Decode, Transform::C14n(c14n)],
7159 vec![Transform::Base64Decode, Transform::Base64Decode],
7160 ] {
7161 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, vec![0; 32]);
7162 enforce_reference_policies(
7163 std::slice::from_ref(&reference),
7164 UriTypeSet::default(),
7165 Some(&allowed),
7166 )
7167 .expect("terminal binary output must not require implicit C14N");
7168 }
7169
7170 let terminal_base64 = make_reference(
7171 "",
7172 vec![Transform::Base64Decode, Transform::Base64Decode],
7173 DigestAlgorithm::Sha256,
7174 vec![0; 32],
7175 );
7176 enforce_reference_policies(
7177 std::slice::from_ref(&terminal_base64),
7178 UriTypeSet::default(),
7179 Some(&without_implicit_c14n),
7180 )
7181 .expect("terminal Base64 output must not require implicit C14N");
7182
7183 let no_transforms = make_reference("", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
7184 let error = enforce_reference_policies(
7185 std::slice::from_ref(&no_transforms),
7186 UriTypeSet::default(),
7187 Some(&without_implicit_c14n),
7188 )
7189 .expect_err("a node-set result must require allowlisted implicit C14N");
7190 assert!(matches!(
7191 error,
7192 SignatureVerificationPipelineError::Policy(
7193 crate::policy::PolicyViolation::Algorithm {
7194 operation: "verification transform",
7195 ref algorithm,
7196 }
7197 )
7198 if algorithm == DEFAULT_IMPLICIT_C14N_URI
7199 ));
7200
7201 let detached = make_reference("urn:payload", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
7202 enforce_reference_policies(
7203 std::slice::from_ref(&detached),
7204 UriTypeSet::ALL,
7205 Some(&without_implicit_c14n),
7206 )
7207 .expect("external octets without transforms must not require implicit C14N");
7208
7209 let external_xpath = make_reference(
7210 "urn:payload",
7211 vec![Transform::XPath(
7212 super::super::transforms::XPathExpression::new("true()"),
7213 )],
7214 DigestAlgorithm::Sha256,
7215 vec![0; 32],
7216 );
7217 let error = enforce_reference_policies(
7218 std::slice::from_ref(&external_xpath),
7219 UriTypeSet::ALL,
7220 Some(&HashSet::from([XPATH_TRANSFORM_URI.to_owned()])),
7221 )
7222 .expect_err("external XML converted to a node-set must require implicit C14N");
7223 assert!(matches!(
7224 error,
7225 SignatureVerificationPipelineError::Policy(
7226 crate::policy::PolicyViolation::Algorithm {
7227 operation: "verification transform",
7228 ref algorithm,
7229 }
7230 )
7231 if algorithm == DEFAULT_IMPLICIT_C14N_URI
7232 ));
7233 }
7234
7235 #[test]
7236 fn stored_pre_digest_budget_counts_repeated_external_references() {
7237 let document =
7240 Document::parse("<ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"/>")
7241 .unwrap();
7242 let payload = vec![b'x'; 7];
7243 let digest = compute_digest(DigestAlgorithm::Sha256, &payload);
7244 let references = (0..5)
7245 .map(|_| {
7246 make_reference(
7247 "urn:repeated",
7248 Vec::new(),
7249 DigestAlgorithm::Sha256,
7250 digest.clone(),
7251 )
7252 })
7253 .collect::<Vec<_>>();
7254 let resources = HashMap::from([("urn:repeated".to_owned(), payload)]);
7255 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7256 let transform_budget = TransformExecutionBudget::default();
7257 let canonicalized_data_budget = CanonicalizedDataBudget::with_limit(32);
7258 let execution = ReferenceExecutionContext {
7259 store_pre_digest: true,
7260 transform_options: TransformOptions::default(),
7261 transform_budget: &transform_budget,
7262 canonicalized_data_budget: &canonicalized_data_budget,
7263 provider: crate::provider::default_provider(),
7264 };
7265
7266 let error = process_all_references_with_options(
7267 &references,
7268 &resolver,
7269 document.root_element(),
7270 &execution,
7271 )
7272 .expect_err(
7273 "retained diagnostics must not multiply one external allocation past the aggregate cap",
7274 );
7275 assert!(matches!(
7276 error,
7277 ReferenceProcessingError::Policy(crate::policy::PolicyViolation::ResourceLimit {
7278 resource: "canonicalized bytes",
7279 maximum: 32,
7280 ..
7281 })
7282 ));
7283 }
7284
7285 #[test]
7286 fn operation_memoizes_repeated_external_resource_identity() {
7287 let document = XmlDocument::parse("<root/>").expect("fixture must parse");
7290 let resources = HashMap::from([("urn:repeated".to_owned(), vec![b'x'; 8 * 1_024])]);
7291 let reference = make_reference(
7292 "urn:repeated",
7293 Vec::new(),
7294 DigestAlgorithm::Sha256,
7295 vec![0; 32],
7296 );
7297 let budgets = VerificationOperationBudgets::with_transforms(
7298 &crate::policy::VerificationPolicy::default(),
7299 TransformExecutionBudget::default(),
7300 );
7301
7302 document.with_view(|view| {
7303 let resolver =
7304 UriReferenceResolver::new(view.document()).with_external_resources(&resources);
7305 let first = budgets.resource_identity_for_reference(&reference, 0, &resolver, view);
7306 let second = budgets.resource_identity_for_reference(&reference, 1, &resolver, view);
7307
7308 assert_eq!(first, second);
7309 assert_eq!(budgets.external_resource_identities.borrow().len(), 1);
7310 });
7311 }
7312
7313 #[test]
7314 fn canonical_signed_info_obeys_policy_without_diagnostic_retention() {
7315 let xml = signature_with_target_reference("AQ==");
7319 let marker = "<ds:SignatureMethod";
7320 let padding = " ".repeat(1_025);
7321 let xml = xml.replacen(marker, &format!("{padding}{marker}"), 1);
7322 let policy = crate::policy::VerificationPolicy {
7323 resources: crate::policy::ResourcePolicy {
7324 max_canonicalized_bytes: 1_024,
7325 ..crate::policy::ResourcePolicy::default()
7326 },
7327 ..crate::policy::VerificationPolicy::default()
7328 };
7329
7330 let error = VerifyContext::new()
7331 .key(&AcceptingKey)
7332 .policy(policy)
7333 .verify(&xml)
7334 .expect_err("canonicalized SignedInfo must remain policy-bounded");
7335
7336 assert!(matches!(
7337 error,
7338 SignatureVerificationPipelineError::Policy(
7339 crate::policy::PolicyViolation::ResourceLimit {
7340 resource: "canonicalized bytes",
7341 ..
7342 }
7343 )
7344 ));
7345 }
7346
7347 #[test]
7348 fn push_normalized_signature_text_rejects_form_feed() {
7349 let mut normalized = Vec::new();
7350 let mut raw_text_len = 0usize;
7351 let err =
7352 push_normalized_signature_text("ab\u{000C}cd", &mut raw_text_len, &mut normalized)
7353 .expect_err("form-feed must not be treated as XML base64 whitespace");
7354 assert!(matches!(
7355 err,
7356 SignatureVerificationPipelineError::SignatureValueBase64(
7357 base64::DecodeError::InvalidByte(_, 0x0C)
7358 )
7359 ));
7360 }
7361
7362 #[test]
7363 fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
7364 let mut normalized = vec![b'A'; MAX_SIGNATURE_VALUE_LEN - 1];
7365 let mut raw_text_len = normalized.len();
7366 let err = push_normalized_signature_text("é", &mut raw_text_len, &mut normalized)
7367 .expect_err("multibyte characters must not bypass byte-size limit");
7368 assert!(matches!(
7369 err,
7370 SignatureVerificationPipelineError::InvalidStructure {
7371 reason: "SignatureValue exceeds maximum allowed length"
7372 }
7373 ));
7374 }
7375
7376 #[test]
7379 fn reference_with_correct_digest_passes() {
7380 let xml = r##"<root>
7383 <data>hello world</data>
7384 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
7385 <ds:SignedInfo/>
7386 </ds:Signature>
7387 </root>"##;
7388 let doc = Document::parse(xml).unwrap();
7389 let resolver = UriReferenceResolver::new(&doc);
7390 let sig_node = doc
7391 .descendants()
7392 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
7393 .unwrap();
7394
7395 let initial_data = resolver.dereference("").unwrap();
7397 let transforms = vec![
7398 Transform::Enveloped,
7399 Transform::C14n(
7400 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
7401 .unwrap(),
7402 ),
7403 ];
7404 let pre_digest_bytes =
7405 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
7406 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
7407
7408 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
7410
7411 let result = process_reference(
7412 &reference,
7413 &resolver,
7414 sig_node,
7415 ReferenceSet::SignedInfo,
7416 0,
7417 false,
7418 )
7419 .unwrap();
7420 assert!(
7421 matches!(result.status, DsigStatus::Valid),
7422 "digest should match"
7423 );
7424 assert!(result.pre_digest_data.is_none());
7425 }
7426
7427 #[test]
7428 fn reference_with_wrong_digest_fails() {
7429 let xml = r##"<root>
7430 <data>hello</data>
7431 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7432 <ds:SignedInfo/>
7433 </ds:Signature>
7434 </root>"##;
7435 let doc = Document::parse(xml).unwrap();
7436 let resolver = UriReferenceResolver::new(&doc);
7437 let sig_node = doc
7438 .descendants()
7439 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
7440 .unwrap();
7441
7442 let transforms = vec![Transform::Enveloped];
7443 let wrong_digest = vec![0u8; 32];
7445 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
7446
7447 let result = process_reference(
7448 &reference,
7449 &resolver,
7450 sig_node,
7451 ReferenceSet::SignedInfo,
7452 0,
7453 false,
7454 )
7455 .unwrap();
7456 assert!(matches!(
7457 result.status,
7458 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
7459 ));
7460 }
7461
7462 #[test]
7463 fn reference_with_wrong_digest_preserves_supplied_ref_index() {
7464 let xml = r##"<root>
7465 <data>hello</data>
7466 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7467 <ds:SignedInfo/>
7468 </ds:Signature>
7469 </root>"##;
7470 let doc = Document::parse(xml).unwrap();
7471 let resolver = UriReferenceResolver::new(&doc);
7472 let sig_node = doc
7473 .descendants()
7474 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
7475 .unwrap();
7476
7477 let reference = make_reference(
7478 "",
7479 vec![Transform::Enveloped],
7480 DigestAlgorithm::Sha256,
7481 vec![0u8; 32],
7482 );
7483 let result = process_reference(
7484 &reference,
7485 &resolver,
7486 sig_node,
7487 ReferenceSet::SignedInfo,
7488 7,
7489 false,
7490 )
7491 .unwrap();
7492 assert!(matches!(
7493 result.status,
7494 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
7495 ));
7496 }
7497
7498 #[test]
7499 fn reference_stores_pre_digest_data() {
7500 let xml = "<root><child>text</child></root>";
7501 let doc = Document::parse(xml).unwrap();
7502 let resolver = UriReferenceResolver::new(&doc);
7503
7504 let initial_data = resolver.dereference("").unwrap();
7506 let pre_digest =
7507 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
7508 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
7509
7510 let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
7511 let result = process_reference(
7512 &reference,
7513 &resolver,
7514 doc.root_element(),
7515 ReferenceSet::SignedInfo,
7516 0,
7517 true,
7518 )
7519 .unwrap();
7520
7521 assert!(matches!(result.status, DsigStatus::Valid));
7522 assert!(result.pre_digest_data.is_some());
7523 assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
7524 }
7525
7526 #[test]
7529 fn reference_with_id_uri() {
7530 let xml = r##"<root>
7531 <item ID="target">specific content</item>
7532 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7533 <ds:SignedInfo/>
7534 </ds:Signature>
7535 </root>"##;
7536 let doc = Document::parse(xml).unwrap();
7537 let resolver = UriReferenceResolver::new(&doc);
7538 let sig_node = doc
7539 .descendants()
7540 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
7541 .unwrap();
7542
7543 let initial_data = resolver.dereference("#target").unwrap();
7545 let transforms = vec![Transform::C14n(
7546 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
7547 .unwrap(),
7548 )];
7549 let pre_digest =
7550 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
7551 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
7552
7553 let reference = make_reference(
7554 "#target",
7555 transforms,
7556 DigestAlgorithm::Sha256,
7557 expected_digest,
7558 );
7559 let result = process_reference(
7560 &reference,
7561 &resolver,
7562 sig_node,
7563 ReferenceSet::SignedInfo,
7564 0,
7565 false,
7566 )
7567 .unwrap();
7568 assert!(matches!(result.status, DsigStatus::Valid));
7569 }
7570
7571 #[test]
7572 fn reference_with_nonexistent_id_fails() {
7573 let xml = "<root><child/></root>";
7574 let doc = Document::parse(xml).unwrap();
7575 let resolver = UriReferenceResolver::new(&doc);
7576
7577 let reference =
7578 make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
7579 let result = process_reference(
7580 &reference,
7581 &resolver,
7582 doc.root_element(),
7583 ReferenceSet::SignedInfo,
7584 0,
7585 false,
7586 );
7587 assert!(result.is_err());
7588 }
7589
7590 #[test]
7591 fn reference_with_absent_uri_fails_closed() {
7592 let xml = "<root><child>text</child></root>";
7593 let doc = Document::parse(xml).unwrap();
7594 let resolver = UriReferenceResolver::new(&doc);
7595
7596 let reference = Reference {
7597 uri: None, id: None,
7599 ref_type: None,
7600 transforms: vec![],
7601 digest_method: DigestAlgorithm::Sha256,
7602 digest_value: vec![0; 32],
7603 };
7604
7605 let result = process_reference(
7606 &reference,
7607 &resolver,
7608 doc.root_element(),
7609 ReferenceSet::SignedInfo,
7610 0,
7611 false,
7612 );
7613 assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
7614 }
7615
7616 #[test]
7619 fn all_references_pass() {
7620 let xml = "<root><child>text</child></root>";
7621 let doc = Document::parse(xml).unwrap();
7622 let resolver = UriReferenceResolver::new(&doc);
7623
7624 let initial_data = resolver.dereference("").unwrap();
7626 let pre_digest =
7627 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
7628 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
7629
7630 let refs = vec![
7631 make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
7632 make_reference("", vec![], DigestAlgorithm::Sha256, digest),
7633 ];
7634
7635 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
7636 assert!(result.all_valid());
7637 assert_eq!(result.results.len(), 2);
7638 assert!(result.first_failure.is_none());
7639 }
7640
7641 #[test]
7642 fn reference_processing_shares_xpath_work_across_references() {
7643 let document = Document::parse("<root/>").unwrap();
7646 let resolver = UriReferenceResolver::new(&document);
7647 let transform = Transform::XPath(super::super::transforms::XPathExpression::new("true()"));
7648 let initial_data = resolver.dereference("").unwrap();
7649 let pre_digest = crate::xmldsig::execute_transforms(
7650 document.root_element(),
7651 initial_data,
7652 std::slice::from_ref(&transform),
7653 )
7654 .unwrap();
7655 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
7656 let references = vec![
7657 make_reference(
7658 "",
7659 vec![transform.clone()],
7660 DigestAlgorithm::Sha256,
7661 digest.clone(),
7662 ),
7663 make_reference("", vec![transform], DigestAlgorithm::Sha256, digest),
7664 ];
7665 let budget = TransformExecutionBudget::with_xpath_limit(12);
7666 let canonicalized_data_budget = CanonicalizedDataBudget::default();
7667 let execution = ReferenceExecutionContext {
7668 store_pre_digest: false,
7669 transform_options: TransformOptions::default(),
7670 transform_budget: &budget,
7671 canonicalized_data_budget: &canonicalized_data_budget,
7672 provider: crate::provider::default_provider(),
7673 };
7674
7675 let error = process_all_references_with_options(
7676 &references,
7677 &resolver,
7678 document.root_element(),
7679 &execution,
7680 )
7681 .expect_err("the second Reference must consume the first Reference's XPath work");
7682
7683 assert!(matches!(
7684 error,
7685 ReferenceProcessingError::Transform(TransformError::Policy(
7686 crate::policy::PolicyViolation::ResourceLimit {
7687 resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
7688 ..
7689 }
7690 ))
7691 ));
7692 }
7693
7694 #[test]
7695 fn reference_processing_shares_node_set_materialization_across_references() {
7696 let document = Document::parse(
7700 r#"<root xmlns:n="urn:0123456789"><target Id="selected">payload</target></root>"#,
7701 )
7702 .unwrap();
7703 let resolver = UriReferenceResolver::new(&document);
7704 let initial_data = resolver.dereference("#selected").unwrap();
7705 let pre_digest =
7706 crate::xmldsig::execute_transforms(document.root_element(), initial_data, &[]).unwrap();
7707 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
7708 let references = vec![
7709 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest.clone()),
7710 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest),
7711 ];
7712 let budget = TransformExecutionBudget::with_node_set_materialization_limit(30);
7713 let canonicalized_data_budget = CanonicalizedDataBudget::default();
7714 let execution = ReferenceExecutionContext {
7715 store_pre_digest: false,
7716 transform_options: TransformOptions::default(),
7717 transform_budget: &budget,
7718 canonicalized_data_budget: &canonicalized_data_budget,
7719 provider: crate::provider::default_provider(),
7720 };
7721
7722 let error = process_all_references_with_options(
7723 &references,
7724 &resolver,
7725 document.root_element(),
7726 &execution,
7727 )
7728 .expect_err("the second Reference must consume the first Reference's materialization work");
7729
7730 assert!(matches!(
7731 error,
7732 ReferenceProcessingError::UriDereference(TransformError::Policy(
7733 crate::policy::PolicyViolation::ResourceLimit {
7734 resource: crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
7735 ..
7736 }
7737 ))
7738 ));
7739 }
7740
7741 #[test]
7742 fn fail_fast_on_first_mismatch() {
7743 let xml = "<root><child>text</child></root>";
7744 let doc = Document::parse(xml).unwrap();
7745 let resolver = UriReferenceResolver::new(&doc);
7746
7747 let wrong_digest = vec![0u8; 32];
7748 let refs = vec![
7749 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
7750 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
7752 ];
7753
7754 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
7755 assert!(!result.all_valid());
7756 assert_eq!(result.first_failure, Some(0));
7757 assert_eq!(result.results.len(), 1);
7759 assert!(matches!(
7760 result.results[0].status,
7761 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
7762 ));
7763 }
7764
7765 #[test]
7766 fn fail_fast_second_reference() {
7767 let xml = "<root><child>text</child></root>";
7768 let doc = Document::parse(xml).unwrap();
7769 let resolver = UriReferenceResolver::new(&doc);
7770
7771 let initial_data = resolver.dereference("").unwrap();
7773 let pre_digest =
7774 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
7775 let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
7776 let wrong_digest = vec![0u8; 32];
7777
7778 let refs = vec![
7779 make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
7780 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
7781 ];
7782
7783 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
7784 assert!(!result.all_valid());
7785 assert_eq!(result.first_failure, Some(1));
7786 assert_eq!(result.results.len(), 2);
7788 assert!(matches!(result.results[0].status, DsigStatus::Valid));
7789 assert!(matches!(
7790 result.results[1].status,
7791 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
7792 ));
7793 }
7794
7795 #[test]
7796 fn empty_references_list() {
7797 let xml = "<root/>";
7798 let doc = Document::parse(xml).unwrap();
7799 let resolver = UriReferenceResolver::new(&doc);
7800
7801 let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
7802 assert!(result.all_valid());
7803 assert!(result.results.is_empty());
7804 }
7805
7806 #[test]
7809 fn reference_sha1_digest() {
7810 let xml = "<root>content</root>";
7811 let doc = Document::parse(xml).unwrap();
7812 let resolver = UriReferenceResolver::new(&doc);
7813
7814 let initial_data = resolver.dereference("").unwrap();
7815 let pre_digest =
7816 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
7817 let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
7818
7819 let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
7820 let result = process_reference(
7821 &reference,
7822 &resolver,
7823 doc.root_element(),
7824 ReferenceSet::SignedInfo,
7825 0,
7826 false,
7827 )
7828 .unwrap();
7829 assert!(matches!(result.status, DsigStatus::Valid));
7830 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
7831 }
7832
7833 #[test]
7834 fn reference_sha512_digest() {
7835 let xml = "<root>content</root>";
7836 let doc = Document::parse(xml).unwrap();
7837 let resolver = UriReferenceResolver::new(&doc);
7838
7839 let initial_data = resolver.dereference("").unwrap();
7840 let pre_digest =
7841 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
7842 let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
7843
7844 let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
7845 let result = process_reference(
7846 &reference,
7847 &resolver,
7848 doc.root_element(),
7849 ReferenceSet::SignedInfo,
7850 0,
7851 false,
7852 )
7853 .unwrap();
7854 assert!(matches!(result.status, DsigStatus::Valid));
7855 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
7856 }
7857
7858 #[test]
7861 fn saml_enveloped_reference_processing() {
7862 let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
7864 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
7865 ID="_resp1">
7866 <saml:Assertion ID="_assert1">
7867 <saml:Subject>user@example.com</saml:Subject>
7868 </saml:Assertion>
7869 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7870 <ds:SignedInfo>
7871 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7872 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
7873 <ds:Reference URI="">
7874 <ds:Transforms>
7875 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
7876 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7877 </ds:Transforms>
7878 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
7879 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
7880 </ds:Reference>
7881 </ds:SignedInfo>
7882 <ds:SignatureValue>fakesig==</ds:SignatureValue>
7883 </ds:Signature>
7884 </samlp:Response>"##;
7885 let doc = Document::parse(xml).unwrap();
7886 let resolver = UriReferenceResolver::new(&doc);
7887 let sig_node = doc
7888 .descendants()
7889 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
7890 .unwrap();
7891
7892 let signed_info_node = sig_node
7894 .children()
7895 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
7896 .unwrap();
7897 let signed_info = parse_signed_info(signed_info_node).unwrap();
7898 let reference = &signed_info.references[0];
7899
7900 let initial_data = resolver.dereference("").unwrap();
7902 let pre_digest =
7903 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
7904 .unwrap();
7905 let correct_digest = compute_digest(reference.digest_method, &pre_digest);
7906
7907 let corrected_ref = make_reference(
7909 "",
7910 reference.transforms.clone(),
7911 reference.digest_method,
7912 correct_digest,
7913 );
7914
7915 let result = process_reference(
7917 &corrected_ref,
7918 &resolver,
7919 sig_node,
7920 ReferenceSet::SignedInfo,
7921 0,
7922 true,
7923 )
7924 .unwrap();
7925 assert!(
7926 matches!(result.status, DsigStatus::Valid),
7927 "SAML reference should verify"
7928 );
7929 assert!(result.pre_digest_data.is_some());
7930
7931 let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
7933 assert!(
7934 pre_digest_str.contains("samlp:Response"),
7935 "pre-digest should contain Response"
7936 );
7937 assert!(
7938 !pre_digest_str.contains("SignatureValue"),
7939 "pre-digest should NOT contain Signature"
7940 );
7941 }
7942
7943 #[test]
7944 fn pipeline_missing_signed_info_returns_missing_element() {
7945 let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
7946
7947 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
7948 .expect_err("missing SignedInfo must fail before crypto stage");
7949 assert!(matches!(
7950 err,
7951 SignatureVerificationPipelineError::MissingElement {
7952 element: "SignedInfo"
7953 }
7954 ));
7955 }
7956
7957 #[test]
7958 fn pipeline_multiple_signature_elements_are_rejected() {
7959 let xml = r#"
7960<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7961 <ds:Signature>
7962 <ds:SignedInfo/>
7963 </ds:Signature>
7964 <ds:Signature/>
7965</root>
7966"#;
7967
7968 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
7969 .expect_err("multiple signatures must fail closed");
7970 assert!(matches!(
7971 err,
7972 SignatureVerificationPipelineError::InvalidStructure {
7973 reason: "Signature must appear exactly once in document",
7974 }
7975 ));
7976 }
7977
7978 #[test]
7979 fn pipeline_start_node_limits_signature_cardinality_to_its_subtree() {
7980 let xml = r#"
7983<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7984 <scope Id="selected"><ds:Signature/></scope>
7985 <scope Id="other"><ds:Signature/></scope>
7986</root>
7987"#;
7988 let err = VerifyContext::new()
7989 .start_node_id("selected")
7990 .verify(xml)
7991 .expect_err("the selected Signature remains structurally incomplete");
7992 assert!(matches!(
7993 err,
7994 SignatureVerificationPipelineError::MissingElement {
7995 element: "SignedInfo"
7996 }
7997 ));
7998 }
7999
8000 #[test]
8001 fn pipeline_reports_keyinfo_parse_error() {
8002 let xml = r#"
8003<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
8004 xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
8005 <ds:SignedInfo>
8006 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
8007 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
8008 <ds:Reference URI="">
8009 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
8010 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
8011 </ds:Reference>
8012 </ds:SignedInfo>
8013 <ds:SignatureValue>AA==</ds:SignatureValue>
8014 <ds:KeyInfo>
8015 <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
8016 </ds:KeyInfo>
8017</ds:Signature>
8018"#;
8019
8020 let err = VerifyContext::new().verify(xml).expect_err(
8021 "invalid KeyInfo must map to ParseKeyInfo when no explicit key is supplied",
8022 );
8023 assert!(matches!(
8024 err,
8025 SignatureVerificationPipelineError::ParseKeyInfo(_)
8026 ));
8027 }
8028
8029 #[test]
8030 fn pipeline_ignores_malformed_keyinfo_when_explicit_key_is_supplied() {
8031 let base_xml = signature_with_target_reference("AQ==");
8032 let xml = base_xml
8033 .replace(
8034 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
8035 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
8036 )
8037 .replace(
8038 "</ds:SignatureValue>\n </ds:Signature>",
8039 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
8040 );
8041
8042 let result = VerifyContext::new()
8043 .key(&RejectingKey)
8044 .verify(&xml)
8045 .expect("explicit key path should not fail on malformed KeyInfo");
8046 assert!(matches!(
8047 result.status,
8048 DsigStatus::Invalid(FailureReason::SignatureMismatch)
8049 ));
8050 }
8051
8052 #[test]
8053 fn pipeline_rejects_foreign_element_children_under_signature() {
8054 let base_xml = signature_with_target_reference("AQ==");
8055 let xml = base_xml
8056 .replace(
8057 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
8058 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:foo="urn:example:foo">"#,
8059 )
8060 .replace(
8061 "</ds:SignedInfo>\n <ds:SignatureValue>",
8062 "</ds:SignedInfo>\n <foo:Bar/>\n <ds:SignatureValue>",
8063 );
8064
8065 let err = VerifyContext::new()
8066 .key(&RejectingKey)
8067 .verify(&xml)
8068 .expect_err("foreign element children under Signature must fail closed");
8069 assert!(matches!(
8070 err,
8071 SignatureVerificationPipelineError::InvalidStructure {
8072 reason: "Signature must contain only XMLDSIG element children",
8073 }
8074 ));
8075 }
8076
8077 #[test]
8078 fn pipeline_rejects_non_whitespace_mixed_content_under_signature() {
8079 let base_xml = signature_with_target_reference("AQ==");
8080 let xml = base_xml.replace(
8081 "</ds:SignedInfo>\n <ds:SignatureValue>",
8082 "</ds:SignedInfo>\n oops\n <ds:SignatureValue>",
8083 );
8084
8085 let err = VerifyContext::new()
8086 .key(&RejectingKey)
8087 .verify(&xml)
8088 .expect_err("non-whitespace mixed content under Signature must fail closed");
8089 assert!(matches!(
8090 err,
8091 SignatureVerificationPipelineError::InvalidStructure {
8092 reason: "Signature must not contain non-whitespace mixed content",
8093 }
8094 ));
8095 }
8096
8097 #[test]
8098 fn pipeline_rejects_keyinfo_out_of_order() {
8099 let base_xml = signature_with_target_reference("AQ==");
8100 let xml = base_xml.replace(
8101 "</ds:SignatureValue>\n </ds:Signature>",
8102 "</ds:SignatureValue>\n <ds:Object/>\n <ds:KeyInfo><ds:KeyName>late</ds:KeyName></ds:KeyInfo>\n </ds:Signature>",
8103 );
8104
8105 let err = VerifyContext::new()
8106 .key(&RejectingKey)
8107 .verify(&xml)
8108 .expect_err("KeyInfo after Object must be rejected by Signature child order checks");
8109 assert!(matches!(
8110 err,
8111 SignatureVerificationPipelineError::InvalidStructure {
8112 reason: "KeyInfo must be the third element child of Signature when present"
8113 }
8114 ));
8115 }
8116
8117 #[test]
8118 fn pipeline_accepts_comments_and_processing_instructions_under_signature() {
8119 let xml = r#"
8120<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8121 <?dbg keep ?>
8122 <!-- signature metadata -->
8123 <ds:SignedInfo>
8124 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
8125 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
8126 <ds:Reference URI="">
8127 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
8128 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
8129 </ds:Reference>
8130 </ds:SignedInfo>
8131 <!-- between required children -->
8132 <ds:SignatureValue>AA==</ds:SignatureValue>
8133</ds:Signature>
8134"#;
8135
8136 let doc = Document::parse(xml).expect("test XML must parse");
8137 let signature_node = doc.root_element();
8138 let parsed = parse_signature_children(signature_node)
8139 .expect("comment/PI nodes under Signature must be ignored");
8140
8141 assert_eq!(parsed.signed_info_node.tag_name().name(), "SignedInfo");
8142 assert_eq!(
8143 parsed.signature_value_node.tag_name().name(),
8144 "SignatureValue"
8145 );
8146 assert!(parsed.key_info_node.is_none());
8147 }
8148}