Skip to main content

xml_sec/xmldsig/
verify.rs

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