1use crate::xml::dom::{Node, NodeId};
15use base64::Engine;
16use std::cell::{Cell, RefCell};
17use std::collections::{HashMap, HashSet};
18
19use crate::c14n::canonicalize_bounded_with_xml_base_budget;
20use crate::document::{
21 DocumentParseSettings, DocumentView, NodeIdentity, XmlDocument, XmlDocumentError,
22};
23use crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING;
24use crate::operation::{
25 OperationDecisionReason, OperationExecutionContext, OperationNodeId, OperationNodeKind,
26 OperationPlanError, OperationResourceIdentity, OperationStage,
27};
28
29#[cfg(test)]
30use super::digest::compute_digest;
31use super::digest::{DigestAlgorithm, constant_time_eq};
32#[cfg(test)]
33use super::parse::MAX_REFERENCES_PER_SIGNATURE;
34#[cfg(test)]
35use super::parse::parse_key_info;
36use super::parse::{
37 KeyInfo, MAX_X509_DATA_TOTAL_BINARY_LEN, MAX_X509_DECODED_BINARY_LEN, ParseError, Reference,
38 RetrievalMethodTransforms, SignatureAlgorithm, XMLDSIG_NS,
39};
40use super::parse::{
41 parse_reference_with_xpath_budget, parse_signed_info_with_xpath_budget,
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 map_c14n_resource_policy_violation, transform_chain_uses_implicit_c14n,
54};
55use super::types::{NodeSet, TransformError, subtree_node_id_range};
56use super::uri::{ExternalResourceMapError, UriReferenceResolver, validate_external_resource_map};
57use super::whitespace::{is_xml_whitespace_only, normalize_xml_base64_bytes};
58
59mod session;
60pub use session::*;
61mod capture;
62pub use capture::SelectedSignatureMethod;
63use capture::{CapturedOperation, ScopedCapture, VerificationCapture};
64
65const MAX_SIGNATURE_VALUE_LEN: usize = 8192;
66const MAX_RETRIEVAL_METHOD_COUNT: usize = 64;
67pub trait VerifyingKey {
72 fn association_spki(&self, _algorithm: SignatureAlgorithm) -> Option<&[u8]> {
78 None
79 }
80 fn provider_binding(&self) -> Option<&crate::provider::ProviderBinding> {
82 None
83 }
84 fn verify_candidate_keys(
88 &self,
89 _verify: &mut dyn FnMut(&dyn VerifyingKey) -> Result<bool, DsigError>,
90 ) -> Result<Option<bool>, DsigError> {
91 Ok(None)
92 }
93 fn verification_spki(
96 &self,
97 _algorithm: SignatureAlgorithm,
98 ) -> Result<Option<&[u8]>, DsigError> {
99 Ok(None)
100 }
101
102 fn ecdsa_encoding(&self) -> crate::policy::EcdsaSignatureValueEncoding {
104 crate::policy::EcdsaSignatureValueEncoding::XmlDsig
105 }
106 fn verify_with_context(
108 &self,
109 algorithm: SignatureAlgorithm,
110 context: &super::SignatureContext,
111 data: &[u8],
112 signature: &[u8],
113 ) -> Result<bool, DsigError> {
114 if !context.as_bytes().is_empty() {
115 return Err(super::SignatureVerificationError::UnsupportedAlgorithm {
116 uri: algorithm.uri().to_owned(),
117 }
118 .into());
119 }
120 self.verify(algorithm, data, signature)
121 }
122 fn validate_policy(
128 &self,
129 _policy: &crate::policy::VerificationPolicy,
130 ) -> Result<(), DsigError> {
131 Ok(())
132 }
133
134 fn validate_signature_value(
140 &self,
141 algorithm: SignatureAlgorithm,
142 signature_value: &[u8],
143 ) -> Result<bool, DsigError> {
144 Ok(super::signature::signature_value_matches_algorithm(
145 algorithm,
146 signature_value,
147 ))
148 }
149
150 fn validate_signature_value_with_policy(
158 &self,
159 policy: &crate::policy::VerificationPolicy,
160 algorithm: SignatureAlgorithm,
161 signature_value: &[u8],
162 ) -> Result<bool, DsigError> {
163 let _ = policy;
164 self.validate_signature_value(algorithm, signature_value)
165 }
166
167 fn verify(
169 &self,
170 algorithm: SignatureAlgorithm,
171 signed_data: &[u8],
172 signature_value: &[u8],
173 ) -> Result<bool, DsigError>;
174
175 fn verify_with_policy(
181 &self,
182 policy: &crate::policy::VerificationPolicy,
183 algorithm: SignatureAlgorithm,
184 signed_data: &[u8],
185 signature_value: &[u8],
186 ) -> Result<bool, DsigError> {
187 let _ = policy;
188 self.verify(algorithm, signed_data, signature_value)
189 }
190}
191
192pub trait KeyResolver {
197 fn resolve_for_verification_with_budget<'a>(
202 &'a self,
203 key_info: Option<&KeyInfo>,
204 algorithm: SignatureAlgorithm,
205 policy: &crate::policy::VerificationPolicy,
206 provider: &dyn crate::provider::CryptoProvider,
207 budget: &mut super::InspectedKeyCandidateBudget,
208 ) -> Result<Option<super::ResolvedVerificationKey<'a>>, DsigError> {
209 budget.bind_policy(policy)?;
210 budget.charge()?;
211 self.resolve_for_verification(key_info, algorithm, policy, provider)
212 }
213
214 fn resolve_for_verification<'a>(
218 &'a self,
219 key_info: Option<&KeyInfo>,
220 algorithm: SignatureAlgorithm,
221 policy: &crate::policy::VerificationPolicy,
222 provider: &dyn crate::provider::CryptoProvider,
223 ) -> Result<Option<super::ResolvedVerificationKey<'a>>, DsigError> {
224 Ok(self
225 .resolve_with_policy_and_provider(key_info, algorithm, policy, provider)?
226 .map(super::ResolvedVerificationKey::Candidate))
227 }
228 fn resolve<'a>(
235 &'a self,
236 key_info: Option<&KeyInfo>,
237 algorithm: SignatureAlgorithm,
238 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError>;
239
240 fn resolve_with_policy<'a>(
250 &'a self,
251 key_info: Option<&KeyInfo>,
252 algorithm: SignatureAlgorithm,
253 _policy: &crate::policy::VerificationPolicy,
254 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
255 self.resolve(key_info, algorithm)
256 }
257
258 fn resolve_with_policy_and_provider<'a>(
264 &'a self,
265 key_info: Option<&KeyInfo>,
266 algorithm: SignatureAlgorithm,
267 policy: &crate::policy::VerificationPolicy,
268 _provider: &dyn crate::provider::CryptoProvider,
269 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
270 self.resolve_with_policy(key_info, algorithm, policy)
271 }
272
273 fn consumes_document_key_info(&self) -> bool {
280 false
281 }
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289#[must_use = "pass the policy to VerifyContext::allowed_uri_types(), or store it for reuse"]
290pub struct UriTypeSet {
291 allow_empty: bool,
292 allow_same_document: bool,
293 allow_external: bool,
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297enum UriClass {
298 Empty,
299 SameDocument,
300 External,
301}
302
303fn classify_uri(uri: &str) -> UriClass {
304 if uri.is_empty() {
305 UriClass::Empty
306 } else if uri.starts_with('#') {
307 UriClass::SameDocument
308 } else {
309 UriClass::External
310 }
311}
312
313impl UriTypeSet {
314 pub const fn new(allow_empty: bool, allow_same_document: bool, allow_external: bool) -> Self {
316 Self {
317 allow_empty,
318 allow_same_document,
319 allow_external,
320 }
321 }
322
323 pub const SAME_DOCUMENT: Self = Self {
325 allow_empty: true,
326 allow_same_document: true,
327 allow_external: false,
328 };
329
330 pub const ALL: Self = Self {
334 allow_empty: true,
335 allow_same_document: true,
336 allow_external: true,
337 };
338
339 pub fn allows(self, uri: &str) -> bool {
341 match classify_uri(uri) {
342 UriClass::Empty => self.allow_empty,
343 UriClass::SameDocument => self.allow_same_document,
344 UriClass::External => self.allow_external,
345 }
346 }
347}
348
349impl Default for UriTypeSet {
350 fn default() -> Self {
351 Self::SAME_DOCUMENT
352 }
353}
354
355#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
357pub enum SignatureSelection<'a> {
358 #[default]
360 UniqueDocumentSignature,
361 FirstDocumentSignature,
363 FirstSignatureUnderId(&'a str),
365}
366
367#[must_use = "configure the context and call verify(), or store it for reuse"]
369#[derive(Clone)]
370pub struct VerifyContext<'a> {
371 utc_facts: Option<structured_pkix::temporal::LeapTable<'a>>,
372 decapsulation_key: Option<&'a dyn crate::provider::KeyDecapsulationKey>,
373 key: Option<&'a dyn VerifyingKey>,
374 key_resolver: Option<&'a dyn KeyResolver>,
375 policy: crate::policy::VerificationPolicy,
376 provider: &'a dyn crate::provider::CryptoProvider,
377 xml_backend: crate::XmlBackend,
378 store_pre_digest: bool,
379 external_resources: Option<&'a HashMap<String, Vec<u8>>>,
380 signature_selection: SignatureSelection<'a>,
381 id_attributes: &'a [crate::IdAttributeRegistration],
382}
383
384impl<'a> VerifyContext<'a> {
385 pub fn new() -> Self {
394 Self {
395 utc_facts: None,
396 decapsulation_key: None,
397 key: None,
398 key_resolver: None,
399 policy: crate::policy::VerificationPolicy::default(),
400 provider: crate::provider::default_provider(),
401 xml_backend: crate::XmlBackend::default(),
402 store_pre_digest: false,
403 external_resources: None,
404 signature_selection: SignatureSelection::UniqueDocumentSignature,
405 id_attributes: &[],
406 }
407 }
408
409 pub fn utc_facts(mut self, facts: structured_pkix::temporal::LeapTable<'a>) -> Self {
412 self.utc_facts = Some(facts);
413 self
414 }
415
416 pub fn key(mut self, key: &'a dyn VerifyingKey) -> Self {
423 self.key = Some(key);
424 self
425 }
426
427 pub fn decapsulation_key(mut self, key: &'a dyn crate::provider::KeyDecapsulationKey) -> Self {
431 self.decapsulation_key = Some(key);
432 self
433 }
434
435 pub fn key_resolver(mut self, resolver: &'a dyn KeyResolver) -> Self {
437 self.key_resolver = Some(resolver);
438 self
439 }
440
441 pub fn policy(mut self, policy: crate::policy::VerificationPolicy) -> Self {
443 self.policy = policy;
444 self
445 }
446
447 pub fn provider(mut self, provider: &'a dyn crate::provider::CryptoProvider) -> Self {
449 self.provider = provider;
450 self
451 }
452
453 pub fn xml_backend(mut self, backend: crate::XmlBackend) -> Self {
455 self.xml_backend = backend;
456 self
457 }
458
459 pub fn process_manifests(mut self, enabled: bool) -> Self {
483 self.policy.manifest_processing = if enabled {
484 crate::policy::ManifestProcessing::Process
485 } else {
486 crate::policy::ManifestProcessing::Ignore
487 };
488 self
489 }
490
491 pub fn allowed_uri_types(mut self, types: UriTypeSet) -> Self {
493 self.policy.uris.references = types;
494 self
495 }
496
497 pub fn allowed_retrieval_method_uri_types(mut self, types: UriTypeSet) -> Self {
504 self.policy.uris.retrieval_methods = types;
505 self
506 }
507
508 pub fn external_resources(mut self, resources: &'a HashMap<String, Vec<u8>>) -> Self {
516 self.external_resources = Some(resources);
517 self
518 }
519
520 pub fn start_node_id(mut self, id: &'a str) -> Self {
526 self.signature_selection = SignatureSelection::FirstSignatureUnderId(id);
527 self
528 }
529
530 pub fn first_document_signature(mut self) -> Self {
535 self.signature_selection = SignatureSelection::FirstDocumentSignature;
536 self
537 }
538
539 pub fn id_attributes(mut self, registrations: &'a [crate::IdAttributeRegistration]) -> Self {
541 self.id_attributes = registrations;
542 self
543 }
544
545 pub fn allow_internal_dtd(mut self, enabled: bool) -> Self {
548 self.policy.xml.allow_internal_dtd = enabled;
549 self
550 }
551
552 pub fn allowed_transforms<I, S>(mut self, transforms: I) -> Self
563 where
564 I: IntoIterator<Item = S>,
565 S: Into<String>,
566 {
567 self.policy.transforms.allowed_algorithms =
568 Some(transforms.into_iter().map(Into::into).collect());
569 self
570 }
571
572 pub fn store_pre_digest(mut self, enabled: bool) -> Self {
580 self.store_pre_digest = enabled;
581 self
582 }
583
584 pub fn xpath_here_semantics(mut self, semantics: XPathHereSemantics) -> Self {
590 self.policy.transforms.xpath_here_semantics = semantics;
591 self
592 }
593
594 fn allowed_transform_uris(&self) -> Option<&HashSet<String>> {
595 self.policy.transforms.allowed_algorithms.as_ref()
596 }
597
598 fn transform_options(&self) -> TransformOptions<'_> {
599 TransformOptions::default()
600 .id_attributes(self.id_attributes)
601 .allow_internal_dtd(self.policy.xml.allow_internal_dtd)
602 .xpath_here_semantics(self.policy.transforms.xpath_here_semantics)
603 .opc_relationship_edition(self.policy.transforms.opc_relationship_edition)
604 .xslt_stylesheet_semantics(self.policy.transforms.xslt_stylesheet_semantics)
605 }
606
607 pub fn verify(&self, xml: &str) -> Result<VerifyResult, DsigError> {
615 verify_signature_with_context(xml, self)
616 }
617
618 pub fn verify_document(&self, document: &XmlDocument) -> Result<VerifyResult, DsigError> {
624 verify_signature_document_with_context(document, self)
625 }
626
627 pub fn verify_all(&self, document: &XmlDocument) -> Result<VerifyEvidence, DsigError> {
636 verify_all_document(document, self, &VerificationRequest::default())
637 }
638
639 pub fn signature_identities(
644 &self,
645 document: &XmlDocument,
646 ) -> Result<Vec<NodeIdentity>, DsigError> {
647 self.policy.validate()?;
648 document.validate_operation_policy(&self.policy.xml, &self.policy.resources)?;
649 document.with_view(|view| {
650 let count = count_signature_nodes(view, self.policy.resources.max_signatures)?;
651 let mut identities = Vec::with_capacity(count);
652 for node in view.document().descendants() {
653 if node.has_tag_name((XMLDSIG_NS, "Signature")) {
654 identities.push(view.node_identity(node));
655 }
656 }
657 Ok(identities)
658 })
659 }
660
661 pub fn verify_request(
666 &self,
667 document: &XmlDocument,
668 request: &VerificationRequest<'_>,
669 ) -> Result<VerifyEvidence, DsigError> {
670 verify_all_document(document, self, request)
671 }
672}
673
674#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
676pub enum SignatureRequirement {
677 #[default]
679 All,
680 AtLeast(usize),
682 Exactly(usize),
684}
685
686pub struct CallerTrustedSignatureKey<'a> {
688 pub signature: NodeIdentity,
690 pub key: &'a dyn VerifyingKey,
692}
693
694#[derive(Default)]
696pub struct VerificationRequest<'a> {
697 pub expected_targets: &'a [NodeIdentity],
700 pub trusted_keys: &'a [CallerTrustedSignatureKey<'a>],
702 pub external_resources: Option<&'a HashMap<String, Vec<u8>>>,
704 pub signatures: SignatureRequirement,
706 pub correlation: Option<[u8; 16]>,
708}
709
710#[derive(Debug)]
712pub struct SignatureEvidence {
713 identity: NodeIdentity,
714 result: Result<VerifyResult, DsigError>,
715}
716
717impl SignatureEvidence {
718 #[must_use]
720 pub const fn identity(&self) -> NodeIdentity {
721 self.identity
722 }
723
724 pub const fn result(&self) -> &Result<VerifyResult, DsigError> {
726 &self.result
727 }
728}
729
730#[derive(Debug)]
732#[must_use = "inspect the individual outcomes before accepting the document"]
733pub struct VerifyEvidence<V = Vec<SignatureEvidence>> {
734 document: NodeIdentity,
735 signatures: V,
736 accepted: bool,
737 correlation: Option<[u8; 16]>,
738}
739
740impl<V: std::ops::Deref<Target = [SignatureEvidence]>> VerifyEvidence<V> {
741 pub fn accepted(&self, document: &XmlDocument) -> Result<bool, DsigError> {
744 document.with_view(|view| {
745 view.resolve_node(self.document)?;
746 Ok(self.accepted)
747 })
748 }
749
750 #[must_use]
752 pub const fn correlation(&self) -> Option<[u8; 16]> {
753 self.correlation
754 }
755
756 pub fn covers_element(
764 &self,
765 document: &XmlDocument,
766 target: NodeIdentity,
767 ) -> Result<bool, DsigError> {
768 document.with_view(|view| {
769 view.resolve_node(self.document)?;
770 let target_node = view.resolve_node(target)?;
771 if !target_node.is_element() {
772 return Err(DsigError::InvalidRequest {
773 reason: "coverage target must be an element",
774 });
775 }
776 let target_id = target_node.id().get();
777 for signature in self.signatures() {
778 let Ok(result) = &signature.result else {
779 continue;
780 };
781 if result.status != DsigStatus::Valid
782 || !matches!(
783 result.key_trust,
784 super::KeyTrustEvidence::CallerTrusted
785 | super::KeyTrustEvidence::ValidatedX509(_)
786 )
787 {
788 continue;
789 }
790 let signature_node = view.resolve_node(signature.identity)?;
791 for reference in result
792 .signed_info_references
793 .iter()
794 .chain(&result.manifest_references)
795 {
796 if reference.status != DsigStatus::Valid {
797 continue;
798 }
799 let Some(root) = reference.target_identity else {
800 continue;
801 };
802 if reference.coverage == ReferenceCoverage::TransformedData {
803 continue;
804 }
805 let root = view.resolve_node(root)?;
806 if !subtree_node_id_range(root).contains(&target_id) {
810 continue;
811 }
812 if reference.coverage == ReferenceCoverage::EnvelopedElementContent
813 && subtree_node_id_range(signature_node).contains(&target_id)
814 {
815 continue;
816 }
817 return Ok(true);
818 }
819 }
820 Ok(false)
821 })
822 }
823
824 #[must_use]
826 pub fn signatures(&self) -> &[SignatureEvidence] {
827 &self.signatures
828 }
829
830 #[must_use]
835 pub fn all_valid(&self) -> bool {
836 !self.signatures.is_empty()
837 && self.signatures.iter().all(|entry| {
838 entry.result.as_ref().is_ok_and(|result| {
839 result.status == DsigStatus::Valid
840 && result
841 .manifest_references
842 .iter()
843 .all(|reference| reference.status == DsigStatus::Valid)
844 })
845 })
846 }
847}
848
849impl Default for VerifyContext<'_> {
850 fn default() -> Self {
851 Self::new()
852 }
853}
854
855#[derive(Debug, Clone)]
857#[non_exhaustive]
858#[must_use = "inspect status before accepting the reference result"]
859pub struct ReferenceResult {
860 pub reference_identity: Option<NodeIdentity>,
862 pub target_identity: Option<NodeIdentity>,
864 pub external_resource_fingerprint: Option<[u8; 32]>,
867 pub coverage: ReferenceCoverage,
869 pub reference_set: ReferenceSet,
871 pub reference_index: usize,
873 pub uri: String,
875 pub digest_algorithm: DigestAlgorithm,
877 pub status: DsigStatus,
879 pub pre_digest_data: Option<Vec<u8>>,
881}
882
883#[derive(Debug, Clone, Copy, PartialEq, Eq)]
889#[non_exhaustive]
890pub enum ReferenceCoverage {
891 TransformedData,
893 ElementContent,
895 EnvelopedElementContent,
897}
898
899#[derive(Debug, Clone, Copy, PartialEq, Eq)]
901#[non_exhaustive]
902pub enum ReferenceSet {
903 SignedInfo,
905 Manifest,
907}
908
909#[derive(Debug, Clone, Copy, PartialEq, Eq)]
911#[non_exhaustive]
912pub enum DsigStatus {
913 Valid,
915 Invalid(FailureReason),
917}
918
919#[derive(Debug, Clone, Copy, PartialEq, Eq)]
921#[non_exhaustive]
922pub enum FailureReason {
923 ReferenceDigestMismatch {
925 ref_index: usize,
935 },
936 ReferencePolicyViolation {
938 ref_index: usize,
940 },
941 ReferenceProcessingFailure {
943 ref_index: usize,
945 },
946 SignatureMismatch,
948 KeyNotFound,
950}
951
952#[derive(Debug)]
954#[non_exhaustive]
955#[must_use = "check first_failure/results before accepting the reference set"]
956pub struct ReferencesResult {
957 pub results: Vec<ReferenceResult>,
960 pub first_failure: Option<usize>,
962}
963
964impl ReferencesResult {
965 #[must_use]
967 pub fn all_valid(&self) -> bool {
968 self.results
969 .iter()
970 .all(|result| matches!(result.status, DsigStatus::Valid))
971 }
972}
973
974pub fn process_reference(
993 reference: &Reference,
994 resolver: &UriReferenceResolver<'_>,
995 signature_node: Node<'_, '_>,
996 reference_set: ReferenceSet,
997 reference_index: usize,
998 store_pre_digest: bool,
999) -> Result<ReferenceResult, ReferenceProcessingError> {
1000 let execution_budget = TransformExecutionBudget::default();
1001 let canonicalized_data_budget = CanonicalizedDataBudget::default();
1002 let execution = ReferenceExecutionContext {
1003 store_pre_digest,
1004 transform_options: TransformOptions::default().id_attributes(resolver.id_registrations()),
1005 transform_budget: &execution_budget,
1006 canonicalized_data_budget: &canonicalized_data_budget,
1007 provider: crate::provider::default_provider(),
1008 capture: &(),
1009 };
1010 process_reference_with_options(
1011 reference,
1012 resolver,
1013 signature_node,
1014 reference_set,
1015 reference_index,
1016 reference_origin_node(signature_node, reference_set, reference_index),
1017 &execution,
1018 )
1019}
1020
1021fn reference_origin_node<'a, 'input>(
1022 signature_node: Node<'a, 'input>,
1023 reference_set: ReferenceSet,
1024 reference_index: usize,
1025) -> Option<Node<'a, 'input>> {
1026 let is_reference = |node: &Node<'_, '_>| {
1027 node.is_element()
1028 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1029 && node.tag_name().name() == "Reference"
1030 };
1031 match reference_set {
1032 ReferenceSet::SignedInfo => signature_node
1033 .children()
1034 .find(|node| {
1035 node.is_element()
1036 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1037 && node.tag_name().name() == "SignedInfo"
1038 })?
1039 .children()
1040 .filter(is_reference)
1041 .nth(reference_index),
1042 ReferenceSet::Manifest => signature_node
1043 .children()
1044 .filter(|node| {
1045 node.is_element()
1046 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1047 && node.tag_name().name() == "Object"
1048 })
1049 .flat_map(|object| {
1050 object.children().filter(|node| {
1051 node.is_element()
1052 && node.tag_name().namespace() == Some(XMLDSIG_NS)
1053 && node.tag_name().name() == "Manifest"
1054 })
1055 })
1056 .flat_map(|manifest| manifest.children().filter(is_reference))
1057 .nth(reference_index),
1058 }
1059}
1060
1061fn bind_reference_evidence(
1062 result: &mut ReferenceResult,
1063 reference: &Reference,
1064 view: DocumentView<'_>,
1065 origin: Option<Node<'_, '_>>,
1066 observed: &OperationResourceIdentity,
1067) {
1068 result.reference_identity = origin.map(|node| view.node_identity(node));
1069 if let OperationResourceIdentity::External { fingerprint, .. } = observed {
1070 result.external_resource_fingerprint = Some(*fingerprint);
1071 }
1072 if let OperationResourceIdentity::DocumentNode(target) = observed {
1073 result.target_identity = Some(*target);
1074 let mut enveloped = false;
1079 let mut canonicalized = false;
1080 for transform in &reference.transforms {
1081 match transform {
1082 Transform::C14n(_) if !canonicalized => canonicalized = true,
1083 Transform::Enveloped if !canonicalized => enveloped = true,
1084 _ => return,
1085 }
1086 }
1087 result.coverage = if enveloped {
1088 ReferenceCoverage::EnvelopedElementContent
1089 } else {
1090 ReferenceCoverage::ElementContent
1091 };
1092 }
1093}
1094
1095struct ReferenceExecutionContext<'a, C = ()> {
1096 store_pre_digest: bool,
1097 transform_options: TransformOptions<'a>,
1098 transform_budget: &'a TransformExecutionBudget,
1099 canonicalized_data_budget: &'a CanonicalizedDataBudget,
1100 provider: &'a dyn crate::provider::CryptoProvider,
1101 capture: &'a C,
1102}
1103
1104struct CanonicalizedDataBudget {
1105 remaining: Cell<usize>,
1106 max_bytes: usize,
1107}
1108
1109struct VerificationOperationBudgets<'a> {
1110 key_info_parse: RefCell<super::parse::KeyInfoParseUsage>,
1111 key_candidates: RefCell<super::InspectedKeyCandidateBudget<'a>>,
1112 key_establishment: RefCell<crate::key_establishment::KeyEstablishmentUsage>,
1113 transforms: TransformExecutionBudget,
1114 canonicalized: CanonicalizedDataBudget,
1115 xpath_parse: RefCell<XPathSignatureParseBudget>,
1116 key_info_materialization: RefCell<KeyInfoMaterializationState>,
1117 external_resource_identities: RefCell<HashMap<String, OperationResourceIdentity>>,
1118 resolved_reference_resources: RefCell<HashMap<NodeId, OperationResourceIdentity>>,
1119 external_resource_set_identity: RefCell<Option<OperationResourceIdentity>>,
1120}
1121
1122impl<'a> VerificationOperationBudgets<'a> {
1123 fn with_transforms(
1124 policy: &crate::policy::VerificationPolicy,
1125 transforms: TransformExecutionBudget,
1126 utc_facts: Option<structured_pkix::temporal::LeapTable<'a>>,
1127 ) -> Self {
1128 let key_info_parse = super::parse::KeyInfoParseUsage::new(&policy.resources);
1129 let key_candidates = super::InspectedKeyCandidateBudget::with_ingestion(
1130 policy,
1131 &key_info_parse.certificates,
1132 utc_facts,
1133 );
1134 let xpath_parse = XPathSignatureParseBudget::for_execution(&policy.resources, &transforms);
1135 Self {
1136 key_info_parse: RefCell::new(key_info_parse),
1137 key_candidates: RefCell::new(key_candidates),
1138 key_establishment: Default::default(),
1139 transforms,
1140 canonicalized: CanonicalizedDataBudget::with_limit(
1141 policy.resources.effective_canonicalized_bytes(),
1142 ),
1143 xpath_parse: RefCell::new(xpath_parse),
1144 key_info_materialization: RefCell::new(KeyInfoMaterializationState::default()),
1145 external_resource_identities: RefCell::new(HashMap::new()),
1146 resolved_reference_resources: RefCell::new(HashMap::new()),
1147 external_resource_set_identity: RefCell::new(None),
1148 }
1149 }
1150
1151 fn external_resource_set_identity(
1152 &self,
1153 resolver: &UriReferenceResolver<'_>,
1154 ) -> OperationResourceIdentity {
1155 let mut cached = self.external_resource_set_identity.borrow_mut();
1158 cached
1159 .get_or_insert_with(|| resolver.external_resource_set_identity())
1160 .clone()
1161 }
1162
1163 fn resource_identity_for_reference(
1164 &self,
1165 reference: &Reference,
1166 index: usize,
1167 resolver: &UriReferenceResolver<'_>,
1168 view: DocumentView<'_>,
1169 origin: Option<Node<'_, '_>>,
1170 ) -> OperationResourceIdentity {
1171 let Some(uri) = reference.uri.as_deref() else {
1172 return OperationResourceIdentity::Generated("omitted-reference", index);
1173 };
1174 if uri.is_empty() || uri.starts_with('#') {
1175 return resolver
1176 .node_id_for_reference_resource(
1177 uri,
1178 Some(self.transforms.node_set_materialization()),
1179 )
1180 .ok()
1181 .flatten()
1182 .map(|node| OperationResourceIdentity::DocumentNode(view.node_identity_by_id(node)))
1183 .unwrap_or(OperationResourceIdentity::Generated(
1184 "missing-document-reference",
1185 index,
1186 ));
1187 }
1188
1189 let resolved_uri;
1190 let uri = if let Some(origin) = origin {
1191 if let Some(identity) = self.resolved_reference_resources.borrow().get(&origin.id()) {
1192 return identity.clone();
1193 }
1194 let Ok(resolved) = crate::c14n::xml_base::resolve_uri_from_node_with_budget(
1198 origin,
1199 uri,
1200 self.transforms.xml_base_resolution(),
1201 ) else {
1202 return OperationResourceIdentity::Generated(
1203 "unresolved-external-reference",
1204 index,
1205 );
1206 };
1207 resolved_uri = resolved;
1208 resolved_uri.as_str()
1209 } else {
1210 uri
1211 };
1212 if let Some(identity) = self.external_resource_identities.borrow().get(uri).cloned() {
1213 if let Some(origin) = origin {
1214 self.resolved_reference_resources
1215 .borrow_mut()
1216 .insert(origin.id(), identity.clone());
1217 }
1218 return identity;
1219 }
1220 let identity = resolver
1221 .external_resource_identity(uri)
1222 .unwrap_or(OperationResourceIdentity::MissingExternal);
1223 if matches!(identity, OperationResourceIdentity::External { .. }) {
1224 self.external_resource_identities
1225 .borrow_mut()
1226 .insert(uri.to_owned(), identity.clone());
1227 }
1228 if let Some(origin) = origin {
1229 self.resolved_reference_resources
1230 .borrow_mut()
1231 .insert(origin.id(), identity.clone());
1232 }
1233 identity
1234 }
1235}
1236
1237struct VerificationPlanNodes {
1238 document: OperationNodeId,
1239 key_materialization: OperationNodeId,
1240 key: OperationNodeId,
1241 digests: Vec<OperationNodeId>,
1242 canonicalization: OperationNodeId,
1243 crypto: OperationNodeId,
1244}
1245
1246fn compile_verification_operation_plan(
1247 operation: &mut OperationExecutionContext<
1248 &crate::policy::VerificationPolicy,
1249 &VerificationOperationBudgets,
1250 >,
1251 view: DocumentView<'_>,
1252 signature_node: Node<'_, '_>,
1253 references: &[Reference],
1254 resolver: &UriReferenceResolver<'_>,
1255) -> Result<VerificationPlanNodes, SignatureVerificationPipelineError> {
1256 operation
1257 .validate_document_view(view)
1258 .map_err(map_verification_plan_error)?;
1259 let document_node = operation.add_node(
1260 OperationNodeKind::Document,
1261 OperationStage::Parse,
1262 Some(OperationResourceIdentity::DocumentNode(
1263 view.node_identity(signature_node),
1264 )),
1265 );
1266 let key_materialization_resource = operation.budgets().external_resource_set_identity(resolver);
1267 let key_materialization = operation.add_node(
1268 OperationNodeKind::Key { index: 0 },
1269 OperationStage::Resolve,
1270 Some(key_materialization_resource.clone()),
1271 );
1272 let key_node = operation.add_node(
1273 OperationNodeKind::Key { index: 1 },
1274 OperationStage::Resolve,
1275 None,
1276 );
1277 operation
1278 .add_dependency(key_materialization, document_node)
1279 .map_err(map_verification_plan_error)?;
1280 operation
1281 .add_dependency(key_node, key_materialization)
1282 .map_err(map_verification_plan_error)?;
1283 let mut digests = Vec::with_capacity(references.len());
1284 for (index, reference) in references.iter().enumerate() {
1285 let resource = operation.budgets().resource_identity_for_reference(
1286 reference,
1287 index,
1288 resolver,
1289 view,
1290 reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
1291 );
1292 let digest_node = operation.add_node(
1293 OperationNodeKind::Digest { index },
1294 OperationStage::Digest,
1295 Some(resource.clone()),
1296 );
1297 operation
1298 .add_dependency(digest_node, document_node)
1299 .map_err(map_verification_plan_error)?;
1300 digests.push(digest_node);
1301 }
1302 let canonicalization = operation.add_node(
1303 OperationNodeKind::Canonicalization,
1304 OperationStage::Canonicalization,
1305 None,
1306 );
1307 for digest in &digests {
1308 operation
1309 .add_dependency(canonicalization, *digest)
1310 .map_err(map_verification_plan_error)?;
1311 }
1312 let crypto = operation.add_node(OperationNodeKind::Crypto, OperationStage::Crypto, None);
1313 operation
1314 .add_dependency(crypto, key_node)
1315 .map_err(map_verification_plan_error)?;
1316 operation
1317 .add_dependency(crypto, canonicalization)
1318 .map_err(map_verification_plan_error)?;
1319 operation.compile().map_err(map_verification_plan_error)?;
1320 Ok(VerificationPlanNodes {
1321 document: document_node,
1322 key_materialization,
1323 key: key_node,
1324 digests,
1325 canonicalization,
1326 crypto,
1327 })
1328}
1329
1330fn map_verification_plan_error(error: OperationPlanError) -> SignatureVerificationPipelineError {
1331 SignatureVerificationPipelineError::OperationPlan(error.to_string())
1332}
1333
1334impl Default for CanonicalizedDataBudget {
1335 fn default() -> Self {
1336 Self {
1337 remaining: Cell::new(CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING),
1338 max_bytes: CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
1339 }
1340 }
1341}
1342
1343impl CanonicalizedDataBudget {
1344 fn remaining(&self) -> usize {
1345 self.remaining.get()
1346 }
1347
1348 fn charge(&self, bytes: usize) -> Result<(), ReferenceProcessingError> {
1349 let available = self.remaining.get();
1350 let Some(remaining) = available.checked_sub(bytes) else {
1351 self.remaining.set(0);
1352 return Err(crate::policy::PolicyViolation::ResourceLimit {
1353 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
1354 maximum: self.max_bytes,
1355 actual: self
1356 .max_bytes
1357 .saturating_add(bytes.saturating_sub(available)),
1358 }
1359 .into());
1360 };
1361 self.remaining.set(remaining);
1362 Ok(())
1363 }
1364
1365 fn with_limit(max_bytes: usize) -> Self {
1366 Self {
1367 remaining: Cell::new(max_bytes),
1368 max_bytes,
1369 }
1370 }
1371}
1372
1373fn process_reference_with_options<C: VerificationCapture>(
1374 reference: &Reference,
1375 resolver: &UriReferenceResolver<'_>,
1376 signature_node: Node<'_, '_>,
1377 reference_set: ReferenceSet,
1378 reference_index: usize,
1379 reference_node: Option<Node<'_, '_>>,
1380 execution: &ReferenceExecutionContext<'_, C>,
1381) -> Result<ReferenceResult, ReferenceProcessingError> {
1382 execution.capture.admit_digest(
1383 reference_node.map(|node| node.id()),
1384 reference.digest_method,
1385 )?;
1386 let uri = reference
1389 .uri
1390 .as_deref()
1391 .ok_or(ReferenceProcessingError::MissingUri)?;
1392 let initial_data = reference_node
1393 .map_or_else(
1394 || {
1395 resolver.dereference_with_budget(
1396 uri,
1397 execution.transform_budget.node_set_materialization(),
1398 )
1399 },
1400 |node| {
1401 resolver.dereference_from_with_budget(
1402 uri,
1403 node,
1404 execution.transform_budget.node_set_materialization(),
1405 execution.transform_budget.xml_base_resolution(),
1406 )
1407 },
1408 )
1409 .map_err(ReferenceProcessingError::UriDereference)?;
1410
1411 let source = reference_node.unwrap_or(signature_node);
1413 let containing_signature =
1414 super::transforms::enveloped_signature_for_reference(source, &reference.transforms, || {
1415 execution.transform_budget.charge_node_filter_work(1)
1416 })
1417 .map_err(ReferenceProcessingError::Transform)?;
1418 let super::transforms::ReferenceDigest {
1421 digest: computed_digest,
1422 pre_digest,
1423 } = super::transforms::digest_reference_transforms_with_budget(
1424 source,
1425 containing_signature,
1426 initial_data,
1427 &reference.transforms,
1428 execution.transform_options,
1429 execution.transform_budget,
1430 execution.transform_budget.xml_parse_work(),
1431 super::transforms::DigestRequest {
1432 provider: execution.provider,
1433 algorithm: reference.digest_method,
1434 retain_pre_digest: execution.store_pre_digest,
1435 },
1436 )
1437 .map_err(|error| match error {
1438 super::transforms::ReferenceDigestError::Transform(error) => {
1439 ReferenceProcessingError::Transform(error)
1440 }
1441 super::transforms::ReferenceDigestError::Provider(error) => error.into(),
1442 })?;
1443 execution
1444 .capture
1445 .computed_digest(reference_node.map(|node| node.id()))?;
1446
1447 let status = if constant_time_eq(&computed_digest, &reference.digest_value) {
1449 DsigStatus::Valid
1450 } else {
1451 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch {
1452 ref_index: reference_index,
1453 })
1454 };
1455
1456 let pre_digest_data = match pre_digest {
1457 Some(bytes) => {
1458 execution.canonicalized_data_budget.charge(bytes.len())?;
1459 Some(bytes)
1460 }
1461 None => None,
1462 };
1463
1464 Ok(ReferenceResult {
1465 reference_identity: None,
1466 target_identity: None,
1467 external_resource_fingerprint: None,
1468 coverage: ReferenceCoverage::TransformedData,
1469 reference_set,
1470 reference_index,
1471 uri: uri.to_owned(),
1472 digest_algorithm: reference.digest_method,
1473 status,
1474 pre_digest_data,
1475 })
1476}
1477
1478pub fn process_all_references(
1490 references: &[Reference],
1491 resolver: &UriReferenceResolver<'_>,
1492 signature_node: Node<'_, '_>,
1493 store_pre_digest: bool,
1494) -> Result<ReferencesResult, ReferenceProcessingError> {
1495 let execution_budget = TransformExecutionBudget::default();
1496 let canonicalized_data_budget = CanonicalizedDataBudget::default();
1497 let execution = ReferenceExecutionContext {
1498 store_pre_digest,
1499 transform_options: TransformOptions::default().id_attributes(resolver.id_registrations()),
1500 transform_budget: &execution_budget,
1501 canonicalized_data_budget: &canonicalized_data_budget,
1502 provider: crate::provider::default_provider(),
1503 capture: &(),
1504 };
1505 process_all_references_with_options(references, resolver, signature_node, &execution)
1506}
1507
1508fn process_all_references_with_options(
1509 references: &[Reference],
1510 resolver: &UriReferenceResolver<'_>,
1511 signature_node: Node<'_, '_>,
1512 execution: &ReferenceExecutionContext<'_>,
1513) -> Result<ReferencesResult, ReferenceProcessingError> {
1514 let mut results = Vec::with_capacity(references.len());
1515
1516 for (i, reference) in references.iter().enumerate() {
1517 let result = process_reference_with_options(
1518 reference,
1519 resolver,
1520 signature_node,
1521 ReferenceSet::SignedInfo,
1522 i,
1523 reference_origin_node(signature_node, ReferenceSet::SignedInfo, i),
1524 execution,
1525 )?;
1526 let failed = matches!(result.status, DsigStatus::Invalid(_));
1527 results.push(result);
1528
1529 if failed {
1530 return Ok(ReferencesResult {
1531 results,
1532 first_failure: Some(i),
1533 });
1534 }
1535 }
1536
1537 Ok(ReferencesResult {
1538 results,
1539 first_failure: None,
1540 })
1541}
1542
1543#[derive(Debug, thiserror::Error)]
1547#[non_exhaustive]
1548pub enum ReferenceProcessingError {
1549 #[error("shared reference execution: {0}")]
1551 SharedExecution(#[from] structured_asn1::memory::MemoryError),
1552 #[error("verification policy violation: {0}")]
1554 Policy(#[from] crate::policy::PolicyViolation),
1555
1556 #[error("cryptographic provider error: {0}")]
1558 Provider(#[from] crate::provider::ProviderError),
1559
1560 #[error("reference URI is required; omitted URI references are not supported")]
1562 MissingUri,
1563
1564 #[error("URI dereference failed: {0}")]
1566 UriDereference(#[source] super::types::TransformError),
1567
1568 #[error("transform failed: {0}")]
1570 Transform(#[source] super::types::TransformError),
1571}
1572
1573impl ReferenceProcessingError {
1574 fn into_policy_violation(self) -> Result<crate::policy::PolicyViolation, Self> {
1575 match self {
1576 Self::Policy(error)
1577 | Self::UriDereference(TransformError::Policy(error))
1578 | Self::Transform(TransformError::Policy(error)) => Ok(error),
1579 error => Err(error),
1580 }
1581 }
1582}
1583
1584#[derive(Debug)]
1586#[non_exhaustive]
1587#[must_use = "inspect status before accepting the document"]
1588pub struct VerifyResult {
1589 pub key_trust: super::KeyTrustEvidence,
1591 pub status: DsigStatus,
1596 pub signed_info_references: Vec<ReferenceResult>,
1600 pub manifest_references: Vec<ReferenceResult>,
1611 pub canonicalized_signed_info: Option<Vec<u8>>,
1614}
1615
1616#[derive(Debug, thiserror::Error)]
1618#[non_exhaustive]
1619pub enum DsigError {
1620 #[error("shared verification execution: {0}")]
1622 SharedExecution(#[from] structured_asn1::memory::MemoryError),
1623 #[error(transparent)]
1625 KeyEstablishment(#[from] crate::key_establishment::KeyEstablishmentError),
1626 #[error("invalid verification request: {reason}")]
1628 InvalidRequest {
1629 reason: &'static str,
1631 },
1632 #[error("verification policy violation: {0}")]
1634 Policy(#[from] crate::policy::PolicyViolation),
1635
1636 #[error("verification policy violation: {operation} policy rejects algorithm {algorithm}")]
1638 AccountedPolicyAlgorithm {
1639 operation: &'static str,
1641 algorithm: structured_asn1::memory::OwnedText,
1643 },
1644
1645 #[error("cryptographic provider error: {0}")]
1647 Provider(#[from] crate::provider::ProviderError),
1648
1649 #[error("XML parse error: {0}")]
1651 XmlParse(#[from] crate::xml::dom::ParseError),
1652
1653 #[error("XML document error: {0}")]
1655 Document(#[from] crate::document::XmlDocumentError),
1656
1657 #[error("missing required element: <{element}>")]
1659 MissingElement {
1660 element: &'static str,
1662 },
1663
1664 #[error("invalid Signature structure: {reason}")]
1666 InvalidStructure {
1667 reason: &'static str,
1669 },
1670
1671 #[error("invalid operation plan: {0}")]
1673 OperationPlan(String),
1674
1675 #[error("selected node ID is missing or ambiguous: {id}")]
1677 SelectedNodeUnavailable {
1678 id: String,
1680 },
1681
1682 #[error("failed to parse SignedInfo: {0}")]
1684 ParseSignedInfo(super::parse::ParseError),
1685
1686 #[error("failed to parse KeyInfo: {0}")]
1688 ParseKeyInfo(#[source] super::parse::ParseError),
1689
1690 #[error("key resolution failed: {0}")]
1692 KeyResolution(#[from] super::keys::KeyResolutionError),
1693
1694 #[error("failed to parse Manifest reference: {0}")]
1696 ParseManifestReference(#[source] ParseError),
1697
1698 #[error("reference processing failed: {0}")]
1700 Reference(ReferenceProcessingError),
1701
1702 #[error("SignedInfo canonicalization failed: {0}")]
1704 Canonicalization(#[from] crate::c14n::C14nError),
1705
1706 #[error("invalid SignatureValue base64: {0}")]
1708 SignatureValueBase64(#[from] base64::DecodeError),
1709
1710 #[error("signature verification failed: {0}")]
1712 Crypto(#[from] SignatureVerificationError),
1713}
1714
1715impl From<super::parse::ParseError> for DsigError {
1716 fn from(error: super::parse::ParseError) -> Self {
1717 match error {
1718 super::parse::ParseError::Policy(error) => Self::Policy(error),
1719 super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1720 Self::Policy(error)
1721 }
1722 error => Self::ParseSignedInfo(error),
1723 }
1724 }
1725}
1726
1727fn map_key_info_parse_error(error: super::parse::ParseError) -> DsigError {
1728 match error {
1729 super::parse::ParseError::Policy(error)
1730 | super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1731 DsigError::Policy(error)
1732 }
1733 error => DsigError::ParseKeyInfo(error),
1734 }
1735}
1736
1737fn map_manifest_parse_error(error: super::parse::ParseError) -> DsigError {
1738 match error {
1739 super::parse::ParseError::Policy(error)
1740 | super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1741 DsigError::Policy(error)
1742 }
1743 error => DsigError::ParseManifestReference(error),
1744 }
1745}
1746
1747impl From<ReferenceProcessingError> for DsigError {
1748 fn from(error: ReferenceProcessingError) -> Self {
1749 match error.into_policy_violation() {
1750 Ok(error) => Self::Policy(error),
1751 Err(error) => Self::Reference(error),
1752 }
1753 }
1754}
1755
1756type SignatureVerificationPipelineError = DsigError;
1757
1758impl From<OperationPlanError> for DsigError {
1759 fn from(error: OperationPlanError) -> Self {
1760 Self::OperationPlan(error.to_string())
1761 }
1762}
1763
1764pub fn verify_signature_with_pem_key(
1792 xml: &str,
1793 public_key_pem: &str,
1794 store_pre_digest: bool,
1795) -> Result<VerifyResult, DsigError> {
1796 struct PemVerifyingKey<'a> {
1797 public_key_pem: &'a str,
1798 }
1799
1800 impl VerifyingKey for PemVerifyingKey<'_> {
1801 fn verify_with_context(
1802 &self,
1803 algorithm: SignatureAlgorithm,
1804 context: &super::SignatureContext,
1805 signed_data: &[u8],
1806 signature_value: &[u8],
1807 ) -> Result<bool, DsigError> {
1808 if context.as_bytes().is_empty() {
1809 return self.verify(algorithm, signed_data, signature_value);
1810 }
1811 if algorithm.context_element().is_none() {
1812 return Err(SignatureVerificationError::UnsupportedAlgorithm {
1813 uri: algorithm.uri().to_owned(),
1814 }
1815 .into());
1816 }
1817 let (rest, pem) = x509_parser::pem::parse_x509_pem(self.public_key_pem.as_bytes())
1818 .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
1819 if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
1820 return Err(SignatureVerificationError::InvalidKeyPem.into());
1821 }
1822 Ok(super::modern::verify_with_context(
1823 algorithm,
1824 &pem.contents,
1825 context,
1826 signed_data,
1827 signature_value,
1828 )?)
1829 }
1830
1831 fn verify(
1832 &self,
1833 algorithm: SignatureAlgorithm,
1834 signed_data: &[u8],
1835 signature_value: &[u8],
1836 ) -> Result<bool, DsigError> {
1837 verify_with_algorithm(algorithm, self.public_key_pem, signed_data, signature_value)
1838 }
1839 }
1840
1841 let key = PemVerifyingKey { public_key_pem };
1842 VerifyContext::new()
1843 .key(&key)
1844 .store_pre_digest(store_pre_digest)
1845 .verify(xml)
1846}
1847
1848fn verify_signature_with_context(
1849 xml: &str,
1850 ctx: &VerifyContext<'_>,
1851) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1852 ctx.policy.validate()?;
1853 ctx.policy.signature_constraints.require_targets(false)?;
1854 ctx.policy.resources.validate_xml_document_len(xml.len())?;
1855 let execution_budget = TransformExecutionBudget::from_resources(&ctx.policy.resources)
1856 .with_xml_backend(ctx.xml_backend);
1857 let settings = DocumentParseSettings::from_policy(&ctx.policy.xml, &ctx.policy.resources)
1858 .with_backend(ctx.xml_backend);
1859 let document = XmlDocument::parse_with_settings_and_budget(
1860 xml.to_owned(),
1861 settings,
1862 execution_budget.xml_parse_work(),
1863 )
1864 .map_err(|error| map_document_parse_error(error, settings))?;
1865 verify_signature_document_with_context_and_transforms(&document, ctx, execution_budget)
1866}
1867
1868fn map_document_parse_error(error: XmlDocumentError, settings: DocumentParseSettings) -> DsigError {
1869 match error.into_policy_violation(settings) {
1870 Ok(error) => DsigError::Policy(error),
1871 Err(XmlDocumentError::Parse(error)) => DsigError::XmlParse(error),
1872 Err(error) => DsigError::Document(error),
1873 }
1874}
1875
1876#[cfg(test)]
1877mod xml_parse_budget_tests {
1878 use super::*;
1879
1880 #[test]
1881 fn verification_initial_parse_uses_the_policy_work_budget() {
1882 let xml = "<root/>";
1885 let mut policy = crate::policy::VerificationPolicy::default();
1886 policy.resources.max_xml_parse_work_bytes = 0;
1887
1888 let error = VerifyContext::new()
1889 .policy(policy)
1890 .verify(xml)
1891 .expect_err("a zero parse-work budget must reject the input parse");
1892
1893 assert!(matches!(
1894 error,
1895 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1896 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
1897 maximum: 0,
1898 actual,
1899 }) if actual == xml.len()
1900 ));
1901 }
1902}
1903
1904fn verify_signature_document_with_context(
1905 document: &XmlDocument,
1906 ctx: &VerifyContext<'_>,
1907) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1908 document.validate_operation_policy(&ctx.policy.xml, &ctx.policy.resources)?;
1909 verify_signature_document_with_context_and_transforms(
1910 document,
1911 ctx,
1912 TransformExecutionBudget::from_resources(&ctx.policy.resources)
1913 .with_xml_backend(ctx.xml_backend),
1914 )
1915}
1916
1917fn count_signature_nodes(view: DocumentView<'_>, maximum: usize) -> Result<usize, DsigError> {
1918 let mut count = 0;
1919 for node in view.document().descendants() {
1920 if !node.has_tag_name((XMLDSIG_NS, "Signature")) {
1921 continue;
1922 }
1923 if count == maximum {
1924 return Err(crate::policy::PolicyViolation::ResourceLimit {
1925 resource: crate::policy::resource_name::VERIFICATION_SIGNATURES,
1926 maximum,
1927 actual: count + 1,
1928 }
1929 .into());
1930 }
1931 count += 1;
1932 }
1933 Ok(count)
1934}
1935
1936trait SignatureBuffer: std::ops::Deref<Target = [SignatureEvidence]> {
1937 fn push_signature(
1938 &mut self,
1939 evidence: SignatureEvidence,
1940 ) -> Result<(), structured_asn1::memory::MemoryError>;
1941}
1942
1943impl SignatureBuffer for Vec<SignatureEvidence> {
1944 fn push_signature(
1945 &mut self,
1946 evidence: SignatureEvidence,
1947 ) -> Result<(), structured_asn1::memory::MemoryError> {
1948 self.push(evidence);
1949 Ok(())
1950 }
1951}
1952
1953impl<W: structured_asn1::memory::Work> SignatureBuffer
1954 for structured_asn1::memory::OwnedVec<SignatureEvidence, W>
1955{
1956 fn push_signature(
1957 &mut self,
1958 evidence: SignatureEvidence,
1959 ) -> Result<(), structured_asn1::memory::MemoryError> {
1960 self.try_push(evidence)
1961 }
1962}
1963
1964trait OperationRetention<P, B, C> {
1965 fn retain(
1966 &mut self,
1967 operation: OperationExecutionContext<P, B>,
1968 capture: C,
1969 ) -> Result<(), structured_asn1::memory::MemoryError>;
1970}
1971
1972impl<P, B, C> OperationRetention<P, B, C> for () {
1973 fn retain(
1974 &mut self,
1975 operation: OperationExecutionContext<P, B>,
1976 _capture: C,
1977 ) -> Result<(), structured_asn1::memory::MemoryError> {
1978 drop(operation);
1979 Ok(())
1980 }
1981}
1982
1983impl<P, B, C, W: structured_asn1::memory::Work> OperationRetention<P, B, C>
1984 for structured_asn1::memory::OwnedVec<CapturedOperation<P, B, C>, W>
1985{
1986 fn retain(
1987 &mut self,
1988 operation: OperationExecutionContext<P, B>,
1989 capture: C,
1990 ) -> Result<(), structured_asn1::memory::MemoryError> {
1991 self.try_push(CapturedOperation { operation, capture })
1992 }
1993}
1994
1995trait RequestStorage {
1996 type Signatures: SignatureBuffer;
1997 type Capture: VerificationCapture;
1998 type Operations<P, B>: OperationRetention<P, B, Self::Capture>;
2001
2002 fn capture(&self) -> Self::Capture;
2003
2004 fn admit_request(
2005 &self,
2006 document: &XmlDocument,
2007 ) -> Result<(), structured_asn1::memory::MemoryError>;
2008 fn signatures(
2009 &self,
2010 count: usize,
2011 ) -> Result<Self::Signatures, structured_asn1::memory::MemoryError>;
2012 fn operations<P, B>(
2013 &self,
2014 count: usize,
2015 ) -> Result<Self::Operations<P, B>, structured_asn1::memory::MemoryError>;
2016}
2017
2018struct CollectedRequestStorage;
2019
2020impl RequestStorage for CollectedRequestStorage {
2021 type Signatures = Vec<SignatureEvidence>;
2022 type Capture = ();
2023 type Operations<P, B> = ();
2024
2025 fn capture(&self) {}
2026
2027 fn admit_request(
2028 &self,
2029 _document: &XmlDocument,
2030 ) -> Result<(), structured_asn1::memory::MemoryError> {
2031 Ok(())
2032 }
2033 fn signatures(
2034 &self,
2035 count: usize,
2036 ) -> Result<Self::Signatures, structured_asn1::memory::MemoryError> {
2037 Ok(Vec::with_capacity(count))
2038 }
2039 fn operations<P, B>(
2040 &self,
2041 _count: usize,
2042 ) -> Result<Self::Operations<P, B>, structured_asn1::memory::MemoryError> {
2043 Ok(())
2044 }
2045}
2046
2047struct ScopedRequestStorage<'work, W: structured_asn1::memory::Work>(&'work W);
2048
2049impl<'work, W: structured_asn1::memory::Work> RequestStorage for ScopedRequestStorage<'work, W> {
2050 type Signatures = structured_asn1::memory::OwnedVec<SignatureEvidence, W>;
2051 type Capture = ScopedCapture<'work, W>;
2052 type Operations<P, B> =
2053 structured_asn1::memory::OwnedVec<CapturedOperation<P, B, ScopedCapture<'work, W>>, W>;
2054
2055 fn capture(&self) -> Self::Capture {
2056 ScopedCapture::new(self.0)
2057 }
2058
2059 fn admit_request(
2060 &self,
2061 document: &XmlDocument,
2062 ) -> Result<(), structured_asn1::memory::MemoryError> {
2063 let units = document
2064 .with_view(|view| {
2065 u64::try_from(view.node_count())
2066 .ok()
2067 .and_then(|count| count.checked_mul(2))
2068 .and_then(|count| count.checked_add(1))
2069 })
2070 .ok_or(structured_asn1::memory::MemoryError::WorkExceeded)?;
2071 self.0.charge_work(units)
2072 }
2073 fn signatures(
2074 &self,
2075 count: usize,
2076 ) -> Result<Self::Signatures, structured_asn1::memory::MemoryError> {
2077 let mut values = self.0.vector();
2078 values.try_reserve_exact(count)?;
2079 Ok(values)
2080 }
2081 fn operations<P, B>(
2082 &self,
2083 count: usize,
2084 ) -> Result<Self::Operations<P, B>, structured_asn1::memory::MemoryError> {
2085 let mut values = self.0.vector();
2086 values.try_reserve_exact(count)?;
2087 Ok(values)
2088 }
2089}
2090
2091fn verify_all_document(
2092 document: &XmlDocument,
2093 ctx: &VerifyContext<'_>,
2094 request: &VerificationRequest<'_>,
2095) -> Result<VerifyEvidence, DsigError> {
2096 with_verified_document(
2097 document,
2098 ctx,
2099 request,
2100 CollectedRequestStorage,
2101 |_, _, _, evidence, (), _| Ok(evidence),
2102 )
2103}
2104
2105fn with_verified_document<'inputs, R, S: RequestStorage>(
2106 document: &XmlDocument,
2107 ctx: &VerifyContext<'inputs>,
2108 request: &VerificationRequest<'inputs>,
2109 storage: S,
2110 continuation: impl for<'view, 'scope> FnOnce(
2111 DocumentView<'view>,
2112 &'scope VerifyContext<'inputs>,
2113 &'scope VerificationOperationBudgets<'inputs>,
2114 VerifyEvidence<S::Signatures>,
2115 S::Operations<
2116 &'scope crate::policy::VerificationPolicy,
2117 &'scope VerificationOperationBudgets<'inputs>,
2118 >,
2119 UriReferenceResolver<'view>,
2120 ) -> Result<R, DsigError>,
2121) -> Result<R, DsigError> {
2122 storage.admit_request(document)?;
2123 ctx.policy.validate()?;
2124 document.validate_operation_policy(&ctx.policy.xml, &ctx.policy.resources)?;
2125 ctx.policy
2126 .signature_constraints
2127 .require_targets(!request.expected_targets.is_empty())?;
2128 if request.external_resources.is_some() && ctx.external_resources.is_some() {
2129 return Err(DsigError::InvalidRequest {
2130 reason: "external resources configured in both context and request",
2131 });
2132 }
2133 let capture_time =
2137 ctx.policy.key_trust.verify_x509_chains && ctx.policy.key_trust.verification_time.is_none();
2138 let captured_context;
2139 let ctx = if capture_time || request.external_resources.is_some() {
2140 captured_context = {
2141 let mut captured = ctx.clone();
2142 if let Some(resources) = request.external_resources {
2143 captured.external_resources = Some(resources);
2144 }
2145 if capture_time {
2146 captured.policy.key_trust.verification_time = Some(std::time::SystemTime::now());
2147 }
2148 captured
2149 };
2150 &captured_context
2151 } else {
2152 ctx
2153 };
2154 let budgets = VerificationOperationBudgets::with_transforms(
2155 &ctx.policy,
2156 TransformExecutionBudget::from_resources(&ctx.policy.resources)
2157 .with_xml_backend(ctx.xml_backend),
2158 ctx.utc_facts,
2159 );
2160 document.with_view(|view| {
2161 match request.signatures {
2162 SignatureRequirement::AtLeast(0) | SignatureRequirement::Exactly(0) => {
2163 return Err(DsigError::InvalidRequest {
2164 reason: "signature requirement must be nonzero",
2165 });
2166 }
2167 _ => {}
2168 }
2169 if request.expected_targets.len() > ctx.policy.resources.max_references {
2170 return Err(DsigError::InvalidRequest {
2171 reason: "too many expected coverage targets",
2172 });
2173 }
2174 if request.trusted_keys.len() > ctx.policy.resources.max_signatures {
2175 return Err(DsigError::InvalidRequest {
2176 reason: "too many trusted signature keys",
2177 });
2178 }
2179 for (index, target) in request.expected_targets.iter().copied().enumerate() {
2180 if !view.resolve_node(target)?.is_element() {
2181 return Err(DsigError::InvalidRequest {
2182 reason: "coverage target must be an element",
2183 });
2184 }
2185 if request.expected_targets[..index].contains(&target) {
2186 return Err(DsigError::InvalidRequest {
2187 reason: "duplicate expected coverage target",
2188 });
2189 }
2190 }
2191 for (index, entry) in request.trusted_keys.iter().enumerate() {
2192 if !view
2193 .resolve_node(entry.signature)?
2194 .has_tag_name((XMLDSIG_NS, "Signature"))
2195 {
2196 return Err(DsigError::InvalidRequest {
2197 reason: "trusted key target must be a Signature",
2198 });
2199 }
2200 if request.trusted_keys[..index]
2201 .iter()
2202 .any(|prior| prior.signature == entry.signature)
2203 {
2204 return Err(DsigError::InvalidRequest {
2205 reason: "ambiguous trusted keys for one Signature",
2206 });
2207 }
2208 }
2209 let is_signature = |node: &Node<'_, '_>| node.has_tag_name((XMLDSIG_NS, "Signature"));
2210 let count = count_signature_nodes(view, ctx.policy.resources.max_signatures)?;
2211 let mut signatures = storage.signatures(count)?;
2214 let mut operations = storage.operations(count)?;
2215 let resolver = UriReferenceResolver::with_document_view(view, ctx.id_attributes)
2216 .with_same_document_id_semantics(ctx.policy.transforms.same_document_id_semantics)
2217 .with_external_resource_limits(
2218 ctx.policy.resources.max_external_resource_bytes,
2219 ctx.policy.resources.max_external_resource_total_bytes,
2220 );
2221 let resolver = match ctx.external_resources {
2222 Some(resources) => resolver.with_external_resources(resources),
2223 None => resolver,
2224 };
2225 for signature in view.document().descendants().filter(is_signature) {
2226 let mut operation = OperationExecutionContext::new(
2230 &ctx.policy,
2231 &budgets,
2232 Some((view.identity(), view.generation())),
2233 );
2234 let signature_identity = view.node_identity(signature);
2235 let key = request
2236 .trusted_keys
2237 .iter()
2238 .find(|entry| entry.signature == signature_identity)
2239 .map(|entry| entry.key)
2240 .or(ctx.key);
2241 let capture = storage.capture();
2242 let result = verify_signature_node(
2243 view,
2244 ctx,
2245 key,
2246 &mut operation,
2247 &resolver,
2248 signature,
2249 &capture,
2250 );
2251 signatures.push_signature(SignatureEvidence {
2252 identity: view.node_identity(signature),
2253 result,
2254 })?;
2255 operations.retain(operation, capture)?;
2256 }
2257 let mut evidence = VerifyEvidence {
2258 document: view.root(),
2259 signatures,
2260 accepted: false,
2261 correlation: request.correlation,
2262 };
2263 let valid = evidence
2264 .signatures
2265 .iter()
2266 .filter(|entry| {
2267 entry.result.as_ref().is_ok_and(|result| {
2268 result.status == DsigStatus::Valid
2269 && result
2270 .manifest_references
2271 .iter()
2272 .all(|reference| reference.status == DsigStatus::Valid)
2273 })
2274 })
2275 .count();
2276 evidence.accepted = match request.signatures {
2277 SignatureRequirement::All => valid > 0 && valid == count,
2278 SignatureRequirement::AtLeast(minimum) => valid >= minimum,
2279 SignatureRequirement::Exactly(required) => valid == required && count == required,
2280 };
2281 for target in request.expected_targets {
2282 if !evidence.covers_element(document, *target)? {
2283 evidence.accepted = false;
2284 break;
2285 }
2286 }
2287 continuation(view, ctx, &budgets, evidence, operations, resolver)
2288 })
2289}
2290
2291fn verify_signature_document_with_context_and_transforms(
2292 document: &XmlDocument,
2293 ctx: &VerifyContext<'_>,
2294 transforms: TransformExecutionBudget,
2295) -> Result<VerifyResult, SignatureVerificationPipelineError> {
2296 let captured_context;
2300 let ctx = if ctx.policy.key_trust.verify_x509_chains
2301 && ctx.policy.key_trust.verification_time.is_none()
2302 {
2303 captured_context = {
2304 let mut context = ctx.clone();
2305 context.policy.key_trust.verification_time = Some(std::time::SystemTime::now());
2306 context
2307 };
2308 &captured_context
2309 } else {
2310 ctx
2311 };
2312 let budgets =
2313 VerificationOperationBudgets::with_transforms(&ctx.policy, transforms, ctx.utc_facts);
2314 let mut operation = OperationExecutionContext::new(
2315 &ctx.policy,
2316 &budgets,
2317 Some((document.identity(), document.generation())),
2318 );
2319 document.with_view(|view| verify_signature_view(view, ctx, &mut operation))
2320}
2321
2322fn verify_signature_view<'a>(
2323 view: DocumentView<'a>,
2324 ctx: &VerifyContext<'_>,
2325 operation: &mut OperationExecutionContext<
2326 &crate::policy::VerificationPolicy,
2327 &VerificationOperationBudgets,
2328 >,
2329) -> Result<VerifyResult, SignatureVerificationPipelineError> {
2330 ctx.policy.validate()?;
2331 ctx.policy.signature_constraints.require_targets(false)?;
2332 let doc = view.document();
2333 let resolver = UriReferenceResolver::with_document_view(view, ctx.id_attributes)
2334 .with_same_document_id_semantics(ctx.policy.transforms.same_document_id_semantics)
2335 .with_external_resource_limits(
2336 ctx.policy.resources.max_external_resource_bytes,
2337 ctx.policy.resources.max_external_resource_total_bytes,
2338 );
2339 let resolver = match ctx.external_resources {
2340 Some(resources) => resolver.with_external_resources(resources),
2341 None => resolver,
2342 };
2343 let start_node = match ctx.signature_selection {
2344 SignatureSelection::FirstSignatureUnderId(id) => {
2345 resolver.node_for_id(id).ok_or_else(|| {
2346 SignatureVerificationPipelineError::SelectedNodeUnavailable { id: id.to_owned() }
2347 })?
2348 }
2349 SignatureSelection::UniqueDocumentSignature
2350 | SignatureSelection::FirstDocumentSignature => doc.root(),
2351 };
2352 let mut signatures = start_node.descendants().filter(|node| {
2353 node.is_element()
2354 && node.tag_name().name() == "Signature"
2355 && node.tag_name().namespace() == Some(XMLDSIG_NS)
2356 });
2357 let signature_node = match (signatures.next(), ctx.signature_selection) {
2358 (None, _) => {
2359 return Err(SignatureVerificationPipelineError::MissingElement {
2360 element: "Signature",
2361 });
2362 }
2363 (
2367 Some(node),
2368 SignatureSelection::FirstDocumentSignature
2369 | SignatureSelection::FirstSignatureUnderId(_),
2370 ) => node,
2371 (Some(node), SignatureSelection::UniqueDocumentSignature)
2372 if signatures.next().is_none() =>
2373 {
2374 node
2375 }
2376 (Some(_), SignatureSelection::UniqueDocumentSignature) => {
2377 return Err(SignatureVerificationPipelineError::InvalidStructure {
2378 reason: "Signature must appear exactly once in document",
2379 });
2380 }
2381 };
2382
2383 verify_signature_node(
2384 view,
2385 ctx,
2386 ctx.key,
2387 operation,
2388 &resolver,
2389 signature_node,
2390 &(),
2391 )
2392}
2393
2394fn verify_signature_node<'a, C: VerificationCapture>(
2395 view: DocumentView<'a>,
2396 ctx: &VerifyContext<'_>,
2397 key: Option<&dyn VerifyingKey>,
2398 operation: &mut OperationExecutionContext<
2399 &crate::policy::VerificationPolicy,
2400 &VerificationOperationBudgets,
2401 >,
2402 resolver: &UriReferenceResolver<'a>,
2403 signature_node: Node<'a, 'a>,
2404 capture: &C,
2405) -> Result<VerifyResult, DsigError> {
2406 if ctx.policy.resources.max_signatures == 0 {
2407 return Err(crate::policy::PolicyViolation::ResourceLimit {
2408 resource: crate::policy::resource_name::VERIFICATION_SIGNATURES,
2409 maximum: 0,
2410 actual: 1,
2411 }
2412 .into());
2413 }
2414 let doc = view.document();
2415 let signature_children = parse_signature_children(signature_node)?;
2416 ctx.policy
2417 .signature_constraints
2418 .check(signature_node, &operation.budgets().transforms)?;
2419 if ctx.decapsulation_key.is_some() {
2420 if ctx.policy.key_trust.mode != crate::policy::VerificationTrustMode::CryptographicOnly {
2423 return Err(crate::policy::PolicyViolation::KeyTrust {
2424 reason: "KEM/HMAC does not authenticate sender identity",
2425 }
2426 .into());
2427 }
2428 }
2429 let signed_info_node = signature_children.signed_info_node;
2430 let should_parse_key_info = ctx.decapsulation_key.is_none()
2431 && match (key, ctx.key_resolver) {
2432 (Some(_), _) => false,
2433 (None, Some(resolver)) => resolver.consumes_document_key_info(),
2434 (None, None) => true,
2435 };
2436 let mut key_info = if should_parse_key_info {
2437 signature_children
2438 .key_info_node
2439 .map(|node| {
2440 super::parse::validate_x509_digest_policy(node, &|algorithm| {
2441 ctx.policy.check_digest_algorithm(algorithm)
2442 })?;
2443 super::parse::parse_key_info_in_session(
2444 node,
2445 ctx.provider,
2446 operation.budgets().transforms.xml_base_resolution(),
2447 &ctx.policy.resources,
2448 None,
2449 ctx.policy
2450 .key_trust
2451 .certificate_signature_algorithms
2452 .as_ref(),
2453 &mut operation.budgets().key_info_parse.borrow_mut(),
2454 )
2455 })
2456 .transpose()
2457 .map_err(map_key_info_parse_error)?
2458 } else {
2459 None
2460 };
2461
2462 let signed_info = parse_signed_info_with_xpath_budget(
2463 signed_info_node,
2464 &mut operation.budgets().xpath_parse.borrow_mut(),
2465 &ctx.policy.resources,
2466 )?;
2467 capture.method(view, signed_info_node, signed_info.signature_method)?;
2468 if signed_info.references.len() > ctx.policy.resources.max_references {
2469 return Err(crate::policy::PolicyViolation::ResourceLimit {
2470 resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
2471 maximum: ctx.policy.resources.max_references,
2472 actual: signed_info.references.len(),
2473 }
2474 .into());
2475 }
2476 for reference in &signed_info.references {
2477 if reference.transforms.len() > ctx.policy.resources.max_transforms_per_reference {
2478 return Err(crate::policy::PolicyViolation::ResourceLimit {
2479 resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
2480 maximum: ctx.policy.resources.max_transforms_per_reference,
2481 actual: reference.transforms.len(),
2482 }
2483 .into());
2484 }
2485 }
2486 ctx.policy
2487 .check_signature_algorithm(signed_info.signature_method)?;
2488 if let Some(full_output_bits) = signed_info.signature_method.hmac_output_bits() {
2493 ctx.policy.hmac.validate_output(
2494 signed_info.signature_method,
2495 signed_info
2496 .hmac_output_length_bits
2497 .unwrap_or(full_output_bits),
2498 )?;
2499 }
2500 for reference in &signed_info.references {
2501 ctx.policy.check_digest_algorithm(reference.digest_method)?;
2502 super::transforms::validate_relationship_chain(
2503 &reference.transforms,
2504 ctx.policy.transforms.opc_relationship_edition,
2505 )
2506 .map_err(ReferenceProcessingError::Transform)?;
2507 }
2508 enforce_reference_policies(
2509 &signed_info.references,
2510 ctx.policy.uris.references,
2511 ctx.allowed_transform_uris(),
2512 )?;
2513 enforce_transform_allowed(ctx.allowed_transform_uris(), signed_info.c14n_method.uri())?;
2514
2515 if let Some(resources) = ctx.external_resources {
2516 validate_external_resource_map(
2517 resources,
2518 ctx.policy.resources.max_external_resource_bytes,
2519 ctx.policy.resources.max_external_resource_total_bytes,
2520 )
2521 .map_err(|error| match error {
2522 ExternalResourceMapError::Policy(error) => error.into(),
2523 ExternalResourceMapError::TotalLengthOverflow => {
2524 SignatureVerificationPipelineError::InvalidStructure {
2525 reason: "external resource total length overflow",
2526 }
2527 }
2528 })?;
2529 }
2530 let remaining_reference_capacity = ctx
2531 .policy
2532 .resources
2533 .max_references
2534 .checked_sub(signed_info.references.len())
2535 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2536 reason: "SignedInfo exceeds the per-signature Reference limit",
2537 })?;
2538 let plan_nodes = compile_verification_operation_plan(
2539 operation,
2540 view,
2541 signature_node,
2542 &signed_info.references,
2543 resolver,
2544 )?;
2545 let signature_identity =
2546 OperationResourceIdentity::DocumentNode(view.node_identity(signature_node));
2547 operation.run_with_resource(plan_nodes.document, &signature_identity, || {
2548 operation
2549 .validate_document_view(view)
2550 .map_err(map_verification_plan_error)
2551 })?;
2552 let observed_key_resources = operation.budgets().external_resource_set_identity(resolver);
2553 let retrieval_materialization = operation.run_with_resource(
2554 plan_nodes.key_materialization,
2555 &observed_key_resources,
2556 || {
2557 let budgets = operation.budgets();
2558 let mut materialization = budgets.key_info_materialization.borrow_mut();
2559 materialization.collect_encapsulation = true;
2560 materialization.encapsulation = None;
2561 if let Some(node) = signature_children.key_info_node {
2562 if ctx.decapsulation_key.is_some() {
2565 discover_encapsulation_with_budget(
2566 node,
2567 resolver,
2568 &ctx.policy.resources,
2569 &budgets.transforms,
2570 Some((&ctx.policy, ctx.xml_backend)),
2571 Some(&mut materialization),
2572 )?;
2573 } else if !should_parse_key_info {
2574 if has_encapsulation_with_budget(
2575 node,
2576 resolver,
2577 &ctx.policy.resources,
2578 &budgets.transforms,
2579 Some((&ctx.policy, ctx.xml_backend)),
2580 )? {
2581 return Err(crate::key_establishment::KeyEstablishmentError::Structure(
2582 "mechanism and explicit recipient key must be supplied together",
2583 )
2584 .into());
2585 }
2586 } else {
2587 materialization.observe_encapsulation(node, resolver, 0)?;
2588 }
2589 }
2590 if let Some(info) = key_info.as_mut() {
2591 let mut xpath_parse = budgets.xpath_parse.borrow_mut();
2592 let mut retrieval_budgets = RetrievalMaterializationBudgets {
2593 key_info_parse: &budgets.key_info_parse,
2594 xpath_parse: &mut xpath_parse,
2595 execution: &budgets.transforms,
2596 resources: &ctx.policy.resources,
2597 certificate_signature_algorithms: ctx
2598 .policy
2599 .key_trust
2600 .certificate_signature_algorithms
2601 .as_ref(),
2602 xml_backend: ctx.xml_backend,
2603 };
2604 let mut outcome = materialize_key_info_references_with_budgets(
2605 info,
2606 resolver,
2607 &ctx.policy,
2608 ctx.provider,
2609 &mut retrieval_budgets,
2610 &mut materialization,
2611 )?;
2612 outcome.merge(materialize_retrieval_methods_with_budgets(
2613 info,
2614 resolver,
2615 &ctx.policy,
2616 ctx.provider,
2617 &mut retrieval_budgets,
2618 &mut materialization.candidate_work,
2619 )?);
2620 Ok::<_, SignatureVerificationPipelineError>(outcome)
2621 } else {
2622 Ok(RetrievalMaterialization::default())
2623 }
2624 },
2625 )?;
2626 let encapsulation = operation
2627 .budgets()
2628 .key_info_materialization
2629 .borrow_mut()
2630 .encapsulation
2631 .take();
2632 if encapsulation.is_some() != ctx.decapsulation_key.is_some() {
2633 return Err(crate::key_establishment::KeyEstablishmentError::Structure(
2634 "mechanism and explicit recipient key must be supplied together",
2635 )
2636 .into());
2637 }
2638 if let Some(mechanism) = &encapsulation {
2639 ctx.policy
2640 .key_establishment
2641 .check_encapsulation(mechanism.algorithm)?;
2642 if ctx
2643 .decapsulation_key
2644 .is_some_and(|key| key.algorithm() != mechanism.algorithm)
2645 {
2646 return Err(crate::key_establishment::KeyEstablishmentError::Structure(
2647 "recipient parameter set mismatch",
2648 )
2649 .into());
2650 }
2651 }
2652 let mut reference_results = Vec::with_capacity(signed_info.references.len());
2653 let mut first_failure = None;
2654 for (index, (node, reference)) in plan_nodes
2655 .digests
2656 .iter()
2657 .copied()
2658 .zip(&signed_info.references)
2659 .enumerate()
2660 {
2661 let observed = operation.budgets().resource_identity_for_reference(
2662 reference,
2663 index,
2664 resolver,
2665 view,
2666 reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2667 );
2668 let mut result = operation.run_with_resource(node, &observed, || {
2669 let budgets = operation.budgets();
2670 let execution = ReferenceExecutionContext {
2671 store_pre_digest: ctx.store_pre_digest,
2672 transform_options: ctx.transform_options(),
2673 transform_budget: &budgets.transforms,
2674 canonicalized_data_budget: &budgets.canonicalized,
2675 provider: ctx.provider,
2676 capture,
2677 };
2678 process_reference_with_options(
2679 reference,
2680 resolver,
2681 signature_node,
2682 ReferenceSet::SignedInfo,
2683 index,
2684 reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2685 &execution,
2686 )
2687 .map_err(SignatureVerificationPipelineError::from)
2688 })?;
2689 bind_reference_evidence(
2690 &mut result,
2691 reference,
2692 view,
2693 reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2694 &observed,
2695 );
2696 let accepted = result.status == DsigStatus::Valid;
2697 operation.set_outcome(
2698 node,
2699 accepted,
2700 if accepted {
2701 OperationDecisionReason::ReferenceDigestVerified
2702 } else {
2703 OperationDecisionReason::ReferenceDigestRejected
2704 },
2705 );
2706 if result.status == DsigStatus::Valid
2707 && let Some(reference) = signed_info.references.get(result.reference_index)
2708 && reference
2709 .transforms
2710 .iter()
2711 .all(transform_preserves_manifest_structure)
2712 && let Some(uri) = reference.uri.as_deref()
2713 && let Ok(Some(target)) = resolver.node_id_for_same_document_reference(uri)
2714 {
2715 let identity = view.node_identity_by_id(target);
2716 operation.authenticate(identity);
2717 debug_assert!(operation.is_authenticated(identity));
2718 }
2719 reference_results.push(result);
2720 if !accepted {
2721 first_failure = Some(index);
2722 break;
2723 }
2724 }
2725 debug_assert!(
2726 plan_nodes.digests[reference_results.len()..]
2727 .iter()
2728 .all(|node| !operation.is_executed(*node))
2729 );
2730 let references = ReferencesResult {
2731 results: reference_results,
2732 first_failure,
2733 };
2734
2735 if let Some(first_failure) = references.first_failure {
2736 debug_assert!(operation.first_failure().is_some());
2737 let status = references.results[first_failure].status;
2738 return Ok(VerifyResult {
2739 key_trust: super::KeyTrustEvidence::NotEvaluated,
2740 status,
2741 signed_info_references: references.results,
2742 manifest_references: Vec::new(),
2743 canonicalized_signed_info: None,
2744 });
2745 }
2746
2747 let canonical_signed_info = operation.run(plan_nodes.canonicalization, || {
2748 let signed_info_subtree: HashSet<_> = signed_info_node
2749 .descendants()
2750 .map(|node: Node<'_, '_>| node.id())
2751 .collect();
2752 let mut canonical_signed_info = Vec::new();
2753 let signed_info_limit = operation
2754 .budgets()
2755 .canonicalized
2756 .remaining()
2757 .min(operation.budgets().transforms.remaining_c14n_output());
2758 canonicalize_bounded_with_xml_base_budget(
2759 doc,
2760 Some(&|node| signed_info_subtree.contains(&node.id())),
2761 &signed_info.c14n_method,
2762 signed_info_limit,
2763 operation.budgets().transforms.xml_base_resolution(),
2764 &mut canonical_signed_info,
2765 )
2766 .map_err(|error| {
2767 if let Some(violation) = map_c14n_resource_policy_violation(
2768 &error,
2769 crate::policy::resource_name::CANONICALIZED_BYTES,
2770 operation.budgets().canonicalized.max_bytes,
2771 ) {
2772 SignatureVerificationPipelineError::Policy(violation)
2773 } else {
2774 SignatureVerificationPipelineError::Canonicalization(error)
2775 }
2776 })?;
2777 operation
2778 .budgets()
2779 .transforms
2780 .charge_c14n_output(canonical_signed_info.len())
2781 .map_err(ReferenceProcessingError::Transform)?;
2782 operation
2783 .budgets()
2784 .canonicalized
2785 .charge(canonical_signed_info.len())?;
2786 Ok::<_, SignatureVerificationPipelineError>(canonical_signed_info)
2787 })?;
2788
2789 let signature_value = decode_signature_value(
2790 signature_children.signature_value_node,
2791 signed_info.signature_method,
2792 &ctx.policy.resources,
2793 )?;
2794 if let Some(full_output_bits) = signed_info.signature_method.hmac_output_bits() {
2795 let expected_bits = signed_info
2796 .hmac_output_length_bits
2797 .unwrap_or(full_output_bits);
2798 if signature_value.len() != expected_bits / 8 {
2799 return Err(SignatureVerificationPipelineError::InvalidStructure {
2800 reason: "SignatureValue length does not match HMACOutputLength",
2801 });
2802 }
2803 }
2804 let resolved_key = operation.run(plan_nodes.key, || {
2805 if let Some(recipient) = ctx.decapsulation_key {
2806 if key.is_some() || ctx.key_resolver.is_some() {
2807 return Err(crate::key_establishment::KeyEstablishmentError::Structure(
2808 "ambiguous verification key request",
2809 )
2810 .into());
2811 }
2812 require_verifying_key_candidate_capacity(&ctx.policy)?;
2813 if !ctx.policy.key_sources.preset_key
2814 || signed_info.signature_method.hmac_output_bits().is_none()
2815 {
2816 return Err(crate::key_establishment::KeyEstablishmentError::Structure(
2817 "encapsulation requires permitted explicit key and HMAC",
2818 )
2819 .into());
2820 }
2821 ctx.policy.hmac.validate_key_bits(256)?;
2822 let mechanism = encapsulation.as_ref().ok_or(
2823 crate::key_establishment::KeyEstablishmentError::Structure("missing mechanism"),
2824 )?;
2825 operation.budgets().key_candidates.borrow_mut().charge()?;
2826 operation
2827 .budgets()
2828 .key_establishment
2829 .borrow_mut()
2830 .commit_all(&ctx.policy.key_establishment, 0, 0, 32)?;
2831 let secret = operation
2832 .budgets()
2833 .key_establishment
2834 .borrow_mut()
2835 .decapsulate(
2836 &ctx.policy.key_establishment,
2837 ctx.provider,
2838 recipient,
2839 mechanism.algorithm,
2840 &mechanism.ciphertext[..mechanism.algorithm.ciphertext_len()],
2841 )?;
2842 let hmac = super::HmacVerificationKey::new(secret.to_vec())?;
2843 return Ok(Some(ResolvedVerifyingKey::Established(hmac)));
2844 }
2845 resolve_verifying_key(
2846 ctx,
2847 key,
2848 key_info.as_ref(),
2849 signed_info.signature_method,
2850 &mut operation.budgets().key_candidates.borrow_mut(),
2851 )
2852 })?;
2853 let Some(resolved_key) = resolved_key else {
2854 operation.set_outcome(
2855 plan_nodes.key,
2856 false,
2857 OperationDecisionReason::KeyUnavailable,
2858 );
2859 if let Some(error) = retrieval_materialization.deferred_error {
2860 return Err(error);
2861 }
2862 return Ok(VerifyResult {
2863 key_trust: super::KeyTrustEvidence::NotEvaluated,
2864 status: DsigStatus::Invalid(FailureReason::KeyNotFound),
2865 signed_info_references: references.results,
2866 manifest_references: Vec::new(),
2867 canonicalized_signed_info: if ctx.store_pre_digest {
2868 Some(canonical_signed_info)
2869 } else {
2870 None
2871 },
2872 });
2873 };
2874 operation.set_outcome(plan_nodes.key, true, OperationDecisionReason::KeyResolved);
2875 let verifier = resolved_key.as_ref();
2876 let signature_valid = operation.run(plan_nodes.crypto, || {
2877 verifier.validate_policy(&ctx.policy)?;
2878 if !verifier.validate_signature_value_with_policy(
2879 &ctx.policy,
2880 signed_info.signature_method,
2881 &signature_value,
2882 )? {
2883 return Ok::<_, SignatureVerificationPipelineError>(false);
2884 }
2885 ctx.provider
2886 .require_capability(crate::provider::ProviderCapability::Verify(
2887 signed_info.signature_method,
2888 ))?;
2889 let candidate_refusal = RefCell::new(None);
2890 let policy_verifier = PolicyVerifyingKey {
2891 key: verifier,
2892 policy: &ctx.policy,
2893 candidates: &operation.budgets().key_candidates,
2894 candidate_refusal: &candidate_refusal,
2895 };
2896 let result = capture.verify(
2897 ctx.provider,
2898 &policy_verifier,
2899 crate::provider::SignatureVerificationInput {
2900 algorithm: signed_info.signature_method,
2901 context: &signed_info.signature_context,
2902 data: &canonical_signed_info,
2903 signature: &signature_value,
2904 },
2905 );
2906 if let Some(error) = candidate_refusal.into_inner() {
2907 return Err(error.into());
2908 }
2909 result
2910 })?;
2911
2912 if !signature_valid {
2913 operation.set_outcome(
2914 plan_nodes.crypto,
2915 false,
2916 OperationDecisionReason::SignatureRejected,
2917 );
2918 return Ok(VerifyResult {
2919 key_trust: resolved_key.evidence(&ctx.policy)?,
2920 status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
2921 signed_info_references: references.results,
2922 manifest_references: Vec::new(),
2923 canonicalized_signed_info: if ctx.store_pre_digest {
2924 Some(canonical_signed_info)
2925 } else {
2926 None
2927 },
2928 });
2929 }
2930 operation.set_outcome(
2931 plan_nodes.crypto,
2932 true,
2933 OperationDecisionReason::SignatureVerified,
2934 );
2935 let (manifest_references, manifest_digests) =
2939 if ctx.policy.manifest_processing == crate::policy::ManifestProcessing::Process {
2940 process_authenticated_manifest_references(
2941 operation,
2942 view,
2943 signature_node,
2944 resolver,
2945 ManifestVerification {
2946 context: ctx,
2947 capture,
2948 },
2949 remaining_reference_capacity,
2950 plan_nodes.crypto,
2951 )?
2952 } else {
2953 (Vec::new(), Vec::new())
2954 };
2955 operation.extend();
2956 let evidence = operation.add_node(OperationNodeKind::Evidence, OperationStage::Evidence, None);
2957 operation
2958 .add_dependency(evidence, plan_nodes.crypto)
2959 .map_err(map_verification_plan_error)?;
2960 for digest in manifest_digests {
2961 operation
2962 .add_dependency(evidence, digest)
2963 .map_err(map_verification_plan_error)?;
2964 }
2965 operation.compile().map_err(map_verification_plan_error)?;
2966 operation.run(evidence, || {
2967 Ok::<_, SignatureVerificationPipelineError>(VerifyResult {
2968 key_trust: resolved_key.evidence(&ctx.policy)?,
2969 status: DsigStatus::Valid,
2970 signed_info_references: references.results,
2971 manifest_references,
2972 canonicalized_signed_info: if ctx.store_pre_digest {
2973 Some(canonical_signed_info)
2974 } else {
2975 None
2976 },
2977 })
2978 })
2979}
2980
2981struct PolicyVerifyingKey<'a, 'inputs> {
2982 key: &'a dyn VerifyingKey,
2983 policy: &'a crate::policy::VerificationPolicy,
2984 candidates: &'a RefCell<super::InspectedKeyCandidateBudget<'inputs>>,
2985 candidate_refusal: &'a RefCell<Option<crate::policy::PolicyViolation>>,
2986}
2987
2988impl VerifyingKey for PolicyVerifyingKey<'_, '_> {
2989 fn association_spki(&self, algorithm: SignatureAlgorithm) -> Option<&[u8]> {
2990 self.key.association_spki(algorithm)
2991 }
2992 fn provider_binding(&self) -> Option<&crate::provider::ProviderBinding> {
2993 self.key.provider_binding()
2994 }
2995 fn verify_candidate_keys(
2996 &self,
2997 verify: &mut dyn FnMut(&dyn VerifyingKey) -> Result<bool, DsigError>,
2998 ) -> Result<Option<bool>, DsigError> {
2999 self.key.verify_candidate_keys(&mut |key| {
3000 if let Some(error) = self.candidate_refusal.borrow().as_ref() {
3001 return Err(error.clone().into());
3002 }
3003 if let Err(error) = self.candidates.borrow_mut().charge_policy(1) {
3006 *self.candidate_refusal.borrow_mut() = Some(error.clone());
3007 return Err(error.into());
3008 }
3009 key.validate_policy(self.policy)?;
3010 verify(&PolicyVerifyingKey {
3011 key,
3012 policy: self.policy,
3013 candidates: self.candidates,
3014 candidate_refusal: self.candidate_refusal,
3015 })
3016 })
3017 }
3018 fn verification_spki(&self, algorithm: SignatureAlgorithm) -> Result<Option<&[u8]>, DsigError> {
3019 self.key.verification_spki(algorithm)
3020 }
3021
3022 fn ecdsa_encoding(&self) -> crate::policy::EcdsaSignatureValueEncoding {
3023 self.policy.ecdsa_signature_value_encoding
3024 }
3025 fn verify_with_context(
3026 &self,
3027 algorithm: SignatureAlgorithm,
3028 context: &super::SignatureContext,
3029 data: &[u8],
3030 signature: &[u8],
3031 ) -> Result<bool, DsigError> {
3032 if context.as_bytes().is_empty() {
3033 self.verify(algorithm, data, signature)
3034 } else {
3035 self.key
3036 .verify_with_context(algorithm, context, data, signature)
3037 }
3038 }
3039 fn validate_policy(&self, policy: &crate::policy::VerificationPolicy) -> Result<(), DsigError> {
3040 self.key.validate_policy(policy)
3041 }
3042
3043 fn validate_signature_value(
3044 &self,
3045 algorithm: SignatureAlgorithm,
3046 signature_value: &[u8],
3047 ) -> Result<bool, DsigError> {
3048 self.key
3049 .validate_signature_value_with_policy(self.policy, algorithm, signature_value)
3050 }
3051
3052 fn verify(
3053 &self,
3054 algorithm: SignatureAlgorithm,
3055 signed_data: &[u8],
3056 signature_value: &[u8],
3057 ) -> Result<bool, DsigError> {
3058 self.key
3059 .verify_with_policy(self.policy, algorithm, signed_data, signature_value)
3060 }
3061}
3062
3063#[derive(Debug, Default)]
3064struct RetrievalMaterialization {
3065 deferred_error: Option<SignatureVerificationPipelineError>,
3066}
3067
3068impl RetrievalMaterialization {
3069 fn merge(&mut self, other: Self) {
3070 if self.deferred_error.is_none() {
3071 self.deferred_error = other.deferred_error;
3072 }
3073 }
3074}
3075
3076struct RetrievalMaterializationBudgets<'a> {
3077 key_info_parse: &'a RefCell<super::parse::KeyInfoParseUsage>,
3078 certificate_signature_algorithms: Option<&'a crate::policy::CertificateSignatureAlgorithms>,
3079 xpath_parse: &'a mut XPathSignatureParseBudget,
3080 execution: &'a TransformExecutionBudget,
3081 resources: &'a crate::policy::ResourcePolicy,
3082 xml_backend: crate::XmlBackend,
3083}
3084
3085#[derive(Default)]
3086struct KeyInfoMaterializationState {
3087 active: HashSet<(super::uri::TraversalDocumentIdentity, String)>,
3088 candidate_work: usize,
3089 collect_encapsulation: bool,
3090 encapsulation_template: bool,
3091 retain_encapsulation_target: bool,
3092 encapsulation_depth: usize,
3093 reference_depth_base: usize,
3094 encapsulation_target: Option<(super::uri::TraversalDocumentIdentity, NodeId, NodeId)>,
3095 encapsulation: Option<ResolvedEncapsulation>,
3096}
3097
3098struct ResolvedEncapsulation {
3099 algorithm: crate::provider::KeyEncapsulationAlgorithm,
3100 ciphertext: [u8; 1568],
3101}
3102
3103impl KeyInfoMaterializationState {
3104 fn observe_encapsulation(
3105 &mut self,
3106 node: Node<'_, '_>,
3107 resolver: &UriReferenceResolver<'_>,
3108 depth: usize,
3109 ) -> Result<(), SignatureVerificationPipelineError> {
3110 if !self.collect_encapsulation {
3111 return Ok(());
3112 }
3113 let Some(mechanism) =
3118 crate::key_establishment::direct_encapsulation(node, self.encapsulation_template)
3119 .map_err(|_| SignatureVerificationPipelineError::InvalidStructure {
3120 reason: "invalid encapsulation mechanism in resolved KeyInfo",
3121 })?
3122 else {
3123 return Ok(());
3124 };
3125 self.record_encapsulation(mechanism, resolver, depth)
3126 }
3127
3128 fn record_encapsulation(
3129 &mut self,
3130 mechanism: crate::key_establishment::ParsedEncapsulation<'_, '_>,
3131 resolver: &UriReferenceResolver<'_>,
3132 depth: usize,
3133 ) -> Result<(), SignatureVerificationPipelineError> {
3134 if self.encapsulation.is_some() {
3135 return Err(SignatureVerificationPipelineError::InvalidStructure {
3136 reason: "multiple encapsulation mechanisms in resolved KeyInfo",
3137 });
3138 }
3139 if self.retain_encapsulation_target {
3140 self.encapsulation_depth = depth;
3141 self.encapsulation_target = Some((
3142 resolver.traversal_document_identity(),
3143 mechanism.cipher_value.id(),
3144 mechanism.key_info.id(),
3145 ));
3146 }
3147 self.encapsulation = Some(ResolvedEncapsulation {
3148 algorithm: mechanism.algorithm,
3149 ciphertext: mechanism.into_ciphertext(),
3150 });
3151 Ok(())
3152 }
3153}
3154
3155trait KeyInfoReferencePolicy {
3156 fn key_establishment(&self) -> &crate::policy::KeyEstablishmentPolicy;
3157 fn check_digest_algorithm(
3158 &self,
3159 algorithm: DigestAlgorithm,
3160 ) -> Result<(), crate::policy::PolicyViolation>;
3161 fn certificate_signature_algorithms(
3162 &self,
3163 ) -> Option<&crate::policy::CertificateSignatureAlgorithms>;
3164 fn resources(&self) -> &crate::policy::ResourcePolicy;
3165 fn xml(&self) -> &crate::policy::XmlInputPolicy;
3166 fn key_info_reference_uris(&self) -> UriTypeSet;
3167 fn retrieval_method_uris(&self) -> UriTypeSet;
3168 fn key_info_reference_source_enabled(&self) -> bool;
3169 fn allowed_transforms(&self) -> Option<&HashSet<String>>;
3170}
3171
3172impl KeyInfoReferencePolicy for crate::policy::SigningPolicy {
3173 fn key_establishment(&self) -> &crate::policy::KeyEstablishmentPolicy {
3174 &self.key_establishment
3175 }
3176 fn check_digest_algorithm(
3177 &self,
3178 algorithm: DigestAlgorithm,
3179 ) -> Result<(), crate::policy::PolicyViolation> {
3180 self.check_digest_algorithm(algorithm)
3181 }
3182 fn certificate_signature_algorithms(
3183 &self,
3184 ) -> Option<&crate::policy::CertificateSignatureAlgorithms> {
3185 None
3186 }
3187 fn resources(&self) -> &crate::policy::ResourcePolicy {
3188 &self.resources
3189 }
3190
3191 fn xml(&self) -> &crate::policy::XmlInputPolicy {
3192 &self.xml
3193 }
3194
3195 fn key_info_reference_uris(&self) -> UriTypeSet {
3196 self.uris.key_info_references
3197 }
3198
3199 fn retrieval_method_uris(&self) -> UriTypeSet {
3200 self.uris.retrieval_methods
3201 }
3202
3203 fn key_info_reference_source_enabled(&self) -> bool {
3204 true
3205 }
3206
3207 fn allowed_transforms(&self) -> Option<&HashSet<String>> {
3208 self.transforms.allowed_algorithms.as_ref()
3209 }
3210}
3211
3212impl KeyInfoReferencePolicy for crate::policy::VerificationPolicy {
3213 fn key_establishment(&self) -> &crate::policy::KeyEstablishmentPolicy {
3214 &self.key_establishment
3215 }
3216 fn check_digest_algorithm(
3217 &self,
3218 algorithm: DigestAlgorithm,
3219 ) -> Result<(), crate::policy::PolicyViolation> {
3220 self.check_digest_algorithm(algorithm)
3221 }
3222 fn certificate_signature_algorithms(
3223 &self,
3224 ) -> Option<&crate::policy::CertificateSignatureAlgorithms> {
3225 self.key_trust.certificate_signature_algorithms.as_ref()
3226 }
3227 fn resources(&self) -> &crate::policy::ResourcePolicy {
3228 &self.resources
3229 }
3230
3231 fn xml(&self) -> &crate::policy::XmlInputPolicy {
3232 &self.xml
3233 }
3234
3235 fn key_info_reference_uris(&self) -> UriTypeSet {
3236 self.uris.key_info_references
3237 }
3238
3239 fn retrieval_method_uris(&self) -> UriTypeSet {
3240 self.uris.retrieval_methods
3241 }
3242
3243 fn key_info_reference_source_enabled(&self) -> bool {
3244 self.key_sources.key_info_reference
3245 }
3246
3247 fn allowed_transforms(&self) -> Option<&HashSet<String>> {
3248 self.transforms.allowed_algorithms.as_ref()
3249 }
3250}
3251
3252struct KeyInfoReferenceMaterializationContext<'a, 'budget, P> {
3253 policy: &'a P,
3254 provider: &'a dyn crate::provider::CryptoProvider,
3255 budgets: &'a mut RetrievalMaterializationBudgets<'budget>,
3256}
3257
3258fn materialize_key_info_references_with_budgets<P: KeyInfoReferencePolicy>(
3259 key_info: &mut KeyInfo,
3260 resolver: &UriReferenceResolver<'_>,
3261 policy: &P,
3262 provider: &dyn crate::provider::CryptoProvider,
3263 budgets: &mut RetrievalMaterializationBudgets<'_>,
3264 materialization: &mut KeyInfoMaterializationState,
3265) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
3266 fn visit<P: KeyInfoReferencePolicy>(
3267 key_info: &mut KeyInfo,
3268 resolver: &UriReferenceResolver<'_>,
3269 context: &mut KeyInfoReferenceMaterializationContext<'_, '_, P>,
3270 materialization: &mut KeyInfoMaterializationState,
3271 depth: usize,
3272 ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
3273 let mut materialized = Vec::new();
3274 let mut outcome = RetrievalMaterialization::default();
3275 for source in std::mem::take(&mut key_info.sources) {
3276 let source_work = match &source {
3277 super::parse::KeyInfoSource::X509Data(info) => info.certificates.len().max(1),
3278 _ => 1,
3279 };
3280 materialization.candidate_work =
3281 materialization.candidate_work.saturating_add(source_work);
3282 context
3283 .policy
3284 .resources()
3285 .validate_key_candidates(materialization.candidate_work)?;
3286
3287 let super::parse::KeyInfoSource::KeyInfoReference { uri } = source else {
3288 materialized.push(source);
3289 continue;
3290 };
3291 if !context.policy.key_info_reference_source_enabled() {
3292 return Err(crate::policy::PolicyViolation::KeyTrust {
3293 reason: "KeyInfoReference key sources are disabled",
3294 }
3295 .into());
3296 }
3297 if !context.policy.key_info_reference_uris().allows(&uri) {
3298 return Err(crate::policy::PolicyViolation::Uri {
3299 operation: "KeyInfoReference",
3300 reason: "URI class is disabled",
3301 }
3302 .into());
3303 }
3304 let next_depth = depth.saturating_add(1);
3305 context
3306 .policy
3307 .resources()
3308 .validate_key_info_reference_depth(next_depth)?;
3309 let cycle_key = (resolver.traversal_document_identity(), uri.clone());
3310 if !materialization.active.insert(cycle_key.clone()) {
3311 return Err(SignatureVerificationPipelineError::InvalidStructure {
3312 reason: "KeyInfoReference cycle detected",
3313 });
3314 }
3315
3316 let is_same_document = uri.is_empty() || uri.starts_with('#');
3317 let (mut referenced, mut nested_outcome) = if is_same_document {
3318 let node = resolver
3319 .node_for_same_document_reference(&uri)
3320 .map_err(|error| {
3321 SignatureVerificationPipelineError::from(
3322 ReferenceProcessingError::UriDereference(error),
3323 )
3324 })?
3325 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
3326 reason: "KeyInfoReference target is missing or ambiguous",
3327 })?;
3328 if !node.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
3329 return Err(SignatureVerificationPipelineError::InvalidStructure {
3330 reason: "KeyInfoReference target must be KeyInfo",
3331 });
3332 }
3333 materialization.observe_encapsulation(node, resolver, next_depth)?;
3334 if let Some(mechanism) = &materialization.encapsulation {
3335 context
3336 .policy
3337 .key_establishment()
3338 .check_encapsulation(mechanism.algorithm)?;
3339 }
3340 super::parse::validate_x509_digest_policy(node, &|algorithm| {
3341 context.policy.check_digest_algorithm(algorithm)
3342 })
3343 .map_err(map_key_info_parse_error)?;
3344 (
3345 super::parse::parse_key_info_in_session(
3346 node,
3347 context.provider,
3348 context.budgets.execution.xml_base_resolution(),
3349 context.policy.resources(),
3350 None,
3351 context.policy.certificate_signature_algorithms(),
3352 &mut context.budgets.key_info_parse.borrow_mut(),
3353 )
3354 .map_err(map_key_info_parse_error)?,
3355 RetrievalMaterialization::default(),
3356 )
3357 } else {
3358 let (resource_uri, fragment) = uri
3362 .split_once('#')
3363 .map_or((uri.as_str(), None), |(resource, fragment)| {
3364 (resource, (!fragment.is_empty()).then_some(fragment))
3365 });
3366 let bytes = resolver
3367 .external_resource(resource_uri)
3368 .map_err(|error| {
3369 SignatureVerificationPipelineError::from(
3370 ReferenceProcessingError::UriDereference(error),
3371 )
3372 })?
3373 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
3374 reason: "KeyInfoReference external resource is unavailable",
3375 })?;
3376 context
3379 .budgets
3380 .execution
3381 .xml_parse_work()
3382 .charge_policy(bytes.len())?;
3383 let xml = crate::encoding::decode_xml_octets(
3384 bytes,
3385 context.policy.resources().max_xml_document_bytes,
3386 )
3387 .map_err(map_key_info_xml_decode_error)?;
3388 let settings = DocumentParseSettings::from_policy(
3389 context.policy.xml(),
3390 context.policy.resources(),
3391 )
3392 .with_backend(context.budgets.xml_backend);
3393 let document = XmlDocument::parse_with_settings_and_budget(
3394 xml.into_owned(),
3395 settings,
3396 context.budgets.execution.xml_parse_work(),
3397 )
3398 .map_err(|error| map_document_parse_error(error, settings))?;
3399 document.with_view(|view| {
3400 let external_resolver = resolver.for_external_document_view(view, resource_uri);
3401 let target = match fragment {
3402 Some(fragment) => external_resolver
3403 .node_for_same_document_reference(&format!("#{fragment}"))
3404 .map_err(|error| {
3405 SignatureVerificationPipelineError::from(
3406 ReferenceProcessingError::UriDereference(error),
3407 )
3408 })?
3409 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
3410 reason: "KeyInfoReference external target is missing or ambiguous",
3411 })?,
3412 _ => view.document().root_element(),
3413 };
3414 if !target.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
3415 return Err(SignatureVerificationPipelineError::InvalidStructure {
3416 reason: "KeyInfoReference external target must be KeyInfo",
3417 });
3418 }
3419 materialization.observe_encapsulation(
3420 target,
3421 &external_resolver,
3422 next_depth,
3423 )?;
3424 if let Some(mechanism) = &materialization.encapsulation {
3425 context
3426 .policy
3427 .key_establishment()
3428 .check_encapsulation(mechanism.algorithm)?;
3429 }
3430 super::parse::validate_x509_digest_policy(target, &|algorithm| {
3431 context.policy.check_digest_algorithm(algorithm)
3432 })
3433 .map_err(map_key_info_parse_error)?;
3434 let mut referenced = super::parse::parse_key_info_in_session(
3435 target,
3436 context.provider,
3437 context.budgets.execution.xml_base_resolution(),
3438 context.policy.resources(),
3439 Some(resource_uri),
3440 context.policy.certificate_signature_algorithms(),
3441 &mut context.budgets.key_info_parse.borrow_mut(),
3442 )
3443 .map_err(map_key_info_parse_error)?;
3444 let mut nested_outcome = visit(
3445 &mut referenced,
3446 &external_resolver,
3447 context,
3448 materialization,
3449 next_depth,
3450 )?;
3451 nested_outcome.merge(materialize_retrieval_methods_with_budgets(
3452 &mut referenced,
3453 &external_resolver,
3454 context.policy,
3455 context.provider,
3456 context.budgets,
3457 &mut materialization.candidate_work,
3458 )?);
3459 referenced.sources.retain(|source| {
3462 !matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. })
3463 });
3464 Ok((referenced, nested_outcome))
3465 })?
3466 };
3467 if is_same_document {
3468 nested_outcome.merge(visit(
3469 &mut referenced,
3470 resolver,
3471 context,
3472 materialization,
3473 next_depth,
3474 )?);
3475 }
3476 outcome.merge(nested_outcome);
3477 materialization.active.remove(&cycle_key);
3478 materialized.extend(referenced.sources);
3479 }
3480 key_info.sources = materialized;
3481 context
3482 .policy
3483 .resources()
3484 .validate_key_candidates(key_info.embedded_candidate_count())?;
3485 Ok(outcome)
3486 }
3487
3488 let mut context = KeyInfoReferenceMaterializationContext {
3489 policy,
3490 provider,
3491 budgets,
3492 };
3493 let depth = materialization.reference_depth_base;
3494 visit(key_info, resolver, &mut context, materialization, depth)
3495}
3496
3497fn map_key_info_xml_decode_error(
3498 error: crate::encoding::XmlEncodingError,
3499) -> SignatureVerificationPipelineError {
3500 match error {
3501 crate::encoding::XmlEncodingError::DecodedLimit { maximum, actual } => {
3502 SignatureVerificationPipelineError::Policy(
3503 crate::policy::PolicyViolation::ResourceLimit {
3504 resource: crate::policy::resource_name::XML_DOCUMENT,
3505 maximum,
3506 actual,
3507 },
3508 )
3509 }
3510 _ => SignatureVerificationPipelineError::InvalidStructure {
3511 reason: "KeyInfoReference external resource has an invalid XML encoding",
3512 },
3513 }
3514}
3515
3516pub struct SignatureKeyInfoMetadata {
3520 pub key_info: KeyInfo,
3522 pub encapsulation: Option<crate::provider::KeyEncapsulationAlgorithm>,
3524 pub(crate) cipher_value: Option<NodeId>,
3525}
3526
3527pub fn has_same_document_encapsulation(
3530 node: Node<'_, '_>,
3531 resolver: &UriReferenceResolver<'_>,
3532 resources: &crate::policy::ResourcePolicy,
3533) -> Result<bool, DsigError> {
3534 resources.validate()?;
3535 let execution = TransformExecutionBudget::from_resources(resources);
3536 has_same_document_encapsulation_with_budget(node, resolver, resources, &execution)
3537}
3538
3539pub(crate) fn has_same_document_encapsulation_with_budget(
3540 node: Node<'_, '_>,
3541 resolver: &UriReferenceResolver<'_>,
3542 resources: &crate::policy::ResourcePolicy,
3543 execution: &TransformExecutionBudget,
3544) -> Result<bool, DsigError> {
3545 has_encapsulation_with_budget::<crate::policy::SigningPolicy>(
3546 node, resolver, resources, execution, None,
3547 )
3548}
3549
3550pub(crate) fn has_signing_encapsulation_with_budget(
3551 node: Node<'_, '_>,
3552 resolver: &UriReferenceResolver<'_>,
3553 policy: &crate::policy::SigningPolicy,
3554 execution: &TransformExecutionBudget,
3555 backend: crate::XmlBackend,
3556) -> Result<bool, DsigError> {
3557 has_encapsulation_with_budget(
3558 node,
3559 resolver,
3560 &policy.resources,
3561 execution,
3562 Some((policy, backend)),
3563 )
3564}
3565
3566fn has_encapsulation_with_budget<P: KeyInfoReferencePolicy>(
3567 node: Node<'_, '_>,
3568 resolver: &UriReferenceResolver<'_>,
3569 resources: &crate::policy::ResourcePolicy,
3570 execution: &TransformExecutionBudget,
3571 external: Option<(&P, crate::XmlBackend)>,
3572) -> Result<bool, DsigError> {
3573 discover_encapsulation_with_budget(node, resolver, resources, execution, external, None)
3574}
3575
3576fn discover_encapsulation_with_budget<P: KeyInfoReferencePolicy>(
3577 node: Node<'_, '_>,
3578 resolver: &UriReferenceResolver<'_>,
3579 resources: &crate::policy::ResourcePolicy,
3580 execution: &TransformExecutionBudget,
3581 external: Option<(&P, crate::XmlBackend)>,
3582 materialization: Option<&mut KeyInfoMaterializationState>,
3583) -> Result<bool, DsigError> {
3584 if resolver.node_for_node_id(node.id()) != Some(node) {
3585 return Err(DsigError::InvalidStructure {
3586 reason: "KeyInfo and resolver belong to different documents",
3587 });
3588 }
3589 struct ExternalProbePath<'a> {
3590 uri: &'a str,
3591 parent: Option<&'a ExternalProbePath<'a>>,
3592 }
3593 struct ProbeContext<'a, P> {
3594 resources: &'a crate::policy::ResourcePolicy,
3595 external: Option<(&'a P, crate::XmlBackend)>,
3596 document_base: Option<&'a str>,
3597 external_path: Option<&'a ExternalProbePath<'a>>,
3598 }
3599 struct DiscoveryState<'a> {
3600 path: [Option<(usize, NodeId)>; crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING + 1],
3601 work: usize,
3602 materialization: Option<&'a mut KeyInfoMaterializationState>,
3603 }
3604 fn visit<P: KeyInfoReferencePolicy>(
3605 node: Node<'_, '_>,
3606 resolver: &UriReferenceResolver<'_>,
3607 context: &ProbeContext<'_, P>,
3608 state: &mut DiscoveryState<'_>,
3609 depth: usize,
3610 execution: &TransformExecutionBudget,
3611 ) -> Result<bool, DsigError> {
3612 let ProbeContext {
3613 resources,
3614 external,
3615 document_base,
3616 external_path,
3617 } = *context;
3618 resources.validate_key_info_reference_depth(depth)?;
3619 if depth > crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING {
3620 return Err(DsigError::InvalidStructure {
3621 reason: "KeyInfoReference safety depth exceeded",
3622 });
3623 }
3624 let identity = (std::ptr::from_ref(node.document()) as usize, node.id());
3627 if state.path[..depth].contains(&Some(identity)) {
3628 if state.materialization.is_some() {
3629 return Err(DsigError::InvalidStructure {
3630 reason: "KeyInfoReference cycle detected",
3631 });
3632 }
3633 return Ok(false);
3634 }
3635 state.path[depth] = Some(identity);
3636 let mut found = false;
3637 for child in node.children().filter(Node::is_element) {
3638 let work = match state.materialization.as_deref_mut() {
3639 Some(materialization) => &mut materialization.candidate_work,
3640 None => &mut state.work,
3641 };
3642 *work += 1;
3643 resources.validate_key_candidates(*work)?;
3644 if child.has_tag_name((
3645 crate::key_establishment::ENCAPSULATION_NS,
3646 "EncapsulationMechanism",
3647 )) {
3648 if state.materialization.is_none() {
3649 return Ok(true);
3650 }
3651 if let Some(materialization) = state.materialization.as_deref_mut() {
3652 let mechanism = crate::key_establishment::parse_encapsulation(
3653 child,
3654 materialization.encapsulation_template,
3655 )
3656 .map_err(|_| DsigError::InvalidStructure {
3657 reason: "invalid encapsulation mechanism in resolved KeyInfo",
3658 })?;
3659 materialization.record_encapsulation(mechanism, resolver, depth)?;
3660 if let (Some(mechanism), Some((policy, _))) =
3661 (&materialization.encapsulation, external)
3662 {
3663 policy
3664 .key_establishment()
3665 .check_encapsulation(mechanism.algorithm)?;
3666 }
3667 }
3668 found = true;
3669 continue;
3670 }
3671 if !child.has_tag_name(("http://www.w3.org/2009/xmldsig11#", "KeyInfoReference")) {
3672 continue;
3673 }
3674 let Some(uri) = child.attribute("URI") else {
3675 continue;
3676 };
3677 if uri.len() > super::parse::MAX_KEY_NAME_TEXT_LEN {
3678 continue;
3679 }
3680 if state.materialization.is_some()
3681 && let Some((policy, _)) = external
3682 && !policy.key_info_reference_source_enabled()
3683 {
3684 return Err(crate::policy::PolicyViolation::KeyTrust {
3685 reason: "KeyInfoReference key sources are disabled",
3686 }
3687 .into());
3688 }
3689 if !uri.is_empty() && !uri.starts_with('#') {
3690 let Some((policy, backend)) = external else {
3691 continue;
3692 };
3693 let resolved =
3697 crate::c14n::xml_base::resolve_uri_from_node_with_document_base_with_budget(
3698 child,
3699 uri,
3700 document_base,
3701 execution.xml_base_resolution(),
3702 )
3703 .map_err(|error| {
3704 ReferenceProcessingError::UriDereference(
3705 super::uri::map_xml_base_resolution_error(error),
3706 )
3707 })?;
3708 if !policy.key_info_reference_uris().allows(&resolved) {
3709 if state.materialization.is_some() {
3710 return Err(crate::policy::PolicyViolation::Uri {
3711 operation: "KeyInfoReference",
3712 reason: "URI class is disabled",
3713 }
3714 .into());
3715 }
3716 continue;
3717 }
3718 let mut ancestor = external_path;
3721 while let Some(entry) = ancestor {
3722 if entry.uri == resolved {
3723 break;
3724 }
3725 ancestor = entry.parent;
3726 }
3727 if ancestor.is_some() {
3728 if state.materialization.is_some() {
3729 return Err(DsigError::InvalidStructure {
3730 reason: "KeyInfoReference cycle detected",
3731 });
3732 }
3733 continue;
3734 }
3735 let external_entry = ExternalProbePath {
3736 uri: &resolved,
3737 parent: external_path,
3738 };
3739 let (resource, fragment) = resolved
3740 .split_once('#')
3741 .map_or((resolved.as_str(), None), |(resource, fragment)| {
3742 (resource, Some(fragment))
3743 });
3744 resources.validate_key_info_reference_depth(depth + 1)?;
3745 if depth == crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING {
3746 return Err(DsigError::InvalidStructure {
3747 reason: "KeyInfoReference safety depth exceeded",
3748 });
3749 }
3750 let bytes = match resolver.external_resource(resource) {
3751 Ok(Some(bytes)) => bytes,
3752 Err(TransformError::Policy(violation)) => {
3753 return Err(DsigError::Policy(violation));
3754 }
3755 _ => continue,
3756 };
3757 execution.xml_parse_work().charge_policy(bytes.len())?;
3758 let xml =
3759 crate::encoding::decode_xml_octets(bytes, resources.max_xml_document_bytes)
3760 .map_err(map_key_info_xml_decode_error)?;
3761 let settings = DocumentParseSettings::from_policy(policy.xml(), resources)
3762 .with_backend(backend);
3763 let document = XmlDocument::parse_with_settings_and_budget(
3764 xml.into_owned(),
3765 settings,
3766 execution.xml_parse_work(),
3767 )
3768 .map_err(|error| map_document_parse_error(error, settings))?;
3769 let target_found = document.with_view(|view| {
3770 let external_resolver = resolver.for_external_document_view(view, resource);
3771 let target = match fragment {
3772 Some(fragment) if !fragment.is_empty() => external_resolver
3773 .node_for_same_document_reference_with_budget(
3774 &resolved[resource.len()..],
3775 Some(execution.node_set_materialization()),
3776 )
3777 .map_err(ReferenceProcessingError::UriDereference)?,
3778 _ => Some(view.document().root_element()),
3779 };
3780 let Some(target) = target else {
3781 return Ok(false);
3782 };
3783 if !target.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
3784 return Ok(false);
3785 }
3786 visit(
3787 target,
3788 &external_resolver,
3789 &ProbeContext {
3790 document_base: Some(resource),
3791 external_path: Some(&external_entry),
3792 resources,
3793 external,
3794 },
3795 state,
3796 depth + 1,
3797 execution,
3798 )
3799 })?;
3800 if target_found {
3801 if state.materialization.is_none() {
3802 return Ok(true);
3803 }
3804 found = true;
3805 }
3806 continue;
3807 }
3808 if state.materialization.is_some()
3814 && let Some((policy, _)) = external
3815 && !policy.key_info_reference_uris().allows(uri)
3816 {
3817 return Err(crate::policy::PolicyViolation::Uri {
3818 operation: "KeyInfoReference",
3819 reason: "URI class is disabled",
3820 }
3821 .into());
3822 }
3823 let target = match resolver.node_for_same_document_reference_with_budget(
3824 uri,
3825 Some(execution.node_set_materialization()),
3826 ) {
3827 Ok(Some(target)) => target,
3828 Err(TransformError::Policy(violation)) => return Err(DsigError::Policy(violation)),
3829 _ => continue,
3830 };
3831 if target.has_tag_name((XMLDSIG_NS, "KeyInfo"))
3832 && visit(target, resolver, context, state, depth + 1, execution)?
3833 {
3834 if state.materialization.is_none() {
3835 return Ok(true);
3836 }
3837 found = true;
3838 }
3839 }
3840 Ok(found)
3841 }
3842 visit(
3843 node,
3844 resolver,
3845 &ProbeContext {
3846 resources,
3847 external,
3848 document_base: None,
3849 external_path: None,
3850 },
3851 &mut DiscoveryState {
3852 path: [None; crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING + 1],
3853 work: 0,
3854 materialization,
3855 },
3856 0,
3857 execution,
3858 )
3859}
3860
3861fn inspect_signature_key_info_with_budgets<P: KeyInfoReferencePolicy>(
3862 node: Node<'_, '_>,
3863 resolver: &UriReferenceResolver<'_>,
3864 policy: &P,
3865 provider: &dyn crate::provider::CryptoProvider,
3866 budgets: &mut RetrievalMaterializationBudgets<'_>,
3867 template: bool,
3868 retain_recipient_hints: bool,
3869) -> Result<SignatureKeyInfoMetadata, DsigError> {
3870 if resolver.node_for_node_id(node.id()) != Some(node) {
3871 return Err(DsigError::InvalidStructure {
3872 reason: "KeyInfo and resolver belong to different documents",
3873 });
3874 }
3875 let mut materialization = KeyInfoMaterializationState {
3876 collect_encapsulation: true,
3877 encapsulation_template: template,
3878 retain_encapsulation_target: true,
3879 ..Default::default()
3880 };
3881 materialization.observe_encapsulation(node, resolver, 0)?;
3882 if let Some(mechanism) = &materialization.encapsulation {
3883 policy
3884 .key_establishment()
3885 .check_encapsulation(mechanism.algorithm)?;
3886 }
3887 let mut key_info = super::parse::parse_key_info_in_session(
3888 node,
3889 provider,
3890 budgets.execution.xml_base_resolution(),
3891 policy.resources(),
3892 None,
3893 policy.certificate_signature_algorithms(),
3894 &mut budgets.key_info_parse.borrow_mut(),
3895 )
3896 .map_err(map_key_info_parse_error)?;
3897 materialize_key_info_references_with_budgets(
3898 &mut key_info,
3899 resolver,
3900 policy,
3901 provider,
3902 budgets,
3903 &mut materialization,
3904 )?;
3905 let encapsulation = materialization
3906 .encapsulation
3907 .as_ref()
3908 .map(|value| value.algorithm);
3909 let cipher_value =
3910 if let Some((resource, cipher, recipient)) = materialization.encapsulation_target.take() {
3911 if resource != resolver.traversal_document_identity() {
3914 return Err(DsigError::InvalidStructure {
3915 reason: "recipient metadata requires its original document context",
3916 });
3917 }
3918 if retain_recipient_hints {
3919 let recipient =
3920 resolver
3921 .node_for_node_id(recipient)
3922 .ok_or(DsigError::InvalidStructure {
3923 reason: "recipient KeyInfo target is unavailable",
3924 })?;
3925 key_info = super::parse::parse_key_info_in_session(
3926 recipient,
3927 provider,
3928 budgets.execution.xml_base_resolution(),
3929 policy.resources(),
3930 None,
3931 policy.certificate_signature_algorithms(),
3932 &mut budgets.key_info_parse.borrow_mut(),
3933 )
3934 .map_err(map_key_info_parse_error)?;
3935 materialization.reference_depth_base = materialization.encapsulation_depth;
3938 materialize_key_info_references_with_budgets(
3939 &mut key_info,
3940 resolver,
3941 policy,
3942 provider,
3943 budgets,
3944 &mut materialization,
3945 )?;
3946 }
3947 Some(cipher)
3948 } else {
3949 None
3950 };
3951 Ok(SignatureKeyInfoMetadata {
3952 key_info,
3953 encapsulation,
3954 cipher_value,
3955 })
3956}
3957
3958pub(crate) fn inspect_signing_key_info_with_budgets(
3959 node: Node<'_, '_>,
3960 resolver: &UriReferenceResolver<'_>,
3961 policy: &crate::policy::SigningPolicy,
3962 provider: &dyn crate::provider::CryptoProvider,
3963 shared_budgets: (&TransformExecutionBudget, &mut XPathSignatureParseBudget),
3964 xml_backend: crate::XmlBackend,
3965) -> Result<SignatureKeyInfoMetadata, DsigError> {
3966 inspect_signature_key_info_with_budgets(
3967 node,
3968 resolver,
3969 policy,
3970 provider,
3971 &mut RetrievalMaterializationBudgets {
3972 key_info_parse: &RefCell::new(super::parse::KeyInfoParseUsage::new(&policy.resources)),
3973 xpath_parse: shared_budgets.1,
3974 execution: shared_budgets.0,
3975 resources: &policy.resources,
3976 certificate_signature_algorithms: None,
3977 xml_backend,
3978 },
3979 true,
3980 false,
3981 )
3982}
3983
3984fn inspect_signature_key_info_for_policy<P: KeyInfoReferencePolicy>(
3985 node: Node<'_, '_>,
3986 resolver: UriReferenceResolver<'_>,
3987 policy: &P,
3988 provider: &dyn crate::provider::CryptoProvider,
3989 xml_backend: crate::XmlBackend,
3990 template: bool,
3991) -> Result<SignatureKeyInfoMetadata, DsigError> {
3992 let resolver = resolver.with_external_resource_limits(
3993 policy.resources().max_external_resource_bytes,
3994 policy.resources().max_external_resource_total_bytes,
3995 );
3996 let execution = TransformExecutionBudget::from_resources(policy.resources());
3997 let mut xpath_parse = XPathSignatureParseBudget::for_execution(policy.resources(), &execution);
3998 inspect_signature_key_info_with_budgets(
3999 node,
4000 &resolver,
4001 policy,
4002 provider,
4003 &mut RetrievalMaterializationBudgets {
4004 key_info_parse: &RefCell::new(super::parse::KeyInfoParseUsage::new(policy.resources())),
4005 xpath_parse: &mut xpath_parse,
4006 execution: &execution,
4007 resources: policy.resources(),
4008 certificate_signature_algorithms: policy.certificate_signature_algorithms(),
4009 xml_backend,
4010 },
4011 template,
4012 true,
4013 )
4014}
4015
4016pub fn inspect_signing_key_info(
4019 node: Node<'_, '_>,
4020 resolver: UriReferenceResolver<'_>,
4021 policy: &crate::policy::SigningPolicy,
4022 provider: &dyn crate::provider::CryptoProvider,
4023 xml_backend: crate::XmlBackend,
4024) -> Result<SignatureKeyInfoMetadata, DsigError> {
4025 policy.validate()?;
4026 inspect_signature_key_info_for_policy(node, resolver, policy, provider, xml_backend, true)
4027}
4028
4029pub fn inspect_verification_key_info(
4034 node: Node<'_, '_>,
4035 resolver: UriReferenceResolver<'_>,
4036 policy: &crate::policy::VerificationPolicy,
4037 provider: &dyn crate::provider::CryptoProvider,
4038 xml_backend: crate::XmlBackend,
4039) -> Result<SignatureKeyInfoMetadata, DsigError> {
4040 policy.validate()?;
4041 inspect_signature_key_info_for_policy(node, resolver, policy, provider, xml_backend, false)
4042}
4043
4044pub fn inspect_pinned_verification_key_info(
4050 node: Node<'_, '_>,
4051 resolver: UriReferenceResolver<'_>,
4052 policy: &crate::policy::VerificationPolicy,
4053 provider: &dyn crate::provider::CryptoProvider,
4054 xml_backend: crate::XmlBackend,
4055) -> Result<Option<SignatureKeyInfoMetadata>, DsigError> {
4056 policy.validate()?;
4057 let resolver = resolver.with_external_resource_limits(
4058 policy.resources.max_external_resource_bytes,
4059 policy.resources.max_external_resource_total_bytes,
4060 );
4061 let execution = TransformExecutionBudget::from_resources(&policy.resources);
4062 if !has_encapsulation_with_budget(
4063 node,
4064 &resolver,
4065 &policy.resources,
4066 &execution,
4067 Some((policy, xml_backend)),
4068 )? {
4069 return Ok(None);
4070 }
4071 let mut xpath_parse = XPathSignatureParseBudget::for_execution(&policy.resources, &execution);
4072 inspect_signature_key_info_with_budgets(
4073 node,
4074 &resolver,
4075 policy,
4076 provider,
4077 &mut RetrievalMaterializationBudgets {
4078 key_info_parse: &RefCell::new(super::parse::KeyInfoParseUsage::new(&policy.resources)),
4079 xpath_parse: &mut xpath_parse,
4080 execution: &execution,
4081 resources: &policy.resources,
4082 certificate_signature_algorithms: policy.certificate_signature_algorithms(),
4083 xml_backend,
4084 },
4085 false,
4086 true,
4087 )
4088 .map(Some)
4089}
4090
4091fn materialize_key_info_references_for_policy<P: KeyInfoReferencePolicy>(
4092 key_info: &mut KeyInfo,
4093 resolver: UriReferenceResolver<'_>,
4094 policy: &P,
4095 provider: &dyn crate::provider::CryptoProvider,
4096 xml_backend: crate::XmlBackend,
4097) -> Result<(), DsigError> {
4098 let resolver = resolver.with_external_resource_limits(
4099 policy.resources().max_external_resource_bytes,
4100 policy.resources().max_external_resource_total_bytes,
4101 );
4102 let execution_budget = TransformExecutionBudget::from_resources(policy.resources());
4103 let mut xpath_parse_budget =
4104 XPathSignatureParseBudget::for_execution(policy.resources(), &execution_budget);
4105 let mut budgets = RetrievalMaterializationBudgets {
4106 key_info_parse: &RefCell::new(super::parse::KeyInfoParseUsage::new(policy.resources())),
4107 xpath_parse: &mut xpath_parse_budget,
4108 execution: &execution_budget,
4109 resources: policy.resources(),
4110 certificate_signature_algorithms: policy.certificate_signature_algorithms(),
4111 xml_backend,
4112 };
4113 let mut materialization = KeyInfoMaterializationState::default();
4114 materialize_key_info_references_with_budgets(
4117 key_info,
4118 &resolver,
4119 policy,
4120 provider,
4121 &mut budgets,
4122 &mut materialization,
4123 )?;
4124 Ok(())
4125}
4126
4127pub fn materialize_signing_key_info_references(
4144 key_info: &mut KeyInfo,
4145 resolver: UriReferenceResolver<'_>,
4146 policy: &crate::policy::SigningPolicy,
4147 provider: &dyn crate::provider::CryptoProvider,
4148 xml_backend: crate::XmlBackend,
4149) -> Result<(), DsigError> {
4150 policy.validate()?;
4151 materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
4152}
4153
4154pub fn materialize_verification_key_info_references(
4169 key_info: &mut KeyInfo,
4170 resolver: UriReferenceResolver<'_>,
4171 policy: &crate::policy::VerificationPolicy,
4172 provider: &dyn crate::provider::CryptoProvider,
4173 xml_backend: crate::XmlBackend,
4174) -> Result<(), DsigError> {
4175 policy.validate()?;
4176 materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
4177}
4178
4179fn materialize_retrieval_methods_with_budgets<P: KeyInfoReferencePolicy>(
4180 key_info: &mut KeyInfo,
4181 resolver: &UriReferenceResolver<'_>,
4182 policy: &P,
4183 provider: &dyn crate::provider::CryptoProvider,
4184 budgets: &mut RetrievalMaterializationBudgets<'_>,
4185 candidate_work: &mut usize,
4186) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
4187 let allowed_uri_types = policy.retrieval_method_uris();
4188 let allowed_transforms = policy.allowed_transforms();
4189 let retrieval_count = key_info
4190 .sources
4191 .iter()
4192 .filter(|source| matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. }))
4193 .count();
4194 if retrieval_count > MAX_RETRIEVAL_METHOD_COUNT {
4195 return Err(SignatureVerificationPipelineError::InvalidStructure {
4196 reason: "KeyInfo contains too many RetrievalMethod elements",
4197 });
4198 }
4199
4200 let mut total_binary_len = existing_x509_binary_len(key_info)?;
4201 let mut seen = HashSet::new();
4202 let mut materialized = Vec::with_capacity(key_info.sources.len());
4203 let mut outcome = RetrievalMaterialization::default();
4204 for source in std::mem::take(&mut key_info.sources) {
4205 let super::parse::KeyInfoSource::RetrievalMethod {
4206 uri: resolved_uri,
4207 resource_type,
4208 transforms,
4209 } = source
4210 else {
4211 materialized.push(source);
4212 continue;
4213 };
4214
4215 let identity = (
4216 resolved_uri.clone(),
4217 resource_type.clone(),
4218 transforms.clone(),
4219 );
4220 if !seen.insert(identity) {
4221 continue;
4222 }
4223
4224 if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#rawX509Certificate")
4225 {
4226 if transforms != RetrievalMethodTransforms::None
4227 || classify_uri(&resolved_uri) != UriClass::External
4228 {
4229 return Err(SignatureVerificationPipelineError::InvalidStructure {
4230 reason: "raw X509 RetrievalMethod requires an untransformed external URI",
4231 });
4232 }
4233 if !allowed_uri_types.allows(&resolved_uri) {
4234 return Err(crate::policy::PolicyViolation::Uri {
4235 operation: "verification",
4236 reason: "retrieval method URI class is not permitted",
4237 }
4238 .into());
4239 }
4240 let certificate = resolver.external_resource(&resolved_uri).map_err(|error| {
4241 SignatureVerificationPipelineError::from(ReferenceProcessingError::Transform(error))
4242 })?;
4243 let Some(certificate) = certificate else {
4244 outcome.deferred_error.get_or_insert_with(|| {
4245 SignatureVerificationPipelineError::Reference(
4246 ReferenceProcessingError::Transform(super::TransformError::UnsupportedUri(
4247 resolved_uri.clone(),
4248 )),
4249 )
4250 });
4251 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
4252 uri: resolved_uri,
4253 resource_type,
4254 transforms,
4255 });
4256 continue;
4257 };
4258 *candidate_work = candidate_work.saturating_add(1);
4259 budgets.resources.validate_key_candidates(*candidate_work)?;
4260 if certificate.len() > MAX_X509_DECODED_BINARY_LEN {
4261 return Err(SignatureVerificationPipelineError::InvalidStructure {
4262 reason: "raw X509 RetrievalMethod certificate exceeds maximum allowed length",
4263 });
4264 }
4265 add_retrieval_binary_usage(&mut total_binary_len, certificate.len())?;
4266 let parsed = match super::parse::parse_x509_certificate_in(
4267 certificate,
4268 &budgets.key_info_parse.borrow().certificates,
4269 ) {
4270 Ok(parsed) => parsed,
4271 Err(error) => {
4272 let error = map_key_info_parse_error(error);
4273 if matches!(error, SignatureVerificationPipelineError::Policy(_)) {
4274 return Err(error);
4275 }
4276 outcome.deferred_error.get_or_insert(error);
4277 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
4278 uri: resolved_uri,
4279 resource_type,
4280 transforms,
4281 });
4282 continue;
4283 }
4284 };
4285 materialized.push(super::parse::KeyInfoSource::X509Data(
4286 super::parse::X509DataInfo {
4287 certificates: vec![certificate.to_vec()],
4288 parsed_certificates: vec![parsed],
4289 certificate_chain: vec![0],
4290 ..super::parse::X509DataInfo::default()
4291 },
4292 ));
4293 } else if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#X509Data") {
4294 if !allowed_uri_types.allows(&resolved_uri) {
4295 return Err(crate::policy::PolicyViolation::Uri {
4296 operation: "verification",
4297 reason: "retrieval method URI class is not permitted",
4298 }
4299 .into());
4300 }
4301 let target = resolver
4302 .node_for_same_document_reference(&resolved_uri)
4303 .map_err(ReferenceProcessingError::Transform)?;
4304 let Some(target) = target else {
4305 outcome.deferred_error.get_or_insert(
4306 SignatureVerificationPipelineError::InvalidStructure {
4307 reason: "X509Data RetrievalMethod target is missing or ambiguous",
4308 },
4309 );
4310 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
4311 uri: resolved_uri,
4312 resource_type,
4313 transforms,
4314 });
4315 continue;
4316 };
4317 let node = match transforms {
4318 RetrievalMethodTransforms::None
4319 if target.has_tag_name((XMLDSIG_NS, "X509Data")) =>
4320 {
4321 target
4322 }
4323 RetrievalMethodTransforms::None => {
4324 return Err(SignatureVerificationPipelineError::InvalidStructure {
4325 reason: "untransformed X509Data RetrievalMethod must target X509Data directly",
4326 });
4327 }
4328 RetrievalMethodTransforms::X509DataNodeSetFilter {
4329 expression,
4330 namespaces,
4331 } => {
4332 enforce_transform_allowed(allowed_transforms, XPATH_TRANSFORM_URI)?;
4333 budgets
4334 .xpath_parse
4335 .validate_expression(&expression)
4336 .map_err(ReferenceProcessingError::Transform)?;
4337 budgets
4338 .xpath_parse
4339 .validate_namespaces(&namespaces)
4340 .map_err(ReferenceProcessingError::Transform)?;
4341 select_retrieved_x509_data_root(target, budgets.execution)?
4342 }
4343 RetrievalMethodTransforms::Unsupported => {
4344 return Err(SignatureVerificationPipelineError::InvalidStructure {
4345 reason: "X509Data RetrievalMethod contains unsupported transforms",
4346 });
4347 }
4348 };
4349 super::parse::validate_x509_digest_policy(node, &|algorithm| {
4350 policy.check_digest_algorithm(algorithm)
4351 })
4352 .map_err(map_key_info_parse_error)?;
4353 let data = parse_x509_data_dispatch_with_budget_and_provider(
4354 node,
4355 &mut total_binary_len,
4356 candidate_work,
4357 provider,
4358 budgets.resources,
4359 budgets.certificate_signature_algorithms,
4360 &budgets.key_info_parse.borrow().certificates,
4361 )
4362 .map_err(map_key_info_parse_error)?;
4363 materialized.push(super::parse::KeyInfoSource::X509Data(data));
4364 } else {
4365 materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
4366 uri: resolved_uri,
4367 resource_type,
4368 transforms,
4369 });
4370 }
4371 }
4372 key_info.sources = materialized;
4373 Ok(outcome)
4374}
4375
4376fn select_retrieved_x509_data_root<'a, 'input>(
4377 target: Node<'a, 'input>,
4378 execution_budget: &TransformExecutionBudget,
4379) -> Result<Node<'a, 'input>, SignatureVerificationPipelineError> {
4380 let context_nodes = NodeSet::ensure_subtree_materialization_fits_with_budget(
4385 target,
4386 false,
4387 execution_budget.node_set_materialization(),
4388 )
4389 .map_err(ReferenceProcessingError::Transform)?;
4390 execution_budget
4391 .validate_xpath_context_evaluations(context_nodes)
4392 .map_err(ReferenceProcessingError::Transform)?;
4393 execution_budget
4394 .charge_xpath_work(context_nodes)
4395 .map_err(ReferenceProcessingError::Transform)?;
4396 execution_budget
4397 .charge_node_filter_work(context_nodes)
4398 .map_err(ReferenceProcessingError::Transform)?;
4399 let mut root = None;
4400 for candidate in target.descendants() {
4401 if !candidate.is_element()
4402 || candidate.tag_name().namespace() != Some(XMLDSIG_NS)
4403 || candidate.tag_name().name() != "X509Data"
4404 {
4405 continue;
4406 }
4407 if root.replace(candidate).is_some() {
4408 return Err(SignatureVerificationPipelineError::InvalidStructure {
4409 reason: "X509Data RetrievalMethod selected multiple X509Data elements",
4410 });
4411 }
4412 }
4413 root.ok_or(SignatureVerificationPipelineError::InvalidStructure {
4414 reason: "X509Data RetrievalMethod selected no X509Data element",
4415 })
4416}
4417
4418#[cfg(test)]
4419fn materialize_retrieval_methods(
4420 key_info: &mut KeyInfo,
4421 resolver: &UriReferenceResolver<'_>,
4422 allowed_uri_types: UriTypeSet,
4423 allowed_transforms: Option<&HashSet<String>>,
4424 provider: &dyn crate::provider::CryptoProvider,
4425) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
4426 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
4427 let execution_budget = TransformExecutionBudget::default();
4428 let resources = crate::policy::ResourcePolicy::default();
4429 let mut budgets = RetrievalMaterializationBudgets {
4430 key_info_parse: &RefCell::new(super::parse::KeyInfoParseUsage::new(&resources)),
4431 xpath_parse: &mut xpath_parse_budget,
4432 execution: &execution_budget,
4433 resources: &resources,
4434 certificate_signature_algorithms: None,
4435 xml_backend: crate::XmlBackend::default(),
4436 };
4437 let mut candidate_work = key_info.embedded_candidate_count();
4438 let mut policy = crate::policy::VerificationPolicy::default();
4439 policy.uris.retrieval_methods = allowed_uri_types;
4440 policy.transforms.allowed_algorithms = allowed_transforms.cloned();
4441 materialize_retrieval_methods_with_budgets(
4442 key_info,
4443 resolver,
4444 &policy,
4445 provider,
4446 &mut budgets,
4447 &mut candidate_work,
4448 )
4449}
4450
4451fn existing_x509_binary_len(
4452 key_info: &KeyInfo,
4453) -> Result<usize, SignatureVerificationPipelineError> {
4454 let mut total = 0usize;
4455 for source in &key_info.sources {
4456 if let super::parse::KeyInfoSource::X509Data(info) = source {
4457 for len in info
4458 .certificates
4459 .iter()
4460 .chain(&info.skis)
4461 .chain(&info.crls)
4462 .map(Vec::len)
4463 .chain(info.digests.iter().map(|(_, digest)| digest.len()))
4464 {
4465 add_retrieval_binary_usage(&mut total, len)?;
4466 }
4467 }
4468 }
4469 Ok(total)
4470}
4471
4472fn add_retrieval_binary_usage(
4473 total: &mut usize,
4474 delta: usize,
4475) -> Result<(), SignatureVerificationPipelineError> {
4476 *total =
4477 total
4478 .checked_add(delta)
4479 .ok_or(SignatureVerificationPipelineError::InvalidStructure {
4480 reason: "RetrievalMethod X509Data binary length overflow",
4481 })?;
4482 if *total > MAX_X509_DATA_TOTAL_BINARY_LEN {
4483 return Err(SignatureVerificationPipelineError::InvalidStructure {
4484 reason: "RetrievalMethod X509Data exceeds maximum aggregate binary length",
4485 });
4486 }
4487 Ok(())
4488}
4489
4490fn manifest_reference_failure_reason(
4491 error: ReferenceProcessingError,
4492 ref_index: usize,
4493) -> FailureReason {
4494 match error.into_policy_violation() {
4495 Ok(_) => FailureReason::ReferencePolicyViolation { ref_index },
4496 Err(_) => FailureReason::ReferenceProcessingFailure { ref_index },
4497 }
4498}
4499
4500struct CompiledManifestReference {
4501 index: usize,
4502 reference: Reference,
4503 reference_node_id: NodeId,
4504}
4505
4506struct CompiledManifestInvalid {
4507 result: ReferenceResult,
4508}
4509
4510enum ScheduledManifestReference {
4511 Parsed {
4512 node: OperationNodeId,
4513 reference: CompiledManifestReference,
4514 },
4515 Invalid {
4516 node: OperationNodeId,
4517 invalid: CompiledManifestInvalid,
4518 },
4519}
4520
4521impl ScheduledManifestReference {
4522 fn index(&self) -> usize {
4523 match self {
4524 Self::Parsed { reference, .. } => reference.index,
4525 Self::Invalid { invalid, .. } => invalid.result.reference_index,
4526 }
4527 }
4528}
4529
4530struct ManifestVerification<'context, 'inputs, C> {
4531 context: &'context VerifyContext<'inputs>,
4532 capture: &'context C,
4533}
4534
4535fn process_authenticated_manifest_references<C: VerificationCapture>(
4536 operation: &mut OperationExecutionContext<
4537 &crate::policy::VerificationPolicy,
4538 &VerificationOperationBudgets,
4539 >,
4540 view: DocumentView<'_>,
4541 signature_node: Node<'_, '_>,
4542 resolver: &UriReferenceResolver<'_>,
4543 verification: ManifestVerification<'_, '_, C>,
4544 mut remaining_reference_capacity: usize,
4545 crypto: OperationNodeId,
4546) -> Result<(Vec<ReferenceResult>, Vec<OperationNodeId>), SignatureVerificationPipelineError> {
4547 let ctx = verification.context;
4548 let capture = verification.capture;
4549 let mut processed_manifests = HashSet::new();
4550 let mut next_reference_index = 0usize;
4551 let mut frontier_index = 0usize;
4552 let mut completed_digests = Vec::new();
4553 let mut results = Vec::new();
4554 loop {
4555 operation.extend();
4556 let discovery = operation.add_node(
4557 OperationNodeKind::Manifest {
4558 index: frontier_index,
4559 },
4560 OperationStage::AuthenticatedDependency,
4561 None,
4562 );
4563 operation
4564 .add_dependency(discovery, crypto)
4565 .map_err(map_verification_plan_error)?;
4566 for digest in &completed_digests {
4567 operation
4568 .add_dependency(discovery, *digest)
4569 .map_err(map_verification_plan_error)?;
4570 }
4571 operation.compile().map_err(map_verification_plan_error)?;
4572 let parsed = operation.run(discovery, || {
4573 let mut xpath_parse = operation.budgets().xpath_parse.borrow_mut();
4574 let mut state = ManifestDiscoveryState {
4575 processed: &mut processed_manifests,
4576 remaining_capacity: &mut remaining_reference_capacity,
4577 next_reference_index: &mut next_reference_index,
4578 xpath_parse: &mut xpath_parse,
4579 };
4580 parse_manifest_references(
4581 signature_node,
4582 operation,
4583 view,
4584 &mut state,
4585 ctx.allowed_transform_uris(),
4586 )
4587 })?;
4588 let discovered = parsed.references.len() + parsed.invalid_results.len();
4589 if discovered == 0 {
4590 break;
4591 }
4592
4593 operation.extend();
4594 let mut scheduled = Vec::with_capacity(discovered);
4595 for item in parsed.references {
4596 let resource = operation.budgets().resource_identity_for_reference(
4597 &item.reference,
4598 item.index,
4599 resolver,
4600 view,
4601 resolver.node_for_node_id(item.reference_node_id),
4602 );
4603 let node = operation.add_node(
4604 OperationNodeKind::Digest { index: item.index },
4605 OperationStage::AuthenticatedDependency,
4606 Some(resource.clone()),
4607 );
4608 operation
4609 .add_dependency(node, discovery)
4610 .map_err(map_verification_plan_error)?;
4611 scheduled.push(ScheduledManifestReference::Parsed {
4612 node,
4613 reference: item,
4614 });
4615 }
4616 for item in parsed.invalid_results {
4617 let node = operation.add_node(
4618 OperationNodeKind::Digest {
4619 index: item.result.reference_index,
4620 },
4621 OperationStage::AuthenticatedDependency,
4622 None,
4623 );
4624 operation
4625 .add_dependency(node, discovery)
4626 .map_err(map_verification_plan_error)?;
4627 scheduled.push(ScheduledManifestReference::Invalid {
4628 node,
4629 invalid: item,
4630 });
4631 }
4632 scheduled.sort_by_key(ScheduledManifestReference::index);
4633 operation.compile().map_err(map_verification_plan_error)?;
4634
4635 for scheduled_reference in scheduled {
4636 let (node, result) = match scheduled_reference {
4637 ScheduledManifestReference::Invalid { node, invalid } => {
4638 let result = operation.run(node, || {
4639 Ok::<_, SignatureVerificationPipelineError>(invalid.result)
4640 })?;
4641 (node, result)
4642 }
4643 ScheduledManifestReference::Parsed {
4644 node,
4645 reference: compiled,
4646 } => {
4647 let observed = operation.budgets().resource_identity_for_reference(
4648 &compiled.reference,
4649 compiled.index,
4650 resolver,
4651 view,
4652 resolver.node_for_node_id(compiled.reference_node_id),
4653 );
4654 let result = operation.run_with_resource(node, &observed, || {
4655 let reference = &compiled.reference;
4656 let budgets = operation.budgets();
4657 let execution = ReferenceExecutionContext {
4658 store_pre_digest: ctx.store_pre_digest,
4659 transform_options: ctx.transform_options(),
4660 transform_budget: &budgets.transforms,
4661 canonicalized_data_budget: &budgets.canonicalized,
4662 provider: ctx.provider,
4663 capture,
4664 };
4665 let mut result = if execution.transform_budget.remaining_c14n_output() == 0
4666 || super::transforms::validate_relationship_chain(
4667 &reference.transforms,
4668 ctx.policy.transforms.opc_relationship_edition,
4669 )
4670 .is_err()
4671 || reference.transforms.len()
4672 > ctx.policy.resources.max_transforms_per_reference
4673 || ctx
4674 .policy
4675 .check_digest_algorithm(reference.digest_method)
4676 .is_err()
4677 {
4678 manifest_reference_invalid_result(
4679 reference,
4680 compiled.index,
4681 FailureReason::ReferencePolicyViolation {
4682 ref_index: compiled.index,
4683 },
4684 )
4685 } else {
4686 match enforce_reference_policies(
4687 std::slice::from_ref(reference),
4688 ctx.policy.uris.references,
4689 ctx.allowed_transform_uris(),
4690 ) {
4691 Ok(()) => process_reference_with_options(
4692 reference,
4693 resolver,
4694 signature_node,
4695 ReferenceSet::Manifest,
4696 compiled.index,
4697 resolver.node_for_node_id(compiled.reference_node_id),
4698 &execution,
4699 )
4700 .unwrap_or_else(|error| {
4701 manifest_reference_invalid_result(
4702 reference,
4703 compiled.index,
4704 manifest_reference_failure_reason(error, compiled.index),
4705 )
4706 }),
4707 Err(SignatureVerificationPipelineError::Policy(_)) => {
4708 manifest_reference_invalid_result(
4709 reference,
4710 compiled.index,
4711 FailureReason::ReferencePolicyViolation {
4712 ref_index: compiled.index,
4713 },
4714 )
4715 }
4716 Err(_) => manifest_reference_invalid_result(
4717 reference,
4718 compiled.index,
4719 FailureReason::ReferenceProcessingFailure {
4720 ref_index: compiled.index,
4721 },
4722 ),
4723 }
4724 };
4725 bind_reference_evidence(
4726 &mut result,
4727 reference,
4728 view,
4729 resolver.node_for_node_id(compiled.reference_node_id),
4730 &observed,
4731 );
4732 if result.status == DsigStatus::Valid
4733 && reference
4734 .transforms
4735 .iter()
4736 .all(transform_preserves_manifest_structure)
4737 && let Some(uri) = reference.uri.as_deref()
4738 && let Ok(Some(target)) =
4739 resolver.node_id_for_same_document_reference(uri)
4740 {
4741 operation.authenticate(view.node_identity_by_id(target));
4742 }
4743 Ok::<_, SignatureVerificationPipelineError>(result)
4744 })?;
4745 (node, result)
4746 }
4747 };
4748 let accepted = result.status == DsigStatus::Valid;
4749 operation.set_outcome(
4750 node,
4751 accepted,
4752 if accepted {
4753 OperationDecisionReason::ReferenceDigestVerified
4754 } else {
4755 OperationDecisionReason::ReferenceDigestRejected
4756 },
4757 );
4758 completed_digests.push(node);
4759 results.push(result);
4760 }
4761 frontier_index += 1;
4762 }
4763 results.sort_by_key(|result| result.reference_index);
4764 Ok((results, completed_digests))
4765}
4766
4767fn manifest_reference_invalid_result(
4768 reference: &Reference,
4769 index: usize,
4770 reason: FailureReason,
4771) -> ReferenceResult {
4772 ReferenceResult {
4773 reference_identity: None,
4774 target_identity: None,
4775 external_resource_fingerprint: None,
4776 coverage: ReferenceCoverage::TransformedData,
4777 reference_set: ReferenceSet::Manifest,
4778 reference_index: index,
4779 uri: reference
4780 .uri
4781 .clone()
4782 .unwrap_or_else(|| "<omitted>".to_owned()),
4783 digest_algorithm: reference.digest_method,
4784 status: DsigStatus::Invalid(reason),
4785 pre_digest_data: None,
4786 }
4787}
4788
4789fn parse_manifest_references(
4790 signature_node: Node<'_, '_>,
4791 operation: &OperationExecutionContext<
4792 &crate::policy::VerificationPolicy,
4793 &VerificationOperationBudgets,
4794 >,
4795 view: DocumentView<'_>,
4796 state: &mut ManifestDiscoveryState<'_>,
4797 allowed_transforms: Option<&HashSet<String>>,
4798) -> Result<ParsedManifestReferences, SignatureVerificationPipelineError> {
4799 let mut references = Vec::new();
4800 let mut invalid = Vec::new();
4801 let document = signature_node.document();
4806 let scan = &operation.budgets().transforms;
4807 for manifest_node in document.descendants() {
4808 scan.charge_node_filter_work(1)
4809 .map_err(ReferenceProcessingError::Transform)?;
4810 if !manifest_node.has_tag_name((XMLDSIG_NS, "Manifest"))
4811 || state.processed.contains(&manifest_node.id())
4812 {
4813 continue;
4814 }
4815 let mut manifest_is_signed = false;
4816 for ancestor in manifest_node.ancestors() {
4817 scan.charge_node_filter_work(1)
4818 .map_err(ReferenceProcessingError::Transform)?;
4819 if operation.is_authenticated(view.node_identity(ancestor)) {
4820 manifest_is_signed = true;
4821 break;
4822 }
4823 }
4824 if !manifest_is_signed {
4827 continue;
4828 }
4829 if !state.processed.insert(manifest_node.id()) {
4830 continue;
4831 }
4832 let mut manifest_children = Vec::new();
4833 for child in manifest_node.children() {
4834 if child.is_text()
4835 && child.text().is_some_and(|text| {
4836 text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
4837 })
4838 {
4839 return Err(SignatureVerificationPipelineError::InvalidStructure {
4840 reason: "Manifest contains non-whitespace mixed content",
4841 });
4842 }
4843 if child.is_element() {
4844 manifest_children.push(child);
4845 }
4846 }
4847 if manifest_children.is_empty() {
4848 return Err(SignatureVerificationPipelineError::InvalidStructure {
4849 reason: "Manifest must contain at least one ds:Reference element child",
4850 });
4851 }
4852 for child in manifest_children {
4853 if child.tag_name().namespace() != Some(XMLDSIG_NS)
4854 || child.tag_name().name() != "Reference"
4855 {
4856 return Err(SignatureVerificationPipelineError::InvalidStructure {
4857 reason: "Manifest must contain only ds:Reference element children",
4858 });
4859 }
4860 if *state.remaining_capacity == 0 {
4861 return Err(SignatureVerificationPipelineError::InvalidStructure {
4862 reason: "signed Manifests exceed the per-signature Reference limit",
4863 });
4864 }
4865 *state.remaining_capacity -= 1;
4866 let reference_index = *state.next_reference_index;
4867 *state.next_reference_index += 1;
4868 match parse_reference_with_xpath_budget(child, state.xpath_parse) {
4869 Ok(reference) => references.push(CompiledManifestReference {
4870 index: reference_index,
4871 reference,
4872 reference_node_id: child.id(),
4873 }),
4874 Err(ParseError::Transform(super::TransformError::UnsupportedTransform(uri))) => {
4875 let digest_algorithm =
4876 reference_digest_method(child).map_err(map_manifest_parse_error)?;
4877 let reason =
4878 if allowed_transforms.is_some_and(|allowed| !allowed.contains(&uri)) {
4879 FailureReason::ReferencePolicyViolation {
4880 ref_index: reference_index,
4881 }
4882 } else {
4883 FailureReason::ReferenceProcessingFailure {
4884 ref_index: reference_index,
4885 }
4886 };
4887 invalid.push(CompiledManifestInvalid {
4888 result: ReferenceResult {
4889 reference_identity: Some(view.node_identity(child)),
4890 target_identity: None,
4891 external_resource_fingerprint: None,
4892 coverage: ReferenceCoverage::TransformedData,
4893 reference_set: ReferenceSet::Manifest,
4894 reference_index,
4895 uri: child.attribute("URI").unwrap_or("<omitted>").to_owned(),
4896 digest_algorithm,
4897 status: DsigStatus::Invalid(reason),
4898 pre_digest_data: None,
4899 },
4900 });
4901 }
4902 Err(error) => return Err(map_manifest_parse_error(error)),
4903 }
4904 }
4905 }
4906 Ok(ParsedManifestReferences {
4907 references,
4908 invalid_results: invalid,
4909 })
4910}
4911
4912struct ParsedManifestReferences {
4913 references: Vec<CompiledManifestReference>,
4914 invalid_results: Vec<CompiledManifestInvalid>,
4915}
4916
4917struct ManifestDiscoveryState<'a> {
4918 processed: &'a mut HashSet<NodeId>,
4919 remaining_capacity: &'a mut usize,
4920 next_reference_index: &'a mut usize,
4921 xpath_parse: &'a mut XPathSignatureParseBudget,
4922}
4923
4924fn transform_preserves_manifest_structure(transform: &Transform) -> bool {
4925 match transform {
4926 Transform::C14n(_) => true,
4927 Transform::Enveloped
4932 | Transform::XpathExcludeAllSignatures
4933 | Transform::XPath(_)
4934 | Transform::XPathFilter2(_)
4935 | Transform::Base64Decode
4936 | Transform::Relationship(_) => false,
4937 #[cfg(feature = "xslt")]
4938 Transform::Xslt(_) => false,
4939 }
4940}
4941
4942#[expect(
4943 clippy::large_enum_variant,
4944 reason = "single bounded authorization result avoids heap allocation"
4945)]
4946enum ResolvedVerifyingKey<'a> {
4947 Established(super::HmacVerificationKey),
4948 Borrowed(&'a dyn VerifyingKey),
4949 Owned(super::ResolvedVerificationKey<'a>),
4950}
4951
4952impl ResolvedVerifyingKey<'_> {
4953 fn evidence(
4954 &self,
4955 policy: &crate::policy::VerificationPolicy,
4956 ) -> Result<super::KeyTrustEvidence, DsigError> {
4957 match self {
4958 Self::Established(_) => {
4959 if policy.key_trust.mode != crate::policy::VerificationTrustMode::CryptographicOnly
4960 {
4961 return Err(crate::policy::PolicyViolation::KeyTrust {
4962 reason: "KEM/HMAC does not authenticate sender identity",
4963 }
4964 .into());
4965 }
4966 Ok(super::KeyTrustEvidence::NotEstablished)
4967 }
4968 Self::Borrowed(_)
4969 if policy.key_trust.mode
4970 == crate::policy::VerificationTrustMode::CryptographicOnly =>
4971 {
4972 Ok(super::KeyTrustEvidence::NotEstablished)
4973 }
4974 Self::Borrowed(_) => Ok(super::KeyTrustEvidence::CallerTrusted),
4975 Self::Owned(key) => key.authorize(policy),
4976 }
4977 }
4978 fn as_ref(&self) -> &dyn VerifyingKey {
4979 match self {
4980 Self::Established(key) => key,
4981 Self::Borrowed(key) => *key,
4982 Self::Owned(key) => key.as_ref(),
4983 }
4984 }
4985}
4986
4987fn resolve_verifying_key<'k>(
4988 ctx: &VerifyContext<'k>,
4989 key: Option<&'k dyn VerifyingKey>,
4990 key_info: Option<&KeyInfo>,
4991 algorithm: SignatureAlgorithm,
4992 budget: &mut super::InspectedKeyCandidateBudget,
4993) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
4994 if let Some(key) = key {
4995 if !ctx.policy.key_sources.preset_key {
4996 return Err(crate::policy::PolicyViolation::KeyTrust {
4997 reason: "pre-resolved verification keys are disabled",
4998 }
4999 .into());
5000 }
5001 require_verifying_key_candidate_capacity(&ctx.policy)?;
5002 budget.charge()?;
5003 return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
5004 }
5005 if let Some(resolver) = ctx.key_resolver {
5006 require_verifying_key_candidate_capacity(&ctx.policy)?;
5007 let resolved = resolver.resolve_for_verification_with_budget(
5008 key_info,
5009 algorithm,
5010 &ctx.policy,
5011 ctx.provider,
5012 budget,
5013 )?;
5014 if let Some(key) = &resolved {
5015 key.authorize(&ctx.policy)?;
5016 }
5017 return Ok(resolved.map(ResolvedVerifyingKey::Owned));
5018 }
5019 Ok(None)
5020}
5021
5022fn require_verifying_key_candidate_capacity(
5023 policy: &crate::policy::VerificationPolicy,
5024) -> Result<(), SignatureVerificationPipelineError> {
5025 policy
5026 .resources
5027 .validate_key_candidates(1)
5028 .map_err(Into::into)
5029}
5030
5031fn enforce_reference_policies(
5032 references: &[Reference],
5033 allowed_uri_types: UriTypeSet,
5034 allowed_transforms: Option<&HashSet<String>>,
5035) -> Result<(), SignatureVerificationPipelineError> {
5036 for reference in references {
5037 let uri = reference
5038 .uri
5039 .as_deref()
5040 .ok_or(SignatureVerificationPipelineError::Reference(
5041 ReferenceProcessingError::MissingUri,
5042 ))?;
5043 if !allowed_uri_types.allows(uri) {
5044 return Err(crate::policy::PolicyViolation::Uri {
5045 operation: "verification",
5046 reason: "reference URI class is not permitted",
5047 }
5048 .into());
5049 }
5050
5051 if let Some(allowed) = allowed_transforms {
5052 for transform in &reference.transforms {
5053 let transform_uri = transform.algorithm_uri();
5054 enforce_transform_allowed(Some(allowed), transform_uri)?;
5055 }
5056
5057 if transform_chain_uses_implicit_c14n(
5062 classify_uri(uri) == UriClass::External,
5063 &reference.transforms,
5064 ) {
5065 enforce_transform_allowed(Some(allowed), DEFAULT_IMPLICIT_C14N_URI)?;
5066 }
5067 }
5068 }
5069 Ok(())
5070}
5071
5072fn enforce_transform_allowed(
5073 allowed_transforms: Option<&HashSet<String>>,
5074 algorithm: &str,
5075) -> Result<(), SignatureVerificationPipelineError> {
5076 if allowed_transforms.is_some_and(|allowed| !allowed.contains(algorithm)) {
5077 return Err(crate::policy::PolicyViolation::Algorithm {
5078 operation: "verification transform",
5079 algorithm: algorithm.to_owned(),
5080 }
5081 .into());
5082 }
5083 Ok(())
5084}
5085
5086fn enforce_current_transform_allowed<W: structured_asn1::memory::Work>(
5088 allowed_transforms: Option<&HashSet<String>>,
5089 algorithm: &str,
5090 work: &W,
5091) -> Result<(), SignatureVerificationPipelineError> {
5092 use crate::xml::dom::storage::Storage as _;
5093 if allowed_transforms.is_some_and(|allowed| !allowed.contains(algorithm)) {
5094 return Err(
5095 match crate::xml::dom::storage::Accounted(work).formatted(format_args!("{algorithm}")) {
5096 Ok(algorithm) => DsigError::AccountedPolicyAlgorithm {
5097 operation: "verification transform",
5098 algorithm: algorithm.into_owned(),
5099 },
5100 Err(crate::xml::dom::ParseError::SharedExecution(cause)) => cause.into(),
5101 Err(cause) => cause.into(),
5102 },
5103 );
5104 }
5105 Ok(())
5106}
5107
5108#[derive(Debug, Clone, Copy)]
5109pub(super) struct SignatureChildNodes<'a, 'input> {
5110 signed_info_node: Node<'a, 'input>,
5111 signature_value_node: Node<'a, 'input>,
5112 pub(super) key_info_node: Option<Node<'a, 'input>>,
5113}
5114
5115pub(super) fn parse_signature_children<'a, 'input>(
5116 signature_node: Node<'a, 'input>,
5117) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
5118 let mut signed_info_node: Option<Node<'_, '_>> = None;
5119 let mut signature_value_node: Option<Node<'_, '_>> = None;
5120 let mut key_info_node: Option<Node<'_, '_>> = None;
5121 let mut signed_info_index: Option<usize> = None;
5122 let mut signature_value_index: Option<usize> = None;
5123 let mut key_info_index: Option<usize> = None;
5124 let mut first_unexpected_dsig_index: Option<usize> = None;
5125
5126 let mut element_index = 0usize;
5127 for child in signature_node.children() {
5128 if child.is_text() {
5129 if child
5130 .text()
5131 .is_some_and(|text| !is_xml_whitespace_only(text))
5132 {
5133 return Err(SignatureVerificationPipelineError::InvalidStructure {
5134 reason: "Signature must not contain non-whitespace mixed content",
5135 });
5136 }
5137 continue;
5138 }
5139 if !child.is_element() {
5140 continue;
5141 }
5142
5143 element_index += 1;
5144 if child.tag_name().namespace() != Some(XMLDSIG_NS) {
5145 return Err(SignatureVerificationPipelineError::InvalidStructure {
5146 reason: "Signature must contain only XMLDSIG element children",
5147 });
5148 }
5149 match child.tag_name().name() {
5150 "SignedInfo" => {
5151 if signed_info_node.is_some() {
5152 return Err(SignatureVerificationPipelineError::InvalidStructure {
5153 reason: "SignedInfo must appear exactly once under Signature",
5154 });
5155 }
5156 signed_info_node = Some(child);
5157 signed_info_index = Some(element_index);
5158 }
5159 "SignatureValue" => {
5160 if signature_value_node.is_some() {
5161 return Err(SignatureVerificationPipelineError::InvalidStructure {
5162 reason: "SignatureValue must appear exactly once under Signature",
5163 });
5164 }
5165 signature_value_node = Some(child);
5166 signature_value_index = Some(element_index);
5167 }
5168 "KeyInfo" => {
5169 if key_info_node.is_some() {
5170 return Err(SignatureVerificationPipelineError::InvalidStructure {
5171 reason: "KeyInfo must appear at most once under Signature",
5172 });
5173 }
5174 key_info_node = Some(child);
5175 key_info_index = Some(element_index);
5176 }
5177 "Object" => {
5178 }
5181 _ => {
5182 if first_unexpected_dsig_index.is_none() {
5183 first_unexpected_dsig_index = Some(element_index);
5184 }
5185 }
5186 }
5187 }
5188
5189 let signed_info_node =
5190 signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
5191 element: "SignedInfo",
5192 })?;
5193 let signature_value_node =
5194 signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
5195 element: "SignatureValue",
5196 })?;
5197 if signed_info_index != Some(1) {
5198 return Err(SignatureVerificationPipelineError::InvalidStructure {
5199 reason: "SignedInfo must be the first element child of Signature",
5200 });
5201 }
5202 if signature_value_index != Some(2) {
5203 return Err(SignatureVerificationPipelineError::InvalidStructure {
5204 reason: "SignatureValue must be the second element child of Signature",
5205 });
5206 }
5207 if let Some(index) = key_info_index
5208 && index != 3
5209 {
5210 return Err(SignatureVerificationPipelineError::InvalidStructure {
5211 reason: "KeyInfo must be the third element child of Signature when present",
5212 });
5213 }
5214
5215 let allowed_prefix_end = key_info_index.unwrap_or(2);
5216 if let Some(unexpected_index) = first_unexpected_dsig_index {
5217 return Err(SignatureVerificationPipelineError::InvalidStructure {
5218 reason: if unexpected_index > allowed_prefix_end {
5219 "After SignedInfo, SignatureValue, and optional KeyInfo, Signature may contain only Object elements"
5220 } else {
5221 "Signature may contain SignedInfo first, SignatureValue second, optional KeyInfo third, and Object elements thereafter"
5222 },
5223 });
5224 }
5225
5226 Ok(SignatureChildNodes {
5227 signed_info_node,
5228 signature_value_node,
5229 key_info_node,
5230 })
5231}
5232
5233fn decode_signature_value(
5234 signature_value_node: Node<'_, '_>,
5235 algorithm: SignatureAlgorithm,
5236 resources: &crate::policy::ResourcePolicy,
5237) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
5238 if matches!(algorithm, SignatureAlgorithm::PostQuantum(_)) {
5239 let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
5240 let payload = super::whitespace::XmlBase64Payload::bounded(
5244 signature_value_node,
5245 raw_maximum,
5246 maximum,
5247 )
5248 .map_err(
5249 |reason| SignatureVerificationPipelineError::InvalidStructure {
5250 reason: match reason {
5251 "unexpected nested element" => {
5252 "SignatureValue must not contain element children"
5253 }
5254 "maximum allowed text length" => {
5255 "SignatureValue exceeds maximum allowed text length"
5256 }
5257 "maximum allowed base64 length" => {
5258 "SignatureValue exceeds maximum allowed length"
5259 }
5260 _ => "SignatureValue contains malformed Base64",
5261 },
5262 },
5263 )?;
5264 return payload.decode().map_err(|_| {
5265 SignatureVerificationPipelineError::InvalidStructure {
5266 reason: "SignatureValue contains malformed Base64",
5267 }
5268 });
5269 }
5270 if signature_value_node
5271 .children()
5272 .any(|child| child.is_element())
5273 {
5274 return Err(SignatureVerificationPipelineError::InvalidStructure {
5275 reason: "SignatureValue must not contain element children",
5276 });
5277 }
5278
5279 let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
5283 let mut text_len = 0usize;
5284 let mut normalized_len = 0usize;
5285 for child in signature_value_node
5286 .children()
5287 .filter(|child| child.is_text())
5288 {
5289 if let Some(text) = child.text() {
5290 if text.len() > raw_maximum - text_len {
5291 return Err(SignatureVerificationPipelineError::InvalidStructure {
5292 reason: "SignatureValue exceeds maximum allowed text length",
5293 });
5294 }
5295 text_len += text.len();
5296 for byte in text.bytes() {
5297 if matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
5298 continue;
5299 }
5300 if normalized_len == maximum {
5301 return Err(SignatureVerificationPipelineError::InvalidStructure {
5302 reason: "SignatureValue exceeds maximum allowed length",
5303 });
5304 }
5305 normalized_len += 1;
5306 }
5307 }
5308 }
5309 let mut normalized = Vec::with_capacity(normalized_len);
5310 let mut raw_text_len = 0usize;
5311 for child in signature_value_node
5312 .children()
5313 .filter(|child| child.is_text())
5314 {
5315 if let Some(text) = child.text() {
5316 push_normalized_signature_text(
5317 text,
5318 &mut raw_text_len,
5319 &mut normalized,
5320 algorithm,
5321 resources,
5322 )?;
5323 }
5324 }
5325
5326 Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
5327}
5328
5329fn push_normalized_signature_text(
5330 text: &str,
5331 raw_text_len: &mut usize,
5332 normalized: &mut Vec<u8>,
5333 algorithm: SignatureAlgorithm,
5334 resources: &crate::policy::ResourcePolicy,
5335) -> Result<(), SignatureVerificationPipelineError> {
5336 let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
5337 if text.len() > raw_maximum - *raw_text_len {
5338 return Err(SignatureVerificationPipelineError::InvalidStructure {
5339 reason: "SignatureValue exceeds maximum allowed text length",
5340 });
5341 }
5342 *raw_text_len += text.len();
5343 let additional = text
5344 .bytes()
5345 .filter(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
5346 .count();
5347 if additional > maximum - normalized.len() {
5348 return Err(SignatureVerificationPipelineError::InvalidStructure {
5349 reason: "SignatureValue exceeds maximum allowed length",
5350 });
5351 }
5352
5353 normalize_xml_base64_bytes(text.as_bytes(), normalized, |_| true).map_err(|err| {
5354 SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(
5355 err.normalized_offset,
5356 err.invalid_byte,
5357 ))
5358 })?;
5359
5360 Ok(())
5361}
5362
5363fn signature_value_limits(
5364 algorithm: SignatureAlgorithm,
5365 resources: &crate::policy::ResourcePolicy,
5366) -> (usize, usize) {
5367 let encoded = match algorithm {
5374 SignatureAlgorithm::PostQuantum(parameter) => parameter.signature_len().div_ceil(3) * 4,
5375 _ => MAX_SIGNATURE_VALUE_LEN,
5376 };
5377 (encoded, resources.max_xml_document_bytes)
5378}
5379
5380fn verify_with_algorithm(
5381 algorithm: SignatureAlgorithm,
5382 public_key_pem: &str,
5383 signed_data: &[u8],
5384 signature_value: &[u8],
5385) -> Result<bool, SignatureVerificationPipelineError> {
5386 match algorithm {
5387 #[cfg(feature = "legacy-algorithms")]
5388 SignatureAlgorithm::RsaMd5 | SignatureAlgorithm::RsaRipemd160 => Ok(
5389 verify_rsa_signature_pem(algorithm, public_key_pem, signed_data, signature_value)?,
5390 ),
5391 #[cfg(feature = "legacy-algorithms")]
5392 SignatureAlgorithm::EcdsaRipemd160 => Ok(verify_ecdsa_signature_pem(
5393 algorithm,
5394 public_key_pem,
5395 signed_data,
5396 signature_value,
5397 )?),
5398 #[cfg(feature = "legacy-algorithms")]
5399 SignatureAlgorithm::HmacMd5 | SignatureAlgorithm::HmacRipemd160 => {
5400 Err(SignatureVerificationError::UnsupportedAlgorithm {
5401 uri: algorithm.uri().to_owned(),
5402 }
5403 .into())
5404 }
5405 SignatureAlgorithm::Ed25519
5406 | SignatureAlgorithm::Ed25519Ctx
5407 | SignatureAlgorithm::Ed25519Ph
5408 | SignatureAlgorithm::Ed448
5409 | SignatureAlgorithm::Ed448Ph
5410 | SignatureAlgorithm::PostQuantum(_) => {
5411 let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
5412 .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
5413 if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
5414 return Err(SignatureVerificationError::InvalidKeyPem.into());
5415 }
5416 Ok(super::modern::verify(
5417 algorithm,
5418 &pem.contents,
5419 signed_data,
5420 signature_value,
5421 )?)
5422 }
5423 SignatureAlgorithm::DsaSha1 | SignatureAlgorithm::DsaSha256 => {
5424 let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
5425 .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
5426 if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
5427 return Err(SignatureVerificationError::InvalidKeyPem.into());
5428 }
5429 Ok(verify_dsa_signature_spki(
5430 algorithm,
5431 &pem.contents,
5432 signed_data,
5433 signature_value,
5434 )?)
5435 }
5436 SignatureAlgorithm::HmacSha1
5437 | SignatureAlgorithm::HmacSha224
5438 | SignatureAlgorithm::HmacSha256
5439 | SignatureAlgorithm::HmacSha384
5440 | SignatureAlgorithm::HmacSha512 => Err(SignatureVerificationError::UnsupportedAlgorithm {
5441 uri: algorithm.uri().to_string(),
5442 }
5443 .into()),
5444 SignatureAlgorithm::RsaSha1
5445 | SignatureAlgorithm::RsaSha224
5446 | SignatureAlgorithm::RsaSha256
5447 | SignatureAlgorithm::RsaSha384
5448 | SignatureAlgorithm::RsaSha512
5449 | SignatureAlgorithm::RsaPssSha1
5450 | SignatureAlgorithm::RsaPssSha224
5451 | SignatureAlgorithm::RsaPssSha256
5452 | SignatureAlgorithm::RsaPssSha384
5453 | SignatureAlgorithm::RsaPssSha512
5454 | SignatureAlgorithm::RsaPssSha3_224
5455 | SignatureAlgorithm::RsaPssSha3_256
5456 | SignatureAlgorithm::RsaPssSha3_384
5457 | SignatureAlgorithm::RsaPssSha3_512
5458 | SignatureAlgorithm::RsaPss(_) => Ok(verify_rsa_signature_pem(
5459 algorithm,
5460 public_key_pem,
5461 signed_data,
5462 signature_value,
5463 )?),
5464 SignatureAlgorithm::EcdsaSha1
5465 | SignatureAlgorithm::EcdsaSha224
5466 | SignatureAlgorithm::EcdsaSha256
5467 | SignatureAlgorithm::EcdsaSha384
5468 | SignatureAlgorithm::EcdsaSha512
5469 | SignatureAlgorithm::EcdsaSha3_224
5470 | SignatureAlgorithm::EcdsaSha3_256
5471 | SignatureAlgorithm::EcdsaSha3_384
5472 | SignatureAlgorithm::EcdsaSha3_512 => {
5473 match verify_ecdsa_signature_pem(
5477 algorithm,
5478 public_key_pem,
5479 signed_data,
5480 signature_value,
5481 ) {
5482 Ok(valid) => Ok(valid),
5483 Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
5484 Err(error) => Err(error.into()),
5485 }
5486 }
5487 }
5488}
5489
5490#[cfg(test)]
5491#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
5492mod tests {
5493 use super::*;
5494 use crate::c14n::C14nAlgorithm;
5495 use crate::xml::dom::Document;
5496 use crate::xmldsig::TransformError;
5497 use crate::xmldsig::digest::DigestAlgorithm;
5498 use crate::xmldsig::parse::{Reference, parse_signed_info};
5499 use crate::xmldsig::transforms::Transform;
5500 use crate::xmldsig::uri::UriReferenceResolver;
5501 use base64::Engine;
5502
5503 fn make_reference(
5507 uri: &str,
5508 transforms: Vec<Transform>,
5509 digest_method: DigestAlgorithm,
5510 digest_value: Vec<u8>,
5511 ) -> Reference {
5512 Reference {
5513 uri: Some(uri.to_string()),
5514 id: None,
5515 ref_type: None,
5516 transforms,
5517 digest_method,
5518 digest_value,
5519 }
5520 }
5521
5522 #[test]
5523 fn operation_borrows_caller_utc_facts_without_policy_rebinding()
5524 -> Result<(), Box<dyn std::error::Error>> {
5525 let transitions = [structured_pkix::temporal::Transition::new(2_678_400, 1)];
5529 let facts = structured_pkix::temporal::LeapTable::new(0, 6_000_000, &transitions)
5530 .expect("valid synthetic UTC facts");
5531 let ctx = VerifyContext::new().utc_facts(facts);
5532 let budgets = VerificationOperationBudgets::with_transforms(
5533 &ctx.policy,
5534 TransformExecutionBudget::from_resources(&ctx.policy.resources),
5535 ctx.utc_facts,
5536 );
5537 let mut budget = budgets.key_candidates.borrow_mut();
5538 assert_eq!(
5539 budget
5540 .utc_facts()
5541 .ok_or("operation UTC facts")?
5542 .transitions()
5543 .as_ptr(),
5544 transitions.as_ptr()
5545 );
5546 budget.bind_policy(&ctx.policy)?;
5547 assert_eq!(
5548 budget
5549 .utc_facts()
5550 .ok_or("retained UTC facts")?
5551 .transitions()
5552 .as_ptr(),
5553 transitions.as_ptr()
5554 );
5555 let standalone =
5556 super::super::InspectedKeyCandidateBudget::new_with_utc_facts(&ctx.policy, facts);
5557 assert_eq!(
5558 standalone
5559 .utc_facts()
5560 .ok_or("standalone UTC facts")?
5561 .transitions()
5562 .as_ptr(),
5563 transitions.as_ptr()
5564 );
5565 assert!(
5566 super::super::InspectedKeyCandidateBudget::new(&ctx.policy)
5567 .utc_facts()
5568 .is_none()
5569 );
5570 Ok(())
5571 }
5572
5573 #[test]
5574 fn reference_resolution_uses_each_elements_effective_xml_base() {
5575 let first = b"first payload";
5578 let second = b"second payload";
5579 let first_digest = base64::engine::general_purpose::STANDARD
5580 .encode(compute_digest(DigestAlgorithm::Sha256, first));
5581 let second_digest = base64::engine::general_purpose::STANDARD
5582 .encode(compute_digest(DigestAlgorithm::Sha256, second));
5583 let xml = format!(
5584 r#"<root xml:base="https://example.test/base/" xmlns:ds="{XMLDSIG_NS}">
5585 <ds:Signature><ds:SignedInfo>
5586 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5587 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5588 <ds:Reference xml:base="one/" URI="payload.bin">
5589 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5590 <ds:DigestValue>{first_digest}</ds:DigestValue>
5591 </ds:Reference>
5592 <ds:Reference xml:base="../two/" URI="payload.bin">
5593 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5594 <ds:DigestValue>{second_digest}</ds:DigestValue>
5595 </ds:Reference>
5596 </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>
5597 </root>"#
5598 );
5599 let document = Document::parse(&xml).unwrap();
5600 let signature = document
5601 .descendants()
5602 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
5603 .unwrap();
5604 let signed_info_node = signature
5605 .children()
5606 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
5607 .unwrap();
5608 let signed_info = parse_signed_info(signed_info_node).unwrap();
5609 let resources = HashMap::from([
5610 (
5611 "https://example.test/base/one/payload.bin".into(),
5612 first.to_vec(),
5613 ),
5614 (
5615 "https://example.test/two/payload.bin".into(),
5616 second.to_vec(),
5617 ),
5618 ]);
5619 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5620
5621 let result = process_all_references(&signed_info.references, &resolver, signature, false)
5622 .expect("each Reference should resolve against its own effective base");
5623
5624 assert!(result.all_valid());
5625 }
5626
5627 #[test]
5628 fn internal_dtd_opt_in_applies_to_detached_xml_transforms() {
5629 let detached = b"<!DOCTYPE payload [<!ELEMENT payload (#PCDATA)>]><payload>ok</payload>";
5632 let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
5633 DigestAlgorithm::Sha256,
5634 b"<payload>ok</payload>",
5635 ));
5636 let xml = format!(
5637 r#"<root xmlns:ds="{XMLDSIG_NS}">
5638 <ds:Signature>
5639 <ds:SignedInfo>
5640 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5641 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5642 <ds:Reference URI="urn:detached-dtd">
5643 <ds:Transforms>
5644 <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
5645 </ds:Transforms>
5646 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5647 <ds:DigestValue>{digest}</ds:DigestValue>
5648 </ds:Reference>
5649 </ds:SignedInfo>
5650 <ds:SignatureValue>AQ==</ds:SignatureValue>
5651 </ds:Signature>
5652</root>"#
5653 );
5654 let resources = HashMap::from([("urn:detached-dtd".to_owned(), detached.to_vec())]);
5655 let key = AcceptingKey;
5656
5657 let default_error = VerifyContext::new()
5658 .key(&key)
5659 .allowed_uri_types(UriTypeSet::ALL)
5660 .external_resources(&resources)
5661 .verify(&xml)
5662 .expect_err("internal DTD parsing must remain disabled by default");
5663 assert!(matches!(
5664 default_error,
5665 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
5666 crate::xmldsig::TransformError::XmlParse(_)
5667 ))
5668 ));
5669
5670 let result = VerifyContext::new()
5671 .key(&key)
5672 .allowed_uri_types(UriTypeSet::ALL)
5673 .external_resources(&resources)
5674 .allow_internal_dtd(true)
5675 .verify(&xml)
5676 .expect("the explicit DTD opt-in must cover detached XML transforms");
5677
5678 assert_eq!(result.status, DsigStatus::Valid);
5679
5680 let external_entity = br#"<!DOCTYPE payload [
5681 <!ENTITY ext SYSTEM "file:///etc/passwd">
5682 ]><payload>&ext;</payload>"#;
5683 let external_entity_resources =
5684 HashMap::from([("urn:detached-dtd".to_owned(), external_entity.to_vec())]);
5685 let external_entity_error = VerifyContext::new()
5686 .key(&key)
5687 .allowed_uri_types(UriTypeSet::ALL)
5688 .external_resources(&external_entity_resources)
5689 .allow_internal_dtd(true)
5690 .verify(&xml)
5691 .expect_err("the internal-DTD opt-in must not resolve external entities");
5692 assert!(matches!(
5693 external_entity_error,
5694 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
5695 crate::xmldsig::TransformError::XmlParse(_)
5696 ))
5697 ));
5698 }
5699
5700 #[test]
5701 fn owned_verification_revalidates_internal_dtd_provenance() {
5702 let document = XmlDocument::parse_with_settings(
5705 "<!DOCTYPE root [<!ENTITY value \"ok\">]><root>&value;</root>".into(),
5706 DocumentParseSettings::new(
5707 true,
5708 crate::hard_limits::XML_DOCUMENT_NODE_CEILING,
5709 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
5710 ),
5711 )
5712 .expect("explicitly permitted DTD fixture must parse");
5713
5714 assert!(matches!(
5715 VerifyContext::new().verify_document(&document),
5716 Err(DsigError::Policy(
5717 crate::policy::PolicyViolation::XmlInput {
5718 reason: "owned document requires internal DTD support"
5719 }
5720 ))
5721 ));
5722 assert!(!matches!(
5723 VerifyContext::new()
5724 .allow_internal_dtd(true)
5725 .verify_document(&document),
5726 Err(DsigError::Policy(
5727 crate::policy::PolicyViolation::XmlInput { .. }
5728 ))
5729 ));
5730 }
5731
5732 #[test]
5733 fn verification_policy_bounds_reference_canonicalization() {
5734 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
5737 let xml = format!(
5738 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>"#,
5739 "payload".repeat(16)
5740 );
5741 let policy = crate::policy::VerificationPolicy {
5742 resources: crate::policy::ResourcePolicy {
5743 max_canonicalized_bytes: 64,
5744 ..crate::policy::ResourcePolicy::default()
5745 },
5746 ..crate::policy::VerificationPolicy::default()
5747 };
5748
5749 let error = VerifyContext::new()
5750 .key(&AcceptingKey)
5751 .policy(policy)
5752 .verify(&xml)
5753 .expect_err("reference canonicalization must consume the policy budget");
5754
5755 assert!(
5756 matches!(
5757 error,
5758 SignatureVerificationPipelineError::Policy(
5759 crate::policy::PolicyViolation::ResourceLimit {
5760 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
5761 maximum: 64,
5762 ..
5763 }
5764 )
5765 ),
5766 "unexpected error: {error:?}"
5767 );
5768 }
5769
5770 #[test]
5771 fn verification_policy_bounds_document_bytes_before_parsing() {
5772 let xml = format!("<root>{}</root>", "x".repeat(1_024));
5775 let policy = crate::policy::VerificationPolicy {
5776 resources: crate::policy::ResourcePolicy {
5777 max_xml_document_bytes: xml.len() - 1,
5778 ..crate::policy::ResourcePolicy::default()
5779 },
5780 ..crate::policy::VerificationPolicy::default()
5781 };
5782
5783 assert!(matches!(
5784 VerifyContext::new().policy(policy).verify(&xml),
5785 Err(SignatureVerificationPipelineError::Policy(
5786 crate::policy::PolicyViolation::ResourceLimit {
5787 resource: crate::policy::resource_name::XML_DOCUMENT,
5788 maximum,
5789 actual,
5790 }
5791 )) if maximum == xml.len() - 1 && actual == xml.len()
5792 ));
5793 }
5794
5795 #[test]
5796 fn verification_entry_points_enforce_policy_depth() {
5797 let xml = "<root><child><leaf/></child></root>";
5800 let policy = crate::policy::VerificationPolicy {
5801 resources: crate::policy::ResourcePolicy {
5802 max_xml_depth: 2,
5803 ..crate::policy::ResourcePolicy::default()
5804 },
5805 ..crate::policy::VerificationPolicy::default()
5806 };
5807 let document = XmlDocument::parse(xml).expect("wide retained fixture must parse");
5808
5809 assert!(matches!(
5810 VerifyContext::new().policy(policy.clone()).verify(xml),
5811 Err(DsigError::Policy(
5812 crate::policy::PolicyViolation::ResourceLimit {
5813 resource: crate::policy::resource_name::XML_DEPTH,
5814 maximum: 2,
5815 actual: 3,
5816 }
5817 ))
5818 ));
5819 assert!(matches!(
5820 VerifyContext::new()
5821 .policy(policy)
5822 .verify_document(&document),
5823 Err(DsigError::Policy(
5824 crate::policy::PolicyViolation::ResourceLimit {
5825 resource: crate::policy::resource_name::XML_DEPTH,
5826 maximum: 2,
5827 actual: 3,
5828 }
5829 ))
5830 ));
5831 }
5832
5833 #[test]
5834 fn verification_policy_bounds_base64_transform_input() {
5835 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
5838 let xml = format!(
5839 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>"##
5840 );
5841 let policy = crate::policy::VerificationPolicy {
5842 resources: crate::policy::ResourcePolicy {
5843 max_base64_transform_input_bytes: 4,
5844 ..crate::policy::ResourcePolicy::default()
5845 },
5846 ..crate::policy::VerificationPolicy::default()
5847 };
5848
5849 let error = VerifyContext::new()
5850 .key(&AcceptingKey)
5851 .policy(policy)
5852 .verify(&xml)
5853 .expect_err("Base64 input must use the operation policy ceiling");
5854
5855 assert!(matches!(
5856 error,
5857 SignatureVerificationPipelineError::Policy(
5858 crate::policy::PolicyViolation::ResourceLimit {
5859 resource: crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
5860 maximum: 4,
5861 ..
5862 }
5863 )
5864 ));
5865 }
5866
5867 #[test]
5868 fn verification_policy_bounds_cumulative_base64_transform_output() {
5869 let first_digest = base64::engine::general_purpose::STANDARD
5872 .encode(compute_digest(DigestAlgorithm::Sha256, b"a"));
5873 let second_digest = base64::engine::general_purpose::STANDARD
5874 .encode(compute_digest(DigestAlgorithm::Sha256, b"b"));
5875 let xml = format!(
5876 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>"##
5877 );
5878 let policy = crate::policy::VerificationPolicy {
5879 resources: crate::policy::ResourcePolicy {
5880 max_base64_transform_input_bytes: 8,
5881 max_base64_transform_output_bytes: 1,
5882 ..crate::policy::ResourcePolicy::default()
5883 },
5884 ..crate::policy::VerificationPolicy::default()
5885 };
5886
5887 let error = VerifyContext::new()
5888 .key(&AcceptingKey)
5889 .policy(policy)
5890 .verify(&xml)
5891 .expect_err("references must share the Base64 output allowance");
5892
5893 assert!(matches!(
5894 error,
5895 SignatureVerificationPipelineError::Policy(
5896 crate::policy::PolicyViolation::ResourceLimit {
5897 resource: crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
5898 maximum: 1,
5899 actual: 2,
5900 }
5901 )
5902 ));
5903 }
5904
5905 #[test]
5906 fn verification_policy_bounds_xpath_source_before_compilation() {
5907 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
5910 let xml = format!(
5911 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>"#
5912 );
5913 let policy = crate::policy::VerificationPolicy {
5914 resources: crate::policy::ResourcePolicy {
5915 max_xpath_expression_bytes: 4,
5916 ..crate::policy::ResourcePolicy::default()
5917 },
5918 ..crate::policy::VerificationPolicy::default()
5919 };
5920
5921 let error = VerifyContext::new()
5922 .key(&AcceptingKey)
5923 .policy(policy)
5924 .verify(&xml)
5925 .expect_err("XPath source must use the operation policy ceiling");
5926
5927 assert!(matches!(
5928 error,
5929 SignatureVerificationPipelineError::Policy(
5930 crate::policy::PolicyViolation::ResourceLimit {
5931 resource: crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
5932 maximum: 4,
5933 ..
5934 }
5935 )
5936 ));
5937 }
5938
5939 #[test]
5940 fn verification_policy_shares_canonicalization_budget_with_signed_info() {
5941 let payload_text = "x".repeat(700);
5945 let canonical_payload = format!("<payload ID=\"payload\">{payload_text}</payload>");
5946 let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
5947 DigestAlgorithm::Sha256,
5948 canonical_payload.as_bytes(),
5949 ));
5950 let xml = format!(
5951 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>"##
5952 );
5953 let policy = crate::policy::VerificationPolicy {
5954 resources: crate::policy::ResourcePolicy {
5955 max_canonicalized_bytes: 1_024,
5956 ..crate::policy::ResourcePolicy::default()
5957 },
5958 ..crate::policy::VerificationPolicy::default()
5959 };
5960
5961 let error = VerifyContext::new()
5962 .key(&AcceptingKey)
5963 .policy(policy)
5964 .verify(&xml)
5965 .expect_err("SignedInfo must consume the remaining operation C14N budget");
5966
5967 assert!(
5968 matches!(
5969 &error,
5970 SignatureVerificationPipelineError::Policy(
5971 crate::policy::PolicyViolation::ResourceLimit {
5972 resource: "canonicalized bytes",
5973 maximum: 1_024,
5974 ..
5975 }
5976 )
5977 ),
5978 "unexpected error: {error:?}"
5979 );
5980 }
5981
5982 #[test]
5983 fn manifest_processing_stops_after_c14n_budget_exhaustion() {
5984 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
5987 let xml = format!(
5988 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>"##
5989 );
5990 let resources = HashMap::from([("urn:small".to_owned(), b"small".to_vec())]);
5991 let transform_budget = TransformExecutionBudget::with_c14n_limit(8);
5992 let ctx = VerifyContext::new()
5993 .allowed_uri_types(UriTypeSet::ALL)
5994 .external_resources(&resources);
5995 let document = XmlDocument::parse(xml).expect("test signature must parse");
5996 let results = document
5997 .with_view(|view| {
5998 let signature = view
5999 .document()
6000 .descendants()
6001 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
6002 .expect("test signature must contain Signature");
6003 let object = signature
6004 .children()
6005 .find(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
6006 .expect("test signature must contain Object");
6007 let resolver = UriReferenceResolver::with_document_view(view, &[])
6008 .with_external_resources(&resources);
6009 let budgets = VerificationOperationBudgets::with_transforms(
6010 &ctx.policy,
6011 transform_budget,
6012 ctx.utc_facts,
6013 );
6014 let mut operation = OperationExecutionContext::new(
6015 &ctx.policy,
6016 &budgets,
6017 Some((view.identity(), view.generation())),
6018 );
6019 let crypto =
6020 operation.add_node(OperationNodeKind::Crypto, OperationStage::Crypto, None);
6021 operation.compile().expect("base plan");
6022 operation
6023 .run(crypto, || Ok::<_, SignatureVerificationPipelineError>(()))
6024 .expect("crypto gate");
6025 operation.authenticate(view.node_identity(object));
6026 process_authenticated_manifest_references(
6027 &mut operation,
6028 view,
6029 signature,
6030 &resolver,
6031 ManifestVerification {
6032 context: &ctx,
6033 capture: &(),
6034 },
6035 2,
6036 crypto,
6037 )
6038 .map(|(results, _)| results)
6039 })
6040 .expect("resource exhaustion is reported per Manifest reference");
6041
6042 assert_eq!(results.len(), 2);
6043 assert!(results.iter().all(|result| {
6044 matches!(
6045 result.status,
6046 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { .. })
6047 )
6048 }));
6049 }
6050
6051 #[test]
6052 fn verification_policy_bounds_detached_xml_nodes() {
6053 let detached = format!("<payload>{}</payload>", "<n/>".repeat(32));
6056 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
6057 let xml = format!(
6058 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>"#
6059 );
6060 let resources = HashMap::from([("urn:detached-nodes".to_owned(), detached.into_bytes())]);
6061 let policy = crate::policy::VerificationPolicy {
6062 uris: crate::policy::UriPolicy {
6063 references: UriTypeSet::ALL,
6064 ..crate::policy::UriPolicy::default()
6065 },
6066 resources: crate::policy::ResourcePolicy {
6067 max_xml_nodes: 24,
6068 ..crate::policy::ResourcePolicy::default()
6069 },
6070 ..crate::policy::VerificationPolicy::default()
6071 };
6072
6073 let error = VerifyContext::new()
6074 .key(&AcceptingKey)
6075 .policy(policy)
6076 .external_resources(&resources)
6077 .verify(&xml)
6078 .expect_err("detached XML must inherit the policy node ceiling");
6079
6080 assert!(
6081 matches!(
6082 error,
6083 SignatureVerificationPipelineError::Policy(
6084 crate::policy::PolicyViolation::ResourceLimit {
6085 resource: crate::policy::resource_name::XML_NODES,
6086 maximum: 24,
6087 ..
6088 }
6089 )
6090 ),
6091 "unexpected error: {error:?}"
6092 );
6093 }
6094
6095 #[test]
6096 fn query_only_reference_resolves_against_relative_xml_base() {
6097 let payload = b"query-selected payload";
6100 let digest = base64::engine::general_purpose::STANDARD
6101 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
6102 let xml = format!(
6103 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo>
6104 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6105 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6106 <ds:Reference xml:base="a/b?old" URI="?new">
6107 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6108 <ds:DigestValue>{digest}</ds:DigestValue>
6109 </ds:Reference>
6110 </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>"#
6111 );
6112 let document = Document::parse(&xml).unwrap();
6113 let signature = document.root_element();
6114 let signed_info_node = signature
6115 .children()
6116 .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
6117 .unwrap();
6118 let signed_info = parse_signed_info(signed_info_node).unwrap();
6119 let resources = HashMap::from([("a/b?new".to_string(), payload.to_vec())]);
6120 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6121
6122 let result = process_all_references(&signed_info.references, &resolver, signature, false)
6123 .expect("query-only URI must resolve against the complete relative base path");
6124
6125 assert!(result.all_valid());
6126 }
6127
6128 #[test]
6129 fn manifest_reference_resolution_uses_its_effective_xml_base() {
6130 let payload = b"manifest payload";
6133 let digest = base64::engine::general_purpose::STANDARD
6134 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
6135 let xml = format!(
6136 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
6137 <ds:Object><ds:Manifest xml:base="manifests/">
6138 <ds:Reference URI="payload.bin">
6139 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6140 <ds:DigestValue>{digest}</ds:DigestValue>
6141 </ds:Reference>
6142 </ds:Manifest></ds:Object>
6143 </ds:Signature>"#
6144 );
6145 let document = Document::parse(&xml).unwrap();
6146 let signature = document.root_element();
6147 let reference_node = signature
6148 .descendants()
6149 .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
6150 .unwrap();
6151 let reference = super::super::parse::parse_reference(reference_node).unwrap();
6152 let resources = HashMap::from([(
6153 "https://example.test/manifests/payload.bin".to_string(),
6154 payload.to_vec(),
6155 )]);
6156 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6157
6158 let result = process_reference(
6159 &reference,
6160 &resolver,
6161 signature,
6162 ReferenceSet::Manifest,
6163 0,
6164 false,
6165 )
6166 .expect("Manifest Reference should inherit its own XML Base context");
6167
6168 assert_eq!(result.status, DsigStatus::Valid);
6169 }
6170
6171 #[test]
6172 fn manifest_reference_index_ignores_nested_manifest_descendants() {
6173 let payload = b"direct manifest payload";
6176 let digest = base64::engine::general_purpose::STANDARD
6177 .encode(compute_digest(DigestAlgorithm::Sha256, payload));
6178 let xml = format!(
6179 r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
6180 <ds:Object><wrapper><ds:Manifest xml:base="nested/">
6181 <ds:Reference URI="payload.bin">
6182 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6183 <ds:DigestValue>{digest}</ds:DigestValue>
6184 </ds:Reference>
6185 </ds:Manifest></wrapper></ds:Object>
6186 <ds:Object><ds:Manifest xml:base="direct/">
6187 <ds:Reference URI="payload.bin">
6188 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6189 <ds:DigestValue>{digest}</ds:DigestValue>
6190 </ds:Reference>
6191 </ds:Manifest></ds:Object>
6192 </ds:Signature>"#
6193 );
6194 let document = Document::parse(&xml).unwrap();
6195 let signature = document.root_element();
6196 let direct_reference_node = signature
6197 .children()
6198 .filter(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
6199 .nth(1)
6200 .unwrap()
6201 .children()
6202 .find(|node| node.has_tag_name((XMLDSIG_NS, "Manifest")))
6203 .unwrap()
6204 .children()
6205 .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
6206 .unwrap();
6207 let reference = super::super::parse::parse_reference(direct_reference_node).unwrap();
6208 let resources = HashMap::from([(
6209 "https://example.test/direct/payload.bin".to_string(),
6210 payload.to_vec(),
6211 )]);
6212 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6213
6214 let result = process_reference(
6215 &reference,
6216 &resolver,
6217 signature,
6218 ReferenceSet::Manifest,
6219 0,
6220 false,
6221 )
6222 .expect("Manifest index must select the direct Object/Manifest reference");
6223
6224 assert_eq!(result.status, DsigStatus::Valid);
6225 }
6226
6227 struct RejectingKey;
6228
6229 impl VerifyingKey for RejectingKey {
6230 fn verify(
6231 &self,
6232 _algorithm: SignatureAlgorithm,
6233 _signed_data: &[u8],
6234 _signature_value: &[u8],
6235 ) -> Result<bool, SignatureVerificationPipelineError> {
6236 Ok(false)
6237 }
6238 }
6239
6240 struct AcceptingKey;
6241
6242 impl VerifyingKey for AcceptingKey {
6243 fn verify(
6244 &self,
6245 _algorithm: SignatureAlgorithm,
6246 _signed_data: &[u8],
6247 _signature_value: &[u8],
6248 ) -> Result<bool, SignatureVerificationPipelineError> {
6249 Ok(true)
6250 }
6251 }
6252
6253 struct PanicResolver;
6254
6255 struct UntrustedTestResolver;
6256
6257 impl KeyResolver for UntrustedTestResolver {
6258 fn resolve<'a>(
6259 &'a self,
6260 _key_info: Option<&KeyInfo>,
6261 _algorithm: SignatureAlgorithm,
6262 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
6263 Ok(Some(Box::new(AcceptingKey)))
6264 }
6265 }
6266
6267 #[test]
6268 fn verification_requires_explicit_key_authorization() {
6269 let xml = signature_with_target_reference("AQ==");
6271 let error = VerifyContext::new()
6272 .key_resolver(&UntrustedTestResolver)
6273 .verify(&xml)
6274 .expect_err("default verification must reject an untrusted resolver candidate");
6275 assert!(matches!(
6276 error,
6277 DsigError::Policy(crate::policy::PolicyViolation::KeyTrust { .. })
6278 ));
6279 }
6280
6281 #[test]
6282 fn cryptographic_only_is_explicit_and_never_reports_trust() {
6283 let mut policy = crate::policy::VerificationPolicy::default();
6285 policy.key_trust.mode = crate::policy::VerificationTrustMode::CryptographicOnly;
6286 let result = VerifyContext::new()
6287 .policy(policy)
6288 .key_resolver(&UntrustedTestResolver)
6289 .verify(&signature_with_target_reference("AQ=="))
6290 .expect("explicit mathematical mode permits an untrusted candidate");
6291 assert_eq!(result.status, DsigStatus::Valid);
6292 assert_eq!(
6293 result.key_trust,
6294 super::super::KeyTrustEvidence::NotEstablished
6295 );
6296 }
6297
6298 #[test]
6299 fn caller_key_authorization_does_not_authenticate_an_invalid_signature() {
6300 let result = VerifyContext::new()
6302 .key(&RejectingKey)
6303 .verify(&signature_with_target_reference("AQ=="))
6304 .expect("an invalid mathematical signature is a completed result");
6305 assert_eq!(
6306 result.status,
6307 DsigStatus::Invalid(FailureReason::SignatureMismatch)
6308 );
6309 assert_eq!(
6310 result.key_trust,
6311 super::super::KeyTrustEvidence::CallerTrusted
6312 );
6313 }
6314
6315 impl KeyResolver for PanicResolver {
6316 fn resolve<'a>(
6317 &'a self,
6318 _key_info: Option<&KeyInfo>,
6319 _algorithm: SignatureAlgorithm,
6320 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
6321 {
6322 panic!("resolver should not be called when references already fail");
6323 }
6324 }
6325
6326 struct MissingKeyResolver;
6327
6328 impl KeyResolver for MissingKeyResolver {
6329 fn resolve<'a>(
6330 &'a self,
6331 _key_info: Option<&KeyInfo>,
6332 _algorithm: SignatureAlgorithm,
6333 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
6334 {
6335 Ok(None)
6336 }
6337 }
6338
6339 struct ConsumingKeyInfoResolver;
6340
6341 impl KeyResolver for ConsumingKeyInfoResolver {
6342 fn resolve<'a>(
6343 &'a self,
6344 _key_info: Option<&KeyInfo>,
6345 _algorithm: SignatureAlgorithm,
6346 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
6347 {
6348 Ok(None)
6349 }
6350
6351 fn consumes_document_key_info(&self) -> bool {
6352 true
6353 }
6354 }
6355
6356 struct FallbackKeyInfoResolver;
6357
6358 impl KeyResolver for FallbackKeyInfoResolver {
6359 fn resolve_for_verification<'a>(
6360 &'a self,
6361 key_info: Option<&KeyInfo>,
6362 algorithm: SignatureAlgorithm,
6363 _policy: &crate::policy::VerificationPolicy,
6364 _provider: &dyn crate::provider::CryptoProvider,
6365 ) -> Result<Option<super::super::ResolvedVerificationKey<'a>>, DsigError> {
6366 self.resolve(key_info, algorithm)
6368 .map(|key| key.map(super::super::ResolvedVerificationKey::CallerTrusted))
6369 }
6370
6371 fn resolve<'a>(
6372 &'a self,
6373 key_info: Option<&KeyInfo>,
6374 _algorithm: SignatureAlgorithm,
6375 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
6376 {
6377 let sources = &key_info.expect("KeyInfo must be parsed").sources;
6378 assert!(matches!(
6379 sources.as_slice(),
6380 [
6381 super::super::parse::KeyInfoSource::RetrievalMethod { .. },
6382 super::super::parse::KeyInfoSource::KeyName(name),
6383 ] if name == "fallback"
6384 ));
6385 Ok(Some(Box::new(AcceptingKey)))
6386 }
6387
6388 fn consumes_document_key_info(&self) -> bool {
6389 true
6390 }
6391 }
6392
6393 struct EarlyKeyInfoResolver;
6394
6395 impl KeyResolver for EarlyKeyInfoResolver {
6396 fn resolve_for_verification<'a>(
6397 &'a self,
6398 key_info: Option<&KeyInfo>,
6399 algorithm: SignatureAlgorithm,
6400 _policy: &crate::policy::VerificationPolicy,
6401 _provider: &dyn crate::provider::CryptoProvider,
6402 ) -> Result<Option<super::super::ResolvedVerificationKey<'a>>, DsigError> {
6403 self.resolve(key_info, algorithm)
6405 .map(|key| key.map(super::super::ResolvedVerificationKey::CallerTrusted))
6406 }
6407
6408 fn resolve<'a>(
6409 &'a self,
6410 key_info: Option<&KeyInfo>,
6411 _algorithm: SignatureAlgorithm,
6412 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
6413 {
6414 let sources = &key_info.expect("KeyInfo must be parsed").sources;
6415 assert!(matches!(
6416 sources.as_slice(),
6417 [
6418 super::super::parse::KeyInfoSource::KeyName(name),
6419 super::super::parse::KeyInfoSource::RetrievalMethod { .. },
6420 ] if name == "primary"
6421 ));
6422 Ok(Some(Box::new(AcceptingKey)))
6423 }
6424
6425 fn consumes_document_key_info(&self) -> bool {
6426 true
6427 }
6428 }
6429
6430 fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
6431 format!(
6432 r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6433 <ds:SignedInfo>
6434 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6435 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6436 <ds:Reference URI="{reference_uri}">
6437 {transforms_xml}
6438 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
6439 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6440 </ds:Reference>
6441 </ds:SignedInfo>
6442 <ds:SignatureValue>AQ==</ds:SignatureValue>
6443</ds:Signature>"#
6444 )
6445 }
6446
6447 #[test]
6448 fn retained_verification_preserves_original_authenticated_operation() {
6449 use structured_asn1::memory::{Memory, MemoryAccount};
6452 let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(16384, 100000)));
6453 let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6454 let context = VerifyContext::new().key(&AcceptingKey);
6455 with_verified_document(
6456 &document,
6457 &context,
6458 &VerificationRequest::default(),
6459 ScopedRequestStorage(&work),
6460 |view, _, budgets, evidence, operations, _| {
6461 assert_eq!(evidence.signatures().len(), 1);
6462 assert_eq!(operations.len(), 1);
6463 let target = view.node_for_id("target", &[]).unwrap();
6464 assert!(operations[0].operation.is_authenticated(target));
6465 assert!(std::ptr::eq(*operations[0].operation.budgets(), budgets));
6466 assert!(budgets.canonicalized.remaining() < budgets.canonicalized.max_bytes);
6467 Ok(())
6468 },
6469 )
6470 .unwrap();
6471 }
6472
6473 #[test]
6474 fn retained_verification_accepts_borrowed_keys_and_admits_storage_first() {
6475 use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
6478 struct BorrowedKey<'a>(&'a Cell<usize>);
6479 impl VerifyingKey for BorrowedKey<'_> {
6480 fn validate_signature_value(
6481 &self,
6482 _: SignatureAlgorithm,
6483 _: &[u8],
6484 ) -> Result<bool, DsigError> {
6485 Ok(true)
6486 }
6487 fn verify(&self, _: SignatureAlgorithm, _: &[u8], _: &[u8]) -> Result<bool, DsigError> {
6488 self.0.set(self.0.get() + 1);
6489 Ok(true)
6490 }
6491 }
6492 let calls = Cell::new(0);
6493 let key = BorrowedKey(&calls);
6494 let context = VerifyContext::new().key(&key);
6495 let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6496 let denied = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 100000)));
6497 let result: Result<(), DsigError> = with_verified_document(
6498 &document,
6499 &context,
6500 &VerificationRequest::default(),
6501 ScopedRequestStorage(&denied),
6502 |_, _, _, _, _, _| panic!("refused storage must not reach the continuation"),
6503 );
6504 assert!(matches!(
6505 result,
6506 Err(DsigError::SharedExecution(MemoryError::LimitExceeded))
6507 ));
6508 assert_eq!(calls.get(), 0);
6509 let admitted = Memory::new(std::sync::Arc::new(MemoryAccount::new(16384, 100000)));
6510 with_verified_document(
6511 &document,
6512 &context,
6513 &VerificationRequest::default(),
6514 ScopedRequestStorage(&admitted),
6515 |_, _, _, evidence, operations, _| {
6516 assert!(evidence.all_valid());
6517 assert_eq!(operations.len(), 1);
6518 Ok(())
6519 },
6520 )
6521 .unwrap();
6522 assert_eq!(calls.get(), 1);
6523 }
6524
6525 #[test]
6526 fn verified_continuation_keeps_the_consumed_operation_budget() {
6527 let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6530 let context = VerifyContext::new().key(&AcceptingKey);
6531 let called = Cell::new(false);
6532 let evidence = with_verified_document(
6533 &document,
6534 &context,
6535 &VerificationRequest::default(),
6536 CollectedRequestStorage,
6537 |view, _captured_context, budgets, evidence, (), _| {
6538 called.set(true);
6539 assert_eq!(view.root(), evidence.document);
6540 assert!(evidence.signatures()[0].result().is_ok());
6541 let remaining = budgets.canonicalized.remaining();
6542 assert!(remaining < budgets.canonicalized.max_bytes);
6543 budgets.canonicalized.charge(remaining).unwrap();
6544 assert!(budgets.canonicalized.charge(1).is_err());
6545 Ok(evidence)
6546 },
6547 )
6548 .unwrap();
6549 assert!(called.get());
6550 assert_eq!(evidence.signatures().len(), 1);
6551 }
6552
6553 #[test]
6554 fn public_session_catalogue_replays_the_original_reference() {
6555 use structured_asn1::memory::{Memory, MemoryAccount};
6558 let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6559 let context = VerifyContext::new().key(&AcceptingKey);
6560 let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
6561 with_verified_request(
6562 &document,
6563 &context,
6564 &VerificationRequest::default(),
6565 &work,
6566 |session| {
6567 let signature = session.evidence().signatures()[0].identity();
6568 let selected = session.select_signature(signature)?;
6569 let mut count = 0;
6570 let completeness = selected
6571 .visit_authenticated_references(|reference| {
6572 count += 1;
6573 assert!(matches!(reference.parent, ReferenceParent::SignedInfo(_)));
6574 assert_eq!(reference.uri, Some("#target"));
6575 let mut bytes = Vec::new();
6576 let outcome = selected
6577 .replay_reference(
6578 reference.identity,
6579 ReplayMode::ReferenceOutput,
6580 &mut |chunk: &[u8]| {
6581 bytes.extend_from_slice(chunk);
6582 Ok::<_, ()>(())
6583 },
6584 )
6585 .unwrap();
6586 assert_eq!(outcome, ReplayOutcome::Complete);
6587 assert_eq!(bytes, b"<target ID=\"target\">payload</target>");
6588 Ok::<_, ()>(())
6589 })
6590 .unwrap();
6591 assert_eq!(completeness, CatalogueCompleteness::Complete);
6592 assert_eq!(count, 1);
6593 Ok(())
6594 },
6595 )
6596 .unwrap();
6597 }
6598
6599 #[test]
6600 fn public_session_resolves_original_attribute_and_fragmented_simple_content() {
6601 use structured_asn1::memory::{Memory, MemoryAccount};
6604 let document = XmlDocument::parse(signature_with_target_content(
6605 "AQ==",
6606 "<property URI=\"#ref\"/><content>#r<!--split--><![CDATA[ef]]></content>",
6607 ))
6608 .unwrap();
6609 let context = VerifyContext::new().key(&AcceptingKey);
6610 let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
6611 with_verified_request(
6612 &document,
6613 &context,
6614 &VerificationRequest::default(),
6615 &work,
6616 |session| {
6617 let selected =
6618 session.select_signature(session.evidence().signatures()[0].identity())?;
6619 for (name, source) in [
6620 (
6621 "property",
6622 LexicalSource::Attribute {
6623 namespace: None,
6624 local_name: "URI",
6625 },
6626 ),
6627 ("content", LexicalSource::SimpleContent),
6628 ] {
6629 let node = session
6630 .view()
6631 .document()
6632 .descendants()
6633 .find(|node| node.has_tag_name(name))
6634 .unwrap();
6635 let ReferenceResolution::Reference(reference) =
6636 selected.resolve_reference(session.view().node_identity(node), source)?
6637 else {
6638 panic!("original Reference must resolve");
6639 };
6640 assert_eq!(reference.uri, Some("#target"));
6641 }
6642 Ok(())
6643 },
6644 )
6645 .unwrap();
6646 }
6647
6648 #[test]
6649 fn public_session_manifest_instruction_survives_old_digest_mismatch() {
6650 use structured_asn1::memory::{Memory, MemoryAccount};
6653 let document = XmlDocument::parse(signature_with_manifest_xml(false)).unwrap();
6654 let context = VerifyContext::new()
6655 .key(&AcceptingKey)
6656 .process_manifests(true);
6657 let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
6658 with_verified_request(
6659 &document,
6660 &context,
6661 &VerificationRequest::default(),
6662 &work,
6663 |session| {
6664 let selected =
6665 session.select_signature(session.evidence().signatures()[0].identity())?;
6666 let mut manifests = 0;
6667 let completeness = selected
6668 .visit_authenticated_references(|reference| {
6669 if let ReferenceParent::Manifest(_) = reference.parent {
6670 manifests += 1;
6671 assert!(matches!(
6672 reference.result.unwrap().status,
6673 DsigStatus::Invalid(_)
6674 ));
6675 let mut calls = 0;
6676 let outcome = selected
6677 .replay_reference(
6678 reference.identity,
6679 ReplayMode::ReferenceOutput,
6680 &mut |_: &[u8]| {
6681 calls += 1;
6682 Ok::<_, ()>(())
6683 },
6684 )
6685 .unwrap();
6686 assert_eq!(outcome, ReplayOutcome::Complete);
6687 assert!(calls > 0);
6688 }
6689 Ok::<_, ()>(())
6690 })
6691 .unwrap();
6692 assert_eq!(manifests, 1);
6693 assert_eq!(completeness, CatalogueCompleteness::Complete);
6694 Ok(())
6695 },
6696 )
6697 .unwrap();
6698 }
6699
6700 #[test]
6701 fn public_session_replay_keeps_original_receiver_failure_sticky() {
6702 use structured_asn1::memory::{Memory, MemoryAccount};
6705 #[derive(Debug, PartialEq)]
6706 struct Refusal(u32);
6707 let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6708 let context = VerifyContext::new().key(&AcceptingKey);
6709 let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
6710 with_verified_request(
6711 &document,
6712 &context,
6713 &VerificationRequest::default(),
6714 &work,
6715 |session| {
6716 let selected =
6717 session.select_signature(session.evidence().signatures()[0].identity())?;
6718 selected
6719 .visit_authenticated_references(|reference| {
6720 let mut calls = 0;
6721 let result = selected.replay_reference(
6722 reference.identity,
6723 ReplayMode::ReferenceOutput,
6724 &mut |_: &[u8]| {
6725 calls += 1;
6726 Err(Refusal(7))
6727 },
6728 );
6729 assert!(matches!(result, Err(ReplayError::Receiver(Refusal(7)))));
6730 assert_eq!(calls, 1);
6731 Ok::<_, ()>(())
6732 })
6733 .unwrap();
6734 Ok(())
6735 },
6736 )
6737 .unwrap();
6738 }
6739
6740 #[test]
6741 fn verify_all_retains_each_signature_without_vacuous_success() {
6742 let document = XmlDocument::parse(format!(
6745 "<root xmlns:ds=\"{XMLDSIG_NS}\"><ds:Signature/><ds:Signature/></root>"
6746 ))
6747 .unwrap();
6748 let evidence = VerifyContext::new().verify_all(&document).unwrap();
6749 assert_eq!(evidence.signatures().len(), 2);
6750 assert!(
6751 evidence
6752 .signatures()
6753 .iter()
6754 .all(|entry| entry.result().is_err())
6755 );
6756 assert!(!evidence.all_valid());
6757 let empty = XmlDocument::parse("<root/>").unwrap();
6758 assert!(!VerifyContext::new().verify_all(&empty).unwrap().all_valid());
6759 }
6760
6761 #[test]
6762 fn verify_all_checks_cardinality_before_running_keys() {
6763 let document = XmlDocument::parse(format!(
6766 "<root xmlns:ds=\"{XMLDSIG_NS}\"><ds:Signature/><ds:Signature/></root>"
6767 ))
6768 .unwrap();
6769 let mut policy = crate::policy::VerificationPolicy::default();
6770 policy.resources.max_signatures = 1;
6771 assert!(matches!(
6772 VerifyContext::new().policy(policy).verify_all(&document),
6773 Err(DsigError::Policy(
6774 crate::policy::PolicyViolation::ResourceLimit {
6775 resource: "verification signatures",
6776 maximum: 1,
6777 actual: 2,
6778 }
6779 ))
6780 ));
6781 }
6782
6783 #[test]
6784 fn verification_signature_limit_also_gates_the_single_api() {
6785 let mut policy = crate::policy::VerificationPolicy::default();
6787 policy.resources.max_signatures = 0;
6788 assert!(matches!(
6789 VerifyContext::new()
6790 .policy(policy)
6791 .key(&AcceptingKey)
6792 .verify(&signature_with_target_reference("AQ==")),
6793 Err(DsigError::Policy(
6794 crate::policy::PolicyViolation::ResourceLimit {
6795 resource: "verification signatures",
6796 maximum: 0,
6797 actual: 1,
6798 }
6799 ))
6800 ));
6801 }
6802
6803 #[test]
6804 fn verify_all_preserves_generation_and_rejects_wrapped_siblings() {
6805 let xml = signature_with_target_reference("AQ==")
6807 .replace("</root>", "<target ID=\"unsigned\">payload</target></root>");
6808 let document = XmlDocument::parse(xml).unwrap();
6809 let (signed, unsigned) = document.with_view(|view| {
6810 (
6811 view.node_for_id("target", &[]).unwrap(),
6812 view.node_for_id("unsigned", &[]).unwrap(),
6813 )
6814 });
6815 let evidence = VerifyContext::new()
6816 .key(&AcceptingKey)
6817 .verify_all(&document)
6818 .unwrap();
6819 assert!(evidence.all_valid());
6820 assert!(evidence.covers_element(&document, signed).unwrap());
6821 assert!(!evidence.covers_element(&document, unsigned).unwrap());
6822 let foreign = XmlDocument::parse("<target/>").unwrap();
6823 let target = foreign.with_view(|view| view.root_element());
6824 assert!(matches!(
6825 evidence.covers_element(&document, target),
6826 Err(DsigError::Document(XmlDocumentError::ForeignIdentity))
6827 ));
6828 }
6829
6830 #[test]
6831 fn verify_request_rejects_duplicate_targets_before_crypto() {
6832 let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6834 let target = document.with_view(|view| view.node_for_id("target", &[]).unwrap());
6835 let targets = [target, target];
6836 let request = VerificationRequest {
6837 expected_targets: &targets,
6838 ..VerificationRequest::default()
6839 };
6840 assert!(matches!(
6841 VerifyContext::new().verify_request(&document, &request),
6842 Err(DsigError::InvalidRequest {
6843 reason: "duplicate expected coverage target"
6844 })
6845 ));
6846 }
6847
6848 #[test]
6849 fn verify_all_shares_canonicalization_retention_budget() {
6850 let xml = signature_with_target_reference("AQ==");
6852 let single = VerifyContext::new()
6853 .key(&AcceptingKey)
6854 .store_pre_digest(true)
6855 .verify(&xml)
6856 .unwrap();
6857 let bytes = single.canonicalized_signed_info.unwrap().len()
6858 + single.signed_info_references[0]
6859 .pre_digest_data
6860 .as_ref()
6861 .unwrap()
6862 .len();
6863 let parsed = Document::parse(&xml).unwrap();
6864 let signature = parsed
6865 .descendants()
6866 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
6867 .unwrap();
6868 let xml = xml.replace("</root>", &format!("{}</root>", &xml[signature.range()]));
6869 let document = XmlDocument::parse(xml).unwrap();
6870 let mut policy = crate::policy::VerificationPolicy::default();
6871 policy.resources.max_canonicalized_bytes = bytes * 2 - 1;
6872 let evidence = VerifyContext::new()
6873 .key(&AcceptingKey)
6874 .store_pre_digest(true)
6875 .policy(policy)
6876 .verify_all(&document)
6877 .unwrap();
6878 assert_eq!(evidence.signatures.len(), 2);
6879 assert_eq!(
6880 evidence.signatures[0].result().as_ref().unwrap().status,
6881 DsigStatus::Valid
6882 );
6883 assert!(matches!(
6884 evidence.signatures[1].result(),
6885 Err(DsigError::Policy(
6886 crate::policy::PolicyViolation::ResourceLimit {
6887 resource: "canonicalized bytes",
6888 ..
6889 }
6890 ))
6891 ));
6892 assert!(!evidence.accepted(&document).unwrap());
6893 }
6894
6895 #[test]
6896 fn verify_all_manifest_coverage_requires_its_authenticated_digest() {
6897 for valid in [true, false] {
6900 let document = XmlDocument::parse(signature_with_manifest_xml(valid)).unwrap();
6901 let target = document.with_view(|view| view.node_for_id("target", &[]).unwrap());
6902 let evidence = VerifyContext::new()
6903 .key(&AcceptingKey)
6904 .process_manifests(true)
6905 .verify_all(&document)
6906 .unwrap();
6907 let result = evidence.signatures()[0].result().as_ref().unwrap();
6908 assert_eq!(result.manifest_references.len(), 1);
6909 assert!(result.manifest_references[0].reference_identity.is_some());
6910 assert_eq!(evidence.covers_element(&document, target).unwrap(), valid);
6911 assert_eq!(evidence.accepted(&document).unwrap(), valid);
6912 }
6913 }
6914
6915 #[test]
6916 fn verify_all_filtered_digest_does_not_claim_original_subtree() {
6917 let mut reference = make_reference(
6920 "#target",
6921 vec![Transform::XpathExcludeAllSignatures],
6922 DigestAlgorithm::Sha256,
6923 vec![],
6924 );
6925 let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6926 document.with_view(|view| {
6927 let target = view.node_for_id("target", &[]).unwrap();
6928 let mut result = manifest_reference_invalid_result(
6929 &reference,
6930 0,
6931 FailureReason::ReferenceProcessingFailure { ref_index: 0 },
6932 );
6933 result.status = DsigStatus::Valid;
6934 bind_reference_evidence(
6935 &mut result,
6936 &reference,
6937 view,
6938 None,
6939 &OperationResourceIdentity::DocumentNode(target),
6940 );
6941 assert_eq!(result.target_identity, Some(target));
6942 assert_eq!(result.coverage, ReferenceCoverage::TransformedData);
6943 reference.transforms = vec![Transform::C14n(crate::c14n::C14nAlgorithm::new(
6944 crate::c14n::C14nMode::Exclusive1_0,
6945 false,
6946 ))];
6947 bind_reference_evidence(
6948 &mut result,
6949 &reference,
6950 view,
6951 None,
6952 &OperationResourceIdentity::DocumentNode(target),
6953 );
6954 assert_eq!(result.coverage, ReferenceCoverage::ElementContent);
6955 });
6956 }
6957
6958 #[test]
6959 fn verify_request_rejects_conflicting_resource_owners() {
6960 let document = XmlDocument::parse("<root/>").unwrap();
6963 let resources = HashMap::new();
6964 let request = VerificationRequest {
6965 external_resources: Some(&resources),
6966 ..VerificationRequest::default()
6967 };
6968 assert!(matches!(
6969 VerifyContext::new()
6970 .external_resources(&resources)
6971 .verify_request(&document, &request),
6972 Err(DsigError::InvalidRequest {
6973 reason: "external resources configured in both context and request"
6974 })
6975 ));
6976 }
6977
6978 fn signature_with_target_reference(signature_value_b64: &str) -> String {
6979 signature_with_target_content(signature_value_b64, "payload")
6980 .replace("<ds:Reference Id=\"ref\"", "<ds:Reference")
6981 }
6982
6983 fn signature_with_target_content(signature_value_b64: &str, content: &str) -> String {
6984 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6985 <target ID="target">payload</target>
6986 <ds:Signature>
6987 <ds:SignedInfo>
6988 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6989 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6990 <ds:Reference Id="ref" URI="#target">
6991 <ds:Transforms>
6992 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6993 </ds:Transforms>
6994 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
6995 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6996 </ds:Reference>
6997 </ds:SignedInfo>
6998 <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
6999 </ds:Signature>
7000</root>"##
7001 .replace(">payload</target>", &format!(">{content}</target>"));
7002
7003 let doc = Document::parse(&xml_template).unwrap();
7004 let sig_node = doc
7005 .descendants()
7006 .find(|node| node.is_element() && node.tag_name().name() == "Signature")
7007 .unwrap();
7008 let signed_info_node = sig_node
7009 .children()
7010 .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
7011 .unwrap();
7012 let signed_info = parse_signed_info(signed_info_node).unwrap();
7013 let reference = &signed_info.references[0];
7014 let resolver = UriReferenceResolver::new(&doc);
7015 let initial_data = resolver
7016 .dereference(reference.uri.as_deref().unwrap())
7017 .unwrap();
7018 let pre_digest =
7019 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
7020 .unwrap();
7021 let digest = compute_digest(reference.digest_method, &pre_digest);
7022 let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
7023 xml_template
7024 .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
7025 .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
7026 }
7027
7028 #[test]
7029 fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
7030 let xml = signature_with_target_reference("AQ==");
7031
7032 let result = VerifyContext::new()
7033 .verify(&xml)
7034 .expect("missing key config must be reported as verification status");
7035 assert!(
7036 matches!(
7037 result.status,
7038 DsigStatus::Invalid(FailureReason::KeyNotFound)
7039 ),
7040 "unexpected status: {:?}",
7041 result.status
7042 );
7043 }
7044
7045 #[test]
7046 fn verify_context_rejects_disallowed_uri() {
7047 let xml = minimal_signature_xml("http://example.com/external", "");
7048 let err = VerifyContext::new()
7049 .key(&RejectingKey)
7050 .verify(&xml)
7051 .expect_err("external URI should be rejected by default policy");
7052 assert!(matches!(
7053 err,
7054 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
7055 operation: "verification",
7056 ..
7057 })
7058 ));
7059 }
7060
7061 #[test]
7062 fn verify_context_bounds_effective_xml_base_components() {
7063 let mut xml = minimal_signature_xml("payload", "");
7066 for _ in 0..65 {
7067 xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
7068 }
7069 let resources = HashMap::new();
7070 let error = VerifyContext::new()
7071 .key(&AcceptingKey)
7072 .allowed_uri_types(UriTypeSet::ALL)
7073 .external_resources(&resources)
7074 .verify(&xml)
7075 .expect_err("XML Base component work must be bounded before lookup");
7076
7077 assert!(matches!(
7078 error,
7079 SignatureVerificationPipelineError::Policy(
7080 crate::policy::PolicyViolation::ResourceLimit {
7081 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
7082 maximum: 64,
7083 actual: 65,
7084 }
7085 )
7086 ));
7087 }
7088
7089 #[test]
7090 fn verify_context_bounds_cumulative_xml_base_resolution_bytes() {
7091 let mut xml = minimal_signature_xml("payload", "");
7094 for _ in 0..2 {
7095 xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
7096 }
7097 let resources = HashMap::new();
7098 let mut policy = crate::policy::VerificationPolicy::default();
7099 policy.resources.max_xml_base_resolution_bytes = 32;
7100 let error = VerifyContext::new()
7101 .policy(policy)
7102 .key(&AcceptingKey)
7103 .allowed_uri_types(UriTypeSet::ALL)
7104 .external_resources(&resources)
7105 .verify(&xml)
7106 .expect_err("cumulative XML Base copies must obey the operation budget");
7107
7108 assert!(matches!(
7109 error,
7110 SignatureVerificationPipelineError::Policy(
7111 crate::policy::PolicyViolation::ResourceLimit {
7112 resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
7113 maximum: 32,
7114 ..
7115 }
7116 )
7117 ));
7118 }
7119
7120 #[test]
7121 fn verify_context_applies_xml_base_policy_to_signed_info_c14n() {
7122 let xml = signature_with_target_reference("AQ==")
7126 .replacen(
7127 "http://www.w3.org/2001/10/xml-exc-c14n#",
7128 "http://www.w3.org/2006/12/xml-c14n11",
7129 1,
7130 )
7131 .replace(
7132 " <ds:Signature>",
7133 " <outer xml:base=\"one/\"><inner xml:base=\"two/\"><ds:Signature>",
7134 )
7135 .replace(" </ds:Signature>", " </ds:Signature></inner></outer>");
7136 let policy = crate::policy::VerificationPolicy {
7137 resources: crate::policy::ResourcePolicy {
7138 max_xml_base_components: 1,
7139 ..crate::policy::ResourcePolicy::default()
7140 },
7141 ..crate::policy::VerificationPolicy::default()
7142 };
7143
7144 let error = VerifyContext::new()
7145 .key(&AcceptingKey)
7146 .policy(policy)
7147 .verify(&xml)
7148 .expect_err("SignedInfo C14N must use the operation XML Base budget");
7149
7150 assert!(matches!(
7151 error,
7152 SignatureVerificationPipelineError::Policy(
7153 crate::policy::PolicyViolation::ResourceLimit {
7154 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
7155 maximum: 1,
7156 actual: 2,
7157 }
7158 )
7159 ));
7160 }
7161
7162 #[test]
7163 fn verify_context_classifies_signed_info_xml_base_byte_limit_as_policy() {
7164 let xml = signature_with_target_reference("AQ==")
7167 .replacen(
7168 "http://www.w3.org/2001/10/xml-exc-c14n#",
7169 "http://www.w3.org/2006/12/xml-c14n11",
7170 1,
7171 )
7172 .replace(
7173 " <ds:Signature>",
7174 " <outer xml:base=\"segment/\"><ds:Signature>",
7175 )
7176 .replace(" </ds:Signature>", " </ds:Signature></outer>");
7177 let policy = crate::policy::VerificationPolicy {
7178 resources: crate::policy::ResourcePolicy {
7179 max_xml_base_resolution_bytes: 1,
7180 ..crate::policy::ResourcePolicy::default()
7181 },
7182 ..crate::policy::VerificationPolicy::default()
7183 };
7184
7185 let error = VerifyContext::new()
7186 .key(&AcceptingKey)
7187 .policy(policy)
7188 .verify(&xml)
7189 .expect_err("SignedInfo XML Base byte exhaustion must be a policy error");
7190
7191 assert!(matches!(
7192 error,
7193 SignatureVerificationPipelineError::Policy(
7194 crate::policy::PolicyViolation::ResourceLimit {
7195 resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
7196 maximum: 1,
7197 actual,
7198 }
7199 ) if actual > 1
7200 ));
7201 }
7202
7203 #[test]
7204 fn verify_context_meters_repeated_external_dereferences() {
7205 let payload = b"payload";
7208 let digest = base64::engine::general_purpose::STANDARD.encode(
7209 crate::xmldsig::compute_digest(DigestAlgorithm::Sha1, payload),
7210 );
7211 let reference = format!(
7212 r#"<ds:Reference URI="urn:payload"><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference>"#
7213 );
7214 let xml = format!(
7215 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>"#
7216 );
7217 let resources = HashMap::from([("urn:payload".to_owned(), payload.to_vec())]);
7218 let policy = crate::policy::VerificationPolicy {
7219 uris: crate::policy::UriPolicy {
7220 references: UriTypeSet::ALL,
7221 ..crate::policy::UriPolicy::default()
7222 },
7223 resources: crate::policy::ResourcePolicy {
7224 max_external_resource_bytes: payload.len(),
7225 max_external_resource_total_bytes: payload.len(),
7226 ..crate::policy::ResourcePolicy::default()
7227 },
7228 ..crate::policy::VerificationPolicy::default()
7229 };
7230
7231 let error = VerifyContext::new()
7232 .key(&AcceptingKey)
7233 .policy(policy)
7234 .external_resources(&resources)
7235 .verify(&xml)
7236 .expect_err("the second dereference must exhaust the aggregate byte ceiling");
7237
7238 assert!(
7239 error
7240 .to_string()
7241 .contains("aggregate external resource bytes")
7242 );
7243 }
7244
7245 #[test]
7246 fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
7247 let xml = minimal_signature_xml("", "");
7248 let err = VerifyContext::new()
7249 .key(&RejectingKey)
7250 .allowed_uri_types(UriTypeSet::new(false, true, false))
7251 .verify(&xml)
7252 .expect_err("empty URI must be rejected when empty references are disabled");
7253 assert!(matches!(
7254 err,
7255 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
7256 operation: "verification",
7257 ..
7258 })
7259 ));
7260 }
7261
7262 #[test]
7263 fn verify_context_rejects_disallowed_transform() {
7264 let xml = minimal_signature_xml(
7265 "",
7266 r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
7267 );
7268 let err = VerifyContext::new()
7269 .key(&RejectingKey)
7270 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
7271 .verify(&xml)
7272 .expect_err("enveloped transform should be rejected by allowlist");
7273 assert!(matches!(
7274 err,
7275 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
7276 operation: "verification transform",
7277 ..
7278 })
7279 ));
7280 }
7281
7282 #[test]
7283 fn verify_context_applies_transform_allowlist_to_signed_info_c14n() {
7284 let xml = signature_with_target_reference("AQ==").replacen(
7287 "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
7288 "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"/>",
7289 1,
7290 );
7291 let error = VerifyContext::new()
7292 .key(&AcceptingKey)
7293 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
7294 .verify(&xml)
7295 .expect_err("SignedInfo C14N must obey the operation transform allowlist");
7296
7297 assert!(matches!(
7298 error,
7299 SignatureVerificationPipelineError::Policy(
7300 crate::policy::PolicyViolation::Algorithm {
7301 operation: "verification transform",
7302 ref algorithm,
7303 }
7304 )
7305 if algorithm == "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
7306 ));
7307 }
7308
7309 #[test]
7310 fn verify_context_applies_transform_allowlist_to_key_retrieval() {
7311 let xml = signature_with_target_reference("AQ==")
7314 .replacen(
7315 "</ds:Signature>",
7316 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>"##,
7317 1,
7318 )
7319 .replacen(
7320 "</root>",
7321 r#"<holder ID="keys"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder></root>"#,
7322 1,
7323 );
7324 let error = VerifyContext::new()
7325 .key_resolver(&ConsumingKeyInfoResolver)
7326 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
7327 .verify(&xml)
7328 .expect_err("RetrievalMethod XPath must obey the operation transform allowlist");
7329
7330 assert!(matches!(
7331 error,
7332 SignatureVerificationPipelineError::Policy(
7333 crate::policy::PolicyViolation::Algorithm {
7334 operation: "verification transform",
7335 ref algorithm,
7336 }
7337 )
7338 if algorithm == XPATH_TRANSFORM_URI
7339 ));
7340 }
7341
7342 fn signature_with_manifest_xml(valid_manifest_digest: bool) -> String {
7343 signature_with_manifest_xml_with_manifest_mutation(valid_manifest_digest, |xml| xml)
7344 }
7345
7346 fn signature_with_manifest_xml_with_manifest_mutation<F>(
7347 valid_manifest_digest: bool,
7348 mutate_manifest: F,
7349 ) -> String
7350 where
7351 F: FnOnce(String) -> String,
7352 {
7353 const TMP_SIGNED_INFO_DIGEST: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
7354 const INVALID_MANIFEST_DIGEST: &str = "//////////////////////////8=";
7355 let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7356 <target ID="target">payload</target>
7357 <ds:Signature>
7358 <ds:SignedInfo>
7359 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7360 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
7361 <ds:Reference URI="#manifest">
7362 <ds:Transforms>
7363 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7364 </ds:Transforms>
7365 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
7366 <ds:DigestValue>SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER</ds:DigestValue>
7367 </ds:Reference>
7368 </ds:SignedInfo>
7369 <ds:SignatureValue>AQ==</ds:SignatureValue>
7370 <ds:Object>
7371 <ds:Manifest ID="manifest">
7372 <ds:Reference URI="#target">
7373 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
7374 <ds:DigestValue>MANIFEST_DIGEST_PLACEHOLDER</ds:DigestValue>
7375 </ds:Reference>
7376 </ds:Manifest>
7377 </ds:Object>
7378 </ds:Signature>
7379</root>"##;
7380 let seed_xml = xml_template.replace(
7381 "SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER",
7382 TMP_SIGNED_INFO_DIGEST,
7383 );
7384 let doc = Document::parse(&seed_xml).unwrap();
7385 let signature_node = doc
7386 .descendants()
7387 .find(|node| {
7388 node.is_element()
7389 && node.tag_name().namespace() == Some(XMLDSIG_NS)
7390 && node.tag_name().name() == "Signature"
7391 })
7392 .unwrap();
7393 let resolver = UriReferenceResolver::new(&doc);
7394 let initial_data = resolver.dereference("#target").unwrap();
7395 let manifest_pre_digest =
7396 crate::xmldsig::execute_transforms(signature_node, initial_data, &[]).unwrap();
7397 let computed_manifest_digest_b64 = base64::engine::general_purpose::STANDARD
7398 .encode(compute_digest(DigestAlgorithm::Sha1, &manifest_pre_digest));
7399 let final_manifest_digest_b64 = if valid_manifest_digest {
7400 computed_manifest_digest_b64.as_str()
7401 } else {
7402 INVALID_MANIFEST_DIGEST
7403 };
7404 let xml_with_manifest_digest = mutate_manifest(
7405 seed_xml.replace("MANIFEST_DIGEST_PLACEHOLDER", final_manifest_digest_b64),
7406 );
7407 let signed_doc = Document::parse(&xml_with_manifest_digest).unwrap();
7408 let signed_signature_node = signed_doc
7409 .descendants()
7410 .find(|node| {
7411 node.is_element()
7412 && node.tag_name().namespace() == Some(XMLDSIG_NS)
7413 && node.tag_name().name() == "Signature"
7414 })
7415 .unwrap();
7416 let signed_info_node = signed_signature_node
7417 .children()
7418 .find(|node| {
7419 node.is_element()
7420 && node.tag_name().namespace() == Some(XMLDSIG_NS)
7421 && node.tag_name().name() == "SignedInfo"
7422 })
7423 .unwrap();
7424 let signed_info = parse_signed_info(signed_info_node).unwrap();
7425 let object_reference = &signed_info.references[0];
7426 let signed_resolver = UriReferenceResolver::new(&signed_doc);
7427 let signed_initial_data = signed_resolver
7428 .dereference(object_reference.uri.as_deref().unwrap())
7429 .unwrap();
7430 let signed_pre_digest = crate::xmldsig::execute_transforms(
7431 signed_signature_node,
7432 signed_initial_data,
7433 &object_reference.transforms,
7434 )
7435 .unwrap();
7436 let signed_digest_b64 = base64::engine::general_purpose::STANDARD.encode(compute_digest(
7437 object_reference.digest_method,
7438 &signed_pre_digest,
7439 ));
7440
7441 xml_with_manifest_digest.replacen(TMP_SIGNED_INFO_DIGEST, &signed_digest_b64, 1)
7442 }
7443
7444 fn replace_fixture_manifest_digest(xml: &str, replacement: &str) -> String {
7445 let object_marker = "<ds:Object>";
7446 let object_start = xml
7447 .find(object_marker)
7448 .expect("fixture should contain ds:Object")
7449 + object_marker.len();
7450 let open = "<ds:DigestValue>";
7451 let close = "</ds:DigestValue>";
7452 let value_start = xml[object_start..]
7453 .find(open)
7454 .map(|offset| object_start + offset + open.len())
7455 .expect("Manifest should contain DigestValue");
7456 let value_end = xml[value_start..]
7457 .find(close)
7458 .map(|offset| value_start + offset)
7459 .expect("Manifest DigestValue must be closed");
7460
7461 format!("{}{replacement}{}", &xml[..value_start], &xml[value_end..])
7462 }
7463
7464 #[test]
7465 fn verify_context_processes_manifest_references_when_enabled() {
7466 let xml = signature_with_manifest_xml(true);
7467
7468 let result_without_manifests = VerifyContext::new()
7469 .key(&RejectingKey)
7470 .verify(&xml)
7471 .expect("manifest processing disabled should still verify SignedInfo");
7472 assert!(
7473 result_without_manifests.manifest_references.is_empty(),
7474 "manifest results must stay empty when manifest processing is disabled",
7475 );
7476 assert!(matches!(
7477 result_without_manifests.status,
7478 DsigStatus::Invalid(FailureReason::SignatureMismatch)
7479 ));
7480
7481 let malformed_manifest_xml = signature_with_manifest_xml(true).replacen(
7482 "</ds:Object>",
7483 "</ds:Object><ds:Object><ds:Manifest><ds:Foo/></ds:Manifest></ds:Object>",
7484 1,
7485 );
7486 let malformed_with_manifests_disabled = VerifyContext::new()
7487 .key(&RejectingKey)
7488 .verify(&malformed_manifest_xml)
7489 .expect("malformed Manifest must be ignored when manifest processing is disabled");
7490 assert!(
7491 malformed_with_manifests_disabled
7492 .manifest_references
7493 .is_empty(),
7494 "manifest parser must not run when process_manifests is disabled",
7495 );
7496 assert!(matches!(
7497 malformed_with_manifests_disabled.status,
7498 DsigStatus::Invalid(FailureReason::SignatureMismatch)
7499 ));
7500
7501 let result_with_manifests = VerifyContext::new()
7502 .key(&AcceptingKey)
7503 .process_manifests(true)
7504 .verify(&xml)
7505 .expect("manifest references should be processed when enabled");
7506 assert_eq!(result_with_manifests.manifest_references.len(), 1);
7507 assert_eq!(
7508 result_with_manifests.manifest_references[0].reference_set,
7509 ReferenceSet::Manifest
7510 );
7511 assert_eq!(
7512 result_with_manifests.manifest_references[0].reference_index,
7513 0
7514 );
7515 assert!(matches!(
7516 result_with_manifests.manifest_references[0].status,
7517 DsigStatus::Valid
7518 ));
7519 assert!(matches!(result_with_manifests.status, DsigStatus::Valid));
7520 }
7521
7522 #[test]
7523 fn verify_context_skips_manifest_work_when_signature_value_is_invalid() {
7524 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7528 replace_fixture_manifest_digest(&xml, "!!!")
7529 });
7530 assert!(
7531 xml.split_once("<ds:Object>")
7532 .is_some_and(|(_, object)| object.contains("<ds:DigestValue>!!!</ds:DigestValue>")),
7533 "fixture mutation must corrupt the nested Manifest DigestValue",
7534 );
7535
7536 let result = VerifyContext::new()
7537 .key(&RejectingKey)
7538 .process_manifests(true)
7539 .verify(&xml)
7540 .expect("invalid SignatureValue must short-circuit Manifest parsing");
7541
7542 assert!(matches!(
7543 result.status,
7544 DsigStatus::Invalid(FailureReason::SignatureMismatch)
7545 ));
7546 assert!(result.manifest_references.is_empty());
7547 }
7548
7549 #[test]
7550 fn verify_context_shares_xpath_parse_budget_with_manifest_references() {
7551 let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
7554 .repeat(64);
7555 let transform = format!(
7556 r#"<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</ds:Transform>"#
7557 );
7558 let max_transforms = transform.repeat(16);
7559 let max_manifest_reference = format!(
7560 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>"##
7561 );
7562 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7563 xml.replacen(
7564 r##"<ds:Reference URI="#target">"##,
7565 &format!(
7566 r##"<ds:Reference URI="#target"><ds:Transforms>{}</ds:Transforms>"##,
7567 max_transforms
7568 ),
7569 1,
7570 )
7571 .replacen(
7572 "</ds:SignedInfo>",
7573 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>"##,
7574 1,
7575 )
7576 .replacen(
7577 "</ds:Manifest>",
7578 &format!("{}</ds:Manifest>", max_manifest_reference.repeat(3)),
7579 1,
7580 )
7581 });
7582
7583 let error = VerifyContext::new()
7584 .key(&AcceptingKey)
7585 .process_manifests(true)
7586 .verify(&xml)
7587 .expect_err("SignedInfo and Manifest References must share one XPath parse budget");
7588
7589 assert!(
7590 matches!(
7591 &error,
7592 SignatureVerificationPipelineError::Policy(
7593 crate::policy::PolicyViolation::ResourceLimit {
7594 resource: "XPath expressions",
7595 ..
7596 }
7597 )
7598 ),
7599 "unexpected error: {error:?}"
7600 );
7601 }
7602
7603 #[test]
7604 fn verify_context_processes_manifest_when_signedinfo_references_object() {
7605 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7606 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
7607 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
7608 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
7609 });
7610
7611 let result = VerifyContext::new()
7612 .key(&AcceptingKey)
7613 .process_manifests(true)
7614 .verify(&xml)
7615 .expect("manifest references should be processed when SignedInfo references ds:Object");
7616 assert_eq!(
7617 result.manifest_references.len(),
7618 1,
7619 "signed ds:Object should enable processing of its direct-child ds:Manifest",
7620 );
7621 assert_eq!(
7622 result.manifest_references[0].reference_set,
7623 ReferenceSet::Manifest
7624 );
7625 assert_eq!(result.manifest_references[0].reference_index, 0);
7626 assert!(matches!(
7627 result.manifest_references[0].status,
7628 DsigStatus::Valid
7629 ));
7630 }
7631
7632 #[test]
7633 fn verify_context_skips_manifest_removed_by_enveloped_transform() {
7634 for target_object in [false, true] {
7638 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7639 let xml = xml.replacen(
7640 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
7641 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#"/>"#,
7642 1,
7643 );
7644 if target_object {
7645 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
7646 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
7647 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
7648 } else {
7649 xml
7650 }
7651 });
7652
7653 let result = VerifyContext::new()
7654 .key(&AcceptingKey)
7655 .process_manifests(true)
7656 .store_pre_digest(true)
7657 .verify(&xml)
7658 .expect("an emptied reference remains a valid core digest input");
7659
7660 assert!(matches!(result.status, DsigStatus::Valid));
7661 assert_eq!(
7662 result.signed_info_references[0].pre_digest_data.as_deref(),
7663 Some([].as_slice()),
7664 "target_object={target_object} must have empty transformed bytes",
7665 );
7666 assert!(
7667 result.manifest_references.is_empty(),
7668 "target_object={target_object} must not authenticate the Manifest",
7669 );
7670 }
7671 }
7672
7673 #[test]
7674 fn verify_context_ignores_manifest_excluded_from_signed_object() {
7675 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7679 xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
7680 .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
7681 .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
7682 .replacen(
7683 r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
7684 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#"/>"#,
7685 1,
7686 )
7687 });
7688
7689 let result = VerifyContext::new()
7690 .key(&AcceptingKey)
7691 .process_manifests(true)
7692 .verify(&xml)
7693 .expect("excluded Manifest content must be ignored, not parsed");
7694
7695 assert!(matches!(result.status, DsigStatus::Valid));
7696 assert!(
7697 result.manifest_references.is_empty(),
7698 "a transform-excluded Manifest is not authenticated by SignedInfo",
7699 );
7700 }
7701
7702 #[test]
7703 fn verify_context_skips_manifest_digest_work_when_signature_is_invalid() {
7704 let xml = signature_with_manifest_xml(false);
7705 let result = VerifyContext::new()
7706 .key(&RejectingKey)
7707 .process_manifests(true)
7708 .verify(&xml)
7709 .expect("invalid SignatureValue must short-circuit Manifest digest work");
7710 assert!(result.manifest_references.is_empty());
7711 assert!(matches!(
7712 result.status,
7713 DsigStatus::Invalid(FailureReason::SignatureMismatch)
7714 ));
7715 }
7716
7717 #[test]
7718 fn verify_context_manifest_digest_mismatch_is_non_fatal_with_accepting_key() {
7719 let xml = signature_with_manifest_xml(false);
7720 let result = VerifyContext::new()
7721 .key(&AcceptingKey)
7722 .process_manifests(true)
7723 .verify(&xml)
7724 .expect("manifest digest mismatches should be recorded while signature stays valid");
7725 assert_eq!(result.manifest_references.len(), 1);
7726 assert!(matches!(
7727 result.manifest_references[0].status,
7728 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
7729 ));
7730 assert!(matches!(result.status, DsigStatus::Valid));
7731 }
7732
7733 #[test]
7734 fn invalid_outer_manifest_digest_does_not_parse_nested_manifest() {
7735 let xml = signature_with_manifest_xml_with_manifest_mutation(false, |xml| {
7739 xml.replacen("URI=\"#target\"", "URI=\"#inner-object\"", 1)
7740 .replacen(
7741 "</ds:Object>",
7742 "</ds:Object><ds:Object ID=\"inner-object\"><ds:Manifest>junk<ds:Unexpected/></ds:Manifest></ds:Object>",
7743 1,
7744 )
7745 });
7746
7747 let result = VerifyContext::new()
7748 .key(&AcceptingKey)
7749 .process_manifests(true)
7750 .verify(&xml)
7751 .expect("an unauthenticated nested Manifest must remain opaque");
7752
7753 assert_eq!(result.manifest_references.len(), 1);
7754 assert!(matches!(
7755 result.manifest_references[0].status,
7756 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
7757 ));
7758 }
7759
7760 #[test]
7761 fn verify_context_skips_manifest_parsing_when_signedinfo_reference_fails() {
7762 let xml = signature_with_manifest_xml(true);
7765 let (signed_info_prefix, object_suffix) = xml
7766 .split_once("<ds:Object>")
7767 .expect("fixture should contain ds:Object");
7768 let open = "<ds:DigestValue>";
7769 let close = "</ds:DigestValue>";
7770 let digest_start = signed_info_prefix
7771 .find(open)
7772 .expect("SignedInfo should contain DigestValue");
7773 let digest_end = signed_info_prefix[digest_start + open.len()..]
7774 .find(close)
7775 .map(|offset| digest_start + open.len() + offset)
7776 .expect("SignedInfo DigestValue must be closed");
7777 let broken_signed_info_prefix = format!(
7778 "{}{}AAAAAAAAAAAAAAAAAAAAAAAAAAA={}{}",
7779 &signed_info_prefix[..digest_start],
7780 open,
7781 close,
7782 &signed_info_prefix[digest_end + close.len()..],
7783 );
7784 let broken_xml = format!("{broken_signed_info_prefix}<ds:Object>{object_suffix}");
7785 let result = VerifyContext::new()
7786 .key(&RejectingKey)
7787 .process_manifests(true)
7788 .verify(&broken_xml)
7789 .expect("SignedInfo digest failure should return without parsing Manifests");
7790 assert!(matches!(
7791 result.status,
7792 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
7793 ));
7794 assert!(
7795 result.manifest_references.is_empty(),
7796 "unauthenticated Manifest content must not be parsed",
7797 );
7798 }
7799
7800 #[test]
7801 fn verify_context_skips_manifest_policy_work_when_signature_is_invalid() {
7802 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7805 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
7806 });
7807 let result = VerifyContext::new()
7808 .key(&RejectingKey)
7809 .process_manifests(true)
7810 .verify(&broken_xml)
7811 .expect("invalid SignatureValue must short-circuit Manifest policy work");
7812 assert!(result.manifest_references.is_empty());
7813 assert!(matches!(
7814 result.status,
7815 DsigStatus::Invalid(FailureReason::SignatureMismatch)
7816 ));
7817 }
7818
7819 #[test]
7820 fn verify_context_records_manifest_policy_violations_with_accepting_key() {
7821 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7822 xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
7823 });
7824 let result = VerifyContext::new()
7825 .key(&AcceptingKey)
7826 .process_manifests(true)
7827 .verify(&broken_xml)
7828 .expect("manifest policy violations should be recorded while signature stays valid");
7829 assert_eq!(result.manifest_references.len(), 1);
7830 assert!(matches!(
7831 result.manifest_references[0].status,
7832 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
7833 ));
7834 assert!(matches!(result.status, DsigStatus::Valid));
7835 }
7836
7837 #[test]
7838 fn verify_context_applies_digest_policy_to_manifest_references() {
7839 let policy = crate::policy::VerificationPolicy {
7842 manifest_processing: crate::policy::ManifestProcessing::Process,
7843 digest_algorithms: Some(HashSet::from([DigestAlgorithm::Sha1])),
7844 ..crate::policy::VerificationPolicy::default()
7845 };
7846 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
7847 let legacy = "http://www.w3.org/2000/09/xmldsig#sha1";
7848 let offset = xml
7849 .rfind(legacy)
7850 .expect("Manifest DigestMethod must be present");
7851 xml.replace_range(offset..offset + legacy.len(), DigestAlgorithm::Sha256.uri());
7852 let value_start = xml[offset..]
7853 .find("<ds:DigestValue>")
7854 .map(|relative| offset + relative + "<ds:DigestValue>".len())
7855 .expect("Manifest DigestValue must be present");
7856 let value_end = xml[value_start..]
7857 .find("</ds:DigestValue>")
7858 .map(|relative| value_start + relative)
7859 .expect("Manifest DigestValue must be closed");
7860 xml.replace_range(
7861 value_start..value_end,
7862 &base64::engine::general_purpose::STANDARD.encode([0_u8; 32]),
7863 );
7864 xml
7865 });
7866 let result = VerifyContext::new()
7867 .key(&AcceptingKey)
7868 .policy(policy)
7869 .verify(&xml)
7870 .expect("a disallowed Manifest digest is a per-reference result");
7871
7872 assert!(matches!(result.status, DsigStatus::Valid));
7873 assert!(matches!(
7874 result.manifest_references[0].status,
7875 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
7876 ));
7877 }
7878
7879 #[test]
7880 fn verify_context_applies_transform_count_policy_to_manifest_references() {
7881 let policy = crate::policy::VerificationPolicy {
7884 manifest_processing: crate::policy::ManifestProcessing::Process,
7885 resources: crate::policy::ResourcePolicy {
7886 max_transforms_per_reference: 1,
7887 ..crate::policy::ResourcePolicy::default()
7888 },
7889 ..crate::policy::VerificationPolicy::default()
7890 };
7891 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
7892 let manifest_start = xml
7893 .find("<ds:Manifest")
7894 .expect("fixture must contain a Manifest");
7895 let manifest = xml[manifest_start..].replacen(
7896 "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
7897 concat!(
7898 "<ds:Transforms>",
7899 "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
7900 "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
7901 "</ds:Transforms>",
7902 "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>"
7903 ),
7904 1,
7905 );
7906 xml.replace_range(manifest_start.., &manifest);
7907 xml
7908 });
7909 let result = VerifyContext::new()
7910 .key(&AcceptingKey)
7911 .policy(policy)
7912 .verify(&xml)
7913 .expect("Manifest transform policy is a per-reference result");
7914
7915 assert!(matches!(result.status, DsigStatus::Valid));
7916 assert!(matches!(
7917 result.manifest_references[0].status,
7918 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
7919 ));
7920 }
7921
7922 #[test]
7923 fn verify_context_skips_manifest_uri_work_when_signature_is_invalid() {
7924 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7927 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
7928 });
7929
7930 let result = VerifyContext::new()
7931 .key(&RejectingKey)
7932 .process_manifests(true)
7933 .verify(&broken_xml)
7934 .expect("invalid SignatureValue must short-circuit Manifest URI processing");
7935 assert!(result.manifest_references.is_empty());
7936 assert!(matches!(
7937 result.status,
7938 DsigStatus::Invalid(FailureReason::SignatureMismatch)
7939 ));
7940 }
7941
7942 #[test]
7943 fn verify_context_records_manifest_missing_uri_with_accepting_key() {
7944 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7945 xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
7946 });
7947
7948 let result = VerifyContext::new()
7949 .key(&AcceptingKey)
7950 .process_manifests(true)
7951 .verify(&broken_xml)
7952 .expect("manifest missing URI should be recorded while signature stays valid");
7953 assert_eq!(result.manifest_references.len(), 1);
7954 assert_eq!(result.manifest_references[0].uri, "<omitted>");
7955 assert!(matches!(
7956 result.manifest_references[0].status,
7957 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
7958 ));
7959 assert!(matches!(result.status, DsigStatus::Valid));
7960 }
7961
7962 #[test]
7963 fn verify_context_processes_authenticated_nested_manifests_in_object() {
7964 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7968 xml.replacen(
7969 "<ds:Manifest ID=\"manifest\">",
7970 "<wrapper><ds:Manifest ID=\"manifest\">",
7971 1,
7972 )
7973 .replacen("</ds:Manifest>", "</ds:Manifest></wrapper>", 1)
7974 });
7975
7976 let result = VerifyContext::new()
7977 .key(&AcceptingKey)
7978 .process_manifests(true)
7979 .verify(&xml)
7980 .expect("authenticated nested Manifest is processed");
7981 assert_eq!(result.manifest_references.len(), 1);
7982 assert_eq!(result.manifest_references[0].uri, "#target");
7983 assert_eq!(result.manifest_references[0].status, DsigStatus::Valid);
7984 assert!(matches!(result.status, DsigStatus::Valid));
7985 }
7986
7987 #[test]
7988 fn verify_context_reports_manifest_reference_parse_errors_explicitly() {
7989 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7992 replace_fixture_manifest_digest(&xml, "!!!")
7993 });
7994
7995 let err = VerifyContext::new()
7996 .key(&AcceptingKey)
7997 .process_manifests(true)
7998 .verify(&broken_xml)
7999 .expect_err("invalid Manifest DigestValue must map to ParseManifestReference");
8000 assert!(matches!(
8001 err,
8002 SignatureVerificationPipelineError::ParseManifestReference(_)
8003 ));
8004 }
8005
8006 #[test]
8007 fn verify_context_reports_unsupported_manifest_transform_with_declared_digest() {
8008 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
8011 let xml = xml.replacen(
8012 "<ds:Reference URI=\"#target\">",
8013 "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
8014 1,
8015 );
8016 let xml = xml.replacen(
8017 "</ds:Transforms>\n <ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
8018 "</ds:Transforms>\n <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>",
8019 1,
8020 );
8021 replace_fixture_manifest_digest(&xml, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
8022 });
8023 assert!(xml.contains("urn:unsupported"));
8024 assert!(xml.contains("http://www.w3.org/2001/04/xmlenc#sha256"));
8025
8026 let result = VerifyContext::new()
8027 .key(&AcceptingKey)
8028 .process_manifests(true)
8029 .verify(&xml)
8030 .expect("unsupported Manifest transform is a per-reference result");
8031 assert_eq!(result.status, DsigStatus::Valid);
8032 assert_eq!(result.manifest_references.len(), 1);
8033 assert_eq!(
8034 result.manifest_references[0].digest_algorithm,
8035 DigestAlgorithm::Sha256
8036 );
8037 assert!(matches!(
8038 result.manifest_references[0].status,
8039 DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
8040 ));
8041
8042 let restricted = VerifyContext::new()
8043 .key(&AcceptingKey)
8044 .process_manifests(true)
8045 .allowed_transforms([
8046 DEFAULT_IMPLICIT_C14N_URI,
8047 "http://www.w3.org/2001/10/xml-exc-c14n#",
8048 ])
8049 .verify(&xml)
8050 .expect("a disallowed Manifest transform is a per-reference policy result");
8051 assert!(matches!(
8052 restricted.manifest_references[0].status,
8053 DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
8054 ));
8055 }
8056
8057 #[test]
8058 fn manifest_reference_limit_counts_unsupported_entries() {
8059 let references = (0..=MAX_REFERENCES_PER_SIGNATURE)
8060 .map(|index| {
8061 format!(
8062 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>"##
8063 )
8064 })
8065 .collect::<String>();
8066 let xml = format!(
8067 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>"#
8068 );
8069 let document = XmlDocument::parse(xml).unwrap();
8070 let error = document.with_view(|view| {
8071 let signature = view.document().root_element();
8072 let object = signature.children().find(|node| node.is_element()).unwrap();
8073 let policy = crate::policy::VerificationPolicy::default();
8074 let budgets = VerificationOperationBudgets::with_transforms(
8075 &policy,
8076 TransformExecutionBudget::from_resources(&policy.resources),
8077 None,
8078 );
8079 let operation = OperationExecutionContext::new(
8080 &policy,
8081 &budgets,
8082 Some((view.identity(), view.generation())),
8083 );
8084 operation.authenticate(view.node_identity(object));
8085 let mut processed = HashSet::new();
8086 let mut remaining = MAX_REFERENCES_PER_SIGNATURE;
8087 let mut next_index = 0;
8088 let mut xpath_parse = XPathSignatureParseBudget::default();
8089 let mut state = ManifestDiscoveryState {
8090 processed: &mut processed,
8091 remaining_capacity: &mut remaining,
8092 next_reference_index: &mut next_index,
8093 xpath_parse: &mut xpath_parse,
8094 };
8095 match parse_manifest_references(signature, &operation, view, &mut state, None) {
8096 Ok(_) => panic!("unsupported references must consume the same aggregate limit"),
8097 Err(error) => error,
8098 }
8099 });
8100 assert!(matches!(
8101 error,
8102 SignatureVerificationPipelineError::InvalidStructure {
8103 reason: "signed Manifests exceed the per-signature Reference limit"
8104 }
8105 ));
8106 }
8107
8108 #[test]
8109 fn unsigned_manifest_discovery_cannot_bypass_the_filter_work_budget() {
8110 let document = XmlDocument::parse(format!(
8113 "<ds:Signature xmlns:ds=\"{XMLDSIG_NS}\"><ds:Object><ds:Manifest/></ds:Object></ds:Signature>"
8114 )).unwrap();
8115 document.with_view(|view| {
8116 let mut policy = crate::policy::VerificationPolicy::default();
8117 policy.resources.max_node_set_filter_work = 0;
8118 let budgets = VerificationOperationBudgets::with_transforms(
8119 &policy,
8120 TransformExecutionBudget::from_resources(&policy.resources),
8121 None,
8122 );
8123 let operation = OperationExecutionContext::new(
8124 &policy,
8125 &budgets,
8126 Some((view.identity(), view.generation())),
8127 );
8128 let mut processed = HashSet::new();
8129 let mut remaining = 1;
8130 let mut index = 0;
8131 let mut xpath = XPathSignatureParseBudget::default();
8132 let result = parse_manifest_references(
8133 view.document().root_element(),
8134 &operation,
8135 view,
8136 &mut ManifestDiscoveryState {
8137 processed: &mut processed,
8138 remaining_capacity: &mut remaining,
8139 next_reference_index: &mut index,
8140 xpath_parse: &mut xpath,
8141 },
8142 None,
8143 );
8144 assert!(result.is_err(), "unsigned scans are not free");
8145 });
8146 }
8147
8148 #[test]
8149 fn unsigned_manifest_remains_eligible_after_trust_expands() {
8150 let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
8154 let xml = format!(
8155 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>"##
8156 );
8157 let document = XmlDocument::parse(xml).expect("nested Manifest fixture must parse");
8158 document.with_view(|view| {
8159 let signature = view.document().root_element();
8160 let mut objects = signature.children().filter(|node| node.is_element());
8161 let outer = objects.next().expect("outer Object");
8162 let inner = objects.next().expect("inner Object");
8163 let policy = crate::policy::VerificationPolicy::default();
8164 let budgets = VerificationOperationBudgets::with_transforms(
8165 &policy,
8166 TransformExecutionBudget::from_resources(&policy.resources),
8167 None,
8168 );
8169 let operation = OperationExecutionContext::new(
8170 &policy,
8171 &budgets,
8172 Some((view.identity(), view.generation())),
8173 );
8174 operation.authenticate(view.node_identity(outer));
8175 let mut processed = HashSet::new();
8176 let mut remaining = 2;
8177 let mut next_index = 0;
8178 let mut xpath_budget = XPathSignatureParseBudget::default();
8179
8180 let first = parse_manifest_references(
8181 signature,
8182 &operation,
8183 view,
8184 &mut ManifestDiscoveryState {
8185 processed: &mut processed,
8186 remaining_capacity: &mut remaining,
8187 next_reference_index: &mut next_index,
8188 xpath_parse: &mut xpath_budget,
8189 },
8190 None,
8191 )
8192 .expect("outer Manifest must be discovered");
8193 assert_eq!(first.references.len(), 1);
8194 assert_eq!(first.references[0].reference.uri.as_deref(), Some("#inner"));
8195
8196 operation.authenticate(view.node_identity(inner));
8197 let second = parse_manifest_references(
8198 signature,
8199 &operation,
8200 view,
8201 &mut ManifestDiscoveryState {
8202 processed: &mut processed,
8203 remaining_capacity: &mut remaining,
8204 next_reference_index: &mut next_index,
8205 xpath_parse: &mut xpath_budget,
8206 },
8207 None,
8208 )
8209 .expect("newly authenticated sibling Manifest must remain eligible");
8210 assert_eq!(second.references.len(), 1);
8211 assert_eq!(
8212 second.references[0].reference.uri.as_deref(),
8213 Some("#payload")
8214 );
8215 });
8216 }
8217
8218 #[test]
8219 fn manifest_reference_limit_includes_signed_info_references() {
8220 let xml = signature_with_manifest_xml(true);
8223 let reference_start = xml
8224 .find(r##"<ds:Reference URI="#manifest">"##)
8225 .expect("fixture SignedInfo must reference the Manifest");
8226 let reference_end = xml[reference_start..]
8227 .find("</ds:Reference>")
8228 .map(|offset| reference_start + offset + "</ds:Reference>".len())
8229 .expect("fixture SignedInfo Reference must be closed");
8230 let repeated = xml[reference_start..reference_end].repeat(MAX_REFERENCES_PER_SIGNATURE);
8231 let xml = format!(
8232 "{}{repeated}{}",
8233 &xml[..reference_start],
8234 &xml[reference_end..]
8235 );
8236
8237 let error = VerifyContext::new()
8238 .key(&AcceptingKey)
8239 .process_manifests(true)
8240 .verify(&xml)
8241 .expect_err("one Manifest Reference must exceed the exhausted signature-wide limit");
8242
8243 assert!(matches!(
8244 error,
8245 SignatureVerificationPipelineError::InvalidStructure {
8246 reason: "signed Manifests exceed the per-signature Reference limit"
8247 }
8248 ));
8249 }
8250
8251 #[test]
8252 fn configured_reference_limit_is_shared_with_manifests() {
8253 let policy = crate::policy::VerificationPolicy {
8256 manifest_processing: crate::policy::ManifestProcessing::Process,
8257 resources: crate::policy::ResourcePolicy {
8258 max_references: 1,
8259 ..crate::policy::ResourcePolicy::default()
8260 },
8261 ..crate::policy::VerificationPolicy::default()
8262 };
8263
8264 let error = VerifyContext::new()
8265 .key(&AcceptingKey)
8266 .policy(policy)
8267 .verify(&signature_with_manifest_xml(true))
8268 .expect_err("Manifest must exceed the caller-selected aggregate limit");
8269 assert!(matches!(
8270 error,
8271 SignatureVerificationPipelineError::InvalidStructure {
8272 reason: "signed Manifests exceed the per-signature Reference limit"
8273 }
8274 ));
8275 }
8276
8277 #[test]
8278 fn retrieval_method_materializes_single_x509_data_subtree() {
8279 for uri in [
8280 "#target",
8281 "#xpointer(id('target'))",
8282 "#xpointer(id("target"))",
8283 ] {
8284 for target_xml in [
8285 r#"<ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>"#,
8286 r#"<holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>"#,
8287 ] {
8288 let xml = format!(
8289 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>"#
8290 );
8291 let document = Document::parse(&xml).unwrap();
8292 let key_info_node = document
8293 .descendants()
8294 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8295 .unwrap();
8296 let mut key_info = parse_key_info(key_info_node).unwrap();
8297 let resolver = UriReferenceResolver::new(&document);
8298
8299 materialize_retrieval_methods(
8300 &mut key_info,
8301 &resolver,
8302 UriTypeSet::SAME_DOCUMENT,
8303 None,
8304 crate::provider::default_provider(),
8305 )
8306 .expect("XPath filter must produce one X509Data-rooted node-set");
8307 assert!(matches!(
8308 key_info.sources.as_slice(),
8309 [super::super::parse::KeyInfoSource::X509Data(info)]
8310 if info.subject_names == ["CN=leaf"]
8311 ));
8312 }
8313 }
8314 }
8315
8316 fn retrieval_method_xpath_signature() -> String {
8317 format!(
8318 r##"<root xmlns:ds="{XMLDSIG_NS}">
8319 <payload Id="payload">ok</payload>
8320 <ds:Signature>
8321 <ds:SignedInfo>
8322 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
8323 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
8324 <ds:Reference URI="#payload">
8325 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
8326 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
8327 </ds:Reference>
8328 </ds:SignedInfo>
8329 <ds:SignatureValue>AQ==</ds:SignatureValue>
8330 <ds:KeyInfo>
8331 <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
8332 <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
8333 <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
8334 </ds:Transform></ds:Transforms>
8335 </ds:RetrievalMethod>
8336 </ds:KeyInfo>
8337 </ds:Signature>
8338 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
8339 </root>"##
8340 )
8341 }
8342
8343 #[test]
8344 fn retrieval_method_xpath_uses_signature_expression_budget() {
8345 let mut policy = crate::policy::VerificationPolicy::default();
8348 policy.resources.max_xpath_expressions = 0;
8349
8350 let error = VerifyContext::new()
8351 .policy(policy)
8352 .verify(&retrieval_method_xpath_signature())
8353 .expect_err("RetrievalMethod XPath must consume the signature parse budget");
8354
8355 assert!(
8356 matches!(
8357 &error,
8358 SignatureVerificationPipelineError::Policy(
8359 crate::policy::PolicyViolation::ResourceLimit {
8360 resource: "XPath expressions",
8361 maximum: 0,
8362 actual: 1,
8363 }
8364 )
8365 ),
8366 "unexpected error: {error:?}"
8367 );
8368 }
8369
8370 #[test]
8371 fn retrieval_method_xpath_obeys_expression_byte_limit() {
8372 let expression = "ancestor-or-self::ds:X509Data";
8375 let padded_expression = format!(" {expression} ");
8376 let xml = retrieval_method_xpath_signature().replace(expression, &padded_expression);
8377 let mut policy = crate::policy::VerificationPolicy::default();
8378 policy.resources.max_xpath_expression_bytes = expression.len();
8379
8380 let error = VerifyContext::new()
8381 .policy(policy)
8382 .verify(&xml)
8383 .expect_err("RetrievalMethod XPath must obey the expression byte limit");
8384
8385 assert!(
8386 matches!(
8387 &error,
8388 SignatureVerificationPipelineError::Policy(
8389 crate::policy::PolicyViolation::ResourceLimit {
8390 resource: "XPath expression bytes",
8391 maximum,
8392 actual,
8393 }
8394 ) if *maximum == expression.len() && *actual == padded_expression.len()
8395 ),
8396 "unexpected error: {error:?}"
8397 );
8398 }
8399
8400 #[test]
8401 fn retrieval_method_xpath_obeys_expression_complexity_limit() {
8402 let mut policy = crate::policy::VerificationPolicy::default();
8405 policy.resources.max_xpath_expression_complexity = 0;
8406
8407 let error = VerifyContext::new()
8408 .policy(policy)
8409 .verify(&retrieval_method_xpath_signature())
8410 .expect_err("RetrievalMethod XPath must obey the complexity limit");
8411
8412 assert!(
8413 matches!(
8414 &error,
8415 SignatureVerificationPipelineError::Policy(
8416 crate::policy::PolicyViolation::ResourceLimit {
8417 resource: "XPath expression complexity",
8418 maximum: 0,
8419 actual,
8420 }
8421 ) if *actual > 0
8422 ),
8423 "unexpected error: {error:?}"
8424 );
8425 }
8426
8427 #[test]
8428 fn retrieval_method_xpath_uses_node_filter_work_budget() {
8429 let mut policy = crate::policy::VerificationPolicy::default();
8432 policy.resources.max_node_set_filter_work = 4;
8433 let xml = retrieval_method_xpath_signature().replace(
8434 "<holder Id=\"target\">",
8435 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
8436 );
8437
8438 let error = VerifyContext::new()
8439 .policy(policy)
8440 .verify(&xml)
8441 .expect_err("RetrievalMethod XPath must consume node-filter work");
8442
8443 assert!(
8444 matches!(
8445 &error,
8446 SignatureVerificationPipelineError::Policy(
8447 crate::policy::PolicyViolation::ResourceLimit {
8448 resource: crate::policy::resource_name::NODE_SET_FILTER_WORK,
8449 maximum: 4,
8450 actual,
8451 }
8452 ) if *actual > 4
8453 ),
8454 "unexpected error: {error:?}"
8455 );
8456 }
8457
8458 #[test]
8459 fn retrieval_method_xpath_charges_every_context_to_evaluation_work() {
8460 let mut policy = crate::policy::VerificationPolicy::default();
8463 policy.resources.max_xpath_evaluation_work = 4;
8464 let xml = retrieval_method_xpath_signature().replace(
8465 "<holder Id=\"target\">",
8466 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
8467 );
8468
8469 let error = VerifyContext::new()
8470 .policy(policy)
8471 .verify(&xml)
8472 .expect_err("RetrievalMethod XPath must charge every evaluation context");
8473
8474 assert!(matches!(
8475 error,
8476 SignatureVerificationPipelineError::Policy(
8477 crate::policy::PolicyViolation::ResourceLimit {
8478 resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
8479 maximum: 4,
8480 actual,
8481 }
8482 ) if actual > 4
8483 ));
8484 }
8485
8486 #[test]
8487 fn retrieval_method_xpath_obeys_namespace_binding_limit() {
8488 let mut policy = crate::policy::VerificationPolicy::default();
8491 policy.resources.max_xpath_namespace_bindings = 0;
8492
8493 let error = VerifyContext::new()
8494 .policy(policy)
8495 .verify(&retrieval_method_xpath_signature())
8496 .expect_err("RetrievalMethod XPath namespaces must obey the binding limit");
8497
8498 assert!(matches!(
8499 error,
8500 SignatureVerificationPipelineError::Policy(
8501 crate::policy::PolicyViolation::ResourceLimit {
8502 resource: crate::policy::resource_name::XPATH_NAMESPACE_BINDINGS,
8503 maximum: 0,
8504 actual,
8505 }
8506 ) if actual > 0
8507 ));
8508 }
8509
8510 #[test]
8511 fn retrieval_method_xpath_obeys_namespace_byte_limit() {
8512 let mut policy = crate::policy::VerificationPolicy::default();
8515 policy.resources.max_xpath_namespace_bytes = 0;
8516
8517 let error = VerifyContext::new()
8518 .policy(policy)
8519 .verify(&retrieval_method_xpath_signature())
8520 .expect_err("RetrievalMethod XPath namespaces must obey the byte limit");
8521
8522 assert!(matches!(
8523 error,
8524 SignatureVerificationPipelineError::Policy(
8525 crate::policy::PolicyViolation::ResourceLimit {
8526 resource: crate::policy::resource_name::XPATH_NAMESPACE_BYTES,
8527 maximum: 0,
8528 actual,
8529 }
8530 ) if actual > 0
8531 ));
8532 }
8533
8534 #[test]
8535 fn retrieval_method_xpath_obeys_context_evaluation_limit() {
8536 let mut policy = crate::policy::VerificationPolicy::default();
8540 policy.resources.max_xpath_context_evaluations = 4;
8541 let xml = retrieval_method_xpath_signature().replace(
8542 "<holder Id=\"target\">",
8543 "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
8544 );
8545
8546 let error = VerifyContext::new()
8547 .policy(policy)
8548 .verify(&xml)
8549 .expect_err("RetrievalMethod XPath contexts must obey the evaluation limit");
8550
8551 assert!(matches!(
8552 error,
8553 SignatureVerificationPipelineError::Policy(
8554 crate::policy::PolicyViolation::ResourceLimit {
8555 resource: crate::policy::resource_name::XPATH_CONTEXT_EVALUATIONS,
8556 maximum: 4,
8557 actual,
8558 }
8559 ) if actual > 4
8560 ));
8561 }
8562
8563 #[test]
8564 fn retrieval_method_materializes_direct_untransformed_x509_data() {
8565 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8568 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
8569 <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
8570 </root>"##;
8571 let document = Document::parse(xml).unwrap();
8572 let key_info_node = document
8573 .descendants()
8574 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8575 .unwrap();
8576 let mut key_info = parse_key_info(key_info_node).unwrap();
8577
8578 materialize_retrieval_methods(
8579 &mut key_info,
8580 &UriReferenceResolver::new(&document),
8581 UriTypeSet::SAME_DOCUMENT,
8582 None,
8583 crate::provider::default_provider(),
8584 )
8585 .expect("a direct X509Data target needs no transform");
8586 assert!(matches!(
8587 key_info.sources.as_slice(),
8588 [super::super::parse::KeyInfoSource::X509Data(info)]
8589 if info.subject_names == ["CN=leaf"]
8590 ));
8591 }
8592
8593 #[test]
8594 fn retrieval_method_respects_configured_same_document_id_semantics() {
8595 fn materialize(
8598 uri: &str,
8599 id: &str,
8600 semantics: crate::policy::SameDocumentIdSemantics,
8601 ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
8602 let xml = format!(
8603 r#"<root xmlns:ds="{XMLDSIG_NS}">
8604 <ds:KeyInfo><ds:RetrievalMethod URI="{uri}" Type="{XMLDSIG_NS}X509Data"/></ds:KeyInfo>
8605 <ds:X509Data Id="{id}"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
8606 </root>"#
8607 );
8608 let document = Document::parse(&xml).unwrap();
8609 let key_info_node = document
8610 .descendants()
8611 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8612 .unwrap();
8613 let mut key_info = parse_key_info(key_info_node).unwrap();
8614 let resolver =
8615 UriReferenceResolver::new(&document).with_same_document_id_semantics(semantics);
8616
8617 materialize_retrieval_methods(
8618 &mut key_info,
8619 &resolver,
8620 UriTypeSet::SAME_DOCUMENT,
8621 None,
8622 crate::provider::default_provider(),
8623 )
8624 }
8625
8626 assert!(
8627 materialize(
8628 "#12345",
8629 "12345",
8630 crate::policy::SameDocumentIdSemantics::Specification,
8631 )
8632 .is_err(),
8633 "the standards mode must reject a non-NCName bare fragment"
8634 );
8635 assert!(
8636 materialize(
8637 "#visa'3d",
8638 "visa'3d",
8639 crate::policy::SameDocumentIdSemantics::XmlSecBarename,
8640 )
8641 .is_err(),
8642 "the donor barename wrapper cannot represent an apostrophe"
8643 );
8644 assert!(
8645 materialize(
8646 "#visa'3d",
8647 "visa'3d",
8648 crate::policy::SameDocumentIdSemantics::XmlSecVisa3d,
8649 )
8650 .is_ok(),
8651 "Visa3D mode resolves the registered ID without an XPointer literal"
8652 );
8653 }
8654
8655 #[test]
8656 fn raw_x509_retrieval_method_uses_inherited_xml_base() {
8657 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
8660 let xml = format!(
8661 r#"<root xml:base="https://example.test/keys/nested/" xmlns:ds="{XMLDSIG_NS}">
8662 <ds:KeyInfo><ds:RetrievalMethod URI="../signer.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>
8663 </root>"#
8664 );
8665 let document = Document::parse(&xml).unwrap();
8666 let key_info_node = document
8667 .descendants()
8668 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8669 .unwrap();
8670 let mut key_info = parse_key_info(key_info_node).unwrap();
8671 let certificate = include_bytes!(
8672 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
8673 )
8674 .to_vec();
8675 let resources = HashMap::from([(
8676 "https://example.test/keys/signer.der".to_string(),
8677 certificate,
8678 )]);
8679 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
8680
8681 materialize_retrieval_methods(
8682 &mut key_info,
8683 &resolver,
8684 UriTypeSet::ALL,
8685 None,
8686 crate::provider::default_provider(),
8687 )
8688 .expect("RetrievalMethod should resolve against inherited xml:base");
8689
8690 assert!(matches!(
8691 key_info.sources.as_slice(),
8692 [super::super::parse::KeyInfoSource::X509Data(info)]
8693 if info.certificates.len() == 1
8694 ));
8695 }
8696
8697 #[test]
8698 fn retrieval_method_requires_xpath_for_x509_data_below_uri_root() {
8699 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8702 <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
8703 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
8704 </root>"##;
8705 let document = Document::parse(xml).unwrap();
8706 let key_info_node = document
8707 .descendants()
8708 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8709 .unwrap();
8710 let mut key_info = parse_key_info(key_info_node).unwrap();
8711
8712 let error = materialize_retrieval_methods(
8713 &mut key_info,
8714 &UriReferenceResolver::new(&document),
8715 UriTypeSet::SAME_DOCUMENT,
8716 None,
8717 crate::provider::default_provider(),
8718 )
8719 .expect_err("a wrapper target requires an explicit selection transform");
8720 assert!(matches!(
8721 error,
8722 SignatureVerificationPipelineError::InvalidStructure {
8723 reason: "untransformed X509Data RetrievalMethod must target X509Data directly"
8724 }
8725 ));
8726 }
8727
8728 #[test]
8729 fn retrieval_method_rejects_target_inside_external_x509_data_ancestor() {
8730 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8733 <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>
8734 <ds:X509Data><ds:X509SubjectName Id="target">CN=leaf</ds:X509SubjectName></ds:X509Data>
8735 </root>"##;
8736 let document = Document::parse(xml).unwrap();
8737 let key_info_node = document
8738 .descendants()
8739 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8740 .unwrap();
8741 let mut key_info = parse_key_info(key_info_node).unwrap();
8742
8743 let error = materialize_retrieval_methods(
8744 &mut key_info,
8745 &UriReferenceResolver::new(&document),
8746 UriTypeSet::SAME_DOCUMENT,
8747 None,
8748 crate::provider::default_provider(),
8749 )
8750 .expect_err("filter output without an X509Data root must be rejected");
8751 assert!(matches!(
8752 error,
8753 SignatureVerificationPipelineError::InvalidStructure {
8754 reason: "X509Data RetrievalMethod selected no X509Data element"
8755 }
8756 ));
8757 }
8758
8759 #[test]
8760 fn retrieval_method_rejects_ambiguous_x509_data_relation() {
8761 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8763 <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>
8764 <holder Id="target"><ds:X509Data/><ds:X509Data/></holder>
8765 </root>"##;
8766 let document = Document::parse(xml).unwrap();
8767 let key_info_node = document
8768 .descendants()
8769 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8770 .unwrap();
8771 let mut key_info = parse_key_info(key_info_node).unwrap();
8772
8773 let error = materialize_retrieval_methods(
8774 &mut key_info,
8775 &UriReferenceResolver::new(&document),
8776 UriTypeSet::SAME_DOCUMENT,
8777 None,
8778 crate::provider::default_provider(),
8779 )
8780 .expect_err("multiple transformed X509Data roots must be rejected");
8781 assert!(matches!(
8782 error,
8783 SignatureVerificationPipelineError::InvalidStructure {
8784 reason: "X509Data RetrievalMethod selected multiple X509Data elements"
8785 }
8786 ));
8787 }
8788
8789 #[test]
8790 fn retrieval_method_materialization_preserves_key_info_order() {
8791 let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8794 <ds:KeyInfo>
8795 <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>
8796 <ds:KeyName>fallback</ds:KeyName>
8797 </ds:KeyInfo>
8798 <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
8799 </root>"##;
8800 let document = Document::parse(xml).unwrap();
8801 let key_info_node = document
8802 .descendants()
8803 .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8804 .unwrap();
8805 let mut key_info = parse_key_info(key_info_node).unwrap();
8806
8807 materialize_retrieval_methods(
8808 &mut key_info,
8809 &UriReferenceResolver::new(&document),
8810 UriTypeSet::SAME_DOCUMENT,
8811 None,
8812 crate::provider::default_provider(),
8813 )
8814 .unwrap();
8815 assert!(matches!(
8816 key_info.sources.as_slice(),
8817 [
8818 super::super::parse::KeyInfoSource::X509Data(_),
8819 super::super::parse::KeyInfoSource::KeyName(name)
8820 ] if name == "fallback"
8821 ));
8822 }
8823
8824 #[test]
8825 fn retrieval_method_materialization_bounds_repeated_sources() {
8826 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
8829 let certificate = include_bytes!(
8830 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
8831 )
8832 .to_vec();
8833 let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
8834 let mut key_info = KeyInfo {
8835 sources: (0..=64)
8836 .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
8837 uri: "urn:certificate".into(),
8838 resource_type: Some(RAW_X509_TYPE.into()),
8839 transforms: RetrievalMethodTransforms::None,
8840 })
8841 .collect(),
8842 };
8843 let document = Document::parse("<root/>").unwrap();
8844 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
8845
8846 let error = materialize_retrieval_methods(
8847 &mut key_info,
8848 &resolver,
8849 UriTypeSet::ALL,
8850 None,
8851 crate::provider::default_provider(),
8852 )
8853 .expect_err("retrieval count must be bounded before materialization");
8854 assert!(matches!(
8855 error,
8856 SignatureVerificationPipelineError::InvalidStructure {
8857 reason: "KeyInfo contains too many RetrievalMethod elements"
8858 }
8859 ));
8860 }
8861
8862 #[test]
8863 fn key_info_reference_materializes_an_allowed_empty_uri() {
8864 let document = XmlDocument::parse(format!(
8868 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>root-key</ds:KeyName></ds:KeyInfo>"#
8869 ))
8870 .unwrap();
8871 let mut key_info = KeyInfo {
8872 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
8873 uri: String::new(),
8874 }],
8875 };
8876 let mut policy = crate::policy::VerificationPolicy::default();
8877 policy.key_sources.key_info_reference = true;
8878 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
8879 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
8880 let mut budgets = RetrievalMaterializationBudgets {
8881 key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
8882 &policy.resources,
8883 )),
8884 xpath_parse: &mut xpath_parse_budget,
8885 execution: &execution_budget,
8886 resources: &policy.resources,
8887 certificate_signature_algorithms: policy
8888 .key_trust
8889 .certificate_signature_algorithms
8890 .as_ref(),
8891 xml_backend: crate::XmlBackend::default(),
8892 };
8893 let mut materialization = KeyInfoMaterializationState::default();
8894
8895 document
8896 .with_view(|view| {
8897 materialize_key_info_references_with_budgets(
8898 &mut key_info,
8899 &UriReferenceResolver::with_document_view(view, &[]),
8900 &policy,
8901 crate::provider::default_provider(),
8902 &mut budgets,
8903 &mut materialization,
8904 )?;
8905 Ok::<_, SignatureVerificationPipelineError>(())
8906 })
8907 .unwrap();
8908
8909 assert!(matches!(
8910 key_info.sources.as_slice(),
8911 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "root-key"
8912 ));
8913 }
8914
8915 #[test]
8916 fn raw_certificate_retrieval_retains_spent_operation_work() {
8917 let pair = rcgen::KeyPair::generate().expect("test key generation");
8919 let cert = rcgen::CertificateParams::new(Vec::<String>::new())
8920 .expect("empty SAN list")
8921 .self_signed(&pair)
8922 .expect("test certificate signing");
8923 let external = HashMap::from([("urn:cert".to_owned(), cert.der().to_vec())]);
8924 let document = Document::parse("<root/>").expect("test XML");
8925 let resolver = UriReferenceResolver::new(&document).with_external_resources(&external);
8926 let mut policy = crate::policy::VerificationPolicy::default();
8927 policy.resources.max_x509_work = 1;
8928 policy.uris.retrieval_methods = UriTypeSet::ALL;
8929 let usage = RefCell::new(super::super::parse::KeyInfoParseUsage::new(
8930 &policy.resources,
8931 ));
8932 usage
8933 .borrow()
8934 .certificates
8935 .memory()
8936 .charge_work(1)
8937 .expect("spend allowance");
8938 let mut xpath = XPathSignatureParseBudget::from_resources(&policy.resources);
8939 let execution = TransformExecutionBudget::from_resources(&policy.resources);
8940 let mut budgets = RetrievalMaterializationBudgets {
8941 key_info_parse: &usage,
8942 xpath_parse: &mut xpath,
8943 execution: &execution,
8944 resources: &policy.resources,
8945 certificate_signature_algorithms: None,
8946 xml_backend: crate::XmlBackend::default(),
8947 };
8948 let mut info = KeyInfo {
8949 sources: vec![super::super::parse::KeyInfoSource::RetrievalMethod {
8950 uri: "urn:cert".into(),
8951 resource_type: Some("http://www.w3.org/2000/09/xmldsig#rawX509Certificate".into()),
8952 transforms: RetrievalMethodTransforms::None,
8953 }],
8954 };
8955 let result = materialize_retrieval_methods_with_budgets(
8956 &mut info,
8957 &resolver,
8958 &policy,
8959 crate::provider::default_provider(),
8960 &mut budgets,
8961 &mut 0,
8962 );
8963 assert!(matches!(
8964 result,
8965 Err(SignatureVerificationPipelineError::Policy(
8966 crate::policy::PolicyViolation::ResourceLimitExceeded {
8967 resource: crate::policy::resource_name::X509_WORK,
8968 maximum: 1,
8969 }
8970 ))
8971 ));
8972 }
8973
8974 #[test]
8975 fn empty_key_info_reference_participates_in_cycle_detection() {
8976 let document = XmlDocument::parse(format!(
8980 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#"><dsig11:KeyInfoReference URI=""/></ds:KeyInfo>"#
8981 ))
8982 .unwrap();
8983 let mut key_info = KeyInfo {
8984 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
8985 uri: String::new(),
8986 }],
8987 };
8988 let mut policy = crate::policy::VerificationPolicy::default();
8989 policy.key_sources.key_info_reference = true;
8990 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
8991 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
8992 let mut budgets = RetrievalMaterializationBudgets {
8993 key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
8994 &policy.resources,
8995 )),
8996 xpath_parse: &mut xpath_parse_budget,
8997 execution: &execution_budget,
8998 resources: &policy.resources,
8999 certificate_signature_algorithms: policy
9000 .key_trust
9001 .certificate_signature_algorithms
9002 .as_ref(),
9003 xml_backend: crate::XmlBackend::default(),
9004 };
9005 let mut materialization = KeyInfoMaterializationState::default();
9006
9007 let error = document
9008 .with_view(|view| {
9009 materialize_key_info_references_with_budgets(
9010 &mut key_info,
9011 &UriReferenceResolver::with_document_view(view, &[]),
9012 &policy,
9013 crate::provider::default_provider(),
9014 &mut budgets,
9015 &mut materialization,
9016 )
9017 })
9018 .expect_err("empty-URI self-reference must be rejected as a cycle");
9019
9020 assert!(matches!(
9021 error,
9022 SignatureVerificationPipelineError::InvalidStructure {
9023 reason: "KeyInfoReference cycle detected"
9024 }
9025 ));
9026 }
9027
9028 #[test]
9029 fn key_info_materialization_shares_candidate_work_across_source_kinds() {
9030 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
9033 let document = XmlDocument::parse(format!(
9034 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>"##
9035 ))
9036 .unwrap();
9037 let certificate = include_bytes!(
9038 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
9039 )
9040 .to_vec();
9041 let resources = HashMap::from([("signer.der".to_owned(), certificate)]);
9042 let mut key_info = KeyInfo {
9043 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9044 uri: "#target".into(),
9045 }],
9046 };
9047 let mut policy = crate::policy::VerificationPolicy::default();
9048 policy.key_sources.key_info_reference = true;
9049 policy.uris.retrieval_methods = UriTypeSet::ALL;
9050 policy.resources.max_key_candidates = 2;
9051 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
9052 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
9053 let mut budgets = RetrievalMaterializationBudgets {
9054 key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
9055 &policy.resources,
9056 )),
9057 xpath_parse: &mut xpath_parse_budget,
9058 execution: &execution_budget,
9059 resources: &policy.resources,
9060 certificate_signature_algorithms: policy
9061 .key_trust
9062 .certificate_signature_algorithms
9063 .as_ref(),
9064 xml_backend: crate::XmlBackend::default(),
9065 };
9066 let mut materialization = KeyInfoMaterializationState::default();
9067
9068 let error = document
9069 .with_view(|view| {
9070 let resolver = UriReferenceResolver::with_document_view(view, &[])
9071 .with_external_resources(&resources);
9072 let mut outcome = materialize_key_info_references_with_budgets(
9073 &mut key_info,
9074 &resolver,
9075 &policy,
9076 crate::provider::default_provider(),
9077 &mut budgets,
9078 &mut materialization,
9079 )?;
9080 outcome.merge(materialize_retrieval_methods_with_budgets(
9081 &mut key_info,
9082 &resolver,
9083 &policy,
9084 crate::provider::default_provider(),
9085 &mut budgets,
9086 &mut materialization.candidate_work,
9087 )?);
9088 Ok::<_, DsigError>(outcome)
9089 })
9090 .expect_err("all materializers must share the candidate-work limit");
9091
9092 assert!(matches!(
9093 error,
9094 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
9095 resource: crate::policy::resource_name::KEY_CANDIDATES,
9096 maximum: 2,
9097 actual: 3,
9098 })
9099 ));
9100 }
9101
9102 #[test]
9103 fn public_key_info_materialization_binds_external_resource_policy() {
9104 let document = Document::parse("<root/>").unwrap();
9107 let external = format!(
9108 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
9109 );
9110 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
9111 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9112 let mut key_info = KeyInfo {
9113 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9114 uri: "key.xml".into(),
9115 }],
9116 };
9117 let mut policy = crate::policy::VerificationPolicy::default();
9118 policy.key_sources.key_info_reference = true;
9119 policy.uris.key_info_references = UriTypeSet::ALL;
9120 policy.resources.max_external_resource_bytes = 0;
9121
9122 let error = materialize_verification_key_info_references(
9123 &mut key_info,
9124 resolver,
9125 &policy,
9126 crate::provider::default_provider(),
9127 crate::XmlBackend::default(),
9128 )
9129 .expect_err("policy must reject non-empty external KeyInfo bytes");
9130
9131 assert!(matches!(
9132 error,
9133 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
9134 resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
9135 maximum: 0,
9136 actual,
9137 }) if actual == resources["key.xml"].len()
9138 ));
9139 }
9140
9141 #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
9142 #[test]
9143 fn public_key_info_materialization_uses_the_selected_backend() {
9144 let external = format!(
9147 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
9148 );
9149 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
9150 let document = Document::parse("<root/>").unwrap();
9151
9152 for backend in [crate::XmlBackend::Xmloxide, crate::XmlBackend::Roxmltree] {
9153 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9154 let mut key_info = KeyInfo {
9155 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9156 uri: "key.xml".into(),
9157 }],
9158 };
9159 let mut policy = crate::policy::VerificationPolicy::default();
9160 policy.key_sources.key_info_reference = true;
9161 policy.uris.key_info_references = UriTypeSet::ALL;
9162
9163 materialize_verification_key_info_references(
9164 &mut key_info,
9165 resolver,
9166 &policy,
9167 crate::provider::default_provider(),
9168 backend,
9169 )
9170 .unwrap_or_else(|error| panic!("{backend:?} materialization failed: {error}"));
9171
9172 assert!(matches!(
9173 key_info.sources.as_slice(),
9174 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "external"
9175 ));
9176 }
9177 }
9178
9179 #[test]
9180 fn public_key_info_materialization_defers_nested_retrieval_failure() {
9181 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
9185 let external = format!(
9186 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>usable</ds:KeyName><ds:RetrievalMethod URI="missing.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>"#
9187 );
9188 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
9189 let document = Document::parse("<root/>").unwrap();
9190
9191 for operation in ["signing", "verification"] {
9192 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9193 let mut key_info = KeyInfo {
9194 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9195 uri: "key.xml".into(),
9196 }],
9197 };
9198 match operation {
9199 "signing" => {
9200 let mut policy = crate::policy::SigningPolicy::default();
9201 policy.uris.key_info_references = UriTypeSet::ALL;
9202 policy.uris.retrieval_methods = UriTypeSet::ALL;
9203 materialize_signing_key_info_references(
9204 &mut key_info,
9205 resolver,
9206 &policy,
9207 crate::provider::default_provider(),
9208 crate::XmlBackend::default(),
9209 )
9210 }
9211 "verification" => {
9212 let mut policy = crate::policy::VerificationPolicy::default();
9213 policy.key_sources.key_info_reference = true;
9214 policy.uris.key_info_references = UriTypeSet::ALL;
9215 policy.uris.retrieval_methods = UriTypeSet::ALL;
9216 materialize_verification_key_info_references(
9217 &mut key_info,
9218 resolver,
9219 &policy,
9220 crate::provider::default_provider(),
9221 crate::XmlBackend::default(),
9222 )
9223 }
9224 _ => unreachable!(),
9225 }
9226 .unwrap_or_else(|error| panic!("{operation} materialization failed: {error}"));
9227
9228 assert!(matches!(
9229 key_info.sources.as_slice(),
9230 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "usable"
9231 ));
9232 }
9233 }
9234
9235 #[test]
9236 fn public_key_info_materialization_binds_xpath_policy() {
9237 let external = format!(
9240 r##"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}">
9241 <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
9242 <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
9243 <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
9244 </ds:Transform></ds:Transforms>
9245 </ds:RetrievalMethod>
9246 <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
9247 </ds:KeyInfo>"##
9248 );
9249 let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
9250 let document = Document::parse("<root/>").unwrap();
9251
9252 for operation in ["signing", "verification"] {
9253 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9254 let mut key_info = KeyInfo {
9255 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9256 uri: "key.xml".into(),
9257 }],
9258 };
9259 let error = match operation {
9260 "signing" => {
9261 let mut policy = crate::policy::SigningPolicy::default();
9262 policy.uris.key_info_references = UriTypeSet::ALL;
9263 policy.resources.max_xpath_expressions = 0;
9264 materialize_signing_key_info_references(
9265 &mut key_info,
9266 resolver,
9267 &policy,
9268 crate::provider::default_provider(),
9269 crate::XmlBackend::default(),
9270 )
9271 }
9272 "verification" => {
9273 let mut policy = crate::policy::VerificationPolicy::default();
9274 policy.key_sources.key_info_reference = true;
9275 policy.uris.key_info_references = UriTypeSet::ALL;
9276 policy.resources.max_xpath_expressions = 0;
9277 materialize_verification_key_info_references(
9278 &mut key_info,
9279 resolver,
9280 &policy,
9281 crate::provider::default_provider(),
9282 crate::XmlBackend::default(),
9283 )
9284 }
9285 _ => unreachable!(),
9286 }
9287 .expect_err("the public helper must enforce the supplied XPath expression limit");
9288
9289 assert!(
9290 matches!(
9291 error,
9292 DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
9293 resource: crate::policy::resource_name::XPATH_EXPRESSIONS,
9294 maximum: 0,
9295 actual: 1,
9296 })
9297 ),
9298 "unexpected {operation} error: {error:?}"
9299 );
9300 }
9301 }
9302
9303 fn materialize_external_key_info_chain(
9304 terminal_reference: &str,
9305 ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
9306 let a = format!(
9307 r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
9308 <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
9309 <ds:KeyInfo ID="next"><dsig11:KeyInfoReference URI="b.xml#root"/></ds:KeyInfo>
9310 </doc>"##
9311 );
9312 let b = format!(
9313 r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
9314 <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
9315 <ds:KeyInfo ID="next">{terminal_reference}</ds:KeyInfo>
9316 </doc>"##
9317 );
9318 let resources = HashMap::from([
9319 ("a.xml".to_owned(), a.into_bytes()),
9320 ("b.xml".to_owned(), b.into_bytes()),
9321 ]);
9322 let document = XmlDocument::parse("<root/>").unwrap();
9323 let mut key_info = KeyInfo {
9324 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9325 uri: "a.xml#root".into(),
9326 }],
9327 };
9328 let mut policy = crate::policy::VerificationPolicy::default();
9329 policy.key_sources.key_info_reference = true;
9330 policy.uris.key_info_references = UriTypeSet::ALL;
9331 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
9332 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
9333 let mut budgets = RetrievalMaterializationBudgets {
9334 key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
9335 &policy.resources,
9336 )),
9337 xpath_parse: &mut xpath_parse_budget,
9338 execution: &execution_budget,
9339 resources: &policy.resources,
9340 certificate_signature_algorithms: policy
9341 .key_trust
9342 .certificate_signature_algorithms
9343 .as_ref(),
9344 xml_backend: crate::XmlBackend::default(),
9345 };
9346 let mut materialization = KeyInfoMaterializationState::default();
9347
9348 document.with_view(|view| {
9349 let resolver = UriReferenceResolver::with_document_view(view, &[])
9350 .with_external_resources(&resources);
9351 materialize_key_info_references_with_budgets(
9352 &mut key_info,
9353 &resolver,
9354 &policy,
9355 crate::provider::default_provider(),
9356 &mut budgets,
9357 &mut materialization,
9358 )?;
9359 Ok::<_, SignatureVerificationPipelineError>(())
9360 })?;
9361 Ok(key_info)
9362 }
9363
9364 fn materialize_external_key_info_bytes_with_limit(
9365 encoded: Vec<u8>,
9366 maximum_document_bytes: usize,
9367 ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
9368 let resources = HashMap::from([("key.xml".to_owned(), encoded)]);
9369 let document = XmlDocument::parse("<root/>").unwrap();
9370 let mut key_info = KeyInfo {
9371 sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9372 uri: "key.xml".into(),
9373 }],
9374 };
9375 let mut policy = crate::policy::VerificationPolicy::default();
9376 policy.key_sources.key_info_reference = true;
9377 policy.uris.key_info_references = UriTypeSet::ALL;
9378 policy.resources.max_xml_document_bytes = maximum_document_bytes;
9379 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
9380 let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
9381 let mut budgets = RetrievalMaterializationBudgets {
9382 key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
9383 &policy.resources,
9384 )),
9385 xpath_parse: &mut xpath_parse_budget,
9386 execution: &execution_budget,
9387 resources: &policy.resources,
9388 certificate_signature_algorithms: policy
9389 .key_trust
9390 .certificate_signature_algorithms
9391 .as_ref(),
9392 xml_backend: crate::XmlBackend::default(),
9393 };
9394 let mut materialization = KeyInfoMaterializationState::default();
9395
9396 document.with_view(|view| {
9397 let resolver = UriReferenceResolver::with_document_view(view, &[])
9398 .with_external_resources(&resources);
9399 materialize_key_info_references_with_budgets(
9400 &mut key_info,
9401 &resolver,
9402 &policy,
9403 crate::provider::default_provider(),
9404 &mut budgets,
9405 &mut materialization,
9406 )?;
9407 Ok::<_, SignatureVerificationPipelineError>(())
9408 })?;
9409 Ok(key_info)
9410 }
9411
9412 fn materialize_external_key_info_bytes(
9413 encoded: Vec<u8>,
9414 ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
9415 materialize_external_key_info_bytes_with_limit(
9416 encoded,
9417 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
9418 )
9419 }
9420
9421 #[test]
9422 fn external_key_info_reference_decodes_utf16_xml_octets() {
9423 let xml = format!(
9426 r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>utf16-key</ds:KeyName></ds:KeyInfo>"#
9427 );
9428 let mut encoded = vec![0xff, 0xfe];
9429 encoded.extend(xml.encode_utf16().flat_map(u16::to_le_bytes));
9430 let key_info = materialize_external_key_info_bytes(encoded)
9431 .expect("UTF-16 external KeyInfo must materialize");
9432 assert!(matches!(
9433 key_info.sources.as_slice(),
9434 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "utf16-key"
9435 ));
9436 }
9437
9438 #[test]
9439 fn external_key_info_reference_rejects_malformed_xml_encoding() {
9440 let error = materialize_external_key_info_bytes(vec![0xff, 0xfe, b'<'])
9443 .expect_err("truncated UTF-16 must be rejected during octet decoding");
9444
9445 assert!(matches!(
9446 error,
9447 SignatureVerificationPipelineError::InvalidStructure {
9448 reason: "KeyInfoReference external resource has an invalid XML encoding"
9449 }
9450 ));
9451 }
9452
9453 #[test]
9454 fn external_key_info_reference_preserves_decoded_document_limit() {
9455 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();
9458 let maximum = encoded.len();
9459 let error = materialize_external_key_info_bytes_with_limit(encoded, maximum)
9460 .expect_err("decoded XML expansion must retain resource-limit classification");
9461
9462 assert!(matches!(
9463 error,
9464 SignatureVerificationPipelineError::Policy(
9465 crate::policy::PolicyViolation::ResourceLimit {
9466 resource: crate::policy::resource_name::XML_DOCUMENT,
9467 maximum: observed_maximum,
9468 actual,
9469 }
9470 ) if observed_maximum == maximum && actual > maximum
9471 ));
9472 }
9473
9474 #[test]
9475 fn key_info_reference_cycle_identity_includes_the_owning_resource() {
9476 let key_info = materialize_external_key_info_chain("<ds:KeyName>terminal</ds:KeyName>")
9479 .expect("cross-document duplicate fragments must materialize");
9480 assert!(matches!(
9481 key_info.sources.as_slice(),
9482 [super::super::parse::KeyInfoSource::KeyName(name)] if name == "terminal"
9483 ));
9484 }
9485
9486 #[test]
9487 fn key_info_reference_cycle_identity_survives_external_reparse() {
9488 let error =
9491 materialize_external_key_info_chain("<dsig11:KeyInfoReference URI=\"a.xml#root\"/>")
9492 .expect_err("a resource cycle must fail before exhausting depth");
9493 assert!(matches!(
9494 error,
9495 SignatureVerificationPipelineError::InvalidStructure {
9496 reason: "KeyInfoReference cycle detected"
9497 }
9498 ));
9499 }
9500
9501 #[test]
9502 fn retrieval_method_materialization_deduplicates_within_count_limit() {
9503 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
9506 let certificate = include_bytes!(
9507 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
9508 )
9509 .to_vec();
9510 let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
9511 let mut key_info = KeyInfo {
9512 sources: (0..MAX_RETRIEVAL_METHOD_COUNT)
9513 .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
9514 uri: "urn:certificate".into(),
9515 resource_type: Some(RAW_X509_TYPE.into()),
9516 transforms: RetrievalMethodTransforms::None,
9517 })
9518 .collect(),
9519 };
9520 let document = Document::parse("<root/>").unwrap();
9521 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9522
9523 materialize_retrieval_methods(
9524 &mut key_info,
9525 &resolver,
9526 UriTypeSet::ALL,
9527 None,
9528 crate::provider::default_provider(),
9529 )
9530 .unwrap();
9531 assert!(matches!(
9532 key_info.sources.as_slice(),
9533 [super::super::parse::KeyInfoSource::X509Data(info)]
9534 if info.certificates.len() == 1
9535 ));
9536 }
9537
9538 #[test]
9539 fn retrieval_method_candidate_budget_includes_embedded_key_values() {
9540 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
9543 let resources = HashMap::from([("urn:certificate".to_string(), vec![1, 2, 3])]);
9544 let mut key_info = KeyInfo {
9545 sources: vec![
9546 super::super::parse::KeyInfoSource::KeyValue(
9547 super::super::parse::KeyValueInfo::Unsupported {
9548 namespace: Some(XMLDSIG_NS.into()),
9549 local_name: "FutureKeyValue".into(),
9550 },
9551 ),
9552 super::super::parse::KeyInfoSource::RetrievalMethod {
9553 uri: "urn:certificate".into(),
9554 resource_type: Some(RAW_X509_TYPE.into()),
9555 transforms: RetrievalMethodTransforms::None,
9556 },
9557 ],
9558 };
9559 let document = Document::parse("<root/>").unwrap();
9560 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9561 let mut xpath_parse_budget = XPathSignatureParseBudget::default();
9562 let execution_budget = TransformExecutionBudget::default();
9563 let resource_policy = crate::policy::ResourcePolicy {
9564 max_key_candidates: 1,
9565 ..crate::policy::ResourcePolicy::default()
9566 };
9567 let mut budgets = RetrievalMaterializationBudgets {
9568 key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
9569 &resource_policy,
9570 )),
9571 xpath_parse: &mut xpath_parse_budget,
9572 execution: &execution_budget,
9573 resources: &resource_policy,
9574 certificate_signature_algorithms: None,
9575 xml_backend: crate::XmlBackend::default(),
9576 };
9577 let mut candidate_work = key_info.embedded_candidate_count();
9578
9579 let mut policy = crate::policy::VerificationPolicy::default();
9580 policy.uris.retrieval_methods = UriTypeSet::ALL;
9581 let error = materialize_retrieval_methods_with_budgets(
9582 &mut key_info,
9583 &resolver,
9584 &policy,
9585 crate::provider::default_provider(),
9586 &mut budgets,
9587 &mut candidate_work,
9588 )
9589 .expect_err("the retrieved certificate must exceed the aggregate candidate limit");
9590
9591 assert!(matches!(
9592 error,
9593 SignatureVerificationPipelineError::Policy(
9594 crate::policy::PolicyViolation::ResourceLimit {
9595 resource: crate::policy::resource_name::KEY_CANDIDATES,
9596 maximum: 1,
9597 actual: 2,
9598 }
9599 )
9600 ));
9601 }
9602
9603 #[test]
9604 fn raw_x509_retrieval_rejects_empty_same_document_uri() {
9605 const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
9608 let certificate = include_bytes!(
9609 "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
9610 )
9611 .to_vec();
9612 let resources = HashMap::from([(String::new(), certificate)]);
9613 let mut key_info = KeyInfo {
9614 sources: vec![super::super::parse::KeyInfoSource::RetrievalMethod {
9615 uri: String::new(),
9616 resource_type: Some(RAW_X509_TYPE.into()),
9617 transforms: RetrievalMethodTransforms::None,
9618 }],
9619 };
9620 let document = Document::parse("<root/>").unwrap();
9621 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9622
9623 let error = materialize_retrieval_methods(
9624 &mut key_info,
9625 &resolver,
9626 UriTypeSet::ALL,
9627 None,
9628 crate::provider::default_provider(),
9629 )
9630 .expect_err("empty URI must retain same-document semantics");
9631 assert!(matches!(
9632 error,
9633 SignatureVerificationPipelineError::InvalidStructure {
9634 reason: "raw X509 RetrievalMethod requires an untransformed external URI"
9635 }
9636 ));
9637 }
9638
9639 #[test]
9640 fn verify_context_does_not_hide_malformed_digest_behind_unsupported_transform() {
9641 let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
9643 let xml = xml.replacen(
9644 "<ds:Reference URI=\"#target\">",
9645 "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
9646 1,
9647 );
9648 replace_fixture_manifest_digest(&xml, "!!!")
9649 });
9650
9651 let error = VerifyContext::new()
9652 .key(&AcceptingKey)
9653 .process_manifests(true)
9654 .verify(&broken_xml)
9655 .expect_err("malformed Manifest digest must not become a validity result");
9656 assert!(matches!(
9657 error,
9658 SignatureVerificationPipelineError::ParseManifestReference(_)
9659 ));
9660 }
9661
9662 #[test]
9663 fn verify_context_rejects_manifest_non_whitespace_mixed_content() {
9664 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
9667 xml.replacen(
9668 "<ds:Manifest ID=\"manifest\">",
9669 "<ds:Manifest ID=\"manifest\">junk",
9670 1,
9671 )
9672 });
9673
9674 let err = VerifyContext::new()
9675 .key(&AcceptingKey)
9676 .process_manifests(true)
9677 .verify(&xml)
9678 .expect_err("Manifest mixed content must fail verification");
9679 assert!(matches!(
9680 err,
9681 SignatureVerificationPipelineError::InvalidStructure {
9682 reason: "Manifest contains non-whitespace mixed content"
9683 }
9684 ));
9685 }
9686
9687 #[test]
9688 fn verify_context_rejects_empty_manifest_children() {
9689 let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
9692 let (prefix, rest) = xml
9693 .split_once("<ds:Manifest ID=\"manifest\">")
9694 .expect("fixture should contain Manifest");
9695 let (_, suffix) = rest
9696 .split_once("</ds:Manifest>")
9697 .expect("fixture should contain closing Manifest");
9698 format!("{prefix}<ds:Manifest ID=\"manifest\"></ds:Manifest>{suffix}")
9699 });
9700
9701 let err = VerifyContext::new()
9702 .key(&AcceptingKey)
9703 .process_manifests(true)
9704 .verify(&xml)
9705 .expect_err("empty Manifest must fail verification");
9706 assert!(matches!(
9707 err,
9708 SignatureVerificationPipelineError::InvalidStructure {
9709 reason: "Manifest must contain at least one ds:Reference element child"
9710 }
9711 ));
9712 }
9713
9714 #[test]
9715 fn verify_context_ignores_unsigned_malformed_manifest_blocks() {
9716 let xml = signature_with_manifest_xml(true).replacen(
9717 "</ds:Object>",
9718 "</ds:Object><ds:Object><ds:Manifest>junk<ds:Foo/></ds:Manifest></ds:Object>",
9719 1,
9720 );
9721 let result = VerifyContext::new()
9722 .key(&AcceptingKey)
9723 .process_manifests(true)
9724 .verify(&xml)
9725 .expect("unsigned malformed Manifest must be ignored");
9726 assert_eq!(
9727 result.manifest_references.len(),
9728 1,
9729 "only signed Manifest references must be reported",
9730 );
9731 assert!(matches!(result.status, DsigStatus::Valid));
9732 }
9733
9734 #[test]
9735 fn verify_context_skips_ambiguous_manifest_id_blocks() {
9736 let xml = signature_with_manifest_xml(true).replacen(
9737 "</ds:Object>",
9738 "</ds:Object><ds:Object><ds:Manifest ID=\"manifest\">junk<ds:Foo/></ds:Manifest></ds:Object>",
9739 1,
9740 );
9741 let err = VerifyContext::new()
9742 .key(&RejectingKey)
9743 .process_manifests(true)
9744 .verify(&xml)
9745 .expect_err("ambiguous manifest IDs should make SignedInfo #manifest dereference fail");
9746 assert!(matches!(
9747 err,
9748 SignatureVerificationPipelineError::Reference(
9749 ReferenceProcessingError::UriDereference(
9750 crate::xmldsig::types::TransformError::ElementNotFound(id)
9751 )
9752 ) if id == "manifest"
9753 ));
9754 }
9755
9756 #[test]
9757 fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
9758 let xml = minimal_signature_xml("", "");
9759 let err = VerifyContext::new()
9760 .key(&RejectingKey)
9761 .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
9762 .verify(&xml)
9763 .expect_err("implicit default C14N must be checked against allowlist");
9764 assert!(matches!(
9765 err,
9766 SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
9767 operation: "verification transform",
9768 ..
9769 })
9770 ));
9771 }
9772
9773 #[test]
9774 fn verify_context_skips_resolver_when_reference_processing_fails() {
9775 let xml = minimal_signature_xml("", "");
9776 let result = VerifyContext::new()
9777 .key_resolver(&PanicResolver)
9778 .verify(&xml)
9779 .expect("reference digest mismatch should short-circuit before resolver");
9780 assert!(matches!(
9781 result.status,
9782 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
9783 ));
9784 }
9785
9786 #[test]
9787 fn verify_context_leaves_fail_fast_reference_tail_unexecuted() {
9788 let second_reference = r#"
9791 <ds:Reference URI="">
9792 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
9793 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
9794 </ds:Reference>"#;
9795 let xml = minimal_signature_xml("", "").replacen(
9796 " </ds:SignedInfo>",
9797 &format!("{second_reference}\n </ds:SignedInfo>"),
9798 1,
9799 );
9800
9801 let result = VerifyContext::new()
9802 .key(&AcceptingKey)
9803 .verify(&xml)
9804 .expect("digest mismatch must remain a verification result");
9805
9806 assert_eq!(result.signed_info_references.len(), 1);
9807 assert!(matches!(
9808 result.signed_info_references[0].status,
9809 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
9810 ));
9811 }
9812
9813 #[test]
9814 fn verify_context_reports_key_not_found_when_resolver_misses() {
9815 let xml = signature_with_target_reference("AQ==");
9816 let result = VerifyContext::new()
9817 .key_resolver(&MissingKeyResolver)
9818 .verify(&xml)
9819 .expect("resolver miss should report status, not pipeline error");
9820 assert!(matches!(
9821 result.status,
9822 DsigStatus::Invalid(FailureReason::KeyNotFound)
9823 ));
9824 assert_eq!(
9825 result.signed_info_references.len(),
9826 1,
9827 "KeyNotFound path must preserve SignedInfo reference diagnostics",
9828 );
9829 assert!(matches!(
9830 result.signed_info_references[0].status,
9831 DsigStatus::Valid
9832 ));
9833 }
9834
9835 #[test]
9836 fn verification_candidate_budget_covers_preset_and_custom_resolver_paths() {
9837 let xml = signature_with_target_reference("AQ==");
9840 let mut policy = crate::policy::VerificationPolicy::default();
9841 policy.resources.max_key_candidates = 0;
9842
9843 let preset_error = VerifyContext::new()
9844 .key(&RejectingKey)
9845 .policy(policy.clone())
9846 .verify(&xml)
9847 .expect_err("a preset key consumes one candidate");
9848 assert!(matches!(
9849 preset_error,
9850 SignatureVerificationPipelineError::Policy(
9851 crate::policy::PolicyViolation::ResourceLimit {
9852 resource: crate::policy::resource_name::KEY_CANDIDATES,
9853 maximum: 0,
9854 actual: 1,
9855 }
9856 )
9857 ));
9858
9859 let resolver_error = VerifyContext::new()
9860 .key_resolver(&PanicResolver)
9861 .policy(policy)
9862 .verify(&xml)
9863 .expect_err("a custom resolver requires candidate capacity before dispatch");
9864 assert!(matches!(
9865 resolver_error,
9866 SignatureVerificationPipelineError::Policy(
9867 crate::policy::PolicyViolation::ResourceLimit {
9868 resource: crate::policy::resource_name::KEY_CANDIDATES,
9869 maximum: 0,
9870 actual: 1,
9871 }
9872 )
9873 ));
9874 }
9875
9876 #[test]
9877 fn verification_candidate_account_is_not_reset_for_each_signature() {
9878 let xml = signature_with_target_reference("AQ==");
9880 let start = xml
9881 .find("<ds:Signature>")
9882 .expect("generated signature start");
9883 let end = xml
9884 .find("</ds:Signature>")
9885 .expect("generated signature end")
9886 + "</ds:Signature>".len();
9887 let document =
9888 XmlDocument::parse(xml.replace("</root>", &format!("{}</root>", &xml[start..end])))
9889 .expect("two signatures");
9890 let mut policy = crate::policy::VerificationPolicy::default();
9891 policy.resources.max_key_candidates = 1;
9892 let evidence = VerifyContext::new()
9893 .key(&RejectingKey)
9894 .policy(policy)
9895 .verify_all(&document)
9896 .expect("signature-local failure report");
9897 assert_eq!(evidence.signatures().len(), 2);
9898 assert!(evidence.signatures()[0].result().is_ok());
9899 assert!(matches!(
9900 evidence.signatures()[1].result(),
9901 Err(DsigError::Policy(
9902 crate::policy::PolicyViolation::ResourceLimit {
9903 resource: crate::policy::resource_name::KEY_CANDIDATES,
9904 maximum: 1,
9905 actual: 2,
9906 }
9907 ))
9908 ));
9909 }
9910
9911 #[test]
9912 fn resolver_receives_the_certificate_account_spent_by_ingestion() {
9913 struct ProbeResolver {
9915 called: Cell<bool>,
9916 }
9917 impl KeyResolver for ProbeResolver {
9918 fn resolve<'a>(
9919 &'a self,
9920 _info: Option<&KeyInfo>,
9921 _algorithm: SignatureAlgorithm,
9922 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
9923 panic!("core must dispatch through the budget-aware hook");
9924 }
9925 fn resolve_for_verification_with_budget<'a>(
9926 &'a self,
9927 _info: Option<&KeyInfo>,
9928 _algorithm: SignatureAlgorithm,
9929 _policy: &crate::policy::VerificationPolicy,
9930 _provider: &dyn crate::provider::CryptoProvider,
9931 budget: &mut super::super::InspectedKeyCandidateBudget,
9932 ) -> Result<Option<super::super::ResolvedVerificationKey<'a>>, DsigError> {
9933 assert!(budget.execution().work_used() > 0);
9934 budget.charge()?;
9935 self.called.set(true);
9936 Ok(Some(super::super::ResolvedVerificationKey::CallerTrusted(
9937 Box::new(AcceptingKey),
9938 )))
9939 }
9940 fn consumes_document_key_info(&self) -> bool {
9941 true
9942 }
9943 }
9944 let pair = rcgen::KeyPair::generate().expect("test key generation");
9945 let certificate = rcgen::CertificateParams::new(Vec::<String>::new())
9946 .expect("empty SAN list")
9947 .self_signed(&pair)
9948 .expect("test certificate");
9949 let encoded = base64::engine::general_purpose::STANDARD.encode(certificate.der());
9950 let xml = signature_with_target_reference("AQ==").replace("</ds:Signature>", &format!(
9951 "<ds:KeyInfo><ds:X509Data><ds:X509Certificate>{encoded}</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>",
9952 ));
9953 let resolver = ProbeResolver {
9954 called: Cell::new(false),
9955 };
9956 let result = VerifyContext::new()
9957 .key_resolver(&resolver)
9958 .verify(&xml)
9959 .expect("shared-account resolver dispatch");
9960 assert!(matches!(result.status, DsigStatus::Valid));
9961 assert!(resolver.called.get());
9962 }
9963
9964 #[test]
9965 fn verification_candidate_budget_precedes_embedded_x509_parsing() {
9966 let first_certificate = base64::engine::general_purpose::STANDARD.encode(include_bytes!(
9969 "../../tests/fixtures/xmldsig/phaos-xmldsig-three/certs/rsa-cert.der"
9970 ));
9971 let xml = signature_with_target_reference("AQ==").replace(
9972 "</ds:SignatureValue>\n </ds:Signature>",
9973 &format!(
9974 "</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>"
9975 ),
9976 );
9977 let mut policy = crate::policy::VerificationPolicy::default();
9978 policy.resources.max_key_candidates = 1;
9979
9980 let error = VerifyContext::new()
9981 .policy(policy)
9982 .verify(&xml)
9983 .expect_err("candidate policy must run before embedded certificate parsing");
9984
9985 assert!(matches!(
9986 error,
9987 SignatureVerificationPipelineError::Policy(
9988 crate::policy::PolicyViolation::ResourceLimit {
9989 resource: crate::policy::resource_name::KEY_CANDIDATES,
9990 maximum: 1,
9991 actual: 2,
9992 }
9993 )
9994 ));
9995 }
9996
9997 #[test]
9998 fn verify_context_resolver_can_ignore_malformed_keyinfo_by_default() {
9999 let base_xml = signature_with_target_reference("AQ==");
10000 let xml = base_xml
10001 .replace(
10002 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
10003 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
10004 )
10005 .replace(
10006 "</ds:SignatureValue>\n </ds:Signature>",
10007 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
10008 );
10009
10010 let result = VerifyContext::new()
10011 .key_resolver(&MissingKeyResolver)
10012 .verify(&xml)
10013 .expect("resolver path should not hard-fail on advisory malformed KeyInfo by default");
10014 assert!(matches!(
10015 result.status,
10016 DsigStatus::Invalid(FailureReason::KeyNotFound)
10017 ));
10018 }
10019
10020 #[test]
10021 fn verify_context_resolver_can_opt_in_to_keyinfo_parse_failures() {
10022 let base_xml = signature_with_target_reference("AQ==");
10023 let xml = base_xml
10024 .replace(
10025 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
10026 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
10027 )
10028 .replace(
10029 "</ds:SignatureValue>\n </ds:Signature>",
10030 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
10031 );
10032
10033 let err = VerifyContext::new()
10034 .key_resolver(&ConsumingKeyInfoResolver)
10035 .verify(&xml)
10036 .expect_err("resolver opted into KeyInfo parsing, malformed KeyInfo must fail");
10037 assert!(matches!(
10038 err,
10039 SignatureVerificationPipelineError::ParseKeyInfo(_)
10040 ));
10041 }
10042
10043 #[test]
10044 fn verify_context_ignores_unsupported_retrieval_before_valid_key_source() {
10045 let xml = signature_with_target_reference("AQ==").replace(
10048 "</ds:SignatureValue>\n </ds:Signature>",
10049 r##"</ds:SignatureValue>
10050 <ds:KeyInfo>
10051 <ds:RetrievalMethod URI="#vendor" Type="urn:vendor:key">
10052 <ds:Transforms><ds:Transform Algorithm="urn:vendor:transform"/></ds:Transforms>
10053 </ds:RetrievalMethod>
10054 <ds:KeyName>fallback</ds:KeyName>
10055 </ds:KeyInfo>
10056 </ds:Signature>"##,
10057 );
10058
10059 let result = VerifyContext::new()
10060 .key_resolver(&FallbackKeyInfoResolver)
10061 .verify(&xml)
10062 .expect("unsupported advisory retrieval must not abort key resolution");
10063 assert_eq!(result.status, DsigStatus::Valid);
10064 }
10065
10066 #[test]
10067 fn verify_context_does_not_eagerly_fail_unused_retrieval_fallback() {
10068 let xml = signature_with_target_reference("AQ==").replace(
10071 "</ds:SignatureValue>\n </ds:Signature>",
10072 r#"</ds:SignatureValue>
10073 <ds:KeyInfo>
10074 <ds:KeyName>primary</ds:KeyName>
10075 <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
10076 </ds:KeyInfo>
10077 </ds:Signature>"#,
10078 );
10079
10080 let result = VerifyContext::new()
10081 .key_resolver(&EarlyKeyInfoResolver)
10082 .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
10083 .verify(&xml)
10084 .expect("an unused missing retrieval fallback must not abort verification");
10085
10086 assert_eq!(result.status, DsigStatus::Valid);
10087 }
10088
10089 #[test]
10090 fn verify_context_does_not_eagerly_fail_unused_same_document_x509_retrieval() {
10091 let xml = signature_with_target_reference("AQ==").replace(
10094 "</ds:SignatureValue>\n </ds:Signature>",
10095 r##"</ds:SignatureValue>
10096 <ds:KeyInfo>
10097 <ds:KeyName>primary</ds:KeyName>
10098 <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
10099 </ds:KeyInfo>
10100 </ds:Signature>"##,
10101 );
10102
10103 let result = VerifyContext::new()
10104 .key_resolver(&EarlyKeyInfoResolver)
10105 .verify(&xml)
10106 .expect("an unused missing X509Data retrieval fallback must not abort verification");
10107
10108 assert_eq!(result.status, DsigStatus::Valid);
10109 }
10110
10111 #[test]
10112 fn verify_context_reports_missing_same_document_x509_retrieval_without_fallback() {
10113 let xml = signature_with_target_reference("AQ==").replace(
10116 "</ds:SignatureValue>\n </ds:Signature>",
10117 r##"</ds:SignatureValue>
10118 <ds:KeyInfo>
10119 <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
10120 </ds:KeyInfo>
10121 </ds:Signature>"##,
10122 );
10123
10124 let error = VerifyContext::new()
10125 .key_resolver(&ConsumingKeyInfoResolver)
10126 .verify(&xml)
10127 .expect_err("a sole missing X509Data retrieval must remain an explicit error");
10128
10129 assert!(matches!(
10130 error,
10131 SignatureVerificationPipelineError::InvalidStructure {
10132 reason: "X509Data RetrievalMethod target is missing or ambiguous"
10133 }
10134 ));
10135 }
10136
10137 #[test]
10138 fn verify_context_does_not_eagerly_parse_unused_retrieval_fallback() {
10139 let xml = signature_with_target_reference("AQ==").replace(
10142 "</ds:SignatureValue>\n </ds:Signature>",
10143 r#"</ds:SignatureValue>
10144 <ds:KeyInfo>
10145 <ds:KeyName>primary</ds:KeyName>
10146 <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
10147 </ds:KeyInfo>
10148 </ds:Signature>"#,
10149 );
10150 let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
10151
10152 let result = VerifyContext::new()
10153 .key_resolver(&EarlyKeyInfoResolver)
10154 .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
10155 .external_resources(&resources)
10156 .verify(&xml)
10157 .expect("an unused malformed retrieval fallback must not abort verification");
10158
10159 assert_eq!(result.status, DsigStatus::Valid);
10160 }
10161
10162 #[test]
10163 fn verify_context_reports_missing_retrieval_when_no_key_source_resolves() {
10164 let xml = signature_with_target_reference("AQ==").replace(
10167 "</ds:SignatureValue>\n </ds:Signature>",
10168 r#"</ds:SignatureValue>
10169 <ds:KeyInfo>
10170 <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
10171 </ds:KeyInfo>
10172 </ds:Signature>"#,
10173 );
10174
10175 let error = VerifyContext::new()
10176 .key_resolver(&ConsumingKeyInfoResolver)
10177 .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
10178 .verify(&xml)
10179 .expect_err("a missing sole RetrievalMethod must remain an explicit error");
10180
10181 assert!(matches!(
10182 error,
10183 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
10184 crate::xmldsig::TransformError::UnsupportedUri(uri)
10185 )) if uri == "missing.der"
10186 ));
10187 }
10188
10189 #[test]
10190 fn verify_context_reports_malformed_retrieval_when_no_key_source_resolves() {
10191 let xml = signature_with_target_reference("AQ==").replace(
10194 "</ds:SignatureValue>\n </ds:Signature>",
10195 r#"</ds:SignatureValue>
10196 <ds:KeyInfo>
10197 <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
10198 </ds:KeyInfo>
10199 </ds:Signature>"#,
10200 );
10201 let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
10202
10203 let error = VerifyContext::new()
10204 .key_resolver(&ConsumingKeyInfoResolver)
10205 .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
10206 .external_resources(&resources)
10207 .verify(&xml)
10208 .expect_err("a malformed sole RetrievalMethod must remain a parse error");
10209
10210 assert!(matches!(
10211 error,
10212 SignatureVerificationPipelineError::ParseKeyInfo(_)
10213 ));
10214 }
10215
10216 #[test]
10217 fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
10218 let xml = signature_with_target_reference("@@@");
10219
10220 let err = VerifyContext::new()
10221 .key_resolver(&MissingKeyResolver)
10222 .verify(&xml)
10223 .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
10224 assert!(matches!(
10225 err,
10226 SignatureVerificationPipelineError::SignatureValueBase64(_)
10227 ));
10228 }
10229
10230 #[test]
10231 fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
10232 let xml = signature_with_target_reference("@@@");
10233
10234 let err = VerifyContext::new()
10235 .verify(&xml)
10236 .expect_err("invalid SignatureValue must remain a decode error");
10237 assert!(matches!(
10238 err,
10239 SignatureVerificationPipelineError::SignatureValueBase64(_)
10240 ));
10241 }
10242
10243 #[test]
10244 fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
10245 let references = vec![Reference {
10246 uri: None,
10247 id: None,
10248 ref_type: None,
10249 transforms: vec![],
10250 digest_method: DigestAlgorithm::Sha256,
10251 digest_value: vec![0; 32],
10252 }];
10253 let uri_types = UriTypeSet {
10254 allow_empty: false,
10255 allow_same_document: true,
10256 allow_external: false,
10257 };
10258
10259 let err = enforce_reference_policies(&references, uri_types, None)
10260 .expect_err("missing URI must fail before allow_empty policy is evaluated");
10261 assert!(matches!(
10262 err,
10263 SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
10264 ));
10265 }
10266
10267 #[test]
10268 fn enforce_reference_policies_checks_only_terminal_binary_output() {
10269 let c14n = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI).unwrap();
10270 let allowed = HashSet::from([
10271 BASE64_TRANSFORM_URI.to_owned(),
10272 DEFAULT_IMPLICIT_C14N_URI.to_owned(),
10273 ]);
10274 let without_implicit_c14n = HashSet::from([BASE64_TRANSFORM_URI.to_owned()]);
10275
10276 for transforms in [
10277 vec![Transform::Base64Decode, Transform::C14n(c14n)],
10278 vec![Transform::Base64Decode, Transform::Base64Decode],
10279 ] {
10280 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, vec![0; 32]);
10281 enforce_reference_policies(
10282 std::slice::from_ref(&reference),
10283 UriTypeSet::default(),
10284 Some(&allowed),
10285 )
10286 .expect("terminal binary output must not require implicit C14N");
10287 }
10288
10289 let terminal_base64 = make_reference(
10290 "",
10291 vec![Transform::Base64Decode, Transform::Base64Decode],
10292 DigestAlgorithm::Sha256,
10293 vec![0; 32],
10294 );
10295 enforce_reference_policies(
10296 std::slice::from_ref(&terminal_base64),
10297 UriTypeSet::default(),
10298 Some(&without_implicit_c14n),
10299 )
10300 .expect("terminal Base64 output must not require implicit C14N");
10301
10302 let no_transforms = make_reference("", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
10303 let error = enforce_reference_policies(
10304 std::slice::from_ref(&no_transforms),
10305 UriTypeSet::default(),
10306 Some(&without_implicit_c14n),
10307 )
10308 .expect_err("a node-set result must require allowlisted implicit C14N");
10309 assert!(matches!(
10310 error,
10311 SignatureVerificationPipelineError::Policy(
10312 crate::policy::PolicyViolation::Algorithm {
10313 operation: "verification transform",
10314 ref algorithm,
10315 }
10316 )
10317 if algorithm == DEFAULT_IMPLICIT_C14N_URI
10318 ));
10319
10320 let detached = make_reference("urn:payload", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
10321 enforce_reference_policies(
10322 std::slice::from_ref(&detached),
10323 UriTypeSet::ALL,
10324 Some(&without_implicit_c14n),
10325 )
10326 .expect("external octets without transforms must not require implicit C14N");
10327
10328 let external_xpath = make_reference(
10329 "urn:payload",
10330 vec![Transform::XPath(
10331 super::super::transforms::XPathExpression::new("true()"),
10332 )],
10333 DigestAlgorithm::Sha256,
10334 vec![0; 32],
10335 );
10336 let error = enforce_reference_policies(
10337 std::slice::from_ref(&external_xpath),
10338 UriTypeSet::ALL,
10339 Some(&HashSet::from([XPATH_TRANSFORM_URI.to_owned()])),
10340 )
10341 .expect_err("external XML converted to a node-set must require implicit C14N");
10342 assert!(matches!(
10343 error,
10344 SignatureVerificationPipelineError::Policy(
10345 crate::policy::PolicyViolation::Algorithm {
10346 operation: "verification transform",
10347 ref algorithm,
10348 }
10349 )
10350 if algorithm == DEFAULT_IMPLICIT_C14N_URI
10351 ));
10352 }
10353
10354 #[test]
10355 fn stored_pre_digest_budget_counts_repeated_external_references() {
10356 let document =
10359 Document::parse("<ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"/>")
10360 .unwrap();
10361 let payload = vec![b'x'; 7];
10362 let digest = compute_digest(DigestAlgorithm::Sha256, &payload);
10363 let references = (0..5)
10364 .map(|_| {
10365 make_reference(
10366 "urn:repeated",
10367 Vec::new(),
10368 DigestAlgorithm::Sha256,
10369 digest.clone(),
10370 )
10371 })
10372 .collect::<Vec<_>>();
10373 let resources = HashMap::from([("urn:repeated".to_owned(), payload)]);
10374 let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
10375 let transform_budget = TransformExecutionBudget::default();
10376 let canonicalized_data_budget = CanonicalizedDataBudget::with_limit(32);
10377 let execution = ReferenceExecutionContext {
10378 store_pre_digest: true,
10379 transform_options: TransformOptions::default(),
10380 transform_budget: &transform_budget,
10381 canonicalized_data_budget: &canonicalized_data_budget,
10382 provider: crate::provider::default_provider(),
10383 capture: &(),
10384 };
10385
10386 let error = process_all_references_with_options(
10387 &references,
10388 &resolver,
10389 document.root_element(),
10390 &execution,
10391 )
10392 .expect_err(
10393 "retained diagnostics must not multiply one external allocation past the aggregate cap",
10394 );
10395 assert!(matches!(
10396 error,
10397 ReferenceProcessingError::Policy(crate::policy::PolicyViolation::ResourceLimit {
10398 resource: "canonicalized bytes",
10399 maximum: 32,
10400 ..
10401 })
10402 ));
10403 }
10404
10405 #[test]
10406 fn public_reference_entries_inherit_custom_id_registration() {
10407 let xml = format!(
10409 "<root xmlns:ds='{XMLDSIG_NS}'><data Token='target'>YWJj</data><ds:Signature/></root>"
10410 );
10411 let document = Document::parse(&xml).unwrap();
10412 let signature = document
10413 .descendants()
10414 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
10415 .unwrap();
10416 let registrations = [crate::IdAttributeRegistration::global("Token")];
10417 let resolver = UriReferenceResolver::with_id_registrations(&document, ®istrations);
10418 let expression = crate::xmldsig::transforms::XPathExpression::new(
10419 "count(id('target')) = 1 and count(ancestor-or-self::node() | id('target')) = count(ancestor-or-self::node())",
10420 );
10421 let reference = make_reference(
10422 "#target",
10423 vec![Transform::XPath(expression), Transform::Base64Decode],
10424 DigestAlgorithm::Sha256,
10425 compute_digest(DigestAlgorithm::Sha256, b"abc"),
10426 );
10427 let one = process_reference(
10428 &reference,
10429 &resolver,
10430 signature,
10431 ReferenceSet::SignedInfo,
10432 0,
10433 true,
10434 )
10435 .unwrap();
10436 assert_eq!(one.status, DsigStatus::Valid);
10437 let all = process_all_references(&[reference], &resolver, signature, true).unwrap();
10438 assert_eq!(all.results.len(), 1);
10439 assert_eq!(all.results[0].status, DsigStatus::Valid);
10440 assert!(all.first_failure.is_none());
10441 }
10442
10443 #[test]
10444 fn normalized_root_xpointers_bind_document_identity() {
10445 let document = XmlDocument::parse("<root><item/></root>").unwrap();
10447 let budgets = VerificationOperationBudgets::with_transforms(
10448 &crate::policy::VerificationPolicy::default(),
10449 TransformExecutionBudget::default(),
10450 None,
10451 );
10452 document.with_view(|view| {
10453 let resolver = UriReferenceResolver::new(view.document());
10454 for uri in ["#xpointer(/)", "#xpointer( / )", "#xpointer(%2F)"] {
10455 let reference =
10456 make_reference(uri, Vec::new(), DigestAlgorithm::Sha256, vec![0; 32]);
10457 assert_eq!(
10458 budgets.resource_identity_for_reference(&reference, 0, &resolver, view, None),
10459 OperationResourceIdentity::DocumentNode(view.root()),
10460 "{uri}"
10461 );
10462 }
10463 });
10464 }
10465
10466 #[test]
10467 fn missing_external_identity_does_not_retain_uri_cache_copies() {
10468 let document = XmlDocument::parse("<root/>").unwrap();
10472 let uri = format!("urn:missing:{}", "x".repeat(16384));
10473 let reference = make_reference(&uri, Vec::new(), DigestAlgorithm::Sha256, vec![0; 32]);
10474 let budgets = VerificationOperationBudgets::with_transforms(
10475 &crate::policy::VerificationPolicy::default(),
10476 TransformExecutionBudget::default(),
10477 None,
10478 );
10479 document.with_view(|view| {
10480 let resolver = UriReferenceResolver::new(view.document());
10481 let _ = budgets.resource_identity_for_reference(&reference, 0, &resolver, view, None);
10482 assert!(budgets.external_resource_identities.borrow().is_empty());
10483 });
10484 }
10485
10486 #[test]
10487 fn operation_memoizes_repeated_external_resource_identity() {
10488 let document = XmlDocument::parse("<root/>").expect("fixture must parse");
10491 let resources = HashMap::from([("urn:repeated".to_owned(), vec![b'x'; 8 * 1_024])]);
10492 let reference = make_reference(
10493 "urn:repeated",
10494 Vec::new(),
10495 DigestAlgorithm::Sha256,
10496 vec![0; 32],
10497 );
10498 let budgets = VerificationOperationBudgets::with_transforms(
10499 &crate::policy::VerificationPolicy::default(),
10500 TransformExecutionBudget::default(),
10501 None,
10502 );
10503
10504 document.with_view(|view| {
10505 let resolver =
10506 UriReferenceResolver::new(view.document()).with_external_resources(&resources);
10507 let first =
10508 budgets.resource_identity_for_reference(&reference, 0, &resolver, view, None);
10509 let second =
10510 budgets.resource_identity_for_reference(&reference, 1, &resolver, view, None);
10511
10512 assert_eq!(first, second);
10513 assert_eq!(budgets.external_resource_identities.borrow().len(), 1);
10514 });
10515 }
10516
10517 #[test]
10518 fn canonical_signed_info_obeys_policy_without_diagnostic_retention() {
10519 let xml = signature_with_target_reference("AQ==");
10523 let marker = "<ds:SignatureMethod";
10524 let padding = " ".repeat(1_025);
10525 let xml = xml.replacen(marker, &format!("{padding}{marker}"), 1);
10526 let policy = crate::policy::VerificationPolicy {
10527 resources: crate::policy::ResourcePolicy {
10528 max_canonicalized_bytes: 1_024,
10529 ..crate::policy::ResourcePolicy::default()
10530 },
10531 ..crate::policy::VerificationPolicy::default()
10532 };
10533
10534 let error = VerifyContext::new()
10535 .key(&AcceptingKey)
10536 .policy(policy)
10537 .verify(&xml)
10538 .expect_err("canonicalized SignedInfo must remain policy-bounded");
10539
10540 assert!(matches!(
10541 error,
10542 SignatureVerificationPipelineError::Policy(
10543 crate::policy::PolicyViolation::ResourceLimit {
10544 resource: "canonicalized bytes",
10545 ..
10546 }
10547 )
10548 ));
10549 }
10550
10551 #[test]
10552 fn fragmented_pq_signature_value_decodes_at_exact_wire_limit() {
10553 let parameter = super::super::pq_algorithm::PqAlgorithm::SlhDsaSha2_256f;
10556 let bytes = vec![0x5a; parameter.signature_len()];
10557 let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes);
10558 let mut xml = String::from("<SignatureValue>");
10559 for chunk in encoded.as_bytes().chunks(4) {
10560 xml.push_str(std::str::from_utf8(chunk).unwrap());
10561 xml.push_str("<!--split-->\n");
10562 }
10563 xml.push_str("</SignatureValue>");
10564 let document = Document::parse(&xml).unwrap();
10565 let algorithm = SignatureAlgorithm::PostQuantum(parameter);
10566 assert_eq!(
10567 decode_signature_value(
10568 document.root_element(),
10569 algorithm,
10570 &crate::policy::ResourcePolicy::default()
10571 )
10572 .unwrap(),
10573 bytes
10574 );
10575 let oversized = xml.replace("</SignatureValue>", "AAAA</SignatureValue>");
10576 let document = Document::parse(&oversized).unwrap();
10577 assert!(matches!(
10578 decode_signature_value(
10579 document.root_element(),
10580 algorithm,
10581 &crate::policy::ResourcePolicy::default()
10582 ),
10583 Err(SignatureVerificationPipelineError::InvalidStructure {
10584 reason: "SignatureValue exceeds maximum allowed length"
10585 })
10586 ));
10587 }
10588
10589 #[test]
10590 fn push_normalized_signature_text_rejects_form_feed() {
10591 let mut normalized = Vec::new();
10592 let mut raw_text_len = 0usize;
10593 let err = push_normalized_signature_text(
10594 "ab\u{000C}cd",
10595 &mut raw_text_len,
10596 &mut normalized,
10597 SignatureAlgorithm::RsaSha256,
10598 &crate::policy::ResourcePolicy::default(),
10599 )
10600 .expect_err("form-feed must not be treated as XML base64 whitespace");
10601 assert!(matches!(
10602 err,
10603 SignatureVerificationPipelineError::SignatureValueBase64(
10604 base64::DecodeError::InvalidByte(_, 0x0C)
10605 )
10606 ));
10607 }
10608
10609 #[test]
10610 fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
10611 let mut normalized = vec![b'A'; MAX_SIGNATURE_VALUE_LEN - 1];
10612 let mut raw_text_len = normalized.len();
10613 let err = push_normalized_signature_text(
10614 "é",
10615 &mut raw_text_len,
10616 &mut normalized,
10617 SignatureAlgorithm::RsaSha256,
10618 &crate::policy::ResourcePolicy::default(),
10619 )
10620 .expect_err("multibyte characters must not bypass byte-size limit");
10621 assert!(matches!(
10622 err,
10623 SignatureVerificationPipelineError::InvalidStructure {
10624 reason: "SignatureValue exceeds maximum allowed length"
10625 }
10626 ));
10627 }
10628
10629 #[test]
10632 fn reference_with_correct_digest_passes() {
10633 let xml = r##"<root>
10636 <data>hello world</data>
10637 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
10638 <ds:SignedInfo/>
10639 </ds:Signature>
10640 </root>"##;
10641 let doc = Document::parse(xml).unwrap();
10642 let resolver = UriReferenceResolver::new(&doc);
10643 let sig_node = doc
10644 .descendants()
10645 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
10646 .unwrap();
10647
10648 let initial_data = resolver.dereference("").unwrap();
10650 let transforms = vec![
10651 Transform::Enveloped,
10652 Transform::C14n(
10653 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
10654 .unwrap(),
10655 ),
10656 ];
10657 let pre_digest_bytes =
10658 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
10659 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
10660
10661 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
10663
10664 let result = process_reference(
10665 &reference,
10666 &resolver,
10667 sig_node,
10668 ReferenceSet::SignedInfo,
10669 0,
10670 false,
10671 )
10672 .unwrap();
10673 assert!(
10674 matches!(result.status, DsigStatus::Valid),
10675 "digest should match"
10676 );
10677 assert!(result.pre_digest_data.is_none());
10678 }
10679
10680 #[test]
10681 fn reference_with_wrong_digest_fails() {
10682 let xml = r##"<root>
10683 <data>hello</data>
10684 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
10685 <ds:SignedInfo/>
10686 </ds:Signature>
10687 </root>"##;
10688 let doc = Document::parse(xml).unwrap();
10689 let resolver = UriReferenceResolver::new(&doc);
10690 let sig_node = doc
10691 .descendants()
10692 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
10693 .unwrap();
10694
10695 let transforms = vec![Transform::Enveloped];
10696 let wrong_digest = vec![0u8; 32];
10698 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
10699
10700 let result = process_reference(
10701 &reference,
10702 &resolver,
10703 sig_node,
10704 ReferenceSet::SignedInfo,
10705 0,
10706 false,
10707 )
10708 .unwrap();
10709 assert!(matches!(
10710 result.status,
10711 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
10712 ));
10713 }
10714
10715 #[test]
10716 fn reference_with_wrong_digest_preserves_supplied_ref_index() {
10717 let xml = r##"<root>
10718 <data>hello</data>
10719 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
10720 <ds:SignedInfo/>
10721 </ds:Signature>
10722 </root>"##;
10723 let doc = Document::parse(xml).unwrap();
10724 let resolver = UriReferenceResolver::new(&doc);
10725 let sig_node = doc
10726 .descendants()
10727 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
10728 .unwrap();
10729
10730 let reference = make_reference(
10731 "",
10732 vec![Transform::Enveloped],
10733 DigestAlgorithm::Sha256,
10734 vec![0u8; 32],
10735 );
10736 let result = process_reference(
10737 &reference,
10738 &resolver,
10739 sig_node,
10740 ReferenceSet::SignedInfo,
10741 7,
10742 false,
10743 )
10744 .unwrap();
10745 assert!(matches!(
10746 result.status,
10747 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
10748 ));
10749 }
10750
10751 #[test]
10752 fn reference_stores_pre_digest_data() {
10753 let xml = "<root><child>text</child></root>";
10754 let doc = Document::parse(xml).unwrap();
10755 let resolver = UriReferenceResolver::new(&doc);
10756
10757 let initial_data = resolver.dereference("").unwrap();
10759 let pre_digest =
10760 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
10761 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
10762
10763 let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
10764 let result = process_reference(
10765 &reference,
10766 &resolver,
10767 doc.root_element(),
10768 ReferenceSet::SignedInfo,
10769 0,
10770 true,
10771 )
10772 .unwrap();
10773
10774 assert!(matches!(result.status, DsigStatus::Valid));
10775 assert!(result.pre_digest_data.is_some());
10776 assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
10777 }
10778
10779 #[test]
10782 fn reference_with_id_uri() {
10783 let xml = r##"<root>
10784 <item ID="target">specific content</item>
10785 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
10786 <ds:SignedInfo/>
10787 </ds:Signature>
10788 </root>"##;
10789 let doc = Document::parse(xml).unwrap();
10790 let resolver = UriReferenceResolver::new(&doc);
10791 let sig_node = doc
10792 .descendants()
10793 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
10794 .unwrap();
10795
10796 let initial_data = resolver.dereference("#target").unwrap();
10798 let transforms = vec![Transform::C14n(
10799 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
10800 .unwrap(),
10801 )];
10802 let pre_digest =
10803 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
10804 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
10805
10806 let reference = make_reference(
10807 "#target",
10808 transforms,
10809 DigestAlgorithm::Sha256,
10810 expected_digest,
10811 );
10812 let result = process_reference(
10813 &reference,
10814 &resolver,
10815 sig_node,
10816 ReferenceSet::SignedInfo,
10817 0,
10818 false,
10819 )
10820 .unwrap();
10821 assert!(matches!(result.status, DsigStatus::Valid));
10822 }
10823
10824 #[test]
10825 fn reference_with_nonexistent_id_fails() {
10826 let xml = "<root><child/></root>";
10827 let doc = Document::parse(xml).unwrap();
10828 let resolver = UriReferenceResolver::new(&doc);
10829
10830 let reference =
10831 make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
10832 let result = process_reference(
10833 &reference,
10834 &resolver,
10835 doc.root_element(),
10836 ReferenceSet::SignedInfo,
10837 0,
10838 false,
10839 );
10840 assert!(result.is_err());
10841 }
10842
10843 #[test]
10844 fn reference_with_absent_uri_fails_closed() {
10845 let xml = "<root><child>text</child></root>";
10846 let doc = Document::parse(xml).unwrap();
10847 let resolver = UriReferenceResolver::new(&doc);
10848
10849 let reference = Reference {
10850 uri: None, id: None,
10852 ref_type: None,
10853 transforms: vec![],
10854 digest_method: DigestAlgorithm::Sha256,
10855 digest_value: vec![0; 32],
10856 };
10857
10858 let result = process_reference(
10859 &reference,
10860 &resolver,
10861 doc.root_element(),
10862 ReferenceSet::SignedInfo,
10863 0,
10864 false,
10865 );
10866 assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
10867 }
10868
10869 #[test]
10872 fn all_references_pass() {
10873 let xml = "<root><child>text</child></root>";
10874 let doc = Document::parse(xml).unwrap();
10875 let resolver = UriReferenceResolver::new(&doc);
10876
10877 let initial_data = resolver.dereference("").unwrap();
10879 let pre_digest =
10880 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
10881 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
10882
10883 let refs = vec![
10884 make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
10885 make_reference("", vec![], DigestAlgorithm::Sha256, digest),
10886 ];
10887
10888 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
10889 assert!(result.all_valid());
10890 assert_eq!(result.results.len(), 2);
10891 assert!(result.first_failure.is_none());
10892 }
10893
10894 #[test]
10895 fn reference_processing_shares_xpath_work_across_references() {
10896 let document = Document::parse("<root/>").unwrap();
10899 let resolver = UriReferenceResolver::new(&document);
10900 let transform = Transform::XPath(super::super::transforms::XPathExpression::new("true()"));
10901 let initial_data = resolver.dereference("").unwrap();
10902 let pre_digest = crate::xmldsig::execute_transforms(
10903 document.root_element(),
10904 initial_data,
10905 std::slice::from_ref(&transform),
10906 )
10907 .unwrap();
10908 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
10909 let references = vec![
10910 make_reference(
10911 "",
10912 vec![transform.clone()],
10913 DigestAlgorithm::Sha256,
10914 digest.clone(),
10915 ),
10916 make_reference("", vec![transform], DigestAlgorithm::Sha256, digest),
10917 ];
10918 let budget = TransformExecutionBudget::with_xpath_limit(12);
10919 let canonicalized_data_budget = CanonicalizedDataBudget::default();
10920 let execution = ReferenceExecutionContext {
10921 store_pre_digest: false,
10922 transform_options: TransformOptions::default(),
10923 transform_budget: &budget,
10924 canonicalized_data_budget: &canonicalized_data_budget,
10925 provider: crate::provider::default_provider(),
10926 capture: &(),
10927 };
10928
10929 let error = process_all_references_with_options(
10930 &references,
10931 &resolver,
10932 document.root_element(),
10933 &execution,
10934 )
10935 .expect_err("the second Reference must consume the first Reference's XPath work");
10936
10937 assert!(matches!(
10938 error,
10939 ReferenceProcessingError::Transform(TransformError::Policy(
10940 crate::policy::PolicyViolation::ResourceLimit {
10941 resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
10942 ..
10943 }
10944 ))
10945 ));
10946 }
10947
10948 #[test]
10949 fn reference_processing_shares_node_set_materialization_across_references() {
10950 let document = Document::parse(
10954 r#"<root xmlns:n="urn:0123456789"><target Id="selected">payload</target></root>"#,
10955 )
10956 .unwrap();
10957 let resolver = UriReferenceResolver::new(&document);
10958 let initial_data = resolver.dereference("#selected").unwrap();
10959 let pre_digest =
10960 crate::xmldsig::execute_transforms(document.root_element(), initial_data, &[]).unwrap();
10961 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
10962 let references = vec![
10963 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest.clone()),
10964 make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest),
10965 ];
10966 let budget = TransformExecutionBudget::with_node_set_materialization_limit(30);
10967 let canonicalized_data_budget = CanonicalizedDataBudget::default();
10968 let execution = ReferenceExecutionContext {
10969 store_pre_digest: false,
10970 transform_options: TransformOptions::default(),
10971 transform_budget: &budget,
10972 canonicalized_data_budget: &canonicalized_data_budget,
10973 provider: crate::provider::default_provider(),
10974 capture: &(),
10975 };
10976
10977 let error = process_all_references_with_options(
10978 &references,
10979 &resolver,
10980 document.root_element(),
10981 &execution,
10982 )
10983 .expect_err("the second Reference must consume the first Reference's materialization work");
10984
10985 assert!(matches!(
10986 error,
10987 ReferenceProcessingError::UriDereference(TransformError::Policy(
10988 crate::policy::PolicyViolation::ResourceLimit {
10989 resource: crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
10990 ..
10991 }
10992 ))
10993 ));
10994 }
10995
10996 #[test]
10997 fn fail_fast_on_first_mismatch() {
10998 let xml = "<root><child>text</child></root>";
10999 let doc = Document::parse(xml).unwrap();
11000 let resolver = UriReferenceResolver::new(&doc);
11001
11002 let wrong_digest = vec![0u8; 32];
11003 let refs = vec![
11004 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
11005 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
11007 ];
11008
11009 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
11010 assert!(!result.all_valid());
11011 assert_eq!(result.first_failure, Some(0));
11012 assert_eq!(result.results.len(), 1);
11014 assert!(matches!(
11015 result.results[0].status,
11016 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
11017 ));
11018 }
11019
11020 #[test]
11021 fn fail_fast_second_reference() {
11022 let xml = "<root><child>text</child></root>";
11023 let doc = Document::parse(xml).unwrap();
11024 let resolver = UriReferenceResolver::new(&doc);
11025
11026 let initial_data = resolver.dereference("").unwrap();
11028 let pre_digest =
11029 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
11030 let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
11031 let wrong_digest = vec![0u8; 32];
11032
11033 let refs = vec![
11034 make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
11035 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
11036 ];
11037
11038 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
11039 assert!(!result.all_valid());
11040 assert_eq!(result.first_failure, Some(1));
11041 assert_eq!(result.results.len(), 2);
11043 assert!(matches!(result.results[0].status, DsigStatus::Valid));
11044 assert!(matches!(
11045 result.results[1].status,
11046 DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
11047 ));
11048 }
11049
11050 #[test]
11051 fn empty_references_list() {
11052 let xml = "<root/>";
11053 let doc = Document::parse(xml).unwrap();
11054 let resolver = UriReferenceResolver::new(&doc);
11055
11056 let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
11057 assert!(result.all_valid());
11058 assert!(result.results.is_empty());
11059 }
11060
11061 #[test]
11064 fn reference_sha1_digest() {
11065 let xml = "<root>content</root>";
11066 let doc = Document::parse(xml).unwrap();
11067 let resolver = UriReferenceResolver::new(&doc);
11068
11069 let initial_data = resolver.dereference("").unwrap();
11070 let pre_digest =
11071 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
11072 let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
11073
11074 let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
11075 let result = process_reference(
11076 &reference,
11077 &resolver,
11078 doc.root_element(),
11079 ReferenceSet::SignedInfo,
11080 0,
11081 false,
11082 )
11083 .unwrap();
11084 assert!(matches!(result.status, DsigStatus::Valid));
11085 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
11086 }
11087
11088 #[test]
11089 fn reference_sha512_digest() {
11090 let xml = "<root>content</root>";
11091 let doc = Document::parse(xml).unwrap();
11092 let resolver = UriReferenceResolver::new(&doc);
11093
11094 let initial_data = resolver.dereference("").unwrap();
11095 let pre_digest =
11096 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
11097 let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
11098
11099 let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
11100 let result = process_reference(
11101 &reference,
11102 &resolver,
11103 doc.root_element(),
11104 ReferenceSet::SignedInfo,
11105 0,
11106 false,
11107 )
11108 .unwrap();
11109 assert!(matches!(result.status, DsigStatus::Valid));
11110 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
11111 }
11112
11113 #[test]
11116 fn saml_enveloped_reference_processing() {
11117 let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
11119 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
11120 ID="_resp1">
11121 <saml:Assertion ID="_assert1">
11122 <saml:Subject>user@example.com</saml:Subject>
11123 </saml:Assertion>
11124 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
11125 <ds:SignedInfo>
11126 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
11127 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
11128 <ds:Reference URI="">
11129 <ds:Transforms>
11130 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
11131 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
11132 </ds:Transforms>
11133 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
11134 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
11135 </ds:Reference>
11136 </ds:SignedInfo>
11137 <ds:SignatureValue>fakesig==</ds:SignatureValue>
11138 </ds:Signature>
11139 </samlp:Response>"##;
11140 let doc = Document::parse(xml).unwrap();
11141 let resolver = UriReferenceResolver::new(&doc);
11142 let sig_node = doc
11143 .descendants()
11144 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
11145 .unwrap();
11146
11147 let signed_info_node = sig_node
11149 .children()
11150 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
11151 .unwrap();
11152 let signed_info = parse_signed_info(signed_info_node).unwrap();
11153 let reference = &signed_info.references[0];
11154
11155 let initial_data = resolver.dereference("").unwrap();
11157 let pre_digest =
11158 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
11159 .unwrap();
11160 let correct_digest = compute_digest(reference.digest_method, &pre_digest);
11161
11162 let corrected_ref = make_reference(
11164 "",
11165 reference.transforms.clone(),
11166 reference.digest_method,
11167 correct_digest,
11168 );
11169
11170 let result = process_reference(
11172 &corrected_ref,
11173 &resolver,
11174 sig_node,
11175 ReferenceSet::SignedInfo,
11176 0,
11177 true,
11178 )
11179 .unwrap();
11180 assert!(
11181 matches!(result.status, DsigStatus::Valid),
11182 "SAML reference should verify"
11183 );
11184 assert!(result.pre_digest_data.is_some());
11185
11186 let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
11188 assert!(
11189 pre_digest_str.contains("samlp:Response"),
11190 "pre-digest should contain Response"
11191 );
11192 assert!(
11193 !pre_digest_str.contains("SignatureValue"),
11194 "pre-digest should NOT contain Signature"
11195 );
11196 }
11197
11198 #[test]
11199 fn pipeline_missing_signed_info_returns_missing_element() {
11200 let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
11201
11202 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
11203 .expect_err("missing SignedInfo must fail before crypto stage");
11204 assert!(matches!(
11205 err,
11206 SignatureVerificationPipelineError::MissingElement {
11207 element: "SignedInfo"
11208 }
11209 ));
11210 }
11211
11212 #[test]
11213 fn pipeline_multiple_signature_elements_are_rejected() {
11214 let xml = r#"
11215<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
11216 <ds:Signature>
11217 <ds:SignedInfo/>
11218 </ds:Signature>
11219 <ds:Signature/>
11220</root>
11221"#;
11222
11223 let err = verify_signature_with_pem_key(xml, "dummy-key", false)
11224 .expect_err("multiple signatures must fail closed");
11225 assert!(matches!(
11226 err,
11227 SignatureVerificationPipelineError::InvalidStructure {
11228 reason: "Signature must appear exactly once in document",
11229 }
11230 ));
11231 }
11232
11233 #[test]
11234 fn pipeline_start_node_limits_signature_cardinality_to_its_subtree() {
11235 let xml = r#"
11238<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
11239 <scope Id="selected"><ds:Signature/></scope>
11240 <scope Id="other"><ds:Signature/></scope>
11241</root>
11242"#;
11243 let err = VerifyContext::new()
11244 .start_node_id("selected")
11245 .verify(xml)
11246 .expect_err("the selected Signature remains structurally incomplete");
11247 assert!(matches!(
11248 err,
11249 SignatureVerificationPipelineError::MissingElement {
11250 element: "SignedInfo"
11251 }
11252 ));
11253 }
11254
11255 #[test]
11256 fn pipeline_reports_keyinfo_parse_error() {
11257 let xml = r#"
11258<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
11259 xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
11260 <ds:SignedInfo>
11261 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
11262 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
11263 <ds:Reference URI="">
11264 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
11265 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
11266 </ds:Reference>
11267 </ds:SignedInfo>
11268 <ds:SignatureValue>AA==</ds:SignatureValue>
11269 <ds:KeyInfo>
11270 <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
11271 </ds:KeyInfo>
11272</ds:Signature>
11273"#;
11274
11275 let err = VerifyContext::new().verify(xml).expect_err(
11276 "invalid KeyInfo must map to ParseKeyInfo when no explicit key is supplied",
11277 );
11278 assert!(matches!(
11279 err,
11280 SignatureVerificationPipelineError::ParseKeyInfo(_)
11281 ));
11282 }
11283
11284 #[test]
11285 fn pipeline_ignores_malformed_keyinfo_when_explicit_key_is_supplied() {
11286 let base_xml = signature_with_target_reference("AQ==");
11287 let xml = base_xml
11288 .replace(
11289 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
11290 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
11291 )
11292 .replace(
11293 "</ds:SignatureValue>\n </ds:Signature>",
11294 "</ds:SignatureValue>\n <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n </ds:Signature>",
11295 );
11296
11297 let result = VerifyContext::new()
11298 .key(&RejectingKey)
11299 .verify(&xml)
11300 .expect("explicit key path should not fail on malformed KeyInfo");
11301 assert!(matches!(
11302 result.status,
11303 DsigStatus::Invalid(FailureReason::SignatureMismatch)
11304 ));
11305 }
11306
11307 #[test]
11308 fn pipeline_rejects_foreign_element_children_under_signature() {
11309 let base_xml = signature_with_target_reference("AQ==");
11310 let xml = base_xml
11311 .replace(
11312 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
11313 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:foo="urn:example:foo">"#,
11314 )
11315 .replace(
11316 "</ds:SignedInfo>\n <ds:SignatureValue>",
11317 "</ds:SignedInfo>\n <foo:Bar/>\n <ds:SignatureValue>",
11318 );
11319
11320 let err = VerifyContext::new()
11321 .key(&RejectingKey)
11322 .verify(&xml)
11323 .expect_err("foreign element children under Signature must fail closed");
11324 assert!(matches!(
11325 err,
11326 SignatureVerificationPipelineError::InvalidStructure {
11327 reason: "Signature must contain only XMLDSIG element children",
11328 }
11329 ));
11330 }
11331
11332 #[test]
11333 fn pipeline_rejects_non_whitespace_mixed_content_under_signature() {
11334 let base_xml = signature_with_target_reference("AQ==");
11335 let xml = base_xml.replace(
11336 "</ds:SignedInfo>\n <ds:SignatureValue>",
11337 "</ds:SignedInfo>\n oops\n <ds:SignatureValue>",
11338 );
11339
11340 let err = VerifyContext::new()
11341 .key(&RejectingKey)
11342 .verify(&xml)
11343 .expect_err("non-whitespace mixed content under Signature must fail closed");
11344 assert!(matches!(
11345 err,
11346 SignatureVerificationPipelineError::InvalidStructure {
11347 reason: "Signature must not contain non-whitespace mixed content",
11348 }
11349 ));
11350 }
11351
11352 #[test]
11353 fn pipeline_rejects_keyinfo_out_of_order() {
11354 let base_xml = signature_with_target_reference("AQ==");
11355 let xml = base_xml.replace(
11356 "</ds:SignatureValue>\n </ds:Signature>",
11357 "</ds:SignatureValue>\n <ds:Object/>\n <ds:KeyInfo><ds:KeyName>late</ds:KeyName></ds:KeyInfo>\n </ds:Signature>",
11358 );
11359
11360 let err = VerifyContext::new()
11361 .key(&RejectingKey)
11362 .verify(&xml)
11363 .expect_err("KeyInfo after Object must be rejected by Signature child order checks");
11364 assert!(matches!(
11365 err,
11366 SignatureVerificationPipelineError::InvalidStructure {
11367 reason: "KeyInfo must be the third element child of Signature when present"
11368 }
11369 ));
11370 }
11371
11372 #[test]
11373 fn pipeline_accepts_comments_and_processing_instructions_under_signature() {
11374 let xml = r#"
11375<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
11376 <?dbg keep ?>
11377 <!-- signature metadata -->
11378 <ds:SignedInfo>
11379 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
11380 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
11381 <ds:Reference URI="">
11382 <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
11383 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
11384 </ds:Reference>
11385 </ds:SignedInfo>
11386 <!-- between required children -->
11387 <ds:SignatureValue>AA==</ds:SignatureValue>
11388</ds:Signature>
11389"#;
11390
11391 let doc = Document::parse(xml).expect("test XML must parse");
11392 let signature_node = doc.root_element();
11393 let parsed = parse_signature_children(signature_node)
11394 .expect("comment/PI nodes under Signature must be ignored");
11395
11396 assert_eq!(parsed.signed_info_node.tag_name().name(), "SignedInfo");
11397 assert_eq!(
11398 parsed.signature_value_node.tag_name().name(),
11399 "SignatureValue"
11400 );
11401 assert!(parsed.key_info_node.is_none());
11402 }
11403}