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 super::parse::validate_x509_digest_policy(node, &|algorithm| {
2118 ctx.policy.check_digest_algorithm(algorithm)
2119 })?;
2120 parse_key_info_with_policy_budgets(
2121 node,
2122 ctx.provider,
2123 operation.budgets().transforms.xml_base_resolution(),
2124 &ctx.policy.resources,
2125 ctx.policy
2126 .key_trust
2127 .certificate_signature_algorithms
2128 .as_ref(),
2129 )
2130 })
2131 .transpose()
2132 .map_err(map_key_info_parse_error)?
2133 } else {
2134 None
2135 };
2136
2137 let signed_info = parse_signed_info_with_xpath_budget(
2138 signed_info_node,
2139 &mut operation.budgets().xpath_parse.borrow_mut(),
2140 &ctx.policy.resources,
2141 )?;
2142 if signed_info.references.len() > ctx.policy.resources.max_references {
2143 return Err(crate::policy::PolicyViolation::ResourceLimit {
2144 resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
2145 maximum: ctx.policy.resources.max_references,
2146 actual: signed_info.references.len(),
2147 }
2148 .into());
2149 }
2150 for reference in &signed_info.references {
2151 if reference.transforms.len() > ctx.policy.resources.max_transforms_per_reference {
2152 return Err(crate::policy::PolicyViolation::ResourceLimit {
2153 resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
2154 maximum: ctx.policy.resources.max_transforms_per_reference,
2155 actual: reference.transforms.len(),
2156 }
2157 .into());
2158 }
2159 }
2160 ctx.policy
2161 .check_signature_algorithm(signed_info.signature_method)?;
2162 if let Some(full_output_bits) = signed_info.signature_method.hmac_output_bits() {
2167 ctx.policy.hmac.validate_output(
2168 signed_info.signature_method,
2169 signed_info
2170 .hmac_output_length_bits
2171 .unwrap_or(full_output_bits),
2172 )?;
2173 }
2174 for reference in &signed_info.references {
2175 ctx.policy.check_digest_algorithm(reference.digest_method)?;
2176 }
2177 enforce_reference_policies(
2178 &signed_info.references,
2179 ctx.policy.uris.references,
2180 ctx.allowed_transform_uris(),
2181 )?;
2182 enforce_transform_allowed(ctx.allowed_transform_uris(), signed_info.c14n_method.uri())?;
2183
2184 if let Some(resources) = ctx.external_resources {
2185 validate_external_resource_map(
2186 resources,
2187 ctx.policy.resources.max_external_resource_bytes,
2188 ctx.policy.resources.max_external_resource_total_bytes,
2189 )
2190 .map_err(|error| match error {
2191 ExternalResourceMapError::Policy(error) => error.into(),
2192 ExternalResourceMapError::TotalLengthOverflow => {
2193 SignatureVerificationPipelineError::InvalidStructure {
2194 reason: "external resource total length overflow",
2195 }
2196 }
2197 })?;
2198 }
2199 let remaining_reference_capacity = ctx
2200 .policy
2201 .resources
2202 .max_references
2203 .checked_sub(signed_info.references.len())
2204 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2205 reason: "SignedInfo exceeds the per-signature Reference limit",
2206 })?;
2207 let plan_nodes = compile_verification_operation_plan(
2208 operation,
2209 view,
2210 signature_node,
2211 &signed_info.references,
2212 resolver,
2213 )?;
2214 let signature_identity =
2215 OperationResourceIdentity::DocumentNode(view.node_identity(signature_node));
2216 operation.run_with_resource(plan_nodes.document, &signature_identity, || {
2217 operation
2218 .validate_document_view(view)
2219 .map_err(map_verification_plan_error)
2220 })?;
2221 let observed_key_resources = operation.budgets().external_resource_set_identity(resolver);
2222 let retrieval_materialization = operation.run_with_resource(
2223 plan_nodes.key_materialization,
2224 &observed_key_resources,
2225 || {
2226 if let Some(info) = key_info.as_mut() {
2227 let budgets = operation.budgets();
2228 let mut xpath_parse = budgets.xpath_parse.borrow_mut();
2229 let mut retrieval_budgets = RetrievalMaterializationBudgets {
2230 xpath_parse: &mut xpath_parse,
2231 execution: &budgets.transforms,
2232 resources: &ctx.policy.resources,
2233 certificate_signature_algorithms: ctx
2234 .policy
2235 .key_trust
2236 .certificate_signature_algorithms
2237 .as_ref(),
2238 xml_backend: ctx.xml_backend,
2239 };
2240 let mut materialization = budgets.key_info_materialization.borrow_mut();
2241 let mut outcome = materialize_key_info_references_with_budgets(
2242 info,
2243 resolver,
2244 &ctx.policy,
2245 ctx.provider,
2246 &mut retrieval_budgets,
2247 &mut materialization,
2248 )?;
2249 outcome.merge(materialize_retrieval_methods_with_budgets(
2250 info,
2251 resolver,
2252 &ctx.policy,
2253 ctx.provider,
2254 &mut retrieval_budgets,
2255 &mut materialization.candidate_work,
2256 )?);
2257 Ok::<_, SignatureVerificationPipelineError>(outcome)
2258 } else {
2259 Ok(RetrievalMaterialization::default())
2260 }
2261 },
2262 )?;
2263 let mut reference_results = Vec::with_capacity(signed_info.references.len());
2264 let mut first_failure = None;
2265 for (index, (node, reference)) in plan_nodes
2266 .digests
2267 .iter()
2268 .copied()
2269 .zip(&signed_info.references)
2270 .enumerate()
2271 {
2272 let observed = operation.budgets().resource_identity_for_reference(
2273 reference,
2274 index,
2275 resolver,
2276 view,
2277 reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2278 );
2279 let mut result = operation.run_with_resource(node, &observed, || {
2280 let budgets = operation.budgets();
2281 let execution = ReferenceExecutionContext {
2282 store_pre_digest: ctx.store_pre_digest,
2283 transform_options: ctx.transform_options(),
2284 transform_budget: &budgets.transforms,
2285 canonicalized_data_budget: &budgets.canonicalized,
2286 provider: ctx.provider,
2287 };
2288 process_reference_with_options(
2289 reference,
2290 resolver,
2291 signature_node,
2292 ReferenceSet::SignedInfo,
2293 index,
2294 reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2295 &execution,
2296 )
2297 .map_err(SignatureVerificationPipelineError::from)
2298 })?;
2299 bind_reference_evidence(
2300 &mut result,
2301 reference,
2302 view,
2303 reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2304 &observed,
2305 );
2306 let accepted = result.status == DsigStatus::Valid;
2307 operation.set_outcome(
2308 node,
2309 accepted,
2310 if accepted {
2311 OperationDecisionReason::ReferenceDigestVerified
2312 } else {
2313 OperationDecisionReason::ReferenceDigestRejected
2314 },
2315 );
2316 if result.status == DsigStatus::Valid
2317 && let Some(reference) = signed_info.references.get(result.reference_index)
2318 && reference
2319 .transforms
2320 .iter()
2321 .all(transform_preserves_manifest_structure)
2322 && let Some(uri) = reference.uri.as_deref()
2323 && let Ok(Some(target)) = resolver.node_id_for_same_document_reference(uri)
2324 {
2325 let identity = view.node_identity_by_id(target);
2326 operation.authenticate(identity);
2327 debug_assert!(operation.is_authenticated(identity));
2328 }
2329 reference_results.push(result);
2330 if !accepted {
2331 first_failure = Some(index);
2332 break;
2333 }
2334 }
2335 debug_assert!(
2336 plan_nodes.digests[reference_results.len()..]
2337 .iter()
2338 .all(|node| !operation.is_executed(*node))
2339 );
2340 let references = ReferencesResult {
2341 results: reference_results,
2342 first_failure,
2343 };
2344
2345 if let Some(first_failure) = references.first_failure {
2346 debug_assert!(operation.first_failure().is_some());
2347 let status = references.results[first_failure].status;
2348 return Ok(VerifyResult {
2349 key_trust: super::KeyTrustEvidence::NotEvaluated,
2350 status,
2351 signed_info_references: references.results,
2352 manifest_references: Vec::new(),
2353 canonicalized_signed_info: None,
2354 });
2355 }
2356
2357 let canonical_signed_info = operation.run(plan_nodes.canonicalization, || {
2358 let signed_info_subtree: HashSet<_> = signed_info_node
2359 .descendants()
2360 .map(|node: Node<'_, '_>| node.id())
2361 .collect();
2362 let mut canonical_signed_info = Vec::new();
2363 let signed_info_limit = operation
2364 .budgets()
2365 .canonicalized
2366 .remaining()
2367 .min(operation.budgets().transforms.remaining_c14n_output());
2368 canonicalize_bounded_with_xml_base_budget(
2369 doc,
2370 Some(&|node| signed_info_subtree.contains(&node.id())),
2371 &signed_info.c14n_method,
2372 signed_info_limit,
2373 operation.budgets().transforms.xml_base_resolution(),
2374 &mut canonical_signed_info,
2375 )
2376 .map_err(|error| {
2377 if let Some(violation) = map_c14n_resource_policy_violation(
2378 &error,
2379 crate::policy::resource_name::CANONICALIZED_BYTES,
2380 operation.budgets().canonicalized.max_bytes,
2381 ) {
2382 SignatureVerificationPipelineError::Policy(violation)
2383 } else {
2384 SignatureVerificationPipelineError::Canonicalization(error)
2385 }
2386 })?;
2387 operation
2388 .budgets()
2389 .transforms
2390 .charge_c14n_output(canonical_signed_info.len())
2391 .map_err(ReferenceProcessingError::Transform)?;
2392 operation
2393 .budgets()
2394 .canonicalized
2395 .charge(canonical_signed_info.len())?;
2396 Ok::<_, SignatureVerificationPipelineError>(canonical_signed_info)
2397 })?;
2398
2399 let signature_value = decode_signature_value(
2400 signature_children.signature_value_node,
2401 signed_info.signature_method,
2402 &ctx.policy.resources,
2403 )?;
2404 if let Some(full_output_bits) = signed_info.signature_method.hmac_output_bits() {
2405 let expected_bits = signed_info
2406 .hmac_output_length_bits
2407 .unwrap_or(full_output_bits);
2408 if signature_value.len() != expected_bits / 8 {
2409 return Err(SignatureVerificationPipelineError::InvalidStructure {
2410 reason: "SignatureValue length does not match HMACOutputLength",
2411 });
2412 }
2413 }
2414 let resolved_key = operation.run(plan_nodes.key, || {
2415 resolve_verifying_key(ctx, key, key_info.as_ref(), signed_info.signature_method)
2416 })?;
2417 let Some(resolved_key) = resolved_key else {
2418 operation.set_outcome(
2419 plan_nodes.key,
2420 false,
2421 OperationDecisionReason::KeyUnavailable,
2422 );
2423 if let Some(error) = retrieval_materialization.deferred_error {
2424 return Err(error);
2425 }
2426 return Ok(VerifyResult {
2427 key_trust: super::KeyTrustEvidence::NotEvaluated,
2428 status: DsigStatus::Invalid(FailureReason::KeyNotFound),
2429 signed_info_references: references.results,
2430 manifest_references: Vec::new(),
2431 canonicalized_signed_info: if ctx.store_pre_digest {
2432 Some(canonical_signed_info)
2433 } else {
2434 None
2435 },
2436 });
2437 };
2438 operation.set_outcome(plan_nodes.key, true, OperationDecisionReason::KeyResolved);
2439 let verifier = resolved_key.as_ref();
2440 let signature_valid = operation.run(plan_nodes.crypto, || {
2441 verifier.validate_policy(&ctx.policy)?;
2442 if !verifier.validate_signature_value_with_policy(
2443 &ctx.policy,
2444 signed_info.signature_method,
2445 &signature_value,
2446 )? {
2447 return Ok::<_, SignatureVerificationPipelineError>(false);
2448 }
2449 ctx.provider
2450 .require_capability(crate::provider::ProviderCapability::Verify(
2451 signed_info.signature_method,
2452 ))?;
2453 let policy_verifier = PolicyVerifyingKey {
2454 key: verifier,
2455 policy: &ctx.policy,
2456 };
2457 ctx.provider.verify_with_context(
2458 &policy_verifier,
2459 signed_info.signature_method,
2460 &signed_info.signature_context,
2461 &canonical_signed_info,
2462 &signature_value,
2463 )
2464 })?;
2465
2466 if !signature_valid {
2467 operation.set_outcome(
2468 plan_nodes.crypto,
2469 false,
2470 OperationDecisionReason::SignatureRejected,
2471 );
2472 return Ok(VerifyResult {
2473 key_trust: resolved_key.evidence(&ctx.policy)?,
2474 status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
2475 signed_info_references: references.results,
2476 manifest_references: Vec::new(),
2477 canonicalized_signed_info: if ctx.store_pre_digest {
2478 Some(canonical_signed_info)
2479 } else {
2480 None
2481 },
2482 });
2483 }
2484 operation.set_outcome(
2485 plan_nodes.crypto,
2486 true,
2487 OperationDecisionReason::SignatureVerified,
2488 );
2489 let (manifest_references, manifest_digests) =
2493 if ctx.policy.manifest_processing == crate::policy::ManifestProcessing::Process {
2494 process_authenticated_manifest_references(
2495 operation,
2496 view,
2497 signature_node,
2498 resolver,
2499 ctx,
2500 remaining_reference_capacity,
2501 plan_nodes.crypto,
2502 )?
2503 } else {
2504 (Vec::new(), Vec::new())
2505 };
2506 operation.extend();
2507 let evidence = operation.add_node(OperationNodeKind::Evidence, OperationStage::Evidence, None);
2508 operation
2509 .add_dependency(evidence, plan_nodes.crypto)
2510 .map_err(map_verification_plan_error)?;
2511 for digest in manifest_digests {
2512 operation
2513 .add_dependency(evidence, digest)
2514 .map_err(map_verification_plan_error)?;
2515 }
2516 operation.compile().map_err(map_verification_plan_error)?;
2517 operation.run(evidence, || {
2518 Ok::<_, SignatureVerificationPipelineError>(VerifyResult {
2519 key_trust: resolved_key.evidence(&ctx.policy)?,
2520 status: DsigStatus::Valid,
2521 signed_info_references: references.results,
2522 manifest_references,
2523 canonicalized_signed_info: if ctx.store_pre_digest {
2524 Some(canonical_signed_info)
2525 } else {
2526 None
2527 },
2528 })
2529 })
2530}
2531
2532struct PolicyVerifyingKey<'a> {
2533 key: &'a dyn VerifyingKey,
2534 policy: &'a crate::policy::VerificationPolicy,
2535}
2536
2537impl VerifyingKey for PolicyVerifyingKey<'_> {
2538 fn verify_candidate_keys(
2539 &self,
2540 verify: &mut dyn FnMut(&dyn VerifyingKey) -> Result<bool, DsigError>,
2541 ) -> Result<Option<bool>, DsigError> {
2542 self.key.verify_candidate_keys(&mut |key| {
2543 key.validate_policy(self.policy)?;
2544 verify(&PolicyVerifyingKey {
2545 key,
2546 policy: self.policy,
2547 })
2548 })
2549 }
2550 fn verification_spki(&self, algorithm: SignatureAlgorithm) -> Result<Option<&[u8]>, DsigError> {
2551 self.key.verification_spki(algorithm)
2552 }
2553
2554 fn ecdsa_encoding(&self) -> crate::policy::EcdsaSignatureValueEncoding {
2555 self.policy.ecdsa_signature_value_encoding
2556 }
2557 fn verify_with_context(
2558 &self,
2559 algorithm: SignatureAlgorithm,
2560 context: &super::SignatureContext,
2561 data: &[u8],
2562 signature: &[u8],
2563 ) -> Result<bool, DsigError> {
2564 if context.as_bytes().is_empty() {
2565 self.verify(algorithm, data, signature)
2566 } else {
2567 self.key
2568 .verify_with_context(algorithm, context, data, signature)
2569 }
2570 }
2571 fn validate_policy(&self, policy: &crate::policy::VerificationPolicy) -> Result<(), DsigError> {
2572 self.key.validate_policy(policy)
2573 }
2574
2575 fn validate_signature_value(
2576 &self,
2577 algorithm: SignatureAlgorithm,
2578 signature_value: &[u8],
2579 ) -> Result<bool, DsigError> {
2580 self.key
2581 .validate_signature_value_with_policy(self.policy, algorithm, signature_value)
2582 }
2583
2584 fn verify(
2585 &self,
2586 algorithm: SignatureAlgorithm,
2587 signed_data: &[u8],
2588 signature_value: &[u8],
2589 ) -> Result<bool, DsigError> {
2590 self.key
2591 .verify_with_policy(self.policy, algorithm, signed_data, signature_value)
2592 }
2593}
2594
2595#[derive(Debug, Default)]
2596struct RetrievalMaterialization {
2597 deferred_error: Option<SignatureVerificationPipelineError>,
2598}
2599
2600impl RetrievalMaterialization {
2601 fn merge(&mut self, other: Self) {
2602 if self.deferred_error.is_none() {
2603 self.deferred_error = other.deferred_error;
2604 }
2605 }
2606}
2607
2608struct RetrievalMaterializationBudgets<'a> {
2609 certificate_signature_algorithms: Option<&'a crate::policy::CertificateSignatureAlgorithms>,
2610 xpath_parse: &'a mut XPathSignatureParseBudget,
2611 execution: &'a TransformExecutionBudget,
2612 resources: &'a crate::policy::ResourcePolicy,
2613 xml_backend: crate::XmlBackend,
2614}
2615
2616#[derive(Default)]
2617struct KeyInfoMaterializationState {
2618 active: HashSet<(super::uri::TraversalDocumentIdentity, String)>,
2619 candidate_work: usize,
2620}
2621
2622trait KeyInfoReferencePolicy {
2623 fn check_digest_algorithm(
2624 &self,
2625 algorithm: DigestAlgorithm,
2626 ) -> Result<(), crate::policy::PolicyViolation>;
2627 fn certificate_signature_algorithms(
2628 &self,
2629 ) -> Option<&crate::policy::CertificateSignatureAlgorithms>;
2630 fn resources(&self) -> &crate::policy::ResourcePolicy;
2631 fn xml(&self) -> &crate::policy::XmlInputPolicy;
2632 fn key_info_reference_uris(&self) -> UriTypeSet;
2633 fn retrieval_method_uris(&self) -> UriTypeSet;
2634 fn key_info_reference_source_enabled(&self) -> bool;
2635 fn allowed_transforms(&self) -> Option<&HashSet<String>>;
2636}
2637
2638impl KeyInfoReferencePolicy for crate::policy::SigningPolicy {
2639 fn check_digest_algorithm(
2640 &self,
2641 algorithm: DigestAlgorithm,
2642 ) -> Result<(), crate::policy::PolicyViolation> {
2643 self.check_digest_algorithm(algorithm)
2644 }
2645 fn certificate_signature_algorithms(
2646 &self,
2647 ) -> Option<&crate::policy::CertificateSignatureAlgorithms> {
2648 None
2649 }
2650 fn resources(&self) -> &crate::policy::ResourcePolicy {
2651 &self.resources
2652 }
2653
2654 fn xml(&self) -> &crate::policy::XmlInputPolicy {
2655 &self.xml
2656 }
2657
2658 fn key_info_reference_uris(&self) -> UriTypeSet {
2659 self.uris.key_info_references
2660 }
2661
2662 fn retrieval_method_uris(&self) -> UriTypeSet {
2663 self.uris.retrieval_methods
2664 }
2665
2666 fn key_info_reference_source_enabled(&self) -> bool {
2667 true
2668 }
2669
2670 fn allowed_transforms(&self) -> Option<&HashSet<String>> {
2671 self.transforms.allowed_algorithms.as_ref()
2672 }
2673}
2674
2675impl KeyInfoReferencePolicy for crate::policy::VerificationPolicy {
2676 fn check_digest_algorithm(
2677 &self,
2678 algorithm: DigestAlgorithm,
2679 ) -> Result<(), crate::policy::PolicyViolation> {
2680 self.check_digest_algorithm(algorithm)
2681 }
2682 fn certificate_signature_algorithms(
2683 &self,
2684 ) -> Option<&crate::policy::CertificateSignatureAlgorithms> {
2685 self.key_trust.certificate_signature_algorithms.as_ref()
2686 }
2687 fn resources(&self) -> &crate::policy::ResourcePolicy {
2688 &self.resources
2689 }
2690
2691 fn xml(&self) -> &crate::policy::XmlInputPolicy {
2692 &self.xml
2693 }
2694
2695 fn key_info_reference_uris(&self) -> UriTypeSet {
2696 self.uris.key_info_references
2697 }
2698
2699 fn retrieval_method_uris(&self) -> UriTypeSet {
2700 self.uris.retrieval_methods
2701 }
2702
2703 fn key_info_reference_source_enabled(&self) -> bool {
2704 self.key_sources.key_info_reference
2705 }
2706
2707 fn allowed_transforms(&self) -> Option<&HashSet<String>> {
2708 self.transforms.allowed_algorithms.as_ref()
2709 }
2710}
2711
2712struct KeyInfoReferenceMaterializationContext<'a, 'budget, P> {
2713 policy: &'a P,
2714 provider: &'a dyn crate::provider::CryptoProvider,
2715 budgets: &'a mut RetrievalMaterializationBudgets<'budget>,
2716}
2717
2718fn materialize_key_info_references_with_budgets<P: KeyInfoReferencePolicy>(
2719 key_info: &mut KeyInfo,
2720 resolver: &UriReferenceResolver<'_>,
2721 policy: &P,
2722 provider: &dyn crate::provider::CryptoProvider,
2723 budgets: &mut RetrievalMaterializationBudgets<'_>,
2724 materialization: &mut KeyInfoMaterializationState,
2725) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
2726 fn visit<P: KeyInfoReferencePolicy>(
2727 key_info: &mut KeyInfo,
2728 resolver: &UriReferenceResolver<'_>,
2729 context: &mut KeyInfoReferenceMaterializationContext<'_, '_, P>,
2730 materialization: &mut KeyInfoMaterializationState,
2731 depth: usize,
2732 ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
2733 let mut materialized = Vec::new();
2734 let mut outcome = RetrievalMaterialization::default();
2735 for source in std::mem::take(&mut key_info.sources) {
2736 let source_work = match &source {
2737 super::parse::KeyInfoSource::X509Data(info) => info.certificates.len().max(1),
2738 _ => 1,
2739 };
2740 materialization.candidate_work =
2741 materialization.candidate_work.saturating_add(source_work);
2742 context
2743 .policy
2744 .resources()
2745 .validate_key_candidates(materialization.candidate_work)?;
2746
2747 let super::parse::KeyInfoSource::KeyInfoReference { uri } = source else {
2748 materialized.push(source);
2749 continue;
2750 };
2751 if !context.policy.key_info_reference_source_enabled() {
2752 return Err(crate::policy::PolicyViolation::KeyTrust {
2753 reason: "KeyInfoReference key sources are disabled",
2754 }
2755 .into());
2756 }
2757 if !context.policy.key_info_reference_uris().allows(&uri) {
2758 return Err(crate::policy::PolicyViolation::Uri {
2759 operation: "KeyInfoReference",
2760 reason: "URI class is disabled",
2761 }
2762 .into());
2763 }
2764 let next_depth = depth.saturating_add(1);
2765 context
2766 .policy
2767 .resources()
2768 .validate_key_info_reference_depth(next_depth)?;
2769 let cycle_key = (resolver.traversal_document_identity(), uri.clone());
2770 if !materialization.active.insert(cycle_key.clone()) {
2771 return Err(SignatureVerificationPipelineError::InvalidStructure {
2772 reason: "KeyInfoReference cycle detected",
2773 });
2774 }
2775
2776 let is_same_document = uri.is_empty() || uri.starts_with('#');
2777 let (mut referenced, mut nested_outcome) = if is_same_document {
2778 let node = resolver
2779 .node_for_same_document_reference(&uri)
2780 .map_err(|error| {
2781 SignatureVerificationPipelineError::from(
2782 ReferenceProcessingError::UriDereference(error),
2783 )
2784 })?
2785 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2786 reason: "KeyInfoReference target is missing or ambiguous",
2787 })?;
2788 if !node.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
2789 return Err(SignatureVerificationPipelineError::InvalidStructure {
2790 reason: "KeyInfoReference target must be KeyInfo",
2791 });
2792 }
2793 super::parse::validate_x509_digest_policy(node, &|algorithm| {
2794 context.policy.check_digest_algorithm(algorithm)
2795 })
2796 .map_err(map_key_info_parse_error)?;
2797 (
2798 parse_key_info_with_policy_budgets(
2799 node,
2800 context.provider,
2801 context.budgets.execution.xml_base_resolution(),
2802 context.policy.resources(),
2803 context.policy.certificate_signature_algorithms(),
2804 )
2805 .map_err(map_key_info_parse_error)?,
2806 RetrievalMaterialization::default(),
2807 )
2808 } else {
2809 let (resource_uri, fragment) = uri
2813 .split_once('#')
2814 .map_or((uri.as_str(), None), |(resource, fragment)| {
2815 (resource, (!fragment.is_empty()).then_some(fragment))
2816 });
2817 let bytes = resolver
2818 .external_resource(resource_uri)
2819 .map_err(|error| {
2820 SignatureVerificationPipelineError::from(
2821 ReferenceProcessingError::UriDereference(error),
2822 )
2823 })?
2824 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2825 reason: "KeyInfoReference external resource is unavailable",
2826 })?;
2827 context
2830 .budgets
2831 .execution
2832 .xml_parse_work()
2833 .charge_policy(bytes.len())?;
2834 let xml = crate::encoding::decode_xml_octets(
2835 bytes,
2836 context.policy.resources().max_xml_document_bytes,
2837 )
2838 .map_err(map_key_info_xml_decode_error)?;
2839 let settings = DocumentParseSettings::from_policy(
2840 context.policy.xml(),
2841 context.policy.resources(),
2842 )
2843 .with_backend(context.budgets.xml_backend);
2844 let document = XmlDocument::parse_with_settings_and_budget(
2845 xml.into_owned(),
2846 settings,
2847 context.budgets.execution.xml_parse_work(),
2848 )
2849 .map_err(|error| map_document_parse_error(error, settings))?;
2850 document.with_view(|view| {
2851 let external_resolver = resolver.for_external_document_view(view, resource_uri);
2852 let target = match fragment {
2853 Some(fragment) => external_resolver
2854 .node_for_same_document_reference(&format!("#{fragment}"))
2855 .map_err(|error| {
2856 SignatureVerificationPipelineError::from(
2857 ReferenceProcessingError::UriDereference(error),
2858 )
2859 })?
2860 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2861 reason: "KeyInfoReference external target is missing or ambiguous",
2862 })?,
2863 _ => view.document().root_element(),
2864 };
2865 if !target.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
2866 return Err(SignatureVerificationPipelineError::InvalidStructure {
2867 reason: "KeyInfoReference external target must be KeyInfo",
2868 });
2869 }
2870 super::parse::validate_x509_digest_policy(target, &|algorithm| {
2871 context.policy.check_digest_algorithm(algorithm)
2872 })
2873 .map_err(map_key_info_parse_error)?;
2874 let mut referenced = parse_key_info_with_policy_budgets_and_document_base(
2875 target,
2876 context.provider,
2877 context.budgets.execution.xml_base_resolution(),
2878 context.policy.resources(),
2879 Some(resource_uri),
2880 context.policy.certificate_signature_algorithms(),
2881 )
2882 .map_err(map_key_info_parse_error)?;
2883 let mut nested_outcome = visit(
2884 &mut referenced,
2885 &external_resolver,
2886 context,
2887 materialization,
2888 next_depth,
2889 )?;
2890 nested_outcome.merge(materialize_retrieval_methods_with_budgets(
2891 &mut referenced,
2892 &external_resolver,
2893 context.policy,
2894 context.provider,
2895 context.budgets,
2896 &mut materialization.candidate_work,
2897 )?);
2898 referenced.sources.retain(|source| {
2901 !matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. })
2902 });
2903 Ok((referenced, nested_outcome))
2904 })?
2905 };
2906 if is_same_document {
2907 nested_outcome.merge(visit(
2908 &mut referenced,
2909 resolver,
2910 context,
2911 materialization,
2912 next_depth,
2913 )?);
2914 }
2915 outcome.merge(nested_outcome);
2916 materialization.active.remove(&cycle_key);
2917 materialized.extend(referenced.sources);
2918 }
2919 key_info.sources = materialized;
2920 context
2921 .policy
2922 .resources()
2923 .validate_key_candidates(key_info.embedded_candidate_count())?;
2924 Ok(outcome)
2925 }
2926
2927 let mut context = KeyInfoReferenceMaterializationContext {
2928 policy,
2929 provider,
2930 budgets,
2931 };
2932 visit(key_info, resolver, &mut context, materialization, 0)
2933}
2934
2935fn map_key_info_xml_decode_error(
2936 error: crate::encoding::XmlEncodingError,
2937) -> SignatureVerificationPipelineError {
2938 match error {
2939 crate::encoding::XmlEncodingError::DecodedLimit { maximum, actual } => {
2940 SignatureVerificationPipelineError::Policy(
2941 crate::policy::PolicyViolation::ResourceLimit {
2942 resource: crate::policy::resource_name::XML_DOCUMENT,
2943 maximum,
2944 actual,
2945 },
2946 )
2947 }
2948 _ => SignatureVerificationPipelineError::InvalidStructure {
2949 reason: "KeyInfoReference external resource has an invalid XML encoding",
2950 },
2951 }
2952}
2953
2954fn materialize_key_info_references_for_policy<P: KeyInfoReferencePolicy>(
2955 key_info: &mut KeyInfo,
2956 resolver: UriReferenceResolver<'_>,
2957 policy: &P,
2958 provider: &dyn crate::provider::CryptoProvider,
2959 xml_backend: crate::XmlBackend,
2960) -> Result<(), DsigError> {
2961 let resolver = resolver.with_external_resource_limits(
2962 policy.resources().max_external_resource_bytes,
2963 policy.resources().max_external_resource_total_bytes,
2964 );
2965 let mut xpath_parse_budget = XPathSignatureParseBudget::from_resources(policy.resources());
2966 let execution_budget = TransformExecutionBudget::from_resources(policy.resources());
2967 let mut budgets = RetrievalMaterializationBudgets {
2968 xpath_parse: &mut xpath_parse_budget,
2969 execution: &execution_budget,
2970 resources: policy.resources(),
2971 certificate_signature_algorithms: policy.certificate_signature_algorithms(),
2972 xml_backend,
2973 };
2974 let mut materialization = KeyInfoMaterializationState::default();
2975 materialize_key_info_references_with_budgets(
2978 key_info,
2979 &resolver,
2980 policy,
2981 provider,
2982 &mut budgets,
2983 &mut materialization,
2984 )?;
2985 Ok(())
2986}
2987
2988pub fn materialize_signing_key_info_references(
3005 key_info: &mut KeyInfo,
3006 resolver: UriReferenceResolver<'_>,
3007 policy: &crate::policy::SigningPolicy,
3008 provider: &dyn crate::provider::CryptoProvider,
3009 xml_backend: crate::XmlBackend,
3010) -> Result<(), DsigError> {
3011 policy.validate()?;
3012 materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
3013}
3014
3015pub fn materialize_verification_key_info_references(
3030 key_info: &mut KeyInfo,
3031 resolver: UriReferenceResolver<'_>,
3032 policy: &crate::policy::VerificationPolicy,
3033 provider: &dyn crate::provider::CryptoProvider,
3034 xml_backend: crate::XmlBackend,
3035) -> Result<(), DsigError> {
3036 policy.validate()?;
3037 materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
3038}
3039
3040fn materialize_retrieval_methods_with_budgets<P: KeyInfoReferencePolicy>(
3041 key_info: &mut KeyInfo,
3042 resolver: &UriReferenceResolver<'_>,
3043 policy: &P,
3044 provider: &dyn crate::provider::CryptoProvider,
3045 budgets: &mut RetrievalMaterializationBudgets<'_>,
3046 candidate_work: &mut usize,
3047) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
3048 let allowed_uri_types = policy.retrieval_method_uris();
3049 let allowed_transforms = policy.allowed_transforms();
3050 let retrieval_count = key_info
3051 .sources
3052 .iter()
3053 .filter(|source| matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. }))
3054 .count();
3055 if retrieval_count > MAX_RETRIEVAL_METHOD_COUNT {
3056 return Err(SignatureVerificationPipelineError::InvalidStructure {
3057 reason: "KeyInfo contains too many RetrievalMethod elements",
3058 });
3059 }
3060
3061 let mut total_binary_len = existing_x509_binary_len(key_info)?;
3062 let mut seen = HashSet::new();
3063 let mut materialized = Vec::with_capacity(key_info.sources.len());
3064 let mut outcome = RetrievalMaterialization::default();
3065 for source in std::mem::take(&mut key_info.sources) {
3066 let super::parse::KeyInfoSource::RetrievalMethod {
3067 uri: resolved_uri,
3068 resource_type,
3069 transforms,
3070 } = source
3071 else {
3072 materialized.push(source);
3073 continue;
3074 };
3075
3076 let identity = (
3077 resolved_uri.clone(),
3078 resource_type.clone(),
3079 transforms.clone(),
3080 );
3081 if !seen.insert(identity) {
3082 continue;
3083 }
3084
3085 if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#rawX509Certificate")
3086 {
3087 if transforms != RetrievalMethodTransforms::None
3088 || classify_uri(&resolved_uri) != UriClass::External
3089 {
3090 return Err(SignatureVerificationPipelineError::InvalidStructure {
3091 reason: "raw X509 RetrievalMethod requires an untransformed external URI",
3092 });
3093 }
3094 if !allowed_uri_types.allows(&resolved_uri) {
3095 return Err(crate::policy::PolicyViolation::Uri {
3096 operation: "verification",
3097 reason: "retrieval method URI class is not permitted",
3098 }
3099 .into());
3100 }
3101 let certificate = resolver.external_resource(&resolved_uri).map_err(|error| {
3102 SignatureVerificationPipelineError::from(ReferenceProcessingError::Transform(error))
3103 })?;
3104 let Some(certificate) = certificate else {
3105 outcome.deferred_error.get_or_insert_with(|| {
3106 SignatureVerificationPipelineError::Reference(
3107 ReferenceProcessingError::Transform(super::TransformError::UnsupportedUri(
3108 resolved_uri.clone(),
3109 )),
3110 )
3111 });
3112 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
3113 uri: resolved_uri,
3114 resource_type,
3115 transforms,
3116 });
3117 continue;
3118 };
3119 *candidate_work = candidate_work.saturating_add(1);
3120 budgets.resources.validate_key_candidates(*candidate_work)?;
3121 if certificate.len() > MAX_X509_DECODED_BINARY_LEN {
3122 return Err(SignatureVerificationPipelineError::InvalidStructure {
3123 reason: "raw X509 RetrievalMethod certificate exceeds maximum allowed length",
3124 });
3125 }
3126 add_retrieval_binary_usage(&mut total_binary_len, certificate.len())?;
3127 let parsed = match parse_x509_certificate(certificate) {
3128 Ok(parsed) => parsed,
3129 Err(error) => {
3130 let error = map_key_info_parse_error(error);
3131 if matches!(error, SignatureVerificationPipelineError::Policy(_)) {
3132 return Err(error);
3133 }
3134 outcome.deferred_error.get_or_insert(error);
3135 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
3136 uri: resolved_uri,
3137 resource_type,
3138 transforms,
3139 });
3140 continue;
3141 }
3142 };
3143 materialized.push(super::parse::KeyInfoSource::X509Data(
3144 super::parse::X509DataInfo {
3145 certificates: vec![certificate.to_vec()],
3146 parsed_certificates: vec![parsed],
3147 certificate_chain: vec![0],
3148 ..super::parse::X509DataInfo::default()
3149 },
3150 ));
3151 } else if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#X509Data") {
3152 if !allowed_uri_types.allows(&resolved_uri) {
3153 return Err(crate::policy::PolicyViolation::Uri {
3154 operation: "verification",
3155 reason: "retrieval method URI class is not permitted",
3156 }
3157 .into());
3158 }
3159 let target = resolver
3160 .node_for_same_document_reference(&resolved_uri)
3161 .map_err(ReferenceProcessingError::Transform)?;
3162 let Some(target) = target else {
3163 outcome.deferred_error.get_or_insert(
3164 SignatureVerificationPipelineError::InvalidStructure {
3165 reason: "X509Data RetrievalMethod target is missing or ambiguous",
3166 },
3167 );
3168 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
3169 uri: resolved_uri,
3170 resource_type,
3171 transforms,
3172 });
3173 continue;
3174 };
3175 let node = match transforms {
3176 RetrievalMethodTransforms::None
3177 if target.has_tag_name((XMLDSIG_NS, "X509Data")) =>
3178 {
3179 target
3180 }
3181 RetrievalMethodTransforms::None => {
3182 return Err(SignatureVerificationPipelineError::InvalidStructure {
3183 reason: "untransformed X509Data RetrievalMethod must target X509Data directly",
3184 });
3185 }
3186 RetrievalMethodTransforms::X509DataNodeSetFilter {
3187 expression,
3188 namespaces,
3189 } => {
3190 enforce_transform_allowed(allowed_transforms, XPATH_TRANSFORM_URI)?;
3191 budgets
3192 .xpath_parse
3193 .validate_expression(&expression)
3194 .map_err(ReferenceProcessingError::Transform)?;
3195 budgets
3196 .xpath_parse
3197 .validate_namespaces(&namespaces)
3198 .map_err(ReferenceProcessingError::Transform)?;
3199 select_retrieved_x509_data_root(target, budgets.execution)?
3200 }
3201 RetrievalMethodTransforms::Unsupported => {
3202 return Err(SignatureVerificationPipelineError::InvalidStructure {
3203 reason: "X509Data RetrievalMethod contains unsupported transforms",
3204 });
3205 }
3206 };
3207 super::parse::validate_x509_digest_policy(node, &|algorithm| {
3208 policy.check_digest_algorithm(algorithm)
3209 })
3210 .map_err(map_key_info_parse_error)?;
3211 let data = parse_x509_data_dispatch_with_budget_and_provider(
3212 node,
3213 &mut total_binary_len,
3214 candidate_work,
3215 provider,
3216 budgets.resources,
3217 budgets.certificate_signature_algorithms,
3218 )
3219 .map_err(map_key_info_parse_error)?;
3220 materialized.push(super::parse::KeyInfoSource::X509Data(data));
3221 } else {
3222 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
3223 uri: resolved_uri,
3224 resource_type,
3225 transforms,
3226 });
3227 }
3228 }
3229 key_info.sources = materialized;
3230 Ok(outcome)
3231}
3232
3233fn select_retrieved_x509_data_root<'a, 'input>(
3234 target: Node<'a, 'input>,
3235 execution_budget: &TransformExecutionBudget,
3236) -> Result<Node<'a, 'input>, SignatureVerificationPipelineError> {
3237 let context_nodes = NodeSet::ensure_subtree_materialization_fits_with_budget(
3242 target,
3243 false,
3244 execution_budget.node_set_materialization(),
3245 )
3246 .map_err(ReferenceProcessingError::Transform)?;
3247 execution_budget
3248 .validate_xpath_context_evaluations(context_nodes)
3249 .map_err(ReferenceProcessingError::Transform)?;
3250 execution_budget
3251 .charge_xpath_work(context_nodes)
3252 .map_err(ReferenceProcessingError::Transform)?;
3253 execution_budget
3254 .charge_node_filter_work(context_nodes)
3255 .map_err(ReferenceProcessingError::Transform)?;
3256 let mut root = None;
3257 for candidate in target.descendants() {
3258 if !candidate.is_element()
3259 || candidate.tag_name().namespace() != Some(XMLDSIG_NS)
3260 || candidate.tag_name().name() != "X509Data"
3261 {
3262 continue;
3263 }
3264 if root.replace(candidate).is_some() {
3265 return Err(SignatureVerificationPipelineError::InvalidStructure {
3266 reason: "X509Data RetrievalMethod selected multiple X509Data elements",
3267 });
3268 }
3269 }
3270 root.ok_or(SignatureVerificationPipelineError::InvalidStructure {
3271 reason: "X509Data RetrievalMethod selected no X509Data element",
3272 })
3273}
3274
3275#[cfg(test)]
3276fn materialize_retrieval_methods(
3277 key_info: &mut KeyInfo,
3278 resolver: &UriReferenceResolver<'_>,
3279 allowed_uri_types: UriTypeSet,
3280 allowed_transforms: Option<&HashSet<String>>,
3281 provider: &dyn crate::provider::CryptoProvider,
3282) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
3283 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
3284 let execution_budget = TransformExecutionBudget::default();
3285 let resources = crate::policy::ResourcePolicy::default();
3286 let mut budgets = RetrievalMaterializationBudgets {
3287 xpath_parse: &mut xpath_parse_budget,
3288 execution: &execution_budget,
3289 resources: &resources,
3290 certificate_signature_algorithms: None,
3291 xml_backend: crate::XmlBackend::default(),
3292 };
3293 let mut candidate_work = key_info.embedded_candidate_count();
3294 let mut policy = crate::policy::VerificationPolicy::default();
3295 policy.uris.retrieval_methods = allowed_uri_types;
3296 policy.transforms.allowed_algorithms = allowed_transforms.cloned();
3297 materialize_retrieval_methods_with_budgets(
3298 key_info,
3299 resolver,
3300 &policy,
3301 provider,
3302 &mut budgets,
3303 &mut candidate_work,
3304 )
3305}
3306
3307fn existing_x509_binary_len(
3308 key_info: &KeyInfo,
3309) -> Result<usize, SignatureVerificationPipelineError> {
3310 let mut total = 0usize;
3311 for source in &key_info.sources {
3312 if let super::parse::KeyInfoSource::X509Data(info) = source {
3313 for len in info
3314 .certificates
3315 .iter()
3316 .chain(&info.skis)
3317 .chain(&info.crls)
3318 .map(Vec::len)
3319 .chain(info.digests.iter().map(|(_, digest)| digest.len()))
3320 {
3321 add_retrieval_binary_usage(&mut total, len)?;
3322 }
3323 }
3324 }
3325 Ok(total)
3326}
3327
3328fn add_retrieval_binary_usage(
3329 total: &mut usize,
3330 delta: usize,
3331) -> Result<(), SignatureVerificationPipelineError> {
3332 *total =
3333 total
3334 .checked_add(delta)
3335 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
3336 reason: "RetrievalMethod X509Data binary length overflow",
3337 })?;
3338 if *total > MAX_X509_DATA_TOTAL_BINARY_LEN {
3339 return Err(SignatureVerificationPipelineError::InvalidStructure {
3340 reason: "RetrievalMethod X509Data exceeds maximum aggregate binary length",
3341 });
3342 }
3343 Ok(())
3344}
3345
3346fn manifest_reference_failure_reason(
3347 error: ReferenceProcessingError,
3348 ref_index: usize,
3349) -> FailureReason {
3350 match error.into_policy_violation() {
3351 Ok(_) => FailureReason::ReferencePolicyViolation { ref_index },
3352 Err(_) => FailureReason::ReferenceProcessingFailure { ref_index },
3353 }
3354}
3355
3356struct CompiledManifestReference {
3357 index: usize,
3358 reference: Reference,
3359 reference_node_id: NodeId,
3360}
3361
3362struct CompiledManifestInvalid {
3363 result: ReferenceResult,
3364}
3365
3366enum ScheduledManifestReference {
3367 Parsed {
3368 node: OperationNodeId,
3369 reference: CompiledManifestReference,
3370 },
3371 Invalid {
3372 node: OperationNodeId,
3373 invalid: CompiledManifestInvalid,
3374 },
3375}
3376
3377impl ScheduledManifestReference {
3378 fn index(&self) -> usize {
3379 match self {
3380 Self::Parsed { reference, .. } => reference.index,
3381 Self::Invalid { invalid, .. } => invalid.result.reference_index,
3382 }
3383 }
3384}
3385
3386fn process_authenticated_manifest_references(
3387 operation: &mut OperationExecutionContext<
3388 &crate::policy::VerificationPolicy,
3389 &VerificationOperationBudgets,
3390 >,
3391 view: DocumentView<'_>,
3392 signature_node: Node<'_, '_>,
3393 resolver: &UriReferenceResolver<'_>,
3394 ctx: &VerifyContext<'_>,
3395 mut remaining_reference_capacity: usize,
3396 crypto: OperationNodeId,
3397) -> Result<(Vec<ReferenceResult>, Vec<OperationNodeId>), SignatureVerificationPipelineError> {
3398 let mut processed_manifests = HashSet::new();
3399 let mut next_reference_index = 0usize;
3400 let mut frontier_index = 0usize;
3401 let mut completed_digests = Vec::new();
3402 let mut results = Vec::new();
3403 loop {
3404 operation.extend();
3405 let discovery = operation.add_node(
3406 OperationNodeKind::Manifest {
3407 index: frontier_index,
3408 },
3409 OperationStage::AuthenticatedDependency,
3410 None,
3411 );
3412 operation
3413 .add_dependency(discovery, crypto)
3414 .map_err(map_verification_plan_error)?;
3415 for digest in &completed_digests {
3416 operation
3417 .add_dependency(discovery, *digest)
3418 .map_err(map_verification_plan_error)?;
3419 }
3420 operation.compile().map_err(map_verification_plan_error)?;
3421 let parsed = operation.run(discovery, || {
3422 let mut xpath_parse = operation.budgets().xpath_parse.borrow_mut();
3423 let mut state = ManifestDiscoveryState {
3424 processed: &mut processed_manifests,
3425 remaining_capacity: &mut remaining_reference_capacity,
3426 next_reference_index: &mut next_reference_index,
3427 xpath_parse: &mut xpath_parse,
3428 };
3429 parse_manifest_references(
3430 signature_node,
3431 operation,
3432 view,
3433 &mut state,
3434 ctx.allowed_transform_uris(),
3435 )
3436 })?;
3437 let discovered = parsed.references.len() + parsed.invalid_results.len();
3438 if discovered == 0 {
3439 break;
3440 }
3441
3442 operation.extend();
3443 let mut scheduled = Vec::with_capacity(discovered);
3444 for item in parsed.references {
3445 let resource = operation.budgets().resource_identity_for_reference(
3446 &item.reference,
3447 item.index,
3448 resolver,
3449 view,
3450 resolver.node_for_node_id(item.reference_node_id),
3451 );
3452 let node = operation.add_node(
3453 OperationNodeKind::Digest { index: item.index },
3454 OperationStage::AuthenticatedDependency,
3455 Some(resource.clone()),
3456 );
3457 operation
3458 .add_dependency(node, discovery)
3459 .map_err(map_verification_plan_error)?;
3460 scheduled.push(ScheduledManifestReference::Parsed {
3461 node,
3462 reference: item,
3463 });
3464 }
3465 for item in parsed.invalid_results {
3466 let node = operation.add_node(
3467 OperationNodeKind::Digest {
3468 index: item.result.reference_index,
3469 },
3470 OperationStage::AuthenticatedDependency,
3471 None,
3472 );
3473 operation
3474 .add_dependency(node, discovery)
3475 .map_err(map_verification_plan_error)?;
3476 scheduled.push(ScheduledManifestReference::Invalid {
3477 node,
3478 invalid: item,
3479 });
3480 }
3481 scheduled.sort_by_key(ScheduledManifestReference::index);
3482 operation.compile().map_err(map_verification_plan_error)?;
3483
3484 for scheduled_reference in scheduled {
3485 let (node, result) = match scheduled_reference {
3486 ScheduledManifestReference::Invalid { node, invalid } => {
3487 let result = operation.run(node, || {
3488 Ok::<_, SignatureVerificationPipelineError>(invalid.result)
3489 })?;
3490 (node, result)
3491 }
3492 ScheduledManifestReference::Parsed {
3493 node,
3494 reference: compiled,
3495 } => {
3496 let observed = operation.budgets().resource_identity_for_reference(
3497 &compiled.reference,
3498 compiled.index,
3499 resolver,
3500 view,
3501 resolver.node_for_node_id(compiled.reference_node_id),
3502 );
3503 let result = operation.run_with_resource(node, &observed, || {
3504 let reference = &compiled.reference;
3505 let budgets = operation.budgets();
3506 let execution = ReferenceExecutionContext {
3507 store_pre_digest: ctx.store_pre_digest,
3508 transform_options: ctx.transform_options(),
3509 transform_budget: &budgets.transforms,
3510 canonicalized_data_budget: &budgets.canonicalized,
3511 provider: ctx.provider,
3512 };
3513 let mut result = if execution.transform_budget.remaining_c14n_output() == 0
3514 || reference.transforms.len()
3515 > ctx.policy.resources.max_transforms_per_reference
3516 || ctx
3517 .policy
3518 .check_digest_algorithm(reference.digest_method)
3519 .is_err()
3520 {
3521 manifest_reference_invalid_result(
3522 reference,
3523 compiled.index,
3524 FailureReason::ReferencePolicyViolation {
3525 ref_index: compiled.index,
3526 },
3527 )
3528 } else {
3529 match enforce_reference_policies(
3530 std::slice::from_ref(reference),
3531 ctx.policy.uris.references,
3532 ctx.allowed_transform_uris(),
3533 ) {
3534 Ok(()) => process_reference_with_options(
3535 reference,
3536 resolver,
3537 signature_node,
3538 ReferenceSet::Manifest,
3539 compiled.index,
3540 resolver.node_for_node_id(compiled.reference_node_id),
3541 &execution,
3542 )
3543 .unwrap_or_else(|error| {
3544 manifest_reference_invalid_result(
3545 reference,
3546 compiled.index,
3547 manifest_reference_failure_reason(error, compiled.index),
3548 )
3549 }),
3550 Err(SignatureVerificationPipelineError::Policy(_)) => {
3551 manifest_reference_invalid_result(
3552 reference,
3553 compiled.index,
3554 FailureReason::ReferencePolicyViolation {
3555 ref_index: compiled.index,
3556 },
3557 )
3558 }
3559 Err(_) => manifest_reference_invalid_result(
3560 reference,
3561 compiled.index,
3562 FailureReason::ReferenceProcessingFailure {
3563 ref_index: compiled.index,
3564 },
3565 ),
3566 }
3567 };
3568 bind_reference_evidence(
3569 &mut result,
3570 reference,
3571 view,
3572 resolver.node_for_node_id(compiled.reference_node_id),
3573 &observed,
3574 );
3575 if result.status == DsigStatus::Valid
3576 && reference
3577 .transforms
3578 .iter()
3579 .all(transform_preserves_manifest_structure)
3580 && let Some(uri) = reference.uri.as_deref()
3581 && let Ok(Some(target)) =
3582 resolver.node_id_for_same_document_reference(uri)
3583 {
3584 operation.authenticate(view.node_identity_by_id(target));
3585 }
3586 Ok::<_, SignatureVerificationPipelineError>(result)
3587 })?;
3588 (node, result)
3589 }
3590 };
3591 let accepted = result.status == DsigStatus::Valid;
3592 operation.set_outcome(
3593 node,
3594 accepted,
3595 if accepted {
3596 OperationDecisionReason::ReferenceDigestVerified
3597 } else {
3598 OperationDecisionReason::ReferenceDigestRejected
3599 },
3600 );
3601 completed_digests.push(node);
3602 results.push(result);
3603 }
3604 frontier_index += 1;
3605 }
3606 results.sort_by_key(|result| result.reference_index);
3607 Ok((results, completed_digests))
3608}
3609
3610fn manifest_reference_invalid_result(
3611 reference: &Reference,
3612 index: usize,
3613 reason: FailureReason,
3614) -> ReferenceResult {
3615 ReferenceResult {
3616 reference_identity: None,
3617 target_identity: None,
3618 external_resource_fingerprint: None,
3619 coverage: ReferenceCoverage::TransformedData,
3620 reference_set: ReferenceSet::Manifest,
3621 reference_index: index,
3622 uri: reference
3623 .uri
3624 .clone()
3625 .unwrap_or_else(|| "<omitted>".to_owned()),
3626 digest_algorithm: reference.digest_method,
3627 status: DsigStatus::Invalid(reason),
3628 pre_digest_data: None,
3629 }
3630}
3631
3632fn parse_manifest_references(
3633 signature_node: Node<'_, '_>,
3634 operation: &OperationExecutionContext<
3635 &crate::policy::VerificationPolicy,
3636 &VerificationOperationBudgets,
3637 >,
3638 view: DocumentView<'_>,
3639 state: &mut ManifestDiscoveryState<'_>,
3640 allowed_transforms: Option<&HashSet<String>>,
3641) -> Result<ParsedManifestReferences, SignatureVerificationPipelineError> {
3642 let mut references = Vec::new();
3643 let mut invalid = Vec::new();
3644 for object_node in signature_node.children().filter(|node| {
3645 node.is_element()
3646 && node.tag_name().namespace() == Some(XMLDSIG_NS)
3647 && node.tag_name().name() == "Object"
3648 }) {
3649 let object_is_signed = operation.is_authenticated(view.node_identity(object_node));
3650 for manifest_node in object_node.children().filter(|node| {
3651 node.is_element()
3652 && node.tag_name().namespace() == Some(XMLDSIG_NS)
3653 && node.tag_name().name() == "Manifest"
3654 }) {
3655 let manifest_is_signed = operation.is_authenticated(view.node_identity(manifest_node));
3656 if !object_is_signed && !manifest_is_signed {
3659 continue;
3660 }
3661 if !state.processed.insert(manifest_node.id()) {
3662 continue;
3663 }
3664 let mut manifest_children = Vec::new();
3665 for child in manifest_node.children() {
3666 if child.is_text()
3667 && child.text().is_some_and(|text| {
3668 text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
3669 })
3670 {
3671 return Err(SignatureVerificationPipelineError::InvalidStructure {
3672 reason: "Manifest contains non-whitespace mixed content",
3673 });
3674 }
3675 if child.is_element() {
3676 manifest_children.push(child);
3677 }
3678 }
3679 if manifest_children.is_empty() {
3680 return Err(SignatureVerificationPipelineError::InvalidStructure {
3681 reason: "Manifest must contain at least one ds:Reference element child",
3682 });
3683 }
3684 for child in manifest_children {
3685 if child.tag_name().namespace() != Some(XMLDSIG_NS)
3686 || child.tag_name().name() != "Reference"
3687 {
3688 return Err(SignatureVerificationPipelineError::InvalidStructure {
3689 reason: "Manifest must contain only ds:Reference element children",
3690 });
3691 }
3692 if *state.remaining_capacity == 0 {
3693 return Err(SignatureVerificationPipelineError::InvalidStructure {
3694 reason: "signed Manifests exceed the per-signature Reference limit",
3695 });
3696 }
3697 *state.remaining_capacity -= 1;
3698 let reference_index = *state.next_reference_index;
3699 *state.next_reference_index += 1;
3700 match parse_reference_with_xpath_budget(child, state.xpath_parse) {
3701 Ok(reference) => references.push(CompiledManifestReference {
3702 index: reference_index,
3703 reference,
3704 reference_node_id: child.id(),
3705 }),
3706 Err(ParseError::Transform(super::TransformError::UnsupportedTransform(
3707 uri,
3708 ))) => {
3709 let digest_algorithm =
3710 reference_digest_method(child).map_err(map_manifest_parse_error)?;
3711 let reason =
3712 if allowed_transforms.is_some_and(|allowed| !allowed.contains(&uri)) {
3713 FailureReason::ReferencePolicyViolation {
3714 ref_index: reference_index,
3715 }
3716 } else {
3717 FailureReason::ReferenceProcessingFailure {
3718 ref_index: reference_index,
3719 }
3720 };
3721 invalid.push(CompiledManifestInvalid {
3722 result: ReferenceResult {
3723 reference_identity: Some(view.node_identity(child)),
3724 target_identity: None,
3725 external_resource_fingerprint: None,
3726 coverage: ReferenceCoverage::TransformedData,
3727 reference_set: ReferenceSet::Manifest,
3728 reference_index,
3729 uri: child.attribute("URI").unwrap_or("<omitted>").to_owned(),
3730 digest_algorithm,
3731 status: DsigStatus::Invalid(reason),
3732 pre_digest_data: None,
3733 },
3734 });
3735 }
3736 Err(error) => return Err(map_manifest_parse_error(error)),
3737 }
3738 }
3739 }
3740 }
3741 Ok(ParsedManifestReferences {
3742 references,
3743 invalid_results: invalid,
3744 })
3745}
3746
3747struct ParsedManifestReferences {
3748 references: Vec<CompiledManifestReference>,
3749 invalid_results: Vec<CompiledManifestInvalid>,
3750}
3751
3752struct ManifestDiscoveryState<'a> {
3753 processed: &'a mut HashSet<NodeId>,
3754 remaining_capacity: &'a mut usize,
3755 next_reference_index: &'a mut usize,
3756 xpath_parse: &'a mut XPathSignatureParseBudget,
3757}
3758
3759fn transform_preserves_manifest_structure(transform: &Transform) -> bool {
3760 match transform {
3761 Transform::C14n(_) => true,
3762 Transform::Enveloped
3767 | Transform::XpathExcludeAllSignatures
3768 | Transform::XPath(_)
3769 | Transform::XPathFilter2(_)
3770 | Transform::Base64Decode => false,
3771 }
3772}
3773
3774#[expect(
3775 clippy::large_enum_variant,
3776 reason = "single bounded authorization result avoids heap allocation"
3777)]
3778enum ResolvedVerifyingKey<'a> {
3779 Borrowed(&'a dyn VerifyingKey),
3780 Owned(super::ResolvedVerificationKey<'a>),
3781}
3782
3783impl ResolvedVerifyingKey<'_> {
3784 fn evidence(
3785 &self,
3786 policy: &crate::policy::VerificationPolicy,
3787 ) -> Result<super::KeyTrustEvidence, DsigError> {
3788 match self {
3789 Self::Borrowed(_)
3790 if policy.key_trust.mode
3791 == crate::policy::VerificationTrustMode::CryptographicOnly =>
3792 {
3793 Ok(super::KeyTrustEvidence::NotEstablished)
3794 }
3795 Self::Borrowed(_) => Ok(super::KeyTrustEvidence::CallerTrusted),
3796 Self::Owned(key) => key.authorize(policy),
3797 }
3798 }
3799 fn as_ref(&self) -> &dyn VerifyingKey {
3800 match self {
3801 Self::Borrowed(key) => *key,
3802 Self::Owned(key) => key.as_ref(),
3803 }
3804 }
3805}
3806
3807fn resolve_verifying_key<'k>(
3808 ctx: &VerifyContext<'k>,
3809 key: Option<&'k dyn VerifyingKey>,
3810 key_info: Option<&KeyInfo>,
3811 algorithm: SignatureAlgorithm,
3812) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
3813 if let Some(key) = key {
3814 if !ctx.policy.key_sources.preset_key {
3815 return Err(crate::policy::PolicyViolation::KeyTrust {
3816 reason: "pre-resolved verification keys are disabled",
3817 }
3818 .into());
3819 }
3820 require_verifying_key_candidate_capacity(&ctx.policy)?;
3821 return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
3822 }
3823 if let Some(resolver) = ctx.key_resolver {
3824 require_verifying_key_candidate_capacity(&ctx.policy)?;
3825 let resolved =
3826 resolver.resolve_for_verification(key_info, algorithm, &ctx.policy, ctx.provider)?;
3827 if let Some(key) = &resolved {
3828 key.authorize(&ctx.policy)?;
3829 }
3830 return Ok(resolved.map(ResolvedVerifyingKey::Owned));
3831 }
3832 Ok(None)
3833}
3834
3835fn require_verifying_key_candidate_capacity(
3836 policy: &crate::policy::VerificationPolicy,
3837) -> Result<(), SignatureVerificationPipelineError> {
3838 policy
3839 .resources
3840 .validate_key_candidates(1)
3841 .map_err(Into::into)
3842}
3843
3844fn enforce_reference_policies(
3845 references: &[Reference],
3846 allowed_uri_types: UriTypeSet,
3847 allowed_transforms: Option<&HashSet<String>>,
3848) -> Result<(), SignatureVerificationPipelineError> {
3849 for reference in references {
3850 let uri = reference
3851 .uri
3852 .as_deref()
3853 .ok_or(SignatureVerificationPipelineError::Reference(
3854 ReferenceProcessingError::MissingUri,
3855 ))?;
3856 if !allowed_uri_types.allows(uri) {
3857 return Err(crate::policy::PolicyViolation::Uri {
3858 operation: "verification",
3859 reason: "reference URI class is not permitted",
3860 }
3861 .into());
3862 }
3863
3864 if let Some(allowed) = allowed_transforms {
3865 for transform in &reference.transforms {
3866 let transform_uri = transform.algorithm_uri();
3867 enforce_transform_allowed(Some(allowed), transform_uri)?;
3868 }
3869
3870 let produces_binary = transform_chain_produces_binary(
3875 classify_uri(uri) == UriClass::External,
3876 &reference.transforms,
3877 );
3878 if !produces_binary {
3879 enforce_transform_allowed(Some(allowed), DEFAULT_IMPLICIT_C14N_URI)?;
3880 }
3881 }
3882 }
3883 Ok(())
3884}
3885
3886fn enforce_transform_allowed(
3887 allowed_transforms: Option<&HashSet<String>>,
3888 algorithm: &str,
3889) -> Result<(), SignatureVerificationPipelineError> {
3890 if allowed_transforms.is_some_and(|allowed| !allowed.contains(algorithm)) {
3891 return Err(crate::policy::PolicyViolation::Algorithm {
3892 operation: "verification transform",
3893 algorithm: algorithm.to_owned(),
3894 }
3895 .into());
3896 }
3897 Ok(())
3898}
3899
3900#[derive(Debug, Clone, Copy)]
3901pub(super) struct SignatureChildNodes<'a, 'input> {
3902 signed_info_node: Node<'a, 'input>,
3903 signature_value_node: Node<'a, 'input>,
3904 key_info_node: Option<Node<'a, 'input>>,
3905}
3906
3907pub(super) fn parse_signature_children<'a, 'input>(
3908 signature_node: Node<'a, 'input>,
3909) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
3910 let mut signed_info_node: Option<Node<'_, '_>> = None;
3911 let mut signature_value_node: Option<Node<'_, '_>> = None;
3912 let mut key_info_node: Option<Node<'_, '_>> = None;
3913 let mut signed_info_index: Option<usize> = None;
3914 let mut signature_value_index: Option<usize> = None;
3915 let mut key_info_index: Option<usize> = None;
3916 let mut first_unexpected_dsig_index: Option<usize> = None;
3917
3918 let mut element_index = 0usize;
3919 for child in signature_node.children() {
3920 if child.is_text() {
3921 if child
3922 .text()
3923 .is_some_and(|text| !is_xml_whitespace_only(text))
3924 {
3925 return Err(SignatureVerificationPipelineError::InvalidStructure {
3926 reason: "Signature must not contain non-whitespace mixed content",
3927 });
3928 }
3929 continue;
3930 }
3931 if !child.is_element() {
3932 continue;
3933 }
3934
3935 element_index += 1;
3936 if child.tag_name().namespace() != Some(XMLDSIG_NS) {
3937 return Err(SignatureVerificationPipelineError::InvalidStructure {
3938 reason: "Signature must contain only XMLDSIG element children",
3939 });
3940 }
3941 match child.tag_name().name() {
3942 "SignedInfo" => {
3943 if signed_info_node.is_some() {
3944 return Err(SignatureVerificationPipelineError::InvalidStructure {
3945 reason: "SignedInfo must appear exactly once under Signature",
3946 });
3947 }
3948 signed_info_node = Some(child);
3949 signed_info_index = Some(element_index);
3950 }
3951 "SignatureValue" => {
3952 if signature_value_node.is_some() {
3953 return Err(SignatureVerificationPipelineError::InvalidStructure {
3954 reason: "SignatureValue must appear exactly once under Signature",
3955 });
3956 }
3957 signature_value_node = Some(child);
3958 signature_value_index = Some(element_index);
3959 }
3960 "KeyInfo" => {
3961 if key_info_node.is_some() {
3962 return Err(SignatureVerificationPipelineError::InvalidStructure {
3963 reason: "KeyInfo must appear at most once under Signature",
3964 });
3965 }
3966 key_info_node = Some(child);
3967 key_info_index = Some(element_index);
3968 }
3969 "Object" => {
3970 }
3973 _ => {
3974 if first_unexpected_dsig_index.is_none() {
3975 first_unexpected_dsig_index = Some(element_index);
3976 }
3977 }
3978 }
3979 }
3980
3981 let signed_info_node =
3982 signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
3983 element: "SignedInfo",
3984 })?;
3985 let signature_value_node =
3986 signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
3987 element: "SignatureValue",
3988 })?;
3989 if signed_info_index != Some(1) {
3990 return Err(SignatureVerificationPipelineError::InvalidStructure {
3991 reason: "SignedInfo must be the first element child of Signature",
3992 });
3993 }
3994 if signature_value_index != Some(2) {
3995 return Err(SignatureVerificationPipelineError::InvalidStructure {
3996 reason: "SignatureValue must be the second element child of Signature",
3997 });
3998 }
3999 if let Some(index) = key_info_index
4000 && index != 3
4001 {
4002 return Err(SignatureVerificationPipelineError::InvalidStructure {
4003 reason: "KeyInfo must be the third element child of Signature when present",
4004 });
4005 }
4006
4007 let allowed_prefix_end = key_info_index.unwrap_or(2);
4008 if let Some(unexpected_index) = first_unexpected_dsig_index {
4009 return Err(SignatureVerificationPipelineError::InvalidStructure {
4010 reason: if unexpected_index > allowed_prefix_end {
4011 "After SignedInfo, SignatureValue, and optional KeyInfo, Signature may contain only Object elements"
4012 } else {
4013 "Signature may contain SignedInfo first, SignatureValue second, optional KeyInfo third, and Object elements thereafter"
4014 },
4015 });
4016 }
4017
4018 Ok(SignatureChildNodes {
4019 signed_info_node,
4020 signature_value_node,
4021 key_info_node,
4022 })
4023}
4024
4025fn decode_signature_value(
4026 signature_value_node: Node<'_, '_>,
4027 algorithm: SignatureAlgorithm,
4028 resources: &crate::policy::ResourcePolicy,
4029) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
4030 if matches!(algorithm, SignatureAlgorithm::PostQuantum(_)) {
4031 let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
4032 let payload = super::whitespace::XmlBase64Payload::bounded(
4036 signature_value_node,
4037 raw_maximum,
4038 maximum,
4039 )
4040 .map_err(
4041 |reason| SignatureVerificationPipelineError::InvalidStructure {
4042 reason: match reason {
4043 "unexpected nested element" => {
4044 "SignatureValue must not contain element children"
4045 }
4046 "maximum allowed text length" => {
4047 "SignatureValue exceeds maximum allowed text length"
4048 }
4049 "maximum allowed base64 length" => {
4050 "SignatureValue exceeds maximum allowed length"
4051 }
4052 _ => "SignatureValue contains malformed Base64",
4053 },
4054 },
4055 )?;
4056 return payload.decode().map_err(|_| {
4057 SignatureVerificationPipelineError::InvalidStructure {
4058 reason: "SignatureValue contains malformed Base64",
4059 }
4060 });
4061 }
4062 if signature_value_node
4063 .children()
4064 .any(|child| child.is_element())
4065 {
4066 return Err(SignatureVerificationPipelineError::InvalidStructure {
4067 reason: "SignatureValue must not contain element children",
4068 });
4069 }
4070
4071 let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
4075 let mut text_len = 0usize;
4076 let mut normalized_len = 0usize;
4077 for child in signature_value_node
4078 .children()
4079 .filter(|child| child.is_text())
4080 {
4081 if let Some(text) = child.text() {
4082 if text.len() > raw_maximum - text_len {
4083 return Err(SignatureVerificationPipelineError::InvalidStructure {
4084 reason: "SignatureValue exceeds maximum allowed text length",
4085 });
4086 }
4087 text_len += text.len();
4088 for byte in text.bytes() {
4089 if matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
4090 continue;
4091 }
4092 if normalized_len == maximum {
4093 return Err(SignatureVerificationPipelineError::InvalidStructure {
4094 reason: "SignatureValue exceeds maximum allowed length",
4095 });
4096 }
4097 normalized_len += 1;
4098 }
4099 }
4100 }
4101 let mut normalized = Vec::with_capacity(normalized_len);
4102 let mut raw_text_len = 0usize;
4103 for child in signature_value_node
4104 .children()
4105 .filter(|child| child.is_text())
4106 {
4107 if let Some(text) = child.text() {
4108 push_normalized_signature_text(
4109 text,
4110 &mut raw_text_len,
4111 &mut normalized,
4112 algorithm,
4113 resources,
4114 )?;
4115 }
4116 }
4117
4118 Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
4119}
4120
4121fn push_normalized_signature_text(
4122 text: &str,
4123 raw_text_len: &mut usize,
4124 normalized: &mut Vec<u8>,
4125 algorithm: SignatureAlgorithm,
4126 resources: &crate::policy::ResourcePolicy,
4127) -> Result<(), SignatureVerificationPipelineError> {
4128 let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
4129 if text.len() > raw_maximum - *raw_text_len {
4130 return Err(SignatureVerificationPipelineError::InvalidStructure {
4131 reason: "SignatureValue exceeds maximum allowed text length",
4132 });
4133 }
4134 *raw_text_len += text.len();
4135 let additional = text
4136 .bytes()
4137 .filter(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
4138 .count();
4139 if additional > maximum - normalized.len() {
4140 return Err(SignatureVerificationPipelineError::InvalidStructure {
4141 reason: "SignatureValue exceeds maximum allowed length",
4142 });
4143 }
4144
4145 normalize_xml_base64_bytes(text.as_bytes(), normalized, |_| true).map_err(|err| {
4146 SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(
4147 err.normalized_offset,
4148 err.invalid_byte,
4149 ))
4150 })?;
4151
4152 Ok(())
4153}
4154
4155fn signature_value_limits(
4156 algorithm: SignatureAlgorithm,
4157 resources: &crate::policy::ResourcePolicy,
4158) -> (usize, usize) {
4159 let encoded = match algorithm {
4166 SignatureAlgorithm::PostQuantum(parameter) => parameter.signature_len().div_ceil(3) * 4,
4167 _ => MAX_SIGNATURE_VALUE_LEN,
4168 };
4169 (encoded, resources.max_xml_document_bytes)
4170}
4171
4172fn verify_with_algorithm(
4173 algorithm: SignatureAlgorithm,
4174 public_key_pem: &str,
4175 signed_data: &[u8],
4176 signature_value: &[u8],
4177) -> Result<bool, SignatureVerificationPipelineError> {
4178 match algorithm {
4179 #[cfg(feature = "legacy-algorithms")]
4180 SignatureAlgorithm::RsaMd5 | SignatureAlgorithm::RsaRipemd160 => Ok(
4181 verify_rsa_signature_pem(algorithm, public_key_pem, signed_data, signature_value)?,
4182 ),
4183 #[cfg(feature = "legacy-algorithms")]
4184 SignatureAlgorithm::EcdsaRipemd160 => Ok(verify_ecdsa_signature_pem(
4185 algorithm,
4186 public_key_pem,
4187 signed_data,
4188 signature_value,
4189 )?),
4190 #[cfg(feature = "legacy-algorithms")]
4191 SignatureAlgorithm::HmacMd5 | SignatureAlgorithm::HmacRipemd160 => {
4192 Err(SignatureVerificationError::UnsupportedAlgorithm {
4193 uri: algorithm.uri().to_owned(),
4194 }
4195 .into())
4196 }
4197 SignatureAlgorithm::Ed25519
4198 | SignatureAlgorithm::Ed25519Ctx
4199 | SignatureAlgorithm::Ed25519Ph
4200 | SignatureAlgorithm::Ed448
4201 | SignatureAlgorithm::Ed448Ph
4202 | SignatureAlgorithm::PostQuantum(_) => {
4203 let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
4204 .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
4205 if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
4206 return Err(SignatureVerificationError::InvalidKeyPem.into());
4207 }
4208 Ok(super::modern::verify(
4209 algorithm,
4210 &pem.contents,
4211 signed_data,
4212 signature_value,
4213 )?)
4214 }
4215 SignatureAlgorithm::DsaSha1 | SignatureAlgorithm::DsaSha256 => {
4216 let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
4217 .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
4218 if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
4219 return Err(SignatureVerificationError::InvalidKeyPem.into());
4220 }
4221 Ok(verify_dsa_signature_spki(
4222 algorithm,
4223 &pem.contents,
4224 signed_data,
4225 signature_value,
4226 )?)
4227 }
4228 SignatureAlgorithm::HmacSha1
4229 | SignatureAlgorithm::HmacSha224
4230 | SignatureAlgorithm::HmacSha256
4231 | SignatureAlgorithm::HmacSha384
4232 | SignatureAlgorithm::HmacSha512 => Err(SignatureVerificationError::UnsupportedAlgorithm {
4233 uri: algorithm.uri().to_string(),
4234 }
4235 .into()),
4236 SignatureAlgorithm::RsaSha1
4237 | SignatureAlgorithm::RsaSha224
4238 | SignatureAlgorithm::RsaSha256
4239 | SignatureAlgorithm::RsaSha384
4240 | SignatureAlgorithm::RsaSha512 => Ok(verify_rsa_signature_pem(
4241 algorithm,
4242 public_key_pem,
4243 signed_data,
4244 signature_value,
4245 )?),
4246 SignatureAlgorithm::EcdsaSha1
4247 | SignatureAlgorithm::EcdsaSha224
4248 | SignatureAlgorithm::EcdsaSha256
4249 | SignatureAlgorithm::EcdsaSha384
4250 | SignatureAlgorithm::EcdsaSha512
4251 | SignatureAlgorithm::EcdsaSha3_224
4252 | SignatureAlgorithm::EcdsaSha3_256
4253 | SignatureAlgorithm::EcdsaSha3_384
4254 | SignatureAlgorithm::EcdsaSha3_512 => {
4255 match verify_ecdsa_signature_pem(
4259 algorithm,
4260 public_key_pem,
4261 signed_data,
4262 signature_value,
4263 ) {
4264 Ok(valid) => Ok(valid),
4265 Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
4266 Err(error) => Err(error.into()),
4267 }
4268 }
4269 }
4270}
4271
4272#[cfg(test)]
4273#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
4274mod tests {
4275 use super::*;
4276 use crate::c14n::C14nAlgorithm;
4277 use crate::xml::dom::Document;
4278 use crate::xmldsig::TransformError;
4279 use crate::xmldsig::digest::DigestAlgorithm;
4280 use crate::xmldsig::parse::{Reference, parse_signed_info};
4281 use crate::xmldsig::transforms::Transform;
4282 use crate::xmldsig::uri::UriReferenceResolver;
4283 use base64::Engine;
4284
4285 fn make_reference(
4289 uri: &str,
4290 transforms: Vec<Transform>,
4291 digest_method: DigestAlgorithm,
4292 digest_value: Vec<u8>,
4293 ) -> Reference {
4294 Reference {
4295 uri: Some(uri.to_string()),
4296 id: None,
4297 ref_type: None,
4298 transforms,
4299 digest_method,
4300 digest_value,
4301 }
4302 }
4303
4304 #[test]
4305 fn reference_resolution_uses_each_elements_effective_xml_base() {
4306 let first = b"first payload";
4309 let second = b"second payload";
4310 let first_digest = base64::engine::general_purpose::STANDARD
4311 .encode(compute_digest(DigestAlgorithm::Sha256, first));
4312 let second_digest = base64::engine::general_purpose::STANDARD
4313 .encode(compute_digest(DigestAlgorithm::Sha256, second));
4314 let xml = format!(
4315 r#"<root xml:base="https://example.test/base/" xmlns:ds="{XMLDSIG_NS}">
4316 <ds:Signature><ds:SignedInfo>
4317 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4318 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4319 <ds:Reference xml:base="one/" URI="payload.bin">
4320 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4321 <ds:DigestValue>{first_digest}</ds:DigestValue>
4322 </ds:Reference>
4323 <ds:Reference xml:base="../two/" URI="payload.bin">
4324 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4325 <ds:DigestValue>{second_digest}</ds:DigestValue>
4326 </ds:Reference>
4327 </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>
4328 </root>"#
4329 );
4330 let document = Document::parse(&xml).unwrap();
4331 let signature = document
4332 .descendants()
4333 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
4334 .unwrap();
4335 let signed_info_node = signature
4336 .children()
4337 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
4338 .unwrap();
4339 let signed_info = parse_signed_info(signed_info_node).unwrap();
4340 let resources = HashMap::from([
4341 (
4342 "https://example.test/base/one/payload.bin".into(),
4343 first.to_vec(),
4344 ),
4345 (
4346 "https://example.test/two/payload.bin".into(),
4347 second.to_vec(),
4348 ),
4349 ]);
4350 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4351
4352 let result = process_all_references(&signed_info.references, &resolver, signature, false)
4353 .expect("each Reference should resolve against its own effective base");
4354
4355 assert!(result.all_valid());
4356 }
4357
4358 #[test]
4359 fn internal_dtd_opt_in_applies_to_detached_xml_transforms() {
4360 let detached = b"<!DOCTYPE payload [<!ELEMENT payload (#PCDATA)>]><payload>ok</payload>";
4363 let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
4364 DigestAlgorithm::Sha256,
4365 b"<payload>ok</payload>",
4366 ));
4367 let xml = format!(
4368 r#"<root xmlns:ds="{XMLDSIG_NS}">
4369 <ds:Signature>
4370 <ds:SignedInfo>
4371 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4372 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4373 <ds:Reference URI="urn:detached-dtd">
4374 <ds:Transforms>
4375 <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
4376 </ds:Transforms>
4377 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4378 <ds:DigestValue>{digest}</ds:DigestValue>
4379 </ds:Reference>
4380 </ds:SignedInfo>
4381 <ds:SignatureValue>AQ==</ds:SignatureValue>
4382 </ds:Signature>
4383</root>"#
4384 );
4385 let resources = HashMap::from([("urn:detached-dtd".to_owned(), detached.to_vec())]);
4386 let key = AcceptingKey;
4387
4388 let default_error = VerifyContext::new()
4389 .key(&key)
4390 .allowed_uri_types(UriTypeSet::ALL)
4391 .external_resources(&resources)
4392 .verify(&xml)
4393 .expect_err("internal DTD parsing must remain disabled by default");
4394 assert!(matches!(
4395 default_error,
4396 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
4397 crate::xmldsig::TransformError::XmlParse(_)
4398 ))
4399 ));
4400
4401 let result = VerifyContext::new()
4402 .key(&key)
4403 .allowed_uri_types(UriTypeSet::ALL)
4404 .external_resources(&resources)
4405 .allow_internal_dtd(true)
4406 .verify(&xml)
4407 .expect("the explicit DTD opt-in must cover detached XML transforms");
4408
4409 assert_eq!(result.status, DsigStatus::Valid);
4410
4411 let external_entity = br#"<!DOCTYPE payload [
4412 <!ENTITY ext SYSTEM "file:///etc/passwd">
4413 ]><payload>&ext;</payload>"#;
4414 let external_entity_resources =
4415 HashMap::from([("urn:detached-dtd".to_owned(), external_entity.to_vec())]);
4416 let external_entity_error = VerifyContext::new()
4417 .key(&key)
4418 .allowed_uri_types(UriTypeSet::ALL)
4419 .external_resources(&external_entity_resources)
4420 .allow_internal_dtd(true)
4421 .verify(&xml)
4422 .expect_err("the internal-DTD opt-in must not resolve external entities");
4423 assert!(matches!(
4424 external_entity_error,
4425 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
4426 crate::xmldsig::TransformError::XmlParse(_)
4427 ))
4428 ));
4429 }
4430
4431 #[test]
4432 fn owned_verification_revalidates_internal_dtd_provenance() {
4433 let document = XmlDocument::parse_with_settings(
4436 "<!DOCTYPE root [<!ENTITY value \"ok\">]><root>&value;</root>".into(),
4437 DocumentParseSettings::new(
4438 true,
4439 crate::hard_limits::XML_DOCUMENT_NODE_CEILING,
4440 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
4441 ),
4442 )
4443 .expect("explicitly permitted DTD fixture must parse");
4444
4445 assert!(matches!(
4446 VerifyContext::new().verify_document(&document),
4447 Err(DsigError::Policy(
4448 crate::policy::PolicyViolation::XmlInput {
4449 reason: "owned document requires internal DTD support"
4450 }
4451 ))
4452 ));
4453 assert!(!matches!(
4454 VerifyContext::new()
4455 .allow_internal_dtd(true)
4456 .verify_document(&document),
4457 Err(DsigError::Policy(
4458 crate::policy::PolicyViolation::XmlInput { .. }
4459 ))
4460 ));
4461 }
4462
4463 #[test]
4464 fn verification_policy_bounds_reference_canonicalization() {
4465 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4468 let xml = format!(
4469 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>"#,
4470 "payload".repeat(16)
4471 );
4472 let policy = crate::policy::VerificationPolicy {
4473 resources: crate::policy::ResourcePolicy {
4474 max_canonicalized_bytes: 64,
4475 ..crate::policy::ResourcePolicy::default()
4476 },
4477 ..crate::policy::VerificationPolicy::default()
4478 };
4479
4480 let error = VerifyContext::new()
4481 .key(&AcceptingKey)
4482 .policy(policy)
4483 .verify(&xml)
4484 .expect_err("reference canonicalization must consume the policy budget");
4485
4486 assert!(
4487 matches!(
4488 error,
4489 SignatureVerificationPipelineError::Policy(
4490 crate::policy::PolicyViolation::ResourceLimit {
4491 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
4492 maximum: 64,
4493 ..
4494 }
4495 )
4496 ),
4497 "unexpected error: {error:?}"
4498 );
4499 }
4500
4501 #[test]
4502 fn verification_policy_bounds_document_bytes_before_parsing() {
4503 let xml = format!("<root>{}</root>", "x".repeat(1_024));
4506 let policy = crate::policy::VerificationPolicy {
4507 resources: crate::policy::ResourcePolicy {
4508 max_xml_document_bytes: xml.len() - 1,
4509 ..crate::policy::ResourcePolicy::default()
4510 },
4511 ..crate::policy::VerificationPolicy::default()
4512 };
4513
4514 assert!(matches!(
4515 VerifyContext::new().policy(policy).verify(&xml),
4516 Err(SignatureVerificationPipelineError::Policy(
4517 crate::policy::PolicyViolation::ResourceLimit {
4518 resource: crate::policy::resource_name::XML_DOCUMENT,
4519 maximum,
4520 actual,
4521 }
4522 )) if maximum == xml.len() - 1 && actual == xml.len()
4523 ));
4524 }
4525
4526 #[test]
4527 fn verification_entry_points_enforce_policy_depth() {
4528 let xml = "<root><child><leaf/></child></root>";
4531 let policy = crate::policy::VerificationPolicy {
4532 resources: crate::policy::ResourcePolicy {
4533 max_xml_depth: 2,
4534 ..crate::policy::ResourcePolicy::default()
4535 },
4536 ..crate::policy::VerificationPolicy::default()
4537 };
4538 let document = XmlDocument::parse(xml).expect("wide retained fixture must parse");
4539
4540 assert!(matches!(
4541 VerifyContext::new().policy(policy.clone()).verify(xml),
4542 Err(DsigError::Policy(
4543 crate::policy::PolicyViolation::ResourceLimit {
4544 resource: crate::policy::resource_name::XML_DEPTH,
4545 maximum: 2,
4546 actual: 3,
4547 }
4548 ))
4549 ));
4550 assert!(matches!(
4551 VerifyContext::new()
4552 .policy(policy)
4553 .verify_document(&document),
4554 Err(DsigError::Policy(
4555 crate::policy::PolicyViolation::ResourceLimit {
4556 resource: crate::policy::resource_name::XML_DEPTH,
4557 maximum: 2,
4558 actual: 3,
4559 }
4560 ))
4561 ));
4562 }
4563
4564 #[test]
4565 fn verification_policy_bounds_base64_transform_input() {
4566 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4569 let xml = format!(
4570 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>"##
4571 );
4572 let policy = crate::policy::VerificationPolicy {
4573 resources: crate::policy::ResourcePolicy {
4574 max_base64_transform_input_bytes: 4,
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("Base64 input must use the operation policy ceiling");
4585
4586 assert!(matches!(
4587 error,
4588 SignatureVerificationPipelineError::Policy(
4589 crate::policy::PolicyViolation::ResourceLimit {
4590 resource: crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
4591 maximum: 4,
4592 ..
4593 }
4594 )
4595 ));
4596 }
4597
4598 #[test]
4599 fn verification_policy_bounds_cumulative_base64_transform_output() {
4600 let first_digest = base64::engine::general_purpose::STANDARD
4603 .encode(compute_digest(DigestAlgorithm::Sha256, b"a"));
4604 let second_digest = base64::engine::general_purpose::STANDARD
4605 .encode(compute_digest(DigestAlgorithm::Sha256, b"b"));
4606 let xml = format!(
4607 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>"##
4608 );
4609 let policy = crate::policy::VerificationPolicy {
4610 resources: crate::policy::ResourcePolicy {
4611 max_base64_transform_input_bytes: 8,
4612 max_base64_transform_output_bytes: 1,
4613 ..crate::policy::ResourcePolicy::default()
4614 },
4615 ..crate::policy::VerificationPolicy::default()
4616 };
4617
4618 let error = VerifyContext::new()
4619 .key(&AcceptingKey)
4620 .policy(policy)
4621 .verify(&xml)
4622 .expect_err("references must share the Base64 output allowance");
4623
4624 assert!(matches!(
4625 error,
4626 SignatureVerificationPipelineError::Policy(
4627 crate::policy::PolicyViolation::ResourceLimit {
4628 resource: crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
4629 maximum: 1,
4630 actual: 2,
4631 }
4632 )
4633 ));
4634 }
4635
4636 #[test]
4637 fn verification_policy_bounds_xpath_source_before_compilation() {
4638 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4641 let xml = format!(
4642 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>"#
4643 );
4644 let policy = crate::policy::VerificationPolicy {
4645 resources: crate::policy::ResourcePolicy {
4646 max_xpath_expression_bytes: 4,
4647 ..crate::policy::ResourcePolicy::default()
4648 },
4649 ..crate::policy::VerificationPolicy::default()
4650 };
4651
4652 let error = VerifyContext::new()
4653 .key(&AcceptingKey)
4654 .policy(policy)
4655 .verify(&xml)
4656 .expect_err("XPath source must use the operation policy ceiling");
4657
4658 assert!(matches!(
4659 error,
4660 SignatureVerificationPipelineError::Policy(
4661 crate::policy::PolicyViolation::ResourceLimit {
4662 resource: crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
4663 maximum: 4,
4664 ..
4665 }
4666 )
4667 ));
4668 }
4669
4670 #[test]
4671 fn verification_policy_shares_canonicalization_budget_with_signed_info() {
4672 let payload_text = "x".repeat(700);
4676 let canonical_payload = format!("<payload ID=\"payload\">{payload_text}</payload>");
4677 let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
4678 DigestAlgorithm::Sha256,
4679 canonical_payload.as_bytes(),
4680 ));
4681 let xml = format!(
4682 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>"##
4683 );
4684 let policy = crate::policy::VerificationPolicy {
4685 resources: crate::policy::ResourcePolicy {
4686 max_canonicalized_bytes: 1_024,
4687 ..crate::policy::ResourcePolicy::default()
4688 },
4689 ..crate::policy::VerificationPolicy::default()
4690 };
4691
4692 let error = VerifyContext::new()
4693 .key(&AcceptingKey)
4694 .policy(policy)
4695 .verify(&xml)
4696 .expect_err("SignedInfo must consume the remaining operation C14N budget");
4697
4698 assert!(
4699 matches!(
4700 &error,
4701 SignatureVerificationPipelineError::Policy(
4702 crate::policy::PolicyViolation::ResourceLimit {
4703 resource: "canonicalized bytes",
4704 maximum: 1_024,
4705 ..
4706 }
4707 )
4708 ),
4709 "unexpected error: {error:?}"
4710 );
4711 }
4712
4713 #[test]
4714 fn manifest_processing_stops_after_c14n_budget_exhaustion() {
4715 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4718 let xml = format!(
4719 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>"##
4720 );
4721 let resources = HashMap::from([("urn:small".to_owned(), b"small".to_vec())]);
4722 let transform_budget = TransformExecutionBudget::with_c14n_limit(8);
4723 let ctx = VerifyContext::new()
4724 .allowed_uri_types(UriTypeSet::ALL)
4725 .external_resources(&resources);
4726 let document = XmlDocument::parse(xml).expect("test signature must parse");
4727 let results = document
4728 .with_view(|view| {
4729 let signature = view
4730 .document()
4731 .descendants()
4732 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
4733 .expect("test signature must contain Signature");
4734 let object = signature
4735 .children()
4736 .find(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
4737 .expect("test signature must contain Object");
4738 let resolver = UriReferenceResolver::with_document_view(view, &[])
4739 .with_external_resources(&resources);
4740 let budgets =
4741 VerificationOperationBudgets::with_transforms(&ctx.policy, transform_budget);
4742 let mut operation = OperationExecutionContext::new(
4743 &ctx.policy,
4744 &budgets,
4745 Some((view.identity(), view.generation())),
4746 );
4747 let crypto =
4748 operation.add_node(OperationNodeKind::Crypto, OperationStage::Crypto, None);
4749 operation.compile().expect("base plan");
4750 operation
4751 .run(crypto, || Ok::<_, SignatureVerificationPipelineError>(()))
4752 .expect("crypto gate");
4753 operation.authenticate(view.node_identity(object));
4754 process_authenticated_manifest_references(
4755 &mut operation,
4756 view,
4757 signature,
4758 &resolver,
4759 &ctx,
4760 2,
4761 crypto,
4762 )
4763 .map(|(results, _)| results)
4764 })
4765 .expect("resource exhaustion is reported per Manifest reference");
4766
4767 assert_eq!(results.len(), 2);
4768 assert!(results.iter().all(|result| {
4769 matches!(
4770 result.status,
4771 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { .. })
4772 )
4773 }));
4774 }
4775
4776 #[test]
4777 fn verification_policy_bounds_detached_xml_nodes() {
4778 let detached = format!("<payload>{}</payload>", "<n/>".repeat(32));
4781 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4782 let xml = format!(
4783 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>"#
4784 );
4785 let resources = HashMap::from([("urn:detached-nodes".to_owned(), detached.into_bytes())]);
4786 let policy = crate::policy::VerificationPolicy {
4787 uris: crate::policy::UriPolicy {
4788 references: UriTypeSet::ALL,
4789 ..crate::policy::UriPolicy::default()
4790 },
4791 resources: crate::policy::ResourcePolicy {
4792 max_xml_nodes: 24,
4793 ..crate::policy::ResourcePolicy::default()
4794 },
4795 ..crate::policy::VerificationPolicy::default()
4796 };
4797
4798 let error = VerifyContext::new()
4799 .key(&AcceptingKey)
4800 .policy(policy)
4801 .external_resources(&resources)
4802 .verify(&xml)
4803 .expect_err("detached XML must inherit the policy node ceiling");
4804
4805 assert!(
4806 matches!(
4807 error,
4808 SignatureVerificationPipelineError::Policy(
4809 crate::policy::PolicyViolation::ResourceLimit {
4810 resource: crate::policy::resource_name::XML_NODES,
4811 maximum: 24,
4812 ..
4813 }
4814 )
4815 ),
4816 "unexpected error: {error:?}"
4817 );
4818 }
4819
4820 #[test]
4821 fn query_only_reference_resolves_against_relative_xml_base() {
4822 let payload = b"query-selected payload";
4825 let digest = base64::engine::general_purpose::STANDARD
4826 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
4827 let xml = format!(
4828 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo>
4829 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4830 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4831 <ds:Reference xml:base="a/b?old" URI="?new">
4832 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4833 <ds:DigestValue>{digest}</ds:DigestValue>
4834 </ds:Reference>
4835 </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>"#
4836 );
4837 let document = Document::parse(&xml).unwrap();
4838 let signature = document.root_element();
4839 let signed_info_node = signature
4840 .children()
4841 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
4842 .unwrap();
4843 let signed_info = parse_signed_info(signed_info_node).unwrap();
4844 let resources = HashMap::from([("a/b?new".to_string(), payload.to_vec())]);
4845 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4846
4847 let result = process_all_references(&signed_info.references, &resolver, signature, false)
4848 .expect("query-only URI must resolve against the complete relative base path");
4849
4850 assert!(result.all_valid());
4851 }
4852
4853 #[test]
4854 fn manifest_reference_resolution_uses_its_effective_xml_base() {
4855 let payload = b"manifest payload";
4858 let digest = base64::engine::general_purpose::STANDARD
4859 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
4860 let xml = format!(
4861 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
4862 <ds:Object><ds:Manifest xml:base="manifests/">
4863 <ds:Reference URI="payload.bin">
4864 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4865 <ds:DigestValue>{digest}</ds:DigestValue>
4866 </ds:Reference>
4867 </ds:Manifest></ds:Object>
4868 </ds:Signature>"#
4869 );
4870 let document = Document::parse(&xml).unwrap();
4871 let signature = document.root_element();
4872 let reference_node = signature
4873 .descendants()
4874 .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
4875 .unwrap();
4876 let reference = super::super::parse::parse_reference(reference_node).unwrap();
4877 let resources = HashMap::from([(
4878 "https://example.test/manifests/payload.bin".to_string(),
4879 payload.to_vec(),
4880 )]);
4881 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4882
4883 let result = process_reference(
4884 &reference,
4885 &resolver,
4886 signature,
4887 ReferenceSet::Manifest,
4888 0,
4889 false,
4890 )
4891 .expect("Manifest Reference should inherit its own XML Base context");
4892
4893 assert_eq!(result.status, DsigStatus::Valid);
4894 }
4895
4896 #[test]
4897 fn manifest_reference_index_ignores_nested_manifest_descendants() {
4898 let payload = b"direct manifest payload";
4901 let digest = base64::engine::general_purpose::STANDARD
4902 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
4903 let xml = format!(
4904 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
4905 <ds:Object><wrapper><ds:Manifest xml:base="nested/">
4906 <ds:Reference URI="payload.bin">
4907 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4908 <ds:DigestValue>{digest}</ds:DigestValue>
4909 </ds:Reference>
4910 </ds:Manifest></wrapper></ds:Object>
4911 <ds:Object><ds:Manifest xml:base="direct/">
4912 <ds:Reference URI="payload.bin">
4913 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4914 <ds:DigestValue>{digest}</ds:DigestValue>
4915 </ds:Reference>
4916 </ds:Manifest></ds:Object>
4917 </ds:Signature>"#
4918 );
4919 let document = Document::parse(&xml).unwrap();
4920 let signature = document.root_element();
4921 let direct_reference_node = signature
4922 .children()
4923 .filter(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
4924 .nth(1)
4925 .unwrap()
4926 .children()
4927 .find(|node| node.has_tag_name((XMLDSIG_NS, "Manifest")))
4928 .unwrap()
4929 .children()
4930 .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
4931 .unwrap();
4932 let reference = super::super::parse::parse_reference(direct_reference_node).unwrap();
4933 let resources = HashMap::from([(
4934 "https://example.test/direct/payload.bin".to_string(),
4935 payload.to_vec(),
4936 )]);
4937 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4938
4939 let result = process_reference(
4940 &reference,
4941 &resolver,
4942 signature,
4943 ReferenceSet::Manifest,
4944 0,
4945 false,
4946 )
4947 .expect("Manifest index must select the direct Object/Manifest reference");
4948
4949 assert_eq!(result.status, DsigStatus::Valid);
4950 }
4951
4952 struct RejectingKey;
4953
4954 impl VerifyingKey for RejectingKey {
4955 fn verify(
4956 &self,
4957 _algorithm: SignatureAlgorithm,
4958 _signed_data: &[u8],
4959 _signature_value: &[u8],
4960 ) -> Result<bool, SignatureVerificationPipelineError> {
4961 Ok(false)
4962 }
4963 }
4964
4965 struct AcceptingKey;
4966
4967 impl VerifyingKey for AcceptingKey {
4968 fn verify(
4969 &self,
4970 _algorithm: SignatureAlgorithm,
4971 _signed_data: &[u8],
4972 _signature_value: &[u8],
4973 ) -> Result<bool, SignatureVerificationPipelineError> {
4974 Ok(true)
4975 }
4976 }
4977
4978 struct PanicResolver;
4979
4980 struct UntrustedTestResolver;
4981
4982 impl KeyResolver for UntrustedTestResolver {
4983 fn resolve<'a>(
4984 &'a self,
4985 _key_info: Option<&KeyInfo>,
4986 _algorithm: SignatureAlgorithm,
4987 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
4988 Ok(Some(Box::new(AcceptingKey)))
4989 }
4990 }
4991
4992 #[test]
4993 fn verification_requires_explicit_key_authorization() {
4994 let xml = signature_with_target_reference("AQ==");
4996 let error = VerifyContext::new()
4997 .key_resolver(&UntrustedTestResolver)
4998 .verify(&xml)
4999 .expect_err("default verification must reject an untrusted resolver candidate");
5000 assert!(matches!(
5001 error,
5002 DsigError::Policy(crate::policy::PolicyViolation::KeyTrust { .. })
5003 ));
5004 }
5005
5006 #[test]
5007 fn cryptographic_only_is_explicit_and_never_reports_trust() {
5008 let mut policy = crate::policy::VerificationPolicy::default();
5010 policy.key_trust.mode = crate::policy::VerificationTrustMode::CryptographicOnly;
5011 let result = VerifyContext::new()
5012 .policy(policy)
5013 .key_resolver(&UntrustedTestResolver)
5014 .verify(&signature_with_target_reference("AQ=="))
5015 .expect("explicit mathematical mode permits an untrusted candidate");
5016 assert_eq!(result.status, DsigStatus::Valid);
5017 assert_eq!(
5018 result.key_trust,
5019 super::super::KeyTrustEvidence::NotEstablished
5020 );
5021 }
5022
5023 #[test]
5024 fn caller_key_authorization_does_not_authenticate_an_invalid_signature() {
5025 let result = VerifyContext::new()
5027 .key(&RejectingKey)
5028 .verify(&signature_with_target_reference("AQ=="))
5029 .expect("an invalid mathematical signature is a completed result");
5030 assert_eq!(
5031 result.status,
5032 DsigStatus::Invalid(FailureReason::SignatureMismatch)
5033 );
5034 assert_eq!(
5035 result.key_trust,
5036 super::super::KeyTrustEvidence::CallerTrusted
5037 );
5038 }
5039
5040 impl KeyResolver for PanicResolver {
5041 fn resolve<'a>(
5042 &'a self,
5043 _key_info: Option<&KeyInfo>,
5044 _algorithm: SignatureAlgorithm,
5045 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
5046 {
5047 panic!("resolver should not be called when references already fail");
5048 }
5049 }
5050
5051 struct MissingKeyResolver;
5052
5053 impl KeyResolver for MissingKeyResolver {
5054 fn resolve<'a>(
5055 &'a self,
5056 _key_info: Option<&KeyInfo>,
5057 _algorithm: SignatureAlgorithm,
5058 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
5059 {
5060 Ok(None)
5061 }
5062 }
5063
5064 struct ConsumingKeyInfoResolver;
5065
5066 impl KeyResolver for ConsumingKeyInfoResolver {
5067 fn resolve<'a>(
5068 &'a self,
5069 _key_info: Option<&KeyInfo>,
5070 _algorithm: SignatureAlgorithm,
5071 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
5072 {
5073 Ok(None)
5074 }
5075
5076 fn consumes_document_key_info(&self) -> bool {
5077 true
5078 }
5079 }
5080
5081 struct FallbackKeyInfoResolver;
5082
5083 impl KeyResolver for FallbackKeyInfoResolver {
5084 fn resolve_for_verification<'a>(
5085 &'a self,
5086 key_info: Option<&KeyInfo>,
5087 algorithm: SignatureAlgorithm,
5088 _policy: &crate::policy::VerificationPolicy,
5089 _provider: &dyn crate::provider::CryptoProvider,
5090 ) -> Result<Option<super::super::ResolvedVerificationKey<'a>>, DsigError> {
5091 self.resolve(key_info, algorithm)
5093 .map(|key| key.map(super::super::ResolvedVerificationKey::CallerTrusted))
5094 }
5095
5096 fn resolve<'a>(
5097 &'a self,
5098 key_info: Option<&KeyInfo>,
5099 _algorithm: SignatureAlgorithm,
5100 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
5101 {
5102 let sources = &key_info.expect("KeyInfo must be parsed").sources;
5103 assert!(matches!(
5104 sources.as_slice(),
5105 [
5106 super::super::parse::KeyInfoSource::RetrievalMethod { .. },
5107 super::super::parse::KeyInfoSource::KeyName(name),
5108 ] if name == "fallback"
5109 ));
5110 Ok(Some(Box::new(AcceptingKey)))
5111 }
5112
5113 fn consumes_document_key_info(&self) -> bool {
5114 true
5115 }
5116 }
5117
5118 struct EarlyKeyInfoResolver;
5119
5120 impl KeyResolver for EarlyKeyInfoResolver {
5121 fn resolve_for_verification<'a>(
5122 &'a self,
5123 key_info: Option<&KeyInfo>,
5124 algorithm: SignatureAlgorithm,
5125 _policy: &crate::policy::VerificationPolicy,
5126 _provider: &dyn crate::provider::CryptoProvider,
5127 ) -> Result<Option<super::super::ResolvedVerificationKey<'a>>, DsigError> {
5128 self.resolve(key_info, algorithm)
5130 .map(|key| key.map(super::super::ResolvedVerificationKey::CallerTrusted))
5131 }
5132
5133 fn resolve<'a>(
5134 &'a self,
5135 key_info: Option<&KeyInfo>,
5136 _algorithm: SignatureAlgorithm,
5137 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
5138 {
5139 let sources = &key_info.expect("KeyInfo must be parsed").sources;
5140 assert!(matches!(
5141 sources.as_slice(),
5142 [
5143 super::super::parse::KeyInfoSource::KeyName(name),
5144 super::super::parse::KeyInfoSource::RetrievalMethod { .. },
5145 ] if name == "primary"
5146 ));
5147 Ok(Some(Box::new(AcceptingKey)))
5148 }
5149
5150 fn consumes_document_key_info(&self) -> bool {
5151 true
5152 }
5153 }
5154
5155 fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
5156 format!(
5157 r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5158 <ds:SignedInfo>
5159 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5160 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5161 <ds:Reference URI="{reference_uri}">
5162 {transforms_xml}
5163 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
5164 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
5165 </ds:Reference>
5166 </ds:SignedInfo>
5167 <ds:SignatureValue>AQ==</ds:SignatureValue>
5168</ds:Signature>"#
5169 )
5170 }
5171
5172 #[test]
5173 fn verify_all_retains_each_signature_without_vacuous_success() {
5174 let document = XmlDocument::parse(format!(
5177 "<root xmlns:ds=\"{XMLDSIG_NS}\"><ds:Signature/><ds:Signature/></root>"
5178 ))
5179 .unwrap();
5180 let evidence = VerifyContext::new().verify_all(&document).unwrap();
5181 assert_eq!(evidence.signatures().len(), 2);
5182 assert!(
5183 evidence
5184 .signatures()
5185 .iter()
5186 .all(|entry| entry.result().is_err())
5187 );
5188 assert!(!evidence.all_valid());
5189 let empty = XmlDocument::parse("<root/>").unwrap();
5190 assert!(!VerifyContext::new().verify_all(&empty).unwrap().all_valid());
5191 }
5192
5193 #[test]
5194 fn verify_all_checks_cardinality_before_running_keys() {
5195 let document = XmlDocument::parse(format!(
5198 "<root xmlns:ds=\"{XMLDSIG_NS}\"><ds:Signature/><ds:Signature/></root>"
5199 ))
5200 .unwrap();
5201 let mut policy = crate::policy::VerificationPolicy::default();
5202 policy.resources.max_signatures = 1;
5203 assert!(matches!(
5204 VerifyContext::new().policy(policy).verify_all(&document),
5205 Err(DsigError::Policy(
5206 crate::policy::PolicyViolation::ResourceLimit {
5207 resource: "verification signatures",
5208 maximum: 1,
5209 actual: 2,
5210 }
5211 ))
5212 ));
5213 }
5214
5215 #[test]
5216 fn verification_signature_limit_also_gates_the_single_api() {
5217 let mut policy = crate::policy::VerificationPolicy::default();
5219 policy.resources.max_signatures = 0;
5220 assert!(matches!(
5221 VerifyContext::new()
5222 .policy(policy)
5223 .key(&AcceptingKey)
5224 .verify(&signature_with_target_reference("AQ==")),
5225 Err(DsigError::Policy(
5226 crate::policy::PolicyViolation::ResourceLimit {
5227 resource: "verification signatures",
5228 maximum: 0,
5229 actual: 1,
5230 }
5231 ))
5232 ));
5233 }
5234
5235 #[test]
5236 fn verify_all_preserves_generation_and_rejects_wrapped_siblings() {
5237 let xml = signature_with_target_reference("AQ==")
5239 .replace("</root>", "<target ID=\"unsigned\">payload</target></root>");
5240 let document = XmlDocument::parse(xml).unwrap();
5241 let (signed, unsigned) = document.with_view(|view| {
5242 (
5243 view.node_for_id("target", &[]).unwrap(),
5244 view.node_for_id("unsigned", &[]).unwrap(),
5245 )
5246 });
5247 let evidence = VerifyContext::new()
5248 .key(&AcceptingKey)
5249 .verify_all(&document)
5250 .unwrap();
5251 assert!(evidence.all_valid());
5252 assert!(evidence.covers_element(&document, signed).unwrap());
5253 assert!(!evidence.covers_element(&document, unsigned).unwrap());
5254 let foreign = XmlDocument::parse("<target/>").unwrap();
5255 let target = foreign.with_view(|view| view.root_element());
5256 assert!(matches!(
5257 evidence.covers_element(&document, target),
5258 Err(DsigError::Document(XmlDocumentError::ForeignIdentity))
5259 ));
5260 }
5261
5262 #[test]
5263 fn verify_request_rejects_duplicate_targets_before_crypto() {
5264 let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
5266 let target = document.with_view(|view| view.node_for_id("target", &[]).unwrap());
5267 let targets = [target, target];
5268 let request = VerificationRequest {
5269 expected_targets: &targets,
5270 ..VerificationRequest::default()
5271 };
5272 assert!(matches!(
5273 VerifyContext::new().verify_request(&document, &request),
5274 Err(DsigError::InvalidRequest {
5275 reason: "duplicate expected coverage target"
5276 })
5277 ));
5278 }
5279
5280 #[test]
5281 fn verify_all_shares_canonicalization_retention_budget() {
5282 let xml = signature_with_target_reference("AQ==");
5284 let single = VerifyContext::new()
5285 .key(&AcceptingKey)
5286 .store_pre_digest(true)
5287 .verify(&xml)
5288 .unwrap();
5289 let bytes = single.canonicalized_signed_info.unwrap().len()
5290 + single.signed_info_references[0]
5291 .pre_digest_data
5292 .as_ref()
5293 .unwrap()
5294 .len();
5295 let parsed = Document::parse(&xml).unwrap();
5296 let signature = parsed
5297 .descendants()
5298 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
5299 .unwrap();
5300 let xml = xml.replace("</root>", &format!("{}</root>", &xml[signature.range()]));
5301 let document = XmlDocument::parse(xml).unwrap();
5302 let mut policy = crate::policy::VerificationPolicy::default();
5303 policy.resources.max_canonicalized_bytes = bytes * 2 - 1;
5304 let evidence = VerifyContext::new()
5305 .key(&AcceptingKey)
5306 .store_pre_digest(true)
5307 .policy(policy)
5308 .verify_all(&document)
5309 .unwrap();
5310 assert_eq!(evidence.signatures.len(), 2);
5311 assert_eq!(
5312 evidence.signatures[0].result().as_ref().unwrap().status,
5313 DsigStatus::Valid
5314 );
5315 assert!(matches!(
5316 evidence.signatures[1].result(),
5317 Err(DsigError::Policy(
5318 crate::policy::PolicyViolation::ResourceLimit {
5319 resource: "canonicalized bytes",
5320 ..
5321 }
5322 ))
5323 ));
5324 assert!(!evidence.accepted(&document).unwrap());
5325 }
5326
5327 #[test]
5328 fn verify_all_manifest_coverage_requires_its_authenticated_digest() {
5329 for valid in [true, false] {
5332 let document = XmlDocument::parse(signature_with_manifest_xml(valid)).unwrap();
5333 let target = document.with_view(|view| view.node_for_id("target", &[]).unwrap());
5334 let evidence = VerifyContext::new()
5335 .key(&AcceptingKey)
5336 .process_manifests(true)
5337 .verify_all(&document)
5338 .unwrap();
5339 let result = evidence.signatures()[0].result().as_ref().unwrap();
5340 assert_eq!(result.manifest_references.len(), 1);
5341 assert!(result.manifest_references[0].reference_identity.is_some());
5342 assert_eq!(evidence.covers_element(&document, target).unwrap(), valid);
5343 assert_eq!(evidence.accepted(&document).unwrap(), valid);
5344 }
5345 }
5346
5347 #[test]
5348 fn verify_all_filtered_digest_does_not_claim_original_subtree() {
5349 let mut reference = make_reference(
5352 "#target",
5353 vec![Transform::XpathExcludeAllSignatures],
5354 DigestAlgorithm::Sha256,
5355 vec![],
5356 );
5357 let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
5358 document.with_view(|view| {
5359 let target = view.node_for_id("target", &[]).unwrap();
5360 let mut result = manifest_reference_invalid_result(
5361 &reference,
5362 0,
5363 FailureReason::ReferenceProcessingFailure { ref_index: 0 },
5364 );
5365 result.status = DsigStatus::Valid;
5366 bind_reference_evidence(
5367 &mut result,
5368 &reference,
5369 view,
5370 None,
5371 &OperationResourceIdentity::DocumentNode(target),
5372 );
5373 assert_eq!(result.target_identity, Some(target));
5374 assert_eq!(result.coverage, ReferenceCoverage::TransformedData);
5375 reference.transforms = vec![Transform::C14n(crate::c14n::C14nAlgorithm::new(
5376 crate::c14n::C14nMode::Exclusive1_0,
5377 false,
5378 ))];
5379 bind_reference_evidence(
5380 &mut result,
5381 &reference,
5382 view,
5383 None,
5384 &OperationResourceIdentity::DocumentNode(target),
5385 );
5386 assert_eq!(result.coverage, ReferenceCoverage::ElementContent);
5387 });
5388 }
5389
5390 #[test]
5391 fn verify_request_rejects_conflicting_resource_owners() {
5392 let document = XmlDocument::parse("<root/>").unwrap();
5395 let resources = HashMap::new();
5396 let request = VerificationRequest {
5397 external_resources: Some(&resources),
5398 ..VerificationRequest::default()
5399 };
5400 assert!(matches!(
5401 VerifyContext::new()
5402 .external_resources(&resources)
5403 .verify_request(&document, &request),
5404 Err(DsigError::InvalidRequest {
5405 reason: "external resources configured in both context and request"
5406 })
5407 ));
5408 }
5409
5410 fn signature_with_target_reference(signature_value_b64: &str) -> String {
5411 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5412 <target ID="target">payload</target>
5413 <ds:Signature>
5414 <ds:SignedInfo>
5415 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5416 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5417 <ds:Reference URI="#target">
5418 <ds:Transforms>
5419 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5420 </ds:Transforms>
5421 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
5422 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
5423 </ds:Reference>
5424 </ds:SignedInfo>
5425 <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
5426 </ds:Signature>
5427</root>"##;
5428
5429 let doc = Document::parse(xml_template).unwrap();
5430 let sig_node = doc
5431 .descendants()
5432 .find(|node| node.is_element() && node.tag_name().name() == "Signature")
5433 .unwrap();
5434 let signed_info_node = sig_node
5435 .children()
5436 .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
5437 .unwrap();
5438 let signed_info = parse_signed_info(signed_info_node).unwrap();
5439 let reference = &signed_info.references[0];
5440 let resolver = UriReferenceResolver::new(&doc);
5441 let initial_data = resolver
5442 .dereference(reference.uri.as_deref().unwrap())
5443 .unwrap();
5444 let pre_digest =
5445 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
5446 .unwrap();
5447 let digest = compute_digest(reference.digest_method, &pre_digest);
5448 let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
5449 xml_template
5450 .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
5451 .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
5452 }
5453
5454 #[test]
5455 fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
5456 let xml = signature_with_target_reference("AQ==");
5457
5458 let result = VerifyContext::new()
5459 .verify(&xml)
5460 .expect("missing key config must be reported as verification status");
5461 assert!(
5462 matches!(
5463 result.status,
5464 DsigStatus::Invalid(FailureReason::KeyNotFound)
5465 ),
5466 "unexpected status: {:?}",
5467 result.status
5468 );
5469 }
5470
5471 #[test]
5472 fn verify_context_rejects_disallowed_uri() {
5473 let xml = minimal_signature_xml("http://example.com/external", "");
5474 let err = VerifyContext::new()
5475 .key(&RejectingKey)
5476 .verify(&xml)
5477 .expect_err("external URI should be rejected by default policy");
5478 assert!(matches!(
5479 err,
5480 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
5481 operation: "verification",
5482 ..
5483 })
5484 ));
5485 }
5486
5487 #[test]
5488 fn verify_context_bounds_effective_xml_base_components() {
5489 let mut xml = minimal_signature_xml("payload", "");
5492 for _ in 0..65 {
5493 xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
5494 }
5495 let resources = HashMap::new();
5496 let error = VerifyContext::new()
5497 .key(&AcceptingKey)
5498 .allowed_uri_types(UriTypeSet::ALL)
5499 .external_resources(&resources)
5500 .verify(&xml)
5501 .expect_err("XML Base component work must be bounded before lookup");
5502
5503 assert!(matches!(
5504 error,
5505 SignatureVerificationPipelineError::Policy(
5506 crate::policy::PolicyViolation::ResourceLimit {
5507 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
5508 maximum: 64,
5509 actual: 65,
5510 }
5511 )
5512 ));
5513 }
5514
5515 #[test]
5516 fn verify_context_bounds_cumulative_xml_base_resolution_bytes() {
5517 let mut xml = minimal_signature_xml("payload", "");
5520 for _ in 0..2 {
5521 xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
5522 }
5523 let resources = HashMap::new();
5524 let mut policy = crate::policy::VerificationPolicy::default();
5525 policy.resources.max_xml_base_resolution_bytes = 32;
5526 let error = VerifyContext::new()
5527 .policy(policy)
5528 .key(&AcceptingKey)
5529 .allowed_uri_types(UriTypeSet::ALL)
5530 .external_resources(&resources)
5531 .verify(&xml)
5532 .expect_err("cumulative XML Base copies must obey the operation budget");
5533
5534 assert!(matches!(
5535 error,
5536 SignatureVerificationPipelineError::Policy(
5537 crate::policy::PolicyViolation::ResourceLimit {
5538 resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
5539 maximum: 32,
5540 ..
5541 }
5542 )
5543 ));
5544 }
5545
5546 #[test]
5547 fn verify_context_applies_xml_base_policy_to_signed_info_c14n() {
5548 let xml = signature_with_target_reference("AQ==")
5552 .replacen(
5553 "http://www.w3.org/2001/10/xml-exc-c14n#",
5554 "http://www.w3.org/2006/12/xml-c14n11",
5555 1,
5556 )
5557 .replace(
5558 " <ds:Signature>",
5559 " <outer xml:base=\"one/\"><inner xml:base=\"two/\"><ds:Signature>",
5560 )
5561 .replace(" </ds:Signature>", " </ds:Signature></inner></outer>");
5562 let policy = crate::policy::VerificationPolicy {
5563 resources: crate::policy::ResourcePolicy {
5564 max_xml_base_components: 1,
5565 ..crate::policy::ResourcePolicy::default()
5566 },
5567 ..crate::policy::VerificationPolicy::default()
5568 };
5569
5570 let error = VerifyContext::new()
5571 .key(&AcceptingKey)
5572 .policy(policy)
5573 .verify(&xml)
5574 .expect_err("SignedInfo C14N must use the operation XML Base budget");
5575
5576 assert!(matches!(
5577 error,
5578 SignatureVerificationPipelineError::Policy(
5579 crate::policy::PolicyViolation::ResourceLimit {
5580 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
5581 maximum: 1,
5582 actual: 2,
5583 }
5584 )
5585 ));
5586 }
5587
5588 #[test]
5589 fn verify_context_classifies_signed_info_xml_base_byte_limit_as_policy() {
5590 let xml = signature_with_target_reference("AQ==")
5593 .replacen(
5594 "http://www.w3.org/2001/10/xml-exc-c14n#",
5595 "http://www.w3.org/2006/12/xml-c14n11",
5596 1,
5597 )
5598 .replace(
5599 " <ds:Signature>",
5600 " <outer xml:base=\"segment/\"><ds:Signature>",
5601 )
5602 .replace(" </ds:Signature>", " </ds:Signature></outer>");
5603 let policy = crate::policy::VerificationPolicy {
5604 resources: crate::policy::ResourcePolicy {
5605 max_xml_base_resolution_bytes: 1,
5606 ..crate::policy::ResourcePolicy::default()
5607 },
5608 ..crate::policy::VerificationPolicy::default()
5609 };
5610
5611 let error = VerifyContext::new()
5612 .key(&AcceptingKey)
5613 .policy(policy)
5614 .verify(&xml)
5615 .expect_err("SignedInfo XML Base byte exhaustion must be a policy error");
5616
5617 assert!(matches!(
5618 error,
5619 SignatureVerificationPipelineError::Policy(
5620 crate::policy::PolicyViolation::ResourceLimit {
5621 resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
5622 maximum: 1,
5623 actual,
5624 }
5625 ) if actual > 1
5626 ));
5627 }
5628
5629 #[test]
5630 fn verify_context_meters_repeated_external_dereferences() {
5631 let payload = b"payload";
5634 let digest = base64::engine::general_purpose::STANDARD.encode(
5635 crate::xmldsig::compute_digest(DigestAlgorithm::Sha1, payload),
5636 );
5637 let reference = format!(
5638 r#"<ds:Reference URI="urn:payload"><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference>"#
5639 );
5640 let xml = format!(
5641 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>"#
5642 );
5643 let resources = HashMap::from([("urn:payload".to_owned(), payload.to_vec())]);
5644 let policy = crate::policy::VerificationPolicy {
5645 uris: crate::policy::UriPolicy {
5646 references: UriTypeSet::ALL,
5647 ..crate::policy::UriPolicy::default()
5648 },
5649 resources: crate::policy::ResourcePolicy {
5650 max_external_resource_bytes: payload.len(),
5651 max_external_resource_total_bytes: payload.len(),
5652 ..crate::policy::ResourcePolicy::default()
5653 },
5654 ..crate::policy::VerificationPolicy::default()
5655 };
5656
5657 let error = VerifyContext::new()
5658 .key(&AcceptingKey)
5659 .policy(policy)
5660 .external_resources(&resources)
5661 .verify(&xml)
5662 .expect_err("the second dereference must exhaust the aggregate byte ceiling");
5663
5664 assert!(
5665 error
5666 .to_string()
5667 .contains("aggregate external resource bytes")
5668 );
5669 }
5670
5671 #[test]
5672 fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
5673 let xml = minimal_signature_xml("", "");
5674 let err = VerifyContext::new()
5675 .key(&RejectingKey)
5676 .allowed_uri_types(UriTypeSet::new(false, true, false))
5677 .verify(&xml)
5678 .expect_err("empty URI must be rejected when empty references are disabled");
5679 assert!(matches!(
5680 err,
5681 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
5682 operation: "verification",
5683 ..
5684 })
5685 ));
5686 }
5687
5688 #[test]
5689 fn verify_context_rejects_disallowed_transform() {
5690 let xml = minimal_signature_xml(
5691 "",
5692 r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
5693 );
5694 let err = VerifyContext::new()
5695 .key(&RejectingKey)
5696 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
5697 .verify(&xml)
5698 .expect_err("enveloped transform should be rejected by allowlist");
5699 assert!(matches!(
5700 err,
5701 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
5702 operation: "verification transform",
5703 ..
5704 })
5705 ));
5706 }
5707
5708 #[test]
5709 fn verify_context_applies_transform_allowlist_to_signed_info_c14n() {
5710 let xml = signature_with_target_reference("AQ==").replacen(
5713 "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
5714 "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"/>",
5715 1,
5716 );
5717 let error = VerifyContext::new()
5718 .key(&AcceptingKey)
5719 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
5720 .verify(&xml)
5721 .expect_err("SignedInfo C14N must obey the operation transform allowlist");
5722
5723 assert!(matches!(
5724 error,
5725 SignatureVerificationPipelineError::Policy(
5726 crate::policy::PolicyViolation::Algorithm {
5727 operation: "verification transform",
5728 ref algorithm,
5729 }
5730 )
5731 if algorithm == "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
5732 ));
5733 }
5734
5735 #[test]
5736 fn verify_context_applies_transform_allowlist_to_key_retrieval() {
5737 let xml = signature_with_target_reference("AQ==")
5740 .replacen(
5741 "</ds:Signature>",
5742 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>"##,
5743 1,
5744 )
5745 .replacen(
5746 "</root>",
5747 r#"<holder ID="keys"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder></root>"#,
5748 1,
5749 );
5750 let error = VerifyContext::new()
5751 .key_resolver(&ConsumingKeyInfoResolver)
5752 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
5753 .verify(&xml)
5754 .expect_err("RetrievalMethod XPath must obey the operation transform allowlist");
5755
5756 assert!(matches!(
5757 error,
5758 SignatureVerificationPipelineError::Policy(
5759 crate::policy::PolicyViolation::Algorithm {
5760 operation: "verification transform",
5761 ref algorithm,
5762 }
5763 )
5764 if algorithm == XPATH_TRANSFORM_URI
5765 ));
5766 }
5767
5768 fn signature_with_manifest_xml(valid_manifest_digest: bool) -> String {
5769 signature_with_manifest_xml_with_manifest_mutation(valid_manifest_digest, |xml| xml)
5770 }
5771
5772 fn signature_with_manifest_xml_with_manifest_mutation<F>(
5773 valid_manifest_digest: bool,
5774 mutate_manifest: F,
5775 ) -> String
5776 where
5777 F: FnOnce(String) -> String,
5778 {
5779 const TMP_SIGNED_INFO_DIGEST: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
5780 const INVALID_MANIFEST_DIGEST: &str = "//////////////////////////8=";
5781 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5782 <target ID="target">payload</target>
5783 <ds:Signature>
5784 <ds:SignedInfo>
5785 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5786 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5787 <ds:Reference URI="#manifest">
5788 <ds:Transforms>
5789 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5790 </ds:Transforms>
5791 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
5792 <ds:DigestValue>SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER</ds:DigestValue>
5793 </ds:Reference>
5794 </ds:SignedInfo>
5795 <ds:SignatureValue>AQ==</ds:SignatureValue>
5796 <ds:Object>
5797 <ds:Manifest ID="manifest">
5798 <ds:Reference URI="#target">
5799 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
5800 <ds:DigestValue>MANIFEST_DIGEST_PLACEHOLDER</ds:DigestValue>
5801 </ds:Reference>
5802 </ds:Manifest>
5803 </ds:Object>
5804 </ds:Signature>
5805</root>"##;
5806 let seed_xml = xml_template.replace(
5807 "SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER",
5808 TMP_SIGNED_INFO_DIGEST,
5809 );
5810 let doc = Document::parse(&seed_xml).unwrap();
5811 let signature_node = doc
5812 .descendants()
5813 .find(|node| {
5814 node.is_element()
5815 && node.tag_name().namespace() == Some(XMLDSIG_NS)
5816 && node.tag_name().name() == "Signature"
5817 })
5818 .unwrap();
5819 let resolver = UriReferenceResolver::new(&doc);
5820 let initial_data = resolver.dereference("#target").unwrap();
5821 let manifest_pre_digest =
5822 crate::xmldsig::execute_transforms(signature_node, initial_data, &[]).unwrap();
5823 let computed_manifest_digest_b64 = base64::engine::general_purpose::STANDARD
5824 .encode(compute_digest(DigestAlgorithm::Sha1, &manifest_pre_digest));
5825 let final_manifest_digest_b64 = if valid_manifest_digest {
5826 computed_manifest_digest_b64.as_str()
5827 } else {
5828 INVALID_MANIFEST_DIGEST
5829 };
5830 let xml_with_manifest_digest = mutate_manifest(
5831 seed_xml.replace("MANIFEST_DIGEST_PLACEHOLDER", final_manifest_digest_b64),
5832 );
5833 let signed_doc = Document::parse(&xml_with_manifest_digest).unwrap();
5834 let signed_signature_node = signed_doc
5835 .descendants()
5836 .find(|node| {
5837 node.is_element()
5838 && node.tag_name().namespace() == Some(XMLDSIG_NS)
5839 && node.tag_name().name() == "Signature"
5840 })
5841 .unwrap();
5842 let signed_info_node = signed_signature_node
5843 .children()
5844 .find(|node| {
5845 node.is_element()
5846 && node.tag_name().namespace() == Some(XMLDSIG_NS)
5847 && node.tag_name().name() == "SignedInfo"
5848 })
5849 .unwrap();
5850 let signed_info = parse_signed_info(signed_info_node).unwrap();
5851 let object_reference = &signed_info.references[0];
5852 let signed_resolver = UriReferenceResolver::new(&signed_doc);
5853 let signed_initial_data = signed_resolver
5854 .dereference(object_reference.uri.as_deref().unwrap())
5855 .unwrap();
5856 let signed_pre_digest = crate::xmldsig::execute_transforms(
5857 signed_signature_node,
5858 signed_initial_data,
5859 &object_reference.transforms,
5860 )
5861 .unwrap();
5862 let signed_digest_b64 = base64::engine::general_purpose::STANDARD.encode(compute_digest(
5863 object_reference.digest_method,
5864 &signed_pre_digest,
5865 ));
5866
5867 xml_with_manifest_digest.replacen(TMP_SIGNED_INFO_DIGEST, &signed_digest_b64, 1)
5868 }
5869
5870 fn replace_fixture_manifest_digest(xml: &str, replacement: &str) -> String {
5871 let object_marker = "<ds:Object>";
5872 let object_start = xml
5873 .find(object_marker)
5874 .expect("fixture should contain ds:Object")
5875 + object_marker.len();
5876 let open = "<ds:DigestValue>";
5877 let close = "</ds:DigestValue>";
5878 let value_start = xml[object_start..]
5879 .find(open)
5880 .map(|offset| object_start + offset + open.len())
5881 .expect("Manifest should contain DigestValue");
5882 let value_end = xml[value_start..]
5883 .find(close)
5884 .map(|offset| value_start + offset)
5885 .expect("Manifest DigestValue must be closed");
5886
5887 format!("{}{replacement}{}", &xml[..value_start], &xml[value_end..])
5888 }
5889
5890 #[test]
5891 fn verify_context_processes_manifest_references_when_enabled() {
5892 let xml = signature_with_manifest_xml(true);
5893
5894 let result_without_manifests = VerifyContext::new()
5895 .key(&RejectingKey)
5896 .verify(&xml)
5897 .expect("manifest processing disabled should still verify SignedInfo");
5898 assert!(
5899 result_without_manifests.manifest_references.is_empty(),
5900 "manifest results must stay empty when manifest processing is disabled",
5901 );
5902 assert!(matches!(
5903 result_without_manifests.status,
5904 DsigStatus::Invalid(FailureReason::SignatureMismatch)
5905 ));
5906
5907 let malformed_manifest_xml = signature_with_manifest_xml(true).replacen(
5908 "</ds:Object>",
5909 "</ds:Object><ds:Object><ds:Manifest><ds:Foo/></ds:Manifest></ds:Object>",
5910 1,
5911 );
5912 let malformed_with_manifests_disabled = VerifyContext::new()
5913 .key(&RejectingKey)
5914 .verify(&malformed_manifest_xml)
5915 .expect("malformed Manifest must be ignored when manifest processing is disabled");
5916 assert!(
5917 malformed_with_manifests_disabled
5918 .manifest_references
5919 .is_empty(),
5920 "manifest parser must not run when process_manifests is disabled",
5921 );
5922 assert!(matches!(
5923 malformed_with_manifests_disabled.status,
5924 DsigStatus::Invalid(FailureReason::SignatureMismatch)
5925 ));
5926
5927 let result_with_manifests = VerifyContext::new()
5928 .key(&AcceptingKey)
5929 .process_manifests(true)
5930 .verify(&xml)
5931 .expect("manifest references should be processed when enabled");
5932 assert_eq!(result_with_manifests.manifest_references.len(), 1);
5933 assert_eq!(
5934 result_with_manifests.manifest_references[0].reference_set,
5935 ReferenceSet::Manifest
5936 );
5937 assert_eq!(
5938 result_with_manifests.manifest_references[0].reference_index,
5939 0
5940 );
5941 assert!(matches!(
5942 result_with_manifests.manifest_references[0].status,
5943 DsigStatus::Valid
5944 ));
5945 assert!(matches!(result_with_manifests.status, DsigStatus::Valid));
5946 }
5947
5948 #[test]
5949 fn verify_context_skips_manifest_work_when_signature_value_is_invalid() {
5950 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5954 replace_fixture_manifest_digest(&xml, "!!!")
5955 });
5956 assert!(
5957 xml.split_once("<ds:Object>")
5958 .is_some_and(|(_, object)| object.contains("<ds:DigestValue>!!!</ds:DigestValue>")),
5959 "fixture mutation must corrupt the nested Manifest DigestValue",
5960 );
5961
5962 let result = VerifyContext::new()
5963 .key(&RejectingKey)
5964 .process_manifests(true)
5965 .verify(&xml)
5966 .expect("invalid SignatureValue must short-circuit Manifest parsing");
5967
5968 assert!(matches!(
5969 result.status,
5970 DsigStatus::Invalid(FailureReason::SignatureMismatch)
5971 ));
5972 assert!(result.manifest_references.is_empty());
5973 }
5974
5975 #[test]
5976 fn verify_context_shares_xpath_parse_budget_with_manifest_references() {
5977 let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
5980 .repeat(64);
5981 let transform = format!(
5982 r#"<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</ds:Transform>"#
5983 );
5984 let max_transforms = transform.repeat(16);
5985 let max_manifest_reference = format!(
5986 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>"##
5987 );
5988 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5989 xml.replacen(
5990 r##"<ds:Reference URI="#target">"##,
5991 &format!(
5992 r##"<ds:Reference URI="#target"><ds:Transforms>{}</ds:Transforms>"##,
5993 max_transforms
5994 ),
5995 1,
5996 )
5997 .replacen(
5998 "</ds:SignedInfo>",
5999 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>"##,
6000 1,
6001 )
6002 .replacen(
6003 "</ds:Manifest>",
6004 &format!("{}</ds:Manifest>", max_manifest_reference.repeat(3)),
6005 1,
6006 )
6007 });
6008
6009 let error = VerifyContext::new()
6010 .key(&AcceptingKey)
6011 .process_manifests(true)
6012 .verify(&xml)
6013 .expect_err("SignedInfo and Manifest References must share one XPath parse budget");
6014
6015 assert!(
6016 matches!(
6017 &error,
6018 SignatureVerificationPipelineError::Policy(
6019 crate::policy::PolicyViolation::ResourceLimit {
6020 resource: "XPath expressions",
6021 ..
6022 }
6023 )
6024 ),
6025 "unexpected error: {error:?}"
6026 );
6027 }
6028
6029 #[test]
6030 fn verify_context_processes_manifest_when_signedinfo_references_object() {
6031 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6032 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
6033 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
6034 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
6035 });
6036
6037 let result = VerifyContext::new()
6038 .key(&AcceptingKey)
6039 .process_manifests(true)
6040 .verify(&xml)
6041 .expect("manifest references should be processed when SignedInfo references ds:Object");
6042 assert_eq!(
6043 result.manifest_references.len(),
6044 1,
6045 "signed ds:Object should enable processing of its direct-child ds:Manifest",
6046 );
6047 assert_eq!(
6048 result.manifest_references[0].reference_set,
6049 ReferenceSet::Manifest
6050 );
6051 assert_eq!(result.manifest_references[0].reference_index, 0);
6052 assert!(matches!(
6053 result.manifest_references[0].status,
6054 DsigStatus::Valid
6055 ));
6056 }
6057
6058 #[test]
6059 fn verify_context_skips_manifest_removed_by_enveloped_transform() {
6060 for target_object in [false, true] {
6064 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6065 let xml = xml.replacen(
6066 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
6067 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#"/>"#,
6068 1,
6069 );
6070 if target_object {
6071 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
6072 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
6073 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
6074 } else {
6075 xml
6076 }
6077 });
6078
6079 let result = VerifyContext::new()
6080 .key(&AcceptingKey)
6081 .process_manifests(true)
6082 .store_pre_digest(true)
6083 .verify(&xml)
6084 .expect("an emptied reference remains a valid core digest input");
6085
6086 assert!(matches!(result.status, DsigStatus::Valid));
6087 assert_eq!(
6088 result.signed_info_references[0].pre_digest_data.as_deref(),
6089 Some([].as_slice()),
6090 "target_object={target_object} must have empty transformed bytes",
6091 );
6092 assert!(
6093 result.manifest_references.is_empty(),
6094 "target_object={target_object} must not authenticate the Manifest",
6095 );
6096 }
6097 }
6098
6099 #[test]
6100 fn verify_context_ignores_manifest_excluded_from_signed_object() {
6101 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6105 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
6106 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
6107 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
6108 .replacen(
6109 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
6110 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#"/>"#,
6111 1,
6112 )
6113 });
6114
6115 let result = VerifyContext::new()
6116 .key(&AcceptingKey)
6117 .process_manifests(true)
6118 .verify(&xml)
6119 .expect("excluded Manifest content must be ignored, not parsed");
6120
6121 assert!(matches!(result.status, DsigStatus::Valid));
6122 assert!(
6123 result.manifest_references.is_empty(),
6124 "a transform-excluded Manifest is not authenticated by SignedInfo",
6125 );
6126 }
6127
6128 #[test]
6129 fn verify_context_skips_manifest_digest_work_when_signature_is_invalid() {
6130 let xml = signature_with_manifest_xml(false);
6131 let result = VerifyContext::new()
6132 .key(&RejectingKey)
6133 .process_manifests(true)
6134 .verify(&xml)
6135 .expect("invalid SignatureValue must short-circuit Manifest digest work");
6136 assert!(result.manifest_references.is_empty());
6137 assert!(matches!(
6138 result.status,
6139 DsigStatus::Invalid(FailureReason::SignatureMismatch)
6140 ));
6141 }
6142
6143 #[test]
6144 fn verify_context_manifest_digest_mismatch_is_non_fatal_with_accepting_key() {
6145 let xml = signature_with_manifest_xml(false);
6146 let result = VerifyContext::new()
6147 .key(&AcceptingKey)
6148 .process_manifests(true)
6149 .verify(&xml)
6150 .expect("manifest digest mismatches should be recorded while signature stays valid");
6151 assert_eq!(result.manifest_references.len(), 1);
6152 assert!(matches!(
6153 result.manifest_references[0].status,
6154 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
6155 ));
6156 assert!(matches!(result.status, DsigStatus::Valid));
6157 }
6158
6159 #[test]
6160 fn invalid_outer_manifest_digest_does_not_parse_nested_manifest() {
6161 let xml = signature_with_manifest_xml_with_manifest_mutation(false, |xml| {
6165 xml.replacen("URI=\"#target\"", "URI=\"#inner-object\"", 1)
6166 .replacen(
6167 "</ds:Object>",
6168 "</ds:Object><ds:Object ID=\"inner-object\"><ds:Manifest>junk<ds:Unexpected/></ds:Manifest></ds:Object>",
6169 1,
6170 )
6171 });
6172
6173 let result = VerifyContext::new()
6174 .key(&AcceptingKey)
6175 .process_manifests(true)
6176 .verify(&xml)
6177 .expect("an unauthenticated nested Manifest must remain opaque");
6178
6179 assert_eq!(result.manifest_references.len(), 1);
6180 assert!(matches!(
6181 result.manifest_references[0].status,
6182 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
6183 ));
6184 }
6185
6186 #[test]
6187 fn verify_context_skips_manifest_parsing_when_signedinfo_reference_fails() {
6188 let xml = signature_with_manifest_xml(true);
6191 let (signed_info_prefix, object_suffix) = xml
6192 .split_once("<ds:Object>")
6193 .expect("fixture should contain ds:Object");
6194 let open = "<ds:DigestValue>";
6195 let close = "</ds:DigestValue>";
6196 let digest_start = signed_info_prefix
6197 .find(open)
6198 .expect("SignedInfo should contain DigestValue");
6199 let digest_end = signed_info_prefix[digest_start + open.len()..]
6200 .find(close)
6201 .map(|offset| digest_start + open.len() + offset)
6202 .expect("SignedInfo DigestValue must be closed");
6203 let broken_signed_info_prefix = format!(
6204 "{}{}AAAAAAAAAAAAAAAAAAAAAAAAAAA={}{}",
6205 &signed_info_prefix[..digest_start],
6206 open,
6207 close,
6208 &signed_info_prefix[digest_end + close.len()..],
6209 );
6210 let broken_xml = format!("{broken_signed_info_prefix}<ds:Object>{object_suffix}");
6211 let result = VerifyContext::new()
6212 .key(&RejectingKey)
6213 .process_manifests(true)
6214 .verify(&broken_xml)
6215 .expect("SignedInfo digest failure should return without parsing Manifests");
6216 assert!(matches!(
6217 result.status,
6218 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
6219 ));
6220 assert!(
6221 result.manifest_references.is_empty(),
6222 "unauthenticated Manifest content must not be parsed",
6223 );
6224 }
6225
6226 #[test]
6227 fn verify_context_skips_manifest_policy_work_when_signature_is_invalid() {
6228 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6231 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
6232 });
6233 let result = VerifyContext::new()
6234 .key(&RejectingKey)
6235 .process_manifests(true)
6236 .verify(&broken_xml)
6237 .expect("invalid SignatureValue must short-circuit Manifest policy work");
6238 assert!(result.manifest_references.is_empty());
6239 assert!(matches!(
6240 result.status,
6241 DsigStatus::Invalid(FailureReason::SignatureMismatch)
6242 ));
6243 }
6244
6245 #[test]
6246 fn verify_context_records_manifest_policy_violations_with_accepting_key() {
6247 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6248 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
6249 });
6250 let result = VerifyContext::new()
6251 .key(&AcceptingKey)
6252 .process_manifests(true)
6253 .verify(&broken_xml)
6254 .expect("manifest policy violations should be recorded while signature stays valid");
6255 assert_eq!(result.manifest_references.len(), 1);
6256 assert!(matches!(
6257 result.manifest_references[0].status,
6258 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
6259 ));
6260 assert!(matches!(result.status, DsigStatus::Valid));
6261 }
6262
6263 #[test]
6264 fn verify_context_applies_digest_policy_to_manifest_references() {
6265 let policy = crate::policy::VerificationPolicy {
6268 manifest_processing: crate::policy::ManifestProcessing::Process,
6269 digest_algorithms: Some(HashSet::from([DigestAlgorithm::Sha1])),
6270 ..crate::policy::VerificationPolicy::default()
6271 };
6272 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
6273 let legacy = "http://www.w3.org/2000/09/xmldsig#sha1";
6274 let offset = xml
6275 .rfind(legacy)
6276 .expect("Manifest DigestMethod must be present");
6277 xml.replace_range(offset..offset + legacy.len(), DigestAlgorithm::Sha256.uri());
6278 let value_start = xml[offset..]
6279 .find("<ds:DigestValue>")
6280 .map(|relative| offset + relative + "<ds:DigestValue>".len())
6281 .expect("Manifest DigestValue must be present");
6282 let value_end = xml[value_start..]
6283 .find("</ds:DigestValue>")
6284 .map(|relative| value_start + relative)
6285 .expect("Manifest DigestValue must be closed");
6286 xml.replace_range(
6287 value_start..value_end,
6288 &base64::engine::general_purpose::STANDARD.encode([0_u8; 32]),
6289 );
6290 xml
6291 });
6292 let result = VerifyContext::new()
6293 .key(&AcceptingKey)
6294 .policy(policy)
6295 .verify(&xml)
6296 .expect("a disallowed Manifest digest is a per-reference result");
6297
6298 assert!(matches!(result.status, DsigStatus::Valid));
6299 assert!(matches!(
6300 result.manifest_references[0].status,
6301 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
6302 ));
6303 }
6304
6305 #[test]
6306 fn verify_context_applies_transform_count_policy_to_manifest_references() {
6307 let policy = crate::policy::VerificationPolicy {
6310 manifest_processing: crate::policy::ManifestProcessing::Process,
6311 resources: crate::policy::ResourcePolicy {
6312 max_transforms_per_reference: 1,
6313 ..crate::policy::ResourcePolicy::default()
6314 },
6315 ..crate::policy::VerificationPolicy::default()
6316 };
6317 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
6318 let manifest_start = xml
6319 .find("<ds:Manifest")
6320 .expect("fixture must contain a Manifest");
6321 let manifest = xml[manifest_start..].replacen(
6322 "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
6323 concat!(
6324 "<ds:Transforms>",
6325 "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
6326 "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
6327 "</ds:Transforms>",
6328 "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>"
6329 ),
6330 1,
6331 );
6332 xml.replace_range(manifest_start.., &manifest);
6333 xml
6334 });
6335 let result = VerifyContext::new()
6336 .key(&AcceptingKey)
6337 .policy(policy)
6338 .verify(&xml)
6339 .expect("Manifest transform policy is a per-reference result");
6340
6341 assert!(matches!(result.status, DsigStatus::Valid));
6342 assert!(matches!(
6343 result.manifest_references[0].status,
6344 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
6345 ));
6346 }
6347
6348 #[test]
6349 fn verify_context_skips_manifest_uri_work_when_signature_is_invalid() {
6350 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6353 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
6354 });
6355
6356 let result = VerifyContext::new()
6357 .key(&RejectingKey)
6358 .process_manifests(true)
6359 .verify(&broken_xml)
6360 .expect("invalid SignatureValue must short-circuit Manifest URI processing");
6361 assert!(result.manifest_references.is_empty());
6362 assert!(matches!(
6363 result.status,
6364 DsigStatus::Invalid(FailureReason::SignatureMismatch)
6365 ));
6366 }
6367
6368 #[test]
6369 fn verify_context_records_manifest_missing_uri_with_accepting_key() {
6370 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6371 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
6372 });
6373
6374 let result = VerifyContext::new()
6375 .key(&AcceptingKey)
6376 .process_manifests(true)
6377 .verify(&broken_xml)
6378 .expect("manifest missing URI should be recorded while signature stays valid");
6379 assert_eq!(result.manifest_references.len(), 1);
6380 assert_eq!(result.manifest_references[0].uri, "<omitted>");
6381 assert!(matches!(
6382 result.manifest_references[0].status,
6383 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
6384 ));
6385 assert!(matches!(result.status, DsigStatus::Valid));
6386 }
6387
6388 #[test]
6389 fn verify_context_ignores_nested_manifests_in_object() {
6390 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6393 xml.replacen(
6394 "<ds:Manifest ID=\"manifest\">",
6395 "<wrapper><ds:Manifest ID=\"manifest\">",
6396 1,
6397 )
6398 .replacen("</ds:Manifest>", "</ds:Manifest></wrapper>", 1)
6399 });
6400
6401 let result = VerifyContext::new()
6402 .key(&AcceptingKey)
6403 .process_manifests(true)
6404 .verify(&xml)
6405 .expect("nested Manifest nodes are ignored in strict mode");
6406 assert!(
6407 result.manifest_references.is_empty(),
6408 "only direct ds:Manifest children of ds:Object must be processed"
6409 );
6410 assert!(matches!(result.status, DsigStatus::Valid));
6411 }
6412
6413 #[test]
6414 fn verify_context_reports_manifest_reference_parse_errors_explicitly() {
6415 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6418 replace_fixture_manifest_digest(&xml, "!!!")
6419 });
6420
6421 let err = VerifyContext::new()
6422 .key(&AcceptingKey)
6423 .process_manifests(true)
6424 .verify(&broken_xml)
6425 .expect_err("invalid Manifest DigestValue must map to ParseManifestReference");
6426 assert!(matches!(
6427 err,
6428 SignatureVerificationPipelineError::ParseManifestReference(_)
6429 ));
6430 }
6431
6432 #[test]
6433 fn verify_context_reports_unsupported_manifest_transform_with_declared_digest() {
6434 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6437 let xml = xml.replacen(
6438 "<ds:Reference URI=\"#target\">",
6439 "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
6440 1,
6441 );
6442 let xml = xml.replacen(
6443 "</ds:Transforms>\n <ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
6444 "</ds:Transforms>\n <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>",
6445 1,
6446 );
6447 replace_fixture_manifest_digest(&xml, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
6448 });
6449 assert!(xml.contains("urn:unsupported"));
6450 assert!(xml.contains("http://www.w3.org/2001/04/xmlenc#sha256"));
6451
6452 let result = VerifyContext::new()
6453 .key(&AcceptingKey)
6454 .process_manifests(true)
6455 .verify(&xml)
6456 .expect("unsupported Manifest transform is a per-reference result");
6457 assert_eq!(result.status, DsigStatus::Valid);
6458 assert_eq!(result.manifest_references.len(), 1);
6459 assert_eq!(
6460 result.manifest_references[0].digest_algorithm,
6461 DigestAlgorithm::Sha256
6462 );
6463 assert!(matches!(
6464 result.manifest_references[0].status,
6465 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
6466 ));
6467
6468 let restricted = VerifyContext::new()
6469 .key(&AcceptingKey)
6470 .process_manifests(true)
6471 .allowed_transforms([
6472 DEFAULT_IMPLICIT_C14N_URI,
6473 "http://www.w3.org/2001/10/xml-exc-c14n#",
6474 ])
6475 .verify(&xml)
6476 .expect("a disallowed Manifest transform is a per-reference policy result");
6477 assert!(matches!(
6478 restricted.manifest_references[0].status,
6479 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
6480 ));
6481 }
6482
6483 #[test]
6484 fn manifest_reference_limit_counts_unsupported_entries() {
6485 let references = (0..=MAX_REFERENCES_PER_SIGNATURE)
6486 .map(|index| {
6487 format!(
6488 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>"##
6489 )
6490 })
6491 .collect::<String>();
6492 let xml = format!(
6493 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>"#
6494 );
6495 let document = XmlDocument::parse(xml).unwrap();
6496 let error = document.with_view(|view| {
6497 let signature = view.document().root_element();
6498 let object = signature.children().find(|node| node.is_element()).unwrap();
6499 let policy = crate::policy::VerificationPolicy::default();
6500 let budgets = VerificationOperationBudgets::with_transforms(
6501 &policy,
6502 TransformExecutionBudget::from_resources(&policy.resources),
6503 );
6504 let operation = OperationExecutionContext::new(
6505 &policy,
6506 &budgets,
6507 Some((view.identity(), view.generation())),
6508 );
6509 operation.authenticate(view.node_identity(object));
6510 let mut processed = HashSet::new();
6511 let mut remaining = MAX_REFERENCES_PER_SIGNATURE;
6512 let mut next_index = 0;
6513 let mut xpath_parse = XPathSignatureParseBudget::default();
6514 let mut state = ManifestDiscoveryState {
6515 processed: &mut processed,
6516 remaining_capacity: &mut remaining,
6517 next_reference_index: &mut next_index,
6518 xpath_parse: &mut xpath_parse,
6519 };
6520 match parse_manifest_references(signature, &operation, view, &mut state, None) {
6521 Ok(_) => panic!("unsupported references must consume the same aggregate limit"),
6522 Err(error) => error,
6523 }
6524 });
6525 assert!(matches!(
6526 error,
6527 SignatureVerificationPipelineError::InvalidStructure {
6528 reason: "signed Manifests exceed the per-signature Reference limit"
6529 }
6530 ));
6531 }
6532
6533 #[test]
6534 fn unsigned_manifest_remains_eligible_after_trust_expands() {
6535 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
6539 let xml = format!(
6540 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>"##
6541 );
6542 let document = XmlDocument::parse(xml).expect("nested Manifest fixture must parse");
6543 document.with_view(|view| {
6544 let signature = view.document().root_element();
6545 let mut objects = signature.children().filter(|node| node.is_element());
6546 let outer = objects.next().expect("outer Object");
6547 let inner = objects.next().expect("inner Object");
6548 let policy = crate::policy::VerificationPolicy::default();
6549 let budgets = VerificationOperationBudgets::with_transforms(
6550 &policy,
6551 TransformExecutionBudget::from_resources(&policy.resources),
6552 );
6553 let operation = OperationExecutionContext::new(
6554 &policy,
6555 &budgets,
6556 Some((view.identity(), view.generation())),
6557 );
6558 operation.authenticate(view.node_identity(outer));
6559 let mut processed = HashSet::new();
6560 let mut remaining = 2;
6561 let mut next_index = 0;
6562 let mut xpath_budget = XPathSignatureParseBudget::default();
6563
6564 let first = parse_manifest_references(
6565 signature,
6566 &operation,
6567 view,
6568 &mut ManifestDiscoveryState {
6569 processed: &mut processed,
6570 remaining_capacity: &mut remaining,
6571 next_reference_index: &mut next_index,
6572 xpath_parse: &mut xpath_budget,
6573 },
6574 None,
6575 )
6576 .expect("outer Manifest must be discovered");
6577 assert_eq!(first.references.len(), 1);
6578 assert_eq!(first.references[0].reference.uri.as_deref(), Some("#inner"));
6579
6580 operation.authenticate(view.node_identity(inner));
6581 let second = parse_manifest_references(
6582 signature,
6583 &operation,
6584 view,
6585 &mut ManifestDiscoveryState {
6586 processed: &mut processed,
6587 remaining_capacity: &mut remaining,
6588 next_reference_index: &mut next_index,
6589 xpath_parse: &mut xpath_budget,
6590 },
6591 None,
6592 )
6593 .expect("newly authenticated sibling Manifest must remain eligible");
6594 assert_eq!(second.references.len(), 1);
6595 assert_eq!(
6596 second.references[0].reference.uri.as_deref(),
6597 Some("#payload")
6598 );
6599 });
6600 }
6601
6602 #[test]
6603 fn manifest_reference_limit_includes_signed_info_references() {
6604 let xml = signature_with_manifest_xml(true);
6607 let reference_start = xml
6608 .find(r##"<ds:Reference URI="#manifest">"##)
6609 .expect("fixture SignedInfo must reference the Manifest");
6610 let reference_end = xml[reference_start..]
6611 .find("</ds:Reference>")
6612 .map(|offset| reference_start + offset + "</ds:Reference>".len())
6613 .expect("fixture SignedInfo Reference must be closed");
6614 let repeated = xml[reference_start..reference_end].repeat(MAX_REFERENCES_PER_SIGNATURE);
6615 let xml = format!(
6616 "{}{repeated}{}",
6617 &xml[..reference_start],
6618 &xml[reference_end..]
6619 );
6620
6621 let error = VerifyContext::new()
6622 .key(&AcceptingKey)
6623 .process_manifests(true)
6624 .verify(&xml)
6625 .expect_err("one Manifest Reference must exceed the exhausted signature-wide limit");
6626
6627 assert!(matches!(
6628 error,
6629 SignatureVerificationPipelineError::InvalidStructure {
6630 reason: "signed Manifests exceed the per-signature Reference limit"
6631 }
6632 ));
6633 }
6634
6635 #[test]
6636 fn configured_reference_limit_is_shared_with_manifests() {
6637 let policy = crate::policy::VerificationPolicy {
6640 manifest_processing: crate::policy::ManifestProcessing::Process,
6641 resources: crate::policy::ResourcePolicy {
6642 max_references: 1,
6643 ..crate::policy::ResourcePolicy::default()
6644 },
6645 ..crate::policy::VerificationPolicy::default()
6646 };
6647
6648 let error = VerifyContext::new()
6649 .key(&AcceptingKey)
6650 .policy(policy)
6651 .verify(&signature_with_manifest_xml(true))
6652 .expect_err("Manifest must exceed the caller-selected aggregate limit");
6653 assert!(matches!(
6654 error,
6655 SignatureVerificationPipelineError::InvalidStructure {
6656 reason: "signed Manifests exceed the per-signature Reference limit"
6657 }
6658 ));
6659 }
6660
6661 #[test]
6662 fn retrieval_method_materializes_single_x509_data_subtree() {
6663 for uri in [
6664 "#target",
6665 "#xpointer(id('target'))",
6666 "#xpointer(id("target"))",
6667 ] {
6668 for target_xml in [
6669 r#"<ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>"#,
6670 r#"<holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>"#,
6671 ] {
6672 let xml = format!(
6673 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>"#
6674 );
6675 let document = Document::parse(&xml).unwrap();
6676 let key_info_node = document
6677 .descendants()
6678 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
6679 .unwrap();
6680 let mut key_info = parse_key_info(key_info_node).unwrap();
6681 let resolver = UriReferenceResolver::new(&document);
6682
6683 materialize_retrieval_methods(
6684 &mut key_info,
6685 &resolver,
6686 UriTypeSet::SAME_DOCUMENT,
6687 None,
6688 crate::provider::default_provider(),
6689 )
6690 .expect("XPath filter must produce one X509Data-rooted node-set");
6691 assert!(matches!(
6692 key_info.sources.as_slice(),
6693 [super::super::parse::KeyInfoSource::X509Data(info)]
6694 if info.subject_names == ["CN=leaf"]
6695 ));
6696 }
6697 }
6698 }
6699
6700 fn retrieval_method_xpath_signature() -> String {
6701 format!(
6702 r##"<root xmlns:ds="{XMLDSIG_NS}">
6703 <payload Id="payload">ok</payload>
6704 <ds:Signature>
6705 <ds:SignedInfo>
6706 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6707 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6708 <ds:Reference URI="#payload">
6709 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6710 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6711 </ds:Reference>
6712 </ds:SignedInfo>
6713 <ds:SignatureValue>AQ==</ds:SignatureValue>
6714 <ds:KeyInfo>
6715 <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
6716 <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
6717 <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
6718 </ds:Transform></ds:Transforms>
6719 </ds:RetrievalMethod>
6720 </ds:KeyInfo>
6721 </ds:Signature>
6722 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
6723 </root>"##
6724 )
6725 }
6726
6727 #[test]
6728 fn retrieval_method_xpath_uses_signature_expression_budget() {
6729 let mut policy = crate::policy::VerificationPolicy::default();
6732 policy.resources.max_xpath_expressions = 0;
6733
6734 let error = VerifyContext::new()
6735 .policy(policy)
6736 .verify(&retrieval_method_xpath_signature())
6737 .expect_err("RetrievalMethod XPath must consume the signature parse budget");
6738
6739 assert!(
6740 matches!(
6741 &error,
6742 SignatureVerificationPipelineError::Policy(
6743 crate::policy::PolicyViolation::ResourceLimit {
6744 resource: "XPath expressions",
6745 maximum: 0,
6746 actual: 1,
6747 }
6748 )
6749 ),
6750 "unexpected error: {error:?}"
6751 );
6752 }
6753
6754 #[test]
6755 fn retrieval_method_xpath_obeys_expression_byte_limit() {
6756 let expression = "ancestor-or-self::ds:X509Data";
6759 let padded_expression = format!(" {expression} ");
6760 let xml = retrieval_method_xpath_signature().replace(expression, &padded_expression);
6761 let mut policy = crate::policy::VerificationPolicy::default();
6762 policy.resources.max_xpath_expression_bytes = expression.len();
6763
6764 let error = VerifyContext::new()
6765 .policy(policy)
6766 .verify(&xml)
6767 .expect_err("RetrievalMethod XPath must obey the expression byte limit");
6768
6769 assert!(
6770 matches!(
6771 &error,
6772 SignatureVerificationPipelineError::Policy(
6773 crate::policy::PolicyViolation::ResourceLimit {
6774 resource: "XPath expression bytes",
6775 maximum,
6776 actual,
6777 }
6778 ) if *maximum == expression.len() && *actual == padded_expression.len()
6779 ),
6780 "unexpected error: {error:?}"
6781 );
6782 }
6783
6784 #[test]
6785 fn retrieval_method_xpath_obeys_expression_complexity_limit() {
6786 let mut policy = crate::policy::VerificationPolicy::default();
6789 policy.resources.max_xpath_expression_complexity = 0;
6790
6791 let error = VerifyContext::new()
6792 .policy(policy)
6793 .verify(&retrieval_method_xpath_signature())
6794 .expect_err("RetrievalMethod XPath must obey the complexity limit");
6795
6796 assert!(
6797 matches!(
6798 &error,
6799 SignatureVerificationPipelineError::Policy(
6800 crate::policy::PolicyViolation::ResourceLimit {
6801 resource: "XPath expression complexity",
6802 maximum: 0,
6803 actual,
6804 }
6805 ) if *actual > 0
6806 ),
6807 "unexpected error: {error:?}"
6808 );
6809 }
6810
6811 #[test]
6812 fn retrieval_method_xpath_uses_node_filter_work_budget() {
6813 let mut policy = crate::policy::VerificationPolicy::default();
6816 policy.resources.max_node_set_filter_work = 4;
6817 let xml = retrieval_method_xpath_signature().replace(
6818 "<holder Id=\"target\">",
6819 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
6820 );
6821
6822 let error = VerifyContext::new()
6823 .policy(policy)
6824 .verify(&xml)
6825 .expect_err("RetrievalMethod XPath must consume node-filter work");
6826
6827 assert!(
6828 matches!(
6829 &error,
6830 SignatureVerificationPipelineError::Policy(
6831 crate::policy::PolicyViolation::ResourceLimit {
6832 resource: crate::policy::resource_name::NODE_SET_FILTER_WORK,
6833 maximum: 4,
6834 actual,
6835 }
6836 ) if *actual > 4
6837 ),
6838 "unexpected error: {error:?}"
6839 );
6840 }
6841
6842 #[test]
6843 fn retrieval_method_xpath_charges_every_context_to_evaluation_work() {
6844 let mut policy = crate::policy::VerificationPolicy::default();
6847 policy.resources.max_xpath_evaluation_work = 4;
6848 let xml = retrieval_method_xpath_signature().replace(
6849 "<holder Id=\"target\">",
6850 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
6851 );
6852
6853 let error = VerifyContext::new()
6854 .policy(policy)
6855 .verify(&xml)
6856 .expect_err("RetrievalMethod XPath must charge every evaluation context");
6857
6858 assert!(matches!(
6859 error,
6860 SignatureVerificationPipelineError::Policy(
6861 crate::policy::PolicyViolation::ResourceLimit {
6862 resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
6863 maximum: 4,
6864 actual,
6865 }
6866 ) if actual > 4
6867 ));
6868 }
6869
6870 #[test]
6871 fn retrieval_method_xpath_obeys_namespace_binding_limit() {
6872 let mut policy = crate::policy::VerificationPolicy::default();
6875 policy.resources.max_xpath_namespace_bindings = 0;
6876
6877 let error = VerifyContext::new()
6878 .policy(policy)
6879 .verify(&retrieval_method_xpath_signature())
6880 .expect_err("RetrievalMethod XPath namespaces must obey the binding limit");
6881
6882 assert!(matches!(
6883 error,
6884 SignatureVerificationPipelineError::Policy(
6885 crate::policy::PolicyViolation::ResourceLimit {
6886 resource: crate::policy::resource_name::XPATH_NAMESPACE_BINDINGS,
6887 maximum: 0,
6888 actual,
6889 }
6890 ) if actual > 0
6891 ));
6892 }
6893
6894 #[test]
6895 fn retrieval_method_xpath_obeys_namespace_byte_limit() {
6896 let mut policy = crate::policy::VerificationPolicy::default();
6899 policy.resources.max_xpath_namespace_bytes = 0;
6900
6901 let error = VerifyContext::new()
6902 .policy(policy)
6903 .verify(&retrieval_method_xpath_signature())
6904 .expect_err("RetrievalMethod XPath namespaces must obey the byte limit");
6905
6906 assert!(matches!(
6907 error,
6908 SignatureVerificationPipelineError::Policy(
6909 crate::policy::PolicyViolation::ResourceLimit {
6910 resource: crate::policy::resource_name::XPATH_NAMESPACE_BYTES,
6911 maximum: 0,
6912 actual,
6913 }
6914 ) if actual > 0
6915 ));
6916 }
6917
6918 #[test]
6919 fn retrieval_method_xpath_obeys_context_evaluation_limit() {
6920 let mut policy = crate::policy::VerificationPolicy::default();
6924 policy.resources.max_xpath_context_evaluations = 4;
6925 let xml = retrieval_method_xpath_signature().replace(
6926 "<holder Id=\"target\">",
6927 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
6928 );
6929
6930 let error = VerifyContext::new()
6931 .policy(policy)
6932 .verify(&xml)
6933 .expect_err("RetrievalMethod XPath contexts must obey the evaluation limit");
6934
6935 assert!(matches!(
6936 error,
6937 SignatureVerificationPipelineError::Policy(
6938 crate::policy::PolicyViolation::ResourceLimit {
6939 resource: crate::policy::resource_name::XPATH_CONTEXT_EVALUATIONS,
6940 maximum: 4,
6941 actual,
6942 }
6943 ) if actual > 4
6944 ));
6945 }
6946
6947 #[test]
6948 fn retrieval_method_materializes_direct_untransformed_x509_data() {
6949 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6952 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
6953 <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
6954 </root>"##;
6955 let document = Document::parse(xml).unwrap();
6956 let key_info_node = document
6957 .descendants()
6958 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
6959 .unwrap();
6960 let mut key_info = parse_key_info(key_info_node).unwrap();
6961
6962 materialize_retrieval_methods(
6963 &mut key_info,
6964 &UriReferenceResolver::new(&document),
6965 UriTypeSet::SAME_DOCUMENT,
6966 None,
6967 crate::provider::default_provider(),
6968 )
6969 .expect("a direct X509Data target needs no transform");
6970 assert!(matches!(
6971 key_info.sources.as_slice(),
6972 [super::super::parse::KeyInfoSource::X509Data(info)]
6973 if info.subject_names == ["CN=leaf"]
6974 ));
6975 }
6976
6977 #[test]
6978 fn retrieval_method_respects_configured_same_document_id_semantics() {
6979 fn materialize(
6982 uri: &str,
6983 id: &str,
6984 semantics: crate::policy::SameDocumentIdSemantics,
6985 ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
6986 let xml = format!(
6987 r#"<root xmlns:ds="{XMLDSIG_NS}">
6988 <ds:KeyInfo><ds:RetrievalMethod URI="{uri}" Type="{XMLDSIG_NS}X509Data"/></ds:KeyInfo>
6989 <ds:X509Data Id="{id}"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
6990 </root>"#
6991 );
6992 let document = Document::parse(&xml).unwrap();
6993 let key_info_node = document
6994 .descendants()
6995 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
6996 .unwrap();
6997 let mut key_info = parse_key_info(key_info_node).unwrap();
6998 let resolver =
6999 UriReferenceResolver::new(&document).with_same_document_id_semantics(semantics);
7000
7001 materialize_retrieval_methods(
7002 &mut key_info,
7003 &resolver,
7004 UriTypeSet::SAME_DOCUMENT,
7005 None,
7006 crate::provider::default_provider(),
7007 )
7008 }
7009
7010 assert!(
7011 materialize(
7012 "#12345",
7013 "12345",
7014 crate::policy::SameDocumentIdSemantics::Specification,
7015 )
7016 .is_err(),
7017 "the standards mode must reject a non-NCName bare fragment"
7018 );
7019 assert!(
7020 materialize(
7021 "#visa'3d",
7022 "visa'3d",
7023 crate::policy::SameDocumentIdSemantics::XmlSecBarename,
7024 )
7025 .is_err(),
7026 "the donor barename wrapper cannot represent an apostrophe"
7027 );
7028 assert!(
7029 materialize(
7030 "#visa'3d",
7031 "visa'3d",
7032 crate::policy::SameDocumentIdSemantics::XmlSecVisa3d,
7033 )
7034 .is_ok(),
7035 "Visa3D mode resolves the registered ID without an XPointer literal"
7036 );
7037 }
7038
7039 #[test]
7040 fn raw_x509_retrieval_method_uses_inherited_xml_base() {
7041 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7044 let xml = format!(
7045 r#"<root xml:base="https://example.test/keys/nested/" xmlns:ds="{XMLDSIG_NS}">
7046 <ds:KeyInfo><ds:RetrievalMethod URI="../signer.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>
7047 </root>"#
7048 );
7049 let document = Document::parse(&xml).unwrap();
7050 let key_info_node = document
7051 .descendants()
7052 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
7053 .unwrap();
7054 let mut key_info = parse_key_info(key_info_node).unwrap();
7055 let certificate = include_bytes!(
7056 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
7057 )
7058 .to_vec();
7059 let resources = HashMap::from([(
7060 "https://example.test/keys/signer.der".to_string(),
7061 certificate,
7062 )]);
7063 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7064
7065 materialize_retrieval_methods(
7066 &mut key_info,
7067 &resolver,
7068 UriTypeSet::ALL,
7069 None,
7070 crate::provider::default_provider(),
7071 )
7072 .expect("RetrievalMethod should resolve against inherited xml:base");
7073
7074 assert!(matches!(
7075 key_info.sources.as_slice(),
7076 [super::super::parse::KeyInfoSource::X509Data(info)]
7077 if info.certificates.len() == 1
7078 ));
7079 }
7080
7081 #[test]
7082 fn retrieval_method_requires_xpath_for_x509_data_below_uri_root() {
7083 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7086 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
7087 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
7088 </root>"##;
7089 let document = Document::parse(xml).unwrap();
7090 let key_info_node = document
7091 .descendants()
7092 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
7093 .unwrap();
7094 let mut key_info = parse_key_info(key_info_node).unwrap();
7095
7096 let error = materialize_retrieval_methods(
7097 &mut key_info,
7098 &UriReferenceResolver::new(&document),
7099 UriTypeSet::SAME_DOCUMENT,
7100 None,
7101 crate::provider::default_provider(),
7102 )
7103 .expect_err("a wrapper target requires an explicit selection transform");
7104 assert!(matches!(
7105 error,
7106 SignatureVerificationPipelineError::InvalidStructure {
7107 reason: "untransformed X509Data RetrievalMethod must target X509Data directly"
7108 }
7109 ));
7110 }
7111
7112 #[test]
7113 fn retrieval_method_rejects_target_inside_external_x509_data_ancestor() {
7114 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7117 <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>
7118 <ds:X509Data><ds:X509SubjectName Id="target">CN=leaf</ds:X509SubjectName></ds:X509Data>
7119 </root>"##;
7120 let document = Document::parse(xml).unwrap();
7121 let key_info_node = document
7122 .descendants()
7123 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
7124 .unwrap();
7125 let mut key_info = parse_key_info(key_info_node).unwrap();
7126
7127 let error = materialize_retrieval_methods(
7128 &mut key_info,
7129 &UriReferenceResolver::new(&document),
7130 UriTypeSet::SAME_DOCUMENT,
7131 None,
7132 crate::provider::default_provider(),
7133 )
7134 .expect_err("filter output without an X509Data root must be rejected");
7135 assert!(matches!(
7136 error,
7137 SignatureVerificationPipelineError::InvalidStructure {
7138 reason: "X509Data RetrievalMethod selected no X509Data element"
7139 }
7140 ));
7141 }
7142
7143 #[test]
7144 fn retrieval_method_rejects_ambiguous_x509_data_relation() {
7145 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7147 <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>
7148 <holder Id="target"><ds:X509Data/><ds:X509Data/></holder>
7149 </root>"##;
7150 let document = Document::parse(xml).unwrap();
7151 let key_info_node = document
7152 .descendants()
7153 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
7154 .unwrap();
7155 let mut key_info = parse_key_info(key_info_node).unwrap();
7156
7157 let error = materialize_retrieval_methods(
7158 &mut key_info,
7159 &UriReferenceResolver::new(&document),
7160 UriTypeSet::SAME_DOCUMENT,
7161 None,
7162 crate::provider::default_provider(),
7163 )
7164 .expect_err("multiple transformed X509Data roots must be rejected");
7165 assert!(matches!(
7166 error,
7167 SignatureVerificationPipelineError::InvalidStructure {
7168 reason: "X509Data RetrievalMethod selected multiple X509Data elements"
7169 }
7170 ));
7171 }
7172
7173 #[test]
7174 fn retrieval_method_materialization_preserves_key_info_order() {
7175 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7178 <ds:KeyInfo>
7179 <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>
7180 <ds:KeyName>fallback</ds:KeyName>
7181 </ds:KeyInfo>
7182 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
7183 </root>"##;
7184 let document = Document::parse(xml).unwrap();
7185 let key_info_node = document
7186 .descendants()
7187 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
7188 .unwrap();
7189 let mut key_info = parse_key_info(key_info_node).unwrap();
7190
7191 materialize_retrieval_methods(
7192 &mut key_info,
7193 &UriReferenceResolver::new(&document),
7194 UriTypeSet::SAME_DOCUMENT,
7195 None,
7196 crate::provider::default_provider(),
7197 )
7198 .unwrap();
7199 assert!(matches!(
7200 key_info.sources.as_slice(),
7201 [
7202 super::super::parse::KeyInfoSource::X509Data(_),
7203 super::super::parse::KeyInfoSource::KeyName(name)
7204 ] if name == "fallback"
7205 ));
7206 }
7207
7208 #[test]
7209 fn retrieval_method_materialization_bounds_repeated_sources() {
7210 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7213 let certificate = include_bytes!(
7214 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
7215 )
7216 .to_vec();
7217 let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
7218 let mut key_info = KeyInfo {
7219 sources: (0..=64)
7220 .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
7221 uri: "urn:certificate".into(),
7222 resource_type: Some(RAW_X509_TYPE.into()),
7223 transforms: RetrievalMethodTransforms::None,
7224 })
7225 .collect(),
7226 };
7227 let document = Document::parse("<root/>").unwrap();
7228 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7229
7230 let error = materialize_retrieval_methods(
7231 &mut key_info,
7232 &resolver,
7233 UriTypeSet::ALL,
7234 None,
7235 crate::provider::default_provider(),
7236 )
7237 .expect_err("retrieval count must be bounded before materialization");
7238 assert!(matches!(
7239 error,
7240 SignatureVerificationPipelineError::InvalidStructure {
7241 reason: "KeyInfo contains too many RetrievalMethod elements"
7242 }
7243 ));
7244 }
7245
7246 #[test]
7247 fn key_info_reference_materializes_an_allowed_empty_uri() {
7248 let document = XmlDocument::parse(format!(
7252 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>root-key</ds:KeyName></ds:KeyInfo>"#
7253 ))
7254 .unwrap();
7255 let mut key_info = KeyInfo {
7256 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7257 uri: String::new(),
7258 }],
7259 };
7260 let mut policy = crate::policy::VerificationPolicy::default();
7261 policy.key_sources.key_info_reference = true;
7262 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7263 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
7264 let mut budgets = RetrievalMaterializationBudgets {
7265 xpath_parse: &mut xpath_parse_budget,
7266 execution: &execution_budget,
7267 resources: &policy.resources,
7268 certificate_signature_algorithms: policy
7269 .key_trust
7270 .certificate_signature_algorithms
7271 .as_ref(),
7272 xml_backend: crate::XmlBackend::default(),
7273 };
7274 let mut materialization = KeyInfoMaterializationState::default();
7275
7276 document
7277 .with_view(|view| {
7278 materialize_key_info_references_with_budgets(
7279 &mut key_info,
7280 &UriReferenceResolver::with_document_view(view, &[]),
7281 &policy,
7282 crate::provider::default_provider(),
7283 &mut budgets,
7284 &mut materialization,
7285 )?;
7286 Ok::<_, SignatureVerificationPipelineError>(())
7287 })
7288 .unwrap();
7289
7290 assert!(matches!(
7291 key_info.sources.as_slice(),
7292 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "root-key"
7293 ));
7294 }
7295
7296 #[test]
7297 fn empty_key_info_reference_participates_in_cycle_detection() {
7298 let document = XmlDocument::parse(format!(
7302 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#"><dsig11:KeyInfoReference URI=""/></ds:KeyInfo>"#
7303 ))
7304 .unwrap();
7305 let mut key_info = KeyInfo {
7306 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7307 uri: String::new(),
7308 }],
7309 };
7310 let mut policy = crate::policy::VerificationPolicy::default();
7311 policy.key_sources.key_info_reference = true;
7312 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7313 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
7314 let mut budgets = RetrievalMaterializationBudgets {
7315 xpath_parse: &mut xpath_parse_budget,
7316 execution: &execution_budget,
7317 resources: &policy.resources,
7318 certificate_signature_algorithms: policy
7319 .key_trust
7320 .certificate_signature_algorithms
7321 .as_ref(),
7322 xml_backend: crate::XmlBackend::default(),
7323 };
7324 let mut materialization = KeyInfoMaterializationState::default();
7325
7326 let error = document
7327 .with_view(|view| {
7328 materialize_key_info_references_with_budgets(
7329 &mut key_info,
7330 &UriReferenceResolver::with_document_view(view, &[]),
7331 &policy,
7332 crate::provider::default_provider(),
7333 &mut budgets,
7334 &mut materialization,
7335 )
7336 })
7337 .expect_err("empty-URI self-reference must be rejected as a cycle");
7338
7339 assert!(matches!(
7340 error,
7341 SignatureVerificationPipelineError::InvalidStructure {
7342 reason: "KeyInfoReference cycle detected"
7343 }
7344 ));
7345 }
7346
7347 #[test]
7348 fn key_info_materialization_shares_candidate_work_across_source_kinds() {
7349 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7352 let document = XmlDocument::parse(format!(
7353 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>"##
7354 ))
7355 .unwrap();
7356 let certificate = include_bytes!(
7357 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
7358 )
7359 .to_vec();
7360 let resources = HashMap::from([("signer.der".to_owned(), certificate)]);
7361 let mut key_info = KeyInfo {
7362 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7363 uri: "#target".into(),
7364 }],
7365 };
7366 let mut policy = crate::policy::VerificationPolicy::default();
7367 policy.key_sources.key_info_reference = true;
7368 policy.uris.retrieval_methods = UriTypeSet::ALL;
7369 policy.resources.max_key_candidates = 2;
7370 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7371 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
7372 let mut budgets = RetrievalMaterializationBudgets {
7373 xpath_parse: &mut xpath_parse_budget,
7374 execution: &execution_budget,
7375 resources: &policy.resources,
7376 certificate_signature_algorithms: policy
7377 .key_trust
7378 .certificate_signature_algorithms
7379 .as_ref(),
7380 xml_backend: crate::XmlBackend::default(),
7381 };
7382 let mut materialization = KeyInfoMaterializationState::default();
7383
7384 let error = document
7385 .with_view(|view| {
7386 let resolver = UriReferenceResolver::with_document_view(view, &[])
7387 .with_external_resources(&resources);
7388 let mut outcome = materialize_key_info_references_with_budgets(
7389 &mut key_info,
7390 &resolver,
7391 &policy,
7392 crate::provider::default_provider(),
7393 &mut budgets,
7394 &mut materialization,
7395 )?;
7396 outcome.merge(materialize_retrieval_methods_with_budgets(
7397 &mut key_info,
7398 &resolver,
7399 &policy,
7400 crate::provider::default_provider(),
7401 &mut budgets,
7402 &mut materialization.candidate_work,
7403 )?);
7404 Ok::<_, DsigError>(outcome)
7405 })
7406 .expect_err("all materializers must share the candidate-work limit");
7407
7408 assert!(matches!(
7409 error,
7410 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
7411 resource: crate::policy::resource_name::KEY_CANDIDATES,
7412 maximum: 2,
7413 actual: 3,
7414 })
7415 ));
7416 }
7417
7418 #[test]
7419 fn public_key_info_materialization_binds_external_resource_policy() {
7420 let document = Document::parse("<root/>").unwrap();
7423 let external = format!(
7424 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
7425 );
7426 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
7427 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7428 let mut key_info = KeyInfo {
7429 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7430 uri: "key.xml".into(),
7431 }],
7432 };
7433 let mut policy = crate::policy::VerificationPolicy::default();
7434 policy.key_sources.key_info_reference = true;
7435 policy.uris.key_info_references = UriTypeSet::ALL;
7436 policy.resources.max_external_resource_bytes = 0;
7437
7438 let error = materialize_verification_key_info_references(
7439 &mut key_info,
7440 resolver,
7441 &policy,
7442 crate::provider::default_provider(),
7443 crate::XmlBackend::default(),
7444 )
7445 .expect_err("policy must reject non-empty external KeyInfo bytes");
7446
7447 assert!(matches!(
7448 error,
7449 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
7450 resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
7451 maximum: 0,
7452 actual,
7453 }) if actual == resources["key.xml"].len()
7454 ));
7455 }
7456
7457 #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
7458 #[test]
7459 fn public_key_info_materialization_uses_the_selected_backend() {
7460 let external = format!(
7463 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
7464 );
7465 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
7466 let document = Document::parse("<root/>").unwrap();
7467
7468 for backend in [crate::XmlBackend::Xmloxide, crate::XmlBackend::Roxmltree] {
7469 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7470 let mut key_info = KeyInfo {
7471 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7472 uri: "key.xml".into(),
7473 }],
7474 };
7475 let mut policy = crate::policy::VerificationPolicy::default();
7476 policy.key_sources.key_info_reference = true;
7477 policy.uris.key_info_references = UriTypeSet::ALL;
7478
7479 materialize_verification_key_info_references(
7480 &mut key_info,
7481 resolver,
7482 &policy,
7483 crate::provider::default_provider(),
7484 backend,
7485 )
7486 .unwrap_or_else(|error| panic!("{backend:?} materialization failed: {error}"));
7487
7488 assert!(matches!(
7489 key_info.sources.as_slice(),
7490 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "external"
7491 ));
7492 }
7493 }
7494
7495 #[test]
7496 fn public_key_info_materialization_defers_nested_retrieval_failure() {
7497 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7501 let external = format!(
7502 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>usable</ds:KeyName><ds:RetrievalMethod URI="missing.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>"#
7503 );
7504 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
7505 let document = Document::parse("<root/>").unwrap();
7506
7507 for operation in ["signing", "verification"] {
7508 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7509 let mut key_info = KeyInfo {
7510 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7511 uri: "key.xml".into(),
7512 }],
7513 };
7514 match operation {
7515 "signing" => {
7516 let mut policy = crate::policy::SigningPolicy::default();
7517 policy.uris.key_info_references = UriTypeSet::ALL;
7518 policy.uris.retrieval_methods = UriTypeSet::ALL;
7519 materialize_signing_key_info_references(
7520 &mut key_info,
7521 resolver,
7522 &policy,
7523 crate::provider::default_provider(),
7524 crate::XmlBackend::default(),
7525 )
7526 }
7527 "verification" => {
7528 let mut policy = crate::policy::VerificationPolicy::default();
7529 policy.key_sources.key_info_reference = true;
7530 policy.uris.key_info_references = UriTypeSet::ALL;
7531 policy.uris.retrieval_methods = UriTypeSet::ALL;
7532 materialize_verification_key_info_references(
7533 &mut key_info,
7534 resolver,
7535 &policy,
7536 crate::provider::default_provider(),
7537 crate::XmlBackend::default(),
7538 )
7539 }
7540 _ => unreachable!(),
7541 }
7542 .unwrap_or_else(|error| panic!("{operation} materialization failed: {error}"));
7543
7544 assert!(matches!(
7545 key_info.sources.as_slice(),
7546 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "usable"
7547 ));
7548 }
7549 }
7550
7551 #[test]
7552 fn public_key_info_materialization_binds_xpath_policy() {
7553 let external = format!(
7556 r##"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}">
7557 <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
7558 <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
7559 <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
7560 </ds:Transform></ds:Transforms>
7561 </ds:RetrievalMethod>
7562 <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
7563 </ds:KeyInfo>"##
7564 );
7565 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
7566 let document = Document::parse("<root/>").unwrap();
7567
7568 for operation in ["signing", "verification"] {
7569 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7570 let mut key_info = KeyInfo {
7571 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7572 uri: "key.xml".into(),
7573 }],
7574 };
7575 let error = match operation {
7576 "signing" => {
7577 let mut policy = crate::policy::SigningPolicy::default();
7578 policy.uris.key_info_references = UriTypeSet::ALL;
7579 policy.resources.max_xpath_expressions = 0;
7580 materialize_signing_key_info_references(
7581 &mut key_info,
7582 resolver,
7583 &policy,
7584 crate::provider::default_provider(),
7585 crate::XmlBackend::default(),
7586 )
7587 }
7588 "verification" => {
7589 let mut policy = crate::policy::VerificationPolicy::default();
7590 policy.key_sources.key_info_reference = true;
7591 policy.uris.key_info_references = UriTypeSet::ALL;
7592 policy.resources.max_xpath_expressions = 0;
7593 materialize_verification_key_info_references(
7594 &mut key_info,
7595 resolver,
7596 &policy,
7597 crate::provider::default_provider(),
7598 crate::XmlBackend::default(),
7599 )
7600 }
7601 _ => unreachable!(),
7602 }
7603 .expect_err("the public helper must enforce the supplied XPath expression limit");
7604
7605 assert!(
7606 matches!(
7607 error,
7608 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
7609 resource: crate::policy::resource_name::XPATH_EXPRESSIONS,
7610 maximum: 0,
7611 actual: 1,
7612 })
7613 ),
7614 "unexpected {operation} error: {error:?}"
7615 );
7616 }
7617 }
7618
7619 fn materialize_external_key_info_chain(
7620 terminal_reference: &str,
7621 ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
7622 let a = format!(
7623 r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
7624 <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
7625 <ds:KeyInfo ID="next"><dsig11:KeyInfoReference URI="b.xml#root"/></ds:KeyInfo>
7626 </doc>"##
7627 );
7628 let b = format!(
7629 r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
7630 <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
7631 <ds:KeyInfo ID="next">{terminal_reference}</ds:KeyInfo>
7632 </doc>"##
7633 );
7634 let resources = HashMap::from([
7635 ("a.xml".to_owned(), a.into_bytes()),
7636 ("b.xml".to_owned(), b.into_bytes()),
7637 ]);
7638 let document = XmlDocument::parse("<root/>").unwrap();
7639 let mut key_info = KeyInfo {
7640 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7641 uri: "a.xml#root".into(),
7642 }],
7643 };
7644 let mut policy = crate::policy::VerificationPolicy::default();
7645 policy.key_sources.key_info_reference = true;
7646 policy.uris.key_info_references = UriTypeSet::ALL;
7647 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7648 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
7649 let mut budgets = RetrievalMaterializationBudgets {
7650 xpath_parse: &mut xpath_parse_budget,
7651 execution: &execution_budget,
7652 resources: &policy.resources,
7653 certificate_signature_algorithms: policy
7654 .key_trust
7655 .certificate_signature_algorithms
7656 .as_ref(),
7657 xml_backend: crate::XmlBackend::default(),
7658 };
7659 let mut materialization = KeyInfoMaterializationState::default();
7660
7661 document.with_view(|view| {
7662 let resolver = UriReferenceResolver::with_document_view(view, &[])
7663 .with_external_resources(&resources);
7664 materialize_key_info_references_with_budgets(
7665 &mut key_info,
7666 &resolver,
7667 &policy,
7668 crate::provider::default_provider(),
7669 &mut budgets,
7670 &mut materialization,
7671 )?;
7672 Ok::<_, SignatureVerificationPipelineError>(())
7673 })?;
7674 Ok(key_info)
7675 }
7676
7677 fn materialize_external_key_info_bytes_with_limit(
7678 encoded: Vec<u8>,
7679 maximum_document_bytes: usize,
7680 ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
7681 let resources = HashMap::from([("key.xml".to_owned(), encoded)]);
7682 let document = XmlDocument::parse("<root/>").unwrap();
7683 let mut key_info = KeyInfo {
7684 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7685 uri: "key.xml".into(),
7686 }],
7687 };
7688 let mut policy = crate::policy::VerificationPolicy::default();
7689 policy.key_sources.key_info_reference = true;
7690 policy.uris.key_info_references = UriTypeSet::ALL;
7691 policy.resources.max_xml_document_bytes = maximum_document_bytes;
7692 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7693 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
7694 let mut budgets = RetrievalMaterializationBudgets {
7695 xpath_parse: &mut xpath_parse_budget,
7696 execution: &execution_budget,
7697 resources: &policy.resources,
7698 certificate_signature_algorithms: policy
7699 .key_trust
7700 .certificate_signature_algorithms
7701 .as_ref(),
7702 xml_backend: crate::XmlBackend::default(),
7703 };
7704 let mut materialization = KeyInfoMaterializationState::default();
7705
7706 document.with_view(|view| {
7707 let resolver = UriReferenceResolver::with_document_view(view, &[])
7708 .with_external_resources(&resources);
7709 materialize_key_info_references_with_budgets(
7710 &mut key_info,
7711 &resolver,
7712 &policy,
7713 crate::provider::default_provider(),
7714 &mut budgets,
7715 &mut materialization,
7716 )?;
7717 Ok::<_, SignatureVerificationPipelineError>(())
7718 })?;
7719 Ok(key_info)
7720 }
7721
7722 fn materialize_external_key_info_bytes(
7723 encoded: Vec<u8>,
7724 ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
7725 materialize_external_key_info_bytes_with_limit(
7726 encoded,
7727 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
7728 )
7729 }
7730
7731 #[test]
7732 fn external_key_info_reference_decodes_utf16_xml_octets() {
7733 let xml = format!(
7736 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>utf16-key</ds:KeyName></ds:KeyInfo>"#
7737 );
7738 let mut encoded = vec![0xff, 0xfe];
7739 encoded.extend(xml.encode_utf16().flat_map(u16::to_le_bytes));
7740 let key_info = materialize_external_key_info_bytes(encoded)
7741 .expect("UTF-16 external KeyInfo must materialize");
7742 assert!(matches!(
7743 key_info.sources.as_slice(),
7744 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "utf16-key"
7745 ));
7746 }
7747
7748 #[test]
7749 fn external_key_info_reference_rejects_malformed_xml_encoding() {
7750 let error = materialize_external_key_info_bytes(vec![0xff, 0xfe, b'<'])
7753 .expect_err("truncated UTF-16 must be rejected during octet decoding");
7754
7755 assert!(matches!(
7756 error,
7757 SignatureVerificationPipelineError::InvalidStructure {
7758 reason: "KeyInfoReference external resource has an invalid XML encoding"
7759 }
7760 ));
7761 }
7762
7763 #[test]
7764 fn external_key_info_reference_preserves_decoded_document_limit() {
7765 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();
7768 let maximum = encoded.len();
7769 let error = materialize_external_key_info_bytes_with_limit(encoded, maximum)
7770 .expect_err("decoded XML expansion must retain resource-limit classification");
7771
7772 assert!(matches!(
7773 error,
7774 SignatureVerificationPipelineError::Policy(
7775 crate::policy::PolicyViolation::ResourceLimit {
7776 resource: crate::policy::resource_name::XML_DOCUMENT,
7777 maximum: observed_maximum,
7778 actual,
7779 }
7780 ) if observed_maximum == maximum && actual > maximum
7781 ));
7782 }
7783
7784 #[test]
7785 fn key_info_reference_cycle_identity_includes_the_owning_resource() {
7786 let key_info = materialize_external_key_info_chain("<ds:KeyName>terminal</ds:KeyName>")
7789 .expect("cross-document duplicate fragments must materialize");
7790 assert!(matches!(
7791 key_info.sources.as_slice(),
7792 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "terminal"
7793 ));
7794 }
7795
7796 #[test]
7797 fn key_info_reference_cycle_identity_survives_external_reparse() {
7798 let error =
7801 materialize_external_key_info_chain("<dsig11:KeyInfoReference URI=\"a.xml#root\"/>")
7802 .expect_err("a resource cycle must fail before exhausting depth");
7803 assert!(matches!(
7804 error,
7805 SignatureVerificationPipelineError::InvalidStructure {
7806 reason: "KeyInfoReference cycle detected"
7807 }
7808 ));
7809 }
7810
7811 #[test]
7812 fn retrieval_method_materialization_deduplicates_within_count_limit() {
7813 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7816 let certificate = include_bytes!(
7817 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
7818 )
7819 .to_vec();
7820 let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
7821 let mut key_info = KeyInfo {
7822 sources: (0..MAX_RETRIEVAL_METHOD_COUNT)
7823 .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
7824 uri: "urn:certificate".into(),
7825 resource_type: Some(RAW_X509_TYPE.into()),
7826 transforms: RetrievalMethodTransforms::None,
7827 })
7828 .collect(),
7829 };
7830 let document = Document::parse("<root/>").unwrap();
7831 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7832
7833 materialize_retrieval_methods(
7834 &mut key_info,
7835 &resolver,
7836 UriTypeSet::ALL,
7837 None,
7838 crate::provider::default_provider(),
7839 )
7840 .unwrap();
7841 assert!(matches!(
7842 key_info.sources.as_slice(),
7843 [super::super::parse::KeyInfoSource::X509Data(info)]
7844 if info.certificates.len() == 1
7845 ));
7846 }
7847
7848 #[test]
7849 fn retrieval_method_candidate_budget_includes_embedded_key_values() {
7850 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7853 let resources = HashMap::from([("urn:certificate".to_string(), vec![1, 2, 3])]);
7854 let mut key_info = KeyInfo {
7855 sources: vec![
7856 super::super::parse::KeyInfoSource::KeyValue(
7857 super::super::parse::KeyValueInfo::Unsupported {
7858 namespace: Some(XMLDSIG_NS.into()),
7859 local_name: "FutureKeyValue".into(),
7860 },
7861 ),
7862 super::super::parse::KeyInfoSource::RetrievalMethod {
7863 uri: "urn:certificate".into(),
7864 resource_type: Some(RAW_X509_TYPE.into()),
7865 transforms: RetrievalMethodTransforms::None,
7866 },
7867 ],
7868 };
7869 let document = Document::parse("<root/>").unwrap();
7870 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7871 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7872 let execution_budget = TransformExecutionBudget::default();
7873 let resource_policy = crate::policy::ResourcePolicy {
7874 max_key_candidates: 1,
7875 ..crate::policy::ResourcePolicy::default()
7876 };
7877 let mut budgets = RetrievalMaterializationBudgets {
7878 xpath_parse: &mut xpath_parse_budget,
7879 execution: &execution_budget,
7880 resources: &resource_policy,
7881 certificate_signature_algorithms: None,
7882 xml_backend: crate::XmlBackend::default(),
7883 };
7884 let mut candidate_work = key_info.embedded_candidate_count();
7885
7886 let mut policy = crate::policy::VerificationPolicy::default();
7887 policy.uris.retrieval_methods = UriTypeSet::ALL;
7888 let error = materialize_retrieval_methods_with_budgets(
7889 &mut key_info,
7890 &resolver,
7891 &policy,
7892 crate::provider::default_provider(),
7893 &mut budgets,
7894 &mut candidate_work,
7895 )
7896 .expect_err("the retrieved certificate must exceed the aggregate candidate limit");
7897
7898 assert!(matches!(
7899 error,
7900 SignatureVerificationPipelineError::Policy(
7901 crate::policy::PolicyViolation::ResourceLimit {
7902 resource: crate::policy::resource_name::KEY_CANDIDATES,
7903 maximum: 1,
7904 actual: 2,
7905 }
7906 )
7907 ));
7908 }
7909
7910 #[test]
7911 fn raw_x509_retrieval_rejects_empty_same_document_uri() {
7912 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7915 let certificate = include_bytes!(
7916 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
7917 )
7918 .to_vec();
7919 let resources = HashMap::from([(String::new(), certificate)]);
7920 let mut key_info = KeyInfo {
7921 sources: vec![super::super::parse::KeyInfoSource::RetrievalMethod {
7922 uri: String::new(),
7923 resource_type: Some(RAW_X509_TYPE.into()),
7924 transforms: RetrievalMethodTransforms::None,
7925 }],
7926 };
7927 let document = Document::parse("<root/>").unwrap();
7928 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7929
7930 let error = materialize_retrieval_methods(
7931 &mut key_info,
7932 &resolver,
7933 UriTypeSet::ALL,
7934 None,
7935 crate::provider::default_provider(),
7936 )
7937 .expect_err("empty URI must retain same-document semantics");
7938 assert!(matches!(
7939 error,
7940 SignatureVerificationPipelineError::InvalidStructure {
7941 reason: "raw X509 RetrievalMethod requires an untransformed external URI"
7942 }
7943 ));
7944 }
7945
7946 #[test]
7947 fn verify_context_does_not_hide_malformed_digest_behind_unsupported_transform() {
7948 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7950 let xml = xml.replacen(
7951 "<ds:Reference URI=\"#target\">",
7952 "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
7953 1,
7954 );
7955 replace_fixture_manifest_digest(&xml, "!!!")
7956 });
7957
7958 let error = VerifyContext::new()
7959 .key(&AcceptingKey)
7960 .process_manifests(true)
7961 .verify(&broken_xml)
7962 .expect_err("malformed Manifest digest must not become a validity result");
7963 assert!(matches!(
7964 error,
7965 SignatureVerificationPipelineError::ParseManifestReference(_)
7966 ));
7967 }
7968
7969 #[test]
7970 fn verify_context_rejects_manifest_non_whitespace_mixed_content() {
7971 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7974 xml.replacen(
7975 "<ds:Manifest ID=\"manifest\">",
7976 "<ds:Manifest ID=\"manifest\">junk",
7977 1,
7978 )
7979 });
7980
7981 let err = VerifyContext::new()
7982 .key(&AcceptingKey)
7983 .process_manifests(true)
7984 .verify(&xml)
7985 .expect_err("Manifest mixed content must fail verification");
7986 assert!(matches!(
7987 err,
7988 SignatureVerificationPipelineError::InvalidStructure {
7989 reason: "Manifest contains non-whitespace mixed content"
7990 }
7991 ));
7992 }
7993
7994 #[test]
7995 fn verify_context_rejects_empty_manifest_children() {
7996 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7999 let (prefix, rest) = xml
8000 .split_once("<ds:Manifest ID=\"manifest\">")
8001 .expect("fixture should contain Manifest");
8002 let (_, suffix) = rest
8003 .split_once("</ds:Manifest>")
8004 .expect("fixture should contain closing Manifest");
8005 format!("{prefix}<ds:Manifest ID=\"manifest\"></ds:Manifest>{suffix}")
8006 });
8007
8008 let err = VerifyContext::new()
8009 .key(&AcceptingKey)
8010 .process_manifests(true)
8011 .verify(&xml)
8012 .expect_err("empty Manifest must fail verification");
8013 assert!(matches!(
8014 err,
8015 SignatureVerificationPipelineError::InvalidStructure {
8016 reason: "Manifest must contain at least one ds:Reference element child"
8017 }
8018 ));
8019 }
8020
8021 #[test]
8022 fn verify_context_ignores_unsigned_malformed_manifest_blocks() {
8023 let xml = signature_with_manifest_xml(true).replacen(
8024 "</ds:Object>",
8025 "</ds:Object><ds:Object><ds:Manifest>junk<ds:Foo/></ds:Manifest></ds:Object>",
8026 1,
8027 );
8028 let result = VerifyContext::new()
8029 .key(&AcceptingKey)
8030 .process_manifests(true)
8031 .verify(&xml)
8032 .expect("unsigned malformed Manifest must be ignored");
8033 assert_eq!(
8034 result.manifest_references.len(),
8035 1,
8036 "only signed Manifest references must be reported",
8037 );
8038 assert!(matches!(result.status, DsigStatus::Valid));
8039 }
8040
8041 #[test]
8042 fn verify_context_skips_ambiguous_manifest_id_blocks() {
8043 let xml = signature_with_manifest_xml(true).replacen(
8044 "</ds:Object>",
8045 "</ds:Object><ds:Object><ds:Manifest ID=\"manifest\">junk<ds:Foo/></ds:Manifest></ds:Object>",
8046 1,
8047 );
8048 let err = VerifyContext::new()
8049 .key(&RejectingKey)
8050 .process_manifests(true)
8051 .verify(&xml)
8052 .expect_err("ambiguous manifest IDs should make SignedInfo #manifest dereference fail");
8053 assert!(matches!(
8054 err,
8055 SignatureVerificationPipelineError::Reference(
8056 ReferenceProcessingError::UriDereference(
8057 crate::xmldsig::types::TransformError::ElementNotFound(id)
8058 )
8059 ) if id == "manifest"
8060 ));
8061 }
8062
8063 #[test]
8064 fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
8065 let xml = minimal_signature_xml("", "");
8066 let err = VerifyContext::new()
8067 .key(&RejectingKey)
8068 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
8069 .verify(&xml)
8070 .expect_err("implicit default C14N must be checked against allowlist");
8071 assert!(matches!(
8072 err,
8073 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
8074 operation: "verification transform",
8075 ..
8076 })
8077 ));
8078 }
8079
8080 #[test]
8081 fn verify_context_skips_resolver_when_reference_processing_fails() {
8082 let xml = minimal_signature_xml("", "");
8083 let result = VerifyContext::new()
8084 .key_resolver(&PanicResolver)
8085 .verify(&xml)
8086 .expect("reference digest mismatch should short-circuit before resolver");
8087 assert!(matches!(
8088 result.status,
8089 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
8090 ));
8091 }
8092
8093 #[test]
8094 fn verify_context_leaves_fail_fast_reference_tail_unexecuted() {
8095 let second_reference = r#"
8098 <ds:Reference URI="">
8099 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
8100 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
8101 </ds:Reference>"#;
8102 let xml = minimal_signature_xml("", "").replacen(
8103 " </ds:SignedInfo>",
8104 &format!("{second_reference}\n </ds:SignedInfo>"),
8105 1,
8106 );
8107
8108 let result = VerifyContext::new()
8109 .key(&AcceptingKey)
8110 .verify(&xml)
8111 .expect("digest mismatch must remain a verification result");
8112
8113 assert_eq!(result.signed_info_references.len(), 1);
8114 assert!(matches!(
8115 result.signed_info_references[0].status,
8116 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
8117 ));
8118 }
8119
8120 #[test]
8121 fn verify_context_reports_key_not_found_when_resolver_misses() {
8122 let xml = signature_with_target_reference("AQ==");
8123 let result = VerifyContext::new()
8124 .key_resolver(&MissingKeyResolver)
8125 .verify(&xml)
8126 .expect("resolver miss should report status, not pipeline error");
8127 assert!(matches!(
8128 result.status,
8129 DsigStatus::Invalid(FailureReason::KeyNotFound)
8130 ));
8131 assert_eq!(
8132 result.signed_info_references.len(),
8133 1,
8134 "KeyNotFound path must preserve SignedInfo reference diagnostics",
8135 );
8136 assert!(matches!(
8137 result.signed_info_references[0].status,
8138 DsigStatus::Valid
8139 ));
8140 }
8141
8142 #[test]
8143 fn verification_candidate_budget_covers_preset_and_custom_resolver_paths() {
8144 let xml = signature_with_target_reference("AQ==");
8147 let mut policy = crate::policy::VerificationPolicy::default();
8148 policy.resources.max_key_candidates = 0;
8149
8150 let preset_error = VerifyContext::new()
8151 .key(&RejectingKey)
8152 .policy(policy.clone())
8153 .verify(&xml)
8154 .expect_err("a preset key consumes one candidate");
8155 assert!(matches!(
8156 preset_error,
8157 SignatureVerificationPipelineError::Policy(
8158 crate::policy::PolicyViolation::ResourceLimit {
8159 resource: crate::policy::resource_name::KEY_CANDIDATES,
8160 maximum: 0,
8161 actual: 1,
8162 }
8163 )
8164 ));
8165
8166 let resolver_error = VerifyContext::new()
8167 .key_resolver(&PanicResolver)
8168 .policy(policy)
8169 .verify(&xml)
8170 .expect_err("a custom resolver requires candidate capacity before dispatch");
8171 assert!(matches!(
8172 resolver_error,
8173 SignatureVerificationPipelineError::Policy(
8174 crate::policy::PolicyViolation::ResourceLimit {
8175 resource: crate::policy::resource_name::KEY_CANDIDATES,
8176 maximum: 0,
8177 actual: 1,
8178 }
8179 )
8180 ));
8181 }
8182
8183 #[test]
8184 fn verification_candidate_budget_precedes_embedded_x509_parsing() {
8185 let first_certificate = base64::engine::general_purpose::STANDARD.encode(include_bytes!(
8188 "../../tests/fixtures/xmldsig/phaos-xmldsig-three/certs/rsa-cert.der"
8189 ));
8190 let xml = signature_with_target_reference("AQ==").replace(
8191 "</ds:SignatureValue>\n </ds:Signature>",
8192 &format!(
8193 "</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>"
8194 ),
8195 );
8196 let mut policy = crate::policy::VerificationPolicy::default();
8197 policy.resources.max_key_candidates = 1;
8198
8199 let error = VerifyContext::new()
8200 .policy(policy)
8201 .verify(&xml)
8202 .expect_err("candidate policy must run before embedded certificate parsing");
8203
8204 assert!(matches!(
8205 error,
8206 SignatureVerificationPipelineError::Policy(
8207 crate::policy::PolicyViolation::ResourceLimit {
8208 resource: crate::policy::resource_name::KEY_CANDIDATES,
8209 maximum: 1,
8210 actual: 2,
8211 }
8212 )
8213 ));
8214 }
8215
8216 #[test]
8217 fn verify_context_resolver_can_ignore_malformed_keyinfo_by_default() {
8218 let base_xml = signature_with_target_reference("AQ==");
8219 let xml = base_xml
8220 .replace(
8221 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
8222 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
8223 )
8224 .replace(
8225 "</ds:SignatureValue>\n </ds:Signature>",
8226 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
8227 );
8228
8229 let result = VerifyContext::new()
8230 .key_resolver(&MissingKeyResolver)
8231 .verify(&xml)
8232 .expect("resolver path should not hard-fail on advisory malformed KeyInfo by default");
8233 assert!(matches!(
8234 result.status,
8235 DsigStatus::Invalid(FailureReason::KeyNotFound)
8236 ));
8237 }
8238
8239 #[test]
8240 fn verify_context_resolver_can_opt_in_to_keyinfo_parse_failures() {
8241 let base_xml = signature_with_target_reference("AQ==");
8242 let xml = base_xml
8243 .replace(
8244 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
8245 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
8246 )
8247 .replace(
8248 "</ds:SignatureValue>\n </ds:Signature>",
8249 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
8250 );
8251
8252 let err = VerifyContext::new()
8253 .key_resolver(&ConsumingKeyInfoResolver)
8254 .verify(&xml)
8255 .expect_err("resolver opted into KeyInfo parsing, malformed KeyInfo must fail");
8256 assert!(matches!(
8257 err,
8258 SignatureVerificationPipelineError::ParseKeyInfo(_)
8259 ));
8260 }
8261
8262 #[test]
8263 fn verify_context_ignores_unsupported_retrieval_before_valid_key_source() {
8264 let xml = signature_with_target_reference("AQ==").replace(
8267 "</ds:SignatureValue>\n </ds:Signature>",
8268 r##"</ds:SignatureValue>
8269 <ds:KeyInfo>
8270 <ds:RetrievalMethod URI="#vendor" Type="urn:vendor:key">
8271 <ds:Transforms><ds:Transform Algorithm="urn:vendor:transform"/></ds:Transforms>
8272 </ds:RetrievalMethod>
8273 <ds:KeyName>fallback</ds:KeyName>
8274 </ds:KeyInfo>
8275 </ds:Signature>"##,
8276 );
8277
8278 let result = VerifyContext::new()
8279 .key_resolver(&FallbackKeyInfoResolver)
8280 .verify(&xml)
8281 .expect("unsupported advisory retrieval must not abort key resolution");
8282 assert_eq!(result.status, DsigStatus::Valid);
8283 }
8284
8285 #[test]
8286 fn verify_context_does_not_eagerly_fail_unused_retrieval_fallback() {
8287 let xml = signature_with_target_reference("AQ==").replace(
8290 "</ds:SignatureValue>\n </ds:Signature>",
8291 r#"</ds:SignatureValue>
8292 <ds:KeyInfo>
8293 <ds:KeyName>primary</ds:KeyName>
8294 <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
8295 </ds:KeyInfo>
8296 </ds:Signature>"#,
8297 );
8298
8299 let result = VerifyContext::new()
8300 .key_resolver(&EarlyKeyInfoResolver)
8301 .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
8302 .verify(&xml)
8303 .expect("an unused missing retrieval fallback must not abort verification");
8304
8305 assert_eq!(result.status, DsigStatus::Valid);
8306 }
8307
8308 #[test]
8309 fn verify_context_does_not_eagerly_fail_unused_same_document_x509_retrieval() {
8310 let xml = signature_with_target_reference("AQ==").replace(
8313 "</ds:SignatureValue>\n </ds:Signature>",
8314 r##"</ds:SignatureValue>
8315 <ds:KeyInfo>
8316 <ds:KeyName>primary</ds:KeyName>
8317 <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
8318 </ds:KeyInfo>
8319 </ds:Signature>"##,
8320 );
8321
8322 let result = VerifyContext::new()
8323 .key_resolver(&EarlyKeyInfoResolver)
8324 .verify(&xml)
8325 .expect("an unused missing X509Data retrieval fallback must not abort verification");
8326
8327 assert_eq!(result.status, DsigStatus::Valid);
8328 }
8329
8330 #[test]
8331 fn verify_context_reports_missing_same_document_x509_retrieval_without_fallback() {
8332 let xml = signature_with_target_reference("AQ==").replace(
8335 "</ds:SignatureValue>\n </ds:Signature>",
8336 r##"</ds:SignatureValue>
8337 <ds:KeyInfo>
8338 <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
8339 </ds:KeyInfo>
8340 </ds:Signature>"##,
8341 );
8342
8343 let error = VerifyContext::new()
8344 .key_resolver(&ConsumingKeyInfoResolver)
8345 .verify(&xml)
8346 .expect_err("a sole missing X509Data retrieval must remain an explicit error");
8347
8348 assert!(matches!(
8349 error,
8350 SignatureVerificationPipelineError::InvalidStructure {
8351 reason: "X509Data RetrievalMethod target is missing or ambiguous"
8352 }
8353 ));
8354 }
8355
8356 #[test]
8357 fn verify_context_does_not_eagerly_parse_unused_retrieval_fallback() {
8358 let xml = signature_with_target_reference("AQ==").replace(
8361 "</ds:SignatureValue>\n </ds:Signature>",
8362 r#"</ds:SignatureValue>
8363 <ds:KeyInfo>
8364 <ds:KeyName>primary</ds:KeyName>
8365 <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
8366 </ds:KeyInfo>
8367 </ds:Signature>"#,
8368 );
8369 let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
8370
8371 let result = VerifyContext::new()
8372 .key_resolver(&EarlyKeyInfoResolver)
8373 .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
8374 .external_resources(&resources)
8375 .verify(&xml)
8376 .expect("an unused malformed retrieval fallback must not abort verification");
8377
8378 assert_eq!(result.status, DsigStatus::Valid);
8379 }
8380
8381 #[test]
8382 fn verify_context_reports_missing_retrieval_when_no_key_source_resolves() {
8383 let xml = signature_with_target_reference("AQ==").replace(
8386 "</ds:SignatureValue>\n </ds:Signature>",
8387 r#"</ds:SignatureValue>
8388 <ds:KeyInfo>
8389 <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
8390 </ds:KeyInfo>
8391 </ds:Signature>"#,
8392 );
8393
8394 let error = VerifyContext::new()
8395 .key_resolver(&ConsumingKeyInfoResolver)
8396 .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
8397 .verify(&xml)
8398 .expect_err("a missing sole RetrievalMethod must remain an explicit error");
8399
8400 assert!(matches!(
8401 error,
8402 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
8403 crate::xmldsig::TransformError::UnsupportedUri(uri)
8404 )) if uri == "missing.der"
8405 ));
8406 }
8407
8408 #[test]
8409 fn verify_context_reports_malformed_retrieval_when_no_key_source_resolves() {
8410 let xml = signature_with_target_reference("AQ==").replace(
8413 "</ds:SignatureValue>\n </ds:Signature>",
8414 r#"</ds:SignatureValue>
8415 <ds:KeyInfo>
8416 <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
8417 </ds:KeyInfo>
8418 </ds:Signature>"#,
8419 );
8420 let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
8421
8422 let error = VerifyContext::new()
8423 .key_resolver(&ConsumingKeyInfoResolver)
8424 .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
8425 .external_resources(&resources)
8426 .verify(&xml)
8427 .expect_err("a malformed sole RetrievalMethod must remain a parse error");
8428
8429 assert!(matches!(
8430 error,
8431 SignatureVerificationPipelineError::ParseKeyInfo(_)
8432 ));
8433 }
8434
8435 #[test]
8436 fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
8437 let xml = signature_with_target_reference("@@@");
8438
8439 let err = VerifyContext::new()
8440 .key_resolver(&MissingKeyResolver)
8441 .verify(&xml)
8442 .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
8443 assert!(matches!(
8444 err,
8445 SignatureVerificationPipelineError::SignatureValueBase64(_)
8446 ));
8447 }
8448
8449 #[test]
8450 fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
8451 let xml = signature_with_target_reference("@@@");
8452
8453 let err = VerifyContext::new()
8454 .verify(&xml)
8455 .expect_err("invalid SignatureValue must remain a decode error");
8456 assert!(matches!(
8457 err,
8458 SignatureVerificationPipelineError::SignatureValueBase64(_)
8459 ));
8460 }
8461
8462 #[test]
8463 fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
8464 let references = vec![Reference {
8465 uri: None,
8466 id: None,
8467 ref_type: None,
8468 transforms: vec![],
8469 digest_method: DigestAlgorithm::Sha256,
8470 digest_value: vec![0; 32],
8471 }];
8472 let uri_types = UriTypeSet {
8473 allow_empty: false,
8474 allow_same_document: true,
8475 allow_external: false,
8476 };
8477
8478 let err = enforce_reference_policies(&references, uri_types, None)
8479 .expect_err("missing URI must fail before allow_empty policy is evaluated");
8480 assert!(matches!(
8481 err,
8482 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
8483 ));
8484 }
8485
8486 #[test]
8487 fn enforce_reference_policies_checks_only_terminal_binary_output() {
8488 let c14n = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI).unwrap();
8489 let allowed = HashSet::from([
8490 BASE64_TRANSFORM_URI.to_owned(),
8491 DEFAULT_IMPLICIT_C14N_URI.to_owned(),
8492 ]);
8493 let without_implicit_c14n = HashSet::from([BASE64_TRANSFORM_URI.to_owned()]);
8494
8495 for transforms in [
8496 vec![Transform::Base64Decode, Transform::C14n(c14n)],
8497 vec![Transform::Base64Decode, Transform::Base64Decode],
8498 ] {
8499 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, vec![0; 32]);
8500 enforce_reference_policies(
8501 std::slice::from_ref(&reference),
8502 UriTypeSet::default(),
8503 Some(&allowed),
8504 )
8505 .expect("terminal binary output must not require implicit C14N");
8506 }
8507
8508 let terminal_base64 = make_reference(
8509 "",
8510 vec![Transform::Base64Decode, Transform::Base64Decode],
8511 DigestAlgorithm::Sha256,
8512 vec![0; 32],
8513 );
8514 enforce_reference_policies(
8515 std::slice::from_ref(&terminal_base64),
8516 UriTypeSet::default(),
8517 Some(&without_implicit_c14n),
8518 )
8519 .expect("terminal Base64 output must not require implicit C14N");
8520
8521 let no_transforms = make_reference("", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
8522 let error = enforce_reference_policies(
8523 std::slice::from_ref(&no_transforms),
8524 UriTypeSet::default(),
8525 Some(&without_implicit_c14n),
8526 )
8527 .expect_err("a node-set result must require allowlisted implicit C14N");
8528 assert!(matches!(
8529 error,
8530 SignatureVerificationPipelineError::Policy(
8531 crate::policy::PolicyViolation::Algorithm {
8532 operation: "verification transform",
8533 ref algorithm,
8534 }
8535 )
8536 if algorithm == DEFAULT_IMPLICIT_C14N_URI
8537 ));
8538
8539 let detached = make_reference("urn:payload", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
8540 enforce_reference_policies(
8541 std::slice::from_ref(&detached),
8542 UriTypeSet::ALL,
8543 Some(&without_implicit_c14n),
8544 )
8545 .expect("external octets without transforms must not require implicit C14N");
8546
8547 let external_xpath = make_reference(
8548 "urn:payload",
8549 vec![Transform::XPath(
8550 super::super::transforms::XPathExpression::new("true()"),
8551 )],
8552 DigestAlgorithm::Sha256,
8553 vec![0; 32],
8554 );
8555 let error = enforce_reference_policies(
8556 std::slice::from_ref(&external_xpath),
8557 UriTypeSet::ALL,
8558 Some(&HashSet::from([XPATH_TRANSFORM_URI.to_owned()])),
8559 )
8560 .expect_err("external XML converted to a node-set must require implicit C14N");
8561 assert!(matches!(
8562 error,
8563 SignatureVerificationPipelineError::Policy(
8564 crate::policy::PolicyViolation::Algorithm {
8565 operation: "verification transform",
8566 ref algorithm,
8567 }
8568 )
8569 if algorithm == DEFAULT_IMPLICIT_C14N_URI
8570 ));
8571 }
8572
8573 #[test]
8574 fn stored_pre_digest_budget_counts_repeated_external_references() {
8575 let document =
8578 Document::parse("<ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"/>")
8579 .unwrap();
8580 let payload = vec![b'x'; 7];
8581 let digest = compute_digest(DigestAlgorithm::Sha256, &payload);
8582 let references = (0..5)
8583 .map(|_| {
8584 make_reference(
8585 "urn:repeated",
8586 Vec::new(),
8587 DigestAlgorithm::Sha256,
8588 digest.clone(),
8589 )
8590 })
8591 .collect::<Vec<_>>();
8592 let resources = HashMap::from([("urn:repeated".to_owned(), payload)]);
8593 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
8594 let transform_budget = TransformExecutionBudget::default();
8595 let canonicalized_data_budget = CanonicalizedDataBudget::with_limit(32);
8596 let execution = ReferenceExecutionContext {
8597 store_pre_digest: true,
8598 transform_options: TransformOptions::default(),
8599 transform_budget: &transform_budget,
8600 canonicalized_data_budget: &canonicalized_data_budget,
8601 provider: crate::provider::default_provider(),
8602 };
8603
8604 let error = process_all_references_with_options(
8605 &references,
8606 &resolver,
8607 document.root_element(),
8608 &execution,
8609 )
8610 .expect_err(
8611 "retained diagnostics must not multiply one external allocation past the aggregate cap",
8612 );
8613 assert!(matches!(
8614 error,
8615 ReferenceProcessingError::Policy(crate::policy::PolicyViolation::ResourceLimit {
8616 resource: "canonicalized bytes",
8617 maximum: 32,
8618 ..
8619 })
8620 ));
8621 }
8622
8623 #[test]
8624 fn operation_memoizes_repeated_external_resource_identity() {
8625 let document = XmlDocument::parse("<root/>").expect("fixture must parse");
8628 let resources = HashMap::from([("urn:repeated".to_owned(), vec![b'x'; 8 * 1_024])]);
8629 let reference = make_reference(
8630 "urn:repeated",
8631 Vec::new(),
8632 DigestAlgorithm::Sha256,
8633 vec![0; 32],
8634 );
8635 let budgets = VerificationOperationBudgets::with_transforms(
8636 &crate::policy::VerificationPolicy::default(),
8637 TransformExecutionBudget::default(),
8638 );
8639
8640 document.with_view(|view| {
8641 let resolver =
8642 UriReferenceResolver::new(view.document()).with_external_resources(&resources);
8643 let first =
8644 budgets.resource_identity_for_reference(&reference, 0, &resolver, view, None);
8645 let second =
8646 budgets.resource_identity_for_reference(&reference, 1, &resolver, view, None);
8647
8648 assert_eq!(first, second);
8649 assert_eq!(budgets.external_resource_identities.borrow().len(), 1);
8650 });
8651 }
8652
8653 #[test]
8654 fn canonical_signed_info_obeys_policy_without_diagnostic_retention() {
8655 let xml = signature_with_target_reference("AQ==");
8659 let marker = "<ds:SignatureMethod";
8660 let padding = " ".repeat(1_025);
8661 let xml = xml.replacen(marker, &format!("{padding}{marker}"), 1);
8662 let policy = crate::policy::VerificationPolicy {
8663 resources: crate::policy::ResourcePolicy {
8664 max_canonicalized_bytes: 1_024,
8665 ..crate::policy::ResourcePolicy::default()
8666 },
8667 ..crate::policy::VerificationPolicy::default()
8668 };
8669
8670 let error = VerifyContext::new()
8671 .key(&AcceptingKey)
8672 .policy(policy)
8673 .verify(&xml)
8674 .expect_err("canonicalized SignedInfo must remain policy-bounded");
8675
8676 assert!(matches!(
8677 error,
8678 SignatureVerificationPipelineError::Policy(
8679 crate::policy::PolicyViolation::ResourceLimit {
8680 resource: "canonicalized bytes",
8681 ..
8682 }
8683 )
8684 ));
8685 }
8686
8687 #[test]
8688 fn fragmented_pq_signature_value_decodes_at_exact_wire_limit() {
8689 let parameter = super::super::pq_algorithm::PqAlgorithm::SlhDsaSha2_256f;
8692 let bytes = vec![0x5a; parameter.signature_len()];
8693 let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes);
8694 let mut xml = String::from("<SignatureValue>");
8695 for chunk in encoded.as_bytes().chunks(4) {
8696 xml.push_str(std::str::from_utf8(chunk).unwrap());
8697 xml.push_str("<!--split-->\n");
8698 }
8699 xml.push_str("</SignatureValue>");
8700 let document = Document::parse(&xml).unwrap();
8701 let algorithm = SignatureAlgorithm::PostQuantum(parameter);
8702 assert_eq!(
8703 decode_signature_value(
8704 document.root_element(),
8705 algorithm,
8706 &crate::policy::ResourcePolicy::default()
8707 )
8708 .unwrap(),
8709 bytes
8710 );
8711 let oversized = xml.replace("</SignatureValue>", "AAAA</SignatureValue>");
8712 let document = Document::parse(&oversized).unwrap();
8713 assert!(matches!(
8714 decode_signature_value(
8715 document.root_element(),
8716 algorithm,
8717 &crate::policy::ResourcePolicy::default()
8718 ),
8719 Err(SignatureVerificationPipelineError::InvalidStructure {
8720 reason: "SignatureValue exceeds maximum allowed length"
8721 })
8722 ));
8723 }
8724
8725 #[test]
8726 fn push_normalized_signature_text_rejects_form_feed() {
8727 let mut normalized = Vec::new();
8728 let mut raw_text_len = 0usize;
8729 let err = push_normalized_signature_text(
8730 "ab\u{000C}cd",
8731 &mut raw_text_len,
8732 &mut normalized,
8733 SignatureAlgorithm::RsaSha256,
8734 &crate::policy::ResourcePolicy::default(),
8735 )
8736 .expect_err("form-feed must not be treated as XML base64 whitespace");
8737 assert!(matches!(
8738 err,
8739 SignatureVerificationPipelineError::SignatureValueBase64(
8740 base64::DecodeError::InvalidByte(_, 0x0C)
8741 )
8742 ));
8743 }
8744
8745 #[test]
8746 fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
8747 let mut normalized = vec![b'A'; MAX_SIGNATURE_VALUE_LEN - 1];
8748 let mut raw_text_len = normalized.len();
8749 let err = push_normalized_signature_text(
8750 "é",
8751 &mut raw_text_len,
8752 &mut normalized,
8753 SignatureAlgorithm::RsaSha256,
8754 &crate::policy::ResourcePolicy::default(),
8755 )
8756 .expect_err("multibyte characters must not bypass byte-size limit");
8757 assert!(matches!(
8758 err,
8759 SignatureVerificationPipelineError::InvalidStructure {
8760 reason: "SignatureValue exceeds maximum allowed length"
8761 }
8762 ));
8763 }
8764
8765 #[test]
8768 fn reference_with_correct_digest_passes() {
8769 let xml = r##"<root>
8772 <data>hello world</data>
8773 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
8774 <ds:SignedInfo/>
8775 </ds:Signature>
8776 </root>"##;
8777 let doc = Document::parse(xml).unwrap();
8778 let resolver = UriReferenceResolver::new(&doc);
8779 let sig_node = doc
8780 .descendants()
8781 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
8782 .unwrap();
8783
8784 let initial_data = resolver.dereference("").unwrap();
8786 let transforms = vec![
8787 Transform::Enveloped,
8788 Transform::C14n(
8789 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
8790 .unwrap(),
8791 ),
8792 ];
8793 let pre_digest_bytes =
8794 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
8795 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
8796
8797 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
8799
8800 let result = process_reference(
8801 &reference,
8802 &resolver,
8803 sig_node,
8804 ReferenceSet::SignedInfo,
8805 0,
8806 false,
8807 )
8808 .unwrap();
8809 assert!(
8810 matches!(result.status, DsigStatus::Valid),
8811 "digest should match"
8812 );
8813 assert!(result.pre_digest_data.is_none());
8814 }
8815
8816 #[test]
8817 fn reference_with_wrong_digest_fails() {
8818 let xml = r##"<root>
8819 <data>hello</data>
8820 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8821 <ds:SignedInfo/>
8822 </ds:Signature>
8823 </root>"##;
8824 let doc = Document::parse(xml).unwrap();
8825 let resolver = UriReferenceResolver::new(&doc);
8826 let sig_node = doc
8827 .descendants()
8828 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
8829 .unwrap();
8830
8831 let transforms = vec![Transform::Enveloped];
8832 let wrong_digest = vec![0u8; 32];
8834 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
8835
8836 let result = process_reference(
8837 &reference,
8838 &resolver,
8839 sig_node,
8840 ReferenceSet::SignedInfo,
8841 0,
8842 false,
8843 )
8844 .unwrap();
8845 assert!(matches!(
8846 result.status,
8847 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
8848 ));
8849 }
8850
8851 #[test]
8852 fn reference_with_wrong_digest_preserves_supplied_ref_index() {
8853 let xml = r##"<root>
8854 <data>hello</data>
8855 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8856 <ds:SignedInfo/>
8857 </ds:Signature>
8858 </root>"##;
8859 let doc = Document::parse(xml).unwrap();
8860 let resolver = UriReferenceResolver::new(&doc);
8861 let sig_node = doc
8862 .descendants()
8863 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
8864 .unwrap();
8865
8866 let reference = make_reference(
8867 "",
8868 vec![Transform::Enveloped],
8869 DigestAlgorithm::Sha256,
8870 vec![0u8; 32],
8871 );
8872 let result = process_reference(
8873 &reference,
8874 &resolver,
8875 sig_node,
8876 ReferenceSet::SignedInfo,
8877 7,
8878 false,
8879 )
8880 .unwrap();
8881 assert!(matches!(
8882 result.status,
8883 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
8884 ));
8885 }
8886
8887 #[test]
8888 fn reference_stores_pre_digest_data() {
8889 let xml = "<root><child>text</child></root>";
8890 let doc = Document::parse(xml).unwrap();
8891 let resolver = UriReferenceResolver::new(&doc);
8892
8893 let initial_data = resolver.dereference("").unwrap();
8895 let pre_digest =
8896 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
8897 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
8898
8899 let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
8900 let result = process_reference(
8901 &reference,
8902 &resolver,
8903 doc.root_element(),
8904 ReferenceSet::SignedInfo,
8905 0,
8906 true,
8907 )
8908 .unwrap();
8909
8910 assert!(matches!(result.status, DsigStatus::Valid));
8911 assert!(result.pre_digest_data.is_some());
8912 assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
8913 }
8914
8915 #[test]
8918 fn reference_with_id_uri() {
8919 let xml = r##"<root>
8920 <item ID="target">specific content</item>
8921 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8922 <ds:SignedInfo/>
8923 </ds:Signature>
8924 </root>"##;
8925 let doc = Document::parse(xml).unwrap();
8926 let resolver = UriReferenceResolver::new(&doc);
8927 let sig_node = doc
8928 .descendants()
8929 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
8930 .unwrap();
8931
8932 let initial_data = resolver.dereference("#target").unwrap();
8934 let transforms = vec![Transform::C14n(
8935 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
8936 .unwrap(),
8937 )];
8938 let pre_digest =
8939 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
8940 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
8941
8942 let reference = make_reference(
8943 "#target",
8944 transforms,
8945 DigestAlgorithm::Sha256,
8946 expected_digest,
8947 );
8948 let result = process_reference(
8949 &reference,
8950 &resolver,
8951 sig_node,
8952 ReferenceSet::SignedInfo,
8953 0,
8954 false,
8955 )
8956 .unwrap();
8957 assert!(matches!(result.status, DsigStatus::Valid));
8958 }
8959
8960 #[test]
8961 fn reference_with_nonexistent_id_fails() {
8962 let xml = "<root><child/></root>";
8963 let doc = Document::parse(xml).unwrap();
8964 let resolver = UriReferenceResolver::new(&doc);
8965
8966 let reference =
8967 make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
8968 let result = process_reference(
8969 &reference,
8970 &resolver,
8971 doc.root_element(),
8972 ReferenceSet::SignedInfo,
8973 0,
8974 false,
8975 );
8976 assert!(result.is_err());
8977 }
8978
8979 #[test]
8980 fn reference_with_absent_uri_fails_closed() {
8981 let xml = "<root><child>text</child></root>";
8982 let doc = Document::parse(xml).unwrap();
8983 let resolver = UriReferenceResolver::new(&doc);
8984
8985 let reference = Reference {
8986 uri: None, id: None,
8988 ref_type: None,
8989 transforms: vec![],
8990 digest_method: DigestAlgorithm::Sha256,
8991 digest_value: vec![0; 32],
8992 };
8993
8994 let result = process_reference(
8995 &reference,
8996 &resolver,
8997 doc.root_element(),
8998 ReferenceSet::SignedInfo,
8999 0,
9000 false,
9001 );
9002 assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
9003 }
9004
9005 #[test]
9008 fn all_references_pass() {
9009 let xml = "<root><child>text</child></root>";
9010 let doc = Document::parse(xml).unwrap();
9011 let resolver = UriReferenceResolver::new(&doc);
9012
9013 let initial_data = resolver.dereference("").unwrap();
9015 let pre_digest =
9016 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
9017 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
9018
9019 let refs = vec![
9020 make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
9021 make_reference("", vec![], DigestAlgorithm::Sha256, digest),
9022 ];
9023
9024 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
9025 assert!(result.all_valid());
9026 assert_eq!(result.results.len(), 2);
9027 assert!(result.first_failure.is_none());
9028 }
9029
9030 #[test]
9031 fn reference_processing_shares_xpath_work_across_references() {
9032 let document = Document::parse("<root/>").unwrap();
9035 let resolver = UriReferenceResolver::new(&document);
9036 let transform = Transform::XPath(super::super::transforms::XPathExpression::new("true()"));
9037 let initial_data = resolver.dereference("").unwrap();
9038 let pre_digest = crate::xmldsig::execute_transforms(
9039 document.root_element(),
9040 initial_data,
9041 std::slice::from_ref(&transform),
9042 )
9043 .unwrap();
9044 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
9045 let references = vec![
9046 make_reference(
9047 "",
9048 vec![transform.clone()],
9049 DigestAlgorithm::Sha256,
9050 digest.clone(),
9051 ),
9052 make_reference("", vec![transform], DigestAlgorithm::Sha256, digest),
9053 ];
9054 let budget = TransformExecutionBudget::with_xpath_limit(12);
9055 let canonicalized_data_budget = CanonicalizedDataBudget::default();
9056 let execution = ReferenceExecutionContext {
9057 store_pre_digest: false,
9058 transform_options: TransformOptions::default(),
9059 transform_budget: &budget,
9060 canonicalized_data_budget: &canonicalized_data_budget,
9061 provider: crate::provider::default_provider(),
9062 };
9063
9064 let error = process_all_references_with_options(
9065 &references,
9066 &resolver,
9067 document.root_element(),
9068 &execution,
9069 )
9070 .expect_err("the second Reference must consume the first Reference's XPath work");
9071
9072 assert!(matches!(
9073 error,
9074 ReferenceProcessingError::Transform(TransformError::Policy(
9075 crate::policy::PolicyViolation::ResourceLimit {
9076 resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
9077 ..
9078 }
9079 ))
9080 ));
9081 }
9082
9083 #[test]
9084 fn reference_processing_shares_node_set_materialization_across_references() {
9085 let document = Document::parse(
9089 r#"<root xmlns:n="urn:0123456789"><target Id="selected">payload</target></root>"#,
9090 )
9091 .unwrap();
9092 let resolver = UriReferenceResolver::new(&document);
9093 let initial_data = resolver.dereference("#selected").unwrap();
9094 let pre_digest =
9095 crate::xmldsig::execute_transforms(document.root_element(), initial_data, &[]).unwrap();
9096 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
9097 let references = vec![
9098 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest.clone()),
9099 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest),
9100 ];
9101 let budget = TransformExecutionBudget::with_node_set_materialization_limit(30);
9102 let canonicalized_data_budget = CanonicalizedDataBudget::default();
9103 let execution = ReferenceExecutionContext {
9104 store_pre_digest: false,
9105 transform_options: TransformOptions::default(),
9106 transform_budget: &budget,
9107 canonicalized_data_budget: &canonicalized_data_budget,
9108 provider: crate::provider::default_provider(),
9109 };
9110
9111 let error = process_all_references_with_options(
9112 &references,
9113 &resolver,
9114 document.root_element(),
9115 &execution,
9116 )
9117 .expect_err("the second Reference must consume the first Reference's materialization work");
9118
9119 assert!(matches!(
9120 error,
9121 ReferenceProcessingError::UriDereference(TransformError::Policy(
9122 crate::policy::PolicyViolation::ResourceLimit {
9123 resource: crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
9124 ..
9125 }
9126 ))
9127 ));
9128 }
9129
9130 #[test]
9131 fn fail_fast_on_first_mismatch() {
9132 let xml = "<root><child>text</child></root>";
9133 let doc = Document::parse(xml).unwrap();
9134 let resolver = UriReferenceResolver::new(&doc);
9135
9136 let wrong_digest = vec![0u8; 32];
9137 let refs = vec![
9138 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
9139 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
9141 ];
9142
9143 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
9144 assert!(!result.all_valid());
9145 assert_eq!(result.first_failure, Some(0));
9146 assert_eq!(result.results.len(), 1);
9148 assert!(matches!(
9149 result.results[0].status,
9150 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
9151 ));
9152 }
9153
9154 #[test]
9155 fn fail_fast_second_reference() {
9156 let xml = "<root><child>text</child></root>";
9157 let doc = Document::parse(xml).unwrap();
9158 let resolver = UriReferenceResolver::new(&doc);
9159
9160 let initial_data = resolver.dereference("").unwrap();
9162 let pre_digest =
9163 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
9164 let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
9165 let wrong_digest = vec![0u8; 32];
9166
9167 let refs = vec![
9168 make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
9169 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
9170 ];
9171
9172 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
9173 assert!(!result.all_valid());
9174 assert_eq!(result.first_failure, Some(1));
9175 assert_eq!(result.results.len(), 2);
9177 assert!(matches!(result.results[0].status, DsigStatus::Valid));
9178 assert!(matches!(
9179 result.results[1].status,
9180 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
9181 ));
9182 }
9183
9184 #[test]
9185 fn empty_references_list() {
9186 let xml = "<root/>";
9187 let doc = Document::parse(xml).unwrap();
9188 let resolver = UriReferenceResolver::new(&doc);
9189
9190 let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
9191 assert!(result.all_valid());
9192 assert!(result.results.is_empty());
9193 }
9194
9195 #[test]
9198 fn reference_sha1_digest() {
9199 let xml = "<root>content</root>";
9200 let doc = Document::parse(xml).unwrap();
9201 let resolver = UriReferenceResolver::new(&doc);
9202
9203 let initial_data = resolver.dereference("").unwrap();
9204 let pre_digest =
9205 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
9206 let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
9207
9208 let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
9209 let result = process_reference(
9210 &reference,
9211 &resolver,
9212 doc.root_element(),
9213 ReferenceSet::SignedInfo,
9214 0,
9215 false,
9216 )
9217 .unwrap();
9218 assert!(matches!(result.status, DsigStatus::Valid));
9219 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
9220 }
9221
9222 #[test]
9223 fn reference_sha512_digest() {
9224 let xml = "<root>content</root>";
9225 let doc = Document::parse(xml).unwrap();
9226 let resolver = UriReferenceResolver::new(&doc);
9227
9228 let initial_data = resolver.dereference("").unwrap();
9229 let pre_digest =
9230 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
9231 let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
9232
9233 let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
9234 let result = process_reference(
9235 &reference,
9236 &resolver,
9237 doc.root_element(),
9238 ReferenceSet::SignedInfo,
9239 0,
9240 false,
9241 )
9242 .unwrap();
9243 assert!(matches!(result.status, DsigStatus::Valid));
9244 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
9245 }
9246
9247 #[test]
9250 fn saml_enveloped_reference_processing() {
9251 let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
9253 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
9254 ID="_resp1">
9255 <saml:Assertion ID="_assert1">
9256 <saml:Subject>user@example.com</saml:Subject>
9257 </saml:Assertion>
9258 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
9259 <ds:SignedInfo>
9260 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
9261 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
9262 <ds:Reference URI="">
9263 <ds:Transforms>
9264 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
9265 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
9266 </ds:Transforms>
9267 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
9268 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
9269 </ds:Reference>
9270 </ds:SignedInfo>
9271 <ds:SignatureValue>fakesig==</ds:SignatureValue>
9272 </ds:Signature>
9273 </samlp:Response>"##;
9274 let doc = Document::parse(xml).unwrap();
9275 let resolver = UriReferenceResolver::new(&doc);
9276 let sig_node = doc
9277 .descendants()
9278 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
9279 .unwrap();
9280
9281 let signed_info_node = sig_node
9283 .children()
9284 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
9285 .unwrap();
9286 let signed_info = parse_signed_info(signed_info_node).unwrap();
9287 let reference = &signed_info.references[0];
9288
9289 let initial_data = resolver.dereference("").unwrap();
9291 let pre_digest =
9292 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
9293 .unwrap();
9294 let correct_digest = compute_digest(reference.digest_method, &pre_digest);
9295
9296 let corrected_ref = make_reference(
9298 "",
9299 reference.transforms.clone(),
9300 reference.digest_method,
9301 correct_digest,
9302 );
9303
9304 let result = process_reference(
9306 &corrected_ref,
9307 &resolver,
9308 sig_node,
9309 ReferenceSet::SignedInfo,
9310 0,
9311 true,
9312 )
9313 .unwrap();
9314 assert!(
9315 matches!(result.status, DsigStatus::Valid),
9316 "SAML reference should verify"
9317 );
9318 assert!(result.pre_digest_data.is_some());
9319
9320 let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
9322 assert!(
9323 pre_digest_str.contains("samlp:Response"),
9324 "pre-digest should contain Response"
9325 );
9326 assert!(
9327 !pre_digest_str.contains("SignatureValue"),
9328 "pre-digest should NOT contain Signature"
9329 );
9330 }
9331
9332 #[test]
9333 fn pipeline_missing_signed_info_returns_missing_element() {
9334 let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
9335
9336 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
9337 .expect_err("missing SignedInfo must fail before crypto stage");
9338 assert!(matches!(
9339 err,
9340 SignatureVerificationPipelineError::MissingElement {
9341 element: "SignedInfo"
9342 }
9343 ));
9344 }
9345
9346 #[test]
9347 fn pipeline_multiple_signature_elements_are_rejected() {
9348 let xml = r#"
9349<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
9350 <ds:Signature>
9351 <ds:SignedInfo/>
9352 </ds:Signature>
9353 <ds:Signature/>
9354</root>
9355"#;
9356
9357 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
9358 .expect_err("multiple signatures must fail closed");
9359 assert!(matches!(
9360 err,
9361 SignatureVerificationPipelineError::InvalidStructure {
9362 reason: "Signature must appear exactly once in document",
9363 }
9364 ));
9365 }
9366
9367 #[test]
9368 fn pipeline_start_node_limits_signature_cardinality_to_its_subtree() {
9369 let xml = r#"
9372<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
9373 <scope Id="selected"><ds:Signature/></scope>
9374 <scope Id="other"><ds:Signature/></scope>
9375</root>
9376"#;
9377 let err = VerifyContext::new()
9378 .start_node_id("selected")
9379 .verify(xml)
9380 .expect_err("the selected Signature remains structurally incomplete");
9381 assert!(matches!(
9382 err,
9383 SignatureVerificationPipelineError::MissingElement {
9384 element: "SignedInfo"
9385 }
9386 ));
9387 }
9388
9389 #[test]
9390 fn pipeline_reports_keyinfo_parse_error() {
9391 let xml = r#"
9392<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
9393 xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
9394 <ds:SignedInfo>
9395 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
9396 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
9397 <ds:Reference URI="">
9398 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
9399 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
9400 </ds:Reference>
9401 </ds:SignedInfo>
9402 <ds:SignatureValue>AA==</ds:SignatureValue>
9403 <ds:KeyInfo>
9404 <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
9405 </ds:KeyInfo>
9406</ds:Signature>
9407"#;
9408
9409 let err = VerifyContext::new().verify(xml).expect_err(
9410 "invalid KeyInfo must map to ParseKeyInfo when no explicit key is supplied",
9411 );
9412 assert!(matches!(
9413 err,
9414 SignatureVerificationPipelineError::ParseKeyInfo(_)
9415 ));
9416 }
9417
9418 #[test]
9419 fn pipeline_ignores_malformed_keyinfo_when_explicit_key_is_supplied() {
9420 let base_xml = signature_with_target_reference("AQ==");
9421 let xml = base_xml
9422 .replace(
9423 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
9424 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
9425 )
9426 .replace(
9427 "</ds:SignatureValue>\n </ds:Signature>",
9428 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
9429 );
9430
9431 let result = VerifyContext::new()
9432 .key(&RejectingKey)
9433 .verify(&xml)
9434 .expect("explicit key path should not fail on malformed KeyInfo");
9435 assert!(matches!(
9436 result.status,
9437 DsigStatus::Invalid(FailureReason::SignatureMismatch)
9438 ));
9439 }
9440
9441 #[test]
9442 fn pipeline_rejects_foreign_element_children_under_signature() {
9443 let base_xml = signature_with_target_reference("AQ==");
9444 let xml = base_xml
9445 .replace(
9446 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
9447 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:foo="urn:example:foo">"#,
9448 )
9449 .replace(
9450 "</ds:SignedInfo>\n <ds:SignatureValue>",
9451 "</ds:SignedInfo>\n <foo:Bar/>\n <ds:SignatureValue>",
9452 );
9453
9454 let err = VerifyContext::new()
9455 .key(&RejectingKey)
9456 .verify(&xml)
9457 .expect_err("foreign element children under Signature must fail closed");
9458 assert!(matches!(
9459 err,
9460 SignatureVerificationPipelineError::InvalidStructure {
9461 reason: "Signature must contain only XMLDSIG element children",
9462 }
9463 ));
9464 }
9465
9466 #[test]
9467 fn pipeline_rejects_non_whitespace_mixed_content_under_signature() {
9468 let base_xml = signature_with_target_reference("AQ==");
9469 let xml = base_xml.replace(
9470 "</ds:SignedInfo>\n <ds:SignatureValue>",
9471 "</ds:SignedInfo>\n oops\n <ds:SignatureValue>",
9472 );
9473
9474 let err = VerifyContext::new()
9475 .key(&RejectingKey)
9476 .verify(&xml)
9477 .expect_err("non-whitespace mixed content under Signature must fail closed");
9478 assert!(matches!(
9479 err,
9480 SignatureVerificationPipelineError::InvalidStructure {
9481 reason: "Signature must not contain non-whitespace mixed content",
9482 }
9483 ));
9484 }
9485
9486 #[test]
9487 fn pipeline_rejects_keyinfo_out_of_order() {
9488 let base_xml = signature_with_target_reference("AQ==");
9489 let xml = base_xml.replace(
9490 "</ds:SignatureValue>\n </ds:Signature>",
9491 "</ds:SignatureValue>\n <ds:Object/>\n <ds:KeyInfo><ds:KeyName>late</ds:KeyName></ds:KeyInfo>\n </ds:Signature>",
9492 );
9493
9494 let err = VerifyContext::new()
9495 .key(&RejectingKey)
9496 .verify(&xml)
9497 .expect_err("KeyInfo after Object must be rejected by Signature child order checks");
9498 assert!(matches!(
9499 err,
9500 SignatureVerificationPipelineError::InvalidStructure {
9501 reason: "KeyInfo must be the third element child of Signature when present"
9502 }
9503 ));
9504 }
9505
9506 #[test]
9507 fn pipeline_accepts_comments_and_processing_instructions_under_signature() {
9508 let xml = r#"
9509<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
9510 <?dbg keep ?>
9511 <!-- signature metadata -->
9512 <ds:SignedInfo>
9513 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
9514 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
9515 <ds:Reference URI="">
9516 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
9517 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
9518 </ds:Reference>
9519 </ds:SignedInfo>
9520 <!-- between required children -->
9521 <ds:SignatureValue>AA==</ds:SignatureValue>
9522</ds:Signature>
9523"#;
9524
9525 let doc = Document::parse(xml).expect("test XML must parse");
9526 let signature_node = doc.root_element();
9527 let parsed = parse_signature_children(signature_node)
9528 .expect("comment/PI nodes under Signature must be ignored");
9529
9530 assert_eq!(parsed.signed_info_node.tag_name().name(), "SignedInfo");
9531 assert_eq!(
9532 parsed.signature_value_node.tag_name().name(),
9533 "SignatureValue"
9534 );
9535 assert!(parsed.key_info_node.is_none());
9536 }
9537}