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