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