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::{
20 DocumentParseSettings, DocumentView, NodeIdentity, XmlDocument, XmlDocumentError,
21};
22use crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING;
23use crate::operation::{
24 OperationDecisionReason, OperationExecutionContext, OperationNodeId, OperationNodeKind,
25 OperationPlanError, OperationResourceIdentity, OperationStage,
26};
27
28#[cfg(test)]
29use super::digest::compute_digest;
30use super::digest::{DigestAlgorithm, constant_time_eq};
31#[cfg(test)]
32use super::parse::MAX_REFERENCES_PER_SIGNATURE;
33#[cfg(test)]
34use super::parse::parse_key_info;
35use super::parse::{
36 KeyInfo, MAX_X509_DATA_TOTAL_BINARY_LEN, MAX_X509_DECODED_BINARY_LEN, ParseError, Reference,
37 RetrievalMethodTransforms, SignatureAlgorithm, XMLDSIG_NS,
38};
39use super::parse::{
40 parse_key_info_with_policy_budgets, parse_key_info_with_policy_budgets_and_document_base,
41 parse_reference_with_xpath_budget, parse_signed_info_with_xpath_budget, parse_x509_certificate,
42 parse_x509_data_dispatch_with_budget_and_provider, reference_digest_method,
43};
44use super::signature::{
45 SignatureVerificationError, verify_dsa_signature_spki, verify_ecdsa_signature_pem,
46 verify_rsa_signature_pem,
47};
48#[cfg(test)]
49use super::transforms::BASE64_TRANSFORM_URI;
50use super::transforms::{
51 DEFAULT_IMPLICIT_C14N_URI, Transform, TransformExecutionBudget, TransformOptions,
52 XPATH_TRANSFORM_URI, XPathHereSemantics, XPathSignatureParseBudget,
53 execute_transforms_with_options_and_budget, map_c14n_resource_policy_violation,
54 transform_chain_produces_binary,
55};
56use super::types::{NodeSet, TransformError, subtree_node_id_range};
57use super::uri::{ExternalResourceMapError, UriReferenceResolver, validate_external_resource_map};
58use super::whitespace::{is_xml_whitespace_only, normalize_xml_base64_bytes};
59
60const MAX_SIGNATURE_VALUE_LEN: usize = 8192;
61const MAX_RETRIEVAL_METHOD_COUNT: usize = 64;
62pub trait VerifyingKey {
67 fn verify_candidate_keys(
71 &self,
72 _verify: &mut dyn FnMut(&dyn VerifyingKey) -> Result<bool, DsigError>,
73 ) -> Result<Option<bool>, DsigError> {
74 Ok(None)
75 }
76 fn verification_spki(
79 &self,
80 _algorithm: SignatureAlgorithm,
81 ) -> Result<Option<&[u8]>, DsigError> {
82 Ok(None)
83 }
84
85 fn ecdsa_encoding(&self) -> crate::policy::EcdsaSignatureValueEncoding {
87 crate::policy::EcdsaSignatureValueEncoding::XmlDsig
88 }
89 fn verify_with_context(
91 &self,
92 algorithm: SignatureAlgorithm,
93 context: &super::SignatureContext,
94 data: &[u8],
95 signature: &[u8],
96 ) -> Result<bool, DsigError> {
97 if !context.as_bytes().is_empty() {
98 return Err(super::SignatureVerificationError::UnsupportedAlgorithm {
99 uri: algorithm.uri().to_owned(),
100 }
101 .into());
102 }
103 self.verify(algorithm, data, signature)
104 }
105 fn validate_policy(
111 &self,
112 _policy: &crate::policy::VerificationPolicy,
113 ) -> Result<(), DsigError> {
114 Ok(())
115 }
116
117 fn validate_signature_value(
123 &self,
124 algorithm: SignatureAlgorithm,
125 signature_value: &[u8],
126 ) -> Result<bool, DsigError> {
127 Ok(super::signature::signature_value_matches_algorithm(
128 algorithm,
129 signature_value,
130 ))
131 }
132
133 fn validate_signature_value_with_policy(
141 &self,
142 policy: &crate::policy::VerificationPolicy,
143 algorithm: SignatureAlgorithm,
144 signature_value: &[u8],
145 ) -> Result<bool, DsigError> {
146 let _ = policy;
147 self.validate_signature_value(algorithm, signature_value)
148 }
149
150 fn verify(
152 &self,
153 algorithm: SignatureAlgorithm,
154 signed_data: &[u8],
155 signature_value: &[u8],
156 ) -> Result<bool, DsigError>;
157
158 fn verify_with_policy(
164 &self,
165 policy: &crate::policy::VerificationPolicy,
166 algorithm: SignatureAlgorithm,
167 signed_data: &[u8],
168 signature_value: &[u8],
169 ) -> Result<bool, DsigError> {
170 let _ = policy;
171 self.verify(algorithm, signed_data, signature_value)
172 }
173}
174
175pub trait KeyResolver {
180 fn resolve_for_verification<'a>(
184 &'a self,
185 key_info: Option<&KeyInfo>,
186 algorithm: SignatureAlgorithm,
187 policy: &crate::policy::VerificationPolicy,
188 provider: &dyn crate::provider::CryptoProvider,
189 ) -> Result<Option<super::ResolvedVerificationKey<'a>>, DsigError> {
190 Ok(self
191 .resolve_with_policy_and_provider(key_info, algorithm, policy, provider)?
192 .map(super::ResolvedVerificationKey::Candidate))
193 }
194 fn resolve<'a>(
201 &'a self,
202 key_info: Option<&KeyInfo>,
203 algorithm: SignatureAlgorithm,
204 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError>;
205
206 fn resolve_with_policy<'a>(
216 &'a self,
217 key_info: Option<&KeyInfo>,
218 algorithm: SignatureAlgorithm,
219 _policy: &crate::policy::VerificationPolicy,
220 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
221 self.resolve(key_info, algorithm)
222 }
223
224 fn resolve_with_policy_and_provider<'a>(
230 &'a self,
231 key_info: Option<&KeyInfo>,
232 algorithm: SignatureAlgorithm,
233 policy: &crate::policy::VerificationPolicy,
234 _provider: &dyn crate::provider::CryptoProvider,
235 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
236 self.resolve_with_policy(key_info, algorithm, policy)
237 }
238
239 fn consumes_document_key_info(&self) -> bool {
246 false
247 }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255#[must_use = "pass the policy to VerifyContext::allowed_uri_types(), or store it for reuse"]
256pub struct UriTypeSet {
257 allow_empty: bool,
258 allow_same_document: bool,
259 allow_external: bool,
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263enum UriClass {
264 Empty,
265 SameDocument,
266 External,
267}
268
269fn classify_uri(uri: &str) -> UriClass {
270 if uri.is_empty() {
271 UriClass::Empty
272 } else if uri.starts_with('#') {
273 UriClass::SameDocument
274 } else {
275 UriClass::External
276 }
277}
278
279impl UriTypeSet {
280 pub const fn new(allow_empty: bool, allow_same_document: bool, allow_external: bool) -> Self {
282 Self {
283 allow_empty,
284 allow_same_document,
285 allow_external,
286 }
287 }
288
289 pub const SAME_DOCUMENT: Self = Self {
291 allow_empty: true,
292 allow_same_document: true,
293 allow_external: false,
294 };
295
296 pub const ALL: Self = Self {
300 allow_empty: true,
301 allow_same_document: true,
302 allow_external: true,
303 };
304
305 pub fn allows(self, uri: &str) -> bool {
307 match classify_uri(uri) {
308 UriClass::Empty => self.allow_empty,
309 UriClass::SameDocument => self.allow_same_document,
310 UriClass::External => self.allow_external,
311 }
312 }
313}
314
315impl Default for UriTypeSet {
316 fn default() -> Self {
317 Self::SAME_DOCUMENT
318 }
319}
320
321#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
323pub enum SignatureSelection<'a> {
324 #[default]
326 UniqueDocumentSignature,
327 FirstDocumentSignature,
329 FirstSignatureUnderId(&'a str),
331}
332
333#[must_use = "configure the context and call verify(), or store it for reuse"]
335#[derive(Clone)]
336pub struct VerifyContext<'a> {
337 key: Option<&'a dyn VerifyingKey>,
338 key_resolver: Option<&'a dyn KeyResolver>,
339 policy: crate::policy::VerificationPolicy,
340 provider: &'a dyn crate::provider::CryptoProvider,
341 xml_backend: crate::XmlBackend,
342 store_pre_digest: bool,
343 external_resources: Option<&'a HashMap<String, Vec<u8>>>,
344 signature_selection: SignatureSelection<'a>,
345 id_attributes: &'a [crate::IdAttributeRegistration],
346}
347
348impl<'a> VerifyContext<'a> {
349 pub fn new() -> Self {
358 Self {
359 key: None,
360 key_resolver: None,
361 policy: crate::policy::VerificationPolicy::default(),
362 provider: crate::provider::default_provider(),
363 xml_backend: crate::XmlBackend::default(),
364 store_pre_digest: false,
365 external_resources: None,
366 signature_selection: SignatureSelection::UniqueDocumentSignature,
367 id_attributes: &[],
368 }
369 }
370
371 pub fn key(mut self, key: &'a dyn VerifyingKey) -> Self {
378 self.key = Some(key);
379 self
380 }
381
382 pub fn key_resolver(mut self, resolver: &'a dyn KeyResolver) -> Self {
384 self.key_resolver = Some(resolver);
385 self
386 }
387
388 pub fn policy(mut self, policy: crate::policy::VerificationPolicy) -> Self {
390 self.policy = policy;
391 self
392 }
393
394 pub fn provider(mut self, provider: &'a dyn crate::provider::CryptoProvider) -> Self {
396 self.provider = provider;
397 self
398 }
399
400 pub fn xml_backend(mut self, backend: crate::XmlBackend) -> Self {
402 self.xml_backend = backend;
403 self
404 }
405
406 pub fn process_manifests(mut self, enabled: bool) -> Self {
437 self.policy.manifest_processing = if enabled {
438 crate::policy::ManifestProcessing::Process
439 } else {
440 crate::policy::ManifestProcessing::Ignore
441 };
442 self
443 }
444
445 pub fn allowed_uri_types(mut self, types: UriTypeSet) -> Self {
447 self.policy.uris.references = types;
448 self
449 }
450
451 pub fn allowed_retrieval_method_uri_types(mut self, types: UriTypeSet) -> Self {
458 self.policy.uris.retrieval_methods = types;
459 self
460 }
461
462 pub fn external_resources(mut self, resources: &'a HashMap<String, Vec<u8>>) -> Self {
470 self.external_resources = Some(resources);
471 self
472 }
473
474 pub fn start_node_id(mut self, id: &'a str) -> Self {
480 self.signature_selection = SignatureSelection::FirstSignatureUnderId(id);
481 self
482 }
483
484 pub fn first_document_signature(mut self) -> Self {
489 self.signature_selection = SignatureSelection::FirstDocumentSignature;
490 self
491 }
492
493 pub fn id_attributes(mut self, registrations: &'a [crate::IdAttributeRegistration]) -> Self {
495 self.id_attributes = registrations;
496 self
497 }
498
499 pub fn allow_internal_dtd(mut self, enabled: bool) -> Self {
502 self.policy.xml.allow_internal_dtd = enabled;
503 self
504 }
505
506 pub fn allowed_transforms<I, S>(mut self, transforms: I) -> Self
517 where
518 I: IntoIterator<Item = S>,
519 S: Into<String>,
520 {
521 self.policy.transforms.allowed_algorithms =
522 Some(transforms.into_iter().map(Into::into).collect());
523 self
524 }
525
526 pub fn store_pre_digest(mut self, enabled: bool) -> Self {
534 self.store_pre_digest = enabled;
535 self
536 }
537
538 pub fn xpath_here_semantics(mut self, semantics: XPathHereSemantics) -> Self {
544 self.policy.transforms.xpath_here_semantics = semantics;
545 self
546 }
547
548 fn allowed_transform_uris(&self) -> Option<&HashSet<String>> {
549 self.policy.transforms.allowed_algorithms.as_ref()
550 }
551
552 fn transform_options(&self) -> TransformOptions {
553 TransformOptions::default()
554 .allow_internal_dtd(self.policy.xml.allow_internal_dtd)
555 .xpath_here_semantics(self.policy.transforms.xpath_here_semantics)
556 }
557
558 pub fn verify(&self, xml: &str) -> Result<VerifyResult, DsigError> {
566 verify_signature_with_context(xml, self)
567 }
568
569 pub fn verify_document(&self, document: &XmlDocument) -> Result<VerifyResult, DsigError> {
575 verify_signature_document_with_context(document, self)
576 }
577
578 pub fn verify_all(&self, document: &XmlDocument) -> Result<VerifyEvidence, DsigError> {
587 verify_all_document(document, self, &VerificationRequest::default())
588 }
589
590 pub fn signature_identities(
595 &self,
596 document: &XmlDocument,
597 ) -> Result<Vec<NodeIdentity>, DsigError> {
598 self.policy.validate()?;
599 document.validate_operation_policy(&self.policy.xml, &self.policy.resources)?;
600 document.with_view(|view| {
601 let count = count_signature_nodes(view, self.policy.resources.max_signatures)?;
602 let mut identities = Vec::with_capacity(count);
603 for node in view.document().descendants() {
604 if node.has_tag_name((XMLDSIG_NS, "Signature")) {
605 identities.push(view.node_identity(node));
606 }
607 }
608 Ok(identities)
609 })
610 }
611
612 pub fn verify_request(
617 &self,
618 document: &XmlDocument,
619 request: &VerificationRequest<'_>,
620 ) -> Result<VerifyEvidence, DsigError> {
621 verify_all_document(document, self, request)
622 }
623}
624
625#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
627pub enum SignatureRequirement {
628 #[default]
630 All,
631 AtLeast(usize),
633 Exactly(usize),
635}
636
637pub struct CallerTrustedSignatureKey<'a> {
639 pub signature: NodeIdentity,
641 pub key: &'a dyn VerifyingKey,
643}
644
645#[derive(Default)]
647pub struct VerificationRequest<'a> {
648 pub expected_targets: &'a [NodeIdentity],
651 pub trusted_keys: &'a [CallerTrustedSignatureKey<'a>],
653 pub external_resources: Option<&'a HashMap<String, Vec<u8>>>,
655 pub signatures: SignatureRequirement,
657 pub correlation: Option<[u8; 16]>,
659}
660
661#[derive(Debug)]
663pub struct SignatureEvidence {
664 identity: NodeIdentity,
665 result: Result<VerifyResult, DsigError>,
666}
667
668impl SignatureEvidence {
669 #[must_use]
671 pub const fn identity(&self) -> NodeIdentity {
672 self.identity
673 }
674
675 pub const fn result(&self) -> &Result<VerifyResult, DsigError> {
677 &self.result
678 }
679}
680
681#[derive(Debug)]
683#[must_use = "inspect the individual outcomes before accepting the document"]
684pub struct VerifyEvidence {
685 document: NodeIdentity,
686 signatures: Vec<SignatureEvidence>,
687 accepted: bool,
688 correlation: Option<[u8; 16]>,
689}
690
691impl VerifyEvidence {
692 pub fn accepted(&self, document: &XmlDocument) -> Result<bool, DsigError> {
695 document.with_view(|view| {
696 view.resolve_node(self.document)?;
697 Ok(self.accepted)
698 })
699 }
700
701 #[must_use]
703 pub const fn correlation(&self) -> Option<[u8; 16]> {
704 self.correlation
705 }
706
707 pub fn covers_element(
715 &self,
716 document: &XmlDocument,
717 target: NodeIdentity,
718 ) -> Result<bool, DsigError> {
719 document.with_view(|view| {
720 view.resolve_node(self.document)?;
721 let target_node = view.resolve_node(target)?;
722 if !target_node.is_element() {
723 return Err(DsigError::InvalidRequest {
724 reason: "coverage target must be an element",
725 });
726 }
727 let target_id = target_node.id().get();
728 for signature in &self.signatures {
729 let Ok(result) = &signature.result else {
730 continue;
731 };
732 if result.status != DsigStatus::Valid
733 || !matches!(
734 result.key_trust,
735 super::KeyTrustEvidence::CallerTrusted
736 | super::KeyTrustEvidence::ValidatedX509(_)
737 )
738 {
739 continue;
740 }
741 let signature_node = view.resolve_node(signature.identity)?;
742 for reference in result
743 .signed_info_references
744 .iter()
745 .chain(&result.manifest_references)
746 {
747 if reference.status != DsigStatus::Valid {
748 continue;
749 }
750 let Some(root) = reference.target_identity else {
751 continue;
752 };
753 if reference.coverage == ReferenceCoverage::TransformedData {
754 continue;
755 }
756 let root = view.resolve_node(root)?;
757 if !subtree_node_id_range(root).contains(&target_id) {
761 continue;
762 }
763 if reference.coverage == ReferenceCoverage::EnvelopedElementContent
764 && subtree_node_id_range(signature_node).contains(&target_id)
765 {
766 continue;
767 }
768 return Ok(true);
769 }
770 }
771 Ok(false)
772 })
773 }
774
775 #[must_use]
777 pub fn signatures(&self) -> &[SignatureEvidence] {
778 &self.signatures
779 }
780
781 #[must_use]
786 pub fn all_valid(&self) -> bool {
787 !self.signatures.is_empty()
788 && self.signatures.iter().all(|entry| {
789 entry.result.as_ref().is_ok_and(|result| {
790 result.status == DsigStatus::Valid
791 && result
792 .manifest_references
793 .iter()
794 .all(|reference| reference.status == DsigStatus::Valid)
795 })
796 })
797 }
798}
799
800impl Default for VerifyContext<'_> {
801 fn default() -> Self {
802 Self::new()
803 }
804}
805
806#[derive(Debug, Clone)]
808#[non_exhaustive]
809#[must_use = "inspect status before accepting the reference result"]
810pub struct ReferenceResult {
811 pub reference_identity: Option<NodeIdentity>,
813 pub target_identity: Option<NodeIdentity>,
815 pub external_resource_fingerprint: Option<[u8; 32]>,
818 pub coverage: ReferenceCoverage,
820 pub reference_set: ReferenceSet,
822 pub reference_index: usize,
824 pub uri: String,
826 pub digest_algorithm: DigestAlgorithm,
828 pub status: DsigStatus,
830 pub pre_digest_data: Option<Vec<u8>>,
832}
833
834#[derive(Debug, Clone, Copy, PartialEq, Eq)]
840#[non_exhaustive]
841pub enum ReferenceCoverage {
842 TransformedData,
844 ElementContent,
846 EnvelopedElementContent,
848}
849
850#[derive(Debug, Clone, Copy, PartialEq, Eq)]
852#[non_exhaustive]
853pub enum ReferenceSet {
854 SignedInfo,
856 Manifest,
858}
859
860#[derive(Debug, Clone, Copy, PartialEq, Eq)]
862#[non_exhaustive]
863pub enum DsigStatus {
864 Valid,
866 Invalid(FailureReason),
868}
869
870#[derive(Debug, Clone, Copy, PartialEq, Eq)]
872#[non_exhaustive]
873pub enum FailureReason {
874 ReferenceDigestMismatch {
876 ref_index: usize,
886 },
887 ReferencePolicyViolation {
889 ref_index: usize,
891 },
892 ReferenceProcessingFailure {
894 ref_index: usize,
896 },
897 SignatureMismatch,
899 KeyNotFound,
901}
902
903#[derive(Debug)]
905#[non_exhaustive]
906#[must_use = "check first_failure/results before accepting the reference set"]
907pub struct ReferencesResult {
908 pub results: Vec<ReferenceResult>,
911 pub first_failure: Option<usize>,
913}
914
915impl ReferencesResult {
916 #[must_use]
918 pub fn all_valid(&self) -> bool {
919 self.results
920 .iter()
921 .all(|result| matches!(result.status, DsigStatus::Valid))
922 }
923}
924
925pub fn process_reference(
944 reference: &Reference,
945 resolver: &UriReferenceResolver<'_>,
946 signature_node: Node<'_, '_>,
947 reference_set: ReferenceSet,
948 reference_index: usize,
949 store_pre_digest: bool,
950) -> Result<ReferenceResult, ReferenceProcessingError> {
951 let execution_budget = TransformExecutionBudget::default();
952 let canonicalized_data_budget = CanonicalizedDataBudget::default();
953 let execution = ReferenceExecutionContext {
954 store_pre_digest,
955 transform_options: TransformOptions::default(),
956 transform_budget: &execution_budget,
957 canonicalized_data_budget: &canonicalized_data_budget,
958 provider: crate::provider::default_provider(),
959 };
960 process_reference_with_options(
961 reference,
962 resolver,
963 signature_node,
964 reference_set,
965 reference_index,
966 reference_origin_node(signature_node, reference_set, reference_index),
967 &execution,
968 )
969}
970
971fn reference_origin_node<'a, 'input>(
972 signature_node: Node<'a, 'input>,
973 reference_set: ReferenceSet,
974 reference_index: usize,
975) -> Option<Node<'a, 'input>> {
976 let is_reference = |node: &Node<'_, '_>| {
977 node.is_element()
978 && node.tag_name().namespace() == Some(XMLDSIG_NS)
979 && node.tag_name().name() == "Reference"
980 };
981 match reference_set {
982 ReferenceSet::SignedInfo => signature_node
983 .children()
984 .find(|node| {
985 node.is_element()
986 && node.tag_name().namespace() == Some(XMLDSIG_NS)
987 && node.tag_name().name() == "SignedInfo"
988 })?
989 .children()
990 .filter(is_reference)
991 .nth(reference_index),
992 ReferenceSet::Manifest => signature_node
993 .children()
994 .filter(|node| {
995 node.is_element()
996 && node.tag_name().namespace() == Some(XMLDSIG_NS)
997 && node.tag_name().name() == "Object"
998 })
999 .flat_map(|object| {
1000 object.children().filter(|node| {
1001 node.is_element()
1002 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1003 && node.tag_name().name() == "Manifest"
1004 })
1005 })
1006 .flat_map(|manifest| manifest.children().filter(is_reference))
1007 .nth(reference_index),
1008 }
1009}
1010
1011fn bind_reference_evidence(
1012 result: &mut ReferenceResult,
1013 reference: &Reference,
1014 view: DocumentView<'_>,
1015 origin: Option<Node<'_, '_>>,
1016 observed: &OperationResourceIdentity,
1017) {
1018 result.reference_identity = origin.map(|node| view.node_identity(node));
1019 if let OperationResourceIdentity::External { fingerprint, .. } = observed {
1020 result.external_resource_fingerprint = Some(*fingerprint);
1021 }
1022 if let OperationResourceIdentity::DocumentNode(target) = observed {
1023 result.target_identity = Some(*target);
1024 let mut enveloped = false;
1029 let mut canonicalized = false;
1030 for transform in &reference.transforms {
1031 match transform {
1032 Transform::C14n(_) if !canonicalized => canonicalized = true,
1033 Transform::Enveloped if !canonicalized => enveloped = true,
1034 _ => return,
1035 }
1036 }
1037 result.coverage = if enveloped {
1038 ReferenceCoverage::EnvelopedElementContent
1039 } else {
1040 ReferenceCoverage::ElementContent
1041 };
1042 }
1043}
1044
1045struct ReferenceExecutionContext<'a> {
1046 store_pre_digest: bool,
1047 transform_options: TransformOptions,
1048 transform_budget: &'a TransformExecutionBudget,
1049 canonicalized_data_budget: &'a CanonicalizedDataBudget,
1050 provider: &'a dyn crate::provider::CryptoProvider,
1051}
1052
1053struct CanonicalizedDataBudget {
1054 remaining: Cell<usize>,
1055 max_bytes: usize,
1056}
1057
1058struct VerificationOperationBudgets {
1059 transforms: TransformExecutionBudget,
1060 canonicalized: CanonicalizedDataBudget,
1061 xpath_parse: RefCell<XPathSignatureParseBudget>,
1062 key_info_materialization: RefCell<KeyInfoMaterializationState>,
1063 external_resource_identities: RefCell<HashMap<String, OperationResourceIdentity>>,
1064 resolved_reference_resources: RefCell<HashMap<NodeId, OperationResourceIdentity>>,
1065 external_resource_set_identity: RefCell<Option<OperationResourceIdentity>>,
1066}
1067
1068impl VerificationOperationBudgets {
1069 fn with_transforms(
1070 policy: &crate::policy::VerificationPolicy,
1071 transforms: TransformExecutionBudget,
1072 ) -> Self {
1073 Self {
1074 transforms,
1075 canonicalized: CanonicalizedDataBudget::with_limit(
1076 policy.resources.effective_canonicalized_bytes(),
1077 ),
1078 xpath_parse: RefCell::new(XPathSignatureParseBudget::from_resources(&policy.resources)),
1079 key_info_materialization: RefCell::new(KeyInfoMaterializationState::default()),
1080 external_resource_identities: RefCell::new(HashMap::new()),
1081 resolved_reference_resources: RefCell::new(HashMap::new()),
1082 external_resource_set_identity: RefCell::new(None),
1083 }
1084 }
1085
1086 fn external_resource_set_identity(
1087 &self,
1088 resolver: &UriReferenceResolver<'_>,
1089 ) -> OperationResourceIdentity {
1090 let mut cached = self.external_resource_set_identity.borrow_mut();
1093 cached
1094 .get_or_insert_with(|| resolver.external_resource_set_identity())
1095 .clone()
1096 }
1097
1098 fn resource_identity_for_reference(
1099 &self,
1100 reference: &Reference,
1101 index: usize,
1102 resolver: &UriReferenceResolver<'_>,
1103 view: DocumentView<'_>,
1104 origin: Option<Node<'_, '_>>,
1105 ) -> OperationResourceIdentity {
1106 let Some(uri) = reference.uri.as_deref() else {
1107 return OperationResourceIdentity::Generated("omitted-reference", index);
1108 };
1109 if uri.is_empty() || uri == "#xpointer(/)" {
1110 return OperationResourceIdentity::DocumentNode(view.root());
1117 }
1118 if uri.starts_with('#') {
1119 return resolver
1120 .node_id_for_same_document_reference(uri)
1121 .ok()
1122 .flatten()
1123 .map(|node| OperationResourceIdentity::DocumentNode(view.node_identity_by_id(node)))
1124 .unwrap_or(OperationResourceIdentity::Generated(
1125 "missing-document-reference",
1126 index,
1127 ));
1128 }
1129
1130 let resolved_uri;
1131 let uri = if let Some(origin) = origin {
1132 if let Some(identity) = self.resolved_reference_resources.borrow().get(&origin.id()) {
1133 return identity.clone();
1134 }
1135 let Ok(resolved) = crate::c14n::xml_base::resolve_uri_from_node_with_budget(
1139 origin,
1140 uri,
1141 self.transforms.xml_base_resolution(),
1142 ) else {
1143 return OperationResourceIdentity::Generated(
1144 "unresolved-external-reference",
1145 index,
1146 );
1147 };
1148 resolved_uri = resolved;
1149 resolved_uri.as_str()
1150 } else {
1151 uri
1152 };
1153 if let Some(identity) = self.external_resource_identities.borrow().get(uri).cloned() {
1154 if let Some(origin) = origin {
1155 self.resolved_reference_resources
1156 .borrow_mut()
1157 .insert(origin.id(), identity.clone());
1158 }
1159 return identity;
1160 }
1161 let identity = resolver.external_resource_identity(uri).unwrap_or(
1162 OperationResourceIdentity::Generated("missing-external-reference", index),
1163 );
1164 if matches!(identity, OperationResourceIdentity::External { .. }) {
1165 self.external_resource_identities
1166 .borrow_mut()
1167 .insert(uri.to_owned(), identity.clone());
1168 if let Some(origin) = origin {
1169 self.resolved_reference_resources
1170 .borrow_mut()
1171 .insert(origin.id(), identity.clone());
1172 }
1173 }
1174 identity
1175 }
1176}
1177
1178struct VerificationPlanNodes {
1179 document: OperationNodeId,
1180 key_materialization: OperationNodeId,
1181 key: OperationNodeId,
1182 digests: Vec<OperationNodeId>,
1183 canonicalization: OperationNodeId,
1184 crypto: OperationNodeId,
1185}
1186
1187fn compile_verification_operation_plan(
1188 operation: &mut OperationExecutionContext<
1189 &crate::policy::VerificationPolicy,
1190 &VerificationOperationBudgets,
1191 >,
1192 view: DocumentView<'_>,
1193 signature_node: Node<'_, '_>,
1194 references: &[Reference],
1195 resolver: &UriReferenceResolver<'_>,
1196) -> Result<VerificationPlanNodes, SignatureVerificationPipelineError> {
1197 operation
1198 .validate_document_view(view)
1199 .map_err(map_verification_plan_error)?;
1200 let document_node = operation.add_node(
1201 OperationNodeKind::Document,
1202 OperationStage::Parse,
1203 Some(OperationResourceIdentity::DocumentNode(
1204 view.node_identity(signature_node),
1205 )),
1206 );
1207 let key_materialization_resource = operation.budgets().external_resource_set_identity(resolver);
1208 let key_materialization = operation.add_node(
1209 OperationNodeKind::Key { index: 0 },
1210 OperationStage::Resolve,
1211 Some(key_materialization_resource.clone()),
1212 );
1213 let key_node = operation.add_node(
1214 OperationNodeKind::Key { index: 1 },
1215 OperationStage::Resolve,
1216 None,
1217 );
1218 operation
1219 .add_dependency(key_materialization, document_node)
1220 .map_err(map_verification_plan_error)?;
1221 operation
1222 .add_dependency(key_node, key_materialization)
1223 .map_err(map_verification_plan_error)?;
1224 let mut digests = Vec::with_capacity(references.len());
1225 for (index, reference) in references.iter().enumerate() {
1226 let resource = operation.budgets().resource_identity_for_reference(
1227 reference,
1228 index,
1229 resolver,
1230 view,
1231 reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
1232 );
1233 let digest_node = operation.add_node(
1234 OperationNodeKind::Digest { index },
1235 OperationStage::Digest,
1236 Some(resource.clone()),
1237 );
1238 operation
1239 .add_dependency(digest_node, document_node)
1240 .map_err(map_verification_plan_error)?;
1241 digests.push(digest_node);
1242 }
1243 let canonicalization = operation.add_node(
1244 OperationNodeKind::Canonicalization,
1245 OperationStage::Canonicalization,
1246 None,
1247 );
1248 for digest in &digests {
1249 operation
1250 .add_dependency(canonicalization, *digest)
1251 .map_err(map_verification_plan_error)?;
1252 }
1253 let crypto = operation.add_node(OperationNodeKind::Crypto, OperationStage::Crypto, None);
1254 operation
1255 .add_dependency(crypto, key_node)
1256 .map_err(map_verification_plan_error)?;
1257 operation
1258 .add_dependency(crypto, canonicalization)
1259 .map_err(map_verification_plan_error)?;
1260 operation.compile().map_err(map_verification_plan_error)?;
1261 Ok(VerificationPlanNodes {
1262 document: document_node,
1263 key_materialization,
1264 key: key_node,
1265 digests,
1266 canonicalization,
1267 crypto,
1268 })
1269}
1270
1271fn map_verification_plan_error(error: OperationPlanError) -> SignatureVerificationPipelineError {
1272 SignatureVerificationPipelineError::OperationPlan(error.to_string())
1273}
1274
1275impl Default for CanonicalizedDataBudget {
1276 fn default() -> Self {
1277 Self {
1278 remaining: Cell::new(CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING),
1279 max_bytes: CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
1280 }
1281 }
1282}
1283
1284impl CanonicalizedDataBudget {
1285 fn remaining(&self) -> usize {
1286 self.remaining.get()
1287 }
1288
1289 fn charge(&self, bytes: usize) -> Result<(), ReferenceProcessingError> {
1290 let available = self.remaining.get();
1291 let Some(remaining) = available.checked_sub(bytes) else {
1292 self.remaining.set(0);
1293 return Err(crate::policy::PolicyViolation::ResourceLimit {
1294 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
1295 maximum: self.max_bytes,
1296 actual: self
1297 .max_bytes
1298 .saturating_add(bytes.saturating_sub(available)),
1299 }
1300 .into());
1301 };
1302 self.remaining.set(remaining);
1303 Ok(())
1304 }
1305
1306 fn with_limit(max_bytes: usize) -> Self {
1307 Self {
1308 remaining: Cell::new(max_bytes),
1309 max_bytes,
1310 }
1311 }
1312}
1313
1314fn process_reference_with_options(
1315 reference: &Reference,
1316 resolver: &UriReferenceResolver<'_>,
1317 signature_node: Node<'_, '_>,
1318 reference_set: ReferenceSet,
1319 reference_index: usize,
1320 reference_node: Option<Node<'_, '_>>,
1321 execution: &ReferenceExecutionContext<'_>,
1322) -> Result<ReferenceResult, ReferenceProcessingError> {
1323 let uri = reference
1326 .uri
1327 .as_deref()
1328 .ok_or(ReferenceProcessingError::MissingUri)?;
1329 let initial_data = reference_node
1330 .map_or_else(
1331 || {
1332 resolver.dereference_with_budget(
1333 uri,
1334 execution.transform_budget.node_set_materialization(),
1335 )
1336 },
1337 |node| {
1338 resolver.dereference_from_with_budget(
1339 uri,
1340 node,
1341 execution.transform_budget.node_set_materialization(),
1342 execution.transform_budget.xml_base_resolution(),
1343 )
1344 },
1345 )
1346 .map_err(ReferenceProcessingError::UriDereference)?;
1347
1348 let pre_digest_bytes = execute_transforms_with_options_and_budget(
1350 signature_node,
1351 initial_data,
1352 &reference.transforms,
1353 execution.transform_options,
1354 execution.transform_budget,
1355 )
1356 .map_err(ReferenceProcessingError::Transform)?;
1357
1358 let computed_digest = super::compute_digest_with_provider(
1360 execution.provider,
1361 reference.digest_method,
1362 &pre_digest_bytes,
1363 )?;
1364
1365 let status = if constant_time_eq(&computed_digest, &reference.digest_value) {
1367 DsigStatus::Valid
1368 } else {
1369 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch {
1370 ref_index: reference_index,
1371 })
1372 };
1373
1374 let pre_digest_data = if execution.store_pre_digest {
1375 execution
1376 .canonicalized_data_budget
1377 .charge(pre_digest_bytes.len())?;
1378 Some(pre_digest_bytes)
1379 } else {
1380 None
1381 };
1382
1383 Ok(ReferenceResult {
1384 reference_identity: None,
1385 target_identity: None,
1386 external_resource_fingerprint: None,
1387 coverage: ReferenceCoverage::TransformedData,
1388 reference_set,
1389 reference_index,
1390 uri: uri.to_owned(),
1391 digest_algorithm: reference.digest_method,
1392 status,
1393 pre_digest_data,
1394 })
1395}
1396
1397pub fn process_all_references(
1409 references: &[Reference],
1410 resolver: &UriReferenceResolver<'_>,
1411 signature_node: Node<'_, '_>,
1412 store_pre_digest: bool,
1413) -> Result<ReferencesResult, ReferenceProcessingError> {
1414 let execution_budget = TransformExecutionBudget::default();
1415 let canonicalized_data_budget = CanonicalizedDataBudget::default();
1416 let execution = ReferenceExecutionContext {
1417 store_pre_digest,
1418 transform_options: TransformOptions::default(),
1419 transform_budget: &execution_budget,
1420 canonicalized_data_budget: &canonicalized_data_budget,
1421 provider: crate::provider::default_provider(),
1422 };
1423 process_all_references_with_options(references, resolver, signature_node, &execution)
1424}
1425
1426fn process_all_references_with_options(
1427 references: &[Reference],
1428 resolver: &UriReferenceResolver<'_>,
1429 signature_node: Node<'_, '_>,
1430 execution: &ReferenceExecutionContext<'_>,
1431) -> Result<ReferencesResult, ReferenceProcessingError> {
1432 let mut results = Vec::with_capacity(references.len());
1433
1434 for (i, reference) in references.iter().enumerate() {
1435 let result = process_reference_with_options(
1436 reference,
1437 resolver,
1438 signature_node,
1439 ReferenceSet::SignedInfo,
1440 i,
1441 reference_origin_node(signature_node, ReferenceSet::SignedInfo, i),
1442 execution,
1443 )?;
1444 let failed = matches!(result.status, DsigStatus::Invalid(_));
1445 results.push(result);
1446
1447 if failed {
1448 return Ok(ReferencesResult {
1449 results,
1450 first_failure: Some(i),
1451 });
1452 }
1453 }
1454
1455 Ok(ReferencesResult {
1456 results,
1457 first_failure: None,
1458 })
1459}
1460
1461#[derive(Debug, thiserror::Error)]
1465#[non_exhaustive]
1466pub enum ReferenceProcessingError {
1467 #[error("verification policy violation: {0}")]
1469 Policy(#[from] crate::policy::PolicyViolation),
1470
1471 #[error("cryptographic provider error: {0}")]
1473 Provider(#[from] crate::provider::ProviderError),
1474
1475 #[error("reference URI is required; omitted URI references are not supported")]
1477 MissingUri,
1478
1479 #[error("URI dereference failed: {0}")]
1481 UriDereference(#[source] super::types::TransformError),
1482
1483 #[error("transform failed: {0}")]
1485 Transform(#[source] super::types::TransformError),
1486}
1487
1488impl ReferenceProcessingError {
1489 fn into_policy_violation(self) -> Result<crate::policy::PolicyViolation, Self> {
1490 match self {
1491 Self::Policy(error)
1492 | Self::UriDereference(TransformError::Policy(error))
1493 | Self::Transform(TransformError::Policy(error)) => Ok(error),
1494 error => Err(error),
1495 }
1496 }
1497}
1498
1499#[derive(Debug)]
1501#[non_exhaustive]
1502#[must_use = "inspect status before accepting the document"]
1503pub struct VerifyResult {
1504 pub key_trust: super::KeyTrustEvidence,
1506 pub status: DsigStatus,
1511 pub signed_info_references: Vec<ReferenceResult>,
1515 pub manifest_references: Vec<ReferenceResult>,
1525 pub canonicalized_signed_info: Option<Vec<u8>>,
1528}
1529
1530#[derive(Debug, thiserror::Error)]
1532#[non_exhaustive]
1533pub enum DsigError {
1534 #[error("invalid verification request: {reason}")]
1536 InvalidRequest {
1537 reason: &'static str,
1539 },
1540 #[error("verification policy violation: {0}")]
1542 Policy(#[from] crate::policy::PolicyViolation),
1543
1544 #[error("cryptographic provider error: {0}")]
1546 Provider(#[from] crate::provider::ProviderError),
1547
1548 #[error("XML parse error: {0}")]
1550 XmlParse(#[from] crate::xml::dom::ParseError),
1551
1552 #[error("XML document error: {0}")]
1554 Document(#[from] crate::document::XmlDocumentError),
1555
1556 #[error("missing required element: <{element}>")]
1558 MissingElement {
1559 element: &'static str,
1561 },
1562
1563 #[error("invalid Signature structure: {reason}")]
1565 InvalidStructure {
1566 reason: &'static str,
1568 },
1569
1570 #[error("invalid operation plan: {0}")]
1572 OperationPlan(String),
1573
1574 #[error("selected node ID is missing or ambiguous: {id}")]
1576 SelectedNodeUnavailable {
1577 id: String,
1579 },
1580
1581 #[error("failed to parse SignedInfo: {0}")]
1583 ParseSignedInfo(super::parse::ParseError),
1584
1585 #[error("failed to parse KeyInfo: {0}")]
1587 ParseKeyInfo(#[source] super::parse::ParseError),
1588
1589 #[error("key resolution failed: {0}")]
1591 KeyResolution(#[from] super::keys::KeyResolutionError),
1592
1593 #[error("failed to parse Manifest reference: {0}")]
1595 ParseManifestReference(#[source] ParseError),
1596
1597 #[error("reference processing failed: {0}")]
1599 Reference(ReferenceProcessingError),
1600
1601 #[error("SignedInfo canonicalization failed: {0}")]
1603 Canonicalization(#[from] crate::c14n::C14nError),
1604
1605 #[error("invalid SignatureValue base64: {0}")]
1607 SignatureValueBase64(#[from] base64::DecodeError),
1608
1609 #[error("signature verification failed: {0}")]
1611 Crypto(#[from] SignatureVerificationError),
1612}
1613
1614impl From<super::parse::ParseError> for DsigError {
1615 fn from(error: super::parse::ParseError) -> Self {
1616 match error {
1617 super::parse::ParseError::Policy(error) => Self::Policy(error),
1618 super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1619 Self::Policy(error)
1620 }
1621 error => Self::ParseSignedInfo(error),
1622 }
1623 }
1624}
1625
1626fn map_key_info_parse_error(error: super::parse::ParseError) -> DsigError {
1627 match error {
1628 super::parse::ParseError::Policy(error)
1629 | super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1630 DsigError::Policy(error)
1631 }
1632 error => DsigError::ParseKeyInfo(error),
1633 }
1634}
1635
1636fn map_manifest_parse_error(error: super::parse::ParseError) -> DsigError {
1637 match error {
1638 super::parse::ParseError::Policy(error)
1639 | super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1640 DsigError::Policy(error)
1641 }
1642 error => DsigError::ParseManifestReference(error),
1643 }
1644}
1645
1646impl From<ReferenceProcessingError> for DsigError {
1647 fn from(error: ReferenceProcessingError) -> Self {
1648 match error.into_policy_violation() {
1649 Ok(error) => Self::Policy(error),
1650 Err(error) => Self::Reference(error),
1651 }
1652 }
1653}
1654
1655type SignatureVerificationPipelineError = DsigError;
1656
1657impl From<OperationPlanError> for DsigError {
1658 fn from(error: OperationPlanError) -> Self {
1659 Self::OperationPlan(error.to_string())
1660 }
1661}
1662
1663pub fn verify_signature_with_pem_key(
1691 xml: &str,
1692 public_key_pem: &str,
1693 store_pre_digest: bool,
1694) -> Result<VerifyResult, DsigError> {
1695 struct PemVerifyingKey<'a> {
1696 public_key_pem: &'a str,
1697 }
1698
1699 impl VerifyingKey for PemVerifyingKey<'_> {
1700 fn verify_with_context(
1701 &self,
1702 algorithm: SignatureAlgorithm,
1703 context: &super::SignatureContext,
1704 signed_data: &[u8],
1705 signature_value: &[u8],
1706 ) -> Result<bool, DsigError> {
1707 if context.as_bytes().is_empty() {
1708 return self.verify(algorithm, signed_data, signature_value);
1709 }
1710 if algorithm.context_element().is_none() {
1711 return Err(SignatureVerificationError::UnsupportedAlgorithm {
1712 uri: algorithm.uri().to_owned(),
1713 }
1714 .into());
1715 }
1716 let (rest, pem) = x509_parser::pem::parse_x509_pem(self.public_key_pem.as_bytes())
1717 .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
1718 if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
1719 return Err(SignatureVerificationError::InvalidKeyPem.into());
1720 }
1721 Ok(super::modern::verify_with_context(
1722 algorithm,
1723 &pem.contents,
1724 context,
1725 signed_data,
1726 signature_value,
1727 )?)
1728 }
1729
1730 fn verify(
1731 &self,
1732 algorithm: SignatureAlgorithm,
1733 signed_data: &[u8],
1734 signature_value: &[u8],
1735 ) -> Result<bool, DsigError> {
1736 verify_with_algorithm(algorithm, self.public_key_pem, signed_data, signature_value)
1737 }
1738 }
1739
1740 let key = PemVerifyingKey { public_key_pem };
1741 VerifyContext::new()
1742 .key(&key)
1743 .store_pre_digest(store_pre_digest)
1744 .verify(xml)
1745}
1746
1747fn verify_signature_with_context(
1748 xml: &str,
1749 ctx: &VerifyContext<'_>,
1750) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1751 ctx.policy.validate()?;
1752 ctx.policy.resources.validate_xml_document_len(xml.len())?;
1753 let execution_budget = TransformExecutionBudget::from_resources(&ctx.policy.resources)
1754 .with_xml_backend(ctx.xml_backend);
1755 let settings = DocumentParseSettings::from_policy(&ctx.policy.xml, &ctx.policy.resources)
1756 .with_backend(ctx.xml_backend);
1757 let document = XmlDocument::parse_with_settings_and_budget(
1758 xml.to_owned(),
1759 settings,
1760 execution_budget.xml_parse_work(),
1761 )
1762 .map_err(|error| map_document_parse_error(error, settings))?;
1763 verify_signature_document_with_context_and_transforms(&document, ctx, execution_budget)
1764}
1765
1766fn map_document_parse_error(error: XmlDocumentError, settings: DocumentParseSettings) -> DsigError {
1767 match error.into_policy_violation(settings) {
1768 Ok(error) => DsigError::Policy(error),
1769 Err(XmlDocumentError::Parse(error)) => DsigError::XmlParse(error),
1770 Err(error) => DsigError::Document(error),
1771 }
1772}
1773
1774#[cfg(test)]
1775mod xml_parse_budget_tests {
1776 use super::*;
1777
1778 #[test]
1779 fn verification_initial_parse_uses_the_policy_work_budget() {
1780 let xml = "<root/>";
1783 let mut policy = crate::policy::VerificationPolicy::default();
1784 policy.resources.max_xml_parse_work_bytes = 0;
1785
1786 let error = VerifyContext::new()
1787 .policy(policy)
1788 .verify(xml)
1789 .expect_err("a zero parse-work budget must reject the input parse");
1790
1791 assert!(matches!(
1792 error,
1793 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1794 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
1795 maximum: 0,
1796 actual,
1797 }) if actual == xml.len()
1798 ));
1799 }
1800}
1801
1802fn verify_signature_document_with_context(
1803 document: &XmlDocument,
1804 ctx: &VerifyContext<'_>,
1805) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1806 document.validate_operation_policy(&ctx.policy.xml, &ctx.policy.resources)?;
1807 verify_signature_document_with_context_and_transforms(
1808 document,
1809 ctx,
1810 TransformExecutionBudget::from_resources(&ctx.policy.resources)
1811 .with_xml_backend(ctx.xml_backend),
1812 )
1813}
1814
1815fn count_signature_nodes(view: DocumentView<'_>, maximum: usize) -> Result<usize, DsigError> {
1816 let mut count = 0;
1817 for node in view.document().descendants() {
1818 if !node.has_tag_name((XMLDSIG_NS, "Signature")) {
1819 continue;
1820 }
1821 if count == maximum {
1822 return Err(crate::policy::PolicyViolation::ResourceLimit {
1823 resource: crate::policy::resource_name::VERIFICATION_SIGNATURES,
1824 maximum,
1825 actual: count + 1,
1826 }
1827 .into());
1828 }
1829 count += 1;
1830 }
1831 Ok(count)
1832}
1833
1834fn verify_all_document(
1835 document: &XmlDocument,
1836 ctx: &VerifyContext<'_>,
1837 request: &VerificationRequest<'_>,
1838) -> Result<VerifyEvidence, DsigError> {
1839 ctx.policy.validate()?;
1840 document.validate_operation_policy(&ctx.policy.xml, &ctx.policy.resources)?;
1841 if request.external_resources.is_some() && ctx.external_resources.is_some() {
1842 return Err(DsigError::InvalidRequest {
1843 reason: "external resources configured in both context and request",
1844 });
1845 }
1846 let capture_time =
1850 ctx.policy.key_trust.verify_x509_chains && ctx.policy.key_trust.verification_time.is_none();
1851 let captured_context;
1852 let ctx = if capture_time || request.external_resources.is_some() {
1853 captured_context = {
1854 let mut captured = ctx.clone();
1855 if let Some(resources) = request.external_resources {
1856 captured.external_resources = Some(resources);
1857 }
1858 if capture_time {
1859 captured.policy.key_trust.verification_time = Some(std::time::SystemTime::now());
1860 }
1861 captured
1862 };
1863 &captured_context
1864 } else {
1865 ctx
1866 };
1867 let budgets = VerificationOperationBudgets::with_transforms(
1868 &ctx.policy,
1869 TransformExecutionBudget::from_resources(&ctx.policy.resources)
1870 .with_xml_backend(ctx.xml_backend),
1871 );
1872 document.with_view(|view| {
1873 match request.signatures {
1874 SignatureRequirement::AtLeast(0) | SignatureRequirement::Exactly(0) => {
1875 return Err(DsigError::InvalidRequest {
1876 reason: "signature requirement must be nonzero",
1877 });
1878 }
1879 _ => {}
1880 }
1881 if request.expected_targets.len() > ctx.policy.resources.max_references {
1882 return Err(DsigError::InvalidRequest {
1883 reason: "too many expected coverage targets",
1884 });
1885 }
1886 if request.trusted_keys.len() > ctx.policy.resources.max_signatures {
1887 return Err(DsigError::InvalidRequest {
1888 reason: "too many trusted signature keys",
1889 });
1890 }
1891 for (index, target) in request.expected_targets.iter().copied().enumerate() {
1892 if !view.resolve_node(target)?.is_element() {
1893 return Err(DsigError::InvalidRequest {
1894 reason: "coverage target must be an element",
1895 });
1896 }
1897 if request.expected_targets[..index].contains(&target) {
1898 return Err(DsigError::InvalidRequest {
1899 reason: "duplicate expected coverage target",
1900 });
1901 }
1902 }
1903 for (index, entry) in request.trusted_keys.iter().enumerate() {
1904 if !view
1905 .resolve_node(entry.signature)?
1906 .has_tag_name((XMLDSIG_NS, "Signature"))
1907 {
1908 return Err(DsigError::InvalidRequest {
1909 reason: "trusted key target must be a Signature",
1910 });
1911 }
1912 if request.trusted_keys[..index]
1913 .iter()
1914 .any(|prior| prior.signature == entry.signature)
1915 {
1916 return Err(DsigError::InvalidRequest {
1917 reason: "ambiguous trusted keys for one Signature",
1918 });
1919 }
1920 }
1921 let is_signature = |node: &Node<'_, '_>| node.has_tag_name((XMLDSIG_NS, "Signature"));
1922 let count = count_signature_nodes(view, ctx.policy.resources.max_signatures)?;
1923 let mut signatures = Vec::with_capacity(count);
1926 let resolver = UriReferenceResolver::with_document_view(view, ctx.id_attributes)
1927 .with_same_document_id_semantics(ctx.policy.transforms.same_document_id_semantics)
1928 .with_external_resource_limits(
1929 ctx.policy.resources.max_external_resource_bytes,
1930 ctx.policy.resources.max_external_resource_total_bytes,
1931 );
1932 let resolver = match ctx.external_resources {
1933 Some(resources) => resolver.with_external_resources(resources),
1934 None => resolver,
1935 };
1936 for signature in view.document().descendants().filter(is_signature) {
1937 let mut operation = OperationExecutionContext::new(
1941 &ctx.policy,
1942 &budgets,
1943 Some((view.identity(), view.generation())),
1944 );
1945 let signature_identity = view.node_identity(signature);
1946 let key = request
1947 .trusted_keys
1948 .iter()
1949 .find(|entry| entry.signature == signature_identity)
1950 .map(|entry| entry.key)
1951 .or(ctx.key);
1952 let result =
1953 verify_signature_node(view, ctx, key, &mut operation, &resolver, signature);
1954 signatures.push(SignatureEvidence {
1955 identity: view.node_identity(signature),
1956 result,
1957 });
1958 }
1959 let mut evidence = VerifyEvidence {
1960 document: view.root(),
1961 signatures,
1962 accepted: false,
1963 correlation: request.correlation,
1964 };
1965 let valid = evidence
1966 .signatures
1967 .iter()
1968 .filter(|entry| {
1969 entry.result.as_ref().is_ok_and(|result| {
1970 result.status == DsigStatus::Valid
1971 && result
1972 .manifest_references
1973 .iter()
1974 .all(|reference| reference.status == DsigStatus::Valid)
1975 })
1976 })
1977 .count();
1978 evidence.accepted = match request.signatures {
1979 SignatureRequirement::All => valid > 0 && valid == count,
1980 SignatureRequirement::AtLeast(minimum) => valid >= minimum,
1981 SignatureRequirement::Exactly(required) => valid == required && count == required,
1982 };
1983 for target in request.expected_targets {
1984 if !evidence.covers_element(document, *target)? {
1985 evidence.accepted = false;
1986 break;
1987 }
1988 }
1989 Ok(evidence)
1990 })
1991}
1992
1993fn verify_signature_document_with_context_and_transforms(
1994 document: &XmlDocument,
1995 ctx: &VerifyContext<'_>,
1996 transforms: TransformExecutionBudget,
1997) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1998 let captured_context;
2002 let ctx = if ctx.policy.key_trust.verify_x509_chains
2003 && ctx.policy.key_trust.verification_time.is_none()
2004 {
2005 captured_context = {
2006 let mut context = ctx.clone();
2007 context.policy.key_trust.verification_time = Some(std::time::SystemTime::now());
2008 context
2009 };
2010 &captured_context
2011 } else {
2012 ctx
2013 };
2014 let budgets = VerificationOperationBudgets::with_transforms(&ctx.policy, transforms);
2015 let mut operation = OperationExecutionContext::new(
2016 &ctx.policy,
2017 &budgets,
2018 Some((document.identity(), document.generation())),
2019 );
2020 document.with_view(|view| verify_signature_view(view, ctx, &mut operation))
2021}
2022
2023fn verify_signature_view<'a>(
2024 view: DocumentView<'a>,
2025 ctx: &VerifyContext<'_>,
2026 operation: &mut OperationExecutionContext<
2027 &crate::policy::VerificationPolicy,
2028 &VerificationOperationBudgets,
2029 >,
2030) -> Result<VerifyResult, SignatureVerificationPipelineError> {
2031 ctx.policy.validate()?;
2032 let doc = view.document();
2033 let resolver = UriReferenceResolver::with_document_view(view, ctx.id_attributes)
2034 .with_same_document_id_semantics(ctx.policy.transforms.same_document_id_semantics)
2035 .with_external_resource_limits(
2036 ctx.policy.resources.max_external_resource_bytes,
2037 ctx.policy.resources.max_external_resource_total_bytes,
2038 );
2039 let resolver = match ctx.external_resources {
2040 Some(resources) => resolver.with_external_resources(resources),
2041 None => resolver,
2042 };
2043 let start_node = match ctx.signature_selection {
2044 SignatureSelection::FirstSignatureUnderId(id) => {
2045 resolver.node_for_id(id).ok_or_else(|| {
2046 SignatureVerificationPipelineError::SelectedNodeUnavailable { id: id.to_owned() }
2047 })?
2048 }
2049 SignatureSelection::UniqueDocumentSignature
2050 | SignatureSelection::FirstDocumentSignature => doc.root(),
2051 };
2052 let mut signatures = start_node.descendants().filter(|node| {
2053 node.is_element()
2054 && node.tag_name().name() == "Signature"
2055 && node.tag_name().namespace() == Some(XMLDSIG_NS)
2056 });
2057 let signature_node = match (signatures.next(), ctx.signature_selection) {
2058 (None, _) => {
2059 return Err(SignatureVerificationPipelineError::MissingElement {
2060 element: "Signature",
2061 });
2062 }
2063 (
2067 Some(node),
2068 SignatureSelection::FirstDocumentSignature
2069 | SignatureSelection::FirstSignatureUnderId(_),
2070 ) => node,
2071 (Some(node), SignatureSelection::UniqueDocumentSignature)
2072 if signatures.next().is_none() =>
2073 {
2074 node
2075 }
2076 (Some(_), SignatureSelection::UniqueDocumentSignature) => {
2077 return Err(SignatureVerificationPipelineError::InvalidStructure {
2078 reason: "Signature must appear exactly once in document",
2079 });
2080 }
2081 };
2082
2083 verify_signature_node(view, ctx, ctx.key, operation, &resolver, signature_node)
2084}
2085
2086fn verify_signature_node<'a>(
2087 view: DocumentView<'a>,
2088 ctx: &VerifyContext<'_>,
2089 key: Option<&dyn VerifyingKey>,
2090 operation: &mut OperationExecutionContext<
2091 &crate::policy::VerificationPolicy,
2092 &VerificationOperationBudgets,
2093 >,
2094 resolver: &UriReferenceResolver<'a>,
2095 signature_node: Node<'a, 'a>,
2096) -> Result<VerifyResult, DsigError> {
2097 if ctx.policy.resources.max_signatures == 0 {
2098 return Err(crate::policy::PolicyViolation::ResourceLimit {
2099 resource: crate::policy::resource_name::VERIFICATION_SIGNATURES,
2100 maximum: 0,
2101 actual: 1,
2102 }
2103 .into());
2104 }
2105 let doc = view.document();
2106 let signature_children = parse_signature_children(signature_node)?;
2107 let signed_info_node = signature_children.signed_info_node;
2108 let should_parse_key_info = match (key, ctx.key_resolver) {
2109 (Some(_), _) => false,
2110 (None, Some(resolver)) => resolver.consumes_document_key_info(),
2111 (None, None) => true,
2112 };
2113 let mut key_info = if should_parse_key_info {
2114 signature_children
2115 .key_info_node
2116 .map(|node| {
2117 parse_key_info_with_policy_budgets(
2118 node,
2119 ctx.provider,
2120 operation.budgets().transforms.xml_base_resolution(),
2121 &ctx.policy.resources,
2122 ctx.policy
2123 .key_trust
2124 .certificate_signature_algorithms
2125 .as_ref(),
2126 )
2127 })
2128 .transpose()
2129 .map_err(map_key_info_parse_error)?
2130 } else {
2131 None
2132 };
2133
2134 let signed_info = parse_signed_info_with_xpath_budget(
2135 signed_info_node,
2136 &mut operation.budgets().xpath_parse.borrow_mut(),
2137 &ctx.policy.resources,
2138 )?;
2139 if signed_info.references.len() > ctx.policy.resources.max_references {
2140 return Err(crate::policy::PolicyViolation::ResourceLimit {
2141 resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
2142 maximum: ctx.policy.resources.max_references,
2143 actual: signed_info.references.len(),
2144 }
2145 .into());
2146 }
2147 for reference in &signed_info.references {
2148 if reference.transforms.len() > ctx.policy.resources.max_transforms_per_reference {
2149 return Err(crate::policy::PolicyViolation::ResourceLimit {
2150 resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
2151 maximum: ctx.policy.resources.max_transforms_per_reference,
2152 actual: reference.transforms.len(),
2153 }
2154 .into());
2155 }
2156 }
2157 ctx.policy
2158 .check_signature_algorithm(signed_info.signature_method)?;
2159 if let Some(full_output_bits) = signed_info.signature_method.hmac_output_bits() {
2164 ctx.policy.hmac.validate_output(
2165 signed_info.signature_method,
2166 signed_info
2167 .hmac_output_length_bits
2168 .unwrap_or(full_output_bits),
2169 )?;
2170 }
2171 for reference in &signed_info.references {
2172 if ctx
2173 .policy
2174 .digest_algorithms
2175 .as_ref()
2176 .is_some_and(|allowed| !allowed.contains(&reference.digest_method))
2177 {
2178 return Err(crate::policy::PolicyViolation::Algorithm {
2179 operation: "verification",
2180 algorithm: reference.digest_method.uri().to_string(),
2181 }
2182 .into());
2183 }
2184 }
2185 enforce_reference_policies(
2186 &signed_info.references,
2187 ctx.policy.uris.references,
2188 ctx.allowed_transform_uris(),
2189 )?;
2190 enforce_transform_allowed(ctx.allowed_transform_uris(), signed_info.c14n_method.uri())?;
2191
2192 if let Some(resources) = ctx.external_resources {
2193 validate_external_resource_map(
2194 resources,
2195 ctx.policy.resources.max_external_resource_bytes,
2196 ctx.policy.resources.max_external_resource_total_bytes,
2197 )
2198 .map_err(|error| match error {
2199 ExternalResourceMapError::Policy(error) => error.into(),
2200 ExternalResourceMapError::TotalLengthOverflow => {
2201 SignatureVerificationPipelineError::InvalidStructure {
2202 reason: "external resource total length overflow",
2203 }
2204 }
2205 })?;
2206 }
2207 let remaining_reference_capacity = ctx
2208 .policy
2209 .resources
2210 .max_references
2211 .checked_sub(signed_info.references.len())
2212 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2213 reason: "SignedInfo exceeds the per-signature Reference limit",
2214 })?;
2215 let plan_nodes = compile_verification_operation_plan(
2216 operation,
2217 view,
2218 signature_node,
2219 &signed_info.references,
2220 resolver,
2221 )?;
2222 let signature_identity =
2223 OperationResourceIdentity::DocumentNode(view.node_identity(signature_node));
2224 operation.run_with_resource(plan_nodes.document, &signature_identity, || {
2225 operation
2226 .validate_document_view(view)
2227 .map_err(map_verification_plan_error)
2228 })?;
2229 let observed_key_resources = operation.budgets().external_resource_set_identity(resolver);
2230 let retrieval_materialization = operation.run_with_resource(
2231 plan_nodes.key_materialization,
2232 &observed_key_resources,
2233 || {
2234 if let Some(info) = key_info.as_mut() {
2235 let budgets = operation.budgets();
2236 let mut xpath_parse = budgets.xpath_parse.borrow_mut();
2237 let mut retrieval_budgets = RetrievalMaterializationBudgets {
2238 xpath_parse: &mut xpath_parse,
2239 execution: &budgets.transforms,
2240 resources: &ctx.policy.resources,
2241 certificate_signature_algorithms: ctx
2242 .policy
2243 .key_trust
2244 .certificate_signature_algorithms
2245 .as_ref(),
2246 xml_backend: ctx.xml_backend,
2247 };
2248 let mut materialization = budgets.key_info_materialization.borrow_mut();
2249 let mut outcome = materialize_key_info_references_with_budgets(
2250 info,
2251 resolver,
2252 &ctx.policy,
2253 ctx.provider,
2254 &mut retrieval_budgets,
2255 &mut materialization,
2256 )?;
2257 outcome.merge(materialize_retrieval_methods_with_budgets(
2258 info,
2259 resolver,
2260 ctx.policy.uris.retrieval_methods,
2261 ctx.allowed_transform_uris(),
2262 ctx.provider,
2263 &mut retrieval_budgets,
2264 &mut materialization.candidate_work,
2265 )?);
2266 Ok::<_, SignatureVerificationPipelineError>(outcome)
2267 } else {
2268 Ok(RetrievalMaterialization::default())
2269 }
2270 },
2271 )?;
2272 let mut reference_results = Vec::with_capacity(signed_info.references.len());
2273 let mut first_failure = None;
2274 for (index, (node, reference)) in plan_nodes
2275 .digests
2276 .iter()
2277 .copied()
2278 .zip(&signed_info.references)
2279 .enumerate()
2280 {
2281 let observed = operation.budgets().resource_identity_for_reference(
2282 reference,
2283 index,
2284 resolver,
2285 view,
2286 reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2287 );
2288 let mut result = operation.run_with_resource(node, &observed, || {
2289 let budgets = operation.budgets();
2290 let execution = ReferenceExecutionContext {
2291 store_pre_digest: ctx.store_pre_digest,
2292 transform_options: ctx.transform_options(),
2293 transform_budget: &budgets.transforms,
2294 canonicalized_data_budget: &budgets.canonicalized,
2295 provider: ctx.provider,
2296 };
2297 process_reference_with_options(
2298 reference,
2299 resolver,
2300 signature_node,
2301 ReferenceSet::SignedInfo,
2302 index,
2303 reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2304 &execution,
2305 )
2306 .map_err(SignatureVerificationPipelineError::from)
2307 })?;
2308 bind_reference_evidence(
2309 &mut result,
2310 reference,
2311 view,
2312 reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2313 &observed,
2314 );
2315 let accepted = result.status == DsigStatus::Valid;
2316 operation.set_outcome(
2317 node,
2318 accepted,
2319 if accepted {
2320 OperationDecisionReason::ReferenceDigestVerified
2321 } else {
2322 OperationDecisionReason::ReferenceDigestRejected
2323 },
2324 );
2325 if result.status == DsigStatus::Valid
2326 && let Some(reference) = signed_info.references.get(result.reference_index)
2327 && reference
2328 .transforms
2329 .iter()
2330 .all(transform_preserves_manifest_structure)
2331 && let Some(uri) = reference.uri.as_deref()
2332 && let Ok(Some(target)) = resolver.node_id_for_same_document_reference(uri)
2333 {
2334 let identity = view.node_identity_by_id(target);
2335 operation.authenticate(identity);
2336 debug_assert!(operation.is_authenticated(identity));
2337 }
2338 reference_results.push(result);
2339 if !accepted {
2340 first_failure = Some(index);
2341 break;
2342 }
2343 }
2344 debug_assert!(
2345 plan_nodes.digests[reference_results.len()..]
2346 .iter()
2347 .all(|node| !operation.is_executed(*node))
2348 );
2349 let references = ReferencesResult {
2350 results: reference_results,
2351 first_failure,
2352 };
2353
2354 if let Some(first_failure) = references.first_failure {
2355 debug_assert!(operation.first_failure().is_some());
2356 let status = references.results[first_failure].status;
2357 return Ok(VerifyResult {
2358 key_trust: super::KeyTrustEvidence::NotEvaluated,
2359 status,
2360 signed_info_references: references.results,
2361 manifest_references: Vec::new(),
2362 canonicalized_signed_info: None,
2363 });
2364 }
2365
2366 let canonical_signed_info = operation.run(plan_nodes.canonicalization, || {
2367 let signed_info_subtree: HashSet<_> = signed_info_node
2368 .descendants()
2369 .map(|node: Node<'_, '_>| node.id())
2370 .collect();
2371 let mut canonical_signed_info = Vec::new();
2372 let signed_info_limit = operation
2373 .budgets()
2374 .canonicalized
2375 .remaining()
2376 .min(operation.budgets().transforms.remaining_c14n_output());
2377 canonicalize_bounded_with_xml_base_budget(
2378 doc,
2379 Some(&|node| signed_info_subtree.contains(&node.id())),
2380 &signed_info.c14n_method,
2381 signed_info_limit,
2382 operation.budgets().transforms.xml_base_resolution(),
2383 &mut canonical_signed_info,
2384 )
2385 .map_err(|error| {
2386 if let Some(violation) = map_c14n_resource_policy_violation(
2387 &error,
2388 crate::policy::resource_name::CANONICALIZED_BYTES,
2389 operation.budgets().canonicalized.max_bytes,
2390 ) {
2391 SignatureVerificationPipelineError::Policy(violation)
2392 } else {
2393 SignatureVerificationPipelineError::Canonicalization(error)
2394 }
2395 })?;
2396 operation
2397 .budgets()
2398 .transforms
2399 .charge_c14n_output(canonical_signed_info.len())
2400 .map_err(ReferenceProcessingError::Transform)?;
2401 operation
2402 .budgets()
2403 .canonicalized
2404 .charge(canonical_signed_info.len())?;
2405 Ok::<_, SignatureVerificationPipelineError>(canonical_signed_info)
2406 })?;
2407
2408 let signature_value = decode_signature_value(
2409 signature_children.signature_value_node,
2410 signed_info.signature_method,
2411 &ctx.policy.resources,
2412 )?;
2413 if let Some(full_output_bits) = signed_info.signature_method.hmac_output_bits() {
2414 let expected_bits = signed_info
2415 .hmac_output_length_bits
2416 .unwrap_or(full_output_bits);
2417 if signature_value.len() != expected_bits / 8 {
2418 return Err(SignatureVerificationPipelineError::InvalidStructure {
2419 reason: "SignatureValue length does not match HMACOutputLength",
2420 });
2421 }
2422 }
2423 let resolved_key = operation.run(plan_nodes.key, || {
2424 resolve_verifying_key(ctx, key, key_info.as_ref(), signed_info.signature_method)
2425 })?;
2426 let Some(resolved_key) = resolved_key else {
2427 operation.set_outcome(
2428 plan_nodes.key,
2429 false,
2430 OperationDecisionReason::KeyUnavailable,
2431 );
2432 if let Some(error) = retrieval_materialization.deferred_error {
2433 return Err(error);
2434 }
2435 return Ok(VerifyResult {
2436 key_trust: super::KeyTrustEvidence::NotEvaluated,
2437 status: DsigStatus::Invalid(FailureReason::KeyNotFound),
2438 signed_info_references: references.results,
2439 manifest_references: Vec::new(),
2440 canonicalized_signed_info: if ctx.store_pre_digest {
2441 Some(canonical_signed_info)
2442 } else {
2443 None
2444 },
2445 });
2446 };
2447 operation.set_outcome(plan_nodes.key, true, OperationDecisionReason::KeyResolved);
2448 let verifier = resolved_key.as_ref();
2449 let signature_valid = operation.run(plan_nodes.crypto, || {
2450 verifier.validate_policy(&ctx.policy)?;
2451 if !verifier.validate_signature_value_with_policy(
2452 &ctx.policy,
2453 signed_info.signature_method,
2454 &signature_value,
2455 )? {
2456 return Ok::<_, SignatureVerificationPipelineError>(false);
2457 }
2458 ctx.provider
2459 .require_capability(crate::provider::ProviderCapability::Verify(
2460 signed_info.signature_method,
2461 ))?;
2462 let policy_verifier = PolicyVerifyingKey {
2463 key: verifier,
2464 policy: &ctx.policy,
2465 };
2466 ctx.provider.verify_with_context(
2467 &policy_verifier,
2468 signed_info.signature_method,
2469 &signed_info.signature_context,
2470 &canonical_signed_info,
2471 &signature_value,
2472 )
2473 })?;
2474
2475 if !signature_valid {
2476 operation.set_outcome(
2477 plan_nodes.crypto,
2478 false,
2479 OperationDecisionReason::SignatureRejected,
2480 );
2481 return Ok(VerifyResult {
2482 key_trust: resolved_key.evidence(&ctx.policy)?,
2483 status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
2484 signed_info_references: references.results,
2485 manifest_references: Vec::new(),
2486 canonicalized_signed_info: if ctx.store_pre_digest {
2487 Some(canonical_signed_info)
2488 } else {
2489 None
2490 },
2491 });
2492 }
2493 operation.set_outcome(
2494 plan_nodes.crypto,
2495 true,
2496 OperationDecisionReason::SignatureVerified,
2497 );
2498 let (manifest_references, manifest_digests) =
2502 if ctx.policy.manifest_processing == crate::policy::ManifestProcessing::Process {
2503 process_authenticated_manifest_references(
2504 operation,
2505 view,
2506 signature_node,
2507 resolver,
2508 ctx,
2509 remaining_reference_capacity,
2510 plan_nodes.crypto,
2511 )?
2512 } else {
2513 (Vec::new(), Vec::new())
2514 };
2515 operation.extend();
2516 let evidence = operation.add_node(OperationNodeKind::Evidence, OperationStage::Evidence, None);
2517 operation
2518 .add_dependency(evidence, plan_nodes.crypto)
2519 .map_err(map_verification_plan_error)?;
2520 for digest in manifest_digests {
2521 operation
2522 .add_dependency(evidence, digest)
2523 .map_err(map_verification_plan_error)?;
2524 }
2525 operation.compile().map_err(map_verification_plan_error)?;
2526 operation.run(evidence, || {
2527 Ok::<_, SignatureVerificationPipelineError>(VerifyResult {
2528 key_trust: resolved_key.evidence(&ctx.policy)?,
2529 status: DsigStatus::Valid,
2530 signed_info_references: references.results,
2531 manifest_references,
2532 canonicalized_signed_info: if ctx.store_pre_digest {
2533 Some(canonical_signed_info)
2534 } else {
2535 None
2536 },
2537 })
2538 })
2539}
2540
2541struct PolicyVerifyingKey<'a> {
2542 key: &'a dyn VerifyingKey,
2543 policy: &'a crate::policy::VerificationPolicy,
2544}
2545
2546impl VerifyingKey for PolicyVerifyingKey<'_> {
2547 fn verify_candidate_keys(
2548 &self,
2549 verify: &mut dyn FnMut(&dyn VerifyingKey) -> Result<bool, DsigError>,
2550 ) -> Result<Option<bool>, DsigError> {
2551 self.key.verify_candidate_keys(&mut |key| {
2552 key.validate_policy(self.policy)?;
2553 verify(&PolicyVerifyingKey {
2554 key,
2555 policy: self.policy,
2556 })
2557 })
2558 }
2559 fn verification_spki(&self, algorithm: SignatureAlgorithm) -> Result<Option<&[u8]>, DsigError> {
2560 self.key.verification_spki(algorithm)
2561 }
2562
2563 fn ecdsa_encoding(&self) -> crate::policy::EcdsaSignatureValueEncoding {
2564 self.policy.ecdsa_signature_value_encoding
2565 }
2566 fn verify_with_context(
2567 &self,
2568 algorithm: SignatureAlgorithm,
2569 context: &super::SignatureContext,
2570 data: &[u8],
2571 signature: &[u8],
2572 ) -> Result<bool, DsigError> {
2573 if context.as_bytes().is_empty() {
2574 self.verify(algorithm, data, signature)
2575 } else {
2576 self.key
2577 .verify_with_context(algorithm, context, data, signature)
2578 }
2579 }
2580 fn validate_policy(&self, policy: &crate::policy::VerificationPolicy) -> Result<(), DsigError> {
2581 self.key.validate_policy(policy)
2582 }
2583
2584 fn validate_signature_value(
2585 &self,
2586 algorithm: SignatureAlgorithm,
2587 signature_value: &[u8],
2588 ) -> Result<bool, DsigError> {
2589 self.key
2590 .validate_signature_value_with_policy(self.policy, algorithm, signature_value)
2591 }
2592
2593 fn verify(
2594 &self,
2595 algorithm: SignatureAlgorithm,
2596 signed_data: &[u8],
2597 signature_value: &[u8],
2598 ) -> Result<bool, DsigError> {
2599 self.key
2600 .verify_with_policy(self.policy, algorithm, signed_data, signature_value)
2601 }
2602}
2603
2604#[derive(Debug, Default)]
2605struct RetrievalMaterialization {
2606 deferred_error: Option<SignatureVerificationPipelineError>,
2607}
2608
2609impl RetrievalMaterialization {
2610 fn merge(&mut self, other: Self) {
2611 if self.deferred_error.is_none() {
2612 self.deferred_error = other.deferred_error;
2613 }
2614 }
2615}
2616
2617struct RetrievalMaterializationBudgets<'a> {
2618 certificate_signature_algorithms: Option<&'a crate::policy::CertificateSignatureAlgorithms>,
2619 xpath_parse: &'a mut XPathSignatureParseBudget,
2620 execution: &'a TransformExecutionBudget,
2621 resources: &'a crate::policy::ResourcePolicy,
2622 xml_backend: crate::XmlBackend,
2623}
2624
2625#[derive(Default)]
2626struct KeyInfoMaterializationState {
2627 active: HashSet<(super::uri::TraversalDocumentIdentity, String)>,
2628 candidate_work: usize,
2629}
2630
2631trait KeyInfoReferencePolicy {
2632 fn certificate_signature_algorithms(
2633 &self,
2634 ) -> Option<&crate::policy::CertificateSignatureAlgorithms>;
2635 fn resources(&self) -> &crate::policy::ResourcePolicy;
2636 fn xml(&self) -> &crate::policy::XmlInputPolicy;
2637 fn key_info_reference_uris(&self) -> UriTypeSet;
2638 fn retrieval_method_uris(&self) -> UriTypeSet;
2639 fn key_info_reference_source_enabled(&self) -> bool;
2640 fn allowed_transforms(&self) -> Option<&HashSet<String>>;
2641}
2642
2643impl KeyInfoReferencePolicy for crate::policy::SigningPolicy {
2644 fn certificate_signature_algorithms(
2645 &self,
2646 ) -> Option<&crate::policy::CertificateSignatureAlgorithms> {
2647 None
2648 }
2649 fn resources(&self) -> &crate::policy::ResourcePolicy {
2650 &self.resources
2651 }
2652
2653 fn xml(&self) -> &crate::policy::XmlInputPolicy {
2654 &self.xml
2655 }
2656
2657 fn key_info_reference_uris(&self) -> UriTypeSet {
2658 self.uris.key_info_references
2659 }
2660
2661 fn retrieval_method_uris(&self) -> UriTypeSet {
2662 self.uris.retrieval_methods
2663 }
2664
2665 fn key_info_reference_source_enabled(&self) -> bool {
2666 true
2667 }
2668
2669 fn allowed_transforms(&self) -> Option<&HashSet<String>> {
2670 self.transforms.allowed_algorithms.as_ref()
2671 }
2672}
2673
2674impl KeyInfoReferencePolicy for crate::policy::VerificationPolicy {
2675 fn certificate_signature_algorithms(
2676 &self,
2677 ) -> Option<&crate::policy::CertificateSignatureAlgorithms> {
2678 self.key_trust.certificate_signature_algorithms.as_ref()
2679 }
2680 fn resources(&self) -> &crate::policy::ResourcePolicy {
2681 &self.resources
2682 }
2683
2684 fn xml(&self) -> &crate::policy::XmlInputPolicy {
2685 &self.xml
2686 }
2687
2688 fn key_info_reference_uris(&self) -> UriTypeSet {
2689 self.uris.key_info_references
2690 }
2691
2692 fn retrieval_method_uris(&self) -> UriTypeSet {
2693 self.uris.retrieval_methods
2694 }
2695
2696 fn key_info_reference_source_enabled(&self) -> bool {
2697 self.key_sources.key_info_reference
2698 }
2699
2700 fn allowed_transforms(&self) -> Option<&HashSet<String>> {
2701 self.transforms.allowed_algorithms.as_ref()
2702 }
2703}
2704
2705struct KeyInfoReferenceMaterializationContext<'a, 'budget, P> {
2706 policy: &'a P,
2707 provider: &'a dyn crate::provider::CryptoProvider,
2708 budgets: &'a mut RetrievalMaterializationBudgets<'budget>,
2709}
2710
2711fn materialize_key_info_references_with_budgets<P: KeyInfoReferencePolicy>(
2712 key_info: &mut KeyInfo,
2713 resolver: &UriReferenceResolver<'_>,
2714 policy: &P,
2715 provider: &dyn crate::provider::CryptoProvider,
2716 budgets: &mut RetrievalMaterializationBudgets<'_>,
2717 materialization: &mut KeyInfoMaterializationState,
2718) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
2719 fn visit<P: KeyInfoReferencePolicy>(
2720 key_info: &mut KeyInfo,
2721 resolver: &UriReferenceResolver<'_>,
2722 context: &mut KeyInfoReferenceMaterializationContext<'_, '_, P>,
2723 materialization: &mut KeyInfoMaterializationState,
2724 depth: usize,
2725 ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
2726 let mut materialized = Vec::new();
2727 let mut outcome = RetrievalMaterialization::default();
2728 for source in std::mem::take(&mut key_info.sources) {
2729 let source_work = match &source {
2730 super::parse::KeyInfoSource::X509Data(info) => info.certificates.len().max(1),
2731 _ => 1,
2732 };
2733 materialization.candidate_work =
2734 materialization.candidate_work.saturating_add(source_work);
2735 context
2736 .policy
2737 .resources()
2738 .validate_key_candidates(materialization.candidate_work)?;
2739
2740 let super::parse::KeyInfoSource::KeyInfoReference { uri } = source else {
2741 materialized.push(source);
2742 continue;
2743 };
2744 if !context.policy.key_info_reference_source_enabled() {
2745 return Err(crate::policy::PolicyViolation::KeyTrust {
2746 reason: "KeyInfoReference key sources are disabled",
2747 }
2748 .into());
2749 }
2750 if !context.policy.key_info_reference_uris().allows(&uri) {
2751 return Err(crate::policy::PolicyViolation::Uri {
2752 operation: "KeyInfoReference",
2753 reason: "URI class is disabled",
2754 }
2755 .into());
2756 }
2757 let next_depth = depth.saturating_add(1);
2758 context
2759 .policy
2760 .resources()
2761 .validate_key_info_reference_depth(next_depth)?;
2762 let cycle_key = (resolver.traversal_document_identity(), uri.clone());
2763 if !materialization.active.insert(cycle_key.clone()) {
2764 return Err(SignatureVerificationPipelineError::InvalidStructure {
2765 reason: "KeyInfoReference cycle detected",
2766 });
2767 }
2768
2769 let is_same_document = uri.is_empty() || uri.starts_with('#');
2770 let (mut referenced, mut nested_outcome) = if is_same_document {
2771 let node = resolver
2772 .node_for_same_document_reference(&uri)
2773 .map_err(|error| {
2774 SignatureVerificationPipelineError::from(
2775 ReferenceProcessingError::UriDereference(error),
2776 )
2777 })?
2778 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2779 reason: "KeyInfoReference target is missing or ambiguous",
2780 })?;
2781 if !node.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
2782 return Err(SignatureVerificationPipelineError::InvalidStructure {
2783 reason: "KeyInfoReference target must be KeyInfo",
2784 });
2785 }
2786 (
2787 parse_key_info_with_policy_budgets(
2788 node,
2789 context.provider,
2790 context.budgets.execution.xml_base_resolution(),
2791 context.policy.resources(),
2792 context.policy.certificate_signature_algorithms(),
2793 )
2794 .map_err(map_key_info_parse_error)?,
2795 RetrievalMaterialization::default(),
2796 )
2797 } else {
2798 let (resource_uri, fragment) = uri
2802 .split_once('#')
2803 .map_or((uri.as_str(), None), |(resource, fragment)| {
2804 (resource, (!fragment.is_empty()).then_some(fragment))
2805 });
2806 let bytes = resolver
2807 .external_resource(resource_uri)
2808 .map_err(|error| {
2809 SignatureVerificationPipelineError::from(
2810 ReferenceProcessingError::UriDereference(error),
2811 )
2812 })?
2813 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2814 reason: "KeyInfoReference external resource is unavailable",
2815 })?;
2816 context
2819 .budgets
2820 .execution
2821 .xml_parse_work()
2822 .charge_policy(bytes.len())?;
2823 let xml = crate::encoding::decode_xml_octets(
2824 bytes,
2825 context.policy.resources().max_xml_document_bytes,
2826 )
2827 .map_err(map_key_info_xml_decode_error)?;
2828 let settings = DocumentParseSettings::from_policy(
2829 context.policy.xml(),
2830 context.policy.resources(),
2831 )
2832 .with_backend(context.budgets.xml_backend);
2833 let document = XmlDocument::parse_with_settings_and_budget(
2834 xml.into_owned(),
2835 settings,
2836 context.budgets.execution.xml_parse_work(),
2837 )
2838 .map_err(|error| map_document_parse_error(error, settings))?;
2839 document.with_view(|view| {
2840 let external_resolver = resolver.for_external_document_view(view, resource_uri);
2841 let target = match fragment {
2842 Some(fragment) => external_resolver
2843 .node_for_same_document_reference(&format!("#{fragment}"))
2844 .map_err(|error| {
2845 SignatureVerificationPipelineError::from(
2846 ReferenceProcessingError::UriDereference(error),
2847 )
2848 })?
2849 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2850 reason: "KeyInfoReference external target is missing or ambiguous",
2851 })?,
2852 _ => view.document().root_element(),
2853 };
2854 if !target.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
2855 return Err(SignatureVerificationPipelineError::InvalidStructure {
2856 reason: "KeyInfoReference external target must be KeyInfo",
2857 });
2858 }
2859 let mut referenced = parse_key_info_with_policy_budgets_and_document_base(
2860 target,
2861 context.provider,
2862 context.budgets.execution.xml_base_resolution(),
2863 context.policy.resources(),
2864 Some(resource_uri),
2865 context.policy.certificate_signature_algorithms(),
2866 )
2867 .map_err(map_key_info_parse_error)?;
2868 let mut nested_outcome = visit(
2869 &mut referenced,
2870 &external_resolver,
2871 context,
2872 materialization,
2873 next_depth,
2874 )?;
2875 nested_outcome.merge(materialize_retrieval_methods_with_budgets(
2876 &mut referenced,
2877 &external_resolver,
2878 context.policy.retrieval_method_uris(),
2879 context.policy.allowed_transforms(),
2880 context.provider,
2881 context.budgets,
2882 &mut materialization.candidate_work,
2883 )?);
2884 referenced.sources.retain(|source| {
2887 !matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. })
2888 });
2889 Ok((referenced, nested_outcome))
2890 })?
2891 };
2892 if is_same_document {
2893 nested_outcome.merge(visit(
2894 &mut referenced,
2895 resolver,
2896 context,
2897 materialization,
2898 next_depth,
2899 )?);
2900 }
2901 outcome.merge(nested_outcome);
2902 materialization.active.remove(&cycle_key);
2903 materialized.extend(referenced.sources);
2904 }
2905 key_info.sources = materialized;
2906 context
2907 .policy
2908 .resources()
2909 .validate_key_candidates(key_info.embedded_candidate_count())?;
2910 Ok(outcome)
2911 }
2912
2913 let mut context = KeyInfoReferenceMaterializationContext {
2914 policy,
2915 provider,
2916 budgets,
2917 };
2918 visit(key_info, resolver, &mut context, materialization, 0)
2919}
2920
2921fn map_key_info_xml_decode_error(
2922 error: crate::encoding::XmlEncodingError,
2923) -> SignatureVerificationPipelineError {
2924 match error {
2925 crate::encoding::XmlEncodingError::DecodedLimit { maximum, actual } => {
2926 SignatureVerificationPipelineError::Policy(
2927 crate::policy::PolicyViolation::ResourceLimit {
2928 resource: crate::policy::resource_name::XML_DOCUMENT,
2929 maximum,
2930 actual,
2931 },
2932 )
2933 }
2934 _ => SignatureVerificationPipelineError::InvalidStructure {
2935 reason: "KeyInfoReference external resource has an invalid XML encoding",
2936 },
2937 }
2938}
2939
2940fn materialize_key_info_references_for_policy<P: KeyInfoReferencePolicy>(
2941 key_info: &mut KeyInfo,
2942 resolver: UriReferenceResolver<'_>,
2943 policy: &P,
2944 provider: &dyn crate::provider::CryptoProvider,
2945 xml_backend: crate::XmlBackend,
2946) -> Result<(), DsigError> {
2947 let resolver = resolver.with_external_resource_limits(
2948 policy.resources().max_external_resource_bytes,
2949 policy.resources().max_external_resource_total_bytes,
2950 );
2951 let mut xpath_parse_budget = XPathSignatureParseBudget::from_resources(policy.resources());
2952 let execution_budget = TransformExecutionBudget::from_resources(policy.resources());
2953 let mut budgets = RetrievalMaterializationBudgets {
2954 xpath_parse: &mut xpath_parse_budget,
2955 execution: &execution_budget,
2956 resources: policy.resources(),
2957 certificate_signature_algorithms: policy.certificate_signature_algorithms(),
2958 xml_backend,
2959 };
2960 let mut materialization = KeyInfoMaterializationState::default();
2961 materialize_key_info_references_with_budgets(
2964 key_info,
2965 &resolver,
2966 policy,
2967 provider,
2968 &mut budgets,
2969 &mut materialization,
2970 )?;
2971 Ok(())
2972}
2973
2974pub fn materialize_signing_key_info_references(
2991 key_info: &mut KeyInfo,
2992 resolver: UriReferenceResolver<'_>,
2993 policy: &crate::policy::SigningPolicy,
2994 provider: &dyn crate::provider::CryptoProvider,
2995 xml_backend: crate::XmlBackend,
2996) -> Result<(), DsigError> {
2997 policy.validate()?;
2998 materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
2999}
3000
3001pub fn materialize_verification_key_info_references(
3016 key_info: &mut KeyInfo,
3017 resolver: UriReferenceResolver<'_>,
3018 policy: &crate::policy::VerificationPolicy,
3019 provider: &dyn crate::provider::CryptoProvider,
3020 xml_backend: crate::XmlBackend,
3021) -> Result<(), DsigError> {
3022 policy.validate()?;
3023 materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
3024}
3025
3026fn materialize_retrieval_methods_with_budgets(
3027 key_info: &mut KeyInfo,
3028 resolver: &UriReferenceResolver<'_>,
3029 allowed_uri_types: UriTypeSet,
3030 allowed_transforms: Option<&HashSet<String>>,
3031 provider: &dyn crate::provider::CryptoProvider,
3032 budgets: &mut RetrievalMaterializationBudgets<'_>,
3033 candidate_work: &mut usize,
3034) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
3035 let retrieval_count = key_info
3036 .sources
3037 .iter()
3038 .filter(|source| matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. }))
3039 .count();
3040 if retrieval_count > MAX_RETRIEVAL_METHOD_COUNT {
3041 return Err(SignatureVerificationPipelineError::InvalidStructure {
3042 reason: "KeyInfo contains too many RetrievalMethod elements",
3043 });
3044 }
3045
3046 let mut total_binary_len = existing_x509_binary_len(key_info)?;
3047 let mut seen = HashSet::new();
3048 let mut materialized = Vec::with_capacity(key_info.sources.len());
3049 let mut outcome = RetrievalMaterialization::default();
3050 for source in std::mem::take(&mut key_info.sources) {
3051 let super::parse::KeyInfoSource::RetrievalMethod {
3052 uri: resolved_uri,
3053 resource_type,
3054 transforms,
3055 } = source
3056 else {
3057 materialized.push(source);
3058 continue;
3059 };
3060
3061 let identity = (
3062 resolved_uri.clone(),
3063 resource_type.clone(),
3064 transforms.clone(),
3065 );
3066 if !seen.insert(identity) {
3067 continue;
3068 }
3069
3070 if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#rawX509Certificate")
3071 {
3072 if transforms != RetrievalMethodTransforms::None
3073 || classify_uri(&resolved_uri) != UriClass::External
3074 {
3075 return Err(SignatureVerificationPipelineError::InvalidStructure {
3076 reason: "raw X509 RetrievalMethod requires an untransformed external URI",
3077 });
3078 }
3079 if !allowed_uri_types.allows(&resolved_uri) {
3080 return Err(crate::policy::PolicyViolation::Uri {
3081 operation: "verification",
3082 reason: "retrieval method URI class is not permitted",
3083 }
3084 .into());
3085 }
3086 let certificate = resolver.external_resource(&resolved_uri).map_err(|error| {
3087 SignatureVerificationPipelineError::from(ReferenceProcessingError::Transform(error))
3088 })?;
3089 let Some(certificate) = certificate else {
3090 outcome.deferred_error.get_or_insert_with(|| {
3091 SignatureVerificationPipelineError::Reference(
3092 ReferenceProcessingError::Transform(super::TransformError::UnsupportedUri(
3093 resolved_uri.clone(),
3094 )),
3095 )
3096 });
3097 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
3098 uri: resolved_uri,
3099 resource_type,
3100 transforms,
3101 });
3102 continue;
3103 };
3104 *candidate_work = candidate_work.saturating_add(1);
3105 budgets.resources.validate_key_candidates(*candidate_work)?;
3106 if certificate.len() > MAX_X509_DECODED_BINARY_LEN {
3107 return Err(SignatureVerificationPipelineError::InvalidStructure {
3108 reason: "raw X509 RetrievalMethod certificate exceeds maximum allowed length",
3109 });
3110 }
3111 add_retrieval_binary_usage(&mut total_binary_len, certificate.len())?;
3112 let parsed = match parse_x509_certificate(certificate) {
3113 Ok(parsed) => parsed,
3114 Err(error) => {
3115 let error = map_key_info_parse_error(error);
3116 if matches!(error, SignatureVerificationPipelineError::Policy(_)) {
3117 return Err(error);
3118 }
3119 outcome.deferred_error.get_or_insert(error);
3120 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
3121 uri: resolved_uri,
3122 resource_type,
3123 transforms,
3124 });
3125 continue;
3126 }
3127 };
3128 materialized.push(super::parse::KeyInfoSource::X509Data(
3129 super::parse::X509DataInfo {
3130 certificates: vec![certificate.to_vec()],
3131 parsed_certificates: vec![parsed],
3132 certificate_chain: vec![0],
3133 ..super::parse::X509DataInfo::default()
3134 },
3135 ));
3136 } else if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#X509Data") {
3137 if !allowed_uri_types.allows(&resolved_uri) {
3138 return Err(crate::policy::PolicyViolation::Uri {
3139 operation: "verification",
3140 reason: "retrieval method URI class is not permitted",
3141 }
3142 .into());
3143 }
3144 let target = resolver
3145 .node_for_same_document_reference(&resolved_uri)
3146 .map_err(ReferenceProcessingError::Transform)?;
3147 let Some(target) = target else {
3148 outcome.deferred_error.get_or_insert(
3149 SignatureVerificationPipelineError::InvalidStructure {
3150 reason: "X509Data RetrievalMethod target is missing or ambiguous",
3151 },
3152 );
3153 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
3154 uri: resolved_uri,
3155 resource_type,
3156 transforms,
3157 });
3158 continue;
3159 };
3160 let node = match transforms {
3161 RetrievalMethodTransforms::None
3162 if target.has_tag_name((XMLDSIG_NS, "X509Data")) =>
3163 {
3164 target
3165 }
3166 RetrievalMethodTransforms::None => {
3167 return Err(SignatureVerificationPipelineError::InvalidStructure {
3168 reason: "untransformed X509Data RetrievalMethod must target X509Data directly",
3169 });
3170 }
3171 RetrievalMethodTransforms::X509DataNodeSetFilter {
3172 expression,
3173 namespaces,
3174 } => {
3175 enforce_transform_allowed(allowed_transforms, XPATH_TRANSFORM_URI)?;
3176 budgets
3177 .xpath_parse
3178 .validate_expression(&expression)
3179 .map_err(ReferenceProcessingError::Transform)?;
3180 budgets
3181 .xpath_parse
3182 .validate_namespaces(&namespaces)
3183 .map_err(ReferenceProcessingError::Transform)?;
3184 select_retrieved_x509_data_root(target, budgets.execution)?
3185 }
3186 RetrievalMethodTransforms::Unsupported => {
3187 return Err(SignatureVerificationPipelineError::InvalidStructure {
3188 reason: "X509Data RetrievalMethod contains unsupported transforms",
3189 });
3190 }
3191 };
3192 let data = parse_x509_data_dispatch_with_budget_and_provider(
3193 node,
3194 &mut total_binary_len,
3195 candidate_work,
3196 provider,
3197 budgets.resources,
3198 budgets.certificate_signature_algorithms,
3199 )
3200 .map_err(map_key_info_parse_error)?;
3201 materialized.push(super::parse::KeyInfoSource::X509Data(data));
3202 } else {
3203 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
3204 uri: resolved_uri,
3205 resource_type,
3206 transforms,
3207 });
3208 }
3209 }
3210 key_info.sources = materialized;
3211 Ok(outcome)
3212}
3213
3214fn select_retrieved_x509_data_root<'a, 'input>(
3215 target: Node<'a, 'input>,
3216 execution_budget: &TransformExecutionBudget,
3217) -> Result<Node<'a, 'input>, SignatureVerificationPipelineError> {
3218 let context_nodes = NodeSet::ensure_subtree_materialization_fits_with_budget(
3223 target,
3224 false,
3225 execution_budget.node_set_materialization(),
3226 )
3227 .map_err(ReferenceProcessingError::Transform)?;
3228 execution_budget
3229 .validate_xpath_context_evaluations(context_nodes)
3230 .map_err(ReferenceProcessingError::Transform)?;
3231 execution_budget
3232 .charge_xpath_work(context_nodes)
3233 .map_err(ReferenceProcessingError::Transform)?;
3234 execution_budget
3235 .charge_node_filter_work(context_nodes)
3236 .map_err(ReferenceProcessingError::Transform)?;
3237 let mut root = None;
3238 for candidate in target.descendants() {
3239 if !candidate.is_element()
3240 || candidate.tag_name().namespace() != Some(XMLDSIG_NS)
3241 || candidate.tag_name().name() != "X509Data"
3242 {
3243 continue;
3244 }
3245 if root.replace(candidate).is_some() {
3246 return Err(SignatureVerificationPipelineError::InvalidStructure {
3247 reason: "X509Data RetrievalMethod selected multiple X509Data elements",
3248 });
3249 }
3250 }
3251 root.ok_or(SignatureVerificationPipelineError::InvalidStructure {
3252 reason: "X509Data RetrievalMethod selected no X509Data element",
3253 })
3254}
3255
3256#[cfg(test)]
3257fn materialize_retrieval_methods(
3258 key_info: &mut KeyInfo,
3259 resolver: &UriReferenceResolver<'_>,
3260 allowed_uri_types: UriTypeSet,
3261 allowed_transforms: Option<&HashSet<String>>,
3262 provider: &dyn crate::provider::CryptoProvider,
3263) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
3264 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
3265 let execution_budget = TransformExecutionBudget::default();
3266 let resources = crate::policy::ResourcePolicy::default();
3267 let mut budgets = RetrievalMaterializationBudgets {
3268 xpath_parse: &mut xpath_parse_budget,
3269 execution: &execution_budget,
3270 resources: &resources,
3271 certificate_signature_algorithms: None,
3272 xml_backend: crate::XmlBackend::default(),
3273 };
3274 let mut candidate_work = key_info.embedded_candidate_count();
3275 materialize_retrieval_methods_with_budgets(
3276 key_info,
3277 resolver,
3278 allowed_uri_types,
3279 allowed_transforms,
3280 provider,
3281 &mut budgets,
3282 &mut candidate_work,
3283 )
3284}
3285
3286fn existing_x509_binary_len(
3287 key_info: &KeyInfo,
3288) -> Result<usize, SignatureVerificationPipelineError> {
3289 let mut total = 0usize;
3290 for source in &key_info.sources {
3291 if let super::parse::KeyInfoSource::X509Data(info) = source {
3292 for len in info
3293 .certificates
3294 .iter()
3295 .chain(&info.skis)
3296 .chain(&info.crls)
3297 .map(Vec::len)
3298 .chain(info.digests.iter().map(|(_, digest)| digest.len()))
3299 {
3300 add_retrieval_binary_usage(&mut total, len)?;
3301 }
3302 }
3303 }
3304 Ok(total)
3305}
3306
3307fn add_retrieval_binary_usage(
3308 total: &mut usize,
3309 delta: usize,
3310) -> Result<(), SignatureVerificationPipelineError> {
3311 *total =
3312 total
3313 .checked_add(delta)
3314 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
3315 reason: "RetrievalMethod X509Data binary length overflow",
3316 })?;
3317 if *total > MAX_X509_DATA_TOTAL_BINARY_LEN {
3318 return Err(SignatureVerificationPipelineError::InvalidStructure {
3319 reason: "RetrievalMethod X509Data exceeds maximum aggregate binary length",
3320 });
3321 }
3322 Ok(())
3323}
3324
3325fn manifest_reference_failure_reason(
3326 error: ReferenceProcessingError,
3327 ref_index: usize,
3328) -> FailureReason {
3329 match error.into_policy_violation() {
3330 Ok(_) => FailureReason::ReferencePolicyViolation { ref_index },
3331 Err(_) => FailureReason::ReferenceProcessingFailure { ref_index },
3332 }
3333}
3334
3335struct CompiledManifestReference {
3336 index: usize,
3337 reference: Reference,
3338 reference_node_id: NodeId,
3339}
3340
3341struct CompiledManifestInvalid {
3342 result: ReferenceResult,
3343}
3344
3345enum ScheduledManifestReference {
3346 Parsed {
3347 node: OperationNodeId,
3348 reference: CompiledManifestReference,
3349 },
3350 Invalid {
3351 node: OperationNodeId,
3352 invalid: CompiledManifestInvalid,
3353 },
3354}
3355
3356impl ScheduledManifestReference {
3357 fn index(&self) -> usize {
3358 match self {
3359 Self::Parsed { reference, .. } => reference.index,
3360 Self::Invalid { invalid, .. } => invalid.result.reference_index,
3361 }
3362 }
3363}
3364
3365fn process_authenticated_manifest_references(
3366 operation: &mut OperationExecutionContext<
3367 &crate::policy::VerificationPolicy,
3368 &VerificationOperationBudgets,
3369 >,
3370 view: DocumentView<'_>,
3371 signature_node: Node<'_, '_>,
3372 resolver: &UriReferenceResolver<'_>,
3373 ctx: &VerifyContext<'_>,
3374 mut remaining_reference_capacity: usize,
3375 crypto: OperationNodeId,
3376) -> Result<(Vec<ReferenceResult>, Vec<OperationNodeId>), SignatureVerificationPipelineError> {
3377 let mut processed_manifests = HashSet::new();
3378 let mut next_reference_index = 0usize;
3379 let mut frontier_index = 0usize;
3380 let mut completed_digests = Vec::new();
3381 let mut results = Vec::new();
3382 loop {
3383 operation.extend();
3384 let discovery = operation.add_node(
3385 OperationNodeKind::Manifest {
3386 index: frontier_index,
3387 },
3388 OperationStage::AuthenticatedDependency,
3389 None,
3390 );
3391 operation
3392 .add_dependency(discovery, crypto)
3393 .map_err(map_verification_plan_error)?;
3394 for digest in &completed_digests {
3395 operation
3396 .add_dependency(discovery, *digest)
3397 .map_err(map_verification_plan_error)?;
3398 }
3399 operation.compile().map_err(map_verification_plan_error)?;
3400 let parsed = operation.run(discovery, || {
3401 let mut xpath_parse = operation.budgets().xpath_parse.borrow_mut();
3402 let mut state = ManifestDiscoveryState {
3403 processed: &mut processed_manifests,
3404 remaining_capacity: &mut remaining_reference_capacity,
3405 next_reference_index: &mut next_reference_index,
3406 xpath_parse: &mut xpath_parse,
3407 };
3408 parse_manifest_references(
3409 signature_node,
3410 operation,
3411 view,
3412 &mut state,
3413 ctx.allowed_transform_uris(),
3414 )
3415 })?;
3416 let discovered = parsed.references.len() + parsed.invalid_results.len();
3417 if discovered == 0 {
3418 break;
3419 }
3420
3421 operation.extend();
3422 let mut scheduled = Vec::with_capacity(discovered);
3423 for item in parsed.references {
3424 let resource = operation.budgets().resource_identity_for_reference(
3425 &item.reference,
3426 item.index,
3427 resolver,
3428 view,
3429 resolver.node_for_node_id(item.reference_node_id),
3430 );
3431 let node = operation.add_node(
3432 OperationNodeKind::Digest { index: item.index },
3433 OperationStage::AuthenticatedDependency,
3434 Some(resource.clone()),
3435 );
3436 operation
3437 .add_dependency(node, discovery)
3438 .map_err(map_verification_plan_error)?;
3439 scheduled.push(ScheduledManifestReference::Parsed {
3440 node,
3441 reference: item,
3442 });
3443 }
3444 for item in parsed.invalid_results {
3445 let node = operation.add_node(
3446 OperationNodeKind::Digest {
3447 index: item.result.reference_index,
3448 },
3449 OperationStage::AuthenticatedDependency,
3450 None,
3451 );
3452 operation
3453 .add_dependency(node, discovery)
3454 .map_err(map_verification_plan_error)?;
3455 scheduled.push(ScheduledManifestReference::Invalid {
3456 node,
3457 invalid: item,
3458 });
3459 }
3460 scheduled.sort_by_key(ScheduledManifestReference::index);
3461 operation.compile().map_err(map_verification_plan_error)?;
3462
3463 for scheduled_reference in scheduled {
3464 let (node, result) = match scheduled_reference {
3465 ScheduledManifestReference::Invalid { node, invalid } => {
3466 let result = operation.run(node, || {
3467 Ok::<_, SignatureVerificationPipelineError>(invalid.result)
3468 })?;
3469 (node, result)
3470 }
3471 ScheduledManifestReference::Parsed {
3472 node,
3473 reference: compiled,
3474 } => {
3475 let observed = operation.budgets().resource_identity_for_reference(
3476 &compiled.reference,
3477 compiled.index,
3478 resolver,
3479 view,
3480 resolver.node_for_node_id(compiled.reference_node_id),
3481 );
3482 let result = operation.run_with_resource(node, &observed, || {
3483 let reference = &compiled.reference;
3484 let budgets = operation.budgets();
3485 let execution = ReferenceExecutionContext {
3486 store_pre_digest: ctx.store_pre_digest,
3487 transform_options: ctx.transform_options(),
3488 transform_budget: &budgets.transforms,
3489 canonicalized_data_budget: &budgets.canonicalized,
3490 provider: ctx.provider,
3491 };
3492 let mut result = if execution.transform_budget.remaining_c14n_output() == 0
3493 || reference.transforms.len()
3494 > ctx.policy.resources.max_transforms_per_reference
3495 || ctx
3496 .policy
3497 .digest_algorithms
3498 .as_ref()
3499 .is_some_and(|allowed| !allowed.contains(&reference.digest_method))
3500 {
3501 manifest_reference_invalid_result(
3502 reference,
3503 compiled.index,
3504 FailureReason::ReferencePolicyViolation {
3505 ref_index: compiled.index,
3506 },
3507 )
3508 } else {
3509 match enforce_reference_policies(
3510 std::slice::from_ref(reference),
3511 ctx.policy.uris.references,
3512 ctx.allowed_transform_uris(),
3513 ) {
3514 Ok(()) => process_reference_with_options(
3515 reference,
3516 resolver,
3517 signature_node,
3518 ReferenceSet::Manifest,
3519 compiled.index,
3520 resolver.node_for_node_id(compiled.reference_node_id),
3521 &execution,
3522 )
3523 .unwrap_or_else(|error| {
3524 manifest_reference_invalid_result(
3525 reference,
3526 compiled.index,
3527 manifest_reference_failure_reason(error, compiled.index),
3528 )
3529 }),
3530 Err(SignatureVerificationPipelineError::Policy(_)) => {
3531 manifest_reference_invalid_result(
3532 reference,
3533 compiled.index,
3534 FailureReason::ReferencePolicyViolation {
3535 ref_index: compiled.index,
3536 },
3537 )
3538 }
3539 Err(_) => manifest_reference_invalid_result(
3540 reference,
3541 compiled.index,
3542 FailureReason::ReferenceProcessingFailure {
3543 ref_index: compiled.index,
3544 },
3545 ),
3546 }
3547 };
3548 bind_reference_evidence(
3549 &mut result,
3550 reference,
3551 view,
3552 resolver.node_for_node_id(compiled.reference_node_id),
3553 &observed,
3554 );
3555 if result.status == DsigStatus::Valid
3556 && reference
3557 .transforms
3558 .iter()
3559 .all(transform_preserves_manifest_structure)
3560 && let Some(uri) = reference.uri.as_deref()
3561 && let Ok(Some(target)) =
3562 resolver.node_id_for_same_document_reference(uri)
3563 {
3564 operation.authenticate(view.node_identity_by_id(target));
3565 }
3566 Ok::<_, SignatureVerificationPipelineError>(result)
3567 })?;
3568 (node, result)
3569 }
3570 };
3571 let accepted = result.status == DsigStatus::Valid;
3572 operation.set_outcome(
3573 node,
3574 accepted,
3575 if accepted {
3576 OperationDecisionReason::ReferenceDigestVerified
3577 } else {
3578 OperationDecisionReason::ReferenceDigestRejected
3579 },
3580 );
3581 completed_digests.push(node);
3582 results.push(result);
3583 }
3584 frontier_index += 1;
3585 }
3586 results.sort_by_key(|result| result.reference_index);
3587 Ok((results, completed_digests))
3588}
3589
3590fn manifest_reference_invalid_result(
3591 reference: &Reference,
3592 index: usize,
3593 reason: FailureReason,
3594) -> ReferenceResult {
3595 ReferenceResult {
3596 reference_identity: None,
3597 target_identity: None,
3598 external_resource_fingerprint: None,
3599 coverage: ReferenceCoverage::TransformedData,
3600 reference_set: ReferenceSet::Manifest,
3601 reference_index: index,
3602 uri: reference
3603 .uri
3604 .clone()
3605 .unwrap_or_else(|| "<omitted>".to_owned()),
3606 digest_algorithm: reference.digest_method,
3607 status: DsigStatus::Invalid(reason),
3608 pre_digest_data: None,
3609 }
3610}
3611
3612fn parse_manifest_references(
3613 signature_node: Node<'_, '_>,
3614 operation: &OperationExecutionContext<
3615 &crate::policy::VerificationPolicy,
3616 &VerificationOperationBudgets,
3617 >,
3618 view: DocumentView<'_>,
3619 state: &mut ManifestDiscoveryState<'_>,
3620 allowed_transforms: Option<&HashSet<String>>,
3621) -> Result<ParsedManifestReferences, SignatureVerificationPipelineError> {
3622 let mut references = Vec::new();
3623 let mut invalid = Vec::new();
3624 for object_node in signature_node.children().filter(|node| {
3625 node.is_element()
3626 && node.tag_name().namespace() == Some(XMLDSIG_NS)
3627 && node.tag_name().name() == "Object"
3628 }) {
3629 let object_is_signed = operation.is_authenticated(view.node_identity(object_node));
3630 for manifest_node in object_node.children().filter(|node| {
3631 node.is_element()
3632 && node.tag_name().namespace() == Some(XMLDSIG_NS)
3633 && node.tag_name().name() == "Manifest"
3634 }) {
3635 let manifest_is_signed = operation.is_authenticated(view.node_identity(manifest_node));
3636 if !object_is_signed && !manifest_is_signed {
3639 continue;
3640 }
3641 if !state.processed.insert(manifest_node.id()) {
3642 continue;
3643 }
3644 let mut manifest_children = Vec::new();
3645 for child in manifest_node.children() {
3646 if child.is_text()
3647 && child.text().is_some_and(|text| {
3648 text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
3649 })
3650 {
3651 return Err(SignatureVerificationPipelineError::InvalidStructure {
3652 reason: "Manifest contains non-whitespace mixed content",
3653 });
3654 }
3655 if child.is_element() {
3656 manifest_children.push(child);
3657 }
3658 }
3659 if manifest_children.is_empty() {
3660 return Err(SignatureVerificationPipelineError::InvalidStructure {
3661 reason: "Manifest must contain at least one ds:Reference element child",
3662 });
3663 }
3664 for child in manifest_children {
3665 if child.tag_name().namespace() != Some(XMLDSIG_NS)
3666 || child.tag_name().name() != "Reference"
3667 {
3668 return Err(SignatureVerificationPipelineError::InvalidStructure {
3669 reason: "Manifest must contain only ds:Reference element children",
3670 });
3671 }
3672 if *state.remaining_capacity == 0 {
3673 return Err(SignatureVerificationPipelineError::InvalidStructure {
3674 reason: "signed Manifests exceed the per-signature Reference limit",
3675 });
3676 }
3677 *state.remaining_capacity -= 1;
3678 let reference_index = *state.next_reference_index;
3679 *state.next_reference_index += 1;
3680 match parse_reference_with_xpath_budget(child, state.xpath_parse) {
3681 Ok(reference) => references.push(CompiledManifestReference {
3682 index: reference_index,
3683 reference,
3684 reference_node_id: child.id(),
3685 }),
3686 Err(ParseError::Transform(super::TransformError::UnsupportedTransform(
3687 uri,
3688 ))) => {
3689 let digest_algorithm =
3690 reference_digest_method(child).map_err(map_manifest_parse_error)?;
3691 let reason =
3692 if allowed_transforms.is_some_and(|allowed| !allowed.contains(&uri)) {
3693 FailureReason::ReferencePolicyViolation {
3694 ref_index: reference_index,
3695 }
3696 } else {
3697 FailureReason::ReferenceProcessingFailure {
3698 ref_index: reference_index,
3699 }
3700 };
3701 invalid.push(CompiledManifestInvalid {
3702 result: ReferenceResult {
3703 reference_identity: Some(view.node_identity(child)),
3704 target_identity: None,
3705 external_resource_fingerprint: None,
3706 coverage: ReferenceCoverage::TransformedData,
3707 reference_set: ReferenceSet::Manifest,
3708 reference_index,
3709 uri: child.attribute("URI").unwrap_or("<omitted>").to_owned(),
3710 digest_algorithm,
3711 status: DsigStatus::Invalid(reason),
3712 pre_digest_data: None,
3713 },
3714 });
3715 }
3716 Err(error) => return Err(map_manifest_parse_error(error)),
3717 }
3718 }
3719 }
3720 }
3721 Ok(ParsedManifestReferences {
3722 references,
3723 invalid_results: invalid,
3724 })
3725}
3726
3727struct ParsedManifestReferences {
3728 references: Vec<CompiledManifestReference>,
3729 invalid_results: Vec<CompiledManifestInvalid>,
3730}
3731
3732struct ManifestDiscoveryState<'a> {
3733 processed: &'a mut HashSet<NodeId>,
3734 remaining_capacity: &'a mut usize,
3735 next_reference_index: &'a mut usize,
3736 xpath_parse: &'a mut XPathSignatureParseBudget,
3737}
3738
3739fn transform_preserves_manifest_structure(transform: &Transform) -> bool {
3740 match transform {
3741 Transform::C14n(_) => true,
3742 Transform::Enveloped
3747 | Transform::XpathExcludeAllSignatures
3748 | Transform::XPath(_)
3749 | Transform::XPathFilter2(_)
3750 | Transform::Base64Decode => false,
3751 }
3752}
3753
3754#[expect(
3755 clippy::large_enum_variant,
3756 reason = "single bounded authorization result avoids heap allocation"
3757)]
3758enum ResolvedVerifyingKey<'a> {
3759 Borrowed(&'a dyn VerifyingKey),
3760 Owned(super::ResolvedVerificationKey<'a>),
3761}
3762
3763impl ResolvedVerifyingKey<'_> {
3764 fn evidence(
3765 &self,
3766 policy: &crate::policy::VerificationPolicy,
3767 ) -> Result<super::KeyTrustEvidence, DsigError> {
3768 match self {
3769 Self::Borrowed(_)
3770 if policy.key_trust.mode
3771 == crate::policy::VerificationTrustMode::CryptographicOnly =>
3772 {
3773 Ok(super::KeyTrustEvidence::NotEstablished)
3774 }
3775 Self::Borrowed(_) => Ok(super::KeyTrustEvidence::CallerTrusted),
3776 Self::Owned(key) => key.authorize(policy),
3777 }
3778 }
3779 fn as_ref(&self) -> &dyn VerifyingKey {
3780 match self {
3781 Self::Borrowed(key) => *key,
3782 Self::Owned(key) => key.as_ref(),
3783 }
3784 }
3785}
3786
3787fn resolve_verifying_key<'k>(
3788 ctx: &VerifyContext<'k>,
3789 key: Option<&'k dyn VerifyingKey>,
3790 key_info: Option<&KeyInfo>,
3791 algorithm: SignatureAlgorithm,
3792) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
3793 if let Some(key) = key {
3794 if !ctx.policy.key_sources.preset_key {
3795 return Err(crate::policy::PolicyViolation::KeyTrust {
3796 reason: "pre-resolved verification keys are disabled",
3797 }
3798 .into());
3799 }
3800 require_verifying_key_candidate_capacity(&ctx.policy)?;
3801 return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
3802 }
3803 if let Some(resolver) = ctx.key_resolver {
3804 require_verifying_key_candidate_capacity(&ctx.policy)?;
3805 let resolved =
3806 resolver.resolve_for_verification(key_info, algorithm, &ctx.policy, ctx.provider)?;
3807 if let Some(key) = &resolved {
3808 key.authorize(&ctx.policy)?;
3809 }
3810 return Ok(resolved.map(ResolvedVerifyingKey::Owned));
3811 }
3812 Ok(None)
3813}
3814
3815fn require_verifying_key_candidate_capacity(
3816 policy: &crate::policy::VerificationPolicy,
3817) -> Result<(), SignatureVerificationPipelineError> {
3818 policy
3819 .resources
3820 .validate_key_candidates(1)
3821 .map_err(Into::into)
3822}
3823
3824fn enforce_reference_policies(
3825 references: &[Reference],
3826 allowed_uri_types: UriTypeSet,
3827 allowed_transforms: Option<&HashSet<String>>,
3828) -> Result<(), SignatureVerificationPipelineError> {
3829 for reference in references {
3830 let uri = reference
3831 .uri
3832 .as_deref()
3833 .ok_or(SignatureVerificationPipelineError::Reference(
3834 ReferenceProcessingError::MissingUri,
3835 ))?;
3836 if !allowed_uri_types.allows(uri) {
3837 return Err(crate::policy::PolicyViolation::Uri {
3838 operation: "verification",
3839 reason: "reference URI class is not permitted",
3840 }
3841 .into());
3842 }
3843
3844 if let Some(allowed) = allowed_transforms {
3845 for transform in &reference.transforms {
3846 let transform_uri = transform.algorithm_uri();
3847 enforce_transform_allowed(Some(allowed), transform_uri)?;
3848 }
3849
3850 let produces_binary = transform_chain_produces_binary(
3855 classify_uri(uri) == UriClass::External,
3856 &reference.transforms,
3857 );
3858 if !produces_binary {
3859 enforce_transform_allowed(Some(allowed), DEFAULT_IMPLICIT_C14N_URI)?;
3860 }
3861 }
3862 }
3863 Ok(())
3864}
3865
3866fn enforce_transform_allowed(
3867 allowed_transforms: Option<&HashSet<String>>,
3868 algorithm: &str,
3869) -> Result<(), SignatureVerificationPipelineError> {
3870 if allowed_transforms.is_some_and(|allowed| !allowed.contains(algorithm)) {
3871 return Err(crate::policy::PolicyViolation::Algorithm {
3872 operation: "verification transform",
3873 algorithm: algorithm.to_owned(),
3874 }
3875 .into());
3876 }
3877 Ok(())
3878}
3879
3880#[derive(Debug, Clone, Copy)]
3881pub(super) struct SignatureChildNodes<'a, 'input> {
3882 signed_info_node: Node<'a, 'input>,
3883 signature_value_node: Node<'a, 'input>,
3884 key_info_node: Option<Node<'a, 'input>>,
3885}
3886
3887pub(super) fn parse_signature_children<'a, 'input>(
3888 signature_node: Node<'a, 'input>,
3889) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
3890 let mut signed_info_node: Option<Node<'_, '_>> = None;
3891 let mut signature_value_node: Option<Node<'_, '_>> = None;
3892 let mut key_info_node: Option<Node<'_, '_>> = None;
3893 let mut signed_info_index: Option<usize> = None;
3894 let mut signature_value_index: Option<usize> = None;
3895 let mut key_info_index: Option<usize> = None;
3896 let mut first_unexpected_dsig_index: Option<usize> = None;
3897
3898 let mut element_index = 0usize;
3899 for child in signature_node.children() {
3900 if child.is_text() {
3901 if child
3902 .text()
3903 .is_some_and(|text| !is_xml_whitespace_only(text))
3904 {
3905 return Err(SignatureVerificationPipelineError::InvalidStructure {
3906 reason: "Signature must not contain non-whitespace mixed content",
3907 });
3908 }
3909 continue;
3910 }
3911 if !child.is_element() {
3912 continue;
3913 }
3914
3915 element_index += 1;
3916 if child.tag_name().namespace() != Some(XMLDSIG_NS) {
3917 return Err(SignatureVerificationPipelineError::InvalidStructure {
3918 reason: "Signature must contain only XMLDSIG element children",
3919 });
3920 }
3921 match child.tag_name().name() {
3922 "SignedInfo" => {
3923 if signed_info_node.is_some() {
3924 return Err(SignatureVerificationPipelineError::InvalidStructure {
3925 reason: "SignedInfo must appear exactly once under Signature",
3926 });
3927 }
3928 signed_info_node = Some(child);
3929 signed_info_index = Some(element_index);
3930 }
3931 "SignatureValue" => {
3932 if signature_value_node.is_some() {
3933 return Err(SignatureVerificationPipelineError::InvalidStructure {
3934 reason: "SignatureValue must appear exactly once under Signature",
3935 });
3936 }
3937 signature_value_node = Some(child);
3938 signature_value_index = Some(element_index);
3939 }
3940 "KeyInfo" => {
3941 if key_info_node.is_some() {
3942 return Err(SignatureVerificationPipelineError::InvalidStructure {
3943 reason: "KeyInfo must appear at most once under Signature",
3944 });
3945 }
3946 key_info_node = Some(child);
3947 key_info_index = Some(element_index);
3948 }
3949 "Object" => {
3950 }
3953 _ => {
3954 if first_unexpected_dsig_index.is_none() {
3955 first_unexpected_dsig_index = Some(element_index);
3956 }
3957 }
3958 }
3959 }
3960
3961 let signed_info_node =
3962 signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
3963 element: "SignedInfo",
3964 })?;
3965 let signature_value_node =
3966 signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
3967 element: "SignatureValue",
3968 })?;
3969 if signed_info_index != Some(1) {
3970 return Err(SignatureVerificationPipelineError::InvalidStructure {
3971 reason: "SignedInfo must be the first element child of Signature",
3972 });
3973 }
3974 if signature_value_index != Some(2) {
3975 return Err(SignatureVerificationPipelineError::InvalidStructure {
3976 reason: "SignatureValue must be the second element child of Signature",
3977 });
3978 }
3979 if let Some(index) = key_info_index
3980 && index != 3
3981 {
3982 return Err(SignatureVerificationPipelineError::InvalidStructure {
3983 reason: "KeyInfo must be the third element child of Signature when present",
3984 });
3985 }
3986
3987 let allowed_prefix_end = key_info_index.unwrap_or(2);
3988 if let Some(unexpected_index) = first_unexpected_dsig_index {
3989 return Err(SignatureVerificationPipelineError::InvalidStructure {
3990 reason: if unexpected_index > allowed_prefix_end {
3991 "After SignedInfo, SignatureValue, and optional KeyInfo, Signature may contain only Object elements"
3992 } else {
3993 "Signature may contain SignedInfo first, SignatureValue second, optional KeyInfo third, and Object elements thereafter"
3994 },
3995 });
3996 }
3997
3998 Ok(SignatureChildNodes {
3999 signed_info_node,
4000 signature_value_node,
4001 key_info_node,
4002 })
4003}
4004
4005fn decode_signature_value(
4006 signature_value_node: Node<'_, '_>,
4007 algorithm: SignatureAlgorithm,
4008 resources: &crate::policy::ResourcePolicy,
4009) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
4010 if matches!(algorithm, SignatureAlgorithm::PostQuantum(_)) {
4011 let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
4012 let payload = super::whitespace::XmlBase64Payload::bounded(
4016 signature_value_node,
4017 raw_maximum,
4018 maximum,
4019 )
4020 .map_err(
4021 |reason| SignatureVerificationPipelineError::InvalidStructure {
4022 reason: match reason {
4023 "unexpected nested element" => {
4024 "SignatureValue must not contain element children"
4025 }
4026 "maximum allowed text length" => {
4027 "SignatureValue exceeds maximum allowed text length"
4028 }
4029 "maximum allowed base64 length" => {
4030 "SignatureValue exceeds maximum allowed length"
4031 }
4032 _ => "SignatureValue contains malformed Base64",
4033 },
4034 },
4035 )?;
4036 return payload.decode().map_err(|_| {
4037 SignatureVerificationPipelineError::InvalidStructure {
4038 reason: "SignatureValue contains malformed Base64",
4039 }
4040 });
4041 }
4042 if signature_value_node
4043 .children()
4044 .any(|child| child.is_element())
4045 {
4046 return Err(SignatureVerificationPipelineError::InvalidStructure {
4047 reason: "SignatureValue must not contain element children",
4048 });
4049 }
4050
4051 let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
4055 let mut text_len = 0usize;
4056 let mut normalized_len = 0usize;
4057 for child in signature_value_node
4058 .children()
4059 .filter(|child| child.is_text())
4060 {
4061 if let Some(text) = child.text() {
4062 if text.len() > raw_maximum - text_len {
4063 return Err(SignatureVerificationPipelineError::InvalidStructure {
4064 reason: "SignatureValue exceeds maximum allowed text length",
4065 });
4066 }
4067 text_len += text.len();
4068 for byte in text.bytes() {
4069 if matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
4070 continue;
4071 }
4072 if normalized_len == maximum {
4073 return Err(SignatureVerificationPipelineError::InvalidStructure {
4074 reason: "SignatureValue exceeds maximum allowed length",
4075 });
4076 }
4077 normalized_len += 1;
4078 }
4079 }
4080 }
4081 let mut normalized = Vec::with_capacity(normalized_len);
4082 let mut raw_text_len = 0usize;
4083 for child in signature_value_node
4084 .children()
4085 .filter(|child| child.is_text())
4086 {
4087 if let Some(text) = child.text() {
4088 push_normalized_signature_text(
4089 text,
4090 &mut raw_text_len,
4091 &mut normalized,
4092 algorithm,
4093 resources,
4094 )?;
4095 }
4096 }
4097
4098 Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
4099}
4100
4101fn push_normalized_signature_text(
4102 text: &str,
4103 raw_text_len: &mut usize,
4104 normalized: &mut Vec<u8>,
4105 algorithm: SignatureAlgorithm,
4106 resources: &crate::policy::ResourcePolicy,
4107) -> Result<(), SignatureVerificationPipelineError> {
4108 let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
4109 if text.len() > raw_maximum - *raw_text_len {
4110 return Err(SignatureVerificationPipelineError::InvalidStructure {
4111 reason: "SignatureValue exceeds maximum allowed text length",
4112 });
4113 }
4114 *raw_text_len += text.len();
4115 let additional = text
4116 .bytes()
4117 .filter(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
4118 .count();
4119 if additional > maximum - normalized.len() {
4120 return Err(SignatureVerificationPipelineError::InvalidStructure {
4121 reason: "SignatureValue exceeds maximum allowed length",
4122 });
4123 }
4124
4125 normalize_xml_base64_bytes(text.as_bytes(), normalized, |_| true).map_err(|err| {
4126 SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(
4127 err.normalized_offset,
4128 err.invalid_byte,
4129 ))
4130 })?;
4131
4132 Ok(())
4133}
4134
4135fn signature_value_limits(
4136 algorithm: SignatureAlgorithm,
4137 resources: &crate::policy::ResourcePolicy,
4138) -> (usize, usize) {
4139 let encoded = match algorithm {
4146 SignatureAlgorithm::PostQuantum(parameter) => parameter.signature_len().div_ceil(3) * 4,
4147 _ => MAX_SIGNATURE_VALUE_LEN,
4148 };
4149 (encoded, resources.max_xml_document_bytes)
4150}
4151
4152fn verify_with_algorithm(
4153 algorithm: SignatureAlgorithm,
4154 public_key_pem: &str,
4155 signed_data: &[u8],
4156 signature_value: &[u8],
4157) -> Result<bool, SignatureVerificationPipelineError> {
4158 match algorithm {
4159 SignatureAlgorithm::Ed25519
4160 | SignatureAlgorithm::Ed25519Ctx
4161 | SignatureAlgorithm::Ed25519Ph
4162 | SignatureAlgorithm::Ed448
4163 | SignatureAlgorithm::Ed448Ph
4164 | SignatureAlgorithm::PostQuantum(_) => {
4165 let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
4166 .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
4167 if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
4168 return Err(SignatureVerificationError::InvalidKeyPem.into());
4169 }
4170 Ok(super::modern::verify(
4171 algorithm,
4172 &pem.contents,
4173 signed_data,
4174 signature_value,
4175 )?)
4176 }
4177 SignatureAlgorithm::DsaSha1 | SignatureAlgorithm::DsaSha256 => {
4178 let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
4179 .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
4180 if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
4181 return Err(SignatureVerificationError::InvalidKeyPem.into());
4182 }
4183 Ok(verify_dsa_signature_spki(
4184 algorithm,
4185 &pem.contents,
4186 signed_data,
4187 signature_value,
4188 )?)
4189 }
4190 SignatureAlgorithm::HmacSha1
4191 | SignatureAlgorithm::HmacSha224
4192 | SignatureAlgorithm::HmacSha256
4193 | SignatureAlgorithm::HmacSha384
4194 | SignatureAlgorithm::HmacSha512 => Err(SignatureVerificationError::UnsupportedAlgorithm {
4195 uri: algorithm.uri().to_string(),
4196 }
4197 .into()),
4198 SignatureAlgorithm::RsaSha1
4199 | SignatureAlgorithm::RsaSha224
4200 | SignatureAlgorithm::RsaSha256
4201 | SignatureAlgorithm::RsaSha384
4202 | SignatureAlgorithm::RsaSha512 => Ok(verify_rsa_signature_pem(
4203 algorithm,
4204 public_key_pem,
4205 signed_data,
4206 signature_value,
4207 )?),
4208 SignatureAlgorithm::EcdsaSha1
4209 | SignatureAlgorithm::EcdsaSha224
4210 | SignatureAlgorithm::EcdsaSha256
4211 | SignatureAlgorithm::EcdsaSha384
4212 | SignatureAlgorithm::EcdsaSha512
4213 | SignatureAlgorithm::EcdsaSha3_224
4214 | SignatureAlgorithm::EcdsaSha3_256
4215 | SignatureAlgorithm::EcdsaSha3_384
4216 | SignatureAlgorithm::EcdsaSha3_512 => {
4217 match verify_ecdsa_signature_pem(
4221 algorithm,
4222 public_key_pem,
4223 signed_data,
4224 signature_value,
4225 ) {
4226 Ok(valid) => Ok(valid),
4227 Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
4228 Err(error) => Err(error.into()),
4229 }
4230 }
4231 }
4232}
4233
4234#[cfg(test)]
4235#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
4236mod tests {
4237 use super::*;
4238 use crate::c14n::C14nAlgorithm;
4239 use crate::xml::dom::Document;
4240 use crate::xmldsig::TransformError;
4241 use crate::xmldsig::digest::DigestAlgorithm;
4242 use crate::xmldsig::parse::{Reference, parse_signed_info};
4243 use crate::xmldsig::transforms::Transform;
4244 use crate::xmldsig::uri::UriReferenceResolver;
4245 use base64::Engine;
4246
4247 fn make_reference(
4251 uri: &str,
4252 transforms: Vec<Transform>,
4253 digest_method: DigestAlgorithm,
4254 digest_value: Vec<u8>,
4255 ) -> Reference {
4256 Reference {
4257 uri: Some(uri.to_string()),
4258 id: None,
4259 ref_type: None,
4260 transforms,
4261 digest_method,
4262 digest_value,
4263 }
4264 }
4265
4266 #[test]
4267 fn reference_resolution_uses_each_elements_effective_xml_base() {
4268 let first = b"first payload";
4271 let second = b"second payload";
4272 let first_digest = base64::engine::general_purpose::STANDARD
4273 .encode(compute_digest(DigestAlgorithm::Sha256, first));
4274 let second_digest = base64::engine::general_purpose::STANDARD
4275 .encode(compute_digest(DigestAlgorithm::Sha256, second));
4276 let xml = format!(
4277 r#"<root xml:base="https://example.test/base/" xmlns:ds="{XMLDSIG_NS}">
4278 <ds:Signature><ds:SignedInfo>
4279 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4280 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4281 <ds:Reference xml:base="one/" URI="payload.bin">
4282 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4283 <ds:DigestValue>{first_digest}</ds:DigestValue>
4284 </ds:Reference>
4285 <ds:Reference xml:base="../two/" URI="payload.bin">
4286 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4287 <ds:DigestValue>{second_digest}</ds:DigestValue>
4288 </ds:Reference>
4289 </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>
4290 </root>"#
4291 );
4292 let document = Document::parse(&xml).unwrap();
4293 let signature = document
4294 .descendants()
4295 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
4296 .unwrap();
4297 let signed_info_node = signature
4298 .children()
4299 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
4300 .unwrap();
4301 let signed_info = parse_signed_info(signed_info_node).unwrap();
4302 let resources = HashMap::from([
4303 (
4304 "https://example.test/base/one/payload.bin".into(),
4305 first.to_vec(),
4306 ),
4307 (
4308 "https://example.test/two/payload.bin".into(),
4309 second.to_vec(),
4310 ),
4311 ]);
4312 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4313
4314 let result = process_all_references(&signed_info.references, &resolver, signature, false)
4315 .expect("each Reference should resolve against its own effective base");
4316
4317 assert!(result.all_valid());
4318 }
4319
4320 #[test]
4321 fn internal_dtd_opt_in_applies_to_detached_xml_transforms() {
4322 let detached = b"<!DOCTYPE payload [<!ELEMENT payload (#PCDATA)>]><payload>ok</payload>";
4325 let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
4326 DigestAlgorithm::Sha256,
4327 b"<payload>ok</payload>",
4328 ));
4329 let xml = format!(
4330 r#"<root xmlns:ds="{XMLDSIG_NS}">
4331 <ds:Signature>
4332 <ds:SignedInfo>
4333 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4334 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4335 <ds:Reference URI="urn:detached-dtd">
4336 <ds:Transforms>
4337 <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
4338 </ds:Transforms>
4339 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4340 <ds:DigestValue>{digest}</ds:DigestValue>
4341 </ds:Reference>
4342 </ds:SignedInfo>
4343 <ds:SignatureValue>AQ==</ds:SignatureValue>
4344 </ds:Signature>
4345</root>"#
4346 );
4347 let resources = HashMap::from([("urn:detached-dtd".to_owned(), detached.to_vec())]);
4348 let key = AcceptingKey;
4349
4350 let default_error = VerifyContext::new()
4351 .key(&key)
4352 .allowed_uri_types(UriTypeSet::ALL)
4353 .external_resources(&resources)
4354 .verify(&xml)
4355 .expect_err("internal DTD parsing must remain disabled by default");
4356 assert!(matches!(
4357 default_error,
4358 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
4359 crate::xmldsig::TransformError::XmlParse(_)
4360 ))
4361 ));
4362
4363 let result = VerifyContext::new()
4364 .key(&key)
4365 .allowed_uri_types(UriTypeSet::ALL)
4366 .external_resources(&resources)
4367 .allow_internal_dtd(true)
4368 .verify(&xml)
4369 .expect("the explicit DTD opt-in must cover detached XML transforms");
4370
4371 assert_eq!(result.status, DsigStatus::Valid);
4372
4373 let external_entity = br#"<!DOCTYPE payload [
4374 <!ENTITY ext SYSTEM "file:///etc/passwd">
4375 ]><payload>&ext;</payload>"#;
4376 let external_entity_resources =
4377 HashMap::from([("urn:detached-dtd".to_owned(), external_entity.to_vec())]);
4378 let external_entity_error = VerifyContext::new()
4379 .key(&key)
4380 .allowed_uri_types(UriTypeSet::ALL)
4381 .external_resources(&external_entity_resources)
4382 .allow_internal_dtd(true)
4383 .verify(&xml)
4384 .expect_err("the internal-DTD opt-in must not resolve external entities");
4385 assert!(matches!(
4386 external_entity_error,
4387 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
4388 crate::xmldsig::TransformError::XmlParse(_)
4389 ))
4390 ));
4391 }
4392
4393 #[test]
4394 fn owned_verification_revalidates_internal_dtd_provenance() {
4395 let document = XmlDocument::parse_with_settings(
4398 "<!DOCTYPE root [<!ENTITY value \"ok\">]><root>&value;</root>".into(),
4399 DocumentParseSettings::new(
4400 true,
4401 crate::hard_limits::XML_DOCUMENT_NODE_CEILING,
4402 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
4403 ),
4404 )
4405 .expect("explicitly permitted DTD fixture must parse");
4406
4407 assert!(matches!(
4408 VerifyContext::new().verify_document(&document),
4409 Err(DsigError::Policy(
4410 crate::policy::PolicyViolation::XmlInput {
4411 reason: "owned document requires internal DTD support"
4412 }
4413 ))
4414 ));
4415 assert!(!matches!(
4416 VerifyContext::new()
4417 .allow_internal_dtd(true)
4418 .verify_document(&document),
4419 Err(DsigError::Policy(
4420 crate::policy::PolicyViolation::XmlInput { .. }
4421 ))
4422 ));
4423 }
4424
4425 #[test]
4426 fn verification_policy_bounds_reference_canonicalization() {
4427 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4430 let xml = format!(
4431 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>"#,
4432 "payload".repeat(16)
4433 );
4434 let policy = crate::policy::VerificationPolicy {
4435 resources: crate::policy::ResourcePolicy {
4436 max_canonicalized_bytes: 64,
4437 ..crate::policy::ResourcePolicy::default()
4438 },
4439 ..crate::policy::VerificationPolicy::default()
4440 };
4441
4442 let error = VerifyContext::new()
4443 .key(&AcceptingKey)
4444 .policy(policy)
4445 .verify(&xml)
4446 .expect_err("reference canonicalization must consume the policy budget");
4447
4448 assert!(
4449 matches!(
4450 error,
4451 SignatureVerificationPipelineError::Policy(
4452 crate::policy::PolicyViolation::ResourceLimit {
4453 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
4454 maximum: 64,
4455 ..
4456 }
4457 )
4458 ),
4459 "unexpected error: {error:?}"
4460 );
4461 }
4462
4463 #[test]
4464 fn verification_policy_bounds_document_bytes_before_parsing() {
4465 let xml = format!("<root>{}</root>", "x".repeat(1_024));
4468 let policy = crate::policy::VerificationPolicy {
4469 resources: crate::policy::ResourcePolicy {
4470 max_xml_document_bytes: xml.len() - 1,
4471 ..crate::policy::ResourcePolicy::default()
4472 },
4473 ..crate::policy::VerificationPolicy::default()
4474 };
4475
4476 assert!(matches!(
4477 VerifyContext::new().policy(policy).verify(&xml),
4478 Err(SignatureVerificationPipelineError::Policy(
4479 crate::policy::PolicyViolation::ResourceLimit {
4480 resource: crate::policy::resource_name::XML_DOCUMENT,
4481 maximum,
4482 actual,
4483 }
4484 )) if maximum == xml.len() - 1 && actual == xml.len()
4485 ));
4486 }
4487
4488 #[test]
4489 fn verification_entry_points_enforce_policy_depth() {
4490 let xml = "<root><child><leaf/></child></root>";
4493 let policy = crate::policy::VerificationPolicy {
4494 resources: crate::policy::ResourcePolicy {
4495 max_xml_depth: 2,
4496 ..crate::policy::ResourcePolicy::default()
4497 },
4498 ..crate::policy::VerificationPolicy::default()
4499 };
4500 let document = XmlDocument::parse(xml).expect("wide retained fixture must parse");
4501
4502 assert!(matches!(
4503 VerifyContext::new().policy(policy.clone()).verify(xml),
4504 Err(DsigError::Policy(
4505 crate::policy::PolicyViolation::ResourceLimit {
4506 resource: crate::policy::resource_name::XML_DEPTH,
4507 maximum: 2,
4508 actual: 3,
4509 }
4510 ))
4511 ));
4512 assert!(matches!(
4513 VerifyContext::new()
4514 .policy(policy)
4515 .verify_document(&document),
4516 Err(DsigError::Policy(
4517 crate::policy::PolicyViolation::ResourceLimit {
4518 resource: crate::policy::resource_name::XML_DEPTH,
4519 maximum: 2,
4520 actual: 3,
4521 }
4522 ))
4523 ));
4524 }
4525
4526 #[test]
4527 fn verification_policy_bounds_base64_transform_input() {
4528 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4531 let xml = format!(
4532 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>"##
4533 );
4534 let policy = crate::policy::VerificationPolicy {
4535 resources: crate::policy::ResourcePolicy {
4536 max_base64_transform_input_bytes: 4,
4537 ..crate::policy::ResourcePolicy::default()
4538 },
4539 ..crate::policy::VerificationPolicy::default()
4540 };
4541
4542 let error = VerifyContext::new()
4543 .key(&AcceptingKey)
4544 .policy(policy)
4545 .verify(&xml)
4546 .expect_err("Base64 input must use the operation policy ceiling");
4547
4548 assert!(matches!(
4549 error,
4550 SignatureVerificationPipelineError::Policy(
4551 crate::policy::PolicyViolation::ResourceLimit {
4552 resource: crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
4553 maximum: 4,
4554 ..
4555 }
4556 )
4557 ));
4558 }
4559
4560 #[test]
4561 fn verification_policy_bounds_cumulative_base64_transform_output() {
4562 let first_digest = base64::engine::general_purpose::STANDARD
4565 .encode(compute_digest(DigestAlgorithm::Sha256, b"a"));
4566 let second_digest = base64::engine::general_purpose::STANDARD
4567 .encode(compute_digest(DigestAlgorithm::Sha256, b"b"));
4568 let xml = format!(
4569 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>"##
4570 );
4571 let policy = crate::policy::VerificationPolicy {
4572 resources: crate::policy::ResourcePolicy {
4573 max_base64_transform_input_bytes: 8,
4574 max_base64_transform_output_bytes: 1,
4575 ..crate::policy::ResourcePolicy::default()
4576 },
4577 ..crate::policy::VerificationPolicy::default()
4578 };
4579
4580 let error = VerifyContext::new()
4581 .key(&AcceptingKey)
4582 .policy(policy)
4583 .verify(&xml)
4584 .expect_err("references must share the Base64 output allowance");
4585
4586 assert!(matches!(
4587 error,
4588 SignatureVerificationPipelineError::Policy(
4589 crate::policy::PolicyViolation::ResourceLimit {
4590 resource: crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
4591 maximum: 1,
4592 actual: 2,
4593 }
4594 )
4595 ));
4596 }
4597
4598 #[test]
4599 fn verification_policy_bounds_xpath_source_before_compilation() {
4600 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4603 let xml = format!(
4604 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>"#
4605 );
4606 let policy = crate::policy::VerificationPolicy {
4607 resources: crate::policy::ResourcePolicy {
4608 max_xpath_expression_bytes: 4,
4609 ..crate::policy::ResourcePolicy::default()
4610 },
4611 ..crate::policy::VerificationPolicy::default()
4612 };
4613
4614 let error = VerifyContext::new()
4615 .key(&AcceptingKey)
4616 .policy(policy)
4617 .verify(&xml)
4618 .expect_err("XPath source must use the operation policy ceiling");
4619
4620 assert!(matches!(
4621 error,
4622 SignatureVerificationPipelineError::Policy(
4623 crate::policy::PolicyViolation::ResourceLimit {
4624 resource: crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
4625 maximum: 4,
4626 ..
4627 }
4628 )
4629 ));
4630 }
4631
4632 #[test]
4633 fn verification_policy_shares_canonicalization_budget_with_signed_info() {
4634 let payload_text = "x".repeat(700);
4638 let canonical_payload = format!("<payload ID=\"payload\">{payload_text}</payload>");
4639 let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
4640 DigestAlgorithm::Sha256,
4641 canonical_payload.as_bytes(),
4642 ));
4643 let xml = format!(
4644 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>"##
4645 );
4646 let policy = crate::policy::VerificationPolicy {
4647 resources: crate::policy::ResourcePolicy {
4648 max_canonicalized_bytes: 1_024,
4649 ..crate::policy::ResourcePolicy::default()
4650 },
4651 ..crate::policy::VerificationPolicy::default()
4652 };
4653
4654 let error = VerifyContext::new()
4655 .key(&AcceptingKey)
4656 .policy(policy)
4657 .verify(&xml)
4658 .expect_err("SignedInfo must consume the remaining operation C14N budget");
4659
4660 assert!(
4661 matches!(
4662 &error,
4663 SignatureVerificationPipelineError::Policy(
4664 crate::policy::PolicyViolation::ResourceLimit {
4665 resource: "canonicalized bytes",
4666 maximum: 1_024,
4667 ..
4668 }
4669 )
4670 ),
4671 "unexpected error: {error:?}"
4672 );
4673 }
4674
4675 #[test]
4676 fn manifest_processing_stops_after_c14n_budget_exhaustion() {
4677 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4680 let xml = format!(
4681 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>"##
4682 );
4683 let resources = HashMap::from([("urn:small".to_owned(), b"small".to_vec())]);
4684 let transform_budget = TransformExecutionBudget::with_c14n_limit(8);
4685 let ctx = VerifyContext::new()
4686 .allowed_uri_types(UriTypeSet::ALL)
4687 .external_resources(&resources);
4688 let document = XmlDocument::parse(xml).expect("test signature must parse");
4689 let results = document
4690 .with_view(|view| {
4691 let signature = view
4692 .document()
4693 .descendants()
4694 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
4695 .expect("test signature must contain Signature");
4696 let object = signature
4697 .children()
4698 .find(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
4699 .expect("test signature must contain Object");
4700 let resolver = UriReferenceResolver::with_document_view(view, &[])
4701 .with_external_resources(&resources);
4702 let budgets =
4703 VerificationOperationBudgets::with_transforms(&ctx.policy, transform_budget);
4704 let mut operation = OperationExecutionContext::new(
4705 &ctx.policy,
4706 &budgets,
4707 Some((view.identity(), view.generation())),
4708 );
4709 let crypto =
4710 operation.add_node(OperationNodeKind::Crypto, OperationStage::Crypto, None);
4711 operation.compile().expect("base plan");
4712 operation
4713 .run(crypto, || Ok::<_, SignatureVerificationPipelineError>(()))
4714 .expect("crypto gate");
4715 operation.authenticate(view.node_identity(object));
4716 process_authenticated_manifest_references(
4717 &mut operation,
4718 view,
4719 signature,
4720 &resolver,
4721 &ctx,
4722 2,
4723 crypto,
4724 )
4725 .map(|(results, _)| results)
4726 })
4727 .expect("resource exhaustion is reported per Manifest reference");
4728
4729 assert_eq!(results.len(), 2);
4730 assert!(results.iter().all(|result| {
4731 matches!(
4732 result.status,
4733 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { .. })
4734 )
4735 }));
4736 }
4737
4738 #[test]
4739 fn verification_policy_bounds_detached_xml_nodes() {
4740 let detached = format!("<payload>{}</payload>", "<n/>".repeat(32));
4743 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4744 let xml = format!(
4745 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>"#
4746 );
4747 let resources = HashMap::from([("urn:detached-nodes".to_owned(), detached.into_bytes())]);
4748 let policy = crate::policy::VerificationPolicy {
4749 uris: crate::policy::UriPolicy {
4750 references: UriTypeSet::ALL,
4751 ..crate::policy::UriPolicy::default()
4752 },
4753 resources: crate::policy::ResourcePolicy {
4754 max_xml_nodes: 24,
4755 ..crate::policy::ResourcePolicy::default()
4756 },
4757 ..crate::policy::VerificationPolicy::default()
4758 };
4759
4760 let error = VerifyContext::new()
4761 .key(&AcceptingKey)
4762 .policy(policy)
4763 .external_resources(&resources)
4764 .verify(&xml)
4765 .expect_err("detached XML must inherit the policy node ceiling");
4766
4767 assert!(
4768 matches!(
4769 error,
4770 SignatureVerificationPipelineError::Policy(
4771 crate::policy::PolicyViolation::ResourceLimit {
4772 resource: crate::policy::resource_name::XML_NODES,
4773 maximum: 24,
4774 ..
4775 }
4776 )
4777 ),
4778 "unexpected error: {error:?}"
4779 );
4780 }
4781
4782 #[test]
4783 fn query_only_reference_resolves_against_relative_xml_base() {
4784 let payload = b"query-selected payload";
4787 let digest = base64::engine::general_purpose::STANDARD
4788 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
4789 let xml = format!(
4790 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo>
4791 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4792 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4793 <ds:Reference xml:base="a/b?old" URI="?new">
4794 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4795 <ds:DigestValue>{digest}</ds:DigestValue>
4796 </ds:Reference>
4797 </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>"#
4798 );
4799 let document = Document::parse(&xml).unwrap();
4800 let signature = document.root_element();
4801 let signed_info_node = signature
4802 .children()
4803 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
4804 .unwrap();
4805 let signed_info = parse_signed_info(signed_info_node).unwrap();
4806 let resources = HashMap::from([("a/b?new".to_string(), payload.to_vec())]);
4807 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4808
4809 let result = process_all_references(&signed_info.references, &resolver, signature, false)
4810 .expect("query-only URI must resolve against the complete relative base path");
4811
4812 assert!(result.all_valid());
4813 }
4814
4815 #[test]
4816 fn manifest_reference_resolution_uses_its_effective_xml_base() {
4817 let payload = b"manifest payload";
4820 let digest = base64::engine::general_purpose::STANDARD
4821 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
4822 let xml = format!(
4823 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
4824 <ds:Object><ds:Manifest xml:base="manifests/">
4825 <ds:Reference URI="payload.bin">
4826 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4827 <ds:DigestValue>{digest}</ds:DigestValue>
4828 </ds:Reference>
4829 </ds:Manifest></ds:Object>
4830 </ds:Signature>"#
4831 );
4832 let document = Document::parse(&xml).unwrap();
4833 let signature = document.root_element();
4834 let reference_node = signature
4835 .descendants()
4836 .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
4837 .unwrap();
4838 let reference = super::super::parse::parse_reference(reference_node).unwrap();
4839 let resources = HashMap::from([(
4840 "https://example.test/manifests/payload.bin".to_string(),
4841 payload.to_vec(),
4842 )]);
4843 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4844
4845 let result = process_reference(
4846 &reference,
4847 &resolver,
4848 signature,
4849 ReferenceSet::Manifest,
4850 0,
4851 false,
4852 )
4853 .expect("Manifest Reference should inherit its own XML Base context");
4854
4855 assert_eq!(result.status, DsigStatus::Valid);
4856 }
4857
4858 #[test]
4859 fn manifest_reference_index_ignores_nested_manifest_descendants() {
4860 let payload = b"direct manifest payload";
4863 let digest = base64::engine::general_purpose::STANDARD
4864 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
4865 let xml = format!(
4866 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
4867 <ds:Object><wrapper><ds:Manifest xml:base="nested/">
4868 <ds:Reference URI="payload.bin">
4869 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4870 <ds:DigestValue>{digest}</ds:DigestValue>
4871 </ds:Reference>
4872 </ds:Manifest></wrapper></ds:Object>
4873 <ds:Object><ds:Manifest xml:base="direct/">
4874 <ds:Reference URI="payload.bin">
4875 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4876 <ds:DigestValue>{digest}</ds:DigestValue>
4877 </ds:Reference>
4878 </ds:Manifest></ds:Object>
4879 </ds:Signature>"#
4880 );
4881 let document = Document::parse(&xml).unwrap();
4882 let signature = document.root_element();
4883 let direct_reference_node = signature
4884 .children()
4885 .filter(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
4886 .nth(1)
4887 .unwrap()
4888 .children()
4889 .find(|node| node.has_tag_name((XMLDSIG_NS, "Manifest")))
4890 .unwrap()
4891 .children()
4892 .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
4893 .unwrap();
4894 let reference = super::super::parse::parse_reference(direct_reference_node).unwrap();
4895 let resources = HashMap::from([(
4896 "https://example.test/direct/payload.bin".to_string(),
4897 payload.to_vec(),
4898 )]);
4899 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4900
4901 let result = process_reference(
4902 &reference,
4903 &resolver,
4904 signature,
4905 ReferenceSet::Manifest,
4906 0,
4907 false,
4908 )
4909 .expect("Manifest index must select the direct Object/Manifest reference");
4910
4911 assert_eq!(result.status, DsigStatus::Valid);
4912 }
4913
4914 struct RejectingKey;
4915
4916 impl VerifyingKey for RejectingKey {
4917 fn verify(
4918 &self,
4919 _algorithm: SignatureAlgorithm,
4920 _signed_data: &[u8],
4921 _signature_value: &[u8],
4922 ) -> Result<bool, SignatureVerificationPipelineError> {
4923 Ok(false)
4924 }
4925 }
4926
4927 struct AcceptingKey;
4928
4929 impl VerifyingKey for AcceptingKey {
4930 fn verify(
4931 &self,
4932 _algorithm: SignatureAlgorithm,
4933 _signed_data: &[u8],
4934 _signature_value: &[u8],
4935 ) -> Result<bool, SignatureVerificationPipelineError> {
4936 Ok(true)
4937 }
4938 }
4939
4940 struct PanicResolver;
4941
4942 struct UntrustedTestResolver;
4943
4944 impl KeyResolver for UntrustedTestResolver {
4945 fn resolve<'a>(
4946 &'a self,
4947 _key_info: Option<&KeyInfo>,
4948 _algorithm: SignatureAlgorithm,
4949 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
4950 Ok(Some(Box::new(AcceptingKey)))
4951 }
4952 }
4953
4954 #[test]
4955 fn verification_requires_explicit_key_authorization() {
4956 let xml = signature_with_target_reference("AQ==");
4958 let error = VerifyContext::new()
4959 .key_resolver(&UntrustedTestResolver)
4960 .verify(&xml)
4961 .expect_err("default verification must reject an untrusted resolver candidate");
4962 assert!(matches!(
4963 error,
4964 DsigError::Policy(crate::policy::PolicyViolation::KeyTrust { .. })
4965 ));
4966 }
4967
4968 #[test]
4969 fn cryptographic_only_is_explicit_and_never_reports_trust() {
4970 let mut policy = crate::policy::VerificationPolicy::default();
4972 policy.key_trust.mode = crate::policy::VerificationTrustMode::CryptographicOnly;
4973 let result = VerifyContext::new()
4974 .policy(policy)
4975 .key_resolver(&UntrustedTestResolver)
4976 .verify(&signature_with_target_reference("AQ=="))
4977 .expect("explicit mathematical mode permits an untrusted candidate");
4978 assert_eq!(result.status, DsigStatus::Valid);
4979 assert_eq!(
4980 result.key_trust,
4981 super::super::KeyTrustEvidence::NotEstablished
4982 );
4983 }
4984
4985 #[test]
4986 fn caller_key_authorization_does_not_authenticate_an_invalid_signature() {
4987 let result = VerifyContext::new()
4989 .key(&RejectingKey)
4990 .verify(&signature_with_target_reference("AQ=="))
4991 .expect("an invalid mathematical signature is a completed result");
4992 assert_eq!(
4993 result.status,
4994 DsigStatus::Invalid(FailureReason::SignatureMismatch)
4995 );
4996 assert_eq!(
4997 result.key_trust,
4998 super::super::KeyTrustEvidence::CallerTrusted
4999 );
5000 }
5001
5002 impl KeyResolver for PanicResolver {
5003 fn resolve<'a>(
5004 &'a self,
5005 _key_info: Option<&KeyInfo>,
5006 _algorithm: SignatureAlgorithm,
5007 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
5008 {
5009 panic!("resolver should not be called when references already fail");
5010 }
5011 }
5012
5013 struct MissingKeyResolver;
5014
5015 impl KeyResolver for MissingKeyResolver {
5016 fn resolve<'a>(
5017 &'a self,
5018 _key_info: Option<&KeyInfo>,
5019 _algorithm: SignatureAlgorithm,
5020 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
5021 {
5022 Ok(None)
5023 }
5024 }
5025
5026 struct ConsumingKeyInfoResolver;
5027
5028 impl KeyResolver for ConsumingKeyInfoResolver {
5029 fn resolve<'a>(
5030 &'a self,
5031 _key_info: Option<&KeyInfo>,
5032 _algorithm: SignatureAlgorithm,
5033 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
5034 {
5035 Ok(None)
5036 }
5037
5038 fn consumes_document_key_info(&self) -> bool {
5039 true
5040 }
5041 }
5042
5043 struct FallbackKeyInfoResolver;
5044
5045 impl KeyResolver for FallbackKeyInfoResolver {
5046 fn resolve_for_verification<'a>(
5047 &'a self,
5048 key_info: Option<&KeyInfo>,
5049 algorithm: SignatureAlgorithm,
5050 _policy: &crate::policy::VerificationPolicy,
5051 _provider: &dyn crate::provider::CryptoProvider,
5052 ) -> Result<Option<super::super::ResolvedVerificationKey<'a>>, DsigError> {
5053 self.resolve(key_info, algorithm)
5055 .map(|key| key.map(super::super::ResolvedVerificationKey::CallerTrusted))
5056 }
5057
5058 fn resolve<'a>(
5059 &'a self,
5060 key_info: Option<&KeyInfo>,
5061 _algorithm: SignatureAlgorithm,
5062 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
5063 {
5064 let sources = &key_info.expect("KeyInfo must be parsed").sources;
5065 assert!(matches!(
5066 sources.as_slice(),
5067 [
5068 super::super::parse::KeyInfoSource::RetrievalMethod { .. },
5069 super::super::parse::KeyInfoSource::KeyName(name),
5070 ] if name == "fallback"
5071 ));
5072 Ok(Some(Box::new(AcceptingKey)))
5073 }
5074
5075 fn consumes_document_key_info(&self) -> bool {
5076 true
5077 }
5078 }
5079
5080 struct EarlyKeyInfoResolver;
5081
5082 impl KeyResolver for EarlyKeyInfoResolver {
5083 fn resolve_for_verification<'a>(
5084 &'a self,
5085 key_info: Option<&KeyInfo>,
5086 algorithm: SignatureAlgorithm,
5087 _policy: &crate::policy::VerificationPolicy,
5088 _provider: &dyn crate::provider::CryptoProvider,
5089 ) -> Result<Option<super::super::ResolvedVerificationKey<'a>>, DsigError> {
5090 self.resolve(key_info, algorithm)
5092 .map(|key| key.map(super::super::ResolvedVerificationKey::CallerTrusted))
5093 }
5094
5095 fn resolve<'a>(
5096 &'a self,
5097 key_info: Option<&KeyInfo>,
5098 _algorithm: SignatureAlgorithm,
5099 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
5100 {
5101 let sources = &key_info.expect("KeyInfo must be parsed").sources;
5102 assert!(matches!(
5103 sources.as_slice(),
5104 [
5105 super::super::parse::KeyInfoSource::KeyName(name),
5106 super::super::parse::KeyInfoSource::RetrievalMethod { .. },
5107 ] if name == "primary"
5108 ));
5109 Ok(Some(Box::new(AcceptingKey)))
5110 }
5111
5112 fn consumes_document_key_info(&self) -> bool {
5113 true
5114 }
5115 }
5116
5117 fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
5118 format!(
5119 r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5120 <ds:SignedInfo>
5121 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5122 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5123 <ds:Reference URI="{reference_uri}">
5124 {transforms_xml}
5125 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
5126 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
5127 </ds:Reference>
5128 </ds:SignedInfo>
5129 <ds:SignatureValue>AQ==</ds:SignatureValue>
5130</ds:Signature>"#
5131 )
5132 }
5133
5134 #[test]
5135 fn verify_all_retains_each_signature_without_vacuous_success() {
5136 let document = XmlDocument::parse(format!(
5139 "<root xmlns:ds=\"{XMLDSIG_NS}\"><ds:Signature/><ds:Signature/></root>"
5140 ))
5141 .unwrap();
5142 let evidence = VerifyContext::new().verify_all(&document).unwrap();
5143 assert_eq!(evidence.signatures().len(), 2);
5144 assert!(
5145 evidence
5146 .signatures()
5147 .iter()
5148 .all(|entry| entry.result().is_err())
5149 );
5150 assert!(!evidence.all_valid());
5151 let empty = XmlDocument::parse("<root/>").unwrap();
5152 assert!(!VerifyContext::new().verify_all(&empty).unwrap().all_valid());
5153 }
5154
5155 #[test]
5156 fn verify_all_checks_cardinality_before_running_keys() {
5157 let document = XmlDocument::parse(format!(
5160 "<root xmlns:ds=\"{XMLDSIG_NS}\"><ds:Signature/><ds:Signature/></root>"
5161 ))
5162 .unwrap();
5163 let mut policy = crate::policy::VerificationPolicy::default();
5164 policy.resources.max_signatures = 1;
5165 assert!(matches!(
5166 VerifyContext::new().policy(policy).verify_all(&document),
5167 Err(DsigError::Policy(
5168 crate::policy::PolicyViolation::ResourceLimit {
5169 resource: "verification signatures",
5170 maximum: 1,
5171 actual: 2,
5172 }
5173 ))
5174 ));
5175 }
5176
5177 #[test]
5178 fn verification_signature_limit_also_gates_the_single_api() {
5179 let mut policy = crate::policy::VerificationPolicy::default();
5181 policy.resources.max_signatures = 0;
5182 assert!(matches!(
5183 VerifyContext::new()
5184 .policy(policy)
5185 .key(&AcceptingKey)
5186 .verify(&signature_with_target_reference("AQ==")),
5187 Err(DsigError::Policy(
5188 crate::policy::PolicyViolation::ResourceLimit {
5189 resource: "verification signatures",
5190 maximum: 0,
5191 actual: 1,
5192 }
5193 ))
5194 ));
5195 }
5196
5197 #[test]
5198 fn verify_all_preserves_generation_and_rejects_wrapped_siblings() {
5199 let xml = signature_with_target_reference("AQ==")
5201 .replace("</root>", "<target ID=\"unsigned\">payload</target></root>");
5202 let document = XmlDocument::parse(xml).unwrap();
5203 let (signed, unsigned) = document.with_view(|view| {
5204 (
5205 view.node_for_id("target", &[]).unwrap(),
5206 view.node_for_id("unsigned", &[]).unwrap(),
5207 )
5208 });
5209 let evidence = VerifyContext::new()
5210 .key(&AcceptingKey)
5211 .verify_all(&document)
5212 .unwrap();
5213 assert!(evidence.all_valid());
5214 assert!(evidence.covers_element(&document, signed).unwrap());
5215 assert!(!evidence.covers_element(&document, unsigned).unwrap());
5216 let foreign = XmlDocument::parse("<target/>").unwrap();
5217 let target = foreign.with_view(|view| view.root_element());
5218 assert!(matches!(
5219 evidence.covers_element(&document, target),
5220 Err(DsigError::Document(XmlDocumentError::ForeignIdentity))
5221 ));
5222 }
5223
5224 #[test]
5225 fn verify_request_rejects_duplicate_targets_before_crypto() {
5226 let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
5228 let target = document.with_view(|view| view.node_for_id("target", &[]).unwrap());
5229 let targets = [target, target];
5230 let request = VerificationRequest {
5231 expected_targets: &targets,
5232 ..VerificationRequest::default()
5233 };
5234 assert!(matches!(
5235 VerifyContext::new().verify_request(&document, &request),
5236 Err(DsigError::InvalidRequest {
5237 reason: "duplicate expected coverage target"
5238 })
5239 ));
5240 }
5241
5242 #[test]
5243 fn verify_all_shares_canonicalization_retention_budget() {
5244 let xml = signature_with_target_reference("AQ==");
5246 let single = VerifyContext::new()
5247 .key(&AcceptingKey)
5248 .store_pre_digest(true)
5249 .verify(&xml)
5250 .unwrap();
5251 let bytes = single.canonicalized_signed_info.unwrap().len()
5252 + single.signed_info_references[0]
5253 .pre_digest_data
5254 .as_ref()
5255 .unwrap()
5256 .len();
5257 let parsed = Document::parse(&xml).unwrap();
5258 let signature = parsed
5259 .descendants()
5260 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
5261 .unwrap();
5262 let xml = xml.replace("</root>", &format!("{}</root>", &xml[signature.range()]));
5263 let document = XmlDocument::parse(xml).unwrap();
5264 let mut policy = crate::policy::VerificationPolicy::default();
5265 policy.resources.max_canonicalized_bytes = bytes * 2 - 1;
5266 let evidence = VerifyContext::new()
5267 .key(&AcceptingKey)
5268 .store_pre_digest(true)
5269 .policy(policy)
5270 .verify_all(&document)
5271 .unwrap();
5272 assert_eq!(evidence.signatures.len(), 2);
5273 assert_eq!(
5274 evidence.signatures[0].result().as_ref().unwrap().status,
5275 DsigStatus::Valid
5276 );
5277 assert!(matches!(
5278 evidence.signatures[1].result(),
5279 Err(DsigError::Policy(
5280 crate::policy::PolicyViolation::ResourceLimit {
5281 resource: "canonicalized bytes",
5282 ..
5283 }
5284 ))
5285 ));
5286 assert!(!evidence.accepted(&document).unwrap());
5287 }
5288
5289 #[test]
5290 fn verify_all_manifest_coverage_requires_its_authenticated_digest() {
5291 for valid in [true, false] {
5294 let document = XmlDocument::parse(signature_with_manifest_xml(valid)).unwrap();
5295 let target = document.with_view(|view| view.node_for_id("target", &[]).unwrap());
5296 let evidence = VerifyContext::new()
5297 .key(&AcceptingKey)
5298 .process_manifests(true)
5299 .verify_all(&document)
5300 .unwrap();
5301 let result = evidence.signatures()[0].result().as_ref().unwrap();
5302 assert_eq!(result.manifest_references.len(), 1);
5303 assert!(result.manifest_references[0].reference_identity.is_some());
5304 assert_eq!(evidence.covers_element(&document, target).unwrap(), valid);
5305 assert_eq!(evidence.accepted(&document).unwrap(), valid);
5306 }
5307 }
5308
5309 #[test]
5310 fn verify_all_filtered_digest_does_not_claim_original_subtree() {
5311 let mut reference = make_reference(
5314 "#target",
5315 vec![Transform::XpathExcludeAllSignatures],
5316 DigestAlgorithm::Sha256,
5317 vec![],
5318 );
5319 let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
5320 document.with_view(|view| {
5321 let target = view.node_for_id("target", &[]).unwrap();
5322 let mut result = manifest_reference_invalid_result(
5323 &reference,
5324 0,
5325 FailureReason::ReferenceProcessingFailure { ref_index: 0 },
5326 );
5327 result.status = DsigStatus::Valid;
5328 bind_reference_evidence(
5329 &mut result,
5330 &reference,
5331 view,
5332 None,
5333 &OperationResourceIdentity::DocumentNode(target),
5334 );
5335 assert_eq!(result.target_identity, Some(target));
5336 assert_eq!(result.coverage, ReferenceCoverage::TransformedData);
5337 reference.transforms = vec![Transform::C14n(crate::c14n::C14nAlgorithm::new(
5338 crate::c14n::C14nMode::Exclusive1_0,
5339 false,
5340 ))];
5341 bind_reference_evidence(
5342 &mut result,
5343 &reference,
5344 view,
5345 None,
5346 &OperationResourceIdentity::DocumentNode(target),
5347 );
5348 assert_eq!(result.coverage, ReferenceCoverage::ElementContent);
5349 });
5350 }
5351
5352 #[test]
5353 fn verify_request_rejects_conflicting_resource_owners() {
5354 let document = XmlDocument::parse("<root/>").unwrap();
5357 let resources = HashMap::new();
5358 let request = VerificationRequest {
5359 external_resources: Some(&resources),
5360 ..VerificationRequest::default()
5361 };
5362 assert!(matches!(
5363 VerifyContext::new()
5364 .external_resources(&resources)
5365 .verify_request(&document, &request),
5366 Err(DsigError::InvalidRequest {
5367 reason: "external resources configured in both context and request"
5368 })
5369 ));
5370 }
5371
5372 fn signature_with_target_reference(signature_value_b64: &str) -> String {
5373 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5374 <target ID="target">payload</target>
5375 <ds:Signature>
5376 <ds:SignedInfo>
5377 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5378 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5379 <ds:Reference URI="#target">
5380 <ds:Transforms>
5381 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5382 </ds:Transforms>
5383 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
5384 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
5385 </ds:Reference>
5386 </ds:SignedInfo>
5387 <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
5388 </ds:Signature>
5389</root>"##;
5390
5391 let doc = Document::parse(xml_template).unwrap();
5392 let sig_node = doc
5393 .descendants()
5394 .find(|node| node.is_element() && node.tag_name().name() == "Signature")
5395 .unwrap();
5396 let signed_info_node = sig_node
5397 .children()
5398 .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
5399 .unwrap();
5400 let signed_info = parse_signed_info(signed_info_node).unwrap();
5401 let reference = &signed_info.references[0];
5402 let resolver = UriReferenceResolver::new(&doc);
5403 let initial_data = resolver
5404 .dereference(reference.uri.as_deref().unwrap())
5405 .unwrap();
5406 let pre_digest =
5407 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
5408 .unwrap();
5409 let digest = compute_digest(reference.digest_method, &pre_digest);
5410 let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
5411 xml_template
5412 .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
5413 .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
5414 }
5415
5416 #[test]
5417 fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
5418 let xml = signature_with_target_reference("AQ==");
5419
5420 let result = VerifyContext::new()
5421 .verify(&xml)
5422 .expect("missing key config must be reported as verification status");
5423 assert!(
5424 matches!(
5425 result.status,
5426 DsigStatus::Invalid(FailureReason::KeyNotFound)
5427 ),
5428 "unexpected status: {:?}",
5429 result.status
5430 );
5431 }
5432
5433 #[test]
5434 fn verify_context_rejects_disallowed_uri() {
5435 let xml = minimal_signature_xml("http://example.com/external", "");
5436 let err = VerifyContext::new()
5437 .key(&RejectingKey)
5438 .verify(&xml)
5439 .expect_err("external URI should be rejected by default policy");
5440 assert!(matches!(
5441 err,
5442 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
5443 operation: "verification",
5444 ..
5445 })
5446 ));
5447 }
5448
5449 #[test]
5450 fn verify_context_bounds_effective_xml_base_components() {
5451 let mut xml = minimal_signature_xml("payload", "");
5454 for _ in 0..65 {
5455 xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
5456 }
5457 let resources = HashMap::new();
5458 let error = VerifyContext::new()
5459 .key(&AcceptingKey)
5460 .allowed_uri_types(UriTypeSet::ALL)
5461 .external_resources(&resources)
5462 .verify(&xml)
5463 .expect_err("XML Base component work must be bounded before lookup");
5464
5465 assert!(matches!(
5466 error,
5467 SignatureVerificationPipelineError::Policy(
5468 crate::policy::PolicyViolation::ResourceLimit {
5469 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
5470 maximum: 64,
5471 actual: 65,
5472 }
5473 )
5474 ));
5475 }
5476
5477 #[test]
5478 fn verify_context_bounds_cumulative_xml_base_resolution_bytes() {
5479 let mut xml = minimal_signature_xml("payload", "");
5482 for _ in 0..2 {
5483 xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
5484 }
5485 let resources = HashMap::new();
5486 let mut policy = crate::policy::VerificationPolicy::default();
5487 policy.resources.max_xml_base_resolution_bytes = 32;
5488 let error = VerifyContext::new()
5489 .policy(policy)
5490 .key(&AcceptingKey)
5491 .allowed_uri_types(UriTypeSet::ALL)
5492 .external_resources(&resources)
5493 .verify(&xml)
5494 .expect_err("cumulative XML Base copies must obey the operation budget");
5495
5496 assert!(matches!(
5497 error,
5498 SignatureVerificationPipelineError::Policy(
5499 crate::policy::PolicyViolation::ResourceLimit {
5500 resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
5501 maximum: 32,
5502 ..
5503 }
5504 )
5505 ));
5506 }
5507
5508 #[test]
5509 fn verify_context_applies_xml_base_policy_to_signed_info_c14n() {
5510 let xml = signature_with_target_reference("AQ==")
5514 .replacen(
5515 "http://www.w3.org/2001/10/xml-exc-c14n#",
5516 "http://www.w3.org/2006/12/xml-c14n11",
5517 1,
5518 )
5519 .replace(
5520 " <ds:Signature>",
5521 " <outer xml:base=\"one/\"><inner xml:base=\"two/\"><ds:Signature>",
5522 )
5523 .replace(" </ds:Signature>", " </ds:Signature></inner></outer>");
5524 let policy = crate::policy::VerificationPolicy {
5525 resources: crate::policy::ResourcePolicy {
5526 max_xml_base_components: 1,
5527 ..crate::policy::ResourcePolicy::default()
5528 },
5529 ..crate::policy::VerificationPolicy::default()
5530 };
5531
5532 let error = VerifyContext::new()
5533 .key(&AcceptingKey)
5534 .policy(policy)
5535 .verify(&xml)
5536 .expect_err("SignedInfo C14N must use the operation XML Base budget");
5537
5538 assert!(matches!(
5539 error,
5540 SignatureVerificationPipelineError::Policy(
5541 crate::policy::PolicyViolation::ResourceLimit {
5542 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
5543 maximum: 1,
5544 actual: 2,
5545 }
5546 )
5547 ));
5548 }
5549
5550 #[test]
5551 fn verify_context_classifies_signed_info_xml_base_byte_limit_as_policy() {
5552 let xml = signature_with_target_reference("AQ==")
5555 .replacen(
5556 "http://www.w3.org/2001/10/xml-exc-c14n#",
5557 "http://www.w3.org/2006/12/xml-c14n11",
5558 1,
5559 )
5560 .replace(
5561 " <ds:Signature>",
5562 " <outer xml:base=\"segment/\"><ds:Signature>",
5563 )
5564 .replace(" </ds:Signature>", " </ds:Signature></outer>");
5565 let policy = crate::policy::VerificationPolicy {
5566 resources: crate::policy::ResourcePolicy {
5567 max_xml_base_resolution_bytes: 1,
5568 ..crate::policy::ResourcePolicy::default()
5569 },
5570 ..crate::policy::VerificationPolicy::default()
5571 };
5572
5573 let error = VerifyContext::new()
5574 .key(&AcceptingKey)
5575 .policy(policy)
5576 .verify(&xml)
5577 .expect_err("SignedInfo XML Base byte exhaustion must be a policy error");
5578
5579 assert!(matches!(
5580 error,
5581 SignatureVerificationPipelineError::Policy(
5582 crate::policy::PolicyViolation::ResourceLimit {
5583 resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
5584 maximum: 1,
5585 actual,
5586 }
5587 ) if actual > 1
5588 ));
5589 }
5590
5591 #[test]
5592 fn verify_context_meters_repeated_external_dereferences() {
5593 let payload = b"payload";
5596 let digest = base64::engine::general_purpose::STANDARD.encode(
5597 crate::xmldsig::compute_digest(DigestAlgorithm::Sha1, payload),
5598 );
5599 let reference = format!(
5600 r#"<ds:Reference URI="urn:payload"><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference>"#
5601 );
5602 let xml = format!(
5603 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>"#
5604 );
5605 let resources = HashMap::from([("urn:payload".to_owned(), payload.to_vec())]);
5606 let policy = crate::policy::VerificationPolicy {
5607 uris: crate::policy::UriPolicy {
5608 references: UriTypeSet::ALL,
5609 ..crate::policy::UriPolicy::default()
5610 },
5611 resources: crate::policy::ResourcePolicy {
5612 max_external_resource_bytes: payload.len(),
5613 max_external_resource_total_bytes: payload.len(),
5614 ..crate::policy::ResourcePolicy::default()
5615 },
5616 ..crate::policy::VerificationPolicy::default()
5617 };
5618
5619 let error = VerifyContext::new()
5620 .key(&AcceptingKey)
5621 .policy(policy)
5622 .external_resources(&resources)
5623 .verify(&xml)
5624 .expect_err("the second dereference must exhaust the aggregate byte ceiling");
5625
5626 assert!(
5627 error
5628 .to_string()
5629 .contains("aggregate external resource bytes")
5630 );
5631 }
5632
5633 #[test]
5634 fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
5635 let xml = minimal_signature_xml("", "");
5636 let err = VerifyContext::new()
5637 .key(&RejectingKey)
5638 .allowed_uri_types(UriTypeSet::new(false, true, false))
5639 .verify(&xml)
5640 .expect_err("empty URI must be rejected when empty references are disabled");
5641 assert!(matches!(
5642 err,
5643 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
5644 operation: "verification",
5645 ..
5646 })
5647 ));
5648 }
5649
5650 #[test]
5651 fn verify_context_rejects_disallowed_transform() {
5652 let xml = minimal_signature_xml(
5653 "",
5654 r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
5655 );
5656 let err = VerifyContext::new()
5657 .key(&RejectingKey)
5658 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
5659 .verify(&xml)
5660 .expect_err("enveloped transform should be rejected by allowlist");
5661 assert!(matches!(
5662 err,
5663 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
5664 operation: "verification transform",
5665 ..
5666 })
5667 ));
5668 }
5669
5670 #[test]
5671 fn verify_context_applies_transform_allowlist_to_signed_info_c14n() {
5672 let xml = signature_with_target_reference("AQ==").replacen(
5675 "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
5676 "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"/>",
5677 1,
5678 );
5679 let error = VerifyContext::new()
5680 .key(&AcceptingKey)
5681 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
5682 .verify(&xml)
5683 .expect_err("SignedInfo C14N must obey the operation transform allowlist");
5684
5685 assert!(matches!(
5686 error,
5687 SignatureVerificationPipelineError::Policy(
5688 crate::policy::PolicyViolation::Algorithm {
5689 operation: "verification transform",
5690 ref algorithm,
5691 }
5692 )
5693 if algorithm == "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
5694 ));
5695 }
5696
5697 #[test]
5698 fn verify_context_applies_transform_allowlist_to_key_retrieval() {
5699 let xml = signature_with_target_reference("AQ==")
5702 .replacen(
5703 "</ds:Signature>",
5704 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>"##,
5705 1,
5706 )
5707 .replacen(
5708 "</root>",
5709 r#"<holder ID="keys"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder></root>"#,
5710 1,
5711 );
5712 let error = VerifyContext::new()
5713 .key_resolver(&ConsumingKeyInfoResolver)
5714 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
5715 .verify(&xml)
5716 .expect_err("RetrievalMethod XPath must obey the operation transform allowlist");
5717
5718 assert!(matches!(
5719 error,
5720 SignatureVerificationPipelineError::Policy(
5721 crate::policy::PolicyViolation::Algorithm {
5722 operation: "verification transform",
5723 ref algorithm,
5724 }
5725 )
5726 if algorithm == XPATH_TRANSFORM_URI
5727 ));
5728 }
5729
5730 fn signature_with_manifest_xml(valid_manifest_digest: bool) -> String {
5731 signature_with_manifest_xml_with_manifest_mutation(valid_manifest_digest, |xml| xml)
5732 }
5733
5734 fn signature_with_manifest_xml_with_manifest_mutation<F>(
5735 valid_manifest_digest: bool,
5736 mutate_manifest: F,
5737 ) -> String
5738 where
5739 F: FnOnce(String) -> String,
5740 {
5741 const TMP_SIGNED_INFO_DIGEST: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
5742 const INVALID_MANIFEST_DIGEST: &str = "//////////////////////////8=";
5743 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5744 <target ID="target">payload</target>
5745 <ds:Signature>
5746 <ds:SignedInfo>
5747 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5748 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5749 <ds:Reference URI="#manifest">
5750 <ds:Transforms>
5751 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5752 </ds:Transforms>
5753 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
5754 <ds:DigestValue>SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER</ds:DigestValue>
5755 </ds:Reference>
5756 </ds:SignedInfo>
5757 <ds:SignatureValue>AQ==</ds:SignatureValue>
5758 <ds:Object>
5759 <ds:Manifest ID="manifest">
5760 <ds:Reference URI="#target">
5761 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
5762 <ds:DigestValue>MANIFEST_DIGEST_PLACEHOLDER</ds:DigestValue>
5763 </ds:Reference>
5764 </ds:Manifest>
5765 </ds:Object>
5766 </ds:Signature>
5767</root>"##;
5768 let seed_xml = xml_template.replace(
5769 "SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER",
5770 TMP_SIGNED_INFO_DIGEST,
5771 );
5772 let doc = Document::parse(&seed_xml).unwrap();
5773 let signature_node = doc
5774 .descendants()
5775 .find(|node| {
5776 node.is_element()
5777 && node.tag_name().namespace() == Some(XMLDSIG_NS)
5778 && node.tag_name().name() == "Signature"
5779 })
5780 .unwrap();
5781 let resolver = UriReferenceResolver::new(&doc);
5782 let initial_data = resolver.dereference("#target").unwrap();
5783 let manifest_pre_digest =
5784 crate::xmldsig::execute_transforms(signature_node, initial_data, &[]).unwrap();
5785 let computed_manifest_digest_b64 = base64::engine::general_purpose::STANDARD
5786 .encode(compute_digest(DigestAlgorithm::Sha1, &manifest_pre_digest));
5787 let final_manifest_digest_b64 = if valid_manifest_digest {
5788 computed_manifest_digest_b64.as_str()
5789 } else {
5790 INVALID_MANIFEST_DIGEST
5791 };
5792 let xml_with_manifest_digest = mutate_manifest(
5793 seed_xml.replace("MANIFEST_DIGEST_PLACEHOLDER", final_manifest_digest_b64),
5794 );
5795 let signed_doc = Document::parse(&xml_with_manifest_digest).unwrap();
5796 let signed_signature_node = signed_doc
5797 .descendants()
5798 .find(|node| {
5799 node.is_element()
5800 && node.tag_name().namespace() == Some(XMLDSIG_NS)
5801 && node.tag_name().name() == "Signature"
5802 })
5803 .unwrap();
5804 let signed_info_node = signed_signature_node
5805 .children()
5806 .find(|node| {
5807 node.is_element()
5808 && node.tag_name().namespace() == Some(XMLDSIG_NS)
5809 && node.tag_name().name() == "SignedInfo"
5810 })
5811 .unwrap();
5812 let signed_info = parse_signed_info(signed_info_node).unwrap();
5813 let object_reference = &signed_info.references[0];
5814 let signed_resolver = UriReferenceResolver::new(&signed_doc);
5815 let signed_initial_data = signed_resolver
5816 .dereference(object_reference.uri.as_deref().unwrap())
5817 .unwrap();
5818 let signed_pre_digest = crate::xmldsig::execute_transforms(
5819 signed_signature_node,
5820 signed_initial_data,
5821 &object_reference.transforms,
5822 )
5823 .unwrap();
5824 let signed_digest_b64 = base64::engine::general_purpose::STANDARD.encode(compute_digest(
5825 object_reference.digest_method,
5826 &signed_pre_digest,
5827 ));
5828
5829 xml_with_manifest_digest.replacen(TMP_SIGNED_INFO_DIGEST, &signed_digest_b64, 1)
5830 }
5831
5832 fn replace_fixture_manifest_digest(xml: &str, replacement: &str) -> String {
5833 let object_marker = "<ds:Object>";
5834 let object_start = xml
5835 .find(object_marker)
5836 .expect("fixture should contain ds:Object")
5837 + object_marker.len();
5838 let open = "<ds:DigestValue>";
5839 let close = "</ds:DigestValue>";
5840 let value_start = xml[object_start..]
5841 .find(open)
5842 .map(|offset| object_start + offset + open.len())
5843 .expect("Manifest should contain DigestValue");
5844 let value_end = xml[value_start..]
5845 .find(close)
5846 .map(|offset| value_start + offset)
5847 .expect("Manifest DigestValue must be closed");
5848
5849 format!("{}{replacement}{}", &xml[..value_start], &xml[value_end..])
5850 }
5851
5852 #[test]
5853 fn verify_context_processes_manifest_references_when_enabled() {
5854 let xml = signature_with_manifest_xml(true);
5855
5856 let result_without_manifests = VerifyContext::new()
5857 .key(&RejectingKey)
5858 .verify(&xml)
5859 .expect("manifest processing disabled should still verify SignedInfo");
5860 assert!(
5861 result_without_manifests.manifest_references.is_empty(),
5862 "manifest results must stay empty when manifest processing is disabled",
5863 );
5864 assert!(matches!(
5865 result_without_manifests.status,
5866 DsigStatus::Invalid(FailureReason::SignatureMismatch)
5867 ));
5868
5869 let malformed_manifest_xml = signature_with_manifest_xml(true).replacen(
5870 "</ds:Object>",
5871 "</ds:Object><ds:Object><ds:Manifest><ds:Foo/></ds:Manifest></ds:Object>",
5872 1,
5873 );
5874 let malformed_with_manifests_disabled = VerifyContext::new()
5875 .key(&RejectingKey)
5876 .verify(&malformed_manifest_xml)
5877 .expect("malformed Manifest must be ignored when manifest processing is disabled");
5878 assert!(
5879 malformed_with_manifests_disabled
5880 .manifest_references
5881 .is_empty(),
5882 "manifest parser must not run when process_manifests is disabled",
5883 );
5884 assert!(matches!(
5885 malformed_with_manifests_disabled.status,
5886 DsigStatus::Invalid(FailureReason::SignatureMismatch)
5887 ));
5888
5889 let result_with_manifests = VerifyContext::new()
5890 .key(&AcceptingKey)
5891 .process_manifests(true)
5892 .verify(&xml)
5893 .expect("manifest references should be processed when enabled");
5894 assert_eq!(result_with_manifests.manifest_references.len(), 1);
5895 assert_eq!(
5896 result_with_manifests.manifest_references[0].reference_set,
5897 ReferenceSet::Manifest
5898 );
5899 assert_eq!(
5900 result_with_manifests.manifest_references[0].reference_index,
5901 0
5902 );
5903 assert!(matches!(
5904 result_with_manifests.manifest_references[0].status,
5905 DsigStatus::Valid
5906 ));
5907 assert!(matches!(result_with_manifests.status, DsigStatus::Valid));
5908 }
5909
5910 #[test]
5911 fn verify_context_skips_manifest_work_when_signature_value_is_invalid() {
5912 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5916 replace_fixture_manifest_digest(&xml, "!!!")
5917 });
5918 assert!(
5919 xml.split_once("<ds:Object>")
5920 .is_some_and(|(_, object)| object.contains("<ds:DigestValue>!!!</ds:DigestValue>")),
5921 "fixture mutation must corrupt the nested Manifest DigestValue",
5922 );
5923
5924 let result = VerifyContext::new()
5925 .key(&RejectingKey)
5926 .process_manifests(true)
5927 .verify(&xml)
5928 .expect("invalid SignatureValue must short-circuit Manifest parsing");
5929
5930 assert!(matches!(
5931 result.status,
5932 DsigStatus::Invalid(FailureReason::SignatureMismatch)
5933 ));
5934 assert!(result.manifest_references.is_empty());
5935 }
5936
5937 #[test]
5938 fn verify_context_shares_xpath_parse_budget_with_manifest_references() {
5939 let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
5942 .repeat(64);
5943 let transform = format!(
5944 r#"<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</ds:Transform>"#
5945 );
5946 let max_transforms = transform.repeat(16);
5947 let max_manifest_reference = format!(
5948 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>"##
5949 );
5950 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5951 xml.replacen(
5952 r##"<ds:Reference URI="#target">"##,
5953 &format!(
5954 r##"<ds:Reference URI="#target"><ds:Transforms>{}</ds:Transforms>"##,
5955 max_transforms
5956 ),
5957 1,
5958 )
5959 .replacen(
5960 "</ds:SignedInfo>",
5961 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>"##,
5962 1,
5963 )
5964 .replacen(
5965 "</ds:Manifest>",
5966 &format!("{}</ds:Manifest>", max_manifest_reference.repeat(3)),
5967 1,
5968 )
5969 });
5970
5971 let error = VerifyContext::new()
5972 .key(&AcceptingKey)
5973 .process_manifests(true)
5974 .verify(&xml)
5975 .expect_err("SignedInfo and Manifest References must share one XPath parse budget");
5976
5977 assert!(
5978 matches!(
5979 &error,
5980 SignatureVerificationPipelineError::Policy(
5981 crate::policy::PolicyViolation::ResourceLimit {
5982 resource: "XPath expressions",
5983 ..
5984 }
5985 )
5986 ),
5987 "unexpected error: {error:?}"
5988 );
5989 }
5990
5991 #[test]
5992 fn verify_context_processes_manifest_when_signedinfo_references_object() {
5993 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5994 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
5995 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
5996 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
5997 });
5998
5999 let result = VerifyContext::new()
6000 .key(&AcceptingKey)
6001 .process_manifests(true)
6002 .verify(&xml)
6003 .expect("manifest references should be processed when SignedInfo references ds:Object");
6004 assert_eq!(
6005 result.manifest_references.len(),
6006 1,
6007 "signed ds:Object should enable processing of its direct-child ds:Manifest",
6008 );
6009 assert_eq!(
6010 result.manifest_references[0].reference_set,
6011 ReferenceSet::Manifest
6012 );
6013 assert_eq!(result.manifest_references[0].reference_index, 0);
6014 assert!(matches!(
6015 result.manifest_references[0].status,
6016 DsigStatus::Valid
6017 ));
6018 }
6019
6020 #[test]
6021 fn verify_context_skips_manifest_removed_by_enveloped_transform() {
6022 for target_object in [false, true] {
6026 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6027 let xml = xml.replacen(
6028 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
6029 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#"/>"#,
6030 1,
6031 );
6032 if target_object {
6033 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
6034 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
6035 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
6036 } else {
6037 xml
6038 }
6039 });
6040
6041 let result = VerifyContext::new()
6042 .key(&AcceptingKey)
6043 .process_manifests(true)
6044 .store_pre_digest(true)
6045 .verify(&xml)
6046 .expect("an emptied reference remains a valid core digest input");
6047
6048 assert!(matches!(result.status, DsigStatus::Valid));
6049 assert_eq!(
6050 result.signed_info_references[0].pre_digest_data.as_deref(),
6051 Some([].as_slice()),
6052 "target_object={target_object} must have empty transformed bytes",
6053 );
6054 assert!(
6055 result.manifest_references.is_empty(),
6056 "target_object={target_object} must not authenticate the Manifest",
6057 );
6058 }
6059 }
6060
6061 #[test]
6062 fn verify_context_ignores_manifest_excluded_from_signed_object() {
6063 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6067 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
6068 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
6069 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
6070 .replacen(
6071 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
6072 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#"/>"#,
6073 1,
6074 )
6075 });
6076
6077 let result = VerifyContext::new()
6078 .key(&AcceptingKey)
6079 .process_manifests(true)
6080 .verify(&xml)
6081 .expect("excluded Manifest content must be ignored, not parsed");
6082
6083 assert!(matches!(result.status, DsigStatus::Valid));
6084 assert!(
6085 result.manifest_references.is_empty(),
6086 "a transform-excluded Manifest is not authenticated by SignedInfo",
6087 );
6088 }
6089
6090 #[test]
6091 fn verify_context_skips_manifest_digest_work_when_signature_is_invalid() {
6092 let xml = signature_with_manifest_xml(false);
6093 let result = VerifyContext::new()
6094 .key(&RejectingKey)
6095 .process_manifests(true)
6096 .verify(&xml)
6097 .expect("invalid SignatureValue must short-circuit Manifest digest work");
6098 assert!(result.manifest_references.is_empty());
6099 assert!(matches!(
6100 result.status,
6101 DsigStatus::Invalid(FailureReason::SignatureMismatch)
6102 ));
6103 }
6104
6105 #[test]
6106 fn verify_context_manifest_digest_mismatch_is_non_fatal_with_accepting_key() {
6107 let xml = signature_with_manifest_xml(false);
6108 let result = VerifyContext::new()
6109 .key(&AcceptingKey)
6110 .process_manifests(true)
6111 .verify(&xml)
6112 .expect("manifest digest mismatches should be recorded while signature stays valid");
6113 assert_eq!(result.manifest_references.len(), 1);
6114 assert!(matches!(
6115 result.manifest_references[0].status,
6116 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
6117 ));
6118 assert!(matches!(result.status, DsigStatus::Valid));
6119 }
6120
6121 #[test]
6122 fn invalid_outer_manifest_digest_does_not_parse_nested_manifest() {
6123 let xml = signature_with_manifest_xml_with_manifest_mutation(false, |xml| {
6127 xml.replacen("URI=\"#target\"", "URI=\"#inner-object\"", 1)
6128 .replacen(
6129 "</ds:Object>",
6130 "</ds:Object><ds:Object ID=\"inner-object\"><ds:Manifest>junk<ds:Unexpected/></ds:Manifest></ds:Object>",
6131 1,
6132 )
6133 });
6134
6135 let result = VerifyContext::new()
6136 .key(&AcceptingKey)
6137 .process_manifests(true)
6138 .verify(&xml)
6139 .expect("an unauthenticated nested Manifest must remain opaque");
6140
6141 assert_eq!(result.manifest_references.len(), 1);
6142 assert!(matches!(
6143 result.manifest_references[0].status,
6144 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
6145 ));
6146 }
6147
6148 #[test]
6149 fn verify_context_skips_manifest_parsing_when_signedinfo_reference_fails() {
6150 let xml = signature_with_manifest_xml(true);
6153 let (signed_info_prefix, object_suffix) = xml
6154 .split_once("<ds:Object>")
6155 .expect("fixture should contain ds:Object");
6156 let open = "<ds:DigestValue>";
6157 let close = "</ds:DigestValue>";
6158 let digest_start = signed_info_prefix
6159 .find(open)
6160 .expect("SignedInfo should contain DigestValue");
6161 let digest_end = signed_info_prefix[digest_start + open.len()..]
6162 .find(close)
6163 .map(|offset| digest_start + open.len() + offset)
6164 .expect("SignedInfo DigestValue must be closed");
6165 let broken_signed_info_prefix = format!(
6166 "{}{}AAAAAAAAAAAAAAAAAAAAAAAAAAA={}{}",
6167 &signed_info_prefix[..digest_start],
6168 open,
6169 close,
6170 &signed_info_prefix[digest_end + close.len()..],
6171 );
6172 let broken_xml = format!("{broken_signed_info_prefix}<ds:Object>{object_suffix}");
6173 let result = VerifyContext::new()
6174 .key(&RejectingKey)
6175 .process_manifests(true)
6176 .verify(&broken_xml)
6177 .expect("SignedInfo digest failure should return without parsing Manifests");
6178 assert!(matches!(
6179 result.status,
6180 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
6181 ));
6182 assert!(
6183 result.manifest_references.is_empty(),
6184 "unauthenticated Manifest content must not be parsed",
6185 );
6186 }
6187
6188 #[test]
6189 fn verify_context_skips_manifest_policy_work_when_signature_is_invalid() {
6190 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6193 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
6194 });
6195 let result = VerifyContext::new()
6196 .key(&RejectingKey)
6197 .process_manifests(true)
6198 .verify(&broken_xml)
6199 .expect("invalid SignatureValue must short-circuit Manifest policy work");
6200 assert!(result.manifest_references.is_empty());
6201 assert!(matches!(
6202 result.status,
6203 DsigStatus::Invalid(FailureReason::SignatureMismatch)
6204 ));
6205 }
6206
6207 #[test]
6208 fn verify_context_records_manifest_policy_violations_with_accepting_key() {
6209 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6210 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
6211 });
6212 let result = VerifyContext::new()
6213 .key(&AcceptingKey)
6214 .process_manifests(true)
6215 .verify(&broken_xml)
6216 .expect("manifest policy violations should be recorded while signature stays valid");
6217 assert_eq!(result.manifest_references.len(), 1);
6218 assert!(matches!(
6219 result.manifest_references[0].status,
6220 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
6221 ));
6222 assert!(matches!(result.status, DsigStatus::Valid));
6223 }
6224
6225 #[test]
6226 fn verify_context_applies_digest_policy_to_manifest_references() {
6227 let policy = crate::policy::VerificationPolicy {
6230 manifest_processing: crate::policy::ManifestProcessing::Process,
6231 digest_algorithms: Some(HashSet::from([DigestAlgorithm::Sha1])),
6232 ..crate::policy::VerificationPolicy::default()
6233 };
6234 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
6235 let legacy = "http://www.w3.org/2000/09/xmldsig#sha1";
6236 let offset = xml
6237 .rfind(legacy)
6238 .expect("Manifest DigestMethod must be present");
6239 xml.replace_range(offset..offset + legacy.len(), DigestAlgorithm::Sha256.uri());
6240 let value_start = xml[offset..]
6241 .find("<ds:DigestValue>")
6242 .map(|relative| offset + relative + "<ds:DigestValue>".len())
6243 .expect("Manifest DigestValue must be present");
6244 let value_end = xml[value_start..]
6245 .find("</ds:DigestValue>")
6246 .map(|relative| value_start + relative)
6247 .expect("Manifest DigestValue must be closed");
6248 xml.replace_range(
6249 value_start..value_end,
6250 &base64::engine::general_purpose::STANDARD.encode([0_u8; 32]),
6251 );
6252 xml
6253 });
6254 let result = VerifyContext::new()
6255 .key(&AcceptingKey)
6256 .policy(policy)
6257 .verify(&xml)
6258 .expect("a disallowed Manifest digest is a per-reference result");
6259
6260 assert!(matches!(result.status, DsigStatus::Valid));
6261 assert!(matches!(
6262 result.manifest_references[0].status,
6263 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
6264 ));
6265 }
6266
6267 #[test]
6268 fn verify_context_applies_transform_count_policy_to_manifest_references() {
6269 let policy = crate::policy::VerificationPolicy {
6272 manifest_processing: crate::policy::ManifestProcessing::Process,
6273 resources: crate::policy::ResourcePolicy {
6274 max_transforms_per_reference: 1,
6275 ..crate::policy::ResourcePolicy::default()
6276 },
6277 ..crate::policy::VerificationPolicy::default()
6278 };
6279 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
6280 let manifest_start = xml
6281 .find("<ds:Manifest")
6282 .expect("fixture must contain a Manifest");
6283 let manifest = xml[manifest_start..].replacen(
6284 "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
6285 concat!(
6286 "<ds:Transforms>",
6287 "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
6288 "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
6289 "</ds:Transforms>",
6290 "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>"
6291 ),
6292 1,
6293 );
6294 xml.replace_range(manifest_start.., &manifest);
6295 xml
6296 });
6297 let result = VerifyContext::new()
6298 .key(&AcceptingKey)
6299 .policy(policy)
6300 .verify(&xml)
6301 .expect("Manifest transform policy is a per-reference result");
6302
6303 assert!(matches!(result.status, DsigStatus::Valid));
6304 assert!(matches!(
6305 result.manifest_references[0].status,
6306 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
6307 ));
6308 }
6309
6310 #[test]
6311 fn verify_context_skips_manifest_uri_work_when_signature_is_invalid() {
6312 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6315 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
6316 });
6317
6318 let result = VerifyContext::new()
6319 .key(&RejectingKey)
6320 .process_manifests(true)
6321 .verify(&broken_xml)
6322 .expect("invalid SignatureValue must short-circuit Manifest URI processing");
6323 assert!(result.manifest_references.is_empty());
6324 assert!(matches!(
6325 result.status,
6326 DsigStatus::Invalid(FailureReason::SignatureMismatch)
6327 ));
6328 }
6329
6330 #[test]
6331 fn verify_context_records_manifest_missing_uri_with_accepting_key() {
6332 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6333 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
6334 });
6335
6336 let result = VerifyContext::new()
6337 .key(&AcceptingKey)
6338 .process_manifests(true)
6339 .verify(&broken_xml)
6340 .expect("manifest missing URI should be recorded while signature stays valid");
6341 assert_eq!(result.manifest_references.len(), 1);
6342 assert_eq!(result.manifest_references[0].uri, "<omitted>");
6343 assert!(matches!(
6344 result.manifest_references[0].status,
6345 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
6346 ));
6347 assert!(matches!(result.status, DsigStatus::Valid));
6348 }
6349
6350 #[test]
6351 fn verify_context_ignores_nested_manifests_in_object() {
6352 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6355 xml.replacen(
6356 "<ds:Manifest ID=\"manifest\">",
6357 "<wrapper><ds:Manifest ID=\"manifest\">",
6358 1,
6359 )
6360 .replacen("</ds:Manifest>", "</ds:Manifest></wrapper>", 1)
6361 });
6362
6363 let result = VerifyContext::new()
6364 .key(&AcceptingKey)
6365 .process_manifests(true)
6366 .verify(&xml)
6367 .expect("nested Manifest nodes are ignored in strict mode");
6368 assert!(
6369 result.manifest_references.is_empty(),
6370 "only direct ds:Manifest children of ds:Object must be processed"
6371 );
6372 assert!(matches!(result.status, DsigStatus::Valid));
6373 }
6374
6375 #[test]
6376 fn verify_context_reports_manifest_reference_parse_errors_explicitly() {
6377 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6380 replace_fixture_manifest_digest(&xml, "!!!")
6381 });
6382
6383 let err = VerifyContext::new()
6384 .key(&AcceptingKey)
6385 .process_manifests(true)
6386 .verify(&broken_xml)
6387 .expect_err("invalid Manifest DigestValue must map to ParseManifestReference");
6388 assert!(matches!(
6389 err,
6390 SignatureVerificationPipelineError::ParseManifestReference(_)
6391 ));
6392 }
6393
6394 #[test]
6395 fn verify_context_reports_unsupported_manifest_transform_with_declared_digest() {
6396 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6399 let xml = xml.replacen(
6400 "<ds:Reference URI=\"#target\">",
6401 "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
6402 1,
6403 );
6404 let xml = xml.replacen(
6405 "</ds:Transforms>\n <ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
6406 "</ds:Transforms>\n <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>",
6407 1,
6408 );
6409 replace_fixture_manifest_digest(&xml, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
6410 });
6411 assert!(xml.contains("urn:unsupported"));
6412 assert!(xml.contains("http://www.w3.org/2001/04/xmlenc#sha256"));
6413
6414 let result = VerifyContext::new()
6415 .key(&AcceptingKey)
6416 .process_manifests(true)
6417 .verify(&xml)
6418 .expect("unsupported Manifest transform is a per-reference result");
6419 assert_eq!(result.status, DsigStatus::Valid);
6420 assert_eq!(result.manifest_references.len(), 1);
6421 assert_eq!(
6422 result.manifest_references[0].digest_algorithm,
6423 DigestAlgorithm::Sha256
6424 );
6425 assert!(matches!(
6426 result.manifest_references[0].status,
6427 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
6428 ));
6429
6430 let restricted = VerifyContext::new()
6431 .key(&AcceptingKey)
6432 .process_manifests(true)
6433 .allowed_transforms([
6434 DEFAULT_IMPLICIT_C14N_URI,
6435 "http://www.w3.org/2001/10/xml-exc-c14n#",
6436 ])
6437 .verify(&xml)
6438 .expect("a disallowed Manifest transform is a per-reference policy result");
6439 assert!(matches!(
6440 restricted.manifest_references[0].status,
6441 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
6442 ));
6443 }
6444
6445 #[test]
6446 fn manifest_reference_limit_counts_unsupported_entries() {
6447 let references = (0..=MAX_REFERENCES_PER_SIGNATURE)
6448 .map(|index| {
6449 format!(
6450 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>"##
6451 )
6452 })
6453 .collect::<String>();
6454 let xml = format!(
6455 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>"#
6456 );
6457 let document = XmlDocument::parse(xml).unwrap();
6458 let error = document.with_view(|view| {
6459 let signature = view.document().root_element();
6460 let object = signature.children().find(|node| node.is_element()).unwrap();
6461 let policy = crate::policy::VerificationPolicy::default();
6462 let budgets = VerificationOperationBudgets::with_transforms(
6463 &policy,
6464 TransformExecutionBudget::from_resources(&policy.resources),
6465 );
6466 let operation = OperationExecutionContext::new(
6467 &policy,
6468 &budgets,
6469 Some((view.identity(), view.generation())),
6470 );
6471 operation.authenticate(view.node_identity(object));
6472 let mut processed = HashSet::new();
6473 let mut remaining = MAX_REFERENCES_PER_SIGNATURE;
6474 let mut next_index = 0;
6475 let mut xpath_parse = XPathSignatureParseBudget::default();
6476 let mut state = ManifestDiscoveryState {
6477 processed: &mut processed,
6478 remaining_capacity: &mut remaining,
6479 next_reference_index: &mut next_index,
6480 xpath_parse: &mut xpath_parse,
6481 };
6482 match parse_manifest_references(signature, &operation, view, &mut state, None) {
6483 Ok(_) => panic!("unsupported references must consume the same aggregate limit"),
6484 Err(error) => error,
6485 }
6486 });
6487 assert!(matches!(
6488 error,
6489 SignatureVerificationPipelineError::InvalidStructure {
6490 reason: "signed Manifests exceed the per-signature Reference limit"
6491 }
6492 ));
6493 }
6494
6495 #[test]
6496 fn unsigned_manifest_remains_eligible_after_trust_expands() {
6497 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
6501 let xml = format!(
6502 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>"##
6503 );
6504 let document = XmlDocument::parse(xml).expect("nested Manifest fixture must parse");
6505 document.with_view(|view| {
6506 let signature = view.document().root_element();
6507 let mut objects = signature.children().filter(|node| node.is_element());
6508 let outer = objects.next().expect("outer Object");
6509 let inner = objects.next().expect("inner Object");
6510 let policy = crate::policy::VerificationPolicy::default();
6511 let budgets = VerificationOperationBudgets::with_transforms(
6512 &policy,
6513 TransformExecutionBudget::from_resources(&policy.resources),
6514 );
6515 let operation = OperationExecutionContext::new(
6516 &policy,
6517 &budgets,
6518 Some((view.identity(), view.generation())),
6519 );
6520 operation.authenticate(view.node_identity(outer));
6521 let mut processed = HashSet::new();
6522 let mut remaining = 2;
6523 let mut next_index = 0;
6524 let mut xpath_budget = XPathSignatureParseBudget::default();
6525
6526 let first = parse_manifest_references(
6527 signature,
6528 &operation,
6529 view,
6530 &mut ManifestDiscoveryState {
6531 processed: &mut processed,
6532 remaining_capacity: &mut remaining,
6533 next_reference_index: &mut next_index,
6534 xpath_parse: &mut xpath_budget,
6535 },
6536 None,
6537 )
6538 .expect("outer Manifest must be discovered");
6539 assert_eq!(first.references.len(), 1);
6540 assert_eq!(first.references[0].reference.uri.as_deref(), Some("#inner"));
6541
6542 operation.authenticate(view.node_identity(inner));
6543 let second = parse_manifest_references(
6544 signature,
6545 &operation,
6546 view,
6547 &mut ManifestDiscoveryState {
6548 processed: &mut processed,
6549 remaining_capacity: &mut remaining,
6550 next_reference_index: &mut next_index,
6551 xpath_parse: &mut xpath_budget,
6552 },
6553 None,
6554 )
6555 .expect("newly authenticated sibling Manifest must remain eligible");
6556 assert_eq!(second.references.len(), 1);
6557 assert_eq!(
6558 second.references[0].reference.uri.as_deref(),
6559 Some("#payload")
6560 );
6561 });
6562 }
6563
6564 #[test]
6565 fn manifest_reference_limit_includes_signed_info_references() {
6566 let xml = signature_with_manifest_xml(true);
6569 let reference_start = xml
6570 .find(r##"<ds:Reference URI="#manifest">"##)
6571 .expect("fixture SignedInfo must reference the Manifest");
6572 let reference_end = xml[reference_start..]
6573 .find("</ds:Reference>")
6574 .map(|offset| reference_start + offset + "</ds:Reference>".len())
6575 .expect("fixture SignedInfo Reference must be closed");
6576 let repeated = xml[reference_start..reference_end].repeat(MAX_REFERENCES_PER_SIGNATURE);
6577 let xml = format!(
6578 "{}{repeated}{}",
6579 &xml[..reference_start],
6580 &xml[reference_end..]
6581 );
6582
6583 let error = VerifyContext::new()
6584 .key(&AcceptingKey)
6585 .process_manifests(true)
6586 .verify(&xml)
6587 .expect_err("one Manifest Reference must exceed the exhausted signature-wide limit");
6588
6589 assert!(matches!(
6590 error,
6591 SignatureVerificationPipelineError::InvalidStructure {
6592 reason: "signed Manifests exceed the per-signature Reference limit"
6593 }
6594 ));
6595 }
6596
6597 #[test]
6598 fn configured_reference_limit_is_shared_with_manifests() {
6599 let policy = crate::policy::VerificationPolicy {
6602 manifest_processing: crate::policy::ManifestProcessing::Process,
6603 resources: crate::policy::ResourcePolicy {
6604 max_references: 1,
6605 ..crate::policy::ResourcePolicy::default()
6606 },
6607 ..crate::policy::VerificationPolicy::default()
6608 };
6609
6610 let error = VerifyContext::new()
6611 .key(&AcceptingKey)
6612 .policy(policy)
6613 .verify(&signature_with_manifest_xml(true))
6614 .expect_err("Manifest must exceed the caller-selected aggregate limit");
6615 assert!(matches!(
6616 error,
6617 SignatureVerificationPipelineError::InvalidStructure {
6618 reason: "signed Manifests exceed the per-signature Reference limit"
6619 }
6620 ));
6621 }
6622
6623 #[test]
6624 fn retrieval_method_materializes_single_x509_data_subtree() {
6625 for uri in [
6626 "#target",
6627 "#xpointer(id('target'))",
6628 "#xpointer(id("target"))",
6629 ] {
6630 for target_xml in [
6631 r#"<ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>"#,
6632 r#"<holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>"#,
6633 ] {
6634 let xml = format!(
6635 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>"#
6636 );
6637 let document = Document::parse(&xml).unwrap();
6638 let key_info_node = document
6639 .descendants()
6640 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
6641 .unwrap();
6642 let mut key_info = parse_key_info(key_info_node).unwrap();
6643 let resolver = UriReferenceResolver::new(&document);
6644
6645 materialize_retrieval_methods(
6646 &mut key_info,
6647 &resolver,
6648 UriTypeSet::SAME_DOCUMENT,
6649 None,
6650 crate::provider::default_provider(),
6651 )
6652 .expect("XPath filter must produce one X509Data-rooted node-set");
6653 assert!(matches!(
6654 key_info.sources.as_slice(),
6655 [super::super::parse::KeyInfoSource::X509Data(info)]
6656 if info.subject_names == ["CN=leaf"]
6657 ));
6658 }
6659 }
6660 }
6661
6662 fn retrieval_method_xpath_signature() -> String {
6663 format!(
6664 r##"<root xmlns:ds="{XMLDSIG_NS}">
6665 <payload Id="payload">ok</payload>
6666 <ds:Signature>
6667 <ds:SignedInfo>
6668 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6669 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6670 <ds:Reference URI="#payload">
6671 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6672 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6673 </ds:Reference>
6674 </ds:SignedInfo>
6675 <ds:SignatureValue>AQ==</ds:SignatureValue>
6676 <ds:KeyInfo>
6677 <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
6678 <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
6679 <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
6680 </ds:Transform></ds:Transforms>
6681 </ds:RetrievalMethod>
6682 </ds:KeyInfo>
6683 </ds:Signature>
6684 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
6685 </root>"##
6686 )
6687 }
6688
6689 #[test]
6690 fn retrieval_method_xpath_uses_signature_expression_budget() {
6691 let mut policy = crate::policy::VerificationPolicy::default();
6694 policy.resources.max_xpath_expressions = 0;
6695
6696 let error = VerifyContext::new()
6697 .policy(policy)
6698 .verify(&retrieval_method_xpath_signature())
6699 .expect_err("RetrievalMethod XPath must consume the signature parse budget");
6700
6701 assert!(
6702 matches!(
6703 &error,
6704 SignatureVerificationPipelineError::Policy(
6705 crate::policy::PolicyViolation::ResourceLimit {
6706 resource: "XPath expressions",
6707 maximum: 0,
6708 actual: 1,
6709 }
6710 )
6711 ),
6712 "unexpected error: {error:?}"
6713 );
6714 }
6715
6716 #[test]
6717 fn retrieval_method_xpath_obeys_expression_byte_limit() {
6718 let expression = "ancestor-or-self::ds:X509Data";
6721 let padded_expression = format!(" {expression} ");
6722 let xml = retrieval_method_xpath_signature().replace(expression, &padded_expression);
6723 let mut policy = crate::policy::VerificationPolicy::default();
6724 policy.resources.max_xpath_expression_bytes = expression.len();
6725
6726 let error = VerifyContext::new()
6727 .policy(policy)
6728 .verify(&xml)
6729 .expect_err("RetrievalMethod XPath must obey the expression byte limit");
6730
6731 assert!(
6732 matches!(
6733 &error,
6734 SignatureVerificationPipelineError::Policy(
6735 crate::policy::PolicyViolation::ResourceLimit {
6736 resource: "XPath expression bytes",
6737 maximum,
6738 actual,
6739 }
6740 ) if *maximum == expression.len() && *actual == padded_expression.len()
6741 ),
6742 "unexpected error: {error:?}"
6743 );
6744 }
6745
6746 #[test]
6747 fn retrieval_method_xpath_obeys_expression_complexity_limit() {
6748 let mut policy = crate::policy::VerificationPolicy::default();
6751 policy.resources.max_xpath_expression_complexity = 0;
6752
6753 let error = VerifyContext::new()
6754 .policy(policy)
6755 .verify(&retrieval_method_xpath_signature())
6756 .expect_err("RetrievalMethod XPath must obey the complexity limit");
6757
6758 assert!(
6759 matches!(
6760 &error,
6761 SignatureVerificationPipelineError::Policy(
6762 crate::policy::PolicyViolation::ResourceLimit {
6763 resource: "XPath expression complexity",
6764 maximum: 0,
6765 actual,
6766 }
6767 ) if *actual > 0
6768 ),
6769 "unexpected error: {error:?}"
6770 );
6771 }
6772
6773 #[test]
6774 fn retrieval_method_xpath_uses_node_filter_work_budget() {
6775 let mut policy = crate::policy::VerificationPolicy::default();
6778 policy.resources.max_node_set_filter_work = 4;
6779 let xml = retrieval_method_xpath_signature().replace(
6780 "<holder Id=\"target\">",
6781 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
6782 );
6783
6784 let error = VerifyContext::new()
6785 .policy(policy)
6786 .verify(&xml)
6787 .expect_err("RetrievalMethod XPath must consume node-filter work");
6788
6789 assert!(
6790 matches!(
6791 &error,
6792 SignatureVerificationPipelineError::Policy(
6793 crate::policy::PolicyViolation::ResourceLimit {
6794 resource: crate::policy::resource_name::NODE_SET_FILTER_WORK,
6795 maximum: 4,
6796 actual,
6797 }
6798 ) if *actual > 4
6799 ),
6800 "unexpected error: {error:?}"
6801 );
6802 }
6803
6804 #[test]
6805 fn retrieval_method_xpath_charges_every_context_to_evaluation_work() {
6806 let mut policy = crate::policy::VerificationPolicy::default();
6809 policy.resources.max_xpath_evaluation_work = 4;
6810 let xml = retrieval_method_xpath_signature().replace(
6811 "<holder Id=\"target\">",
6812 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
6813 );
6814
6815 let error = VerifyContext::new()
6816 .policy(policy)
6817 .verify(&xml)
6818 .expect_err("RetrievalMethod XPath must charge every evaluation context");
6819
6820 assert!(matches!(
6821 error,
6822 SignatureVerificationPipelineError::Policy(
6823 crate::policy::PolicyViolation::ResourceLimit {
6824 resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
6825 maximum: 4,
6826 actual,
6827 }
6828 ) if actual > 4
6829 ));
6830 }
6831
6832 #[test]
6833 fn retrieval_method_xpath_obeys_namespace_binding_limit() {
6834 let mut policy = crate::policy::VerificationPolicy::default();
6837 policy.resources.max_xpath_namespace_bindings = 0;
6838
6839 let error = VerifyContext::new()
6840 .policy(policy)
6841 .verify(&retrieval_method_xpath_signature())
6842 .expect_err("RetrievalMethod XPath namespaces must obey the binding limit");
6843
6844 assert!(matches!(
6845 error,
6846 SignatureVerificationPipelineError::Policy(
6847 crate::policy::PolicyViolation::ResourceLimit {
6848 resource: crate::policy::resource_name::XPATH_NAMESPACE_BINDINGS,
6849 maximum: 0,
6850 actual,
6851 }
6852 ) if actual > 0
6853 ));
6854 }
6855
6856 #[test]
6857 fn retrieval_method_xpath_obeys_namespace_byte_limit() {
6858 let mut policy = crate::policy::VerificationPolicy::default();
6861 policy.resources.max_xpath_namespace_bytes = 0;
6862
6863 let error = VerifyContext::new()
6864 .policy(policy)
6865 .verify(&retrieval_method_xpath_signature())
6866 .expect_err("RetrievalMethod XPath namespaces must obey the byte limit");
6867
6868 assert!(matches!(
6869 error,
6870 SignatureVerificationPipelineError::Policy(
6871 crate::policy::PolicyViolation::ResourceLimit {
6872 resource: crate::policy::resource_name::XPATH_NAMESPACE_BYTES,
6873 maximum: 0,
6874 actual,
6875 }
6876 ) if actual > 0
6877 ));
6878 }
6879
6880 #[test]
6881 fn retrieval_method_xpath_obeys_context_evaluation_limit() {
6882 let mut policy = crate::policy::VerificationPolicy::default();
6886 policy.resources.max_xpath_context_evaluations = 4;
6887 let xml = retrieval_method_xpath_signature().replace(
6888 "<holder Id=\"target\">",
6889 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
6890 );
6891
6892 let error = VerifyContext::new()
6893 .policy(policy)
6894 .verify(&xml)
6895 .expect_err("RetrievalMethod XPath contexts must obey the evaluation limit");
6896
6897 assert!(matches!(
6898 error,
6899 SignatureVerificationPipelineError::Policy(
6900 crate::policy::PolicyViolation::ResourceLimit {
6901 resource: crate::policy::resource_name::XPATH_CONTEXT_EVALUATIONS,
6902 maximum: 4,
6903 actual,
6904 }
6905 ) if actual > 4
6906 ));
6907 }
6908
6909 #[test]
6910 fn retrieval_method_materializes_direct_untransformed_x509_data() {
6911 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6914 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
6915 <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
6916 </root>"##;
6917 let document = Document::parse(xml).unwrap();
6918 let key_info_node = document
6919 .descendants()
6920 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
6921 .unwrap();
6922 let mut key_info = parse_key_info(key_info_node).unwrap();
6923
6924 materialize_retrieval_methods(
6925 &mut key_info,
6926 &UriReferenceResolver::new(&document),
6927 UriTypeSet::SAME_DOCUMENT,
6928 None,
6929 crate::provider::default_provider(),
6930 )
6931 .expect("a direct X509Data target needs no transform");
6932 assert!(matches!(
6933 key_info.sources.as_slice(),
6934 [super::super::parse::KeyInfoSource::X509Data(info)]
6935 if info.subject_names == ["CN=leaf"]
6936 ));
6937 }
6938
6939 #[test]
6940 fn retrieval_method_respects_configured_same_document_id_semantics() {
6941 fn materialize(
6944 uri: &str,
6945 id: &str,
6946 semantics: crate::policy::SameDocumentIdSemantics,
6947 ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
6948 let xml = format!(
6949 r#"<root xmlns:ds="{XMLDSIG_NS}">
6950 <ds:KeyInfo><ds:RetrievalMethod URI="{uri}" Type="{XMLDSIG_NS}X509Data"/></ds:KeyInfo>
6951 <ds:X509Data Id="{id}"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
6952 </root>"#
6953 );
6954 let document = Document::parse(&xml).unwrap();
6955 let key_info_node = document
6956 .descendants()
6957 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
6958 .unwrap();
6959 let mut key_info = parse_key_info(key_info_node).unwrap();
6960 let resolver =
6961 UriReferenceResolver::new(&document).with_same_document_id_semantics(semantics);
6962
6963 materialize_retrieval_methods(
6964 &mut key_info,
6965 &resolver,
6966 UriTypeSet::SAME_DOCUMENT,
6967 None,
6968 crate::provider::default_provider(),
6969 )
6970 }
6971
6972 assert!(
6973 materialize(
6974 "#12345",
6975 "12345",
6976 crate::policy::SameDocumentIdSemantics::Specification,
6977 )
6978 .is_err(),
6979 "the standards mode must reject a non-NCName bare fragment"
6980 );
6981 assert!(
6982 materialize(
6983 "#visa'3d",
6984 "visa'3d",
6985 crate::policy::SameDocumentIdSemantics::XmlSecBarename,
6986 )
6987 .is_err(),
6988 "the donor barename wrapper cannot represent an apostrophe"
6989 );
6990 assert!(
6991 materialize(
6992 "#visa'3d",
6993 "visa'3d",
6994 crate::policy::SameDocumentIdSemantics::XmlSecVisa3d,
6995 )
6996 .is_ok(),
6997 "Visa3D mode resolves the registered ID without an XPointer literal"
6998 );
6999 }
7000
7001 #[test]
7002 fn raw_x509_retrieval_method_uses_inherited_xml_base() {
7003 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7006 let xml = format!(
7007 r#"<root xml:base="https://example.test/keys/nested/" xmlns:ds="{XMLDSIG_NS}">
7008 <ds:KeyInfo><ds:RetrievalMethod URI="../signer.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>
7009 </root>"#
7010 );
7011 let document = Document::parse(&xml).unwrap();
7012 let key_info_node = document
7013 .descendants()
7014 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
7015 .unwrap();
7016 let mut key_info = parse_key_info(key_info_node).unwrap();
7017 let certificate = include_bytes!(
7018 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
7019 )
7020 .to_vec();
7021 let resources = HashMap::from([(
7022 "https://example.test/keys/signer.der".to_string(),
7023 certificate,
7024 )]);
7025 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7026
7027 materialize_retrieval_methods(
7028 &mut key_info,
7029 &resolver,
7030 UriTypeSet::ALL,
7031 None,
7032 crate::provider::default_provider(),
7033 )
7034 .expect("RetrievalMethod should resolve against inherited xml:base");
7035
7036 assert!(matches!(
7037 key_info.sources.as_slice(),
7038 [super::super::parse::KeyInfoSource::X509Data(info)]
7039 if info.certificates.len() == 1
7040 ));
7041 }
7042
7043 #[test]
7044 fn retrieval_method_requires_xpath_for_x509_data_below_uri_root() {
7045 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7048 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
7049 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
7050 </root>"##;
7051 let document = Document::parse(xml).unwrap();
7052 let key_info_node = document
7053 .descendants()
7054 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
7055 .unwrap();
7056 let mut key_info = parse_key_info(key_info_node).unwrap();
7057
7058 let error = materialize_retrieval_methods(
7059 &mut key_info,
7060 &UriReferenceResolver::new(&document),
7061 UriTypeSet::SAME_DOCUMENT,
7062 None,
7063 crate::provider::default_provider(),
7064 )
7065 .expect_err("a wrapper target requires an explicit selection transform");
7066 assert!(matches!(
7067 error,
7068 SignatureVerificationPipelineError::InvalidStructure {
7069 reason: "untransformed X509Data RetrievalMethod must target X509Data directly"
7070 }
7071 ));
7072 }
7073
7074 #[test]
7075 fn retrieval_method_rejects_target_inside_external_x509_data_ancestor() {
7076 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7079 <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>
7080 <ds:X509Data><ds:X509SubjectName Id="target">CN=leaf</ds:X509SubjectName></ds:X509Data>
7081 </root>"##;
7082 let document = Document::parse(xml).unwrap();
7083 let key_info_node = document
7084 .descendants()
7085 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
7086 .unwrap();
7087 let mut key_info = parse_key_info(key_info_node).unwrap();
7088
7089 let error = materialize_retrieval_methods(
7090 &mut key_info,
7091 &UriReferenceResolver::new(&document),
7092 UriTypeSet::SAME_DOCUMENT,
7093 None,
7094 crate::provider::default_provider(),
7095 )
7096 .expect_err("filter output without an X509Data root must be rejected");
7097 assert!(matches!(
7098 error,
7099 SignatureVerificationPipelineError::InvalidStructure {
7100 reason: "X509Data RetrievalMethod selected no X509Data element"
7101 }
7102 ));
7103 }
7104
7105 #[test]
7106 fn retrieval_method_rejects_ambiguous_x509_data_relation() {
7107 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7109 <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>
7110 <holder Id="target"><ds:X509Data/><ds:X509Data/></holder>
7111 </root>"##;
7112 let document = Document::parse(xml).unwrap();
7113 let key_info_node = document
7114 .descendants()
7115 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
7116 .unwrap();
7117 let mut key_info = parse_key_info(key_info_node).unwrap();
7118
7119 let error = materialize_retrieval_methods(
7120 &mut key_info,
7121 &UriReferenceResolver::new(&document),
7122 UriTypeSet::SAME_DOCUMENT,
7123 None,
7124 crate::provider::default_provider(),
7125 )
7126 .expect_err("multiple transformed X509Data roots must be rejected");
7127 assert!(matches!(
7128 error,
7129 SignatureVerificationPipelineError::InvalidStructure {
7130 reason: "X509Data RetrievalMethod selected multiple X509Data elements"
7131 }
7132 ));
7133 }
7134
7135 #[test]
7136 fn retrieval_method_materialization_preserves_key_info_order() {
7137 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7140 <ds:KeyInfo>
7141 <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>
7142 <ds:KeyName>fallback</ds:KeyName>
7143 </ds:KeyInfo>
7144 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
7145 </root>"##;
7146 let document = Document::parse(xml).unwrap();
7147 let key_info_node = document
7148 .descendants()
7149 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
7150 .unwrap();
7151 let mut key_info = parse_key_info(key_info_node).unwrap();
7152
7153 materialize_retrieval_methods(
7154 &mut key_info,
7155 &UriReferenceResolver::new(&document),
7156 UriTypeSet::SAME_DOCUMENT,
7157 None,
7158 crate::provider::default_provider(),
7159 )
7160 .unwrap();
7161 assert!(matches!(
7162 key_info.sources.as_slice(),
7163 [
7164 super::super::parse::KeyInfoSource::X509Data(_),
7165 super::super::parse::KeyInfoSource::KeyName(name)
7166 ] if name == "fallback"
7167 ));
7168 }
7169
7170 #[test]
7171 fn retrieval_method_materialization_bounds_repeated_sources() {
7172 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7175 let certificate = include_bytes!(
7176 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
7177 )
7178 .to_vec();
7179 let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
7180 let mut key_info = KeyInfo {
7181 sources: (0..=64)
7182 .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
7183 uri: "urn:certificate".into(),
7184 resource_type: Some(RAW_X509_TYPE.into()),
7185 transforms: RetrievalMethodTransforms::None,
7186 })
7187 .collect(),
7188 };
7189 let document = Document::parse("<root/>").unwrap();
7190 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7191
7192 let error = materialize_retrieval_methods(
7193 &mut key_info,
7194 &resolver,
7195 UriTypeSet::ALL,
7196 None,
7197 crate::provider::default_provider(),
7198 )
7199 .expect_err("retrieval count must be bounded before materialization");
7200 assert!(matches!(
7201 error,
7202 SignatureVerificationPipelineError::InvalidStructure {
7203 reason: "KeyInfo contains too many RetrievalMethod elements"
7204 }
7205 ));
7206 }
7207
7208 #[test]
7209 fn key_info_reference_materializes_an_allowed_empty_uri() {
7210 let document = XmlDocument::parse(format!(
7214 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>root-key</ds:KeyName></ds:KeyInfo>"#
7215 ))
7216 .unwrap();
7217 let mut key_info = KeyInfo {
7218 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7219 uri: String::new(),
7220 }],
7221 };
7222 let mut policy = crate::policy::VerificationPolicy::default();
7223 policy.key_sources.key_info_reference = true;
7224 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7225 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
7226 let mut budgets = RetrievalMaterializationBudgets {
7227 xpath_parse: &mut xpath_parse_budget,
7228 execution: &execution_budget,
7229 resources: &policy.resources,
7230 certificate_signature_algorithms: policy
7231 .key_trust
7232 .certificate_signature_algorithms
7233 .as_ref(),
7234 xml_backend: crate::XmlBackend::default(),
7235 };
7236 let mut materialization = KeyInfoMaterializationState::default();
7237
7238 document
7239 .with_view(|view| {
7240 materialize_key_info_references_with_budgets(
7241 &mut key_info,
7242 &UriReferenceResolver::with_document_view(view, &[]),
7243 &policy,
7244 crate::provider::default_provider(),
7245 &mut budgets,
7246 &mut materialization,
7247 )?;
7248 Ok::<_, SignatureVerificationPipelineError>(())
7249 })
7250 .unwrap();
7251
7252 assert!(matches!(
7253 key_info.sources.as_slice(),
7254 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "root-key"
7255 ));
7256 }
7257
7258 #[test]
7259 fn empty_key_info_reference_participates_in_cycle_detection() {
7260 let document = XmlDocument::parse(format!(
7264 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#"><dsig11:KeyInfoReference URI=""/></ds:KeyInfo>"#
7265 ))
7266 .unwrap();
7267 let mut key_info = KeyInfo {
7268 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7269 uri: String::new(),
7270 }],
7271 };
7272 let mut policy = crate::policy::VerificationPolicy::default();
7273 policy.key_sources.key_info_reference = true;
7274 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7275 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
7276 let mut budgets = RetrievalMaterializationBudgets {
7277 xpath_parse: &mut xpath_parse_budget,
7278 execution: &execution_budget,
7279 resources: &policy.resources,
7280 certificate_signature_algorithms: policy
7281 .key_trust
7282 .certificate_signature_algorithms
7283 .as_ref(),
7284 xml_backend: crate::XmlBackend::default(),
7285 };
7286 let mut materialization = KeyInfoMaterializationState::default();
7287
7288 let error = document
7289 .with_view(|view| {
7290 materialize_key_info_references_with_budgets(
7291 &mut key_info,
7292 &UriReferenceResolver::with_document_view(view, &[]),
7293 &policy,
7294 crate::provider::default_provider(),
7295 &mut budgets,
7296 &mut materialization,
7297 )
7298 })
7299 .expect_err("empty-URI self-reference must be rejected as a cycle");
7300
7301 assert!(matches!(
7302 error,
7303 SignatureVerificationPipelineError::InvalidStructure {
7304 reason: "KeyInfoReference cycle detected"
7305 }
7306 ));
7307 }
7308
7309 #[test]
7310 fn key_info_materialization_shares_candidate_work_across_source_kinds() {
7311 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7314 let document = XmlDocument::parse(format!(
7315 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>"##
7316 ))
7317 .unwrap();
7318 let certificate = include_bytes!(
7319 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
7320 )
7321 .to_vec();
7322 let resources = HashMap::from([("signer.der".to_owned(), certificate)]);
7323 let mut key_info = KeyInfo {
7324 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7325 uri: "#target".into(),
7326 }],
7327 };
7328 let mut policy = crate::policy::VerificationPolicy::default();
7329 policy.key_sources.key_info_reference = true;
7330 policy.uris.retrieval_methods = UriTypeSet::ALL;
7331 policy.resources.max_key_candidates = 2;
7332 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7333 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
7334 let mut budgets = RetrievalMaterializationBudgets {
7335 xpath_parse: &mut xpath_parse_budget,
7336 execution: &execution_budget,
7337 resources: &policy.resources,
7338 certificate_signature_algorithms: policy
7339 .key_trust
7340 .certificate_signature_algorithms
7341 .as_ref(),
7342 xml_backend: crate::XmlBackend::default(),
7343 };
7344 let mut materialization = KeyInfoMaterializationState::default();
7345
7346 let error = document
7347 .with_view(|view| {
7348 let resolver = UriReferenceResolver::with_document_view(view, &[])
7349 .with_external_resources(&resources);
7350 let mut outcome = materialize_key_info_references_with_budgets(
7351 &mut key_info,
7352 &resolver,
7353 &policy,
7354 crate::provider::default_provider(),
7355 &mut budgets,
7356 &mut materialization,
7357 )?;
7358 outcome.merge(materialize_retrieval_methods_with_budgets(
7359 &mut key_info,
7360 &resolver,
7361 policy.uris.retrieval_methods,
7362 policy.transforms.allowed_algorithms.as_ref(),
7363 crate::provider::default_provider(),
7364 &mut budgets,
7365 &mut materialization.candidate_work,
7366 )?);
7367 Ok::<_, DsigError>(outcome)
7368 })
7369 .expect_err("all materializers must share the candidate-work limit");
7370
7371 assert!(matches!(
7372 error,
7373 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
7374 resource: crate::policy::resource_name::KEY_CANDIDATES,
7375 maximum: 2,
7376 actual: 3,
7377 })
7378 ));
7379 }
7380
7381 #[test]
7382 fn public_key_info_materialization_binds_external_resource_policy() {
7383 let document = Document::parse("<root/>").unwrap();
7386 let external = format!(
7387 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
7388 );
7389 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
7390 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7391 let mut key_info = KeyInfo {
7392 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7393 uri: "key.xml".into(),
7394 }],
7395 };
7396 let mut policy = crate::policy::VerificationPolicy::default();
7397 policy.key_sources.key_info_reference = true;
7398 policy.uris.key_info_references = UriTypeSet::ALL;
7399 policy.resources.max_external_resource_bytes = 0;
7400
7401 let error = materialize_verification_key_info_references(
7402 &mut key_info,
7403 resolver,
7404 &policy,
7405 crate::provider::default_provider(),
7406 crate::XmlBackend::default(),
7407 )
7408 .expect_err("policy must reject non-empty external KeyInfo bytes");
7409
7410 assert!(matches!(
7411 error,
7412 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
7413 resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
7414 maximum: 0,
7415 actual,
7416 }) if actual == resources["key.xml"].len()
7417 ));
7418 }
7419
7420 #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
7421 #[test]
7422 fn public_key_info_materialization_uses_the_selected_backend() {
7423 let external = format!(
7426 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
7427 );
7428 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
7429 let document = Document::parse("<root/>").unwrap();
7430
7431 for backend in [crate::XmlBackend::Xmloxide, crate::XmlBackend::Roxmltree] {
7432 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7433 let mut key_info = KeyInfo {
7434 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7435 uri: "key.xml".into(),
7436 }],
7437 };
7438 let mut policy = crate::policy::VerificationPolicy::default();
7439 policy.key_sources.key_info_reference = true;
7440 policy.uris.key_info_references = UriTypeSet::ALL;
7441
7442 materialize_verification_key_info_references(
7443 &mut key_info,
7444 resolver,
7445 &policy,
7446 crate::provider::default_provider(),
7447 backend,
7448 )
7449 .unwrap_or_else(|error| panic!("{backend:?} materialization failed: {error}"));
7450
7451 assert!(matches!(
7452 key_info.sources.as_slice(),
7453 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "external"
7454 ));
7455 }
7456 }
7457
7458 #[test]
7459 fn public_key_info_materialization_defers_nested_retrieval_failure() {
7460 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7464 let external = format!(
7465 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>usable</ds:KeyName><ds:RetrievalMethod URI="missing.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>"#
7466 );
7467 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
7468 let document = Document::parse("<root/>").unwrap();
7469
7470 for operation in ["signing", "verification"] {
7471 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7472 let mut key_info = KeyInfo {
7473 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7474 uri: "key.xml".into(),
7475 }],
7476 };
7477 match operation {
7478 "signing" => {
7479 let mut policy = crate::policy::SigningPolicy::default();
7480 policy.uris.key_info_references = UriTypeSet::ALL;
7481 policy.uris.retrieval_methods = UriTypeSet::ALL;
7482 materialize_signing_key_info_references(
7483 &mut key_info,
7484 resolver,
7485 &policy,
7486 crate::provider::default_provider(),
7487 crate::XmlBackend::default(),
7488 )
7489 }
7490 "verification" => {
7491 let mut policy = crate::policy::VerificationPolicy::default();
7492 policy.key_sources.key_info_reference = true;
7493 policy.uris.key_info_references = UriTypeSet::ALL;
7494 policy.uris.retrieval_methods = UriTypeSet::ALL;
7495 materialize_verification_key_info_references(
7496 &mut key_info,
7497 resolver,
7498 &policy,
7499 crate::provider::default_provider(),
7500 crate::XmlBackend::default(),
7501 )
7502 }
7503 _ => unreachable!(),
7504 }
7505 .unwrap_or_else(|error| panic!("{operation} materialization failed: {error}"));
7506
7507 assert!(matches!(
7508 key_info.sources.as_slice(),
7509 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "usable"
7510 ));
7511 }
7512 }
7513
7514 #[test]
7515 fn public_key_info_materialization_binds_xpath_policy() {
7516 let external = format!(
7519 r##"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}">
7520 <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
7521 <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
7522 <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
7523 </ds:Transform></ds:Transforms>
7524 </ds:RetrievalMethod>
7525 <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
7526 </ds:KeyInfo>"##
7527 );
7528 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
7529 let document = Document::parse("<root/>").unwrap();
7530
7531 for operation in ["signing", "verification"] {
7532 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7533 let mut key_info = KeyInfo {
7534 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7535 uri: "key.xml".into(),
7536 }],
7537 };
7538 let error = match operation {
7539 "signing" => {
7540 let mut policy = crate::policy::SigningPolicy::default();
7541 policy.uris.key_info_references = UriTypeSet::ALL;
7542 policy.resources.max_xpath_expressions = 0;
7543 materialize_signing_key_info_references(
7544 &mut key_info,
7545 resolver,
7546 &policy,
7547 crate::provider::default_provider(),
7548 crate::XmlBackend::default(),
7549 )
7550 }
7551 "verification" => {
7552 let mut policy = crate::policy::VerificationPolicy::default();
7553 policy.key_sources.key_info_reference = true;
7554 policy.uris.key_info_references = UriTypeSet::ALL;
7555 policy.resources.max_xpath_expressions = 0;
7556 materialize_verification_key_info_references(
7557 &mut key_info,
7558 resolver,
7559 &policy,
7560 crate::provider::default_provider(),
7561 crate::XmlBackend::default(),
7562 )
7563 }
7564 _ => unreachable!(),
7565 }
7566 .expect_err("the public helper must enforce the supplied XPath expression limit");
7567
7568 assert!(
7569 matches!(
7570 error,
7571 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
7572 resource: crate::policy::resource_name::XPATH_EXPRESSIONS,
7573 maximum: 0,
7574 actual: 1,
7575 })
7576 ),
7577 "unexpected {operation} error: {error:?}"
7578 );
7579 }
7580 }
7581
7582 fn materialize_external_key_info_chain(
7583 terminal_reference: &str,
7584 ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
7585 let a = format!(
7586 r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
7587 <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
7588 <ds:KeyInfo ID="next"><dsig11:KeyInfoReference URI="b.xml#root"/></ds:KeyInfo>
7589 </doc>"##
7590 );
7591 let b = format!(
7592 r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
7593 <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
7594 <ds:KeyInfo ID="next">{terminal_reference}</ds:KeyInfo>
7595 </doc>"##
7596 );
7597 let resources = HashMap::from([
7598 ("a.xml".to_owned(), a.into_bytes()),
7599 ("b.xml".to_owned(), b.into_bytes()),
7600 ]);
7601 let document = XmlDocument::parse("<root/>").unwrap();
7602 let mut key_info = KeyInfo {
7603 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7604 uri: "a.xml#root".into(),
7605 }],
7606 };
7607 let mut policy = crate::policy::VerificationPolicy::default();
7608 policy.key_sources.key_info_reference = true;
7609 policy.uris.key_info_references = UriTypeSet::ALL;
7610 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7611 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
7612 let mut budgets = RetrievalMaterializationBudgets {
7613 xpath_parse: &mut xpath_parse_budget,
7614 execution: &execution_budget,
7615 resources: &policy.resources,
7616 certificate_signature_algorithms: policy
7617 .key_trust
7618 .certificate_signature_algorithms
7619 .as_ref(),
7620 xml_backend: crate::XmlBackend::default(),
7621 };
7622 let mut materialization = KeyInfoMaterializationState::default();
7623
7624 document.with_view(|view| {
7625 let resolver = UriReferenceResolver::with_document_view(view, &[])
7626 .with_external_resources(&resources);
7627 materialize_key_info_references_with_budgets(
7628 &mut key_info,
7629 &resolver,
7630 &policy,
7631 crate::provider::default_provider(),
7632 &mut budgets,
7633 &mut materialization,
7634 )?;
7635 Ok::<_, SignatureVerificationPipelineError>(())
7636 })?;
7637 Ok(key_info)
7638 }
7639
7640 fn materialize_external_key_info_bytes_with_limit(
7641 encoded: Vec<u8>,
7642 maximum_document_bytes: usize,
7643 ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
7644 let resources = HashMap::from([("key.xml".to_owned(), encoded)]);
7645 let document = XmlDocument::parse("<root/>").unwrap();
7646 let mut key_info = KeyInfo {
7647 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7648 uri: "key.xml".into(),
7649 }],
7650 };
7651 let mut policy = crate::policy::VerificationPolicy::default();
7652 policy.key_sources.key_info_reference = true;
7653 policy.uris.key_info_references = UriTypeSet::ALL;
7654 policy.resources.max_xml_document_bytes = maximum_document_bytes;
7655 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7656 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
7657 let mut budgets = RetrievalMaterializationBudgets {
7658 xpath_parse: &mut xpath_parse_budget,
7659 execution: &execution_budget,
7660 resources: &policy.resources,
7661 certificate_signature_algorithms: policy
7662 .key_trust
7663 .certificate_signature_algorithms
7664 .as_ref(),
7665 xml_backend: crate::XmlBackend::default(),
7666 };
7667 let mut materialization = KeyInfoMaterializationState::default();
7668
7669 document.with_view(|view| {
7670 let resolver = UriReferenceResolver::with_document_view(view, &[])
7671 .with_external_resources(&resources);
7672 materialize_key_info_references_with_budgets(
7673 &mut key_info,
7674 &resolver,
7675 &policy,
7676 crate::provider::default_provider(),
7677 &mut budgets,
7678 &mut materialization,
7679 )?;
7680 Ok::<_, SignatureVerificationPipelineError>(())
7681 })?;
7682 Ok(key_info)
7683 }
7684
7685 fn materialize_external_key_info_bytes(
7686 encoded: Vec<u8>,
7687 ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
7688 materialize_external_key_info_bytes_with_limit(
7689 encoded,
7690 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
7691 )
7692 }
7693
7694 #[test]
7695 fn external_key_info_reference_decodes_utf16_xml_octets() {
7696 let xml = format!(
7699 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>utf16-key</ds:KeyName></ds:KeyInfo>"#
7700 );
7701 let mut encoded = vec![0xff, 0xfe];
7702 encoded.extend(xml.encode_utf16().flat_map(u16::to_le_bytes));
7703 let key_info = materialize_external_key_info_bytes(encoded)
7704 .expect("UTF-16 external KeyInfo must materialize");
7705 assert!(matches!(
7706 key_info.sources.as_slice(),
7707 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "utf16-key"
7708 ));
7709 }
7710
7711 #[test]
7712 fn external_key_info_reference_rejects_malformed_xml_encoding() {
7713 let error = materialize_external_key_info_bytes(vec![0xff, 0xfe, b'<'])
7716 .expect_err("truncated UTF-16 must be rejected during octet decoding");
7717
7718 assert!(matches!(
7719 error,
7720 SignatureVerificationPipelineError::InvalidStructure {
7721 reason: "KeyInfoReference external resource has an invalid XML encoding"
7722 }
7723 ));
7724 }
7725
7726 #[test]
7727 fn external_key_info_reference_preserves_decoded_document_limit() {
7728 let encoded = b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><ds:KeyInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"><ds:KeyName>\xe9\xe9</ds:KeyName></ds:KeyInfo>".to_vec();
7731 let maximum = encoded.len();
7732 let error = materialize_external_key_info_bytes_with_limit(encoded, maximum)
7733 .expect_err("decoded XML expansion must retain resource-limit classification");
7734
7735 assert!(matches!(
7736 error,
7737 SignatureVerificationPipelineError::Policy(
7738 crate::policy::PolicyViolation::ResourceLimit {
7739 resource: crate::policy::resource_name::XML_DOCUMENT,
7740 maximum: observed_maximum,
7741 actual,
7742 }
7743 ) if observed_maximum == maximum && actual > maximum
7744 ));
7745 }
7746
7747 #[test]
7748 fn key_info_reference_cycle_identity_includes_the_owning_resource() {
7749 let key_info = materialize_external_key_info_chain("<ds:KeyName>terminal</ds:KeyName>")
7752 .expect("cross-document duplicate fragments must materialize");
7753 assert!(matches!(
7754 key_info.sources.as_slice(),
7755 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "terminal"
7756 ));
7757 }
7758
7759 #[test]
7760 fn key_info_reference_cycle_identity_survives_external_reparse() {
7761 let error =
7764 materialize_external_key_info_chain("<dsig11:KeyInfoReference URI=\"a.xml#root\"/>")
7765 .expect_err("a resource cycle must fail before exhausting depth");
7766 assert!(matches!(
7767 error,
7768 SignatureVerificationPipelineError::InvalidStructure {
7769 reason: "KeyInfoReference cycle detected"
7770 }
7771 ));
7772 }
7773
7774 #[test]
7775 fn retrieval_method_materialization_deduplicates_within_count_limit() {
7776 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7779 let certificate = include_bytes!(
7780 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
7781 )
7782 .to_vec();
7783 let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
7784 let mut key_info = KeyInfo {
7785 sources: (0..MAX_RETRIEVAL_METHOD_COUNT)
7786 .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
7787 uri: "urn:certificate".into(),
7788 resource_type: Some(RAW_X509_TYPE.into()),
7789 transforms: RetrievalMethodTransforms::None,
7790 })
7791 .collect(),
7792 };
7793 let document = Document::parse("<root/>").unwrap();
7794 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7795
7796 materialize_retrieval_methods(
7797 &mut key_info,
7798 &resolver,
7799 UriTypeSet::ALL,
7800 None,
7801 crate::provider::default_provider(),
7802 )
7803 .unwrap();
7804 assert!(matches!(
7805 key_info.sources.as_slice(),
7806 [super::super::parse::KeyInfoSource::X509Data(info)]
7807 if info.certificates.len() == 1
7808 ));
7809 }
7810
7811 #[test]
7812 fn retrieval_method_candidate_budget_includes_embedded_key_values() {
7813 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7816 let resources = HashMap::from([("urn:certificate".to_string(), vec![1, 2, 3])]);
7817 let mut key_info = KeyInfo {
7818 sources: vec![
7819 super::super::parse::KeyInfoSource::KeyValue(
7820 super::super::parse::KeyValueInfo::Unsupported {
7821 namespace: Some(XMLDSIG_NS.into()),
7822 local_name: "FutureKeyValue".into(),
7823 },
7824 ),
7825 super::super::parse::KeyInfoSource::RetrievalMethod {
7826 uri: "urn:certificate".into(),
7827 resource_type: Some(RAW_X509_TYPE.into()),
7828 transforms: RetrievalMethodTransforms::None,
7829 },
7830 ],
7831 };
7832 let document = Document::parse("<root/>").unwrap();
7833 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7834 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7835 let execution_budget = TransformExecutionBudget::default();
7836 let resource_policy = crate::policy::ResourcePolicy {
7837 max_key_candidates: 1,
7838 ..crate::policy::ResourcePolicy::default()
7839 };
7840 let mut budgets = RetrievalMaterializationBudgets {
7841 xpath_parse: &mut xpath_parse_budget,
7842 execution: &execution_budget,
7843 resources: &resource_policy,
7844 certificate_signature_algorithms: None,
7845 xml_backend: crate::XmlBackend::default(),
7846 };
7847 let mut candidate_work = key_info.embedded_candidate_count();
7848
7849 let error = materialize_retrieval_methods_with_budgets(
7850 &mut key_info,
7851 &resolver,
7852 UriTypeSet::ALL,
7853 None,
7854 crate::provider::default_provider(),
7855 &mut budgets,
7856 &mut candidate_work,
7857 )
7858 .expect_err("the retrieved certificate must exceed the aggregate candidate limit");
7859
7860 assert!(matches!(
7861 error,
7862 SignatureVerificationPipelineError::Policy(
7863 crate::policy::PolicyViolation::ResourceLimit {
7864 resource: crate::policy::resource_name::KEY_CANDIDATES,
7865 maximum: 1,
7866 actual: 2,
7867 }
7868 )
7869 ));
7870 }
7871
7872 #[test]
7873 fn raw_x509_retrieval_rejects_empty_same_document_uri() {
7874 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7877 let certificate = include_bytes!(
7878 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
7879 )
7880 .to_vec();
7881 let resources = HashMap::from([(String::new(), certificate)]);
7882 let mut key_info = KeyInfo {
7883 sources: vec![super::super::parse::KeyInfoSource::RetrievalMethod {
7884 uri: String::new(),
7885 resource_type: Some(RAW_X509_TYPE.into()),
7886 transforms: RetrievalMethodTransforms::None,
7887 }],
7888 };
7889 let document = Document::parse("<root/>").unwrap();
7890 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7891
7892 let error = materialize_retrieval_methods(
7893 &mut key_info,
7894 &resolver,
7895 UriTypeSet::ALL,
7896 None,
7897 crate::provider::default_provider(),
7898 )
7899 .expect_err("empty URI must retain same-document semantics");
7900 assert!(matches!(
7901 error,
7902 SignatureVerificationPipelineError::InvalidStructure {
7903 reason: "raw X509 RetrievalMethod requires an untransformed external URI"
7904 }
7905 ));
7906 }
7907
7908 #[test]
7909 fn verify_context_does_not_hide_malformed_digest_behind_unsupported_transform() {
7910 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7912 let xml = xml.replacen(
7913 "<ds:Reference URI=\"#target\">",
7914 "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
7915 1,
7916 );
7917 replace_fixture_manifest_digest(&xml, "!!!")
7918 });
7919
7920 let error = VerifyContext::new()
7921 .key(&AcceptingKey)
7922 .process_manifests(true)
7923 .verify(&broken_xml)
7924 .expect_err("malformed Manifest digest must not become a validity result");
7925 assert!(matches!(
7926 error,
7927 SignatureVerificationPipelineError::ParseManifestReference(_)
7928 ));
7929 }
7930
7931 #[test]
7932 fn verify_context_rejects_manifest_non_whitespace_mixed_content() {
7933 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7936 xml.replacen(
7937 "<ds:Manifest ID=\"manifest\">",
7938 "<ds:Manifest ID=\"manifest\">junk",
7939 1,
7940 )
7941 });
7942
7943 let err = VerifyContext::new()
7944 .key(&AcceptingKey)
7945 .process_manifests(true)
7946 .verify(&xml)
7947 .expect_err("Manifest mixed content must fail verification");
7948 assert!(matches!(
7949 err,
7950 SignatureVerificationPipelineError::InvalidStructure {
7951 reason: "Manifest contains non-whitespace mixed content"
7952 }
7953 ));
7954 }
7955
7956 #[test]
7957 fn verify_context_rejects_empty_manifest_children() {
7958 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7961 let (prefix, rest) = xml
7962 .split_once("<ds:Manifest ID=\"manifest\">")
7963 .expect("fixture should contain Manifest");
7964 let (_, suffix) = rest
7965 .split_once("</ds:Manifest>")
7966 .expect("fixture should contain closing Manifest");
7967 format!("{prefix}<ds:Manifest ID=\"manifest\"></ds:Manifest>{suffix}")
7968 });
7969
7970 let err = VerifyContext::new()
7971 .key(&AcceptingKey)
7972 .process_manifests(true)
7973 .verify(&xml)
7974 .expect_err("empty Manifest must fail verification");
7975 assert!(matches!(
7976 err,
7977 SignatureVerificationPipelineError::InvalidStructure {
7978 reason: "Manifest must contain at least one ds:Reference element child"
7979 }
7980 ));
7981 }
7982
7983 #[test]
7984 fn verify_context_ignores_unsigned_malformed_manifest_blocks() {
7985 let xml = signature_with_manifest_xml(true).replacen(
7986 "</ds:Object>",
7987 "</ds:Object><ds:Object><ds:Manifest>junk<ds:Foo/></ds:Manifest></ds:Object>",
7988 1,
7989 );
7990 let result = VerifyContext::new()
7991 .key(&AcceptingKey)
7992 .process_manifests(true)
7993 .verify(&xml)
7994 .expect("unsigned malformed Manifest must be ignored");
7995 assert_eq!(
7996 result.manifest_references.len(),
7997 1,
7998 "only signed Manifest references must be reported",
7999 );
8000 assert!(matches!(result.status, DsigStatus::Valid));
8001 }
8002
8003 #[test]
8004 fn verify_context_skips_ambiguous_manifest_id_blocks() {
8005 let xml = signature_with_manifest_xml(true).replacen(
8006 "</ds:Object>",
8007 "</ds:Object><ds:Object><ds:Manifest ID=\"manifest\">junk<ds:Foo/></ds:Manifest></ds:Object>",
8008 1,
8009 );
8010 let err = VerifyContext::new()
8011 .key(&RejectingKey)
8012 .process_manifests(true)
8013 .verify(&xml)
8014 .expect_err("ambiguous manifest IDs should make SignedInfo #manifest dereference fail");
8015 assert!(matches!(
8016 err,
8017 SignatureVerificationPipelineError::Reference(
8018 ReferenceProcessingError::UriDereference(
8019 crate::xmldsig::types::TransformError::ElementNotFound(id)
8020 )
8021 ) if id == "manifest"
8022 ));
8023 }
8024
8025 #[test]
8026 fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
8027 let xml = minimal_signature_xml("", "");
8028 let err = VerifyContext::new()
8029 .key(&RejectingKey)
8030 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
8031 .verify(&xml)
8032 .expect_err("implicit default C14N must be checked against allowlist");
8033 assert!(matches!(
8034 err,
8035 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
8036 operation: "verification transform",
8037 ..
8038 })
8039 ));
8040 }
8041
8042 #[test]
8043 fn verify_context_skips_resolver_when_reference_processing_fails() {
8044 let xml = minimal_signature_xml("", "");
8045 let result = VerifyContext::new()
8046 .key_resolver(&PanicResolver)
8047 .verify(&xml)
8048 .expect("reference digest mismatch should short-circuit before resolver");
8049 assert!(matches!(
8050 result.status,
8051 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
8052 ));
8053 }
8054
8055 #[test]
8056 fn verify_context_leaves_fail_fast_reference_tail_unexecuted() {
8057 let second_reference = r#"
8060 <ds:Reference URI="">
8061 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
8062 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
8063 </ds:Reference>"#;
8064 let xml = minimal_signature_xml("", "").replacen(
8065 " </ds:SignedInfo>",
8066 &format!("{second_reference}\n </ds:SignedInfo>"),
8067 1,
8068 );
8069
8070 let result = VerifyContext::new()
8071 .key(&AcceptingKey)
8072 .verify(&xml)
8073 .expect("digest mismatch must remain a verification result");
8074
8075 assert_eq!(result.signed_info_references.len(), 1);
8076 assert!(matches!(
8077 result.signed_info_references[0].status,
8078 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
8079 ));
8080 }
8081
8082 #[test]
8083 fn verify_context_reports_key_not_found_when_resolver_misses() {
8084 let xml = signature_with_target_reference("AQ==");
8085 let result = VerifyContext::new()
8086 .key_resolver(&MissingKeyResolver)
8087 .verify(&xml)
8088 .expect("resolver miss should report status, not pipeline error");
8089 assert!(matches!(
8090 result.status,
8091 DsigStatus::Invalid(FailureReason::KeyNotFound)
8092 ));
8093 assert_eq!(
8094 result.signed_info_references.len(),
8095 1,
8096 "KeyNotFound path must preserve SignedInfo reference diagnostics",
8097 );
8098 assert!(matches!(
8099 result.signed_info_references[0].status,
8100 DsigStatus::Valid
8101 ));
8102 }
8103
8104 #[test]
8105 fn verification_candidate_budget_covers_preset_and_custom_resolver_paths() {
8106 let xml = signature_with_target_reference("AQ==");
8109 let mut policy = crate::policy::VerificationPolicy::default();
8110 policy.resources.max_key_candidates = 0;
8111
8112 let preset_error = VerifyContext::new()
8113 .key(&RejectingKey)
8114 .policy(policy.clone())
8115 .verify(&xml)
8116 .expect_err("a preset key consumes one candidate");
8117 assert!(matches!(
8118 preset_error,
8119 SignatureVerificationPipelineError::Policy(
8120 crate::policy::PolicyViolation::ResourceLimit {
8121 resource: crate::policy::resource_name::KEY_CANDIDATES,
8122 maximum: 0,
8123 actual: 1,
8124 }
8125 )
8126 ));
8127
8128 let resolver_error = VerifyContext::new()
8129 .key_resolver(&PanicResolver)
8130 .policy(policy)
8131 .verify(&xml)
8132 .expect_err("a custom resolver requires candidate capacity before dispatch");
8133 assert!(matches!(
8134 resolver_error,
8135 SignatureVerificationPipelineError::Policy(
8136 crate::policy::PolicyViolation::ResourceLimit {
8137 resource: crate::policy::resource_name::KEY_CANDIDATES,
8138 maximum: 0,
8139 actual: 1,
8140 }
8141 )
8142 ));
8143 }
8144
8145 #[test]
8146 fn verification_candidate_budget_precedes_embedded_x509_parsing() {
8147 let first_certificate = base64::engine::general_purpose::STANDARD.encode(include_bytes!(
8150 "../../tests/fixtures/xmldsig/phaos-xmldsig-three/certs/rsa-cert.der"
8151 ));
8152 let xml = signature_with_target_reference("AQ==").replace(
8153 "</ds:SignatureValue>\n </ds:Signature>",
8154 &format!(
8155 "</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>"
8156 ),
8157 );
8158 let mut policy = crate::policy::VerificationPolicy::default();
8159 policy.resources.max_key_candidates = 1;
8160
8161 let error = VerifyContext::new()
8162 .policy(policy)
8163 .verify(&xml)
8164 .expect_err("candidate policy must run before embedded certificate parsing");
8165
8166 assert!(matches!(
8167 error,
8168 SignatureVerificationPipelineError::Policy(
8169 crate::policy::PolicyViolation::ResourceLimit {
8170 resource: crate::policy::resource_name::KEY_CANDIDATES,
8171 maximum: 1,
8172 actual: 2,
8173 }
8174 )
8175 ));
8176 }
8177
8178 #[test]
8179 fn verify_context_resolver_can_ignore_malformed_keyinfo_by_default() {
8180 let base_xml = signature_with_target_reference("AQ==");
8181 let xml = base_xml
8182 .replace(
8183 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
8184 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
8185 )
8186 .replace(
8187 "</ds:SignatureValue>\n </ds:Signature>",
8188 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
8189 );
8190
8191 let result = VerifyContext::new()
8192 .key_resolver(&MissingKeyResolver)
8193 .verify(&xml)
8194 .expect("resolver path should not hard-fail on advisory malformed KeyInfo by default");
8195 assert!(matches!(
8196 result.status,
8197 DsigStatus::Invalid(FailureReason::KeyNotFound)
8198 ));
8199 }
8200
8201 #[test]
8202 fn verify_context_resolver_can_opt_in_to_keyinfo_parse_failures() {
8203 let base_xml = signature_with_target_reference("AQ==");
8204 let xml = base_xml
8205 .replace(
8206 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
8207 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
8208 )
8209 .replace(
8210 "</ds:SignatureValue>\n </ds:Signature>",
8211 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
8212 );
8213
8214 let err = VerifyContext::new()
8215 .key_resolver(&ConsumingKeyInfoResolver)
8216 .verify(&xml)
8217 .expect_err("resolver opted into KeyInfo parsing, malformed KeyInfo must fail");
8218 assert!(matches!(
8219 err,
8220 SignatureVerificationPipelineError::ParseKeyInfo(_)
8221 ));
8222 }
8223
8224 #[test]
8225 fn verify_context_ignores_unsupported_retrieval_before_valid_key_source() {
8226 let xml = signature_with_target_reference("AQ==").replace(
8229 "</ds:SignatureValue>\n </ds:Signature>",
8230 r##"</ds:SignatureValue>
8231 <ds:KeyInfo>
8232 <ds:RetrievalMethod URI="#vendor" Type="urn:vendor:key">
8233 <ds:Transforms><ds:Transform Algorithm="urn:vendor:transform"/></ds:Transforms>
8234 </ds:RetrievalMethod>
8235 <ds:KeyName>fallback</ds:KeyName>
8236 </ds:KeyInfo>
8237 </ds:Signature>"##,
8238 );
8239
8240 let result = VerifyContext::new()
8241 .key_resolver(&FallbackKeyInfoResolver)
8242 .verify(&xml)
8243 .expect("unsupported advisory retrieval must not abort key resolution");
8244 assert_eq!(result.status, DsigStatus::Valid);
8245 }
8246
8247 #[test]
8248 fn verify_context_does_not_eagerly_fail_unused_retrieval_fallback() {
8249 let xml = signature_with_target_reference("AQ==").replace(
8252 "</ds:SignatureValue>\n </ds:Signature>",
8253 r#"</ds:SignatureValue>
8254 <ds:KeyInfo>
8255 <ds:KeyName>primary</ds:KeyName>
8256 <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
8257 </ds:KeyInfo>
8258 </ds:Signature>"#,
8259 );
8260
8261 let result = VerifyContext::new()
8262 .key_resolver(&EarlyKeyInfoResolver)
8263 .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
8264 .verify(&xml)
8265 .expect("an unused missing retrieval fallback must not abort verification");
8266
8267 assert_eq!(result.status, DsigStatus::Valid);
8268 }
8269
8270 #[test]
8271 fn verify_context_does_not_eagerly_fail_unused_same_document_x509_retrieval() {
8272 let xml = signature_with_target_reference("AQ==").replace(
8275 "</ds:SignatureValue>\n </ds:Signature>",
8276 r##"</ds:SignatureValue>
8277 <ds:KeyInfo>
8278 <ds:KeyName>primary</ds:KeyName>
8279 <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
8280 </ds:KeyInfo>
8281 </ds:Signature>"##,
8282 );
8283
8284 let result = VerifyContext::new()
8285 .key_resolver(&EarlyKeyInfoResolver)
8286 .verify(&xml)
8287 .expect("an unused missing X509Data retrieval fallback must not abort verification");
8288
8289 assert_eq!(result.status, DsigStatus::Valid);
8290 }
8291
8292 #[test]
8293 fn verify_context_reports_missing_same_document_x509_retrieval_without_fallback() {
8294 let xml = signature_with_target_reference("AQ==").replace(
8297 "</ds:SignatureValue>\n </ds:Signature>",
8298 r##"</ds:SignatureValue>
8299 <ds:KeyInfo>
8300 <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
8301 </ds:KeyInfo>
8302 </ds:Signature>"##,
8303 );
8304
8305 let error = VerifyContext::new()
8306 .key_resolver(&ConsumingKeyInfoResolver)
8307 .verify(&xml)
8308 .expect_err("a sole missing X509Data retrieval must remain an explicit error");
8309
8310 assert!(matches!(
8311 error,
8312 SignatureVerificationPipelineError::InvalidStructure {
8313 reason: "X509Data RetrievalMethod target is missing or ambiguous"
8314 }
8315 ));
8316 }
8317
8318 #[test]
8319 fn verify_context_does_not_eagerly_parse_unused_retrieval_fallback() {
8320 let xml = signature_with_target_reference("AQ==").replace(
8323 "</ds:SignatureValue>\n </ds:Signature>",
8324 r#"</ds:SignatureValue>
8325 <ds:KeyInfo>
8326 <ds:KeyName>primary</ds:KeyName>
8327 <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
8328 </ds:KeyInfo>
8329 </ds:Signature>"#,
8330 );
8331 let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
8332
8333 let result = VerifyContext::new()
8334 .key_resolver(&EarlyKeyInfoResolver)
8335 .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
8336 .external_resources(&resources)
8337 .verify(&xml)
8338 .expect("an unused malformed retrieval fallback must not abort verification");
8339
8340 assert_eq!(result.status, DsigStatus::Valid);
8341 }
8342
8343 #[test]
8344 fn verify_context_reports_missing_retrieval_when_no_key_source_resolves() {
8345 let xml = signature_with_target_reference("AQ==").replace(
8348 "</ds:SignatureValue>\n </ds:Signature>",
8349 r#"</ds:SignatureValue>
8350 <ds:KeyInfo>
8351 <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
8352 </ds:KeyInfo>
8353 </ds:Signature>"#,
8354 );
8355
8356 let error = VerifyContext::new()
8357 .key_resolver(&ConsumingKeyInfoResolver)
8358 .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
8359 .verify(&xml)
8360 .expect_err("a missing sole RetrievalMethod must remain an explicit error");
8361
8362 assert!(matches!(
8363 error,
8364 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
8365 crate::xmldsig::TransformError::UnsupportedUri(uri)
8366 )) if uri == "missing.der"
8367 ));
8368 }
8369
8370 #[test]
8371 fn verify_context_reports_malformed_retrieval_when_no_key_source_resolves() {
8372 let xml = signature_with_target_reference("AQ==").replace(
8375 "</ds:SignatureValue>\n </ds:Signature>",
8376 r#"</ds:SignatureValue>
8377 <ds:KeyInfo>
8378 <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
8379 </ds:KeyInfo>
8380 </ds:Signature>"#,
8381 );
8382 let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
8383
8384 let error = VerifyContext::new()
8385 .key_resolver(&ConsumingKeyInfoResolver)
8386 .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
8387 .external_resources(&resources)
8388 .verify(&xml)
8389 .expect_err("a malformed sole RetrievalMethod must remain a parse error");
8390
8391 assert!(matches!(
8392 error,
8393 SignatureVerificationPipelineError::ParseKeyInfo(_)
8394 ));
8395 }
8396
8397 #[test]
8398 fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
8399 let xml = signature_with_target_reference("@@@");
8400
8401 let err = VerifyContext::new()
8402 .key_resolver(&MissingKeyResolver)
8403 .verify(&xml)
8404 .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
8405 assert!(matches!(
8406 err,
8407 SignatureVerificationPipelineError::SignatureValueBase64(_)
8408 ));
8409 }
8410
8411 #[test]
8412 fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
8413 let xml = signature_with_target_reference("@@@");
8414
8415 let err = VerifyContext::new()
8416 .verify(&xml)
8417 .expect_err("invalid SignatureValue must remain a decode error");
8418 assert!(matches!(
8419 err,
8420 SignatureVerificationPipelineError::SignatureValueBase64(_)
8421 ));
8422 }
8423
8424 #[test]
8425 fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
8426 let references = vec![Reference {
8427 uri: None,
8428 id: None,
8429 ref_type: None,
8430 transforms: vec![],
8431 digest_method: DigestAlgorithm::Sha256,
8432 digest_value: vec![0; 32],
8433 }];
8434 let uri_types = UriTypeSet {
8435 allow_empty: false,
8436 allow_same_document: true,
8437 allow_external: false,
8438 };
8439
8440 let err = enforce_reference_policies(&references, uri_types, None)
8441 .expect_err("missing URI must fail before allow_empty policy is evaluated");
8442 assert!(matches!(
8443 err,
8444 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
8445 ));
8446 }
8447
8448 #[test]
8449 fn enforce_reference_policies_checks_only_terminal_binary_output() {
8450 let c14n = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI).unwrap();
8451 let allowed = HashSet::from([
8452 BASE64_TRANSFORM_URI.to_owned(),
8453 DEFAULT_IMPLICIT_C14N_URI.to_owned(),
8454 ]);
8455 let without_implicit_c14n = HashSet::from([BASE64_TRANSFORM_URI.to_owned()]);
8456
8457 for transforms in [
8458 vec![Transform::Base64Decode, Transform::C14n(c14n)],
8459 vec![Transform::Base64Decode, Transform::Base64Decode],
8460 ] {
8461 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, vec![0; 32]);
8462 enforce_reference_policies(
8463 std::slice::from_ref(&reference),
8464 UriTypeSet::default(),
8465 Some(&allowed),
8466 )
8467 .expect("terminal binary output must not require implicit C14N");
8468 }
8469
8470 let terminal_base64 = make_reference(
8471 "",
8472 vec![Transform::Base64Decode, Transform::Base64Decode],
8473 DigestAlgorithm::Sha256,
8474 vec![0; 32],
8475 );
8476 enforce_reference_policies(
8477 std::slice::from_ref(&terminal_base64),
8478 UriTypeSet::default(),
8479 Some(&without_implicit_c14n),
8480 )
8481 .expect("terminal Base64 output must not require implicit C14N");
8482
8483 let no_transforms = make_reference("", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
8484 let error = enforce_reference_policies(
8485 std::slice::from_ref(&no_transforms),
8486 UriTypeSet::default(),
8487 Some(&without_implicit_c14n),
8488 )
8489 .expect_err("a node-set result must require allowlisted implicit C14N");
8490 assert!(matches!(
8491 error,
8492 SignatureVerificationPipelineError::Policy(
8493 crate::policy::PolicyViolation::Algorithm {
8494 operation: "verification transform",
8495 ref algorithm,
8496 }
8497 )
8498 if algorithm == DEFAULT_IMPLICIT_C14N_URI
8499 ));
8500
8501 let detached = make_reference("urn:payload", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
8502 enforce_reference_policies(
8503 std::slice::from_ref(&detached),
8504 UriTypeSet::ALL,
8505 Some(&without_implicit_c14n),
8506 )
8507 .expect("external octets without transforms must not require implicit C14N");
8508
8509 let external_xpath = make_reference(
8510 "urn:payload",
8511 vec![Transform::XPath(
8512 super::super::transforms::XPathExpression::new("true()"),
8513 )],
8514 DigestAlgorithm::Sha256,
8515 vec![0; 32],
8516 );
8517 let error = enforce_reference_policies(
8518 std::slice::from_ref(&external_xpath),
8519 UriTypeSet::ALL,
8520 Some(&HashSet::from([XPATH_TRANSFORM_URI.to_owned()])),
8521 )
8522 .expect_err("external XML converted to a node-set must require implicit C14N");
8523 assert!(matches!(
8524 error,
8525 SignatureVerificationPipelineError::Policy(
8526 crate::policy::PolicyViolation::Algorithm {
8527 operation: "verification transform",
8528 ref algorithm,
8529 }
8530 )
8531 if algorithm == DEFAULT_IMPLICIT_C14N_URI
8532 ));
8533 }
8534
8535 #[test]
8536 fn stored_pre_digest_budget_counts_repeated_external_references() {
8537 let document =
8540 Document::parse("<ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"/>")
8541 .unwrap();
8542 let payload = vec![b'x'; 7];
8543 let digest = compute_digest(DigestAlgorithm::Sha256, &payload);
8544 let references = (0..5)
8545 .map(|_| {
8546 make_reference(
8547 "urn:repeated",
8548 Vec::new(),
8549 DigestAlgorithm::Sha256,
8550 digest.clone(),
8551 )
8552 })
8553 .collect::<Vec<_>>();
8554 let resources = HashMap::from([("urn:repeated".to_owned(), payload)]);
8555 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
8556 let transform_budget = TransformExecutionBudget::default();
8557 let canonicalized_data_budget = CanonicalizedDataBudget::with_limit(32);
8558 let execution = ReferenceExecutionContext {
8559 store_pre_digest: true,
8560 transform_options: TransformOptions::default(),
8561 transform_budget: &transform_budget,
8562 canonicalized_data_budget: &canonicalized_data_budget,
8563 provider: crate::provider::default_provider(),
8564 };
8565
8566 let error = process_all_references_with_options(
8567 &references,
8568 &resolver,
8569 document.root_element(),
8570 &execution,
8571 )
8572 .expect_err(
8573 "retained diagnostics must not multiply one external allocation past the aggregate cap",
8574 );
8575 assert!(matches!(
8576 error,
8577 ReferenceProcessingError::Policy(crate::policy::PolicyViolation::ResourceLimit {
8578 resource: "canonicalized bytes",
8579 maximum: 32,
8580 ..
8581 })
8582 ));
8583 }
8584
8585 #[test]
8586 fn operation_memoizes_repeated_external_resource_identity() {
8587 let document = XmlDocument::parse("<root/>").expect("fixture must parse");
8590 let resources = HashMap::from([("urn:repeated".to_owned(), vec![b'x'; 8 * 1_024])]);
8591 let reference = make_reference(
8592 "urn:repeated",
8593 Vec::new(),
8594 DigestAlgorithm::Sha256,
8595 vec![0; 32],
8596 );
8597 let budgets = VerificationOperationBudgets::with_transforms(
8598 &crate::policy::VerificationPolicy::default(),
8599 TransformExecutionBudget::default(),
8600 );
8601
8602 document.with_view(|view| {
8603 let resolver =
8604 UriReferenceResolver::new(view.document()).with_external_resources(&resources);
8605 let first =
8606 budgets.resource_identity_for_reference(&reference, 0, &resolver, view, None);
8607 let second =
8608 budgets.resource_identity_for_reference(&reference, 1, &resolver, view, None);
8609
8610 assert_eq!(first, second);
8611 assert_eq!(budgets.external_resource_identities.borrow().len(), 1);
8612 });
8613 }
8614
8615 #[test]
8616 fn canonical_signed_info_obeys_policy_without_diagnostic_retention() {
8617 let xml = signature_with_target_reference("AQ==");
8621 let marker = "<ds:SignatureMethod";
8622 let padding = " ".repeat(1_025);
8623 let xml = xml.replacen(marker, &format!("{padding}{marker}"), 1);
8624 let policy = crate::policy::VerificationPolicy {
8625 resources: crate::policy::ResourcePolicy {
8626 max_canonicalized_bytes: 1_024,
8627 ..crate::policy::ResourcePolicy::default()
8628 },
8629 ..crate::policy::VerificationPolicy::default()
8630 };
8631
8632 let error = VerifyContext::new()
8633 .key(&AcceptingKey)
8634 .policy(policy)
8635 .verify(&xml)
8636 .expect_err("canonicalized SignedInfo must remain policy-bounded");
8637
8638 assert!(matches!(
8639 error,
8640 SignatureVerificationPipelineError::Policy(
8641 crate::policy::PolicyViolation::ResourceLimit {
8642 resource: "canonicalized bytes",
8643 ..
8644 }
8645 )
8646 ));
8647 }
8648
8649 #[test]
8650 fn fragmented_pq_signature_value_decodes_at_exact_wire_limit() {
8651 let parameter = super::super::pq_algorithm::PqAlgorithm::SlhDsaSha2_256f;
8654 let bytes = vec![0x5a; parameter.signature_len()];
8655 let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes);
8656 let mut xml = String::from("<SignatureValue>");
8657 for chunk in encoded.as_bytes().chunks(4) {
8658 xml.push_str(std::str::from_utf8(chunk).unwrap());
8659 xml.push_str("<!--split-->\n");
8660 }
8661 xml.push_str("</SignatureValue>");
8662 let document = Document::parse(&xml).unwrap();
8663 let algorithm = SignatureAlgorithm::PostQuantum(parameter);
8664 assert_eq!(
8665 decode_signature_value(
8666 document.root_element(),
8667 algorithm,
8668 &crate::policy::ResourcePolicy::default()
8669 )
8670 .unwrap(),
8671 bytes
8672 );
8673 let oversized = xml.replace("</SignatureValue>", "AAAA</SignatureValue>");
8674 let document = Document::parse(&oversized).unwrap();
8675 assert!(matches!(
8676 decode_signature_value(
8677 document.root_element(),
8678 algorithm,
8679 &crate::policy::ResourcePolicy::default()
8680 ),
8681 Err(SignatureVerificationPipelineError::InvalidStructure {
8682 reason: "SignatureValue exceeds maximum allowed length"
8683 })
8684 ));
8685 }
8686
8687 #[test]
8688 fn push_normalized_signature_text_rejects_form_feed() {
8689 let mut normalized = Vec::new();
8690 let mut raw_text_len = 0usize;
8691 let err = push_normalized_signature_text(
8692 "ab\u{000C}cd",
8693 &mut raw_text_len,
8694 &mut normalized,
8695 SignatureAlgorithm::RsaSha256,
8696 &crate::policy::ResourcePolicy::default(),
8697 )
8698 .expect_err("form-feed must not be treated as XML base64 whitespace");
8699 assert!(matches!(
8700 err,
8701 SignatureVerificationPipelineError::SignatureValueBase64(
8702 base64::DecodeError::InvalidByte(_, 0x0C)
8703 )
8704 ));
8705 }
8706
8707 #[test]
8708 fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
8709 let mut normalized = vec![b'A'; MAX_SIGNATURE_VALUE_LEN - 1];
8710 let mut raw_text_len = normalized.len();
8711 let err = push_normalized_signature_text(
8712 "é",
8713 &mut raw_text_len,
8714 &mut normalized,
8715 SignatureAlgorithm::RsaSha256,
8716 &crate::policy::ResourcePolicy::default(),
8717 )
8718 .expect_err("multibyte characters must not bypass byte-size limit");
8719 assert!(matches!(
8720 err,
8721 SignatureVerificationPipelineError::InvalidStructure {
8722 reason: "SignatureValue exceeds maximum allowed length"
8723 }
8724 ));
8725 }
8726
8727 #[test]
8730 fn reference_with_correct_digest_passes() {
8731 let xml = r##"<root>
8734 <data>hello world</data>
8735 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
8736 <ds:SignedInfo/>
8737 </ds:Signature>
8738 </root>"##;
8739 let doc = Document::parse(xml).unwrap();
8740 let resolver = UriReferenceResolver::new(&doc);
8741 let sig_node = doc
8742 .descendants()
8743 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
8744 .unwrap();
8745
8746 let initial_data = resolver.dereference("").unwrap();
8748 let transforms = vec![
8749 Transform::Enveloped,
8750 Transform::C14n(
8751 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
8752 .unwrap(),
8753 ),
8754 ];
8755 let pre_digest_bytes =
8756 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
8757 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
8758
8759 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
8761
8762 let result = process_reference(
8763 &reference,
8764 &resolver,
8765 sig_node,
8766 ReferenceSet::SignedInfo,
8767 0,
8768 false,
8769 )
8770 .unwrap();
8771 assert!(
8772 matches!(result.status, DsigStatus::Valid),
8773 "digest should match"
8774 );
8775 assert!(result.pre_digest_data.is_none());
8776 }
8777
8778 #[test]
8779 fn reference_with_wrong_digest_fails() {
8780 let xml = r##"<root>
8781 <data>hello</data>
8782 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8783 <ds:SignedInfo/>
8784 </ds:Signature>
8785 </root>"##;
8786 let doc = Document::parse(xml).unwrap();
8787 let resolver = UriReferenceResolver::new(&doc);
8788 let sig_node = doc
8789 .descendants()
8790 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
8791 .unwrap();
8792
8793 let transforms = vec![Transform::Enveloped];
8794 let wrong_digest = vec![0u8; 32];
8796 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
8797
8798 let result = process_reference(
8799 &reference,
8800 &resolver,
8801 sig_node,
8802 ReferenceSet::SignedInfo,
8803 0,
8804 false,
8805 )
8806 .unwrap();
8807 assert!(matches!(
8808 result.status,
8809 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
8810 ));
8811 }
8812
8813 #[test]
8814 fn reference_with_wrong_digest_preserves_supplied_ref_index() {
8815 let xml = r##"<root>
8816 <data>hello</data>
8817 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8818 <ds:SignedInfo/>
8819 </ds:Signature>
8820 </root>"##;
8821 let doc = Document::parse(xml).unwrap();
8822 let resolver = UriReferenceResolver::new(&doc);
8823 let sig_node = doc
8824 .descendants()
8825 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
8826 .unwrap();
8827
8828 let reference = make_reference(
8829 "",
8830 vec![Transform::Enveloped],
8831 DigestAlgorithm::Sha256,
8832 vec![0u8; 32],
8833 );
8834 let result = process_reference(
8835 &reference,
8836 &resolver,
8837 sig_node,
8838 ReferenceSet::SignedInfo,
8839 7,
8840 false,
8841 )
8842 .unwrap();
8843 assert!(matches!(
8844 result.status,
8845 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
8846 ));
8847 }
8848
8849 #[test]
8850 fn reference_stores_pre_digest_data() {
8851 let xml = "<root><child>text</child></root>";
8852 let doc = Document::parse(xml).unwrap();
8853 let resolver = UriReferenceResolver::new(&doc);
8854
8855 let initial_data = resolver.dereference("").unwrap();
8857 let pre_digest =
8858 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
8859 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
8860
8861 let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
8862 let result = process_reference(
8863 &reference,
8864 &resolver,
8865 doc.root_element(),
8866 ReferenceSet::SignedInfo,
8867 0,
8868 true,
8869 )
8870 .unwrap();
8871
8872 assert!(matches!(result.status, DsigStatus::Valid));
8873 assert!(result.pre_digest_data.is_some());
8874 assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
8875 }
8876
8877 #[test]
8880 fn reference_with_id_uri() {
8881 let xml = r##"<root>
8882 <item ID="target">specific content</item>
8883 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8884 <ds:SignedInfo/>
8885 </ds:Signature>
8886 </root>"##;
8887 let doc = Document::parse(xml).unwrap();
8888 let resolver = UriReferenceResolver::new(&doc);
8889 let sig_node = doc
8890 .descendants()
8891 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
8892 .unwrap();
8893
8894 let initial_data = resolver.dereference("#target").unwrap();
8896 let transforms = vec![Transform::C14n(
8897 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
8898 .unwrap(),
8899 )];
8900 let pre_digest =
8901 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
8902 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
8903
8904 let reference = make_reference(
8905 "#target",
8906 transforms,
8907 DigestAlgorithm::Sha256,
8908 expected_digest,
8909 );
8910 let result = process_reference(
8911 &reference,
8912 &resolver,
8913 sig_node,
8914 ReferenceSet::SignedInfo,
8915 0,
8916 false,
8917 )
8918 .unwrap();
8919 assert!(matches!(result.status, DsigStatus::Valid));
8920 }
8921
8922 #[test]
8923 fn reference_with_nonexistent_id_fails() {
8924 let xml = "<root><child/></root>";
8925 let doc = Document::parse(xml).unwrap();
8926 let resolver = UriReferenceResolver::new(&doc);
8927
8928 let reference =
8929 make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
8930 let result = process_reference(
8931 &reference,
8932 &resolver,
8933 doc.root_element(),
8934 ReferenceSet::SignedInfo,
8935 0,
8936 false,
8937 );
8938 assert!(result.is_err());
8939 }
8940
8941 #[test]
8942 fn reference_with_absent_uri_fails_closed() {
8943 let xml = "<root><child>text</child></root>";
8944 let doc = Document::parse(xml).unwrap();
8945 let resolver = UriReferenceResolver::new(&doc);
8946
8947 let reference = Reference {
8948 uri: None, id: None,
8950 ref_type: None,
8951 transforms: vec![],
8952 digest_method: DigestAlgorithm::Sha256,
8953 digest_value: vec![0; 32],
8954 };
8955
8956 let result = process_reference(
8957 &reference,
8958 &resolver,
8959 doc.root_element(),
8960 ReferenceSet::SignedInfo,
8961 0,
8962 false,
8963 );
8964 assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
8965 }
8966
8967 #[test]
8970 fn all_references_pass() {
8971 let xml = "<root><child>text</child></root>";
8972 let doc = Document::parse(xml).unwrap();
8973 let resolver = UriReferenceResolver::new(&doc);
8974
8975 let initial_data = resolver.dereference("").unwrap();
8977 let pre_digest =
8978 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
8979 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
8980
8981 let refs = vec![
8982 make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
8983 make_reference("", vec![], DigestAlgorithm::Sha256, digest),
8984 ];
8985
8986 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
8987 assert!(result.all_valid());
8988 assert_eq!(result.results.len(), 2);
8989 assert!(result.first_failure.is_none());
8990 }
8991
8992 #[test]
8993 fn reference_processing_shares_xpath_work_across_references() {
8994 let document = Document::parse("<root/>").unwrap();
8997 let resolver = UriReferenceResolver::new(&document);
8998 let transform = Transform::XPath(super::super::transforms::XPathExpression::new("true()"));
8999 let initial_data = resolver.dereference("").unwrap();
9000 let pre_digest = crate::xmldsig::execute_transforms(
9001 document.root_element(),
9002 initial_data,
9003 std::slice::from_ref(&transform),
9004 )
9005 .unwrap();
9006 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
9007 let references = vec![
9008 make_reference(
9009 "",
9010 vec![transform.clone()],
9011 DigestAlgorithm::Sha256,
9012 digest.clone(),
9013 ),
9014 make_reference("", vec![transform], DigestAlgorithm::Sha256, digest),
9015 ];
9016 let budget = TransformExecutionBudget::with_xpath_limit(12);
9017 let canonicalized_data_budget = CanonicalizedDataBudget::default();
9018 let execution = ReferenceExecutionContext {
9019 store_pre_digest: false,
9020 transform_options: TransformOptions::default(),
9021 transform_budget: &budget,
9022 canonicalized_data_budget: &canonicalized_data_budget,
9023 provider: crate::provider::default_provider(),
9024 };
9025
9026 let error = process_all_references_with_options(
9027 &references,
9028 &resolver,
9029 document.root_element(),
9030 &execution,
9031 )
9032 .expect_err("the second Reference must consume the first Reference's XPath work");
9033
9034 assert!(matches!(
9035 error,
9036 ReferenceProcessingError::Transform(TransformError::Policy(
9037 crate::policy::PolicyViolation::ResourceLimit {
9038 resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
9039 ..
9040 }
9041 ))
9042 ));
9043 }
9044
9045 #[test]
9046 fn reference_processing_shares_node_set_materialization_across_references() {
9047 let document = Document::parse(
9051 r#"<root xmlns:n="urn:0123456789"><target Id="selected">payload</target></root>"#,
9052 )
9053 .unwrap();
9054 let resolver = UriReferenceResolver::new(&document);
9055 let initial_data = resolver.dereference("#selected").unwrap();
9056 let pre_digest =
9057 crate::xmldsig::execute_transforms(document.root_element(), initial_data, &[]).unwrap();
9058 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
9059 let references = vec![
9060 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest.clone()),
9061 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest),
9062 ];
9063 let budget = TransformExecutionBudget::with_node_set_materialization_limit(30);
9064 let canonicalized_data_budget = CanonicalizedDataBudget::default();
9065 let execution = ReferenceExecutionContext {
9066 store_pre_digest: false,
9067 transform_options: TransformOptions::default(),
9068 transform_budget: &budget,
9069 canonicalized_data_budget: &canonicalized_data_budget,
9070 provider: crate::provider::default_provider(),
9071 };
9072
9073 let error = process_all_references_with_options(
9074 &references,
9075 &resolver,
9076 document.root_element(),
9077 &execution,
9078 )
9079 .expect_err("the second Reference must consume the first Reference's materialization work");
9080
9081 assert!(matches!(
9082 error,
9083 ReferenceProcessingError::UriDereference(TransformError::Policy(
9084 crate::policy::PolicyViolation::ResourceLimit {
9085 resource: crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
9086 ..
9087 }
9088 ))
9089 ));
9090 }
9091
9092 #[test]
9093 fn fail_fast_on_first_mismatch() {
9094 let xml = "<root><child>text</child></root>";
9095 let doc = Document::parse(xml).unwrap();
9096 let resolver = UriReferenceResolver::new(&doc);
9097
9098 let wrong_digest = vec![0u8; 32];
9099 let refs = vec![
9100 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
9101 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
9103 ];
9104
9105 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
9106 assert!(!result.all_valid());
9107 assert_eq!(result.first_failure, Some(0));
9108 assert_eq!(result.results.len(), 1);
9110 assert!(matches!(
9111 result.results[0].status,
9112 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
9113 ));
9114 }
9115
9116 #[test]
9117 fn fail_fast_second_reference() {
9118 let xml = "<root><child>text</child></root>";
9119 let doc = Document::parse(xml).unwrap();
9120 let resolver = UriReferenceResolver::new(&doc);
9121
9122 let initial_data = resolver.dereference("").unwrap();
9124 let pre_digest =
9125 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
9126 let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
9127 let wrong_digest = vec![0u8; 32];
9128
9129 let refs = vec![
9130 make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
9131 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
9132 ];
9133
9134 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
9135 assert!(!result.all_valid());
9136 assert_eq!(result.first_failure, Some(1));
9137 assert_eq!(result.results.len(), 2);
9139 assert!(matches!(result.results[0].status, DsigStatus::Valid));
9140 assert!(matches!(
9141 result.results[1].status,
9142 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
9143 ));
9144 }
9145
9146 #[test]
9147 fn empty_references_list() {
9148 let xml = "<root/>";
9149 let doc = Document::parse(xml).unwrap();
9150 let resolver = UriReferenceResolver::new(&doc);
9151
9152 let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
9153 assert!(result.all_valid());
9154 assert!(result.results.is_empty());
9155 }
9156
9157 #[test]
9160 fn reference_sha1_digest() {
9161 let xml = "<root>content</root>";
9162 let doc = Document::parse(xml).unwrap();
9163 let resolver = UriReferenceResolver::new(&doc);
9164
9165 let initial_data = resolver.dereference("").unwrap();
9166 let pre_digest =
9167 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
9168 let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
9169
9170 let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
9171 let result = process_reference(
9172 &reference,
9173 &resolver,
9174 doc.root_element(),
9175 ReferenceSet::SignedInfo,
9176 0,
9177 false,
9178 )
9179 .unwrap();
9180 assert!(matches!(result.status, DsigStatus::Valid));
9181 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
9182 }
9183
9184 #[test]
9185 fn reference_sha512_digest() {
9186 let xml = "<root>content</root>";
9187 let doc = Document::parse(xml).unwrap();
9188 let resolver = UriReferenceResolver::new(&doc);
9189
9190 let initial_data = resolver.dereference("").unwrap();
9191 let pre_digest =
9192 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
9193 let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
9194
9195 let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
9196 let result = process_reference(
9197 &reference,
9198 &resolver,
9199 doc.root_element(),
9200 ReferenceSet::SignedInfo,
9201 0,
9202 false,
9203 )
9204 .unwrap();
9205 assert!(matches!(result.status, DsigStatus::Valid));
9206 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
9207 }
9208
9209 #[test]
9212 fn saml_enveloped_reference_processing() {
9213 let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
9215 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
9216 ID="_resp1">
9217 <saml:Assertion ID="_assert1">
9218 <saml:Subject>user@example.com</saml:Subject>
9219 </saml:Assertion>
9220 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
9221 <ds:SignedInfo>
9222 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
9223 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
9224 <ds:Reference URI="">
9225 <ds:Transforms>
9226 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
9227 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
9228 </ds:Transforms>
9229 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
9230 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
9231 </ds:Reference>
9232 </ds:SignedInfo>
9233 <ds:SignatureValue>fakesig==</ds:SignatureValue>
9234 </ds:Signature>
9235 </samlp:Response>"##;
9236 let doc = Document::parse(xml).unwrap();
9237 let resolver = UriReferenceResolver::new(&doc);
9238 let sig_node = doc
9239 .descendants()
9240 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
9241 .unwrap();
9242
9243 let signed_info_node = sig_node
9245 .children()
9246 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
9247 .unwrap();
9248 let signed_info = parse_signed_info(signed_info_node).unwrap();
9249 let reference = &signed_info.references[0];
9250
9251 let initial_data = resolver.dereference("").unwrap();
9253 let pre_digest =
9254 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
9255 .unwrap();
9256 let correct_digest = compute_digest(reference.digest_method, &pre_digest);
9257
9258 let corrected_ref = make_reference(
9260 "",
9261 reference.transforms.clone(),
9262 reference.digest_method,
9263 correct_digest,
9264 );
9265
9266 let result = process_reference(
9268 &corrected_ref,
9269 &resolver,
9270 sig_node,
9271 ReferenceSet::SignedInfo,
9272 0,
9273 true,
9274 )
9275 .unwrap();
9276 assert!(
9277 matches!(result.status, DsigStatus::Valid),
9278 "SAML reference should verify"
9279 );
9280 assert!(result.pre_digest_data.is_some());
9281
9282 let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
9284 assert!(
9285 pre_digest_str.contains("samlp:Response"),
9286 "pre-digest should contain Response"
9287 );
9288 assert!(
9289 !pre_digest_str.contains("SignatureValue"),
9290 "pre-digest should NOT contain Signature"
9291 );
9292 }
9293
9294 #[test]
9295 fn pipeline_missing_signed_info_returns_missing_element() {
9296 let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
9297
9298 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
9299 .expect_err("missing SignedInfo must fail before crypto stage");
9300 assert!(matches!(
9301 err,
9302 SignatureVerificationPipelineError::MissingElement {
9303 element: "SignedInfo"
9304 }
9305 ));
9306 }
9307
9308 #[test]
9309 fn pipeline_multiple_signature_elements_are_rejected() {
9310 let xml = r#"
9311<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
9312 <ds:Signature>
9313 <ds:SignedInfo/>
9314 </ds:Signature>
9315 <ds:Signature/>
9316</root>
9317"#;
9318
9319 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
9320 .expect_err("multiple signatures must fail closed");
9321 assert!(matches!(
9322 err,
9323 SignatureVerificationPipelineError::InvalidStructure {
9324 reason: "Signature must appear exactly once in document",
9325 }
9326 ));
9327 }
9328
9329 #[test]
9330 fn pipeline_start_node_limits_signature_cardinality_to_its_subtree() {
9331 let xml = r#"
9334<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
9335 <scope Id="selected"><ds:Signature/></scope>
9336 <scope Id="other"><ds:Signature/></scope>
9337</root>
9338"#;
9339 let err = VerifyContext::new()
9340 .start_node_id("selected")
9341 .verify(xml)
9342 .expect_err("the selected Signature remains structurally incomplete");
9343 assert!(matches!(
9344 err,
9345 SignatureVerificationPipelineError::MissingElement {
9346 element: "SignedInfo"
9347 }
9348 ));
9349 }
9350
9351 #[test]
9352 fn pipeline_reports_keyinfo_parse_error() {
9353 let xml = r#"
9354<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
9355 xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
9356 <ds:SignedInfo>
9357 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
9358 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
9359 <ds:Reference URI="">
9360 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
9361 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
9362 </ds:Reference>
9363 </ds:SignedInfo>
9364 <ds:SignatureValue>AA==</ds:SignatureValue>
9365 <ds:KeyInfo>
9366 <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
9367 </ds:KeyInfo>
9368</ds:Signature>
9369"#;
9370
9371 let err = VerifyContext::new().verify(xml).expect_err(
9372 "invalid KeyInfo must map to ParseKeyInfo when no explicit key is supplied",
9373 );
9374 assert!(matches!(
9375 err,
9376 SignatureVerificationPipelineError::ParseKeyInfo(_)
9377 ));
9378 }
9379
9380 #[test]
9381 fn pipeline_ignores_malformed_keyinfo_when_explicit_key_is_supplied() {
9382 let base_xml = signature_with_target_reference("AQ==");
9383 let xml = base_xml
9384 .replace(
9385 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
9386 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
9387 )
9388 .replace(
9389 "</ds:SignatureValue>\n </ds:Signature>",
9390 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
9391 );
9392
9393 let result = VerifyContext::new()
9394 .key(&RejectingKey)
9395 .verify(&xml)
9396 .expect("explicit key path should not fail on malformed KeyInfo");
9397 assert!(matches!(
9398 result.status,
9399 DsigStatus::Invalid(FailureReason::SignatureMismatch)
9400 ));
9401 }
9402
9403 #[test]
9404 fn pipeline_rejects_foreign_element_children_under_signature() {
9405 let base_xml = signature_with_target_reference("AQ==");
9406 let xml = base_xml
9407 .replace(
9408 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
9409 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:foo="urn:example:foo">"#,
9410 )
9411 .replace(
9412 "</ds:SignedInfo>\n <ds:SignatureValue>",
9413 "</ds:SignedInfo>\n <foo:Bar/>\n <ds:SignatureValue>",
9414 );
9415
9416 let err = VerifyContext::new()
9417 .key(&RejectingKey)
9418 .verify(&xml)
9419 .expect_err("foreign element children under Signature must fail closed");
9420 assert!(matches!(
9421 err,
9422 SignatureVerificationPipelineError::InvalidStructure {
9423 reason: "Signature must contain only XMLDSIG element children",
9424 }
9425 ));
9426 }
9427
9428 #[test]
9429 fn pipeline_rejects_non_whitespace_mixed_content_under_signature() {
9430 let base_xml = signature_with_target_reference("AQ==");
9431 let xml = base_xml.replace(
9432 "</ds:SignedInfo>\n <ds:SignatureValue>",
9433 "</ds:SignedInfo>\n oops\n <ds:SignatureValue>",
9434 );
9435
9436 let err = VerifyContext::new()
9437 .key(&RejectingKey)
9438 .verify(&xml)
9439 .expect_err("non-whitespace mixed content under Signature must fail closed");
9440 assert!(matches!(
9441 err,
9442 SignatureVerificationPipelineError::InvalidStructure {
9443 reason: "Signature must not contain non-whitespace mixed content",
9444 }
9445 ));
9446 }
9447
9448 #[test]
9449 fn pipeline_rejects_keyinfo_out_of_order() {
9450 let base_xml = signature_with_target_reference("AQ==");
9451 let xml = base_xml.replace(
9452 "</ds:SignatureValue>\n </ds:Signature>",
9453 "</ds:SignatureValue>\n <ds:Object/>\n <ds:KeyInfo><ds:KeyName>late</ds:KeyName></ds:KeyInfo>\n </ds:Signature>",
9454 );
9455
9456 let err = VerifyContext::new()
9457 .key(&RejectingKey)
9458 .verify(&xml)
9459 .expect_err("KeyInfo after Object must be rejected by Signature child order checks");
9460 assert!(matches!(
9461 err,
9462 SignatureVerificationPipelineError::InvalidStructure {
9463 reason: "KeyInfo must be the third element child of Signature when present"
9464 }
9465 ));
9466 }
9467
9468 #[test]
9469 fn pipeline_accepts_comments_and_processing_instructions_under_signature() {
9470 let xml = r#"
9471<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
9472 <?dbg keep ?>
9473 <!-- signature metadata -->
9474 <ds:SignedInfo>
9475 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
9476 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
9477 <ds:Reference URI="">
9478 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
9479 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
9480 </ds:Reference>
9481 </ds:SignedInfo>
9482 <!-- between required children -->
9483 <ds:SignatureValue>AA==</ds:SignatureValue>
9484</ds:Signature>
9485"#;
9486
9487 let doc = Document::parse(xml).expect("test XML must parse");
9488 let signature_node = doc.root_element();
9489 let parsed = parse_signature_children(signature_node)
9490 .expect("comment/PI nodes under Signature must be ignored");
9491
9492 assert_eq!(parsed.signed_info_node.tag_name().name(), "SignedInfo");
9493 assert_eq!(
9494 parsed.signature_value_node.tag_name().name(),
9495 "SignatureValue"
9496 );
9497 assert!(parsed.key_info_node.is_none());
9498 }
9499}