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