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