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`](super::digest::compute_digest) + [`constant_time_eq`] for digest
11//!   computation and comparison
12//! - [`verify_signature_with_pem_key`] for full pipeline validation (`SignedInfo` + `SignatureValue`)
13
14use crate::xml::dom::{Node, NodeId};
15use base64::Engine;
16use std::cell::{Cell, RefCell};
17use std::collections::{HashMap, HashSet};
18
19use crate::c14n::canonicalize_bounded_with_xml_base_budget;
20use crate::document::{
21    DocumentParseSettings, DocumentView, NodeIdentity, XmlDocument, XmlDocumentError,
22};
23use crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING;
24use crate::operation::{
25    OperationDecisionReason, OperationExecutionContext, OperationNodeId, OperationNodeKind,
26    OperationPlanError, OperationResourceIdentity, OperationStage,
27};
28
29#[cfg(test)]
30use super::digest::compute_digest;
31use super::digest::{DigestAlgorithm, constant_time_eq};
32#[cfg(test)]
33use super::parse::MAX_REFERENCES_PER_SIGNATURE;
34#[cfg(test)]
35use super::parse::parse_key_info;
36use super::parse::{
37    KeyInfo, MAX_X509_DATA_TOTAL_BINARY_LEN, MAX_X509_DECODED_BINARY_LEN, ParseError, Reference,
38    RetrievalMethodTransforms, SignatureAlgorithm, XMLDSIG_NS,
39};
40use super::parse::{
41    parse_reference_with_xpath_budget, parse_signed_info_with_xpath_budget,
42    parse_x509_data_dispatch_with_budget_and_provider, reference_digest_method,
43};
44use super::signature::{
45    SignatureVerificationError, verify_dsa_signature_spki, verify_ecdsa_signature_pem,
46    verify_rsa_signature_pem,
47};
48#[cfg(test)]
49use super::transforms::BASE64_TRANSFORM_URI;
50use super::transforms::{
51    DEFAULT_IMPLICIT_C14N_URI, Transform, TransformExecutionBudget, TransformOptions,
52    XPATH_TRANSFORM_URI, XPathHereSemantics, XPathSignatureParseBudget,
53    map_c14n_resource_policy_violation, transform_chain_uses_implicit_c14n,
54};
55use super::types::{NodeSet, TransformError, subtree_node_id_range};
56use super::uri::{ExternalResourceMapError, UriReferenceResolver, validate_external_resource_map};
57use super::whitespace::{is_xml_whitespace_only, normalize_xml_base64_bytes};
58
59mod session;
60pub use session::*;
61mod capture;
62pub use capture::SelectedSignatureMethod;
63use capture::{CapturedOperation, ScopedCapture, VerificationCapture};
64
65const MAX_SIGNATURE_VALUE_LEN: usize = 8192;
66const MAX_RETRIEVAL_METHOD_COUNT: usize = 64;
67/// Cryptographic verifier used by [`VerifyContext`].
68///
69/// This trait intentionally has no `Send + Sync` supertraits so lightweight
70/// single-threaded verifiers can be used without additional bounds.
71pub trait VerifyingKey {
72    /// Borrow public SPKI describing this exact key after successful verification.
73    /// This is evidence only, never authorization for native verification or
74    /// private-key export. Composite handles expose their leaves instead. This
75    /// hook only borrows cached public metadata; it must not allocate, parse,
76    /// perform I/O or manufacture a different key after the decision.
77    fn association_spki(&self, _algorithm: SignatureAlgorithm) -> Option<&[u8]> {
78        None
79    }
80    /// Exact external execution domain; a key cannot be moved between token sessions.
81    fn provider_binding(&self) -> Option<&crate::provider::ProviderBinding> {
82        None
83    }
84    /// Composite resolvers may try individual keys through the selected engine.
85    /// `None` identifies a leaf handle. Wrappers must preserve per-key policy
86    /// when forwarding the visitor; no public/private key copy is required.
87    fn verify_candidate_keys(
88        &self,
89        _verify: &mut dyn FnMut(&dyn VerifyingKey) -> Result<bool, DsigError>,
90    ) -> Result<Option<bool>, DsigError> {
91        Ok(None)
92    }
93    /// Borrow public SPKI for native verification, validating method/key binding.
94    /// Opaque external keys may return `None`; native providers then fail closed.
95    fn verification_spki(
96        &self,
97        _algorithm: SignatureAlgorithm,
98    ) -> Result<Option<&[u8]>, DsigError> {
99        Ok(None)
100    }
101
102    /// Wire encoding already selected by the immutable verification policy.
103    fn ecdsa_encoding(&self) -> crate::policy::EcdsaSignatureValueEncoding {
104        crate::policy::EcdsaSignatureValueEncoding::XmlDsig
105    }
106    /// Verify with validated domain separation, rejecting unsupported contexts.
107    fn verify_with_context(
108        &self,
109        algorithm: SignatureAlgorithm,
110        context: &super::SignatureContext,
111        data: &[u8],
112        signature: &[u8],
113    ) -> Result<bool, DsigError> {
114        if !context.as_bytes().is_empty() {
115            return Err(super::SignatureVerificationError::UnsupportedAlgorithm {
116                uri: algorithm.uri().to_owned(),
117            }
118            .into());
119        }
120        self.verify(algorithm, data, signature)
121    }
122    /// Validate this key against the operation's immutable trust policy.
123    ///
124    /// Built-in keys override this hook so pre-resolved and resolver-produced
125    /// keys enforce identical strength constraints. Custom opaque keys may
126    /// retain their own policy enforcement by accepting the default no-op.
127    fn validate_policy(
128        &self,
129        _policy: &crate::policy::VerificationPolicy,
130    ) -> Result<(), DsigError> {
131        Ok(())
132    }
133
134    /// Check that `signature_value` has the wire framing required by the
135    /// declared algorithm and this key before provider dispatch.
136    ///
137    /// Key implementations with key-size-dependent framing should override
138    /// this method. The default enforces the algorithm-wide XMLDSig envelope.
139    fn validate_signature_value(
140        &self,
141        algorithm: SignatureAlgorithm,
142        signature_value: &[u8],
143    ) -> Result<bool, DsigError> {
144        Ok(super::signature::signature_value_matches_algorithm(
145            algorithm,
146            signature_value,
147        ))
148    }
149
150    /// Validate wire framing under the operation's immutable compatibility policy.
151    ///
152    /// The default delegates to [`VerifyingKey::validate_signature_value`] and
153    /// therefore implements only the standard XMLDSig framing contract. Custom
154    /// keys that support policy-selected wire formats, such as
155    /// [`crate::policy::EcdsaSignatureValueEncoding::XmlSecAsn1Der`], must
156    /// override this hook rather than accepting both formats implicitly.
157    fn validate_signature_value_with_policy(
158        &self,
159        policy: &crate::policy::VerificationPolicy,
160        algorithm: SignatureAlgorithm,
161        signature_value: &[u8],
162    ) -> Result<bool, DsigError> {
163        let _ = policy;
164        self.validate_signature_value(algorithm, signature_value)
165    }
166
167    /// Verify `signature_value` over `signed_data` with the declared algorithm.
168    fn verify(
169        &self,
170        algorithm: SignatureAlgorithm,
171        signed_data: &[u8],
172        signature_value: &[u8],
173    ) -> Result<bool, DsigError>;
174
175    /// Verify after applying operation-scoped compatibility semantics.
176    ///
177    /// The default delegates to [`VerifyingKey::verify`]. Custom keys whose
178    /// provider input depends on compatibility policy must override this hook
179    /// consistently with [`VerifyingKey::validate_signature_value_with_policy`].
180    fn verify_with_policy(
181        &self,
182        policy: &crate::policy::VerificationPolicy,
183        algorithm: SignatureAlgorithm,
184        signed_data: &[u8],
185        signature_value: &[u8],
186    ) -> Result<bool, DsigError> {
187        let _ = policy;
188        self.verify(algorithm, signed_data, signature_value)
189    }
190}
191
192/// Key resolver hook used by [`VerifyContext`] when no pre-set key is provided.
193///
194/// This trait intentionally has no `Send + Sync` supertraits; callers that need
195/// cross-thread sharing can wrap resolvers/keys in their own thread-safe types.
196pub trait KeyResolver {
197    /// Resolve using the core operation's cumulative candidate and certificate account.
198    /// The default charges one custom-resolver dispatch. Implementations searching
199    /// multiple candidates override this method, charging each inspection through
200    /// `budget` and using its execution account for certificate mechanism work.
201    fn resolve_for_verification_with_budget<'a>(
202        &'a self,
203        key_info: Option<&KeyInfo>,
204        algorithm: SignatureAlgorithm,
205        policy: &crate::policy::VerificationPolicy,
206        provider: &dyn crate::provider::CryptoProvider,
207        budget: &mut super::InspectedKeyCandidateBudget,
208    ) -> Result<Option<super::ResolvedVerificationKey<'a>>, DsigError> {
209        budget.bind_policy(policy)?;
210        budget.charge()?;
211        self.resolve_for_verification(key_info, algorithm, policy, provider)
212    }
213
214    /// Discover and explicitly authorize a key for an end-to-end operation.
215    /// The default preserves discovery only: applications that authorize keys
216    /// themselves return `ResolvedVerificationKey::CallerTrusted` explicitly.
217    fn resolve_for_verification<'a>(
218        &'a self,
219        key_info: Option<&KeyInfo>,
220        algorithm: SignatureAlgorithm,
221        policy: &crate::policy::VerificationPolicy,
222        provider: &dyn crate::provider::CryptoProvider,
223    ) -> Result<Option<super::ResolvedVerificationKey<'a>>, DsigError> {
224        Ok(self
225            .resolve_with_policy_and_provider(key_info, algorithm, policy, provider)?
226            .map(super::ResolvedVerificationKey::Candidate))
227    }
228    /// Resolve a verification key from parsed `<KeyInfo>` sources.
229    ///
230    /// Return `Ok(None)` when no suitable key could be resolved from available
231    /// key material (for example, missing `<KeyInfo>` candidates). `VerifyContext`
232    /// maps `Ok(None)` to `DsigStatus::Invalid(FailureReason::KeyNotFound)`;
233    /// reserve `Err(...)` for resolver failures.
234    fn resolve<'a>(
235        &'a self,
236        key_info: Option<&KeyInfo>,
237        algorithm: SignatureAlgorithm,
238    ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError>;
239
240    /// Resolve under the operation's immutable policy snapshot.
241    ///
242    /// Implementations that make trust or key-source decisions must override
243    /// this method. Resolver implementations that inspect multiple candidates
244    /// must enforce [`crate::policy::ResourcePolicy::max_key_candidates`] across
245    /// that internal search. The verification pipeline separately requires
246    /// capacity for the single candidate returned by any resolver. The default
247    /// preserves source-only custom resolvers whose behavior is independent of
248    /// other policy fields.
249    fn resolve_with_policy<'a>(
250        &'a self,
251        key_info: Option<&KeyInfo>,
252        algorithm: SignatureAlgorithm,
253        _policy: &crate::policy::VerificationPolicy,
254    ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
255        self.resolve(key_info, algorithm)
256    }
257
258    /// Resolve under both the operation policy and cryptographic provider.
259    ///
260    /// Resolvers that evaluate cryptographic key metadata, such as
261    /// `X509Digest`, must override this hook. The default keeps existing
262    /// policy-aware custom resolvers source-compatible.
263    fn resolve_with_policy_and_provider<'a>(
264        &'a self,
265        key_info: Option<&KeyInfo>,
266        algorithm: SignatureAlgorithm,
267        policy: &crate::policy::VerificationPolicy,
268        _provider: &dyn crate::provider::CryptoProvider,
269    ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
270        self.resolve_with_policy(key_info, algorithm, policy)
271    }
272
273    /// Return `true` when this resolver consumes document `<KeyInfo>` material.
274    ///
275    /// The verification pipeline uses this to decide whether malformed
276    /// `<KeyInfo>` should raise `DsigError::ParseKeyInfo` before resolver
277    /// execution. Resolvers that ignore document key material can keep the
278    /// default `false` to avoid fail-closed parsing on advisory `<KeyInfo>`.
279    fn consumes_document_key_info(&self) -> bool {
280        false
281    }
282}
283
284/// Allowed structural URI classes for XMLDSig reference and key-source policy.
285///
286/// External URIs resolve only from bytes supplied through
287/// [`VerifyContext::external_resources`]; allowing them never enables I/O.
288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289#[must_use = "pass the policy to VerifyContext::allowed_uri_types(), or store it for reuse"]
290pub struct UriTypeSet {
291    allow_empty: bool,
292    allow_same_document: bool,
293    allow_external: bool,
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297enum UriClass {
298    Empty,
299    SameDocument,
300    External,
301}
302
303fn classify_uri(uri: &str) -> UriClass {
304    if uri.is_empty() {
305        UriClass::Empty
306    } else if uri.starts_with('#') {
307        UriClass::SameDocument
308    } else {
309        UriClass::External
310    }
311}
312
313impl UriTypeSet {
314    /// Create a custom URI policy.
315    pub const fn new(allow_empty: bool, allow_same_document: bool, allow_external: bool) -> Self {
316        Self {
317            allow_empty,
318            allow_same_document,
319            allow_external,
320        }
321    }
322
323    /// Allow only same-document references (`""`, `#id`, `#xpointer(...)`).
324    pub const SAME_DOCUMENT: Self = Self {
325        allow_empty: true,
326        allow_same_document: true,
327        allow_external: false,
328    };
329
330    /// Allow all URI classes.
331    ///
332    /// External URIs still require an explicit caller-owned resource map.
333    pub const ALL: Self = Self {
334        allow_empty: true,
335        allow_same_document: true,
336        allow_external: true,
337    };
338
339    /// Return whether this set permits the URI's structural class.
340    pub fn allows(self, uri: &str) -> bool {
341        match classify_uri(uri) {
342            UriClass::Empty => self.allow_empty,
343            UriClass::SameDocument => self.allow_same_document,
344            UriClass::External => self.allow_external,
345        }
346    }
347}
348
349impl Default for UriTypeSet {
350    fn default() -> Self {
351        Self::SAME_DOCUMENT
352    }
353}
354
355/// Request-scoped selection of the XMLDSig operation node.
356#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
357pub enum SignatureSelection<'a> {
358    /// Require exactly one `Signature` in the complete document.
359    #[default]
360    UniqueDocumentSignature,
361    /// Select the first descendant `Signature` from the document root.
362    FirstDocumentSignature,
363    /// Select the first descendant `Signature` below the element with this ID.
364    FirstSignatureUnderId(&'a str),
365}
366
367/// Verification builder/configuration.
368#[must_use = "configure the context and call verify(), or store it for reuse"]
369#[derive(Clone)]
370pub struct VerifyContext<'a> {
371    utc_facts: Option<structured_pkix::temporal::LeapTable<'a>>,
372    decapsulation_key: Option<&'a dyn crate::provider::KeyDecapsulationKey>,
373    key: Option<&'a dyn VerifyingKey>,
374    key_resolver: Option<&'a dyn KeyResolver>,
375    policy: crate::policy::VerificationPolicy,
376    provider: &'a dyn crate::provider::CryptoProvider,
377    xml_backend: crate::XmlBackend,
378    store_pre_digest: bool,
379    external_resources: Option<&'a HashMap<String, Vec<u8>>>,
380    signature_selection: SignatureSelection<'a>,
381    id_attributes: &'a [crate::IdAttributeRegistration],
382}
383
384impl<'a> VerifyContext<'a> {
385    /// Create a context with conservative defaults.
386    ///
387    /// Defaults:
388    /// - no pre-set key, no key resolver
389    /// - manifests disabled
390    /// - same-document URIs only
391    /// - all transforms allowed
392    /// - pre-digest buffers not stored
393    pub fn new() -> Self {
394        Self {
395            utc_facts: None,
396            decapsulation_key: None,
397            key: None,
398            key_resolver: None,
399            policy: crate::policy::VerificationPolicy::default(),
400            provider: crate::provider::default_provider(),
401            xml_backend: crate::XmlBackend::default(),
402            store_pre_digest: false,
403            external_resources: None,
404            signature_selection: SignatureSelection::UniqueDocumentSignature,
405            id_attributes: &[],
406        }
407    }
408
409    /// Supply application-owned UTC history for certificate evidence. The
410    /// context borrows the table; it neither copies it nor discovers updates.
411    pub fn utc_facts(mut self, facts: structured_pkix::temporal::LeapTable<'a>) -> Self {
412        self.utc_facts = Some(facts);
413        self
414    }
415
416    /// Set a pre-resolved verification key.
417    ///
418    /// Built-in [`super::VerificationKey`] values are validated against the
419    /// same operation key-strength policy as resolver-produced keys. Custom
420    /// opaque [`VerifyingKey`] implementations retain responsibility for any
421    /// key metadata that the core cannot inspect.
422    pub fn key(mut self, key: &'a dyn VerifyingKey) -> Self {
423        self.key = Some(key);
424        self
425    }
426
427    /// Recover the HMAC secret using an explicit recipient private key.
428    /// Policy must grant the exact experimental KEM. A valid KEM/HMAC result
429    /// is not evidence of sender identity.
430    pub fn decapsulation_key(mut self, key: &'a dyn crate::provider::KeyDecapsulationKey) -> Self {
431        self.decapsulation_key = Some(key);
432        self
433    }
434
435    /// Set a key resolver fallback used when `key()` is not provided.
436    pub fn key_resolver(mut self, resolver: &'a dyn KeyResolver) -> Self {
437        self.key_resolver = Some(resolver);
438        self
439    }
440
441    /// Replace the complete immutable verification policy snapshot.
442    pub fn policy(mut self, policy: crate::policy::VerificationPolicy) -> Self {
443        self.policy = policy;
444        self
445    }
446
447    /// Select the cryptographic provider for this verification operation.
448    pub fn provider(mut self, provider: &'a dyn crate::provider::CryptoProvider) -> Self {
449        self.provider = provider;
450        self
451    }
452
453    /// Select the compiled XML parser backend for this verification operation.
454    pub fn xml_backend(mut self, backend: crate::XmlBackend) -> Self {
455        self.xml_backend = backend;
456        self
457    }
458
459    /// Enable or disable `<Manifest>` processing.
460    ///
461    /// Process references only when this Signature authenticated the original
462    /// Manifest or an ancestor through an ID-based same-document reference
463    /// using only canonicalization transforms (or implicit canonicalization).
464    /// Such Manifests may be nested or outside Signature; physical ancestry
465    /// alone never authenticates them. Authenticated Manifest references can
466    /// establish the next discovery frontier through the same operation state.
467    /// Filtering or binary transforms do not prove complete original structure.
468    /// Unsigned/unreferenced Manifests are skipped.
469    /// Whole-document same-document references such as `URI=""` or
470    /// `URI="#xpointer(/)"` do not establish an element-specific Manifest
471    /// authentication proof for this option.
472    ///
473    /// Manifests are parsed and processed only after the SignedInfo references
474    /// and SignatureValue both validate. Their digest mismatches, policy
475    /// violations, and processing failures are then reported independently in
476    /// `VerifyResult::manifest_references` and do not alter `VerifyResult::status`.
477    /// Callers that enable `process_manifests(true)` must inspect
478    /// `VerifyResult::manifest_references` in addition to `VerifyResult::status`
479    /// when interpreting `verify()` results.
480    /// Structural/parse errors in Manifest content abort `verify()` and are
481    /// returned as `Err(...)`.
482    pub fn process_manifests(mut self, enabled: bool) -> Self {
483        self.policy.manifest_processing = if enabled {
484            crate::policy::ManifestProcessing::Process
485        } else {
486            crate::policy::ManifestProcessing::Ignore
487        };
488        self
489    }
490
491    /// Restrict allowed reference URI classes.
492    pub fn allowed_uri_types(mut self, types: UriTypeSet) -> Self {
493        self.policy.uris.references = types;
494        self
495    }
496
497    /// Restrict URI classes used to retrieve key material from `<KeyInfo>`.
498    ///
499    /// This policy is independent from [`Self::allowed_uri_types`]: allowing an
500    /// external signed payload does not implicitly allow external key retrieval.
501    /// Same-document retrieval is enabled by default; external retrieval requires
502    /// an explicit opt-in and still uses only caller-supplied resources.
503    pub fn allowed_retrieval_method_uri_types(mut self, types: UriTypeSet) -> Self {
504        self.policy.uris.retrieval_methods = types;
505        self
506    }
507
508    /// Provide external URI payloads explicitly.
509    ///
510    /// The map is the complete external I/O boundary: verification never
511    /// performs network or filesystem access. External URIs must also be
512    /// enabled through [`UriTypeSet`]. Map keys are RFC 3986 resolved URI
513    /// identities: use normalized paths with dot segments removed and retain
514    /// query or fragment suffixes.
515    pub fn external_resources(mut self, resources: &'a HashMap<String, Vec<u8>>) -> Self {
516        self.external_resources = Some(resources);
517        self
518    }
519
520    /// Select the operation start node by its XML ID value.
521    ///
522    /// Verification selects the first descendant `<Signature>` in document
523    /// order. This is request context, not a policy decision, and mirrors
524    /// libxmlsec1's depth-first `xmlSecFindNode` start-node contract.
525    pub fn start_node_id(mut self, id: &'a str) -> Self {
526        self.signature_selection = SignatureSelection::FirstSignatureUnderId(id);
527        self
528    }
529
530    /// Select the first descendant `<Signature>` from the document root.
531    ///
532    /// This is the libxmlsec1 command-line operation-root contract. The library
533    /// default remains fail-closed and requires a unique document signature.
534    pub fn first_document_signature(mut self) -> Self {
535        self.signature_selection = SignatureSelection::FirstDocumentSignature;
536        self
537    }
538
539    /// Add caller-declared ID attributes for start-node and Reference lookup.
540    pub fn id_attributes(mut self, registrations: &'a [crate::IdAttributeRegistration]) -> Self {
541        self.id_attributes = registrations;
542        self
543    }
544
545    /// Allow bounded internal DTD declarations while keeping external entity
546    /// resolution disabled. This is off by default.
547    pub fn allow_internal_dtd(mut self, enabled: bool) -> Self {
548        self.policy.xml.allow_internal_dtd = enabled;
549        self
550    }
551
552    /// Restrict allowed transform and canonicalization algorithms by URI.
553    ///
554    /// Example values:
555    /// - `http://www.w3.org/2000/09/xmldsig#enveloped-signature`
556    /// - `http://www.w3.org/2001/10/xml-exc-c14n#`
557    ///
558    /// The allowlist covers explicit Reference and RetrievalMethod transforms,
559    /// the declared SignedInfo canonicalization method, and implicit default
560    /// C14N (`http://www.w3.org/TR/2001/REC-xml-c14n-20010315`) when a Reference
561    /// transform chain ends as a node set.
562    pub fn allowed_transforms<I, S>(mut self, transforms: I) -> Self
563    where
564        I: IntoIterator<Item = S>,
565        S: Into<String>,
566    {
567        self.policy.transforms.allowed_algorithms =
568            Some(transforms.into_iter().map(Into::into).collect());
569        self
570    }
571
572    /// Store pre-digest buffers for diagnostics.
573    ///
574    /// Retained reference buffers and canonicalized `<SignedInfo>` share a
575    /// non-configurable 32 MiB safety ceiling. Canonicalized `<SignedInfo>` is
576    /// charged even when diagnostic retention is disabled because signature
577    /// verification always materializes it. Overflow remains a typed policy
578    /// violation at both low-level and end-to-end entry points.
579    pub fn store_pre_digest(mut self, enabled: bool) -> Self {
580        self.store_pre_digest = enabled;
581        self
582    }
583
584    /// Select the node returned by XPath's `here()` extension function.
585    ///
586    /// The default follows XMLDSig and returns the `<XPath>` parameter.
587    /// Use [`XPathHereSemantics::XmlSecLegacy`] only for documents known to
588    /// have been generated with libxmlsec1's `<Transform>` interpretation.
589    pub fn xpath_here_semantics(mut self, semantics: XPathHereSemantics) -> Self {
590        self.policy.transforms.xpath_here_semantics = semantics;
591        self
592    }
593
594    fn allowed_transform_uris(&self) -> Option<&HashSet<String>> {
595        self.policy.transforms.allowed_algorithms.as_ref()
596    }
597
598    fn transform_options(&self) -> TransformOptions<'_> {
599        TransformOptions::default()
600            .id_attributes(self.id_attributes)
601            .allow_internal_dtd(self.policy.xml.allow_internal_dtd)
602            .xpath_here_semantics(self.policy.transforms.xpath_here_semantics)
603            .opc_relationship_edition(self.policy.transforms.opc_relationship_edition)
604            .xslt_stylesheet_semantics(self.policy.transforms.xslt_stylesheet_semantics)
605    }
606
607    /// Verify one XMLDSig signature using this context.
608    ///
609    /// Returns `Ok(VerifyResult)` for both valid and invalid signatures; inspect
610    /// `VerifyResult::status` for the core `<SignedInfo>` and signature-value
611    /// outcome. When Manifest processing is enabled, inspect every
612    /// `VerifyResult::manifest_references` entry separately. `Err(...)` is
613    /// reserved for pipeline failures.
614    pub fn verify(&self, xml: &str) -> Result<VerifyResult, DsigError> {
615        verify_signature_with_context(xml, self)
616    }
617
618    /// Verify a signature against a retained owned document generation.
619    ///
620    /// The active XML input policy is revalidated against the document's parse
621    /// provenance as well as its current byte and node counts. In particular, a
622    /// strict context rejects a document that required internal DTD support.
623    pub fn verify_document(&self, document: &XmlDocument) -> Result<VerifyResult, DsigError> {
624        verify_signature_document_with_context(document, self)
625    }
626
627    /// Verify every Signature in one retained document generation.
628    ///
629    /// All signatures share the operation's work and retention budgets. A
630    /// malformed or invalid signature is retained as an individual outcome,
631    /// never silently skipped. Call [`VerifyEvidence::accepted`] with the
632    /// current document before accepting the request; a successful return
633    /// alone is not validation. [`VerifyEvidence::all_valid`] is historical
634    /// mathematical diagnostics only and does not revalidate document generation.
635    pub fn verify_all(&self, document: &XmlDocument) -> Result<VerifyEvidence, DsigError> {
636        verify_all_document(document, self, &VerificationRequest::default())
637    }
638
639    /// Discover bounded Signature identities without key or digest work.
640    ///
641    /// These are untrusted selection handles, not verification evidence. Use
642    /// them to authorize request keys even when Signatures have no XML ID.
643    pub fn signature_identities(
644        &self,
645        document: &XmlDocument,
646    ) -> Result<Vec<NodeIdentity>, DsigError> {
647        self.policy.validate()?;
648        document.validate_operation_policy(&self.policy.xml, &self.policy.resources)?;
649        document.with_view(|view| {
650            let count = count_signature_nodes(view, self.policy.resources.max_signatures)?;
651            let mut identities = Vec::with_capacity(count);
652            for node in view.document().descendants() {
653                if node.has_tag_name((XMLDSIG_NS, "Signature")) {
654                    identities.push(view.node_identity(node));
655                }
656            }
657            Ok(identities)
658        })
659    }
660
661    /// Verify all signatures against explicit caller-selected targets and keys.
662    ///
663    /// Requests are not policy: they cannot permit algorithms, URI classes or
664    /// XML features forbidden by the immutable policy snapshot.
665    pub fn verify_request(
666        &self,
667        document: &XmlDocument,
668        request: &VerificationRequest<'_>,
669    ) -> Result<VerifyEvidence, DsigError> {
670        verify_all_document(document, self, request)
671    }
672}
673
674/// Application requirement for the independently verified signatures.
675#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
676pub enum SignatureRequirement {
677    /// Every discovered signature must pass, and at least one must exist.
678    #[default]
679    All,
680    /// Explicitly tolerate other invalid signatures if this many pass.
681    AtLeast(usize),
682    /// Require exactly this many discovered signatures, all passing.
683    Exactly(usize),
684}
685
686/// Explicit key authorization for one stable, caller-selected Signature.
687pub struct CallerTrustedSignatureKey<'a> {
688    /// Signature that this key is authorized to verify.
689    pub signature: NodeIdentity,
690    /// Application-owned key; embedded KeyInfo cannot override it.
691    pub key: &'a dyn VerifyingKey,
692}
693
694/// Request data, immutably borrowed for one verification operation.
695#[derive(Default)]
696pub struct VerificationRequest<'a> {
697    /// Elements whose complete expanded content must be authenticated.
698    /// Comments and an enveloped Signature are not part of this assertion.
699    pub expected_targets: &'a [NodeIdentity],
700    /// Per-signature trusted keys. Duplicate/foreign/stale mappings fail closed.
701    pub trusted_keys: &'a [CallerTrustedSignatureKey<'a>],
702    /// Explicit external bytes; no implicit I/O or configuration discovery.
703    pub external_resources: Option<&'a HashMap<String, Vec<u8>>>,
704    /// Required multi-signature outcome; zero counts are invalid requests.
705    pub signatures: SignatureRequirement,
706    /// Opaque caller correlation value, never read from XML or used as trust.
707    pub correlation: Option<[u8; 16]>,
708}
709
710/// Outcome for one exact Signature, including pipeline failures.
711#[derive(Debug)]
712pub struct SignatureEvidence {
713    identity: NodeIdentity,
714    result: Result<VerifyResult, DsigError>,
715}
716
717impl SignatureEvidence {
718    /// Stable identity of the Signature actually processed.
719    #[must_use]
720    pub const fn identity(&self) -> NodeIdentity {
721        self.identity
722    }
723
724    /// Individual outcome. Digest matches do not imply signature validity.
725    pub const fn result(&self) -> &Result<VerifyResult, DsigError> {
726        &self.result
727    }
728}
729
730/// Bounded outcomes for all signatures in one document generation.
731#[derive(Debug)]
732#[must_use = "inspect the individual outcomes before accepting the document"]
733pub struct VerifyEvidence<V = Vec<SignatureEvidence>> {
734    document: NodeIdentity,
735    signatures: V,
736    accepted: bool,
737    correlation: Option<[u8; 16]>,
738}
739
740impl<V: std::ops::Deref<Target = [SignatureEvidence]>> VerifyEvidence<V> {
741    /// Whether the explicit request requirements hold for this document.
742    /// Foreign documents or any mutation after verification invalidate acceptance.
743    pub fn accepted(&self, document: &XmlDocument) -> Result<bool, DsigError> {
744        document.with_view(|view| {
745            view.resolve_node(self.document)?;
746            Ok(self.accepted)
747        })
748    }
749
750    /// Uninterpreted correlation value supplied by the caller.
751    #[must_use]
752    pub const fn correlation(&self) -> Option<[u8; 16]> {
753        self.correlation
754    }
755
756    /// Prove complete expanded element content under an authorized signature.
757    ///
758    /// This does not claim source-byte equality, comment coverage or protection
759    /// of the owning enveloped Signature. Arbitrary XPath/binary transforms
760    /// remain indeterminate rather than falsely covering their original input.
761    /// The exact document generation must still exist; wrapping an unsigned
762    /// sibling with a matching name cannot satisfy an identity-based assertion.
763    pub fn covers_element(
764        &self,
765        document: &XmlDocument,
766        target: NodeIdentity,
767    ) -> Result<bool, DsigError> {
768        document.with_view(|view| {
769            view.resolve_node(self.document)?;
770            let target_node = view.resolve_node(target)?;
771            if !target_node.is_element() {
772                return Err(DsigError::InvalidRequest {
773                    reason: "coverage target must be an element",
774                });
775            }
776            let target_id = target_node.id().get();
777            for signature in self.signatures() {
778                let Ok(result) = &signature.result else {
779                    continue;
780                };
781                if result.status != DsigStatus::Valid
782                    || !matches!(
783                        result.key_trust,
784                        super::KeyTrustEvidence::CallerTrusted
785                            | super::KeyTrustEvidence::ValidatedX509(_)
786                    )
787                {
788                    continue;
789                }
790                let signature_node = view.resolve_node(signature.identity)?;
791                for reference in result
792                    .signed_info_references
793                    .iter()
794                    .chain(&result.manifest_references)
795                {
796                    if reference.status != DsigStatus::Valid {
797                        continue;
798                    }
799                    let Some(root) = reference.target_identity else {
800                        continue;
801                    };
802                    if reference.coverage == ReferenceCoverage::TransformedData {
803                        continue;
804                    }
805                    let root = view.resolve_node(root)?;
806                    // The normalized arena assigns contiguous preorder IDs;
807                    // reuse the same subtree contract as node-set subtraction,
808                    // rather than walking depth for every report/target pair.
809                    if !subtree_node_id_range(root).contains(&target_id) {
810                        continue;
811                    }
812                    if reference.coverage == ReferenceCoverage::EnvelopedElementContent
813                        && subtree_node_id_range(signature_node).contains(&target_id)
814                    {
815                        continue;
816                    }
817                    return Ok(true);
818                }
819            }
820            Ok(false)
821        })
822    }
823
824    /// Outcomes in document order, including malformed signatures.
825    #[must_use]
826    pub fn signatures(&self) -> &[SignatureEvidence] {
827        &self.signatures
828    }
829
830    /// Whether a nonempty set of signatures and their processed Manifests passed.
831    ///
832    /// This is a historical mathematical outcome, not current-document
833    /// authorization. Use `accepted(document)` for generation-bound acceptance.
834    #[must_use]
835    pub fn all_valid(&self) -> bool {
836        !self.signatures.is_empty()
837            && self.signatures.iter().all(|entry| {
838                entry.result.as_ref().is_ok_and(|result| {
839                    result.status == DsigStatus::Valid
840                        && result
841                            .manifest_references
842                            .iter()
843                            .all(|reference| reference.status == DsigStatus::Valid)
844                })
845            })
846    }
847}
848
849impl Default for VerifyContext<'_> {
850    fn default() -> Self {
851        Self::new()
852    }
853}
854
855/// Per-reference verification result.
856#[derive(Debug, Clone)]
857#[non_exhaustive]
858#[must_use = "inspect status before accepting the reference result"]
859pub struct ReferenceResult {
860    /// Exact Reference node when processing a retained document.
861    pub reference_identity: Option<NodeIdentity>,
862    /// Resolved same-document target, not an untrusted URI claim.
863    pub target_identity: Option<NodeIdentity>,
864    /// SHA-256 identity of resolved external bytes before transforms. This is
865    /// provenance, not signer authorization or an original-element proof.
866    pub external_resource_fingerprint: Option<[u8; 32]>,
867    /// What can be proven about original element-content coverage.
868    pub coverage: ReferenceCoverage,
869    /// Whether this reference came from `<SignedInfo>` or `<Manifest>`.
870    pub reference_set: ReferenceSet,
871    /// Zero-based index within `reference_set`.
872    pub reference_index: usize,
873    /// URI from the `<Reference>` element (for diagnostics).
874    pub uri: String,
875    /// Digest algorithm used.
876    pub digest_algorithm: DigestAlgorithm,
877    /// Reference verification status.
878    pub status: DsigStatus,
879    /// Pre-digest bytes (populated when `store_pre_digest` is enabled).
880    pub pre_digest_data: Option<Vec<u8>>,
881}
882
883/// Conservative coverage classification for the original semantic document.
884///
885/// XMLDSig 1.1 §8.1.1 protects only transformed data. Unknown projection is
886/// deliberately not promoted to complete original-element coverage.
887/// <https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-Signed>
888#[derive(Debug, Clone, Copy, PartialEq, Eq)]
889#[non_exhaustive]
890pub enum ReferenceCoverage {
891    /// No complete original-element coverage proof (binary or arbitrary filter).
892    TransformedData,
893    /// Expanded element names, attributes and content, excluding comments.
894    ElementContent,
895    /// Element content except the owning Signature subtree and comments.
896    EnvelopedElementContent,
897}
898
899/// Origin of a processed `<Reference>`.
900#[derive(Debug, Clone, Copy, PartialEq, Eq)]
901#[non_exhaustive]
902pub enum ReferenceSet {
903    /// `<Reference>` under `<SignedInfo>`.
904    SignedInfo,
905    /// `<Reference>` under `<Object>/<Manifest>`.
906    Manifest,
907}
908
909/// Verification status.
910#[derive(Debug, Clone, Copy, PartialEq, Eq)]
911#[non_exhaustive]
912pub enum DsigStatus {
913    /// Signature/reference is cryptographically valid.
914    Valid,
915    /// Signature/reference is invalid with a concrete reason.
916    Invalid(FailureReason),
917}
918
919/// Why XMLDSig verification failed.
920#[derive(Debug, Clone, Copy, PartialEq, Eq)]
921#[non_exhaustive]
922pub enum FailureReason {
923    /// `<DigestValue>` mismatch for a `<Reference>` at `ref_index`.
924    ReferenceDigestMismatch {
925        /// Zero-based index of the failing `<Reference>` in its processed set.
926        ///
927        /// On per-reference verification entries, use
928        /// `ReferenceResult::reference_set` to distinguish the `<SignedInfo>`
929        /// and `<Manifest>` reference sets.
930        ///
931        /// When this reason appears in `VerifyResult::status` without an
932        /// accompanying `ReferenceResult`, `ref_index` always refers to the
933        /// `<SignedInfo>` reference set.
934        ref_index: usize,
935    },
936    /// `<Reference>` rejected by URI/transform allowlist policy.
937    ReferencePolicyViolation {
938        /// Zero-based index of the failing `<Reference>` in its processed set.
939        ref_index: usize,
940    },
941    /// `<Reference>` processing failed (dereference, transform, missing URI).
942    ReferenceProcessingFailure {
943        /// Zero-based index of the failing `<Reference>` in its processed set.
944        ref_index: usize,
945    },
946    /// `<SignatureValue>` does not match canonicalized `<SignedInfo>`.
947    SignatureMismatch,
948    /// No verification key was configured or could be resolved.
949    KeyNotFound,
950}
951
952/// Result of processing all `<Reference>` elements in `<SignedInfo>`.
953#[derive(Debug)]
954#[non_exhaustive]
955#[must_use = "check first_failure/results before accepting the reference set"]
956pub struct ReferencesResult {
957    /// Per-reference results (one per `<Reference>` in order).
958    /// On fail-fast, only references up to and including the failed one are present.
959    pub results: Vec<ReferenceResult>,
960    /// Index of the first failed reference, if any.
961    pub first_failure: Option<usize>,
962}
963
964impl ReferencesResult {
965    /// Whether all references passed digest verification.
966    #[must_use]
967    pub fn all_valid(&self) -> bool {
968        self.results
969            .iter()
970            .all(|result| matches!(result.status, DsigStatus::Valid))
971    }
972}
973
974/// Process a single `<Reference>`: dereference URI → apply transforms → compute
975/// digest → compare with stored `<DigestValue>`.
976///
977/// # Arguments
978///
979/// - `reference`: The parsed `<Reference>` element.
980/// - `resolver`: URI resolver for the document.
981/// - `signature_node`: The `<Signature>` element (for enveloped-signature transform).
982/// - `reference_set`: Whether this reference belongs to `<SignedInfo>` or `<Manifest>`.
983/// - `reference_index`: Zero-based index of this reference inside `reference_set`.
984/// - `store_pre_digest`: If true, store the pre-digest bytes in the result,
985///   subject to the signature-wide diagnostic retention ceiling.
986///
987/// # Errors
988///
989/// Returns `Err` for processing failures (URI dereference, transform errors).
990/// Digest mismatch is NOT an error — it produces
991/// `Ok(ReferenceResult { status: Invalid(ReferenceDigestMismatch { .. }) })`.
992pub fn process_reference(
993    reference: &Reference,
994    resolver: &UriReferenceResolver<'_>,
995    signature_node: Node<'_, '_>,
996    reference_set: ReferenceSet,
997    reference_index: usize,
998    store_pre_digest: bool,
999) -> Result<ReferenceResult, ReferenceProcessingError> {
1000    let execution_budget = TransformExecutionBudget::default();
1001    let canonicalized_data_budget = CanonicalizedDataBudget::default();
1002    let execution = ReferenceExecutionContext {
1003        store_pre_digest,
1004        transform_options: TransformOptions::default().id_attributes(resolver.id_registrations()),
1005        transform_budget: &execution_budget,
1006        canonicalized_data_budget: &canonicalized_data_budget,
1007        provider: crate::provider::default_provider(),
1008        capture: &(),
1009    };
1010    process_reference_with_options(
1011        reference,
1012        resolver,
1013        signature_node,
1014        reference_set,
1015        reference_index,
1016        reference_origin_node(signature_node, reference_set, reference_index),
1017        &execution,
1018    )
1019}
1020
1021fn reference_origin_node<'a, 'input>(
1022    signature_node: Node<'a, 'input>,
1023    reference_set: ReferenceSet,
1024    reference_index: usize,
1025) -> Option<Node<'a, 'input>> {
1026    let is_reference = |node: &Node<'_, '_>| {
1027        node.is_element()
1028            && node.tag_name().namespace() == Some(XMLDSIG_NS)
1029            && node.tag_name().name() == "Reference"
1030    };
1031    match reference_set {
1032        ReferenceSet::SignedInfo => signature_node
1033            .children()
1034            .find(|node| {
1035                node.is_element()
1036                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
1037                    && node.tag_name().name() == "SignedInfo"
1038            })?
1039            .children()
1040            .filter(is_reference)
1041            .nth(reference_index),
1042        ReferenceSet::Manifest => signature_node
1043            .children()
1044            .filter(|node| {
1045                node.is_element()
1046                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
1047                    && node.tag_name().name() == "Object"
1048            })
1049            .flat_map(|object| {
1050                object.children().filter(|node| {
1051                    node.is_element()
1052                        && node.tag_name().namespace() == Some(XMLDSIG_NS)
1053                        && node.tag_name().name() == "Manifest"
1054                })
1055            })
1056            .flat_map(|manifest| manifest.children().filter(is_reference))
1057            .nth(reference_index),
1058    }
1059}
1060
1061fn bind_reference_evidence(
1062    result: &mut ReferenceResult,
1063    reference: &Reference,
1064    view: DocumentView<'_>,
1065    origin: Option<Node<'_, '_>>,
1066    observed: &OperationResourceIdentity,
1067) {
1068    result.reference_identity = origin.map(|node| view.node_identity(node));
1069    if let OperationResourceIdentity::External { fingerprint, .. } = observed {
1070        result.external_resource_fingerprint = Some(*fingerprint);
1071    }
1072    if let OperationResourceIdentity::DocumentNode(target) = observed {
1073        result.target_identity = Some(*target);
1074        // A later node-set transform can reparse C14N output, so arbitrary
1075        // chains cannot retain an original subtree claim. This classification
1076        // describes proven element content, never discarded comments or the
1077        // namespace-declaration spelling normalized by canonicalization.
1078        let mut enveloped = false;
1079        let mut canonicalized = false;
1080        for transform in &reference.transforms {
1081            match transform {
1082                Transform::C14n(_) if !canonicalized => canonicalized = true,
1083                Transform::Enveloped if !canonicalized => enveloped = true,
1084                _ => return,
1085            }
1086        }
1087        result.coverage = if enveloped {
1088            ReferenceCoverage::EnvelopedElementContent
1089        } else {
1090            ReferenceCoverage::ElementContent
1091        };
1092    }
1093}
1094
1095struct ReferenceExecutionContext<'a, C = ()> {
1096    store_pre_digest: bool,
1097    transform_options: TransformOptions<'a>,
1098    transform_budget: &'a TransformExecutionBudget,
1099    canonicalized_data_budget: &'a CanonicalizedDataBudget,
1100    provider: &'a dyn crate::provider::CryptoProvider,
1101    capture: &'a C,
1102}
1103
1104struct CanonicalizedDataBudget {
1105    remaining: Cell<usize>,
1106    max_bytes: usize,
1107}
1108
1109struct VerificationOperationBudgets<'a> {
1110    key_info_parse: RefCell<super::parse::KeyInfoParseUsage>,
1111    key_candidates: RefCell<super::InspectedKeyCandidateBudget<'a>>,
1112    key_establishment: RefCell<crate::key_establishment::KeyEstablishmentUsage>,
1113    transforms: TransformExecutionBudget,
1114    canonicalized: CanonicalizedDataBudget,
1115    xpath_parse: RefCell<XPathSignatureParseBudget>,
1116    key_info_materialization: RefCell<KeyInfoMaterializationState>,
1117    external_resource_identities: RefCell<HashMap<String, OperationResourceIdentity>>,
1118    resolved_reference_resources: RefCell<HashMap<NodeId, OperationResourceIdentity>>,
1119    external_resource_set_identity: RefCell<Option<OperationResourceIdentity>>,
1120}
1121
1122impl<'a> VerificationOperationBudgets<'a> {
1123    fn with_transforms(
1124        policy: &crate::policy::VerificationPolicy,
1125        transforms: TransformExecutionBudget,
1126        utc_facts: Option<structured_pkix::temporal::LeapTable<'a>>,
1127    ) -> Self {
1128        let key_info_parse = super::parse::KeyInfoParseUsage::new(&policy.resources);
1129        let key_candidates = super::InspectedKeyCandidateBudget::with_ingestion(
1130            policy,
1131            &key_info_parse.certificates,
1132            utc_facts,
1133        );
1134        let xpath_parse = XPathSignatureParseBudget::for_execution(&policy.resources, &transforms);
1135        Self {
1136            key_info_parse: RefCell::new(key_info_parse),
1137            key_candidates: RefCell::new(key_candidates),
1138            key_establishment: Default::default(),
1139            transforms,
1140            canonicalized: CanonicalizedDataBudget::with_limit(
1141                policy.resources.effective_canonicalized_bytes(),
1142            ),
1143            xpath_parse: RefCell::new(xpath_parse),
1144            key_info_materialization: RefCell::new(KeyInfoMaterializationState::default()),
1145            external_resource_identities: RefCell::new(HashMap::new()),
1146            resolved_reference_resources: RefCell::new(HashMap::new()),
1147            external_resource_set_identity: RefCell::new(None),
1148        }
1149    }
1150
1151    fn external_resource_set_identity(
1152        &self,
1153        resolver: &UriReferenceResolver<'_>,
1154    ) -> OperationResourceIdentity {
1155        // One resolver borrows the same immutable map for the entire request.
1156        // Hash/sort it once, not once per graph node or Signature.
1157        let mut cached = self.external_resource_set_identity.borrow_mut();
1158        cached
1159            .get_or_insert_with(|| resolver.external_resource_set_identity())
1160            .clone()
1161    }
1162
1163    fn resource_identity_for_reference(
1164        &self,
1165        reference: &Reference,
1166        index: usize,
1167        resolver: &UriReferenceResolver<'_>,
1168        view: DocumentView<'_>,
1169        origin: Option<Node<'_, '_>>,
1170    ) -> OperationResourceIdentity {
1171        let Some(uri) = reference.uri.as_deref() else {
1172            return OperationResourceIdentity::Generated("omitted-reference", index);
1173        };
1174        if uri.is_empty() || uri.starts_with('#') {
1175            return resolver
1176                .node_id_for_reference_resource(
1177                    uri,
1178                    Some(self.transforms.node_set_materialization()),
1179                )
1180                .ok()
1181                .flatten()
1182                .map(|node| OperationResourceIdentity::DocumentNode(view.node_identity_by_id(node)))
1183                .unwrap_or(OperationResourceIdentity::Generated(
1184                    "missing-document-reference",
1185                    index,
1186                ));
1187        }
1188
1189        let resolved_uri;
1190        let uri = if let Some(origin) = origin {
1191            if let Some(identity) = self.resolved_reference_resources.borrow().get(&origin.id()) {
1192                return identity.clone();
1193            }
1194            // XML Base Second Edition §4.2 resolves the external reference in
1195            // its owning element's context. Identity and execution must agree.
1196            // https://www.w3.org/TR/2009/REC-xmlbase-20090128/#resolving
1197            let Ok(resolved) = crate::c14n::xml_base::resolve_uri_from_node_with_budget(
1198                origin,
1199                uri,
1200                self.transforms.xml_base_resolution(),
1201            ) else {
1202                return OperationResourceIdentity::Generated(
1203                    "unresolved-external-reference",
1204                    index,
1205                );
1206            };
1207            resolved_uri = resolved;
1208            resolved_uri.as_str()
1209        } else {
1210            uri
1211        };
1212        if let Some(identity) = self.external_resource_identities.borrow().get(uri).cloned() {
1213            if let Some(origin) = origin {
1214                self.resolved_reference_resources
1215                    .borrow_mut()
1216                    .insert(origin.id(), identity.clone());
1217            }
1218            return identity;
1219        }
1220        let identity = resolver
1221            .external_resource_identity(uri)
1222            .unwrap_or(OperationResourceIdentity::MissingExternal);
1223        if matches!(identity, OperationResourceIdentity::External { .. }) {
1224            self.external_resource_identities
1225                .borrow_mut()
1226                .insert(uri.to_owned(), identity.clone());
1227        }
1228        if let Some(origin) = origin {
1229            self.resolved_reference_resources
1230                .borrow_mut()
1231                .insert(origin.id(), identity.clone());
1232        }
1233        identity
1234    }
1235}
1236
1237struct VerificationPlanNodes {
1238    document: OperationNodeId,
1239    key_materialization: OperationNodeId,
1240    key: OperationNodeId,
1241    digests: Vec<OperationNodeId>,
1242    canonicalization: OperationNodeId,
1243    crypto: OperationNodeId,
1244}
1245
1246fn compile_verification_operation_plan(
1247    operation: &mut OperationExecutionContext<
1248        &crate::policy::VerificationPolicy,
1249        &VerificationOperationBudgets,
1250    >,
1251    view: DocumentView<'_>,
1252    signature_node: Node<'_, '_>,
1253    references: &[Reference],
1254    resolver: &UriReferenceResolver<'_>,
1255) -> Result<VerificationPlanNodes, SignatureVerificationPipelineError> {
1256    operation
1257        .validate_document_view(view)
1258        .map_err(map_verification_plan_error)?;
1259    let document_node = operation.add_node(
1260        OperationNodeKind::Document,
1261        OperationStage::Parse,
1262        Some(OperationResourceIdentity::DocumentNode(
1263            view.node_identity(signature_node),
1264        )),
1265    );
1266    let key_materialization_resource = operation.budgets().external_resource_set_identity(resolver);
1267    let key_materialization = operation.add_node(
1268        OperationNodeKind::Key { index: 0 },
1269        OperationStage::Resolve,
1270        Some(key_materialization_resource.clone()),
1271    );
1272    let key_node = operation.add_node(
1273        OperationNodeKind::Key { index: 1 },
1274        OperationStage::Resolve,
1275        None,
1276    );
1277    operation
1278        .add_dependency(key_materialization, document_node)
1279        .map_err(map_verification_plan_error)?;
1280    operation
1281        .add_dependency(key_node, key_materialization)
1282        .map_err(map_verification_plan_error)?;
1283    let mut digests = Vec::with_capacity(references.len());
1284    for (index, reference) in references.iter().enumerate() {
1285        let resource = operation.budgets().resource_identity_for_reference(
1286            reference,
1287            index,
1288            resolver,
1289            view,
1290            reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
1291        );
1292        let digest_node = operation.add_node(
1293            OperationNodeKind::Digest { index },
1294            OperationStage::Digest,
1295            Some(resource.clone()),
1296        );
1297        operation
1298            .add_dependency(digest_node, document_node)
1299            .map_err(map_verification_plan_error)?;
1300        digests.push(digest_node);
1301    }
1302    let canonicalization = operation.add_node(
1303        OperationNodeKind::Canonicalization,
1304        OperationStage::Canonicalization,
1305        None,
1306    );
1307    for digest in &digests {
1308        operation
1309            .add_dependency(canonicalization, *digest)
1310            .map_err(map_verification_plan_error)?;
1311    }
1312    let crypto = operation.add_node(OperationNodeKind::Crypto, OperationStage::Crypto, None);
1313    operation
1314        .add_dependency(crypto, key_node)
1315        .map_err(map_verification_plan_error)?;
1316    operation
1317        .add_dependency(crypto, canonicalization)
1318        .map_err(map_verification_plan_error)?;
1319    operation.compile().map_err(map_verification_plan_error)?;
1320    Ok(VerificationPlanNodes {
1321        document: document_node,
1322        key_materialization,
1323        key: key_node,
1324        digests,
1325        canonicalization,
1326        crypto,
1327    })
1328}
1329
1330fn map_verification_plan_error(error: OperationPlanError) -> SignatureVerificationPipelineError {
1331    SignatureVerificationPipelineError::OperationPlan(error.to_string())
1332}
1333
1334impl Default for CanonicalizedDataBudget {
1335    fn default() -> Self {
1336        Self {
1337            remaining: Cell::new(CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING),
1338            max_bytes: CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
1339        }
1340    }
1341}
1342
1343impl CanonicalizedDataBudget {
1344    fn remaining(&self) -> usize {
1345        self.remaining.get()
1346    }
1347
1348    fn charge(&self, bytes: usize) -> Result<(), ReferenceProcessingError> {
1349        let available = self.remaining.get();
1350        let Some(remaining) = available.checked_sub(bytes) else {
1351            self.remaining.set(0);
1352            return Err(crate::policy::PolicyViolation::ResourceLimit {
1353                resource: crate::policy::resource_name::CANONICALIZED_BYTES,
1354                maximum: self.max_bytes,
1355                actual: self
1356                    .max_bytes
1357                    .saturating_add(bytes.saturating_sub(available)),
1358            }
1359            .into());
1360        };
1361        self.remaining.set(remaining);
1362        Ok(())
1363    }
1364
1365    fn with_limit(max_bytes: usize) -> Self {
1366        Self {
1367            remaining: Cell::new(max_bytes),
1368            max_bytes,
1369        }
1370    }
1371}
1372
1373fn process_reference_with_options<C: VerificationCapture>(
1374    reference: &Reference,
1375    resolver: &UriReferenceResolver<'_>,
1376    signature_node: Node<'_, '_>,
1377    reference_set: ReferenceSet,
1378    reference_index: usize,
1379    reference_node: Option<Node<'_, '_>>,
1380    execution: &ReferenceExecutionContext<'_, C>,
1381) -> Result<ReferenceResult, ReferenceProcessingError> {
1382    execution.capture.admit_digest(
1383        reference_node.map(|node| node.id()),
1384        reference.digest_method,
1385    )?;
1386    // 1. Dereference URI. Omitted URI is distinct from URI="" in XMLDSig and
1387    // must be rejected until caller-provided external object resolution exists.
1388    let uri = reference
1389        .uri
1390        .as_deref()
1391        .ok_or(ReferenceProcessingError::MissingUri)?;
1392    let initial_data = reference_node
1393        .map_or_else(
1394            || {
1395                resolver.dereference_with_budget(
1396                    uri,
1397                    execution.transform_budget.node_set_materialization(),
1398                )
1399            },
1400            |node| {
1401                resolver.dereference_from_with_budget(
1402                    uri,
1403                    node,
1404                    execution.transform_budget.node_set_materialization(),
1405                    execution.transform_budget.xml_base_resolution(),
1406                )
1407            },
1408        )
1409        .map_err(ReferenceProcessingError::UriDereference)?;
1410
1411    // 2. Apply transform chain
1412    let source = reference_node.unwrap_or(signature_node);
1413    let containing_signature =
1414        super::transforms::enveloped_signature_for_reference(source, &reference.transforms, || {
1415            execution.transform_budget.charge_node_filter_work(1)
1416        })
1417        .map_err(ReferenceProcessingError::Transform)?;
1418    // 3. Compute the digest while the final transform output is produced;
1419    // its bytes are retained only when the caller asked for them.
1420    let super::transforms::ReferenceDigest {
1421        digest: computed_digest,
1422        pre_digest,
1423    } = super::transforms::digest_reference_transforms_with_budget(
1424        source,
1425        containing_signature,
1426        initial_data,
1427        &reference.transforms,
1428        execution.transform_options,
1429        execution.transform_budget,
1430        execution.transform_budget.xml_parse_work(),
1431        super::transforms::DigestRequest {
1432            provider: execution.provider,
1433            algorithm: reference.digest_method,
1434            retain_pre_digest: execution.store_pre_digest,
1435        },
1436    )
1437    .map_err(|error| match error {
1438        super::transforms::ReferenceDigestError::Transform(error) => {
1439            ReferenceProcessingError::Transform(error)
1440        }
1441        super::transforms::ReferenceDigestError::Provider(error) => error.into(),
1442    })?;
1443    execution
1444        .capture
1445        .computed_digest(reference_node.map(|node| node.id()))?;
1446
1447    // 4. Compare with stored DigestValue (constant-time)
1448    let status = if constant_time_eq(&computed_digest, &reference.digest_value) {
1449        DsigStatus::Valid
1450    } else {
1451        DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch {
1452            ref_index: reference_index,
1453        })
1454    };
1455
1456    let pre_digest_data = match pre_digest {
1457        Some(bytes) => {
1458            execution.canonicalized_data_budget.charge(bytes.len())?;
1459            Some(bytes)
1460        }
1461        None => None,
1462    };
1463
1464    Ok(ReferenceResult {
1465        reference_identity: None,
1466        target_identity: None,
1467        external_resource_fingerprint: None,
1468        coverage: ReferenceCoverage::TransformedData,
1469        reference_set,
1470        reference_index,
1471        uri: uri.to_owned(),
1472        digest_algorithm: reference.digest_method,
1473        status,
1474        pre_digest_data,
1475    })
1476}
1477
1478/// Process all `<Reference>` elements in a `<SignedInfo>`, with fail-fast
1479/// on the first digest mismatch.
1480///
1481/// Per XMLDSig spec: if any reference fails, the entire signature is invalid.
1482/// Processing stops at the first failure for efficiency.
1483///
1484/// # Errors
1485///
1486/// Returns `Err` only for processing failures (malformed XML, unsupported
1487/// transform, etc.). Digest mismatches are reported via
1488/// `ReferencesResult::first_failure`.
1489pub fn process_all_references(
1490    references: &[Reference],
1491    resolver: &UriReferenceResolver<'_>,
1492    signature_node: Node<'_, '_>,
1493    store_pre_digest: bool,
1494) -> Result<ReferencesResult, ReferenceProcessingError> {
1495    let execution_budget = TransformExecutionBudget::default();
1496    let canonicalized_data_budget = CanonicalizedDataBudget::default();
1497    let execution = ReferenceExecutionContext {
1498        store_pre_digest,
1499        transform_options: TransformOptions::default().id_attributes(resolver.id_registrations()),
1500        transform_budget: &execution_budget,
1501        canonicalized_data_budget: &canonicalized_data_budget,
1502        provider: crate::provider::default_provider(),
1503        capture: &(),
1504    };
1505    process_all_references_with_options(references, resolver, signature_node, &execution)
1506}
1507
1508fn process_all_references_with_options(
1509    references: &[Reference],
1510    resolver: &UriReferenceResolver<'_>,
1511    signature_node: Node<'_, '_>,
1512    execution: &ReferenceExecutionContext<'_>,
1513) -> Result<ReferencesResult, ReferenceProcessingError> {
1514    let mut results = Vec::with_capacity(references.len());
1515
1516    for (i, reference) in references.iter().enumerate() {
1517        let result = process_reference_with_options(
1518            reference,
1519            resolver,
1520            signature_node,
1521            ReferenceSet::SignedInfo,
1522            i,
1523            reference_origin_node(signature_node, ReferenceSet::SignedInfo, i),
1524            execution,
1525        )?;
1526        let failed = matches!(result.status, DsigStatus::Invalid(_));
1527        results.push(result);
1528
1529        if failed {
1530            return Ok(ReferencesResult {
1531                results,
1532                first_failure: Some(i),
1533            });
1534        }
1535    }
1536
1537    Ok(ReferencesResult {
1538        results,
1539        first_failure: None,
1540    })
1541}
1542
1543/// Errors during reference processing.
1544///
1545/// Distinct from digest mismatch (which is a validation result, not a processing error).
1546#[derive(Debug, thiserror::Error)]
1547#[non_exhaustive]
1548pub enum ReferenceProcessingError {
1549    /// The current shared execution refused original digest-receipt retention.
1550    #[error("shared reference execution: {0}")]
1551    SharedExecution(#[from] structured_asn1::memory::MemoryError),
1552    /// The immutable verification policy rejected reference processing.
1553    #[error("verification policy violation: {0}")]
1554    Policy(#[from] crate::policy::PolicyViolation),
1555
1556    /// The selected provider could not compute the declared digest.
1557    #[error("cryptographic provider error: {0}")]
1558    Provider(#[from] crate::provider::ProviderError),
1559
1560    /// `<Reference>` omitted the `URI` attribute, which we do not resolve implicitly.
1561    #[error("reference URI is required; omitted URI references are not supported")]
1562    MissingUri,
1563
1564    /// URI dereference failed.
1565    #[error("URI dereference failed: {0}")]
1566    UriDereference(#[source] super::types::TransformError),
1567
1568    /// Transform execution failed.
1569    #[error("transform failed: {0}")]
1570    Transform(#[source] super::types::TransformError),
1571}
1572
1573impl ReferenceProcessingError {
1574    fn into_policy_violation(self) -> Result<crate::policy::PolicyViolation, Self> {
1575        match self {
1576            Self::Policy(error)
1577            | Self::UriDereference(TransformError::Policy(error))
1578            | Self::Transform(TransformError::Policy(error)) => Ok(error),
1579            error => Err(error),
1580        }
1581    }
1582}
1583
1584/// End-to-end XMLDSig verification result for one `<Signature>`.
1585#[derive(Debug)]
1586#[non_exhaustive]
1587#[must_use = "inspect status before accepting the document"]
1588pub struct VerifyResult {
1589    /// Key authorization, independent of `status`; neither alone authorizes data.
1590    pub key_trust: super::KeyTrustEvidence,
1591    /// Core XMLDSig status for the `<SignedInfo>` references and signature value.
1592    ///
1593    /// Manifest reference failures do not alter this field; inspect
1594    /// [`Self::manifest_references`] before accepting Manifest-backed data.
1595    pub status: DsigStatus,
1596    /// `<Reference>` verification results from `<SignedInfo>`.
1597    /// On fail-fast, this includes references up to and including
1598    /// the first digest mismatch only.
1599    pub signed_info_references: Vec<ReferenceResult>,
1600    /// `<Manifest>` reference results.
1601    /// Populated only when `VerifyContext::process_manifests(true)` is enabled
1602    /// and core signature validation succeeds.
1603    /// Includes references whose original Manifest or ancestor is authenticated
1604    /// by this Signature's structure-preserving references. Manifests may be
1605    /// nested or outside Signature; DOM ancestry alone never authenticates them.
1606    /// Each entry has an independent status that does not alter [`Self::status`].
1607    /// Callers must inspect every entry before accepting Manifest-backed data.
1608    /// Unsigned/unreferenced Manifest blocks are skipped, so an
1609    /// empty list does not imply that no Manifest elements existed in `verify()` input.
1610    pub manifest_references: Vec<ReferenceResult>,
1611    /// Canonicalized `<SignedInfo>` bytes when `store_pre_digest` is enabled
1612    /// and verification reaches SignedInfo canonicalization.
1613    pub canonicalized_signed_info: Option<Vec<u8>>,
1614}
1615
1616/// Errors while running end-to-end XMLDSig verification.
1617#[derive(Debug, thiserror::Error)]
1618#[non_exhaustive]
1619pub enum DsigError {
1620    /// The current shared execution refused work or storage admission.
1621    #[error("shared verification execution: {0}")]
1622    SharedExecution(#[from] structured_asn1::memory::MemoryError),
1623    /// Key establishment failed before the consuming MAC.
1624    #[error(transparent)]
1625    KeyEstablishment(#[from] crate::key_establishment::KeyEstablishmentError),
1626    /// Caller-supplied request identities or requirements are inconsistent.
1627    #[error("invalid verification request: {reason}")]
1628    InvalidRequest {
1629        /// Concrete invariant violated before signature execution.
1630        reason: &'static str,
1631    },
1632    /// The compiled verification policy rejected an operation input.
1633    #[error("verification policy violation: {0}")]
1634    Policy(#[from] crate::policy::PolicyViolation),
1635
1636    /// A query-funded algorithm rejection; the URI retains its charge until drop.
1637    #[error("verification policy violation: {operation} policy rejects algorithm {algorithm}")]
1638    AccountedPolicyAlgorithm {
1639        /// Operation evaluating the algorithm.
1640        operation: &'static str,
1641        /// Rejected algorithm URI, owned by the current query's account.
1642        algorithm: structured_asn1::memory::OwnedText,
1643    },
1644
1645    /// The selected provider cannot execute the requested operation.
1646    #[error("cryptographic provider error: {0}")]
1647    Provider(#[from] crate::provider::ProviderError),
1648
1649    /// XML parsing failed.
1650    #[error("XML parse error: {0}")]
1651    XmlParse(#[from] crate::xml::dom::ParseError),
1652
1653    /// The owned XML document boundary rejected the document or identity.
1654    #[error("XML document error: {0}")]
1655    Document(#[from] crate::document::XmlDocumentError),
1656
1657    /// Required signature element is missing.
1658    #[error("missing required element: <{element}>")]
1659    MissingElement {
1660        /// Name of the missing element.
1661        element: &'static str,
1662    },
1663
1664    /// Signature element tree shape violates XMLDSig structure requirements.
1665    #[error("invalid Signature structure: {reason}")]
1666    InvalidStructure {
1667        /// Validation failure reason.
1668        reason: &'static str,
1669    },
1670
1671    /// The authenticated dependency graph could not be compiled or executed.
1672    #[error("invalid operation plan: {0}")]
1673    OperationPlan(String),
1674
1675    /// The requested operation start node is absent or has a duplicate ID.
1676    #[error("selected node ID is missing or ambiguous: {id}")]
1677    SelectedNodeUnavailable {
1678        /// Caller-provided XML ID value.
1679        id: String,
1680    },
1681
1682    /// `<SignedInfo>` parsing failed.
1683    #[error("failed to parse SignedInfo: {0}")]
1684    ParseSignedInfo(super::parse::ParseError),
1685
1686    /// `<KeyInfo>` parsing failed.
1687    #[error("failed to parse KeyInfo: {0}")]
1688    ParseKeyInfo(#[source] super::parse::ParseError),
1689
1690    /// Configuration-driven key resolution failed.
1691    #[error("key resolution failed: {0}")]
1692    KeyResolution(#[from] super::keys::KeyResolutionError),
1693
1694    /// `<Object>/<Manifest>/<Reference>` parsing failed.
1695    #[error("failed to parse Manifest reference: {0}")]
1696    ParseManifestReference(#[source] ParseError),
1697
1698    /// Reference processing failed.
1699    #[error("reference processing failed: {0}")]
1700    Reference(ReferenceProcessingError),
1701
1702    /// SignedInfo canonicalization failed.
1703    #[error("SignedInfo canonicalization failed: {0}")]
1704    Canonicalization(#[from] crate::c14n::C14nError),
1705
1706    /// SignatureValue base64 decoding failed.
1707    #[error("invalid SignatureValue base64: {0}")]
1708    SignatureValueBase64(#[from] base64::DecodeError),
1709
1710    /// Cryptographic verification failed before validity decision.
1711    #[error("signature verification failed: {0}")]
1712    Crypto(#[from] SignatureVerificationError),
1713}
1714
1715impl From<super::parse::ParseError> for DsigError {
1716    fn from(error: super::parse::ParseError) -> Self {
1717        match error {
1718            super::parse::ParseError::Policy(error) => Self::Policy(error),
1719            super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1720                Self::Policy(error)
1721            }
1722            error => Self::ParseSignedInfo(error),
1723        }
1724    }
1725}
1726
1727fn map_key_info_parse_error(error: super::parse::ParseError) -> DsigError {
1728    match error {
1729        super::parse::ParseError::Policy(error)
1730        | super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1731            DsigError::Policy(error)
1732        }
1733        error => DsigError::ParseKeyInfo(error),
1734    }
1735}
1736
1737fn map_manifest_parse_error(error: super::parse::ParseError) -> DsigError {
1738    match error {
1739        super::parse::ParseError::Policy(error)
1740        | super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1741            DsigError::Policy(error)
1742        }
1743        error => DsigError::ParseManifestReference(error),
1744    }
1745}
1746
1747impl From<ReferenceProcessingError> for DsigError {
1748    fn from(error: ReferenceProcessingError) -> Self {
1749        match error.into_policy_violation() {
1750            Ok(error) => Self::Policy(error),
1751            Err(error) => Self::Reference(error),
1752        }
1753    }
1754}
1755
1756type SignatureVerificationPipelineError = DsigError;
1757
1758impl From<OperationPlanError> for DsigError {
1759    fn from(error: OperationPlanError) -> Self {
1760        Self::OperationPlan(error.to_string())
1761    }
1762}
1763
1764/// Verify one XMLDSig `<Signature>` end-to-end with a PEM public key.
1765///
1766/// Pipeline:
1767/// 1. Parse `<Signature>` children and enforce structural constraints
1768/// 2. Parse `<SignedInfo>`
1769/// 3. Validate all `<Reference>` digests (fail-fast)
1770/// 4. Canonicalize `<SignedInfo>`
1771/// 5. Base64-decode `<SignatureValue>`
1772/// 6. Verify signature bytes against canonicalized `<SignedInfo>` using the provided PEM key
1773///
1774/// If any `<Reference>` digest mismatches, returns `Ok` with
1775/// `status == Invalid(ReferenceDigestMismatch { .. })`.
1776///
1777/// This API uses only the provided PEM key and does not parse embedded
1778/// `<KeyInfo>` key material for key selection/validation. Consequently,
1779/// malformed optional `<KeyInfo>` does not produce `DsigError::ParseKeyInfo`
1780/// on this API path.
1781///
1782/// Structural constraints enforced by this API:
1783/// - The document must contain exactly one XMLDSig `<Signature>` element.
1784/// - `<SignedInfo>` must be the first element child of `<Signature>` and appear once.
1785/// - `<SignatureValue>` must be the second element child of `<Signature>` and appear once.
1786/// - `<KeyInfo>` is optional and, when present, must be the third element child.
1787/// - Only XMLDSig namespace element children are allowed under `<Signature>`.
1788/// - Non-whitespace mixed text content under `<Signature>` is rejected.
1789/// - After `<SignedInfo>`, `<SignatureValue>`, and optional `<KeyInfo>`, only `<Object>` elements are allowed.
1790/// - `<SignatureValue>` must not contain nested element children.
1791pub fn verify_signature_with_pem_key(
1792    xml: &str,
1793    public_key_pem: &str,
1794    store_pre_digest: bool,
1795) -> Result<VerifyResult, DsigError> {
1796    struct PemVerifyingKey<'a> {
1797        public_key_pem: &'a str,
1798    }
1799
1800    impl VerifyingKey for PemVerifyingKey<'_> {
1801        fn verify_with_context(
1802            &self,
1803            algorithm: SignatureAlgorithm,
1804            context: &super::SignatureContext,
1805            signed_data: &[u8],
1806            signature_value: &[u8],
1807        ) -> Result<bool, DsigError> {
1808            if context.as_bytes().is_empty() {
1809                return self.verify(algorithm, signed_data, signature_value);
1810            }
1811            if algorithm.context_element().is_none() {
1812                return Err(SignatureVerificationError::UnsupportedAlgorithm {
1813                    uri: algorithm.uri().to_owned(),
1814                }
1815                .into());
1816            }
1817            let (rest, pem) = x509_parser::pem::parse_x509_pem(self.public_key_pem.as_bytes())
1818                .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
1819            if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
1820                return Err(SignatureVerificationError::InvalidKeyPem.into());
1821            }
1822            Ok(super::modern::verify_with_context(
1823                algorithm,
1824                &pem.contents,
1825                context,
1826                signed_data,
1827                signature_value,
1828            )?)
1829        }
1830
1831        fn verify(
1832            &self,
1833            algorithm: SignatureAlgorithm,
1834            signed_data: &[u8],
1835            signature_value: &[u8],
1836        ) -> Result<bool, DsigError> {
1837            verify_with_algorithm(algorithm, self.public_key_pem, signed_data, signature_value)
1838        }
1839    }
1840
1841    let key = PemVerifyingKey { public_key_pem };
1842    VerifyContext::new()
1843        .key(&key)
1844        .store_pre_digest(store_pre_digest)
1845        .verify(xml)
1846}
1847
1848fn verify_signature_with_context(
1849    xml: &str,
1850    ctx: &VerifyContext<'_>,
1851) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1852    ctx.policy.validate()?;
1853    ctx.policy.signature_constraints.require_targets(false)?;
1854    ctx.policy.resources.validate_xml_document_len(xml.len())?;
1855    let execution_budget = TransformExecutionBudget::from_resources(&ctx.policy.resources)
1856        .with_xml_backend(ctx.xml_backend);
1857    let settings = DocumentParseSettings::from_policy(&ctx.policy.xml, &ctx.policy.resources)
1858        .with_backend(ctx.xml_backend);
1859    let document = XmlDocument::parse_with_settings_and_budget(
1860        xml.to_owned(),
1861        settings,
1862        execution_budget.xml_parse_work(),
1863    )
1864    .map_err(|error| map_document_parse_error(error, settings))?;
1865    verify_signature_document_with_context_and_transforms(&document, ctx, execution_budget)
1866}
1867
1868fn map_document_parse_error(error: XmlDocumentError, settings: DocumentParseSettings) -> DsigError {
1869    match error.into_policy_violation(settings) {
1870        Ok(error) => DsigError::Policy(error),
1871        Err(XmlDocumentError::Parse(error)) => DsigError::XmlParse(error),
1872        Err(error) => DsigError::Document(error),
1873    }
1874}
1875
1876#[cfg(test)]
1877mod xml_parse_budget_tests {
1878    use super::*;
1879
1880    #[test]
1881    fn verification_initial_parse_uses_the_policy_work_budget() {
1882        // Even a structurally invalid signature must not reach parsing when
1883        // the immutable operation snapshot denies all XML parse work.
1884        let xml = "<root/>";
1885        let mut policy = crate::policy::VerificationPolicy::default();
1886        policy.resources.max_xml_parse_work_bytes = 0;
1887
1888        let error = VerifyContext::new()
1889            .policy(policy)
1890            .verify(xml)
1891            .expect_err("a zero parse-work budget must reject the input parse");
1892
1893        assert!(matches!(
1894            error,
1895            DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1896                resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
1897                maximum: 0,
1898                actual,
1899            }) if actual == xml.len()
1900        ));
1901    }
1902}
1903
1904fn verify_signature_document_with_context(
1905    document: &XmlDocument,
1906    ctx: &VerifyContext<'_>,
1907) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1908    document.validate_operation_policy(&ctx.policy.xml, &ctx.policy.resources)?;
1909    verify_signature_document_with_context_and_transforms(
1910        document,
1911        ctx,
1912        TransformExecutionBudget::from_resources(&ctx.policy.resources)
1913            .with_xml_backend(ctx.xml_backend),
1914    )
1915}
1916
1917fn count_signature_nodes(view: DocumentView<'_>, maximum: usize) -> Result<usize, DsigError> {
1918    let mut count = 0;
1919    for node in view.document().descendants() {
1920        if !node.has_tag_name((XMLDSIG_NS, "Signature")) {
1921            continue;
1922        }
1923        if count == maximum {
1924            return Err(crate::policy::PolicyViolation::ResourceLimit {
1925                resource: crate::policy::resource_name::VERIFICATION_SIGNATURES,
1926                maximum,
1927                actual: count + 1,
1928            }
1929            .into());
1930        }
1931        count += 1;
1932    }
1933    Ok(count)
1934}
1935
1936trait SignatureBuffer: std::ops::Deref<Target = [SignatureEvidence]> {
1937    fn push_signature(
1938        &mut self,
1939        evidence: SignatureEvidence,
1940    ) -> Result<(), structured_asn1::memory::MemoryError>;
1941}
1942
1943impl SignatureBuffer for Vec<SignatureEvidence> {
1944    fn push_signature(
1945        &mut self,
1946        evidence: SignatureEvidence,
1947    ) -> Result<(), structured_asn1::memory::MemoryError> {
1948        self.push(evidence);
1949        Ok(())
1950    }
1951}
1952
1953impl<W: structured_asn1::memory::Work> SignatureBuffer
1954    for structured_asn1::memory::OwnedVec<SignatureEvidence, W>
1955{
1956    fn push_signature(
1957        &mut self,
1958        evidence: SignatureEvidence,
1959    ) -> Result<(), structured_asn1::memory::MemoryError> {
1960        self.try_push(evidence)
1961    }
1962}
1963
1964trait OperationRetention<P, B, C> {
1965    fn retain(
1966        &mut self,
1967        operation: OperationExecutionContext<P, B>,
1968        capture: C,
1969    ) -> Result<(), structured_asn1::memory::MemoryError>;
1970}
1971
1972impl<P, B, C> OperationRetention<P, B, C> for () {
1973    fn retain(
1974        &mut self,
1975        operation: OperationExecutionContext<P, B>,
1976        _capture: C,
1977    ) -> Result<(), structured_asn1::memory::MemoryError> {
1978        drop(operation);
1979        Ok(())
1980    }
1981}
1982
1983impl<P, B, C, W: structured_asn1::memory::Work> OperationRetention<P, B, C>
1984    for structured_asn1::memory::OwnedVec<CapturedOperation<P, B, C>, W>
1985{
1986    fn retain(
1987        &mut self,
1988        operation: OperationExecutionContext<P, B>,
1989        capture: C,
1990    ) -> Result<(), structured_asn1::memory::MemoryError> {
1991        self.try_push(CapturedOperation { operation, capture })
1992    }
1993}
1994
1995trait RequestStorage {
1996    type Signatures: SignatureBuffer;
1997    type Capture: VerificationCapture;
1998    // Actual reference types preserve their lifetimes without the implied
1999    // 'static bound of a lifetime-GAT projection under the scoped callback.
2000    type Operations<P, B>: OperationRetention<P, B, Self::Capture>;
2001
2002    fn capture(&self) -> Self::Capture;
2003
2004    fn admit_request(
2005        &self,
2006        document: &XmlDocument,
2007    ) -> Result<(), structured_asn1::memory::MemoryError>;
2008    fn signatures(
2009        &self,
2010        count: usize,
2011    ) -> Result<Self::Signatures, structured_asn1::memory::MemoryError>;
2012    fn operations<P, B>(
2013        &self,
2014        count: usize,
2015    ) -> Result<Self::Operations<P, B>, structured_asn1::memory::MemoryError>;
2016}
2017
2018struct CollectedRequestStorage;
2019
2020impl RequestStorage for CollectedRequestStorage {
2021    type Signatures = Vec<SignatureEvidence>;
2022    type Capture = ();
2023    type Operations<P, B> = ();
2024
2025    fn capture(&self) {}
2026
2027    fn admit_request(
2028        &self,
2029        _document: &XmlDocument,
2030    ) -> Result<(), structured_asn1::memory::MemoryError> {
2031        Ok(())
2032    }
2033    fn signatures(
2034        &self,
2035        count: usize,
2036    ) -> Result<Self::Signatures, structured_asn1::memory::MemoryError> {
2037        Ok(Vec::with_capacity(count))
2038    }
2039    fn operations<P, B>(
2040        &self,
2041        _count: usize,
2042    ) -> Result<Self::Operations<P, B>, structured_asn1::memory::MemoryError> {
2043        Ok(())
2044    }
2045}
2046
2047struct ScopedRequestStorage<'work, W: structured_asn1::memory::Work>(&'work W);
2048
2049impl<'work, W: structured_asn1::memory::Work> RequestStorage for ScopedRequestStorage<'work, W> {
2050    type Signatures = structured_asn1::memory::OwnedVec<SignatureEvidence, W>;
2051    type Capture = ScopedCapture<'work, W>;
2052    type Operations<P, B> =
2053        structured_asn1::memory::OwnedVec<CapturedOperation<P, B, ScopedCapture<'work, W>>, W>;
2054
2055    fn capture(&self) -> Self::Capture {
2056        ScopedCapture::new(self.0)
2057    }
2058
2059    fn admit_request(
2060        &self,
2061        document: &XmlDocument,
2062    ) -> Result<(), structured_asn1::memory::MemoryError> {
2063        let units = document
2064            .with_view(|view| {
2065                u64::try_from(view.node_count())
2066                    .ok()
2067                    .and_then(|count| count.checked_mul(2))
2068                    .and_then(|count| count.checked_add(1))
2069            })
2070            .ok_or(structured_asn1::memory::MemoryError::WorkExceeded)?;
2071        self.0.charge_work(units)
2072    }
2073    fn signatures(
2074        &self,
2075        count: usize,
2076    ) -> Result<Self::Signatures, structured_asn1::memory::MemoryError> {
2077        let mut values = self.0.vector();
2078        values.try_reserve_exact(count)?;
2079        Ok(values)
2080    }
2081    fn operations<P, B>(
2082        &self,
2083        count: usize,
2084    ) -> Result<Self::Operations<P, B>, structured_asn1::memory::MemoryError> {
2085        let mut values = self.0.vector();
2086        values.try_reserve_exact(count)?;
2087        Ok(values)
2088    }
2089}
2090
2091fn verify_all_document(
2092    document: &XmlDocument,
2093    ctx: &VerifyContext<'_>,
2094    request: &VerificationRequest<'_>,
2095) -> Result<VerifyEvidence, DsigError> {
2096    with_verified_document(
2097        document,
2098        ctx,
2099        request,
2100        CollectedRequestStorage,
2101        |_, _, _, evidence, (), _| Ok(evidence),
2102    )
2103}
2104
2105fn with_verified_document<'inputs, R, S: RequestStorage>(
2106    document: &XmlDocument,
2107    ctx: &VerifyContext<'inputs>,
2108    request: &VerificationRequest<'inputs>,
2109    storage: S,
2110    continuation: impl for<'view, 'scope> FnOnce(
2111        DocumentView<'view>,
2112        &'scope VerifyContext<'inputs>,
2113        &'scope VerificationOperationBudgets<'inputs>,
2114        VerifyEvidence<S::Signatures>,
2115        S::Operations<
2116            &'scope crate::policy::VerificationPolicy,
2117            &'scope VerificationOperationBudgets<'inputs>,
2118        >,
2119        UriReferenceResolver<'view>,
2120    ) -> Result<R, DsigError>,
2121) -> Result<R, DsigError> {
2122    storage.admit_request(document)?;
2123    ctx.policy.validate()?;
2124    document.validate_operation_policy(&ctx.policy.xml, &ctx.policy.resources)?;
2125    ctx.policy
2126        .signature_constraints
2127        .require_targets(!request.expected_targets.is_empty())?;
2128    if request.external_resources.is_some() && ctx.external_resources.is_some() {
2129        return Err(DsigError::InvalidRequest {
2130            reason: "external resources configured in both context and request",
2131        });
2132    }
2133    // RFC 5280 §6.1.1: validation time is one operation input, including
2134    // alternate certification paths and all Signatures in a batch.
2135    // https://www.rfc-editor.org/rfc/rfc5280#section-6.1.1
2136    let capture_time =
2137        ctx.policy.key_trust.verify_x509_chains && ctx.policy.key_trust.verification_time.is_none();
2138    let captured_context;
2139    let ctx = if capture_time || request.external_resources.is_some() {
2140        captured_context = {
2141            let mut captured = ctx.clone();
2142            if let Some(resources) = request.external_resources {
2143                captured.external_resources = Some(resources);
2144            }
2145            if capture_time {
2146                captured.policy.key_trust.verification_time = Some(std::time::SystemTime::now());
2147            }
2148            captured
2149        };
2150        &captured_context
2151    } else {
2152        ctx
2153    };
2154    let budgets = VerificationOperationBudgets::with_transforms(
2155        &ctx.policy,
2156        TransformExecutionBudget::from_resources(&ctx.policy.resources)
2157            .with_xml_backend(ctx.xml_backend),
2158        ctx.utc_facts,
2159    );
2160    document.with_view(|view| {
2161        match request.signatures {
2162            SignatureRequirement::AtLeast(0) | SignatureRequirement::Exactly(0) => {
2163                return Err(DsigError::InvalidRequest {
2164                    reason: "signature requirement must be nonzero",
2165                });
2166            }
2167            _ => {}
2168        }
2169        if request.expected_targets.len() > ctx.policy.resources.max_references {
2170            return Err(DsigError::InvalidRequest {
2171                reason: "too many expected coverage targets",
2172            });
2173        }
2174        if request.trusted_keys.len() > ctx.policy.resources.max_signatures {
2175            return Err(DsigError::InvalidRequest {
2176                reason: "too many trusted signature keys",
2177            });
2178        }
2179        for (index, target) in request.expected_targets.iter().copied().enumerate() {
2180            if !view.resolve_node(target)?.is_element() {
2181                return Err(DsigError::InvalidRequest {
2182                    reason: "coverage target must be an element",
2183                });
2184            }
2185            if request.expected_targets[..index].contains(&target) {
2186                return Err(DsigError::InvalidRequest {
2187                    reason: "duplicate expected coverage target",
2188                });
2189            }
2190        }
2191        for (index, entry) in request.trusted_keys.iter().enumerate() {
2192            if !view
2193                .resolve_node(entry.signature)?
2194                .has_tag_name((XMLDSIG_NS, "Signature"))
2195            {
2196                return Err(DsigError::InvalidRequest {
2197                    reason: "trusted key target must be a Signature",
2198                });
2199            }
2200            if request.trusted_keys[..index]
2201                .iter()
2202                .any(|prior| prior.signature == entry.signature)
2203            {
2204                return Err(DsigError::InvalidRequest {
2205                    reason: "ambiguous trusted keys for one Signature",
2206                });
2207            }
2208        }
2209        let is_signature = |node: &Node<'_, '_>| node.has_tag_name((XMLDSIG_NS, "Signature"));
2210        let count = count_signature_nodes(view, ctx.policy.resources.max_signatures)?;
2211        // Cardinality is checked before allocating the report or calling a
2212        // resolver. No Vec of backend handles or cloned XML is needed.
2213        let mut signatures = storage.signatures(count)?;
2214        let mut operations = storage.operations(count)?;
2215        let resolver = UriReferenceResolver::with_document_view(view, ctx.id_attributes)
2216            .with_same_document_id_semantics(ctx.policy.transforms.same_document_id_semantics)
2217            .with_external_resource_limits(
2218                ctx.policy.resources.max_external_resource_bytes,
2219                ctx.policy.resources.max_external_resource_total_bytes,
2220            );
2221        let resolver = match ctx.external_resources {
2222            Some(resources) => resolver.with_external_resources(resources),
2223            None => resolver,
2224        };
2225        for signature in view.document().descendants().filter(is_signature) {
2226            // Authentication is signature-local; work accounting is request-
2227            // wide. A valid first signature cannot authenticate a Manifest
2228            // belonging to an invalid second signature.
2229            let mut operation = OperationExecutionContext::new(
2230                &ctx.policy,
2231                &budgets,
2232                Some((view.identity(), view.generation())),
2233            );
2234            let signature_identity = view.node_identity(signature);
2235            let key = request
2236                .trusted_keys
2237                .iter()
2238                .find(|entry| entry.signature == signature_identity)
2239                .map(|entry| entry.key)
2240                .or(ctx.key);
2241            let capture = storage.capture();
2242            let result = verify_signature_node(
2243                view,
2244                ctx,
2245                key,
2246                &mut operation,
2247                &resolver,
2248                signature,
2249                &capture,
2250            );
2251            signatures.push_signature(SignatureEvidence {
2252                identity: view.node_identity(signature),
2253                result,
2254            })?;
2255            operations.retain(operation, capture)?;
2256        }
2257        let mut evidence = VerifyEvidence {
2258            document: view.root(),
2259            signatures,
2260            accepted: false,
2261            correlation: request.correlation,
2262        };
2263        let valid = evidence
2264            .signatures
2265            .iter()
2266            .filter(|entry| {
2267                entry.result.as_ref().is_ok_and(|result| {
2268                    result.status == DsigStatus::Valid
2269                        && result
2270                            .manifest_references
2271                            .iter()
2272                            .all(|reference| reference.status == DsigStatus::Valid)
2273                })
2274            })
2275            .count();
2276        evidence.accepted = match request.signatures {
2277            SignatureRequirement::All => valid > 0 && valid == count,
2278            SignatureRequirement::AtLeast(minimum) => valid >= minimum,
2279            SignatureRequirement::Exactly(required) => valid == required && count == required,
2280        };
2281        for target in request.expected_targets {
2282            if !evidence.covers_element(document, *target)? {
2283                evidence.accepted = false;
2284                break;
2285            }
2286        }
2287        continuation(view, ctx, &budgets, evidence, operations, resolver)
2288    })
2289}
2290
2291fn verify_signature_document_with_context_and_transforms(
2292    document: &XmlDocument,
2293    ctx: &VerifyContext<'_>,
2294    transforms: TransformExecutionBudget,
2295) -> Result<VerifyResult, SignatureVerificationPipelineError> {
2296    // RFC 5280 §6.1.1 makes the validation time an input to path validation.
2297    // Capture it once, not separately for each source or alternate path:
2298    // https://www.rfc-editor.org/rfc/rfc5280#section-6.1.1
2299    let captured_context;
2300    let ctx = if ctx.policy.key_trust.verify_x509_chains
2301        && ctx.policy.key_trust.verification_time.is_none()
2302    {
2303        captured_context = {
2304            let mut context = ctx.clone();
2305            context.policy.key_trust.verification_time = Some(std::time::SystemTime::now());
2306            context
2307        };
2308        &captured_context
2309    } else {
2310        ctx
2311    };
2312    let budgets =
2313        VerificationOperationBudgets::with_transforms(&ctx.policy, transforms, ctx.utc_facts);
2314    let mut operation = OperationExecutionContext::new(
2315        &ctx.policy,
2316        &budgets,
2317        Some((document.identity(), document.generation())),
2318    );
2319    document.with_view(|view| verify_signature_view(view, ctx, &mut operation))
2320}
2321
2322fn verify_signature_view<'a>(
2323    view: DocumentView<'a>,
2324    ctx: &VerifyContext<'_>,
2325    operation: &mut OperationExecutionContext<
2326        &crate::policy::VerificationPolicy,
2327        &VerificationOperationBudgets,
2328    >,
2329) -> Result<VerifyResult, SignatureVerificationPipelineError> {
2330    ctx.policy.validate()?;
2331    ctx.policy.signature_constraints.require_targets(false)?;
2332    let doc = view.document();
2333    let resolver = UriReferenceResolver::with_document_view(view, ctx.id_attributes)
2334        .with_same_document_id_semantics(ctx.policy.transforms.same_document_id_semantics)
2335        .with_external_resource_limits(
2336            ctx.policy.resources.max_external_resource_bytes,
2337            ctx.policy.resources.max_external_resource_total_bytes,
2338        );
2339    let resolver = match ctx.external_resources {
2340        Some(resources) => resolver.with_external_resources(resources),
2341        None => resolver,
2342    };
2343    let start_node = match ctx.signature_selection {
2344        SignatureSelection::FirstSignatureUnderId(id) => {
2345            resolver.node_for_id(id).ok_or_else(|| {
2346                SignatureVerificationPipelineError::SelectedNodeUnavailable { id: id.to_owned() }
2347            })?
2348        }
2349        SignatureSelection::UniqueDocumentSignature
2350        | SignatureSelection::FirstDocumentSignature => doc.root(),
2351    };
2352    let mut signatures = start_node.descendants().filter(|node| {
2353        node.is_element()
2354            && node.tag_name().name() == "Signature"
2355            && node.tag_name().namespace() == Some(XMLDSIG_NS)
2356    });
2357    let signature_node = match (signatures.next(), ctx.signature_selection) {
2358        (None, _) => {
2359            return Err(SignatureVerificationPipelineError::MissingElement {
2360                element: "Signature",
2361            });
2362        }
2363        // libxmlsec1 treats --node-id as an operation start node and performs
2364        // a depth-first xmlSecFindNode lookup from there. Without a selector,
2365        // the library API retains its fail-closed document-wide cardinality.
2366        (
2367            Some(node),
2368            SignatureSelection::FirstDocumentSignature
2369            | SignatureSelection::FirstSignatureUnderId(_),
2370        ) => node,
2371        (Some(node), SignatureSelection::UniqueDocumentSignature)
2372            if signatures.next().is_none() =>
2373        {
2374            node
2375        }
2376        (Some(_), SignatureSelection::UniqueDocumentSignature) => {
2377            return Err(SignatureVerificationPipelineError::InvalidStructure {
2378                reason: "Signature must appear exactly once in document",
2379            });
2380        }
2381    };
2382
2383    verify_signature_node(
2384        view,
2385        ctx,
2386        ctx.key,
2387        operation,
2388        &resolver,
2389        signature_node,
2390        &(),
2391    )
2392}
2393
2394fn verify_signature_node<'a, C: VerificationCapture>(
2395    view: DocumentView<'a>,
2396    ctx: &VerifyContext<'_>,
2397    key: Option<&dyn VerifyingKey>,
2398    operation: &mut OperationExecutionContext<
2399        &crate::policy::VerificationPolicy,
2400        &VerificationOperationBudgets,
2401    >,
2402    resolver: &UriReferenceResolver<'a>,
2403    signature_node: Node<'a, 'a>,
2404    capture: &C,
2405) -> Result<VerifyResult, DsigError> {
2406    if ctx.policy.resources.max_signatures == 0 {
2407        return Err(crate::policy::PolicyViolation::ResourceLimit {
2408            resource: crate::policy::resource_name::VERIFICATION_SIGNATURES,
2409            maximum: 0,
2410            actual: 1,
2411        }
2412        .into());
2413    }
2414    let doc = view.document();
2415    let signature_children = parse_signature_children(signature_node)?;
2416    ctx.policy
2417        .signature_constraints
2418        .check(signature_node, &operation.budgets().transforms)?;
2419    if ctx.decapsulation_key.is_some() {
2420        // Public-key encapsulation establishes a recipient secret, not sender
2421        // identity. Reject an impossible trust requirement before key work.
2422        if ctx.policy.key_trust.mode != crate::policy::VerificationTrustMode::CryptographicOnly {
2423            return Err(crate::policy::PolicyViolation::KeyTrust {
2424                reason: "KEM/HMAC does not authenticate sender identity",
2425            }
2426            .into());
2427        }
2428    }
2429    let signed_info_node = signature_children.signed_info_node;
2430    let should_parse_key_info = ctx.decapsulation_key.is_none()
2431        && match (key, ctx.key_resolver) {
2432            (Some(_), _) => false,
2433            (None, Some(resolver)) => resolver.consumes_document_key_info(),
2434            (None, None) => true,
2435        };
2436    let mut key_info = if should_parse_key_info {
2437        signature_children
2438            .key_info_node
2439            .map(|node| {
2440                super::parse::validate_x509_digest_policy(node, &|algorithm| {
2441                    ctx.policy.check_digest_algorithm(algorithm)
2442                })?;
2443                super::parse::parse_key_info_in_session(
2444                    node,
2445                    ctx.provider,
2446                    operation.budgets().transforms.xml_base_resolution(),
2447                    &ctx.policy.resources,
2448                    None,
2449                    ctx.policy
2450                        .key_trust
2451                        .certificate_signature_algorithms
2452                        .as_ref(),
2453                    &mut operation.budgets().key_info_parse.borrow_mut(),
2454                )
2455            })
2456            .transpose()
2457            .map_err(map_key_info_parse_error)?
2458    } else {
2459        None
2460    };
2461
2462    let signed_info = parse_signed_info_with_xpath_budget(
2463        signed_info_node,
2464        &mut operation.budgets().xpath_parse.borrow_mut(),
2465        &ctx.policy.resources,
2466    )?;
2467    capture.method(view, signed_info_node, signed_info.signature_method)?;
2468    if signed_info.references.len() > ctx.policy.resources.max_references {
2469        return Err(crate::policy::PolicyViolation::ResourceLimit {
2470            resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
2471            maximum: ctx.policy.resources.max_references,
2472            actual: signed_info.references.len(),
2473        }
2474        .into());
2475    }
2476    for reference in &signed_info.references {
2477        if reference.transforms.len() > ctx.policy.resources.max_transforms_per_reference {
2478            return Err(crate::policy::PolicyViolation::ResourceLimit {
2479                resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
2480                maximum: ctx.policy.resources.max_transforms_per_reference,
2481                actual: reference.transforms.len(),
2482            }
2483            .into());
2484        }
2485    }
2486    ctx.policy
2487        .check_signature_algorithm(signed_info.signature_method)?;
2488    // Refuse an inadmissible authenticator before reference resolution/transforms.
2489    // XMLDSig 1.1 section 4.4.2 explicitly requires max(80, hash_bits/2),
2490    // independently of data; section 6.3.1 separately requires octet alignment.
2491    // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-SignatureMethod
2492    if let Some(full_output_bits) = signed_info.signature_method.hmac_output_bits() {
2493        ctx.policy.hmac.validate_output(
2494            signed_info.signature_method,
2495            signed_info
2496                .hmac_output_length_bits
2497                .unwrap_or(full_output_bits),
2498        )?;
2499    }
2500    for reference in &signed_info.references {
2501        ctx.policy.check_digest_algorithm(reference.digest_method)?;
2502        super::transforms::validate_relationship_chain(
2503            &reference.transforms,
2504            ctx.policy.transforms.opc_relationship_edition,
2505        )
2506        .map_err(ReferenceProcessingError::Transform)?;
2507    }
2508    enforce_reference_policies(
2509        &signed_info.references,
2510        ctx.policy.uris.references,
2511        ctx.allowed_transform_uris(),
2512    )?;
2513    enforce_transform_allowed(ctx.allowed_transform_uris(), signed_info.c14n_method.uri())?;
2514
2515    if let Some(resources) = ctx.external_resources {
2516        validate_external_resource_map(
2517            resources,
2518            ctx.policy.resources.max_external_resource_bytes,
2519            ctx.policy.resources.max_external_resource_total_bytes,
2520        )
2521        .map_err(|error| match error {
2522            ExternalResourceMapError::Policy(error) => error.into(),
2523            ExternalResourceMapError::TotalLengthOverflow => {
2524                SignatureVerificationPipelineError::InvalidStructure {
2525                    reason: "external resource total length overflow",
2526                }
2527            }
2528        })?;
2529    }
2530    let remaining_reference_capacity = ctx
2531        .policy
2532        .resources
2533        .max_references
2534        .checked_sub(signed_info.references.len())
2535        .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2536            reason: "SignedInfo exceeds the per-signature Reference limit",
2537        })?;
2538    let plan_nodes = compile_verification_operation_plan(
2539        operation,
2540        view,
2541        signature_node,
2542        &signed_info.references,
2543        resolver,
2544    )?;
2545    let signature_identity =
2546        OperationResourceIdentity::DocumentNode(view.node_identity(signature_node));
2547    operation.run_with_resource(plan_nodes.document, &signature_identity, || {
2548        operation
2549            .validate_document_view(view)
2550            .map_err(map_verification_plan_error)
2551    })?;
2552    let observed_key_resources = operation.budgets().external_resource_set_identity(resolver);
2553    let retrieval_materialization = operation.run_with_resource(
2554        plan_nodes.key_materialization,
2555        &observed_key_resources,
2556        || {
2557            let budgets = operation.budgets();
2558            let mut materialization = budgets.key_info_materialization.borrow_mut();
2559            materialization.collect_encapsulation = true;
2560            materialization.encapsulation = None;
2561            if let Some(node) = signature_children.key_info_node {
2562                // A pinned key ignores lookup hints, not mechanism instructions.
2563                // Apply the same bounded reference discovery before key work.
2564                if ctx.decapsulation_key.is_some() {
2565                    discover_encapsulation_with_budget(
2566                        node,
2567                        resolver,
2568                        &ctx.policy.resources,
2569                        &budgets.transforms,
2570                        Some((&ctx.policy, ctx.xml_backend)),
2571                        Some(&mut materialization),
2572                    )?;
2573                } else if !should_parse_key_info {
2574                    if has_encapsulation_with_budget(
2575                        node,
2576                        resolver,
2577                        &ctx.policy.resources,
2578                        &budgets.transforms,
2579                        Some((&ctx.policy, ctx.xml_backend)),
2580                    )? {
2581                        return Err(crate::key_establishment::KeyEstablishmentError::Structure(
2582                            "mechanism and explicit recipient key must be supplied together",
2583                        )
2584                        .into());
2585                    }
2586                } else {
2587                    materialization.observe_encapsulation(node, resolver, 0)?;
2588                }
2589            }
2590            if let Some(info) = key_info.as_mut() {
2591                let mut xpath_parse = budgets.xpath_parse.borrow_mut();
2592                let mut retrieval_budgets = RetrievalMaterializationBudgets {
2593                    key_info_parse: &budgets.key_info_parse,
2594                    xpath_parse: &mut xpath_parse,
2595                    execution: &budgets.transforms,
2596                    resources: &ctx.policy.resources,
2597                    certificate_signature_algorithms: ctx
2598                        .policy
2599                        .key_trust
2600                        .certificate_signature_algorithms
2601                        .as_ref(),
2602                    xml_backend: ctx.xml_backend,
2603                };
2604                let mut outcome = materialize_key_info_references_with_budgets(
2605                    info,
2606                    resolver,
2607                    &ctx.policy,
2608                    ctx.provider,
2609                    &mut retrieval_budgets,
2610                    &mut materialization,
2611                )?;
2612                outcome.merge(materialize_retrieval_methods_with_budgets(
2613                    info,
2614                    resolver,
2615                    &ctx.policy,
2616                    ctx.provider,
2617                    &mut retrieval_budgets,
2618                    &mut materialization.candidate_work,
2619                )?);
2620                Ok::<_, SignatureVerificationPipelineError>(outcome)
2621            } else {
2622                Ok(RetrievalMaterialization::default())
2623            }
2624        },
2625    )?;
2626    let encapsulation = operation
2627        .budgets()
2628        .key_info_materialization
2629        .borrow_mut()
2630        .encapsulation
2631        .take();
2632    if encapsulation.is_some() != ctx.decapsulation_key.is_some() {
2633        return Err(crate::key_establishment::KeyEstablishmentError::Structure(
2634            "mechanism and explicit recipient key must be supplied together",
2635        )
2636        .into());
2637    }
2638    if let Some(mechanism) = &encapsulation {
2639        ctx.policy
2640            .key_establishment
2641            .check_encapsulation(mechanism.algorithm)?;
2642        if ctx
2643            .decapsulation_key
2644            .is_some_and(|key| key.algorithm() != mechanism.algorithm)
2645        {
2646            return Err(crate::key_establishment::KeyEstablishmentError::Structure(
2647                "recipient parameter set mismatch",
2648            )
2649            .into());
2650        }
2651    }
2652    let mut reference_results = Vec::with_capacity(signed_info.references.len());
2653    let mut first_failure = None;
2654    for (index, (node, reference)) in plan_nodes
2655        .digests
2656        .iter()
2657        .copied()
2658        .zip(&signed_info.references)
2659        .enumerate()
2660    {
2661        let observed = operation.budgets().resource_identity_for_reference(
2662            reference,
2663            index,
2664            resolver,
2665            view,
2666            reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2667        );
2668        let mut result = operation.run_with_resource(node, &observed, || {
2669            let budgets = operation.budgets();
2670            let execution = ReferenceExecutionContext {
2671                store_pre_digest: ctx.store_pre_digest,
2672                transform_options: ctx.transform_options(),
2673                transform_budget: &budgets.transforms,
2674                canonicalized_data_budget: &budgets.canonicalized,
2675                provider: ctx.provider,
2676                capture,
2677            };
2678            process_reference_with_options(
2679                reference,
2680                resolver,
2681                signature_node,
2682                ReferenceSet::SignedInfo,
2683                index,
2684                reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2685                &execution,
2686            )
2687            .map_err(SignatureVerificationPipelineError::from)
2688        })?;
2689        bind_reference_evidence(
2690            &mut result,
2691            reference,
2692            view,
2693            reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2694            &observed,
2695        );
2696        let accepted = result.status == DsigStatus::Valid;
2697        operation.set_outcome(
2698            node,
2699            accepted,
2700            if accepted {
2701                OperationDecisionReason::ReferenceDigestVerified
2702            } else {
2703                OperationDecisionReason::ReferenceDigestRejected
2704            },
2705        );
2706        if result.status == DsigStatus::Valid
2707            && let Some(reference) = signed_info.references.get(result.reference_index)
2708            && reference
2709                .transforms
2710                .iter()
2711                .all(transform_preserves_manifest_structure)
2712            && let Some(uri) = reference.uri.as_deref()
2713            && let Ok(Some(target)) = resolver.node_id_for_same_document_reference(uri)
2714        {
2715            let identity = view.node_identity_by_id(target);
2716            operation.authenticate(identity);
2717            debug_assert!(operation.is_authenticated(identity));
2718        }
2719        reference_results.push(result);
2720        if !accepted {
2721            first_failure = Some(index);
2722            break;
2723        }
2724    }
2725    debug_assert!(
2726        plan_nodes.digests[reference_results.len()..]
2727            .iter()
2728            .all(|node| !operation.is_executed(*node))
2729    );
2730    let references = ReferencesResult {
2731        results: reference_results,
2732        first_failure,
2733    };
2734
2735    if let Some(first_failure) = references.first_failure {
2736        debug_assert!(operation.first_failure().is_some());
2737        let status = references.results[first_failure].status;
2738        return Ok(VerifyResult {
2739            key_trust: super::KeyTrustEvidence::NotEvaluated,
2740            status,
2741            signed_info_references: references.results,
2742            manifest_references: Vec::new(),
2743            canonicalized_signed_info: None,
2744        });
2745    }
2746
2747    let canonical_signed_info = operation.run(plan_nodes.canonicalization, || {
2748        let signed_info_subtree: HashSet<_> = signed_info_node
2749            .descendants()
2750            .map(|node: Node<'_, '_>| node.id())
2751            .collect();
2752        let mut canonical_signed_info = Vec::new();
2753        let signed_info_limit = operation
2754            .budgets()
2755            .canonicalized
2756            .remaining()
2757            .min(operation.budgets().transforms.remaining_c14n_output());
2758        canonicalize_bounded_with_xml_base_budget(
2759            doc,
2760            Some(&|node| signed_info_subtree.contains(&node.id())),
2761            &signed_info.c14n_method,
2762            signed_info_limit,
2763            operation.budgets().transforms.xml_base_resolution(),
2764            &mut canonical_signed_info,
2765        )
2766        .map_err(|error| {
2767            if let Some(violation) = map_c14n_resource_policy_violation(
2768                &error,
2769                crate::policy::resource_name::CANONICALIZED_BYTES,
2770                operation.budgets().canonicalized.max_bytes,
2771            ) {
2772                SignatureVerificationPipelineError::Policy(violation)
2773            } else {
2774                SignatureVerificationPipelineError::Canonicalization(error)
2775            }
2776        })?;
2777        operation
2778            .budgets()
2779            .transforms
2780            .charge_c14n_output(canonical_signed_info.len())
2781            .map_err(ReferenceProcessingError::Transform)?;
2782        operation
2783            .budgets()
2784            .canonicalized
2785            .charge(canonical_signed_info.len())?;
2786        Ok::<_, SignatureVerificationPipelineError>(canonical_signed_info)
2787    })?;
2788
2789    let signature_value = decode_signature_value(
2790        signature_children.signature_value_node,
2791        signed_info.signature_method,
2792        &ctx.policy.resources,
2793    )?;
2794    if let Some(full_output_bits) = signed_info.signature_method.hmac_output_bits() {
2795        let expected_bits = signed_info
2796            .hmac_output_length_bits
2797            .unwrap_or(full_output_bits);
2798        if signature_value.len() != expected_bits / 8 {
2799            return Err(SignatureVerificationPipelineError::InvalidStructure {
2800                reason: "SignatureValue length does not match HMACOutputLength",
2801            });
2802        }
2803    }
2804    let resolved_key = operation.run(plan_nodes.key, || {
2805        if let Some(recipient) = ctx.decapsulation_key {
2806            if key.is_some() || ctx.key_resolver.is_some() {
2807                return Err(crate::key_establishment::KeyEstablishmentError::Structure(
2808                    "ambiguous verification key request",
2809                )
2810                .into());
2811            }
2812            require_verifying_key_candidate_capacity(&ctx.policy)?;
2813            if !ctx.policy.key_sources.preset_key
2814                || signed_info.signature_method.hmac_output_bits().is_none()
2815            {
2816                return Err(crate::key_establishment::KeyEstablishmentError::Structure(
2817                    "encapsulation requires permitted explicit key and HMAC",
2818                )
2819                .into());
2820            }
2821            ctx.policy.hmac.validate_key_bits(256)?;
2822            let mechanism = encapsulation.as_ref().ok_or(
2823                crate::key_establishment::KeyEstablishmentError::Structure("missing mechanism"),
2824            )?;
2825            operation.budgets().key_candidates.borrow_mut().charge()?;
2826            operation
2827                .budgets()
2828                .key_establishment
2829                .borrow_mut()
2830                .commit_all(&ctx.policy.key_establishment, 0, 0, 32)?;
2831            let secret = operation
2832                .budgets()
2833                .key_establishment
2834                .borrow_mut()
2835                .decapsulate(
2836                    &ctx.policy.key_establishment,
2837                    ctx.provider,
2838                    recipient,
2839                    mechanism.algorithm,
2840                    &mechanism.ciphertext[..mechanism.algorithm.ciphertext_len()],
2841                )?;
2842            let hmac = super::HmacVerificationKey::new(secret.to_vec())?;
2843            return Ok(Some(ResolvedVerifyingKey::Established(hmac)));
2844        }
2845        resolve_verifying_key(
2846            ctx,
2847            key,
2848            key_info.as_ref(),
2849            signed_info.signature_method,
2850            &mut operation.budgets().key_candidates.borrow_mut(),
2851        )
2852    })?;
2853    let Some(resolved_key) = resolved_key else {
2854        operation.set_outcome(
2855            plan_nodes.key,
2856            false,
2857            OperationDecisionReason::KeyUnavailable,
2858        );
2859        if let Some(error) = retrieval_materialization.deferred_error {
2860            return Err(error);
2861        }
2862        return Ok(VerifyResult {
2863            key_trust: super::KeyTrustEvidence::NotEvaluated,
2864            status: DsigStatus::Invalid(FailureReason::KeyNotFound),
2865            signed_info_references: references.results,
2866            manifest_references: Vec::new(),
2867            canonicalized_signed_info: if ctx.store_pre_digest {
2868                Some(canonical_signed_info)
2869            } else {
2870                None
2871            },
2872        });
2873    };
2874    operation.set_outcome(plan_nodes.key, true, OperationDecisionReason::KeyResolved);
2875    let verifier = resolved_key.as_ref();
2876    let signature_valid = operation.run(plan_nodes.crypto, || {
2877        verifier.validate_policy(&ctx.policy)?;
2878        if !verifier.validate_signature_value_with_policy(
2879            &ctx.policy,
2880            signed_info.signature_method,
2881            &signature_value,
2882        )? {
2883            return Ok::<_, SignatureVerificationPipelineError>(false);
2884        }
2885        ctx.provider
2886            .require_capability(crate::provider::ProviderCapability::Verify(
2887                signed_info.signature_method,
2888            ))?;
2889        let candidate_refusal = RefCell::new(None);
2890        let policy_verifier = PolicyVerifyingKey {
2891            key: verifier,
2892            policy: &ctx.policy,
2893            candidates: &operation.budgets().key_candidates,
2894            candidate_refusal: &candidate_refusal,
2895        };
2896        let result = capture.verify(
2897            ctx.provider,
2898            &policy_verifier,
2899            crate::provider::SignatureVerificationInput {
2900                algorithm: signed_info.signature_method,
2901                context: &signed_info.signature_context,
2902                data: &canonical_signed_info,
2903                signature: &signature_value,
2904            },
2905        );
2906        if let Some(error) = candidate_refusal.into_inner() {
2907            return Err(error.into());
2908        }
2909        result
2910    })?;
2911
2912    if !signature_valid {
2913        operation.set_outcome(
2914            plan_nodes.crypto,
2915            false,
2916            OperationDecisionReason::SignatureRejected,
2917        );
2918        return Ok(VerifyResult {
2919            key_trust: resolved_key.evidence(&ctx.policy)?,
2920            status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
2921            signed_info_references: references.results,
2922            manifest_references: Vec::new(),
2923            canonicalized_signed_info: if ctx.store_pre_digest {
2924                Some(canonical_signed_info)
2925            } else {
2926                None
2927            },
2928        });
2929    }
2930    operation.set_outcome(
2931        plan_nodes.crypto,
2932        true,
2933        OperationDecisionReason::SignatureVerified,
2934    );
2935    // Manifest syntax and references are authenticated content. Delay parsing
2936    // until SignatureValue succeeds, then extend the same operation graph with
2937    // the newly trusted dependency subgraph and its evidence node.
2938    let (manifest_references, manifest_digests) =
2939        if ctx.policy.manifest_processing == crate::policy::ManifestProcessing::Process {
2940            process_authenticated_manifest_references(
2941                operation,
2942                view,
2943                signature_node,
2944                resolver,
2945                ManifestVerification {
2946                    context: ctx,
2947                    capture,
2948                },
2949                remaining_reference_capacity,
2950                plan_nodes.crypto,
2951            )?
2952        } else {
2953            (Vec::new(), Vec::new())
2954        };
2955    operation.extend();
2956    let evidence = operation.add_node(OperationNodeKind::Evidence, OperationStage::Evidence, None);
2957    operation
2958        .add_dependency(evidence, plan_nodes.crypto)
2959        .map_err(map_verification_plan_error)?;
2960    for digest in manifest_digests {
2961        operation
2962            .add_dependency(evidence, digest)
2963            .map_err(map_verification_plan_error)?;
2964    }
2965    operation.compile().map_err(map_verification_plan_error)?;
2966    operation.run(evidence, || {
2967        Ok::<_, SignatureVerificationPipelineError>(VerifyResult {
2968            key_trust: resolved_key.evidence(&ctx.policy)?,
2969            status: DsigStatus::Valid,
2970            signed_info_references: references.results,
2971            manifest_references,
2972            canonicalized_signed_info: if ctx.store_pre_digest {
2973                Some(canonical_signed_info)
2974            } else {
2975                None
2976            },
2977        })
2978    })
2979}
2980
2981struct PolicyVerifyingKey<'a, 'inputs> {
2982    key: &'a dyn VerifyingKey,
2983    policy: &'a crate::policy::VerificationPolicy,
2984    candidates: &'a RefCell<super::InspectedKeyCandidateBudget<'inputs>>,
2985    candidate_refusal: &'a RefCell<Option<crate::policy::PolicyViolation>>,
2986}
2987
2988impl VerifyingKey for PolicyVerifyingKey<'_, '_> {
2989    fn association_spki(&self, algorithm: SignatureAlgorithm) -> Option<&[u8]> {
2990        self.key.association_spki(algorithm)
2991    }
2992    fn provider_binding(&self) -> Option<&crate::provider::ProviderBinding> {
2993        self.key.provider_binding()
2994    }
2995    fn verify_candidate_keys(
2996        &self,
2997        verify: &mut dyn FnMut(&dyn VerifyingKey) -> Result<bool, DsigError>,
2998    ) -> Result<Option<bool>, DsigError> {
2999        self.key.verify_candidate_keys(&mut |key| {
3000            if let Some(error) = self.candidate_refusal.borrow().as_ref() {
3001                return Err(error.clone().into());
3002            }
3003            // The resolver already admitted this handle. Every child exposed
3004            // by it is another attempt in that same operation-wide budget.
3005            if let Err(error) = self.candidates.borrow_mut().charge_policy(1) {
3006                *self.candidate_refusal.borrow_mut() = Some(error.clone());
3007                return Err(error.into());
3008            }
3009            key.validate_policy(self.policy)?;
3010            verify(&PolicyVerifyingKey {
3011                key,
3012                policy: self.policy,
3013                candidates: self.candidates,
3014                candidate_refusal: self.candidate_refusal,
3015            })
3016        })
3017    }
3018    fn verification_spki(&self, algorithm: SignatureAlgorithm) -> Result<Option<&[u8]>, DsigError> {
3019        self.key.verification_spki(algorithm)
3020    }
3021
3022    fn ecdsa_encoding(&self) -> crate::policy::EcdsaSignatureValueEncoding {
3023        self.policy.ecdsa_signature_value_encoding
3024    }
3025    fn verify_with_context(
3026        &self,
3027        algorithm: SignatureAlgorithm,
3028        context: &super::SignatureContext,
3029        data: &[u8],
3030        signature: &[u8],
3031    ) -> Result<bool, DsigError> {
3032        if context.as_bytes().is_empty() {
3033            self.verify(algorithm, data, signature)
3034        } else {
3035            self.key
3036                .verify_with_context(algorithm, context, data, signature)
3037        }
3038    }
3039    fn validate_policy(&self, policy: &crate::policy::VerificationPolicy) -> Result<(), DsigError> {
3040        self.key.validate_policy(policy)
3041    }
3042
3043    fn validate_signature_value(
3044        &self,
3045        algorithm: SignatureAlgorithm,
3046        signature_value: &[u8],
3047    ) -> Result<bool, DsigError> {
3048        self.key
3049            .validate_signature_value_with_policy(self.policy, algorithm, signature_value)
3050    }
3051
3052    fn verify(
3053        &self,
3054        algorithm: SignatureAlgorithm,
3055        signed_data: &[u8],
3056        signature_value: &[u8],
3057    ) -> Result<bool, DsigError> {
3058        self.key
3059            .verify_with_policy(self.policy, algorithm, signed_data, signature_value)
3060    }
3061}
3062
3063#[derive(Debug, Default)]
3064struct RetrievalMaterialization {
3065    deferred_error: Option<SignatureVerificationPipelineError>,
3066}
3067
3068impl RetrievalMaterialization {
3069    fn merge(&mut self, other: Self) {
3070        if self.deferred_error.is_none() {
3071            self.deferred_error = other.deferred_error;
3072        }
3073    }
3074}
3075
3076struct RetrievalMaterializationBudgets<'a> {
3077    key_info_parse: &'a RefCell<super::parse::KeyInfoParseUsage>,
3078    certificate_signature_algorithms: Option<&'a crate::policy::CertificateSignatureAlgorithms>,
3079    xpath_parse: &'a mut XPathSignatureParseBudget,
3080    execution: &'a TransformExecutionBudget,
3081    resources: &'a crate::policy::ResourcePolicy,
3082    xml_backend: crate::XmlBackend,
3083}
3084
3085#[derive(Default)]
3086struct KeyInfoMaterializationState {
3087    active: HashSet<(super::uri::TraversalDocumentIdentity, String)>,
3088    candidate_work: usize,
3089    collect_encapsulation: bool,
3090    encapsulation_template: bool,
3091    retain_encapsulation_target: bool,
3092    encapsulation_depth: usize,
3093    reference_depth_base: usize,
3094    encapsulation_target: Option<(super::uri::TraversalDocumentIdentity, NodeId, NodeId)>,
3095    encapsulation: Option<ResolvedEncapsulation>,
3096}
3097
3098struct ResolvedEncapsulation {
3099    algorithm: crate::provider::KeyEncapsulationAlgorithm,
3100    ciphertext: [u8; 1568],
3101}
3102
3103impl KeyInfoMaterializationState {
3104    fn observe_encapsulation(
3105        &mut self,
3106        node: Node<'_, '_>,
3107        resolver: &UriReferenceResolver<'_>,
3108        depth: usize,
3109    ) -> Result<(), SignatureVerificationPipelineError> {
3110        if !self.collect_encapsulation {
3111            return Ok(());
3112        }
3113        // XMLDSig 1.1 §4.5.10 references KeyInfo, including its extension
3114        // children. Collect metadata in the gated reference traversal, not a
3115        // separate unbounded scan; external document nodes cannot escape it.
3116        // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-KeyInfoReference
3117        let Some(mechanism) =
3118            crate::key_establishment::direct_encapsulation(node, self.encapsulation_template)
3119                .map_err(|_| SignatureVerificationPipelineError::InvalidStructure {
3120                    reason: "invalid encapsulation mechanism in resolved KeyInfo",
3121                })?
3122        else {
3123            return Ok(());
3124        };
3125        self.record_encapsulation(mechanism, resolver, depth)
3126    }
3127
3128    fn record_encapsulation(
3129        &mut self,
3130        mechanism: crate::key_establishment::ParsedEncapsulation<'_, '_>,
3131        resolver: &UriReferenceResolver<'_>,
3132        depth: usize,
3133    ) -> Result<(), SignatureVerificationPipelineError> {
3134        if self.encapsulation.is_some() {
3135            return Err(SignatureVerificationPipelineError::InvalidStructure {
3136                reason: "multiple encapsulation mechanisms in resolved KeyInfo",
3137            });
3138        }
3139        if self.retain_encapsulation_target {
3140            self.encapsulation_depth = depth;
3141            self.encapsulation_target = Some((
3142                resolver.traversal_document_identity(),
3143                mechanism.cipher_value.id(),
3144                mechanism.key_info.id(),
3145            ));
3146        }
3147        self.encapsulation = Some(ResolvedEncapsulation {
3148            algorithm: mechanism.algorithm,
3149            ciphertext: mechanism.into_ciphertext(),
3150        });
3151        Ok(())
3152    }
3153}
3154
3155trait KeyInfoReferencePolicy {
3156    fn key_establishment(&self) -> &crate::policy::KeyEstablishmentPolicy;
3157    fn check_digest_algorithm(
3158        &self,
3159        algorithm: DigestAlgorithm,
3160    ) -> Result<(), crate::policy::PolicyViolation>;
3161    fn certificate_signature_algorithms(
3162        &self,
3163    ) -> Option<&crate::policy::CertificateSignatureAlgorithms>;
3164    fn resources(&self) -> &crate::policy::ResourcePolicy;
3165    fn xml(&self) -> &crate::policy::XmlInputPolicy;
3166    fn key_info_reference_uris(&self) -> UriTypeSet;
3167    fn retrieval_method_uris(&self) -> UriTypeSet;
3168    fn key_info_reference_source_enabled(&self) -> bool;
3169    fn allowed_transforms(&self) -> Option<&HashSet<String>>;
3170}
3171
3172impl KeyInfoReferencePolicy for crate::policy::SigningPolicy {
3173    fn key_establishment(&self) -> &crate::policy::KeyEstablishmentPolicy {
3174        &self.key_establishment
3175    }
3176    fn check_digest_algorithm(
3177        &self,
3178        algorithm: DigestAlgorithm,
3179    ) -> Result<(), crate::policy::PolicyViolation> {
3180        self.check_digest_algorithm(algorithm)
3181    }
3182    fn certificate_signature_algorithms(
3183        &self,
3184    ) -> Option<&crate::policy::CertificateSignatureAlgorithms> {
3185        None
3186    }
3187    fn resources(&self) -> &crate::policy::ResourcePolicy {
3188        &self.resources
3189    }
3190
3191    fn xml(&self) -> &crate::policy::XmlInputPolicy {
3192        &self.xml
3193    }
3194
3195    fn key_info_reference_uris(&self) -> UriTypeSet {
3196        self.uris.key_info_references
3197    }
3198
3199    fn retrieval_method_uris(&self) -> UriTypeSet {
3200        self.uris.retrieval_methods
3201    }
3202
3203    fn key_info_reference_source_enabled(&self) -> bool {
3204        true
3205    }
3206
3207    fn allowed_transforms(&self) -> Option<&HashSet<String>> {
3208        self.transforms.allowed_algorithms.as_ref()
3209    }
3210}
3211
3212impl KeyInfoReferencePolicy for crate::policy::VerificationPolicy {
3213    fn key_establishment(&self) -> &crate::policy::KeyEstablishmentPolicy {
3214        &self.key_establishment
3215    }
3216    fn check_digest_algorithm(
3217        &self,
3218        algorithm: DigestAlgorithm,
3219    ) -> Result<(), crate::policy::PolicyViolation> {
3220        self.check_digest_algorithm(algorithm)
3221    }
3222    fn certificate_signature_algorithms(
3223        &self,
3224    ) -> Option<&crate::policy::CertificateSignatureAlgorithms> {
3225        self.key_trust.certificate_signature_algorithms.as_ref()
3226    }
3227    fn resources(&self) -> &crate::policy::ResourcePolicy {
3228        &self.resources
3229    }
3230
3231    fn xml(&self) -> &crate::policy::XmlInputPolicy {
3232        &self.xml
3233    }
3234
3235    fn key_info_reference_uris(&self) -> UriTypeSet {
3236        self.uris.key_info_references
3237    }
3238
3239    fn retrieval_method_uris(&self) -> UriTypeSet {
3240        self.uris.retrieval_methods
3241    }
3242
3243    fn key_info_reference_source_enabled(&self) -> bool {
3244        self.key_sources.key_info_reference
3245    }
3246
3247    fn allowed_transforms(&self) -> Option<&HashSet<String>> {
3248        self.transforms.allowed_algorithms.as_ref()
3249    }
3250}
3251
3252struct KeyInfoReferenceMaterializationContext<'a, 'budget, P> {
3253    policy: &'a P,
3254    provider: &'a dyn crate::provider::CryptoProvider,
3255    budgets: &'a mut RetrievalMaterializationBudgets<'budget>,
3256}
3257
3258fn materialize_key_info_references_with_budgets<P: KeyInfoReferencePolicy>(
3259    key_info: &mut KeyInfo,
3260    resolver: &UriReferenceResolver<'_>,
3261    policy: &P,
3262    provider: &dyn crate::provider::CryptoProvider,
3263    budgets: &mut RetrievalMaterializationBudgets<'_>,
3264    materialization: &mut KeyInfoMaterializationState,
3265) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
3266    fn visit<P: KeyInfoReferencePolicy>(
3267        key_info: &mut KeyInfo,
3268        resolver: &UriReferenceResolver<'_>,
3269        context: &mut KeyInfoReferenceMaterializationContext<'_, '_, P>,
3270        materialization: &mut KeyInfoMaterializationState,
3271        depth: usize,
3272    ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
3273        let mut materialized = Vec::new();
3274        let mut outcome = RetrievalMaterialization::default();
3275        for source in std::mem::take(&mut key_info.sources) {
3276            let source_work = match &source {
3277                super::parse::KeyInfoSource::X509Data(info) => info.certificates.len().max(1),
3278                _ => 1,
3279            };
3280            materialization.candidate_work =
3281                materialization.candidate_work.saturating_add(source_work);
3282            context
3283                .policy
3284                .resources()
3285                .validate_key_candidates(materialization.candidate_work)?;
3286
3287            let super::parse::KeyInfoSource::KeyInfoReference { uri } = source else {
3288                materialized.push(source);
3289                continue;
3290            };
3291            if !context.policy.key_info_reference_source_enabled() {
3292                return Err(crate::policy::PolicyViolation::KeyTrust {
3293                    reason: "KeyInfoReference key sources are disabled",
3294                }
3295                .into());
3296            }
3297            if !context.policy.key_info_reference_uris().allows(&uri) {
3298                return Err(crate::policy::PolicyViolation::Uri {
3299                    operation: "KeyInfoReference",
3300                    reason: "URI class is disabled",
3301                }
3302                .into());
3303            }
3304            let next_depth = depth.saturating_add(1);
3305            context
3306                .policy
3307                .resources()
3308                .validate_key_info_reference_depth(next_depth)?;
3309            let cycle_key = (resolver.traversal_document_identity(), uri.clone());
3310            if !materialization.active.insert(cycle_key.clone()) {
3311                return Err(SignatureVerificationPipelineError::InvalidStructure {
3312                    reason: "KeyInfoReference cycle detected",
3313                });
3314            }
3315
3316            let is_same_document = uri.is_empty() || uri.starts_with('#');
3317            let (mut referenced, mut nested_outcome) = if is_same_document {
3318                let node = resolver
3319                    .node_for_same_document_reference(&uri)
3320                    .map_err(|error| {
3321                        SignatureVerificationPipelineError::from(
3322                            ReferenceProcessingError::UriDereference(error),
3323                        )
3324                    })?
3325                    .ok_or(SignatureVerificationPipelineError::InvalidStructure {
3326                        reason: "KeyInfoReference target is missing or ambiguous",
3327                    })?;
3328                if !node.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
3329                    return Err(SignatureVerificationPipelineError::InvalidStructure {
3330                        reason: "KeyInfoReference target must be KeyInfo",
3331                    });
3332                }
3333                materialization.observe_encapsulation(node, resolver, next_depth)?;
3334                if let Some(mechanism) = &materialization.encapsulation {
3335                    context
3336                        .policy
3337                        .key_establishment()
3338                        .check_encapsulation(mechanism.algorithm)?;
3339                }
3340                super::parse::validate_x509_digest_policy(node, &|algorithm| {
3341                    context.policy.check_digest_algorithm(algorithm)
3342                })
3343                .map_err(map_key_info_parse_error)?;
3344                (
3345                    super::parse::parse_key_info_in_session(
3346                        node,
3347                        context.provider,
3348                        context.budgets.execution.xml_base_resolution(),
3349                        context.policy.resources(),
3350                        None,
3351                        context.policy.certificate_signature_algorithms(),
3352                        &mut context.budgets.key_info_parse.borrow_mut(),
3353                    )
3354                    .map_err(map_key_info_parse_error)?,
3355                    RetrievalMaterialization::default(),
3356                )
3357            } else {
3358                // The fragment selects a node inside the retrieved document; it
3359                // is not part of either the caller-owned resource identity or
3360                // the base URI used by references nested in that document.
3361                let (resource_uri, fragment) = uri
3362                    .split_once('#')
3363                    .map_or((uri.as_str(), None), |(resource, fragment)| {
3364                        (resource, (!fragment.is_empty()).then_some(fragment))
3365                    });
3366                let bytes = resolver
3367                    .external_resource(resource_uri)
3368                    .map_err(|error| {
3369                        SignatureVerificationPipelineError::from(
3370                            ReferenceProcessingError::UriDereference(error),
3371                        )
3372                    })?
3373                    .ok_or(SignatureVerificationPipelineError::InvalidStructure {
3374                        reason: "KeyInfoReference external resource is unavailable",
3375                    })?;
3376                // External KeyInfo bytes share the operation's work allowance even if decoding
3377                // fails before a document exists. Parsing the decoded text is a separate pass.
3378                context
3379                    .budgets
3380                    .execution
3381                    .xml_parse_work()
3382                    .charge_policy(bytes.len())?;
3383                let xml = crate::encoding::decode_xml_octets(
3384                    bytes,
3385                    context.policy.resources().max_xml_document_bytes,
3386                )
3387                .map_err(map_key_info_xml_decode_error)?;
3388                let settings = DocumentParseSettings::from_policy(
3389                    context.policy.xml(),
3390                    context.policy.resources(),
3391                )
3392                .with_backend(context.budgets.xml_backend);
3393                let document = XmlDocument::parse_with_settings_and_budget(
3394                    xml.into_owned(),
3395                    settings,
3396                    context.budgets.execution.xml_parse_work(),
3397                )
3398                .map_err(|error| map_document_parse_error(error, settings))?;
3399                document.with_view(|view| {
3400                    let external_resolver = resolver.for_external_document_view(view, resource_uri);
3401                    let target = match fragment {
3402                        Some(fragment) => external_resolver
3403                            .node_for_same_document_reference(&format!("#{fragment}"))
3404                            .map_err(|error| {
3405                                SignatureVerificationPipelineError::from(
3406                                    ReferenceProcessingError::UriDereference(error),
3407                                )
3408                            })?
3409                            .ok_or(SignatureVerificationPipelineError::InvalidStructure {
3410                                reason: "KeyInfoReference external target is missing or ambiguous",
3411                            })?,
3412                        _ => view.document().root_element(),
3413                    };
3414                    if !target.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
3415                        return Err(SignatureVerificationPipelineError::InvalidStructure {
3416                            reason: "KeyInfoReference external target must be KeyInfo",
3417                        });
3418                    }
3419                    materialization.observe_encapsulation(
3420                        target,
3421                        &external_resolver,
3422                        next_depth,
3423                    )?;
3424                    if let Some(mechanism) = &materialization.encapsulation {
3425                        context
3426                            .policy
3427                            .key_establishment()
3428                            .check_encapsulation(mechanism.algorithm)?;
3429                    }
3430                    super::parse::validate_x509_digest_policy(target, &|algorithm| {
3431                        context.policy.check_digest_algorithm(algorithm)
3432                    })
3433                    .map_err(map_key_info_parse_error)?;
3434                    let mut referenced = super::parse::parse_key_info_in_session(
3435                        target,
3436                        context.provider,
3437                        context.budgets.execution.xml_base_resolution(),
3438                        context.policy.resources(),
3439                        Some(resource_uri),
3440                        context.policy.certificate_signature_algorithms(),
3441                        &mut context.budgets.key_info_parse.borrow_mut(),
3442                    )
3443                    .map_err(map_key_info_parse_error)?;
3444                    let mut nested_outcome = visit(
3445                        &mut referenced,
3446                        &external_resolver,
3447                        context,
3448                        materialization,
3449                        next_depth,
3450                    )?;
3451                    nested_outcome.merge(materialize_retrieval_methods_with_budgets(
3452                        &mut referenced,
3453                        &external_resolver,
3454                        context.policy,
3455                        context.provider,
3456                        context.budgets,
3457                        &mut materialization.candidate_work,
3458                    )?);
3459                    // RetrievalMethod nodes are meaningful only in the document
3460                    // whose resolver owns their same-document URI context.
3461                    referenced.sources.retain(|source| {
3462                        !matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. })
3463                    });
3464                    Ok((referenced, nested_outcome))
3465                })?
3466            };
3467            if is_same_document {
3468                nested_outcome.merge(visit(
3469                    &mut referenced,
3470                    resolver,
3471                    context,
3472                    materialization,
3473                    next_depth,
3474                )?);
3475            }
3476            outcome.merge(nested_outcome);
3477            materialization.active.remove(&cycle_key);
3478            materialized.extend(referenced.sources);
3479        }
3480        key_info.sources = materialized;
3481        context
3482            .policy
3483            .resources()
3484            .validate_key_candidates(key_info.embedded_candidate_count())?;
3485        Ok(outcome)
3486    }
3487
3488    let mut context = KeyInfoReferenceMaterializationContext {
3489        policy,
3490        provider,
3491        budgets,
3492    };
3493    let depth = materialization.reference_depth_base;
3494    visit(key_info, resolver, &mut context, materialization, depth)
3495}
3496
3497fn map_key_info_xml_decode_error(
3498    error: crate::encoding::XmlEncodingError,
3499) -> SignatureVerificationPipelineError {
3500    match error {
3501        crate::encoding::XmlEncodingError::DecodedLimit { maximum, actual } => {
3502            SignatureVerificationPipelineError::Policy(
3503                crate::policy::PolicyViolation::ResourceLimit {
3504                    resource: crate::policy::resource_name::XML_DOCUMENT,
3505                    maximum,
3506                    actual,
3507                },
3508            )
3509        }
3510        _ => SignatureVerificationPipelineError::InvalidStructure {
3511            reason: "KeyInfoReference external resource has an invalid XML encoding",
3512        },
3513    }
3514}
3515
3516/// Resolved signature hints and experimental recipient mechanism metadata.
3517/// No key hint establishes trust or permission. The mutation handle is private
3518/// and bound to the original document, never an external document's local ID.
3519pub struct SignatureKeyInfoMetadata {
3520    /// Materialized recipient hints for KEM, ordinary hints otherwise.
3521    pub key_info: KeyInfo,
3522    /// Exact KEM parameter set, if the resolved graph contains one mechanism.
3523    pub encapsulation: Option<crate::provider::KeyEncapsulationAlgorithm>,
3524    pub(crate) cipher_value: Option<NodeId>,
3525}
3526
3527/// Discover operation instructions in local KeyInfo references without consuming
3528/// unrelated key hints. A positive result requires full metadata validation.
3529pub fn has_same_document_encapsulation(
3530    node: Node<'_, '_>,
3531    resolver: &UriReferenceResolver<'_>,
3532    resources: &crate::policy::ResourcePolicy,
3533) -> Result<bool, DsigError> {
3534    resources.validate()?;
3535    let execution = TransformExecutionBudget::from_resources(resources);
3536    has_same_document_encapsulation_with_budget(node, resolver, resources, &execution)
3537}
3538
3539pub(crate) fn has_same_document_encapsulation_with_budget(
3540    node: Node<'_, '_>,
3541    resolver: &UriReferenceResolver<'_>,
3542    resources: &crate::policy::ResourcePolicy,
3543    execution: &TransformExecutionBudget,
3544) -> Result<bool, DsigError> {
3545    has_encapsulation_with_budget::<crate::policy::SigningPolicy>(
3546        node, resolver, resources, execution, None,
3547    )
3548}
3549
3550pub(crate) fn has_signing_encapsulation_with_budget(
3551    node: Node<'_, '_>,
3552    resolver: &UriReferenceResolver<'_>,
3553    policy: &crate::policy::SigningPolicy,
3554    execution: &TransformExecutionBudget,
3555    backend: crate::XmlBackend,
3556) -> Result<bool, DsigError> {
3557    has_encapsulation_with_budget(
3558        node,
3559        resolver,
3560        &policy.resources,
3561        execution,
3562        Some((policy, backend)),
3563    )
3564}
3565
3566fn has_encapsulation_with_budget<P: KeyInfoReferencePolicy>(
3567    node: Node<'_, '_>,
3568    resolver: &UriReferenceResolver<'_>,
3569    resources: &crate::policy::ResourcePolicy,
3570    execution: &TransformExecutionBudget,
3571    external: Option<(&P, crate::XmlBackend)>,
3572) -> Result<bool, DsigError> {
3573    discover_encapsulation_with_budget(node, resolver, resources, execution, external, None)
3574}
3575
3576fn discover_encapsulation_with_budget<P: KeyInfoReferencePolicy>(
3577    node: Node<'_, '_>,
3578    resolver: &UriReferenceResolver<'_>,
3579    resources: &crate::policy::ResourcePolicy,
3580    execution: &TransformExecutionBudget,
3581    external: Option<(&P, crate::XmlBackend)>,
3582    materialization: Option<&mut KeyInfoMaterializationState>,
3583) -> Result<bool, DsigError> {
3584    if resolver.node_for_node_id(node.id()) != Some(node) {
3585        return Err(DsigError::InvalidStructure {
3586            reason: "KeyInfo and resolver belong to different documents",
3587        });
3588    }
3589    struct ExternalProbePath<'a> {
3590        uri: &'a str,
3591        parent: Option<&'a ExternalProbePath<'a>>,
3592    }
3593    struct ProbeContext<'a, P> {
3594        resources: &'a crate::policy::ResourcePolicy,
3595        external: Option<(&'a P, crate::XmlBackend)>,
3596        document_base: Option<&'a str>,
3597        external_path: Option<&'a ExternalProbePath<'a>>,
3598    }
3599    struct DiscoveryState<'a> {
3600        path: [Option<(usize, NodeId)>; crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING + 1],
3601        work: usize,
3602        materialization: Option<&'a mut KeyInfoMaterializationState>,
3603    }
3604    fn visit<P: KeyInfoReferencePolicy>(
3605        node: Node<'_, '_>,
3606        resolver: &UriReferenceResolver<'_>,
3607        context: &ProbeContext<'_, P>,
3608        state: &mut DiscoveryState<'_>,
3609        depth: usize,
3610        execution: &TransformExecutionBudget,
3611    ) -> Result<bool, DsigError> {
3612        let ProbeContext {
3613            resources,
3614            external,
3615            document_base,
3616            external_path,
3617        } = *context;
3618        resources.validate_key_info_reference_depth(depth)?;
3619        if depth > crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING {
3620            return Err(DsigError::InvalidStructure {
3621                reason: "KeyInfoReference safety depth exceeded",
3622            });
3623        }
3624        // Every document on the active stack remains alive for this traversal;
3625        // arena identity distinguishes equal node IDs in different documents.
3626        let identity = (std::ptr::from_ref(node.document()) as usize, node.id());
3627        if state.path[..depth].contains(&Some(identity)) {
3628            if state.materialization.is_some() {
3629                return Err(DsigError::InvalidStructure {
3630                    reason: "KeyInfoReference cycle detected",
3631                });
3632            }
3633            return Ok(false);
3634        }
3635        state.path[depth] = Some(identity);
3636        let mut found = false;
3637        for child in node.children().filter(Node::is_element) {
3638            let work = match state.materialization.as_deref_mut() {
3639                Some(materialization) => &mut materialization.candidate_work,
3640                None => &mut state.work,
3641            };
3642            *work += 1;
3643            resources.validate_key_candidates(*work)?;
3644            if child.has_tag_name((
3645                crate::key_establishment::ENCAPSULATION_NS,
3646                "EncapsulationMechanism",
3647            )) {
3648                if state.materialization.is_none() {
3649                    return Ok(true);
3650                }
3651                if let Some(materialization) = state.materialization.as_deref_mut() {
3652                    let mechanism = crate::key_establishment::parse_encapsulation(
3653                        child,
3654                        materialization.encapsulation_template,
3655                    )
3656                    .map_err(|_| DsigError::InvalidStructure {
3657                        reason: "invalid encapsulation mechanism in resolved KeyInfo",
3658                    })?;
3659                    materialization.record_encapsulation(mechanism, resolver, depth)?;
3660                    if let (Some(mechanism), Some((policy, _))) =
3661                        (&materialization.encapsulation, external)
3662                    {
3663                        policy
3664                            .key_establishment()
3665                            .check_encapsulation(mechanism.algorithm)?;
3666                    }
3667                }
3668                found = true;
3669                continue;
3670            }
3671            if !child.has_tag_name(("http://www.w3.org/2009/xmldsig11#", "KeyInfoReference")) {
3672                continue;
3673            }
3674            let Some(uri) = child.attribute("URI") else {
3675                continue;
3676            };
3677            if uri.len() > super::parse::MAX_KEY_NAME_TEXT_LEN {
3678                continue;
3679            }
3680            if state.materialization.is_some()
3681                && let Some((policy, _)) = external
3682                && !policy.key_info_reference_source_enabled()
3683            {
3684                return Err(crate::policy::PolicyViolation::KeyTrust {
3685                    reason: "KeyInfoReference key sources are disabled",
3686                }
3687                .into());
3688            }
3689            if !uri.is_empty() && !uri.starts_with('#') {
3690                let Some((policy, backend)) = external else {
3691                    continue;
3692                };
3693                // XMLDSig 1.1 section 4.5.10 gives KeyInfoReference the same URI
3694                // dereferencing behavior as Reference, including external XML.
3695                // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-KeyInfoReference
3696                let resolved =
3697                    crate::c14n::xml_base::resolve_uri_from_node_with_document_base_with_budget(
3698                        child,
3699                        uri,
3700                        document_base,
3701                        execution.xml_base_resolution(),
3702                    )
3703                    .map_err(|error| {
3704                        ReferenceProcessingError::UriDereference(
3705                            super::uri::map_xml_base_resolution_error(error),
3706                        )
3707                    })?;
3708                if !policy.key_info_reference_uris().allows(&resolved) {
3709                    if state.materialization.is_some() {
3710                        return Err(crate::policy::PolicyViolation::Uri {
3711                            operation: "KeyInfoReference",
3712                            reason: "URI class is disabled",
3713                        }
3714                        .into());
3715                    }
3716                    continue;
3717                }
3718                // Resource identity, not a reparsed arena ID, detects external
3719                // cycles. The active URI chain borrows stack frames, no clones.
3720                let mut ancestor = external_path;
3721                while let Some(entry) = ancestor {
3722                    if entry.uri == resolved {
3723                        break;
3724                    }
3725                    ancestor = entry.parent;
3726                }
3727                if ancestor.is_some() {
3728                    if state.materialization.is_some() {
3729                        return Err(DsigError::InvalidStructure {
3730                            reason: "KeyInfoReference cycle detected",
3731                        });
3732                    }
3733                    continue;
3734                }
3735                let external_entry = ExternalProbePath {
3736                    uri: &resolved,
3737                    parent: external_path,
3738                };
3739                let (resource, fragment) = resolved
3740                    .split_once('#')
3741                    .map_or((resolved.as_str(), None), |(resource, fragment)| {
3742                        (resource, Some(fragment))
3743                    });
3744                resources.validate_key_info_reference_depth(depth + 1)?;
3745                if depth == crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING {
3746                    return Err(DsigError::InvalidStructure {
3747                        reason: "KeyInfoReference safety depth exceeded",
3748                    });
3749                }
3750                let bytes = match resolver.external_resource(resource) {
3751                    Ok(Some(bytes)) => bytes,
3752                    Err(TransformError::Policy(violation)) => {
3753                        return Err(DsigError::Policy(violation));
3754                    }
3755                    _ => continue,
3756                };
3757                execution.xml_parse_work().charge_policy(bytes.len())?;
3758                let xml =
3759                    crate::encoding::decode_xml_octets(bytes, resources.max_xml_document_bytes)
3760                        .map_err(map_key_info_xml_decode_error)?;
3761                let settings = DocumentParseSettings::from_policy(policy.xml(), resources)
3762                    .with_backend(backend);
3763                let document = XmlDocument::parse_with_settings_and_budget(
3764                    xml.into_owned(),
3765                    settings,
3766                    execution.xml_parse_work(),
3767                )
3768                .map_err(|error| map_document_parse_error(error, settings))?;
3769                let target_found = document.with_view(|view| {
3770                    let external_resolver = resolver.for_external_document_view(view, resource);
3771                    let target = match fragment {
3772                        Some(fragment) if !fragment.is_empty() => external_resolver
3773                            .node_for_same_document_reference_with_budget(
3774                                &resolved[resource.len()..],
3775                                Some(execution.node_set_materialization()),
3776                            )
3777                            .map_err(ReferenceProcessingError::UriDereference)?,
3778                        _ => Some(view.document().root_element()),
3779                    };
3780                    let Some(target) = target else {
3781                        return Ok(false);
3782                    };
3783                    if !target.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
3784                        return Ok(false);
3785                    }
3786                    visit(
3787                        target,
3788                        &external_resolver,
3789                        &ProbeContext {
3790                            document_base: Some(resource),
3791                            external_path: Some(&external_entry),
3792                            resources,
3793                            external,
3794                        },
3795                        state,
3796                        depth + 1,
3797                        execution,
3798                    )
3799                })?;
3800                if target_found {
3801                    if state.materialization.is_none() {
3802                        return Ok(true);
3803                    }
3804                    found = true;
3805                }
3806                continue;
3807            }
3808            // Unconsumed key hints are not validity requirements. Mechanism
3809            // discovery checks only reachable KeyInfo instructions, not key
3810            // candidates that an explicitly supplied recipient never uses.
3811            // The target type is defined by XMLDSig 1.1 section 4.5.10.
3812            // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-KeyInfoReference
3813            if state.materialization.is_some()
3814                && let Some((policy, _)) = external
3815                && !policy.key_info_reference_uris().allows(uri)
3816            {
3817                return Err(crate::policy::PolicyViolation::Uri {
3818                    operation: "KeyInfoReference",
3819                    reason: "URI class is disabled",
3820                }
3821                .into());
3822            }
3823            let target = match resolver.node_for_same_document_reference_with_budget(
3824                uri,
3825                Some(execution.node_set_materialization()),
3826            ) {
3827                Ok(Some(target)) => target,
3828                Err(TransformError::Policy(violation)) => return Err(DsigError::Policy(violation)),
3829                _ => continue,
3830            };
3831            if target.has_tag_name((XMLDSIG_NS, "KeyInfo"))
3832                && visit(target, resolver, context, state, depth + 1, execution)?
3833            {
3834                if state.materialization.is_none() {
3835                    return Ok(true);
3836                }
3837                found = true;
3838            }
3839        }
3840        Ok(found)
3841    }
3842    visit(
3843        node,
3844        resolver,
3845        &ProbeContext {
3846            resources,
3847            external,
3848            document_base: None,
3849            external_path: None,
3850        },
3851        &mut DiscoveryState {
3852            path: [None; crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING + 1],
3853            work: 0,
3854            materialization,
3855        },
3856        0,
3857        execution,
3858    )
3859}
3860
3861fn inspect_signature_key_info_with_budgets<P: KeyInfoReferencePolicy>(
3862    node: Node<'_, '_>,
3863    resolver: &UriReferenceResolver<'_>,
3864    policy: &P,
3865    provider: &dyn crate::provider::CryptoProvider,
3866    budgets: &mut RetrievalMaterializationBudgets<'_>,
3867    template: bool,
3868    retain_recipient_hints: bool,
3869) -> Result<SignatureKeyInfoMetadata, DsigError> {
3870    if resolver.node_for_node_id(node.id()) != Some(node) {
3871        return Err(DsigError::InvalidStructure {
3872            reason: "KeyInfo and resolver belong to different documents",
3873        });
3874    }
3875    let mut materialization = KeyInfoMaterializationState {
3876        collect_encapsulation: true,
3877        encapsulation_template: template,
3878        retain_encapsulation_target: true,
3879        ..Default::default()
3880    };
3881    materialization.observe_encapsulation(node, resolver, 0)?;
3882    if let Some(mechanism) = &materialization.encapsulation {
3883        policy
3884            .key_establishment()
3885            .check_encapsulation(mechanism.algorithm)?;
3886    }
3887    let mut key_info = super::parse::parse_key_info_in_session(
3888        node,
3889        provider,
3890        budgets.execution.xml_base_resolution(),
3891        policy.resources(),
3892        None,
3893        policy.certificate_signature_algorithms(),
3894        &mut budgets.key_info_parse.borrow_mut(),
3895    )
3896    .map_err(map_key_info_parse_error)?;
3897    materialize_key_info_references_with_budgets(
3898        &mut key_info,
3899        resolver,
3900        policy,
3901        provider,
3902        budgets,
3903        &mut materialization,
3904    )?;
3905    let encapsulation = materialization
3906        .encapsulation
3907        .as_ref()
3908        .map(|value| value.algorithm);
3909    let cipher_value =
3910        if let Some((resource, cipher, recipient)) = materialization.encapsulation_target.take() {
3911            // Product mutation boundary: immutable external resources cannot be
3912            // updated by signing, and foreign node IDs must not address this DOM.
3913            if resource != resolver.traversal_document_identity() {
3914                return Err(DsigError::InvalidStructure {
3915                    reason: "recipient metadata requires its original document context",
3916                });
3917            }
3918            if retain_recipient_hints {
3919                let recipient =
3920                    resolver
3921                        .node_for_node_id(recipient)
3922                        .ok_or(DsigError::InvalidStructure {
3923                            reason: "recipient KeyInfo target is unavailable",
3924                        })?;
3925                key_info = super::parse::parse_key_info_in_session(
3926                    recipient,
3927                    provider,
3928                    budgets.execution.xml_base_resolution(),
3929                    policy.resources(),
3930                    None,
3931                    policy.certificate_signature_algorithms(),
3932                    &mut budgets.key_info_parse.borrow_mut(),
3933                )
3934                .map_err(map_key_info_parse_error)?;
3935                // Recipient references use the same cumulative candidate, parse and
3936                // external-resource allowances as the containing mechanism graph.
3937                materialization.reference_depth_base = materialization.encapsulation_depth;
3938                materialize_key_info_references_with_budgets(
3939                    &mut key_info,
3940                    resolver,
3941                    policy,
3942                    provider,
3943                    budgets,
3944                    &mut materialization,
3945                )?;
3946            }
3947            Some(cipher)
3948        } else {
3949            None
3950        };
3951    Ok(SignatureKeyInfoMetadata {
3952        key_info,
3953        encapsulation,
3954        cipher_value,
3955    })
3956}
3957
3958pub(crate) fn inspect_signing_key_info_with_budgets(
3959    node: Node<'_, '_>,
3960    resolver: &UriReferenceResolver<'_>,
3961    policy: &crate::policy::SigningPolicy,
3962    provider: &dyn crate::provider::CryptoProvider,
3963    shared_budgets: (&TransformExecutionBudget, &mut XPathSignatureParseBudget),
3964    xml_backend: crate::XmlBackend,
3965) -> Result<SignatureKeyInfoMetadata, DsigError> {
3966    inspect_signature_key_info_with_budgets(
3967        node,
3968        resolver,
3969        policy,
3970        provider,
3971        &mut RetrievalMaterializationBudgets {
3972            key_info_parse: &RefCell::new(super::parse::KeyInfoParseUsage::new(&policy.resources)),
3973            xpath_parse: shared_budgets.1,
3974            execution: shared_budgets.0,
3975            resources: &policy.resources,
3976            certificate_signature_algorithms: None,
3977            xml_backend,
3978        },
3979        true,
3980        false,
3981    )
3982}
3983
3984fn inspect_signature_key_info_for_policy<P: KeyInfoReferencePolicy>(
3985    node: Node<'_, '_>,
3986    resolver: UriReferenceResolver<'_>,
3987    policy: &P,
3988    provider: &dyn crate::provider::CryptoProvider,
3989    xml_backend: crate::XmlBackend,
3990    template: bool,
3991) -> Result<SignatureKeyInfoMetadata, DsigError> {
3992    let resolver = resolver.with_external_resource_limits(
3993        policy.resources().max_external_resource_bytes,
3994        policy.resources().max_external_resource_total_bytes,
3995    );
3996    let execution = TransformExecutionBudget::from_resources(policy.resources());
3997    let mut xpath_parse = XPathSignatureParseBudget::for_execution(policy.resources(), &execution);
3998    inspect_signature_key_info_with_budgets(
3999        node,
4000        &resolver,
4001        policy,
4002        provider,
4003        &mut RetrievalMaterializationBudgets {
4004            key_info_parse: &RefCell::new(super::parse::KeyInfoParseUsage::new(policy.resources())),
4005            xpath_parse: &mut xpath_parse,
4006            execution: &execution,
4007            resources: policy.resources(),
4008            certificate_signature_algorithms: policy.certificate_signature_algorithms(),
4009            xml_backend,
4010        },
4011        template,
4012        true,
4013    )
4014}
4015
4016/// Inspect signing hints and KEM targets through the bounded KeyInfoReference
4017/// graph. KEM targets must belong to the document that will be mutated.
4018pub fn inspect_signing_key_info(
4019    node: Node<'_, '_>,
4020    resolver: UriReferenceResolver<'_>,
4021    policy: &crate::policy::SigningPolicy,
4022    provider: &dyn crate::provider::CryptoProvider,
4023    xml_backend: crate::XmlBackend,
4024) -> Result<SignatureKeyInfoMetadata, DsigError> {
4025    policy.validate()?;
4026    inspect_signature_key_info_for_policy(node, resolver, policy, provider, xml_backend, true)
4027}
4028
4029/// Inspect verification hints and KEM metadata with the same source, URI,
4030/// candidate, depth and cycle gates as core verification.
4031/// This original-document metadata API requires KEM targets in that document;
4032/// core verification also supports caller-supplied external KEM resources.
4033pub fn inspect_verification_key_info(
4034    node: Node<'_, '_>,
4035    resolver: UriReferenceResolver<'_>,
4036    policy: &crate::policy::VerificationPolicy,
4037    provider: &dyn crate::provider::CryptoProvider,
4038    xml_backend: crate::XmlBackend,
4039) -> Result<SignatureKeyInfoMetadata, DsigError> {
4040    policy.validate()?;
4041    inspect_signature_key_info_for_policy(node, resolver, policy, provider, xml_backend, false)
4042}
4043
4044/// Inspect operation instructions for a caller-pinned key, leaving unrelated
4045/// lookup hints unconsumed. Discovery and materialization share one allowance.
4046/// Like `inspect_verification_key_info`, this original-document metadata API
4047/// rejects external KEM targets; it never reports a permitted external mechanism
4048/// as absent. Use core verification with a decapsulation key for those targets.
4049pub fn inspect_pinned_verification_key_info(
4050    node: Node<'_, '_>,
4051    resolver: UriReferenceResolver<'_>,
4052    policy: &crate::policy::VerificationPolicy,
4053    provider: &dyn crate::provider::CryptoProvider,
4054    xml_backend: crate::XmlBackend,
4055) -> Result<Option<SignatureKeyInfoMetadata>, DsigError> {
4056    policy.validate()?;
4057    let resolver = resolver.with_external_resource_limits(
4058        policy.resources.max_external_resource_bytes,
4059        policy.resources.max_external_resource_total_bytes,
4060    );
4061    let execution = TransformExecutionBudget::from_resources(&policy.resources);
4062    if !has_encapsulation_with_budget(
4063        node,
4064        &resolver,
4065        &policy.resources,
4066        &execution,
4067        Some((policy, xml_backend)),
4068    )? {
4069        return Ok(None);
4070    }
4071    let mut xpath_parse = XPathSignatureParseBudget::for_execution(&policy.resources, &execution);
4072    inspect_signature_key_info_with_budgets(
4073        node,
4074        &resolver,
4075        policy,
4076        provider,
4077        &mut RetrievalMaterializationBudgets {
4078            key_info_parse: &RefCell::new(super::parse::KeyInfoParseUsage::new(&policy.resources)),
4079            xpath_parse: &mut xpath_parse,
4080            execution: &execution,
4081            resources: &policy.resources,
4082            certificate_signature_algorithms: policy.certificate_signature_algorithms(),
4083            xml_backend,
4084        },
4085        false,
4086        true,
4087    )
4088    .map(Some)
4089}
4090
4091fn materialize_key_info_references_for_policy<P: KeyInfoReferencePolicy>(
4092    key_info: &mut KeyInfo,
4093    resolver: UriReferenceResolver<'_>,
4094    policy: &P,
4095    provider: &dyn crate::provider::CryptoProvider,
4096    xml_backend: crate::XmlBackend,
4097) -> Result<(), DsigError> {
4098    let resolver = resolver.with_external_resource_limits(
4099        policy.resources().max_external_resource_bytes,
4100        policy.resources().max_external_resource_total_bytes,
4101    );
4102    let execution_budget = TransformExecutionBudget::from_resources(policy.resources());
4103    let mut xpath_parse_budget =
4104        XPathSignatureParseBudget::for_execution(policy.resources(), &execution_budget);
4105    let mut budgets = RetrievalMaterializationBudgets {
4106        key_info_parse: &RefCell::new(super::parse::KeyInfoParseUsage::new(policy.resources())),
4107        xpath_parse: &mut xpath_parse_budget,
4108        execution: &execution_budget,
4109        resources: policy.resources(),
4110        certificate_signature_algorithms: policy.certificate_signature_algorithms(),
4111        xml_backend,
4112    };
4113    let mut materialization = KeyInfoMaterializationState::default();
4114    // Candidate-local retrieval failures remain deferred: this metadata-only
4115    // entry point cannot know whether a later key-selection stage needs them.
4116    materialize_key_info_references_with_budgets(
4117        key_info,
4118        &resolver,
4119        policy,
4120        provider,
4121        &mut budgets,
4122        &mut materialization,
4123    )?;
4124    Ok(())
4125}
4126
4127/// Resolve and recursively expand policy-allowed `KeyInfoReference` sources
4128/// while preparing caller-supplied key material for signing.
4129///
4130/// The recursive reference traversal shares the verifier's candidate-work,
4131/// depth, cycle, target, XML parsing, and external-resource bounds. Retrieval
4132/// methods nested in an external referenced document are materialized while
4133/// that document's URI context is active; retrieval methods in the caller's
4134/// document remain the responsibility of the complete signing pipeline.
4135/// candidate failures are deferred so an earlier usable key source can still
4136/// be selected; the complete signing pipeline reports the retained failure if
4137/// no source resolves to a key. URI classes remain controlled by the signing
4138/// policy's `uris` field; external
4139/// bytes must be attached to `resolver` explicitly by the caller. The resolver
4140/// is consumed so this operation can bind a fresh aggregate external-resource
4141/// budget to the supplied policy. `xml_backend` is used for every recursively
4142/// referenced XML document, preserving the caller's parser semantics.
4143pub fn materialize_signing_key_info_references(
4144    key_info: &mut KeyInfo,
4145    resolver: UriReferenceResolver<'_>,
4146    policy: &crate::policy::SigningPolicy,
4147    provider: &dyn crate::provider::CryptoProvider,
4148    xml_backend: crate::XmlBackend,
4149) -> Result<(), DsigError> {
4150    policy.validate()?;
4151    materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
4152}
4153
4154/// Resolve and recursively expand policy-allowed `KeyInfoReference` sources
4155/// before verification key selection.
4156///
4157/// In addition to URI and resource policy, this entry point enforces the
4158/// verification policy's `key_sources.key_info_reference` trust gate. Retrieval
4159/// methods nested in an external referenced document are materialized in that
4160/// document's URI context; retrieval methods in the caller's document remain
4161/// the responsibility of the complete verification pipeline. Candidate-local
4162/// retrieval failures remain deferred so usable sibling sources survive; the
4163/// complete pipeline reports one only when key selection has no fallback.
4164/// External bytes must be attached to `resolver` explicitly by the caller. The resolver is
4165/// consumed so this operation can bind a fresh aggregate external-resource
4166/// budget to the supplied policy. `xml_backend` is used for every recursively
4167/// referenced XML document, preserving the caller's parser semantics.
4168pub fn materialize_verification_key_info_references(
4169    key_info: &mut KeyInfo,
4170    resolver: UriReferenceResolver<'_>,
4171    policy: &crate::policy::VerificationPolicy,
4172    provider: &dyn crate::provider::CryptoProvider,
4173    xml_backend: crate::XmlBackend,
4174) -> Result<(), DsigError> {
4175    policy.validate()?;
4176    materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
4177}
4178
4179fn materialize_retrieval_methods_with_budgets<P: KeyInfoReferencePolicy>(
4180    key_info: &mut KeyInfo,
4181    resolver: &UriReferenceResolver<'_>,
4182    policy: &P,
4183    provider: &dyn crate::provider::CryptoProvider,
4184    budgets: &mut RetrievalMaterializationBudgets<'_>,
4185    candidate_work: &mut usize,
4186) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
4187    let allowed_uri_types = policy.retrieval_method_uris();
4188    let allowed_transforms = policy.allowed_transforms();
4189    let retrieval_count = key_info
4190        .sources
4191        .iter()
4192        .filter(|source| matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. }))
4193        .count();
4194    if retrieval_count > MAX_RETRIEVAL_METHOD_COUNT {
4195        return Err(SignatureVerificationPipelineError::InvalidStructure {
4196            reason: "KeyInfo contains too many RetrievalMethod elements",
4197        });
4198    }
4199
4200    let mut total_binary_len = existing_x509_binary_len(key_info)?;
4201    let mut seen = HashSet::new();
4202    let mut materialized = Vec::with_capacity(key_info.sources.len());
4203    let mut outcome = RetrievalMaterialization::default();
4204    for source in std::mem::take(&mut key_info.sources) {
4205        let super::parse::KeyInfoSource::RetrievalMethod {
4206            uri: resolved_uri,
4207            resource_type,
4208            transforms,
4209        } = source
4210        else {
4211            materialized.push(source);
4212            continue;
4213        };
4214
4215        let identity = (
4216            resolved_uri.clone(),
4217            resource_type.clone(),
4218            transforms.clone(),
4219        );
4220        if !seen.insert(identity) {
4221            continue;
4222        }
4223
4224        if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#rawX509Certificate")
4225        {
4226            if transforms != RetrievalMethodTransforms::None
4227                || classify_uri(&resolved_uri) != UriClass::External
4228            {
4229                return Err(SignatureVerificationPipelineError::InvalidStructure {
4230                    reason: "raw X509 RetrievalMethod requires an untransformed external URI",
4231                });
4232            }
4233            if !allowed_uri_types.allows(&resolved_uri) {
4234                return Err(crate::policy::PolicyViolation::Uri {
4235                    operation: "verification",
4236                    reason: "retrieval method URI class is not permitted",
4237                }
4238                .into());
4239            }
4240            let certificate = resolver.external_resource(&resolved_uri).map_err(|error| {
4241                SignatureVerificationPipelineError::from(ReferenceProcessingError::Transform(error))
4242            })?;
4243            let Some(certificate) = certificate else {
4244                outcome.deferred_error.get_or_insert_with(|| {
4245                    SignatureVerificationPipelineError::Reference(
4246                        ReferenceProcessingError::Transform(super::TransformError::UnsupportedUri(
4247                            resolved_uri.clone(),
4248                        )),
4249                    )
4250                });
4251                materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
4252                    uri: resolved_uri,
4253                    resource_type,
4254                    transforms,
4255                });
4256                continue;
4257            };
4258            *candidate_work = candidate_work.saturating_add(1);
4259            budgets.resources.validate_key_candidates(*candidate_work)?;
4260            if certificate.len() > MAX_X509_DECODED_BINARY_LEN {
4261                return Err(SignatureVerificationPipelineError::InvalidStructure {
4262                    reason: "raw X509 RetrievalMethod certificate exceeds maximum allowed length",
4263                });
4264            }
4265            add_retrieval_binary_usage(&mut total_binary_len, certificate.len())?;
4266            let parsed = match super::parse::parse_x509_certificate_in(
4267                certificate,
4268                &budgets.key_info_parse.borrow().certificates,
4269            ) {
4270                Ok(parsed) => parsed,
4271                Err(error) => {
4272                    let error = map_key_info_parse_error(error);
4273                    if matches!(error, SignatureVerificationPipelineError::Policy(_)) {
4274                        return Err(error);
4275                    }
4276                    outcome.deferred_error.get_or_insert(error);
4277                    materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
4278                        uri: resolved_uri,
4279                        resource_type,
4280                        transforms,
4281                    });
4282                    continue;
4283                }
4284            };
4285            materialized.push(super::parse::KeyInfoSource::X509Data(
4286                super::parse::X509DataInfo {
4287                    certificates: vec![certificate.to_vec()],
4288                    parsed_certificates: vec![parsed],
4289                    certificate_chain: vec![0],
4290                    ..super::parse::X509DataInfo::default()
4291                },
4292            ));
4293        } else if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#X509Data") {
4294            if !allowed_uri_types.allows(&resolved_uri) {
4295                return Err(crate::policy::PolicyViolation::Uri {
4296                    operation: "verification",
4297                    reason: "retrieval method URI class is not permitted",
4298                }
4299                .into());
4300            }
4301            let target = resolver
4302                .node_for_same_document_reference(&resolved_uri)
4303                .map_err(ReferenceProcessingError::Transform)?;
4304            let Some(target) = target else {
4305                outcome.deferred_error.get_or_insert(
4306                    SignatureVerificationPipelineError::InvalidStructure {
4307                        reason: "X509Data RetrievalMethod target is missing or ambiguous",
4308                    },
4309                );
4310                materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
4311                    uri: resolved_uri,
4312                    resource_type,
4313                    transforms,
4314                });
4315                continue;
4316            };
4317            let node = match transforms {
4318                RetrievalMethodTransforms::None
4319                    if target.has_tag_name((XMLDSIG_NS, "X509Data")) =>
4320                {
4321                    target
4322                }
4323                RetrievalMethodTransforms::None => {
4324                    return Err(SignatureVerificationPipelineError::InvalidStructure {
4325                        reason: "untransformed X509Data RetrievalMethod must target X509Data directly",
4326                    });
4327                }
4328                RetrievalMethodTransforms::X509DataNodeSetFilter {
4329                    expression,
4330                    namespaces,
4331                } => {
4332                    enforce_transform_allowed(allowed_transforms, XPATH_TRANSFORM_URI)?;
4333                    budgets
4334                        .xpath_parse
4335                        .validate_expression(&expression)
4336                        .map_err(ReferenceProcessingError::Transform)?;
4337                    budgets
4338                        .xpath_parse
4339                        .validate_namespaces(&namespaces)
4340                        .map_err(ReferenceProcessingError::Transform)?;
4341                    select_retrieved_x509_data_root(target, budgets.execution)?
4342                }
4343                RetrievalMethodTransforms::Unsupported => {
4344                    return Err(SignatureVerificationPipelineError::InvalidStructure {
4345                        reason: "X509Data RetrievalMethod contains unsupported transforms",
4346                    });
4347                }
4348            };
4349            super::parse::validate_x509_digest_policy(node, &|algorithm| {
4350                policy.check_digest_algorithm(algorithm)
4351            })
4352            .map_err(map_key_info_parse_error)?;
4353            let data = parse_x509_data_dispatch_with_budget_and_provider(
4354                node,
4355                &mut total_binary_len,
4356                candidate_work,
4357                provider,
4358                budgets.resources,
4359                budgets.certificate_signature_algorithms,
4360                &budgets.key_info_parse.borrow().certificates,
4361            )
4362            .map_err(map_key_info_parse_error)?;
4363            materialized.push(super::parse::KeyInfoSource::X509Data(data));
4364        } else {
4365            materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
4366                uri: resolved_uri,
4367                resource_type,
4368                transforms,
4369            });
4370        }
4371    }
4372    key_info.sources = materialized;
4373    Ok(outcome)
4374}
4375
4376fn select_retrieved_x509_data_root<'a, 'input>(
4377    target: Node<'a, 'input>,
4378    execution_budget: &TransformExecutionBudget,
4379) -> Result<Node<'a, 'input>, SignatureVerificationPipelineError> {
4380    // XMLDSig XPath filtering evaluates the predicate for every node in the
4381    // dereferenced node-set, including attribute and namespace nodes. The
4382    // element-only scan below selects X509Data but must not undercount that
4383    // XPath context cardinality.
4384    let context_nodes = NodeSet::ensure_subtree_materialization_fits_with_budget(
4385        target,
4386        false,
4387        execution_budget.node_set_materialization(),
4388    )
4389    .map_err(ReferenceProcessingError::Transform)?;
4390    execution_budget
4391        .validate_xpath_context_evaluations(context_nodes)
4392        .map_err(ReferenceProcessingError::Transform)?;
4393    execution_budget
4394        .charge_xpath_work(context_nodes)
4395        .map_err(ReferenceProcessingError::Transform)?;
4396    execution_budget
4397        .charge_node_filter_work(context_nodes)
4398        .map_err(ReferenceProcessingError::Transform)?;
4399    let mut root = None;
4400    for candidate in target.descendants() {
4401        if !candidate.is_element()
4402            || candidate.tag_name().namespace() != Some(XMLDSIG_NS)
4403            || candidate.tag_name().name() != "X509Data"
4404        {
4405            continue;
4406        }
4407        if root.replace(candidate).is_some() {
4408            return Err(SignatureVerificationPipelineError::InvalidStructure {
4409                reason: "X509Data RetrievalMethod selected multiple X509Data elements",
4410            });
4411        }
4412    }
4413    root.ok_or(SignatureVerificationPipelineError::InvalidStructure {
4414        reason: "X509Data RetrievalMethod selected no X509Data element",
4415    })
4416}
4417
4418#[cfg(test)]
4419fn materialize_retrieval_methods(
4420    key_info: &mut KeyInfo,
4421    resolver: &UriReferenceResolver<'_>,
4422    allowed_uri_types: UriTypeSet,
4423    allowed_transforms: Option<&HashSet<String>>,
4424    provider: &dyn crate::provider::CryptoProvider,
4425) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
4426    let mut xpath_parse_budget = XPathSignatureParseBudget::default();
4427    let execution_budget = TransformExecutionBudget::default();
4428    let resources = crate::policy::ResourcePolicy::default();
4429    let mut budgets = RetrievalMaterializationBudgets {
4430        key_info_parse: &RefCell::new(super::parse::KeyInfoParseUsage::new(&resources)),
4431        xpath_parse: &mut xpath_parse_budget,
4432        execution: &execution_budget,
4433        resources: &resources,
4434        certificate_signature_algorithms: None,
4435        xml_backend: crate::XmlBackend::default(),
4436    };
4437    let mut candidate_work = key_info.embedded_candidate_count();
4438    let mut policy = crate::policy::VerificationPolicy::default();
4439    policy.uris.retrieval_methods = allowed_uri_types;
4440    policy.transforms.allowed_algorithms = allowed_transforms.cloned();
4441    materialize_retrieval_methods_with_budgets(
4442        key_info,
4443        resolver,
4444        &policy,
4445        provider,
4446        &mut budgets,
4447        &mut candidate_work,
4448    )
4449}
4450
4451fn existing_x509_binary_len(
4452    key_info: &KeyInfo,
4453) -> Result<usize, SignatureVerificationPipelineError> {
4454    let mut total = 0usize;
4455    for source in &key_info.sources {
4456        if let super::parse::KeyInfoSource::X509Data(info) = source {
4457            for len in info
4458                .certificates
4459                .iter()
4460                .chain(&info.skis)
4461                .chain(&info.crls)
4462                .map(Vec::len)
4463                .chain(info.digests.iter().map(|(_, digest)| digest.len()))
4464            {
4465                add_retrieval_binary_usage(&mut total, len)?;
4466            }
4467        }
4468    }
4469    Ok(total)
4470}
4471
4472fn add_retrieval_binary_usage(
4473    total: &mut usize,
4474    delta: usize,
4475) -> Result<(), SignatureVerificationPipelineError> {
4476    *total =
4477        total
4478            .checked_add(delta)
4479            .ok_or(SignatureVerificationPipelineError::InvalidStructure {
4480                reason: "RetrievalMethod X509Data binary length overflow",
4481            })?;
4482    if *total > MAX_X509_DATA_TOTAL_BINARY_LEN {
4483        return Err(SignatureVerificationPipelineError::InvalidStructure {
4484            reason: "RetrievalMethod X509Data exceeds maximum aggregate binary length",
4485        });
4486    }
4487    Ok(())
4488}
4489
4490fn manifest_reference_failure_reason(
4491    error: ReferenceProcessingError,
4492    ref_index: usize,
4493) -> FailureReason {
4494    match error.into_policy_violation() {
4495        Ok(_) => FailureReason::ReferencePolicyViolation { ref_index },
4496        Err(_) => FailureReason::ReferenceProcessingFailure { ref_index },
4497    }
4498}
4499
4500struct CompiledManifestReference {
4501    index: usize,
4502    reference: Reference,
4503    reference_node_id: NodeId,
4504}
4505
4506struct CompiledManifestInvalid {
4507    result: ReferenceResult,
4508}
4509
4510enum ScheduledManifestReference {
4511    Parsed {
4512        node: OperationNodeId,
4513        reference: CompiledManifestReference,
4514    },
4515    Invalid {
4516        node: OperationNodeId,
4517        invalid: CompiledManifestInvalid,
4518    },
4519}
4520
4521impl ScheduledManifestReference {
4522    fn index(&self) -> usize {
4523        match self {
4524            Self::Parsed { reference, .. } => reference.index,
4525            Self::Invalid { invalid, .. } => invalid.result.reference_index,
4526        }
4527    }
4528}
4529
4530struct ManifestVerification<'context, 'inputs, C> {
4531    context: &'context VerifyContext<'inputs>,
4532    capture: &'context C,
4533}
4534
4535fn process_authenticated_manifest_references<C: VerificationCapture>(
4536    operation: &mut OperationExecutionContext<
4537        &crate::policy::VerificationPolicy,
4538        &VerificationOperationBudgets,
4539    >,
4540    view: DocumentView<'_>,
4541    signature_node: Node<'_, '_>,
4542    resolver: &UriReferenceResolver<'_>,
4543    verification: ManifestVerification<'_, '_, C>,
4544    mut remaining_reference_capacity: usize,
4545    crypto: OperationNodeId,
4546) -> Result<(Vec<ReferenceResult>, Vec<OperationNodeId>), SignatureVerificationPipelineError> {
4547    let ctx = verification.context;
4548    let capture = verification.capture;
4549    let mut processed_manifests = HashSet::new();
4550    let mut next_reference_index = 0usize;
4551    let mut frontier_index = 0usize;
4552    let mut completed_digests = Vec::new();
4553    let mut results = Vec::new();
4554    loop {
4555        operation.extend();
4556        let discovery = operation.add_node(
4557            OperationNodeKind::Manifest {
4558                index: frontier_index,
4559            },
4560            OperationStage::AuthenticatedDependency,
4561            None,
4562        );
4563        operation
4564            .add_dependency(discovery, crypto)
4565            .map_err(map_verification_plan_error)?;
4566        for digest in &completed_digests {
4567            operation
4568                .add_dependency(discovery, *digest)
4569                .map_err(map_verification_plan_error)?;
4570        }
4571        operation.compile().map_err(map_verification_plan_error)?;
4572        let parsed = operation.run(discovery, || {
4573            let mut xpath_parse = operation.budgets().xpath_parse.borrow_mut();
4574            let mut state = ManifestDiscoveryState {
4575                processed: &mut processed_manifests,
4576                remaining_capacity: &mut remaining_reference_capacity,
4577                next_reference_index: &mut next_reference_index,
4578                xpath_parse: &mut xpath_parse,
4579            };
4580            parse_manifest_references(
4581                signature_node,
4582                operation,
4583                view,
4584                &mut state,
4585                ctx.allowed_transform_uris(),
4586            )
4587        })?;
4588        let discovered = parsed.references.len() + parsed.invalid_results.len();
4589        if discovered == 0 {
4590            break;
4591        }
4592
4593        operation.extend();
4594        let mut scheduled = Vec::with_capacity(discovered);
4595        for item in parsed.references {
4596            let resource = operation.budgets().resource_identity_for_reference(
4597                &item.reference,
4598                item.index,
4599                resolver,
4600                view,
4601                resolver.node_for_node_id(item.reference_node_id),
4602            );
4603            let node = operation.add_node(
4604                OperationNodeKind::Digest { index: item.index },
4605                OperationStage::AuthenticatedDependency,
4606                Some(resource.clone()),
4607            );
4608            operation
4609                .add_dependency(node, discovery)
4610                .map_err(map_verification_plan_error)?;
4611            scheduled.push(ScheduledManifestReference::Parsed {
4612                node,
4613                reference: item,
4614            });
4615        }
4616        for item in parsed.invalid_results {
4617            let node = operation.add_node(
4618                OperationNodeKind::Digest {
4619                    index: item.result.reference_index,
4620                },
4621                OperationStage::AuthenticatedDependency,
4622                None,
4623            );
4624            operation
4625                .add_dependency(node, discovery)
4626                .map_err(map_verification_plan_error)?;
4627            scheduled.push(ScheduledManifestReference::Invalid {
4628                node,
4629                invalid: item,
4630            });
4631        }
4632        scheduled.sort_by_key(ScheduledManifestReference::index);
4633        operation.compile().map_err(map_verification_plan_error)?;
4634
4635        for scheduled_reference in scheduled {
4636            let (node, result) = match scheduled_reference {
4637                ScheduledManifestReference::Invalid { node, invalid } => {
4638                    let result = operation.run(node, || {
4639                        Ok::<_, SignatureVerificationPipelineError>(invalid.result)
4640                    })?;
4641                    (node, result)
4642                }
4643                ScheduledManifestReference::Parsed {
4644                    node,
4645                    reference: compiled,
4646                } => {
4647                    let observed = operation.budgets().resource_identity_for_reference(
4648                        &compiled.reference,
4649                        compiled.index,
4650                        resolver,
4651                        view,
4652                        resolver.node_for_node_id(compiled.reference_node_id),
4653                    );
4654                    let result = operation.run_with_resource(node, &observed, || {
4655                        let reference = &compiled.reference;
4656                        let budgets = operation.budgets();
4657                        let execution = ReferenceExecutionContext {
4658                            store_pre_digest: ctx.store_pre_digest,
4659                            transform_options: ctx.transform_options(),
4660                            transform_budget: &budgets.transforms,
4661                            canonicalized_data_budget: &budgets.canonicalized,
4662                            provider: ctx.provider,
4663                            capture,
4664                        };
4665                        let mut result = if execution.transform_budget.remaining_c14n_output() == 0
4666                            || super::transforms::validate_relationship_chain(
4667                                &reference.transforms,
4668                                ctx.policy.transforms.opc_relationship_edition,
4669                            )
4670                            .is_err()
4671                            || reference.transforms.len()
4672                                > ctx.policy.resources.max_transforms_per_reference
4673                            || ctx
4674                                .policy
4675                                .check_digest_algorithm(reference.digest_method)
4676                                .is_err()
4677                        {
4678                            manifest_reference_invalid_result(
4679                                reference,
4680                                compiled.index,
4681                                FailureReason::ReferencePolicyViolation {
4682                                    ref_index: compiled.index,
4683                                },
4684                            )
4685                        } else {
4686                            match enforce_reference_policies(
4687                                std::slice::from_ref(reference),
4688                                ctx.policy.uris.references,
4689                                ctx.allowed_transform_uris(),
4690                            ) {
4691                                Ok(()) => process_reference_with_options(
4692                                    reference,
4693                                    resolver,
4694                                    signature_node,
4695                                    ReferenceSet::Manifest,
4696                                    compiled.index,
4697                                    resolver.node_for_node_id(compiled.reference_node_id),
4698                                    &execution,
4699                                )
4700                                .unwrap_or_else(|error| {
4701                                    manifest_reference_invalid_result(
4702                                        reference,
4703                                        compiled.index,
4704                                        manifest_reference_failure_reason(error, compiled.index),
4705                                    )
4706                                }),
4707                                Err(SignatureVerificationPipelineError::Policy(_)) => {
4708                                    manifest_reference_invalid_result(
4709                                        reference,
4710                                        compiled.index,
4711                                        FailureReason::ReferencePolicyViolation {
4712                                            ref_index: compiled.index,
4713                                        },
4714                                    )
4715                                }
4716                                Err(_) => manifest_reference_invalid_result(
4717                                    reference,
4718                                    compiled.index,
4719                                    FailureReason::ReferenceProcessingFailure {
4720                                        ref_index: compiled.index,
4721                                    },
4722                                ),
4723                            }
4724                        };
4725                        bind_reference_evidence(
4726                            &mut result,
4727                            reference,
4728                            view,
4729                            resolver.node_for_node_id(compiled.reference_node_id),
4730                            &observed,
4731                        );
4732                        if result.status == DsigStatus::Valid
4733                            && reference
4734                                .transforms
4735                                .iter()
4736                                .all(transform_preserves_manifest_structure)
4737                            && let Some(uri) = reference.uri.as_deref()
4738                            && let Ok(Some(target)) =
4739                                resolver.node_id_for_same_document_reference(uri)
4740                        {
4741                            operation.authenticate(view.node_identity_by_id(target));
4742                        }
4743                        Ok::<_, SignatureVerificationPipelineError>(result)
4744                    })?;
4745                    (node, result)
4746                }
4747            };
4748            let accepted = result.status == DsigStatus::Valid;
4749            operation.set_outcome(
4750                node,
4751                accepted,
4752                if accepted {
4753                    OperationDecisionReason::ReferenceDigestVerified
4754                } else {
4755                    OperationDecisionReason::ReferenceDigestRejected
4756                },
4757            );
4758            completed_digests.push(node);
4759            results.push(result);
4760        }
4761        frontier_index += 1;
4762    }
4763    results.sort_by_key(|result| result.reference_index);
4764    Ok((results, completed_digests))
4765}
4766
4767fn manifest_reference_invalid_result(
4768    reference: &Reference,
4769    index: usize,
4770    reason: FailureReason,
4771) -> ReferenceResult {
4772    ReferenceResult {
4773        reference_identity: None,
4774        target_identity: None,
4775        external_resource_fingerprint: None,
4776        coverage: ReferenceCoverage::TransformedData,
4777        reference_set: ReferenceSet::Manifest,
4778        reference_index: index,
4779        uri: reference
4780            .uri
4781            .clone()
4782            .unwrap_or_else(|| "<omitted>".to_owned()),
4783        digest_algorithm: reference.digest_method,
4784        status: DsigStatus::Invalid(reason),
4785        pre_digest_data: None,
4786    }
4787}
4788
4789fn parse_manifest_references(
4790    signature_node: Node<'_, '_>,
4791    operation: &OperationExecutionContext<
4792        &crate::policy::VerificationPolicy,
4793        &VerificationOperationBudgets,
4794    >,
4795    view: DocumentView<'_>,
4796    state: &mut ManifestDiscoveryState<'_>,
4797    allowed_transforms: Option<&HashSet<String>>,
4798) -> Result<ParsedManifestReferences, SignatureVerificationPipelineError> {
4799    let mut references = Vec::new();
4800    let mut invalid = Vec::new();
4801    // XMLDSig 1.1 §5.1 binds a Manifest through references, independently of
4802    // physical Signature/Object ancestry. Use only this operation's existing
4803    // authentication state; another Signature's successes cannot admit it.
4804    // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-Manifest
4805    let document = signature_node.document();
4806    let scan = &operation.budgets().transforms;
4807    for manifest_node in document.descendants() {
4808        scan.charge_node_filter_work(1)
4809            .map_err(ReferenceProcessingError::Transform)?;
4810        if !manifest_node.has_tag_name((XMLDSIG_NS, "Manifest"))
4811            || state.processed.contains(&manifest_node.id())
4812        {
4813            continue;
4814        }
4815        let mut manifest_is_signed = false;
4816        for ancestor in manifest_node.ancestors() {
4817            scan.charge_node_filter_work(1)
4818                .map_err(ReferenceProcessingError::Transform)?;
4819            if operation.is_authenticated(view.node_identity(ancestor)) {
4820                manifest_is_signed = true;
4821                break;
4822            }
4823        }
4824        // Leave unauthenticated Manifests unmarked so a verified outer
4825        // Manifest can make them eligible on the next discovery pass.
4826        if !manifest_is_signed {
4827            continue;
4828        }
4829        if !state.processed.insert(manifest_node.id()) {
4830            continue;
4831        }
4832        let mut manifest_children = Vec::new();
4833        for child in manifest_node.children() {
4834            if child.is_text()
4835                && child.text().is_some_and(|text| {
4836                    text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
4837                })
4838            {
4839                return Err(SignatureVerificationPipelineError::InvalidStructure {
4840                    reason: "Manifest contains non-whitespace mixed content",
4841                });
4842            }
4843            if child.is_element() {
4844                manifest_children.push(child);
4845            }
4846        }
4847        if manifest_children.is_empty() {
4848            return Err(SignatureVerificationPipelineError::InvalidStructure {
4849                reason: "Manifest must contain at least one ds:Reference element child",
4850            });
4851        }
4852        for child in manifest_children {
4853            if child.tag_name().namespace() != Some(XMLDSIG_NS)
4854                || child.tag_name().name() != "Reference"
4855            {
4856                return Err(SignatureVerificationPipelineError::InvalidStructure {
4857                    reason: "Manifest must contain only ds:Reference element children",
4858                });
4859            }
4860            if *state.remaining_capacity == 0 {
4861                return Err(SignatureVerificationPipelineError::InvalidStructure {
4862                    reason: "signed Manifests exceed the per-signature Reference limit",
4863                });
4864            }
4865            *state.remaining_capacity -= 1;
4866            let reference_index = *state.next_reference_index;
4867            *state.next_reference_index += 1;
4868            match parse_reference_with_xpath_budget(child, state.xpath_parse) {
4869                Ok(reference) => references.push(CompiledManifestReference {
4870                    index: reference_index,
4871                    reference,
4872                    reference_node_id: child.id(),
4873                }),
4874                Err(ParseError::Transform(super::TransformError::UnsupportedTransform(uri))) => {
4875                    let digest_algorithm =
4876                        reference_digest_method(child).map_err(map_manifest_parse_error)?;
4877                    let reason =
4878                        if allowed_transforms.is_some_and(|allowed| !allowed.contains(&uri)) {
4879                            FailureReason::ReferencePolicyViolation {
4880                                ref_index: reference_index,
4881                            }
4882                        } else {
4883                            FailureReason::ReferenceProcessingFailure {
4884                                ref_index: reference_index,
4885                            }
4886                        };
4887                    invalid.push(CompiledManifestInvalid {
4888                        result: ReferenceResult {
4889                            reference_identity: Some(view.node_identity(child)),
4890                            target_identity: None,
4891                            external_resource_fingerprint: None,
4892                            coverage: ReferenceCoverage::TransformedData,
4893                            reference_set: ReferenceSet::Manifest,
4894                            reference_index,
4895                            uri: child.attribute("URI").unwrap_or("<omitted>").to_owned(),
4896                            digest_algorithm,
4897                            status: DsigStatus::Invalid(reason),
4898                            pre_digest_data: None,
4899                        },
4900                    });
4901                }
4902                Err(error) => return Err(map_manifest_parse_error(error)),
4903            }
4904        }
4905    }
4906    Ok(ParsedManifestReferences {
4907        references,
4908        invalid_results: invalid,
4909    })
4910}
4911
4912struct ParsedManifestReferences {
4913    references: Vec<CompiledManifestReference>,
4914    invalid_results: Vec<CompiledManifestInvalid>,
4915}
4916
4917struct ManifestDiscoveryState<'a> {
4918    processed: &'a mut HashSet<NodeId>,
4919    remaining_capacity: &'a mut usize,
4920    next_reference_index: &'a mut usize,
4921    xpath_parse: &'a mut XPathSignatureParseBudget,
4922}
4923
4924fn transform_preserves_manifest_structure(transform: &Transform) -> bool {
4925    match transform {
4926        Transform::C14n(_) => true,
4927        // Both eligible ID targets are descendants of the owning Signature.
4928        // Enveloped subtraction therefore removes their intersection with that
4929        // Signature subtree, even though the Signature node itself is not in the
4930        // dereferenced node set.
4931        Transform::Enveloped
4932        | Transform::XpathExcludeAllSignatures
4933        | Transform::XPath(_)
4934        | Transform::XPathFilter2(_)
4935        | Transform::Base64Decode
4936        | Transform::Relationship(_) => false,
4937        #[cfg(feature = "xslt")]
4938        Transform::Xslt(_) => false,
4939    }
4940}
4941
4942#[expect(
4943    clippy::large_enum_variant,
4944    reason = "single bounded authorization result avoids heap allocation"
4945)]
4946enum ResolvedVerifyingKey<'a> {
4947    Established(super::HmacVerificationKey),
4948    Borrowed(&'a dyn VerifyingKey),
4949    Owned(super::ResolvedVerificationKey<'a>),
4950}
4951
4952impl ResolvedVerifyingKey<'_> {
4953    fn evidence(
4954        &self,
4955        policy: &crate::policy::VerificationPolicy,
4956    ) -> Result<super::KeyTrustEvidence, DsigError> {
4957        match self {
4958            Self::Established(_) => {
4959                if policy.key_trust.mode != crate::policy::VerificationTrustMode::CryptographicOnly
4960                {
4961                    return Err(crate::policy::PolicyViolation::KeyTrust {
4962                        reason: "KEM/HMAC does not authenticate sender identity",
4963                    }
4964                    .into());
4965                }
4966                Ok(super::KeyTrustEvidence::NotEstablished)
4967            }
4968            Self::Borrowed(_)
4969                if policy.key_trust.mode
4970                    == crate::policy::VerificationTrustMode::CryptographicOnly =>
4971            {
4972                Ok(super::KeyTrustEvidence::NotEstablished)
4973            }
4974            Self::Borrowed(_) => Ok(super::KeyTrustEvidence::CallerTrusted),
4975            Self::Owned(key) => key.authorize(policy),
4976        }
4977    }
4978    fn as_ref(&self) -> &dyn VerifyingKey {
4979        match self {
4980            Self::Established(key) => key,
4981            Self::Borrowed(key) => *key,
4982            Self::Owned(key) => key.as_ref(),
4983        }
4984    }
4985}
4986
4987fn resolve_verifying_key<'k>(
4988    ctx: &VerifyContext<'k>,
4989    key: Option<&'k dyn VerifyingKey>,
4990    key_info: Option<&KeyInfo>,
4991    algorithm: SignatureAlgorithm,
4992    budget: &mut super::InspectedKeyCandidateBudget,
4993) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
4994    if let Some(key) = key {
4995        if !ctx.policy.key_sources.preset_key {
4996            return Err(crate::policy::PolicyViolation::KeyTrust {
4997                reason: "pre-resolved verification keys are disabled",
4998            }
4999            .into());
5000        }
5001        require_verifying_key_candidate_capacity(&ctx.policy)?;
5002        budget.charge()?;
5003        return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
5004    }
5005    if let Some(resolver) = ctx.key_resolver {
5006        require_verifying_key_candidate_capacity(&ctx.policy)?;
5007        let resolved = resolver.resolve_for_verification_with_budget(
5008            key_info,
5009            algorithm,
5010            &ctx.policy,
5011            ctx.provider,
5012            budget,
5013        )?;
5014        if let Some(key) = &resolved {
5015            key.authorize(&ctx.policy)?;
5016        }
5017        return Ok(resolved.map(ResolvedVerifyingKey::Owned));
5018    }
5019    Ok(None)
5020}
5021
5022fn require_verifying_key_candidate_capacity(
5023    policy: &crate::policy::VerificationPolicy,
5024) -> Result<(), SignatureVerificationPipelineError> {
5025    policy
5026        .resources
5027        .validate_key_candidates(1)
5028        .map_err(Into::into)
5029}
5030
5031fn enforce_reference_policies(
5032    references: &[Reference],
5033    allowed_uri_types: UriTypeSet,
5034    allowed_transforms: Option<&HashSet<String>>,
5035) -> Result<(), SignatureVerificationPipelineError> {
5036    for reference in references {
5037        let uri = reference
5038            .uri
5039            .as_deref()
5040            .ok_or(SignatureVerificationPipelineError::Reference(
5041                ReferenceProcessingError::MissingUri,
5042            ))?;
5043        if !allowed_uri_types.allows(uri) {
5044            return Err(crate::policy::PolicyViolation::Uri {
5045                operation: "verification",
5046                reason: "reference URI class is not permitted",
5047            }
5048            .into());
5049        }
5050
5051        if let Some(allowed) = allowed_transforms {
5052            for transform in &reference.transforms {
5053                let transform_uri = transform.algorithm_uri();
5054                enforce_transform_allowed(Some(allowed), transform_uri)?;
5055            }
5056
5057            // External dereference has an octet-stream data type independent of
5058            // whether the caller supplied the resource. Every transform then
5059            // determines the next type, including implicit binary-to-node-set
5060            // adapters before XML-level transforms.
5061            if transform_chain_uses_implicit_c14n(
5062                classify_uri(uri) == UriClass::External,
5063                &reference.transforms,
5064            ) {
5065                enforce_transform_allowed(Some(allowed), DEFAULT_IMPLICIT_C14N_URI)?;
5066            }
5067        }
5068    }
5069    Ok(())
5070}
5071
5072fn enforce_transform_allowed(
5073    allowed_transforms: Option<&HashSet<String>>,
5074    algorithm: &str,
5075) -> Result<(), SignatureVerificationPipelineError> {
5076    if allowed_transforms.is_some_and(|allowed| !allowed.contains(algorithm)) {
5077        return Err(crate::policy::PolicyViolation::Algorithm {
5078            operation: "verification transform",
5079            algorithm: algorithm.to_owned(),
5080        }
5081        .into());
5082    }
5083    Ok(())
5084}
5085
5086/// Query-funded form: the attacker-sized URI is copied into `work`'s storage.
5087fn enforce_current_transform_allowed<W: structured_asn1::memory::Work>(
5088    allowed_transforms: Option<&HashSet<String>>,
5089    algorithm: &str,
5090    work: &W,
5091) -> Result<(), SignatureVerificationPipelineError> {
5092    use crate::xml::dom::storage::Storage as _;
5093    if allowed_transforms.is_some_and(|allowed| !allowed.contains(algorithm)) {
5094        return Err(
5095            match crate::xml::dom::storage::Accounted(work).formatted(format_args!("{algorithm}")) {
5096                Ok(algorithm) => DsigError::AccountedPolicyAlgorithm {
5097                    operation: "verification transform",
5098                    algorithm: algorithm.into_owned(),
5099                },
5100                Err(crate::xml::dom::ParseError::SharedExecution(cause)) => cause.into(),
5101                Err(cause) => cause.into(),
5102            },
5103        );
5104    }
5105    Ok(())
5106}
5107
5108#[derive(Debug, Clone, Copy)]
5109pub(super) struct SignatureChildNodes<'a, 'input> {
5110    signed_info_node: Node<'a, 'input>,
5111    signature_value_node: Node<'a, 'input>,
5112    pub(super) key_info_node: Option<Node<'a, 'input>>,
5113}
5114
5115pub(super) fn parse_signature_children<'a, 'input>(
5116    signature_node: Node<'a, 'input>,
5117) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
5118    let mut signed_info_node: Option<Node<'_, '_>> = None;
5119    let mut signature_value_node: Option<Node<'_, '_>> = None;
5120    let mut key_info_node: Option<Node<'_, '_>> = None;
5121    let mut signed_info_index: Option<usize> = None;
5122    let mut signature_value_index: Option<usize> = None;
5123    let mut key_info_index: Option<usize> = None;
5124    let mut first_unexpected_dsig_index: Option<usize> = None;
5125
5126    let mut element_index = 0usize;
5127    for child in signature_node.children() {
5128        if child.is_text() {
5129            if child
5130                .text()
5131                .is_some_and(|text| !is_xml_whitespace_only(text))
5132            {
5133                return Err(SignatureVerificationPipelineError::InvalidStructure {
5134                    reason: "Signature must not contain non-whitespace mixed content",
5135                });
5136            }
5137            continue;
5138        }
5139        if !child.is_element() {
5140            continue;
5141        }
5142
5143        element_index += 1;
5144        if child.tag_name().namespace() != Some(XMLDSIG_NS) {
5145            return Err(SignatureVerificationPipelineError::InvalidStructure {
5146                reason: "Signature must contain only XMLDSIG element children",
5147            });
5148        }
5149        match child.tag_name().name() {
5150            "SignedInfo" => {
5151                if signed_info_node.is_some() {
5152                    return Err(SignatureVerificationPipelineError::InvalidStructure {
5153                        reason: "SignedInfo must appear exactly once under Signature",
5154                    });
5155                }
5156                signed_info_node = Some(child);
5157                signed_info_index = Some(element_index);
5158            }
5159            "SignatureValue" => {
5160                if signature_value_node.is_some() {
5161                    return Err(SignatureVerificationPipelineError::InvalidStructure {
5162                        reason: "SignatureValue must appear exactly once under Signature",
5163                    });
5164                }
5165                signature_value_node = Some(child);
5166                signature_value_index = Some(element_index);
5167            }
5168            "KeyInfo" => {
5169                if key_info_node.is_some() {
5170                    return Err(SignatureVerificationPipelineError::InvalidStructure {
5171                        reason: "KeyInfo must appear at most once under Signature",
5172                    });
5173                }
5174                key_info_node = Some(child);
5175                key_info_index = Some(element_index);
5176            }
5177            "Object" => {
5178                // Valid Object elements are allowed only after SignedInfo, SignatureValue,
5179                // and optional KeyInfo; this is enforced via first_unexpected_dsig_index.
5180            }
5181            _ => {
5182                if first_unexpected_dsig_index.is_none() {
5183                    first_unexpected_dsig_index = Some(element_index);
5184                }
5185            }
5186        }
5187    }
5188
5189    let signed_info_node =
5190        signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
5191            element: "SignedInfo",
5192        })?;
5193    let signature_value_node =
5194        signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
5195            element: "SignatureValue",
5196        })?;
5197    if signed_info_index != Some(1) {
5198        return Err(SignatureVerificationPipelineError::InvalidStructure {
5199            reason: "SignedInfo must be the first element child of Signature",
5200        });
5201    }
5202    if signature_value_index != Some(2) {
5203        return Err(SignatureVerificationPipelineError::InvalidStructure {
5204            reason: "SignatureValue must be the second element child of Signature",
5205        });
5206    }
5207    if let Some(index) = key_info_index
5208        && index != 3
5209    {
5210        return Err(SignatureVerificationPipelineError::InvalidStructure {
5211            reason: "KeyInfo must be the third element child of Signature when present",
5212        });
5213    }
5214
5215    let allowed_prefix_end = key_info_index.unwrap_or(2);
5216    if let Some(unexpected_index) = first_unexpected_dsig_index {
5217        return Err(SignatureVerificationPipelineError::InvalidStructure {
5218            reason: if unexpected_index > allowed_prefix_end {
5219                "After SignedInfo, SignatureValue, and optional KeyInfo, Signature may contain only Object elements"
5220            } else {
5221                "Signature may contain SignedInfo first, SignatureValue second, optional KeyInfo third, and Object elements thereafter"
5222            },
5223        });
5224    }
5225
5226    Ok(SignatureChildNodes {
5227        signed_info_node,
5228        signature_value_node,
5229        key_info_node,
5230    })
5231}
5232
5233fn decode_signature_value(
5234    signature_value_node: Node<'_, '_>,
5235    algorithm: SignatureAlgorithm,
5236    resources: &crate::policy::ResourcePolicy,
5237) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
5238    if matches!(algorithm, SignatureAlgorithm::PostQuantum(_)) {
5239        let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
5240        // PQ signatures can be tens of kilobytes: borrow their XML text and
5241        // stream directly into the decoded allocation, without a second
5242        // full-size normalized Base64 buffer.
5243        let payload = super::whitespace::XmlBase64Payload::bounded(
5244            signature_value_node,
5245            raw_maximum,
5246            maximum,
5247        )
5248        .map_err(
5249            |reason| SignatureVerificationPipelineError::InvalidStructure {
5250                reason: match reason {
5251                    "unexpected nested element" => {
5252                        "SignatureValue must not contain element children"
5253                    }
5254                    "maximum allowed text length" => {
5255                        "SignatureValue exceeds maximum allowed text length"
5256                    }
5257                    "maximum allowed base64 length" => {
5258                        "SignatureValue exceeds maximum allowed length"
5259                    }
5260                    _ => "SignatureValue contains malformed Base64",
5261                },
5262            },
5263        )?;
5264        return payload.decode().map_err(|_| {
5265            SignatureVerificationPipelineError::InvalidStructure {
5266                reason: "SignatureValue contains malformed Base64",
5267            }
5268        });
5269    }
5270    if signature_value_node
5271        .children()
5272        .any(|child| child.is_element())
5273    {
5274        return Err(SignatureVerificationPipelineError::InvalidStructure {
5275            reason: "SignatureValue must not contain element children",
5276        });
5277    }
5278
5279    // Count bounded borrowed text first: comments may split a
5280    // signature into many tiny text nodes. Reserving per node would repeatedly
5281    // copy the accumulated payload; one exact reservation keeps this linear.
5282    let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
5283    let mut text_len = 0usize;
5284    let mut normalized_len = 0usize;
5285    for child in signature_value_node
5286        .children()
5287        .filter(|child| child.is_text())
5288    {
5289        if let Some(text) = child.text() {
5290            if text.len() > raw_maximum - text_len {
5291                return Err(SignatureVerificationPipelineError::InvalidStructure {
5292                    reason: "SignatureValue exceeds maximum allowed text length",
5293                });
5294            }
5295            text_len += text.len();
5296            for byte in text.bytes() {
5297                if matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
5298                    continue;
5299                }
5300                if normalized_len == maximum {
5301                    return Err(SignatureVerificationPipelineError::InvalidStructure {
5302                        reason: "SignatureValue exceeds maximum allowed length",
5303                    });
5304                }
5305                normalized_len += 1;
5306            }
5307        }
5308    }
5309    let mut normalized = Vec::with_capacity(normalized_len);
5310    let mut raw_text_len = 0usize;
5311    for child in signature_value_node
5312        .children()
5313        .filter(|child| child.is_text())
5314    {
5315        if let Some(text) = child.text() {
5316            push_normalized_signature_text(
5317                text,
5318                &mut raw_text_len,
5319                &mut normalized,
5320                algorithm,
5321                resources,
5322            )?;
5323        }
5324    }
5325
5326    Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
5327}
5328
5329fn push_normalized_signature_text(
5330    text: &str,
5331    raw_text_len: &mut usize,
5332    normalized: &mut Vec<u8>,
5333    algorithm: SignatureAlgorithm,
5334    resources: &crate::policy::ResourcePolicy,
5335) -> Result<(), SignatureVerificationPipelineError> {
5336    let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
5337    if text.len() > raw_maximum - *raw_text_len {
5338        return Err(SignatureVerificationPipelineError::InvalidStructure {
5339            reason: "SignatureValue exceeds maximum allowed text length",
5340        });
5341    }
5342    *raw_text_len += text.len();
5343    let additional = text
5344        .bytes()
5345        .filter(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
5346        .count();
5347    if additional > maximum - normalized.len() {
5348        return Err(SignatureVerificationPipelineError::InvalidStructure {
5349            reason: "SignatureValue exceeds maximum allowed length",
5350        });
5351    }
5352
5353    normalize_xml_base64_bytes(text.as_bytes(), normalized, |_| true).map_err(|err| {
5354        SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(
5355            err.normalized_offset,
5356            err.invalid_byte,
5357        ))
5358    })?;
5359
5360    Ok(())
5361}
5362
5363fn signature_value_limits(
5364    algorithm: SignatureAlgorithm,
5365    resources: &crate::policy::ResourcePolicy,
5366) -> (usize, usize) {
5367    // FIPS 205 table 2 includes signatures up to 49,856 octets. The old
5368    // RSA-oriented encoded-text ceiling is not their wire contract.
5369    // https://doi.org/10.6028/NIST.FIPS.205
5370    // XSD 1.0 section 3.2.16 fixes base64Binary whitespace to collapse:
5371    // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#base64Binary
5372    // Raw text is bounded by the operation's XML budget, not the wire width.
5373    let encoded = match algorithm {
5374        SignatureAlgorithm::PostQuantum(parameter) => parameter.signature_len().div_ceil(3) * 4,
5375        _ => MAX_SIGNATURE_VALUE_LEN,
5376    };
5377    (encoded, resources.max_xml_document_bytes)
5378}
5379
5380fn verify_with_algorithm(
5381    algorithm: SignatureAlgorithm,
5382    public_key_pem: &str,
5383    signed_data: &[u8],
5384    signature_value: &[u8],
5385) -> Result<bool, SignatureVerificationPipelineError> {
5386    match algorithm {
5387        #[cfg(feature = "legacy-algorithms")]
5388        SignatureAlgorithm::RsaMd5 | SignatureAlgorithm::RsaRipemd160 => Ok(
5389            verify_rsa_signature_pem(algorithm, public_key_pem, signed_data, signature_value)?,
5390        ),
5391        #[cfg(feature = "legacy-algorithms")]
5392        SignatureAlgorithm::EcdsaRipemd160 => Ok(verify_ecdsa_signature_pem(
5393            algorithm,
5394            public_key_pem,
5395            signed_data,
5396            signature_value,
5397        )?),
5398        #[cfg(feature = "legacy-algorithms")]
5399        SignatureAlgorithm::HmacMd5 | SignatureAlgorithm::HmacRipemd160 => {
5400            Err(SignatureVerificationError::UnsupportedAlgorithm {
5401                uri: algorithm.uri().to_owned(),
5402            }
5403            .into())
5404        }
5405        SignatureAlgorithm::Ed25519
5406        | SignatureAlgorithm::Ed25519Ctx
5407        | SignatureAlgorithm::Ed25519Ph
5408        | SignatureAlgorithm::Ed448
5409        | SignatureAlgorithm::Ed448Ph
5410        | SignatureAlgorithm::PostQuantum(_) => {
5411            let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
5412                .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
5413            if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
5414                return Err(SignatureVerificationError::InvalidKeyPem.into());
5415            }
5416            Ok(super::modern::verify(
5417                algorithm,
5418                &pem.contents,
5419                signed_data,
5420                signature_value,
5421            )?)
5422        }
5423        SignatureAlgorithm::DsaSha1 | SignatureAlgorithm::DsaSha256 => {
5424            let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
5425                .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
5426            if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
5427                return Err(SignatureVerificationError::InvalidKeyPem.into());
5428            }
5429            Ok(verify_dsa_signature_spki(
5430                algorithm,
5431                &pem.contents,
5432                signed_data,
5433                signature_value,
5434            )?)
5435        }
5436        SignatureAlgorithm::HmacSha1
5437        | SignatureAlgorithm::HmacSha224
5438        | SignatureAlgorithm::HmacSha256
5439        | SignatureAlgorithm::HmacSha384
5440        | SignatureAlgorithm::HmacSha512 => Err(SignatureVerificationError::UnsupportedAlgorithm {
5441            uri: algorithm.uri().to_string(),
5442        }
5443        .into()),
5444        SignatureAlgorithm::RsaSha1
5445        | SignatureAlgorithm::RsaSha224
5446        | SignatureAlgorithm::RsaSha256
5447        | SignatureAlgorithm::RsaSha384
5448        | SignatureAlgorithm::RsaSha512
5449        | SignatureAlgorithm::RsaPssSha1
5450        | SignatureAlgorithm::RsaPssSha224
5451        | SignatureAlgorithm::RsaPssSha256
5452        | SignatureAlgorithm::RsaPssSha384
5453        | SignatureAlgorithm::RsaPssSha512
5454        | SignatureAlgorithm::RsaPssSha3_224
5455        | SignatureAlgorithm::RsaPssSha3_256
5456        | SignatureAlgorithm::RsaPssSha3_384
5457        | SignatureAlgorithm::RsaPssSha3_512
5458        | SignatureAlgorithm::RsaPss(_) => Ok(verify_rsa_signature_pem(
5459            algorithm,
5460            public_key_pem,
5461            signed_data,
5462            signature_value,
5463        )?),
5464        SignatureAlgorithm::EcdsaSha1
5465        | SignatureAlgorithm::EcdsaSha224
5466        | SignatureAlgorithm::EcdsaSha256
5467        | SignatureAlgorithm::EcdsaSha384
5468        | SignatureAlgorithm::EcdsaSha512
5469        | SignatureAlgorithm::EcdsaSha3_224
5470        | SignatureAlgorithm::EcdsaSha3_256
5471        | SignatureAlgorithm::EcdsaSha3_384
5472        | SignatureAlgorithm::EcdsaSha3_512 => {
5473            // Malformed ECDSA signature bytes are treated as a verification miss
5474            // (Ok(false)) instead of a pipeline error; only key/algorithm and
5475            // crypto-operation failures propagate as Err.
5476            match verify_ecdsa_signature_pem(
5477                algorithm,
5478                public_key_pem,
5479                signed_data,
5480                signature_value,
5481            ) {
5482                Ok(valid) => Ok(valid),
5483                Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
5484                Err(error) => Err(error.into()),
5485            }
5486        }
5487    }
5488}
5489
5490#[cfg(test)]
5491#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
5492mod tests {
5493    use super::*;
5494    use crate::c14n::C14nAlgorithm;
5495    use crate::xml::dom::Document;
5496    use crate::xmldsig::TransformError;
5497    use crate::xmldsig::digest::DigestAlgorithm;
5498    use crate::xmldsig::parse::{Reference, parse_signed_info};
5499    use crate::xmldsig::transforms::Transform;
5500    use crate::xmldsig::uri::UriReferenceResolver;
5501    use base64::Engine;
5502
5503    // ── Helpers ──────────────────────────────────────────────────────
5504
5505    /// Build a Reference with given URI, transforms, digest method, and expected digest.
5506    fn make_reference(
5507        uri: &str,
5508        transforms: Vec<Transform>,
5509        digest_method: DigestAlgorithm,
5510        digest_value: Vec<u8>,
5511    ) -> Reference {
5512        Reference {
5513            uri: Some(uri.to_string()),
5514            id: None,
5515            ref_type: None,
5516            transforms,
5517            digest_method,
5518            digest_value,
5519        }
5520    }
5521
5522    #[test]
5523    fn operation_borrows_caller_utc_facts_without_policy_rebinding()
5524    -> Result<(), Box<dyn std::error::Error>> {
5525        // Synthetic caller facts test transport, not real-world UTC history.
5526        // Operation capture shares the original slice and never infers facts
5527        // from a different policy or creates a replacement timeline.
5528        let transitions = [structured_pkix::temporal::Transition::new(2_678_400, 1)];
5529        let facts = structured_pkix::temporal::LeapTable::new(0, 6_000_000, &transitions)
5530            .expect("valid synthetic UTC facts");
5531        let ctx = VerifyContext::new().utc_facts(facts);
5532        let budgets = VerificationOperationBudgets::with_transforms(
5533            &ctx.policy,
5534            TransformExecutionBudget::from_resources(&ctx.policy.resources),
5535            ctx.utc_facts,
5536        );
5537        let mut budget = budgets.key_candidates.borrow_mut();
5538        assert_eq!(
5539            budget
5540                .utc_facts()
5541                .ok_or("operation UTC facts")?
5542                .transitions()
5543                .as_ptr(),
5544            transitions.as_ptr()
5545        );
5546        budget.bind_policy(&ctx.policy)?;
5547        assert_eq!(
5548            budget
5549                .utc_facts()
5550                .ok_or("retained UTC facts")?
5551                .transitions()
5552                .as_ptr(),
5553            transitions.as_ptr()
5554        );
5555        let standalone =
5556            super::super::InspectedKeyCandidateBudget::new_with_utc_facts(&ctx.policy, facts);
5557        assert_eq!(
5558            standalone
5559                .utc_facts()
5560                .ok_or("standalone UTC facts")?
5561                .transitions()
5562                .as_ptr(),
5563            transitions.as_ptr()
5564        );
5565        assert!(
5566            super::super::InspectedKeyCandidateBudget::new(&ctx.policy)
5567                .utc_facts()
5568                .is_none()
5569        );
5570        Ok(())
5571    }
5572
5573    #[test]
5574    fn reference_resolution_uses_each_elements_effective_xml_base() {
5575        // Equal lexical URIs under different xml:base values identify distinct
5576        // caller-owned resources and must not collide in the resolver.
5577        let first = b"first payload";
5578        let second = b"second payload";
5579        let first_digest = base64::engine::general_purpose::STANDARD
5580            .encode(compute_digest(DigestAlgorithm::Sha256, first));
5581        let second_digest = base64::engine::general_purpose::STANDARD
5582            .encode(compute_digest(DigestAlgorithm::Sha256, second));
5583        let xml = format!(
5584            r#"<root xml:base="https://example.test/base/" xmlns:ds="{XMLDSIG_NS}">
5585                <ds:Signature><ds:SignedInfo>
5586                    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5587                    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5588                    <ds:Reference xml:base="one/" URI="payload.bin">
5589                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5590                        <ds:DigestValue>{first_digest}</ds:DigestValue>
5591                    </ds:Reference>
5592                    <ds:Reference xml:base="../two/" URI="payload.bin">
5593                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5594                        <ds:DigestValue>{second_digest}</ds:DigestValue>
5595                    </ds:Reference>
5596                </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>
5597            </root>"#
5598        );
5599        let document = Document::parse(&xml).unwrap();
5600        let signature = document
5601            .descendants()
5602            .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
5603            .unwrap();
5604        let signed_info_node = signature
5605            .children()
5606            .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
5607            .unwrap();
5608        let signed_info = parse_signed_info(signed_info_node).unwrap();
5609        let resources = HashMap::from([
5610            (
5611                "https://example.test/base/one/payload.bin".into(),
5612                first.to_vec(),
5613            ),
5614            (
5615                "https://example.test/two/payload.bin".into(),
5616                second.to_vec(),
5617            ),
5618        ]);
5619        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5620
5621        let result = process_all_references(&signed_info.references, &resolver, signature, false)
5622            .expect("each Reference should resolve against its own effective base");
5623
5624        assert!(result.all_valid());
5625    }
5626
5627    #[test]
5628    fn internal_dtd_opt_in_applies_to_detached_xml_transforms() {
5629        // The parse policy covers every XML document in one verification
5630        // pipeline, including caller-owned octets converted to a node-set.
5631        let detached = b"<!DOCTYPE payload [<!ELEMENT payload (#PCDATA)>]><payload>ok</payload>";
5632        let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
5633            DigestAlgorithm::Sha256,
5634            b"<payload>ok</payload>",
5635        ));
5636        let xml = format!(
5637            r#"<root xmlns:ds="{XMLDSIG_NS}">
5638  <ds:Signature>
5639    <ds:SignedInfo>
5640      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5641      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5642      <ds:Reference URI="urn:detached-dtd">
5643        <ds:Transforms>
5644          <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
5645        </ds:Transforms>
5646        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5647        <ds:DigestValue>{digest}</ds:DigestValue>
5648      </ds:Reference>
5649    </ds:SignedInfo>
5650    <ds:SignatureValue>AQ==</ds:SignatureValue>
5651  </ds:Signature>
5652</root>"#
5653        );
5654        let resources = HashMap::from([("urn:detached-dtd".to_owned(), detached.to_vec())]);
5655        let key = AcceptingKey;
5656
5657        let default_error = VerifyContext::new()
5658            .key(&key)
5659            .allowed_uri_types(UriTypeSet::ALL)
5660            .external_resources(&resources)
5661            .verify(&xml)
5662            .expect_err("internal DTD parsing must remain disabled by default");
5663        assert!(matches!(
5664            default_error,
5665            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
5666                crate::xmldsig::TransformError::XmlParse(_)
5667            ))
5668        ));
5669
5670        let result = VerifyContext::new()
5671            .key(&key)
5672            .allowed_uri_types(UriTypeSet::ALL)
5673            .external_resources(&resources)
5674            .allow_internal_dtd(true)
5675            .verify(&xml)
5676            .expect("the explicit DTD opt-in must cover detached XML transforms");
5677
5678        assert_eq!(result.status, DsigStatus::Valid);
5679
5680        let external_entity = br#"<!DOCTYPE payload [
5681            <!ENTITY ext SYSTEM "file:///etc/passwd">
5682        ]><payload>&ext;</payload>"#;
5683        let external_entity_resources =
5684            HashMap::from([("urn:detached-dtd".to_owned(), external_entity.to_vec())]);
5685        let external_entity_error = VerifyContext::new()
5686            .key(&key)
5687            .allowed_uri_types(UriTypeSet::ALL)
5688            .external_resources(&external_entity_resources)
5689            .allow_internal_dtd(true)
5690            .verify(&xml)
5691            .expect_err("the internal-DTD opt-in must not resolve external entities");
5692        assert!(matches!(
5693            external_entity_error,
5694            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
5695                crate::xmldsig::TransformError::XmlParse(_)
5696            ))
5697        ));
5698    }
5699
5700    #[test]
5701    fn owned_verification_revalidates_internal_dtd_provenance() {
5702        // A document accepted by one permissive operation must not bypass a
5703        // later verification context's stricter XML input policy.
5704        let document = XmlDocument::parse_with_settings(
5705            "<!DOCTYPE root [<!ENTITY value \"ok\">]><root>&value;</root>".into(),
5706            DocumentParseSettings::new(
5707                true,
5708                crate::hard_limits::XML_DOCUMENT_NODE_CEILING,
5709                crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
5710            ),
5711        )
5712        .expect("explicitly permitted DTD fixture must parse");
5713
5714        assert!(matches!(
5715            VerifyContext::new().verify_document(&document),
5716            Err(DsigError::Policy(
5717                crate::policy::PolicyViolation::XmlInput {
5718                    reason: "owned document requires internal DTD support"
5719                }
5720            ))
5721        ));
5722        assert!(!matches!(
5723            VerifyContext::new()
5724                .allow_internal_dtd(true)
5725                .verify_document(&document),
5726            Err(DsigError::Policy(
5727                crate::policy::PolicyViolation::XmlInput { .. }
5728            ))
5729        ));
5730    }
5731
5732    #[test]
5733    fn verification_policy_bounds_reference_canonicalization() {
5734        // Reference transforms and SignedInfo canonicalization are one operation;
5735        // references must not fall back to the transform hard-limit budget.
5736        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
5737        let xml = format!(
5738            r#"<root xmlns:ds="{XMLDSIG_NS}"><payload>{}</payload><ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI=""><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"#,
5739            "payload".repeat(16)
5740        );
5741        let policy = crate::policy::VerificationPolicy {
5742            resources: crate::policy::ResourcePolicy {
5743                max_canonicalized_bytes: 64,
5744                ..crate::policy::ResourcePolicy::default()
5745            },
5746            ..crate::policy::VerificationPolicy::default()
5747        };
5748
5749        let error = VerifyContext::new()
5750            .key(&AcceptingKey)
5751            .policy(policy)
5752            .verify(&xml)
5753            .expect_err("reference canonicalization must consume the policy budget");
5754
5755        assert!(
5756            matches!(
5757                error,
5758                SignatureVerificationPipelineError::Policy(
5759                    crate::policy::PolicyViolation::ResourceLimit {
5760                        resource: crate::policy::resource_name::CANONICALIZED_BYTES,
5761                        maximum: 64,
5762                        ..
5763                    }
5764                )
5765            ),
5766            "unexpected error: {error:?}"
5767        );
5768    }
5769
5770    #[test]
5771    fn verification_policy_bounds_document_bytes_before_parsing() {
5772        // A small node count does not bound parser work when one text node is
5773        // large, so the byte ceiling must reject before structural inspection.
5774        let xml = format!("<root>{}</root>", "x".repeat(1_024));
5775        let policy = crate::policy::VerificationPolicy {
5776            resources: crate::policy::ResourcePolicy {
5777                max_xml_document_bytes: xml.len() - 1,
5778                ..crate::policy::ResourcePolicy::default()
5779            },
5780            ..crate::policy::VerificationPolicy::default()
5781        };
5782
5783        assert!(matches!(
5784            VerifyContext::new().policy(policy).verify(&xml),
5785            Err(SignatureVerificationPipelineError::Policy(
5786                crate::policy::PolicyViolation::ResourceLimit {
5787                    resource: crate::policy::resource_name::XML_DOCUMENT,
5788                    maximum,
5789                    actual,
5790                }
5791            )) if maximum == xml.len() - 1 && actual == xml.len()
5792        ));
5793    }
5794
5795    #[test]
5796    fn verification_entry_points_enforce_policy_depth() {
5797        // Both borrowed XML and a retained document parsed under wider defaults
5798        // must be rejected before signature selection traverses the tree.
5799        let xml = "<root><child><leaf/></child></root>";
5800        let policy = crate::policy::VerificationPolicy {
5801            resources: crate::policy::ResourcePolicy {
5802                max_xml_depth: 2,
5803                ..crate::policy::ResourcePolicy::default()
5804            },
5805            ..crate::policy::VerificationPolicy::default()
5806        };
5807        let document = XmlDocument::parse(xml).expect("wide retained fixture must parse");
5808
5809        assert!(matches!(
5810            VerifyContext::new().policy(policy.clone()).verify(xml),
5811            Err(DsigError::Policy(
5812                crate::policy::PolicyViolation::ResourceLimit {
5813                    resource: crate::policy::resource_name::XML_DEPTH,
5814                    maximum: 2,
5815                    actual: 3,
5816                }
5817            ))
5818        ));
5819        assert!(matches!(
5820            VerifyContext::new()
5821                .policy(policy)
5822                .verify_document(&document),
5823            Err(DsigError::Policy(
5824                crate::policy::PolicyViolation::ResourceLimit {
5825                    resource: crate::policy::resource_name::XML_DEPTH,
5826                    maximum: 2,
5827                    actual: 3,
5828                }
5829            ))
5830        ));
5831    }
5832
5833    #[test]
5834    fn verification_policy_bounds_base64_transform_input() {
5835        // The operation snapshot must reach the transform executor rather than
5836        // silently falling back to its implementation-wide default budget.
5837        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
5838        let xml = format!(
5839            r##"<root xmlns:ds="{XMLDSIG_NS}"><payload ID="payload">QUJDRA==</payload><ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#payload"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#base64"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"##
5840        );
5841        let policy = crate::policy::VerificationPolicy {
5842            resources: crate::policy::ResourcePolicy {
5843                max_base64_transform_input_bytes: 4,
5844                ..crate::policy::ResourcePolicy::default()
5845            },
5846            ..crate::policy::VerificationPolicy::default()
5847        };
5848
5849        let error = VerifyContext::new()
5850            .key(&AcceptingKey)
5851            .policy(policy)
5852            .verify(&xml)
5853            .expect_err("Base64 input must use the operation policy ceiling");
5854
5855        assert!(matches!(
5856            error,
5857            SignatureVerificationPipelineError::Policy(
5858                crate::policy::PolicyViolation::ResourceLimit {
5859                    resource: crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
5860                    maximum: 4,
5861                    ..
5862                }
5863            )
5864        ));
5865    }
5866
5867    #[test]
5868    fn verification_policy_bounds_cumulative_base64_transform_output() {
5869        // References share one operation budget. Validating each decoded value
5870        // against the full ceiling would let a signature multiply output work.
5871        let first_digest = base64::engine::general_purpose::STANDARD
5872            .encode(compute_digest(DigestAlgorithm::Sha256, b"a"));
5873        let second_digest = base64::engine::general_purpose::STANDARD
5874            .encode(compute_digest(DigestAlgorithm::Sha256, b"b"));
5875        let xml = format!(
5876            r##"<root xmlns:ds="{XMLDSIG_NS}"><first ID="first">YQ==</first><second ID="second">Yg==</second><ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#first"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#base64"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{first_digest}</ds:DigestValue></ds:Reference><ds:Reference URI="#second"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#base64"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{second_digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"##
5877        );
5878        let policy = crate::policy::VerificationPolicy {
5879            resources: crate::policy::ResourcePolicy {
5880                max_base64_transform_input_bytes: 8,
5881                max_base64_transform_output_bytes: 1,
5882                ..crate::policy::ResourcePolicy::default()
5883            },
5884            ..crate::policy::VerificationPolicy::default()
5885        };
5886
5887        let error = VerifyContext::new()
5888            .key(&AcceptingKey)
5889            .policy(policy)
5890            .verify(&xml)
5891            .expect_err("references must share the Base64 output allowance");
5892
5893        assert!(matches!(
5894            error,
5895            SignatureVerificationPipelineError::Policy(
5896                crate::policy::PolicyViolation::ResourceLimit {
5897                    resource: crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
5898                    maximum: 1,
5899                    actual: 2,
5900                }
5901            )
5902        ));
5903    }
5904
5905    #[test]
5906    fn verification_policy_bounds_xpath_source_before_compilation() {
5907        // XPath parser limits are part of the same operation snapshot as the
5908        // evaluator limits; parsing cannot use a separate hard-coded budget.
5909        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
5910        let xml = format!(
5911            r#"<root xmlns:ds="{XMLDSIG_NS}"><ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI=""><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>true()</ds:XPath></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"#
5912        );
5913        let policy = crate::policy::VerificationPolicy {
5914            resources: crate::policy::ResourcePolicy {
5915                max_xpath_expression_bytes: 4,
5916                ..crate::policy::ResourcePolicy::default()
5917            },
5918            ..crate::policy::VerificationPolicy::default()
5919        };
5920
5921        let error = VerifyContext::new()
5922            .key(&AcceptingKey)
5923            .policy(policy)
5924            .verify(&xml)
5925            .expect_err("XPath source must use the operation policy ceiling");
5926
5927        assert!(matches!(
5928            error,
5929            SignatureVerificationPipelineError::Policy(
5930                crate::policy::PolicyViolation::ResourceLimit {
5931                    resource: crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
5932                    maximum: 4,
5933                    ..
5934                }
5935            )
5936        ));
5937    }
5938
5939    #[test]
5940    fn verification_policy_shares_canonicalization_budget_with_signed_info() {
5941        // Reference transforms and SignedInfo canonicalization are one operation.
5942        // Each output fits independently, but their aggregate must not receive
5943        // two separate copies of the configured canonicalization allowance.
5944        let payload_text = "x".repeat(700);
5945        let canonical_payload = format!("<payload ID=\"payload\">{payload_text}</payload>");
5946        let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
5947            DigestAlgorithm::Sha256,
5948            canonical_payload.as_bytes(),
5949        ));
5950        let xml = format!(
5951            r##"<root xmlns:ds="{XMLDSIG_NS}">{canonical_payload}<ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#payload"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"##
5952        );
5953        let policy = crate::policy::VerificationPolicy {
5954            resources: crate::policy::ResourcePolicy {
5955                max_canonicalized_bytes: 1_024,
5956                ..crate::policy::ResourcePolicy::default()
5957            },
5958            ..crate::policy::VerificationPolicy::default()
5959        };
5960
5961        let error = VerifyContext::new()
5962            .key(&AcceptingKey)
5963            .policy(policy)
5964            .verify(&xml)
5965            .expect_err("SignedInfo must consume the remaining operation C14N budget");
5966
5967        assert!(
5968            matches!(
5969                &error,
5970                SignatureVerificationPipelineError::Policy(
5971                    crate::policy::PolicyViolation::ResourceLimit {
5972                        resource: "canonicalized bytes",
5973                        maximum: 1_024,
5974                        ..
5975                    }
5976                )
5977            ),
5978            "unexpected error: {error:?}"
5979        );
5980    }
5981
5982    #[test]
5983    fn manifest_processing_stops_after_c14n_budget_exhaustion() {
5984        // A failed bounded render consumes the remaining operation allowance.
5985        // Later Manifest references, including cheap binary ones, must not run.
5986        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
5987        let xml = format!(
5988            r##"<root xmlns:ds="{XMLDSIG_NS}"><payload Id="payload">too large</payload><ds:Signature><ds:Object Id="signed-object"><ds:Manifest><ds:Reference URI="#payload"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference><ds:Reference URI="urn:small"><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:Manifest></ds:Object></ds:Signature></root>"##
5989        );
5990        let resources = HashMap::from([("urn:small".to_owned(), b"small".to_vec())]);
5991        let transform_budget = TransformExecutionBudget::with_c14n_limit(8);
5992        let ctx = VerifyContext::new()
5993            .allowed_uri_types(UriTypeSet::ALL)
5994            .external_resources(&resources);
5995        let document = XmlDocument::parse(xml).expect("test signature must parse");
5996        let results = document
5997            .with_view(|view| {
5998                let signature = view
5999                    .document()
6000                    .descendants()
6001                    .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
6002                    .expect("test signature must contain Signature");
6003                let object = signature
6004                    .children()
6005                    .find(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
6006                    .expect("test signature must contain Object");
6007                let resolver = UriReferenceResolver::with_document_view(view, &[])
6008                    .with_external_resources(&resources);
6009                let budgets = VerificationOperationBudgets::with_transforms(
6010                    &ctx.policy,
6011                    transform_budget,
6012                    ctx.utc_facts,
6013                );
6014                let mut operation = OperationExecutionContext::new(
6015                    &ctx.policy,
6016                    &budgets,
6017                    Some((view.identity(), view.generation())),
6018                );
6019                let crypto =
6020                    operation.add_node(OperationNodeKind::Crypto, OperationStage::Crypto, None);
6021                operation.compile().expect("base plan");
6022                operation
6023                    .run(crypto, || Ok::<_, SignatureVerificationPipelineError>(()))
6024                    .expect("crypto gate");
6025                operation.authenticate(view.node_identity(object));
6026                process_authenticated_manifest_references(
6027                    &mut operation,
6028                    view,
6029                    signature,
6030                    &resolver,
6031                    ManifestVerification {
6032                        context: &ctx,
6033                        capture: &(),
6034                    },
6035                    2,
6036                    crypto,
6037                )
6038                .map(|(results, _)| results)
6039            })
6040            .expect("resource exhaustion is reported per Manifest reference");
6041
6042        assert_eq!(results.len(), 2);
6043        assert!(results.iter().all(|result| {
6044            matches!(
6045                result.status,
6046                DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { .. })
6047            )
6048        }));
6049    }
6050
6051    #[test]
6052    fn verification_policy_bounds_detached_xml_nodes() {
6053        // Caller-owned detached octets become a second XML document during a
6054        // node-set transform and must inherit the same operation node ceiling.
6055        let detached = format!("<payload>{}</payload>", "<n/>".repeat(32));
6056        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
6057        let xml = format!(
6058            r#"<root xmlns:ds="{XMLDSIG_NS}"><ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="urn:detached-nodes"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>true()</ds:XPath></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"#
6059        );
6060        let resources = HashMap::from([("urn:detached-nodes".to_owned(), detached.into_bytes())]);
6061        let policy = crate::policy::VerificationPolicy {
6062            uris: crate::policy::UriPolicy {
6063                references: UriTypeSet::ALL,
6064                ..crate::policy::UriPolicy::default()
6065            },
6066            resources: crate::policy::ResourcePolicy {
6067                max_xml_nodes: 24,
6068                ..crate::policy::ResourcePolicy::default()
6069            },
6070            ..crate::policy::VerificationPolicy::default()
6071        };
6072
6073        let error = VerifyContext::new()
6074            .key(&AcceptingKey)
6075            .policy(policy)
6076            .external_resources(&resources)
6077            .verify(&xml)
6078            .expect_err("detached XML must inherit the policy node ceiling");
6079
6080        assert!(
6081            matches!(
6082                error,
6083                SignatureVerificationPipelineError::Policy(
6084                    crate::policy::PolicyViolation::ResourceLimit {
6085                        resource: crate::policy::resource_name::XML_NODES,
6086                        maximum: 24,
6087                        ..
6088                    }
6089                )
6090            ),
6091            "unexpected error: {error:?}"
6092        );
6093    }
6094
6095    #[test]
6096    fn query_only_reference_resolves_against_relative_xml_base() {
6097        // A query-only URI replaces the inherited base query without changing
6098        // its relative path; no absolute document base is required by XML Base.
6099        let payload = b"query-selected payload";
6100        let digest = base64::engine::general_purpose::STANDARD
6101            .encode(compute_digest(DigestAlgorithm::Sha256, payload));
6102        let xml = format!(
6103            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo>
6104                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6105                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6106                <ds:Reference xml:base="a/b?old" URI="?new">
6107                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6108                    <ds:DigestValue>{digest}</ds:DigestValue>
6109                </ds:Reference>
6110            </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>"#
6111        );
6112        let document = Document::parse(&xml).unwrap();
6113        let signature = document.root_element();
6114        let signed_info_node = signature
6115            .children()
6116            .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
6117            .unwrap();
6118        let signed_info = parse_signed_info(signed_info_node).unwrap();
6119        let resources = HashMap::from([("a/b?new".to_string(), payload.to_vec())]);
6120        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6121
6122        let result = process_all_references(&signed_info.references, &resolver, signature, false)
6123            .expect("query-only URI must resolve against the complete relative base path");
6124
6125        assert!(result.all_valid());
6126    }
6127
6128    #[test]
6129    fn manifest_reference_resolution_uses_its_effective_xml_base() {
6130        // Manifest references carry their own XML Base context and must not
6131        // accidentally reuse the SignedInfo or Signature element context.
6132        let payload = b"manifest payload";
6133        let digest = base64::engine::general_purpose::STANDARD
6134            .encode(compute_digest(DigestAlgorithm::Sha256, payload));
6135        let xml = format!(
6136            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
6137                <ds:Object><ds:Manifest xml:base="manifests/">
6138                    <ds:Reference URI="payload.bin">
6139                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6140                        <ds:DigestValue>{digest}</ds:DigestValue>
6141                    </ds:Reference>
6142                </ds:Manifest></ds:Object>
6143            </ds:Signature>"#
6144        );
6145        let document = Document::parse(&xml).unwrap();
6146        let signature = document.root_element();
6147        let reference_node = signature
6148            .descendants()
6149            .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
6150            .unwrap();
6151        let reference = super::super::parse::parse_reference(reference_node).unwrap();
6152        let resources = HashMap::from([(
6153            "https://example.test/manifests/payload.bin".to_string(),
6154            payload.to_vec(),
6155        )]);
6156        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6157
6158        let result = process_reference(
6159            &reference,
6160            &resolver,
6161            signature,
6162            ReferenceSet::Manifest,
6163            0,
6164            false,
6165        )
6166        .expect("Manifest Reference should inherit its own XML Base context");
6167
6168        assert_eq!(result.status, DsigStatus::Valid);
6169    }
6170
6171    #[test]
6172    fn manifest_reference_index_ignores_nested_manifest_descendants() {
6173        // The public Manifest index follows Signature/Object/Manifest structure;
6174        // wrapper descendants must not steal an index and supply another base URI.
6175        let payload = b"direct manifest payload";
6176        let digest = base64::engine::general_purpose::STANDARD
6177            .encode(compute_digest(DigestAlgorithm::Sha256, payload));
6178        let xml = format!(
6179            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
6180                <ds:Object><wrapper><ds:Manifest xml:base="nested/">
6181                    <ds:Reference URI="payload.bin">
6182                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6183                        <ds:DigestValue>{digest}</ds:DigestValue>
6184                    </ds:Reference>
6185                </ds:Manifest></wrapper></ds:Object>
6186                <ds:Object><ds:Manifest xml:base="direct/">
6187                    <ds:Reference URI="payload.bin">
6188                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6189                        <ds:DigestValue>{digest}</ds:DigestValue>
6190                    </ds:Reference>
6191                </ds:Manifest></ds:Object>
6192            </ds:Signature>"#
6193        );
6194        let document = Document::parse(&xml).unwrap();
6195        let signature = document.root_element();
6196        let direct_reference_node = signature
6197            .children()
6198            .filter(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
6199            .nth(1)
6200            .unwrap()
6201            .children()
6202            .find(|node| node.has_tag_name((XMLDSIG_NS, "Manifest")))
6203            .unwrap()
6204            .children()
6205            .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
6206            .unwrap();
6207        let reference = super::super::parse::parse_reference(direct_reference_node).unwrap();
6208        let resources = HashMap::from([(
6209            "https://example.test/direct/payload.bin".to_string(),
6210            payload.to_vec(),
6211        )]);
6212        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6213
6214        let result = process_reference(
6215            &reference,
6216            &resolver,
6217            signature,
6218            ReferenceSet::Manifest,
6219            0,
6220            false,
6221        )
6222        .expect("Manifest index must select the direct Object/Manifest reference");
6223
6224        assert_eq!(result.status, DsigStatus::Valid);
6225    }
6226
6227    struct RejectingKey;
6228
6229    impl VerifyingKey for RejectingKey {
6230        fn verify(
6231            &self,
6232            _algorithm: SignatureAlgorithm,
6233            _signed_data: &[u8],
6234            _signature_value: &[u8],
6235        ) -> Result<bool, SignatureVerificationPipelineError> {
6236            Ok(false)
6237        }
6238    }
6239
6240    struct AcceptingKey;
6241
6242    impl VerifyingKey for AcceptingKey {
6243        fn verify(
6244            &self,
6245            _algorithm: SignatureAlgorithm,
6246            _signed_data: &[u8],
6247            _signature_value: &[u8],
6248        ) -> Result<bool, SignatureVerificationPipelineError> {
6249            Ok(true)
6250        }
6251    }
6252
6253    struct PanicResolver;
6254
6255    struct UntrustedTestResolver;
6256
6257    impl KeyResolver for UntrustedTestResolver {
6258        fn resolve<'a>(
6259            &'a self,
6260            _key_info: Option<&KeyInfo>,
6261            _algorithm: SignatureAlgorithm,
6262        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
6263            Ok(Some(Box::new(AcceptingKey)))
6264        }
6265    }
6266
6267    #[test]
6268    fn verification_requires_explicit_key_authorization() {
6269        // A resolver finding a working key must not authorize its signer.
6270        let xml = signature_with_target_reference("AQ==");
6271        let error = VerifyContext::new()
6272            .key_resolver(&UntrustedTestResolver)
6273            .verify(&xml)
6274            .expect_err("default verification must reject an untrusted resolver candidate");
6275        assert!(matches!(
6276            error,
6277            DsigError::Policy(crate::policy::PolicyViolation::KeyTrust { .. })
6278        ));
6279    }
6280
6281    #[test]
6282    fn cryptographic_only_is_explicit_and_never_reports_trust() {
6283        // Opting out of signer authorization must remain visible in the result.
6284        let mut policy = crate::policy::VerificationPolicy::default();
6285        policy.key_trust.mode = crate::policy::VerificationTrustMode::CryptographicOnly;
6286        let result = VerifyContext::new()
6287            .policy(policy)
6288            .key_resolver(&UntrustedTestResolver)
6289            .verify(&signature_with_target_reference("AQ=="))
6290            .expect("explicit mathematical mode permits an untrusted candidate");
6291        assert_eq!(result.status, DsigStatus::Valid);
6292        assert_eq!(
6293            result.key_trust,
6294            super::super::KeyTrustEvidence::NotEstablished
6295        );
6296    }
6297
6298    #[test]
6299    fn caller_key_authorization_does_not_authenticate_an_invalid_signature() {
6300        // A trusted key is not evidence that the signature itself verified.
6301        let result = VerifyContext::new()
6302            .key(&RejectingKey)
6303            .verify(&signature_with_target_reference("AQ=="))
6304            .expect("an invalid mathematical signature is a completed result");
6305        assert_eq!(
6306            result.status,
6307            DsigStatus::Invalid(FailureReason::SignatureMismatch)
6308        );
6309        assert_eq!(
6310            result.key_trust,
6311            super::super::KeyTrustEvidence::CallerTrusted
6312        );
6313    }
6314
6315    impl KeyResolver for PanicResolver {
6316        fn resolve<'a>(
6317            &'a self,
6318            _key_info: Option<&KeyInfo>,
6319            _algorithm: SignatureAlgorithm,
6320        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
6321        {
6322            panic!("resolver should not be called when references already fail");
6323        }
6324    }
6325
6326    struct MissingKeyResolver;
6327
6328    impl KeyResolver for MissingKeyResolver {
6329        fn resolve<'a>(
6330            &'a self,
6331            _key_info: Option<&KeyInfo>,
6332            _algorithm: SignatureAlgorithm,
6333        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
6334        {
6335            Ok(None)
6336        }
6337    }
6338
6339    struct ConsumingKeyInfoResolver;
6340
6341    impl KeyResolver for ConsumingKeyInfoResolver {
6342        fn resolve<'a>(
6343            &'a self,
6344            _key_info: Option<&KeyInfo>,
6345            _algorithm: SignatureAlgorithm,
6346        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
6347        {
6348            Ok(None)
6349        }
6350
6351        fn consumes_document_key_info(&self) -> bool {
6352            true
6353        }
6354    }
6355
6356    struct FallbackKeyInfoResolver;
6357
6358    impl KeyResolver for FallbackKeyInfoResolver {
6359        fn resolve_for_verification<'a>(
6360            &'a self,
6361            key_info: Option<&KeyInfo>,
6362            algorithm: SignatureAlgorithm,
6363            _policy: &crate::policy::VerificationPolicy,
6364            _provider: &dyn crate::provider::CryptoProvider,
6365        ) -> Result<Option<super::super::ResolvedVerificationKey<'a>>, DsigError> {
6366            // This test resolver explicitly authorizes its application-owned key.
6367            self.resolve(key_info, algorithm)
6368                .map(|key| key.map(super::super::ResolvedVerificationKey::CallerTrusted))
6369        }
6370
6371        fn resolve<'a>(
6372            &'a self,
6373            key_info: Option<&KeyInfo>,
6374            _algorithm: SignatureAlgorithm,
6375        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
6376        {
6377            let sources = &key_info.expect("KeyInfo must be parsed").sources;
6378            assert!(matches!(
6379                sources.as_slice(),
6380                [
6381                    super::super::parse::KeyInfoSource::RetrievalMethod { .. },
6382                    super::super::parse::KeyInfoSource::KeyName(name),
6383                ] if name == "fallback"
6384            ));
6385            Ok(Some(Box::new(AcceptingKey)))
6386        }
6387
6388        fn consumes_document_key_info(&self) -> bool {
6389            true
6390        }
6391    }
6392
6393    struct EarlyKeyInfoResolver;
6394
6395    impl KeyResolver for EarlyKeyInfoResolver {
6396        fn resolve_for_verification<'a>(
6397            &'a self,
6398            key_info: Option<&KeyInfo>,
6399            algorithm: SignatureAlgorithm,
6400            _policy: &crate::policy::VerificationPolicy,
6401            _provider: &dyn crate::provider::CryptoProvider,
6402        ) -> Result<Option<super::super::ResolvedVerificationKey<'a>>, DsigError> {
6403            // Only the selected primary application key is authorized here.
6404            self.resolve(key_info, algorithm)
6405                .map(|key| key.map(super::super::ResolvedVerificationKey::CallerTrusted))
6406        }
6407
6408        fn resolve<'a>(
6409            &'a self,
6410            key_info: Option<&KeyInfo>,
6411            _algorithm: SignatureAlgorithm,
6412        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
6413        {
6414            let sources = &key_info.expect("KeyInfo must be parsed").sources;
6415            assert!(matches!(
6416                sources.as_slice(),
6417                [
6418                    super::super::parse::KeyInfoSource::KeyName(name),
6419                    super::super::parse::KeyInfoSource::RetrievalMethod { .. },
6420                ] if name == "primary"
6421            ));
6422            Ok(Some(Box::new(AcceptingKey)))
6423        }
6424
6425        fn consumes_document_key_info(&self) -> bool {
6426            true
6427        }
6428    }
6429
6430    fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
6431        format!(
6432            r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6433  <ds:SignedInfo>
6434    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6435    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6436    <ds:Reference URI="{reference_uri}">
6437      {transforms_xml}
6438      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
6439      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6440    </ds:Reference>
6441  </ds:SignedInfo>
6442  <ds:SignatureValue>AQ==</ds:SignatureValue>
6443</ds:Signature>"#
6444        )
6445    }
6446
6447    #[test]
6448    fn retained_verification_preserves_original_authenticated_operation() {
6449        // Keep the original per-Signature graph/authentication state, not a new
6450        // set reconstructed from detached digest-result booleans.
6451        use structured_asn1::memory::{Memory, MemoryAccount};
6452        let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(16384, 100000)));
6453        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6454        let context = VerifyContext::new().key(&AcceptingKey);
6455        with_verified_document(
6456            &document,
6457            &context,
6458            &VerificationRequest::default(),
6459            ScopedRequestStorage(&work),
6460            |view, _, budgets, evidence, operations, _| {
6461                assert_eq!(evidence.signatures().len(), 1);
6462                assert_eq!(operations.len(), 1);
6463                let target = view.node_for_id("target", &[]).unwrap();
6464                assert!(operations[0].operation.is_authenticated(target));
6465                assert!(std::ptr::eq(*operations[0].operation.budgets(), budgets));
6466                assert!(budgets.canonicalized.remaining() < budgets.canonicalized.max_bytes);
6467                Ok(())
6468            },
6469        )
6470        .unwrap();
6471    }
6472
6473    #[test]
6474    fn retained_verification_accepts_borrowed_keys_and_admits_storage_first() {
6475        // The scoped storage projection must not impose 'static on request
6476        // keys. Refused report/state storage cannot reach the provider.
6477        use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
6478        struct BorrowedKey<'a>(&'a Cell<usize>);
6479        impl VerifyingKey for BorrowedKey<'_> {
6480            fn validate_signature_value(
6481                &self,
6482                _: SignatureAlgorithm,
6483                _: &[u8],
6484            ) -> Result<bool, DsigError> {
6485                Ok(true)
6486            }
6487            fn verify(&self, _: SignatureAlgorithm, _: &[u8], _: &[u8]) -> Result<bool, DsigError> {
6488                self.0.set(self.0.get() + 1);
6489                Ok(true)
6490            }
6491        }
6492        let calls = Cell::new(0);
6493        let key = BorrowedKey(&calls);
6494        let context = VerifyContext::new().key(&key);
6495        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6496        let denied = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 100000)));
6497        let result: Result<(), DsigError> = with_verified_document(
6498            &document,
6499            &context,
6500            &VerificationRequest::default(),
6501            ScopedRequestStorage(&denied),
6502            |_, _, _, _, _, _| panic!("refused storage must not reach the continuation"),
6503        );
6504        assert!(matches!(
6505            result,
6506            Err(DsigError::SharedExecution(MemoryError::LimitExceeded))
6507        ));
6508        assert_eq!(calls.get(), 0);
6509        let admitted = Memory::new(std::sync::Arc::new(MemoryAccount::new(16384, 100000)));
6510        with_verified_document(
6511            &document,
6512            &context,
6513            &VerificationRequest::default(),
6514            ScopedRequestStorage(&admitted),
6515            |_, _, _, evidence, operations, _| {
6516                assert!(evidence.all_valid());
6517                assert_eq!(operations.len(), 1);
6518                Ok(())
6519            },
6520        )
6521        .unwrap();
6522        assert_eq!(calls.get(), 1);
6523    }
6524
6525    #[test]
6526    fn verified_continuation_keeps_the_consumed_operation_budget() {
6527        // Extension work continues after core verification with the very same
6528        // canonicalization allowance, not a fresh budget built from policy.
6529        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6530        let context = VerifyContext::new().key(&AcceptingKey);
6531        let called = Cell::new(false);
6532        let evidence = with_verified_document(
6533            &document,
6534            &context,
6535            &VerificationRequest::default(),
6536            CollectedRequestStorage,
6537            |view, _captured_context, budgets, evidence, (), _| {
6538                called.set(true);
6539                assert_eq!(view.root(), evidence.document);
6540                assert!(evidence.signatures()[0].result().is_ok());
6541                let remaining = budgets.canonicalized.remaining();
6542                assert!(remaining < budgets.canonicalized.max_bytes);
6543                budgets.canonicalized.charge(remaining).unwrap();
6544                assert!(budgets.canonicalized.charge(1).is_err());
6545                Ok(evidence)
6546            },
6547        )
6548        .unwrap();
6549        assert!(called.get());
6550        assert_eq!(evidence.signatures().len(), 1);
6551    }
6552
6553    #[test]
6554    fn public_session_catalogue_replays_the_original_reference() {
6555        // The public continuation must retain the selected operation, expose
6556        // original Reference/parent identities, and stream the verified bytes.
6557        use structured_asn1::memory::{Memory, MemoryAccount};
6558        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6559        let context = VerifyContext::new().key(&AcceptingKey);
6560        let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
6561        with_verified_request(
6562            &document,
6563            &context,
6564            &VerificationRequest::default(),
6565            &work,
6566            |session| {
6567                let signature = session.evidence().signatures()[0].identity();
6568                let selected = session.select_signature(signature)?;
6569                let mut count = 0;
6570                let completeness = selected
6571                    .visit_authenticated_references(|reference| {
6572                        count += 1;
6573                        assert!(matches!(reference.parent, ReferenceParent::SignedInfo(_)));
6574                        assert_eq!(reference.uri, Some("#target"));
6575                        let mut bytes = Vec::new();
6576                        let outcome = selected
6577                            .replay_reference(
6578                                reference.identity,
6579                                ReplayMode::ReferenceOutput,
6580                                &mut |chunk: &[u8]| {
6581                                    bytes.extend_from_slice(chunk);
6582                                    Ok::<_, ()>(())
6583                                },
6584                            )
6585                            .unwrap();
6586                        assert_eq!(outcome, ReplayOutcome::Complete);
6587                        assert_eq!(bytes, b"<target ID=\"target\">payload</target>");
6588                        Ok::<_, ()>(())
6589                    })
6590                    .unwrap();
6591                assert_eq!(completeness, CatalogueCompleteness::Complete);
6592                assert_eq!(count, 1);
6593                Ok(())
6594            },
6595        )
6596        .unwrap();
6597    }
6598
6599    #[test]
6600    fn public_session_resolves_original_attribute_and_fragmented_simple_content() {
6601        // Both schema lexical sources must resolve through the same original
6602        // ID index. Fragmented text must not drop text after comments or CDATA.
6603        use structured_asn1::memory::{Memory, MemoryAccount};
6604        let document = XmlDocument::parse(signature_with_target_content(
6605            "AQ==",
6606            "<property URI=\"#ref\"/><content>#r<!--split--><![CDATA[ef]]></content>",
6607        ))
6608        .unwrap();
6609        let context = VerifyContext::new().key(&AcceptingKey);
6610        let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
6611        with_verified_request(
6612            &document,
6613            &context,
6614            &VerificationRequest::default(),
6615            &work,
6616            |session| {
6617                let selected =
6618                    session.select_signature(session.evidence().signatures()[0].identity())?;
6619                for (name, source) in [
6620                    (
6621                        "property",
6622                        LexicalSource::Attribute {
6623                            namespace: None,
6624                            local_name: "URI",
6625                        },
6626                    ),
6627                    ("content", LexicalSource::SimpleContent),
6628                ] {
6629                    let node = session
6630                        .view()
6631                        .document()
6632                        .descendants()
6633                        .find(|node| node.has_tag_name(name))
6634                        .unwrap();
6635                    let ReferenceResolution::Reference(reference) =
6636                        selected.resolve_reference(session.view().node_identity(node), source)?
6637                    else {
6638                        panic!("original Reference must resolve");
6639                    };
6640                    assert_eq!(reference.uri, Some("#target"));
6641                }
6642                Ok(())
6643            },
6644        )
6645        .unwrap();
6646    }
6647
6648    #[test]
6649    fn public_session_manifest_instruction_survives_old_digest_mismatch() {
6650        // Renewal covers the authenticated instruction with a new digest. It
6651        // must not require the old target digest to match first (EN §5.5.3).
6652        use structured_asn1::memory::{Memory, MemoryAccount};
6653        let document = XmlDocument::parse(signature_with_manifest_xml(false)).unwrap();
6654        let context = VerifyContext::new()
6655            .key(&AcceptingKey)
6656            .process_manifests(true);
6657        let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
6658        with_verified_request(
6659            &document,
6660            &context,
6661            &VerificationRequest::default(),
6662            &work,
6663            |session| {
6664                let selected =
6665                    session.select_signature(session.evidence().signatures()[0].identity())?;
6666                let mut manifests = 0;
6667                let completeness = selected
6668                    .visit_authenticated_references(|reference| {
6669                        if let ReferenceParent::Manifest(_) = reference.parent {
6670                            manifests += 1;
6671                            assert!(matches!(
6672                                reference.result.unwrap().status,
6673                                DsigStatus::Invalid(_)
6674                            ));
6675                            let mut calls = 0;
6676                            let outcome = selected
6677                                .replay_reference(
6678                                    reference.identity,
6679                                    ReplayMode::ReferenceOutput,
6680                                    &mut |_: &[u8]| {
6681                                        calls += 1;
6682                                        Ok::<_, ()>(())
6683                                    },
6684                                )
6685                                .unwrap();
6686                            assert_eq!(outcome, ReplayOutcome::Complete);
6687                            assert!(calls > 0);
6688                        }
6689                        Ok::<_, ()>(())
6690                    })
6691                    .unwrap();
6692                assert_eq!(manifests, 1);
6693                assert_eq!(completeness, CatalogueCompleteness::Complete);
6694                Ok(())
6695            },
6696        )
6697        .unwrap();
6698    }
6699
6700    #[test]
6701    fn public_session_replay_keeps_original_receiver_failure_sticky() {
6702        // No later C14N fragment may overwrite a typed caller refusal or invoke
6703        // the receiver a second time after the first error.
6704        use structured_asn1::memory::{Memory, MemoryAccount};
6705        #[derive(Debug, PartialEq)]
6706        struct Refusal(u32);
6707        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6708        let context = VerifyContext::new().key(&AcceptingKey);
6709        let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
6710        with_verified_request(
6711            &document,
6712            &context,
6713            &VerificationRequest::default(),
6714            &work,
6715            |session| {
6716                let selected =
6717                    session.select_signature(session.evidence().signatures()[0].identity())?;
6718                selected
6719                    .visit_authenticated_references(|reference| {
6720                        let mut calls = 0;
6721                        let result = selected.replay_reference(
6722                            reference.identity,
6723                            ReplayMode::ReferenceOutput,
6724                            &mut |_: &[u8]| {
6725                                calls += 1;
6726                                Err(Refusal(7))
6727                            },
6728                        );
6729                        assert!(matches!(result, Err(ReplayError::Receiver(Refusal(7)))));
6730                        assert_eq!(calls, 1);
6731                        Ok::<_, ()>(())
6732                    })
6733                    .unwrap();
6734                Ok(())
6735            },
6736        )
6737        .unwrap();
6738    }
6739
6740    #[test]
6741    fn verify_all_retains_each_signature_without_vacuous_success() {
6742        // Malformed signatures remain individually visible; an empty document
6743        // never satisfies a request merely because there were no failures.
6744        let document = XmlDocument::parse(format!(
6745            "<root xmlns:ds=\"{XMLDSIG_NS}\"><ds:Signature/><ds:Signature/></root>"
6746        ))
6747        .unwrap();
6748        let evidence = VerifyContext::new().verify_all(&document).unwrap();
6749        assert_eq!(evidence.signatures().len(), 2);
6750        assert!(
6751            evidence
6752                .signatures()
6753                .iter()
6754                .all(|entry| entry.result().is_err())
6755        );
6756        assert!(!evidence.all_valid());
6757        let empty = XmlDocument::parse("<root/>").unwrap();
6758        assert!(!VerifyContext::new().verify_all(&empty).unwrap().all_valid());
6759    }
6760
6761    #[test]
6762    fn verify_all_checks_cardinality_before_running_keys() {
6763        // The cardinality policy bounds report allocation and crypto work,
6764        // including malformed signatures that never reach core validation.
6765        let document = XmlDocument::parse(format!(
6766            "<root xmlns:ds=\"{XMLDSIG_NS}\"><ds:Signature/><ds:Signature/></root>"
6767        ))
6768        .unwrap();
6769        let mut policy = crate::policy::VerificationPolicy::default();
6770        policy.resources.max_signatures = 1;
6771        assert!(matches!(
6772            VerifyContext::new().policy(policy).verify_all(&document),
6773            Err(DsigError::Policy(
6774                crate::policy::PolicyViolation::ResourceLimit {
6775                    resource: "verification signatures",
6776                    maximum: 1,
6777                    actual: 2,
6778                }
6779            ))
6780        ));
6781    }
6782
6783    #[test]
6784    fn verification_signature_limit_also_gates_the_single_api() {
6785        // The same compiled resource policy must apply to both public paths.
6786        let mut policy = crate::policy::VerificationPolicy::default();
6787        policy.resources.max_signatures = 0;
6788        assert!(matches!(
6789            VerifyContext::new()
6790                .policy(policy)
6791                .key(&AcceptingKey)
6792                .verify(&signature_with_target_reference("AQ==")),
6793            Err(DsigError::Policy(
6794                crate::policy::PolicyViolation::ResourceLimit {
6795                    resource: "verification signatures",
6796                    maximum: 0,
6797                    actual: 1,
6798                }
6799            ))
6800        ));
6801    }
6802
6803    #[test]
6804    fn verify_all_preserves_generation_and_rejects_wrapped_siblings() {
6805        // A signature over one element never authenticates a lookalike sibling.
6806        let xml = signature_with_target_reference("AQ==")
6807            .replace("</root>", "<target ID=\"unsigned\">payload</target></root>");
6808        let document = XmlDocument::parse(xml).unwrap();
6809        let (signed, unsigned) = document.with_view(|view| {
6810            (
6811                view.node_for_id("target", &[]).unwrap(),
6812                view.node_for_id("unsigned", &[]).unwrap(),
6813            )
6814        });
6815        let evidence = VerifyContext::new()
6816            .key(&AcceptingKey)
6817            .verify_all(&document)
6818            .unwrap();
6819        assert!(evidence.all_valid());
6820        assert!(evidence.covers_element(&document, signed).unwrap());
6821        assert!(!evidence.covers_element(&document, unsigned).unwrap());
6822        let foreign = XmlDocument::parse("<target/>").unwrap();
6823        let target = foreign.with_view(|view| view.root_element());
6824        assert!(matches!(
6825            evidence.covers_element(&document, target),
6826            Err(DsigError::Document(XmlDocumentError::ForeignIdentity))
6827        ));
6828    }
6829
6830    #[test]
6831    fn verify_request_rejects_duplicate_targets_before_crypto() {
6832        // Ambiguous request mapping is not a successful duplicate assertion.
6833        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6834        let target = document.with_view(|view| view.node_for_id("target", &[]).unwrap());
6835        let targets = [target, target];
6836        let request = VerificationRequest {
6837            expected_targets: &targets,
6838            ..VerificationRequest::default()
6839        };
6840        assert!(matches!(
6841            VerifyContext::new().verify_request(&document, &request),
6842            Err(DsigError::InvalidRequest {
6843                reason: "duplicate expected coverage target"
6844            })
6845        ));
6846    }
6847
6848    #[test]
6849    fn verify_all_shares_canonicalization_retention_budget() {
6850        // Repeating a Signature may not reset the retained-data allowance.
6851        let xml = signature_with_target_reference("AQ==");
6852        let single = VerifyContext::new()
6853            .key(&AcceptingKey)
6854            .store_pre_digest(true)
6855            .verify(&xml)
6856            .unwrap();
6857        let bytes = single.canonicalized_signed_info.unwrap().len()
6858            + single.signed_info_references[0]
6859                .pre_digest_data
6860                .as_ref()
6861                .unwrap()
6862                .len();
6863        let parsed = Document::parse(&xml).unwrap();
6864        let signature = parsed
6865            .descendants()
6866            .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
6867            .unwrap();
6868        let xml = xml.replace("</root>", &format!("{}</root>", &xml[signature.range()]));
6869        let document = XmlDocument::parse(xml).unwrap();
6870        let mut policy = crate::policy::VerificationPolicy::default();
6871        policy.resources.max_canonicalized_bytes = bytes * 2 - 1;
6872        let evidence = VerifyContext::new()
6873            .key(&AcceptingKey)
6874            .store_pre_digest(true)
6875            .policy(policy)
6876            .verify_all(&document)
6877            .unwrap();
6878        assert_eq!(evidence.signatures.len(), 2);
6879        assert_eq!(
6880            evidence.signatures[0].result().as_ref().unwrap().status,
6881            DsigStatus::Valid
6882        );
6883        assert!(matches!(
6884            evidence.signatures[1].result(),
6885            Err(DsigError::Policy(
6886                crate::policy::PolicyViolation::ResourceLimit {
6887                    resource: "canonicalized bytes",
6888                    ..
6889                }
6890            ))
6891        ));
6892        assert!(!evidence.accepted(&document).unwrap());
6893    }
6894
6895    #[test]
6896    fn verify_all_manifest_coverage_requires_its_authenticated_digest() {
6897        // A valid enclosing Signature authenticates the Manifest declaration,
6898        // not a target whose separately checked Manifest digest mismatches.
6899        for valid in [true, false] {
6900            let document = XmlDocument::parse(signature_with_manifest_xml(valid)).unwrap();
6901            let target = document.with_view(|view| view.node_for_id("target", &[]).unwrap());
6902            let evidence = VerifyContext::new()
6903                .key(&AcceptingKey)
6904                .process_manifests(true)
6905                .verify_all(&document)
6906                .unwrap();
6907            let result = evidence.signatures()[0].result().as_ref().unwrap();
6908            assert_eq!(result.manifest_references.len(), 1);
6909            assert!(result.manifest_references[0].reference_identity.is_some());
6910            assert_eq!(evidence.covers_element(&document, target).unwrap(), valid);
6911            assert_eq!(evidence.accepted(&document).unwrap(), valid);
6912        }
6913    }
6914
6915    #[test]
6916    fn verify_all_filtered_digest_does_not_claim_original_subtree() {
6917        // A projection may discard content even when its local digest matches.
6918        // No arbitrary filter is promoted to complete original-node coverage.
6919        let mut reference = make_reference(
6920            "#target",
6921            vec![Transform::XpathExcludeAllSignatures],
6922            DigestAlgorithm::Sha256,
6923            vec![],
6924        );
6925        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6926        document.with_view(|view| {
6927            let target = view.node_for_id("target", &[]).unwrap();
6928            let mut result = manifest_reference_invalid_result(
6929                &reference,
6930                0,
6931                FailureReason::ReferenceProcessingFailure { ref_index: 0 },
6932            );
6933            result.status = DsigStatus::Valid;
6934            bind_reference_evidence(
6935                &mut result,
6936                &reference,
6937                view,
6938                None,
6939                &OperationResourceIdentity::DocumentNode(target),
6940            );
6941            assert_eq!(result.target_identity, Some(target));
6942            assert_eq!(result.coverage, ReferenceCoverage::TransformedData);
6943            reference.transforms = vec![Transform::C14n(crate::c14n::C14nAlgorithm::new(
6944                crate::c14n::C14nMode::Exclusive1_0,
6945                false,
6946            ))];
6947            bind_reference_evidence(
6948                &mut result,
6949                &reference,
6950                view,
6951                None,
6952                &OperationResourceIdentity::DocumentNode(target),
6953            );
6954            assert_eq!(result.coverage, ReferenceCoverage::ElementContent);
6955        });
6956    }
6957
6958    #[test]
6959    fn verify_request_rejects_conflicting_resource_owners() {
6960        // There is no implicit override between request bytes and old builder
6961        // configuration: ownership must be explicit before resolver work.
6962        let document = XmlDocument::parse("<root/>").unwrap();
6963        let resources = HashMap::new();
6964        let request = VerificationRequest {
6965            external_resources: Some(&resources),
6966            ..VerificationRequest::default()
6967        };
6968        assert!(matches!(
6969            VerifyContext::new()
6970                .external_resources(&resources)
6971                .verify_request(&document, &request),
6972            Err(DsigError::InvalidRequest {
6973                reason: "external resources configured in both context and request"
6974            })
6975        ));
6976    }
6977
6978    fn signature_with_target_reference(signature_value_b64: &str) -> String {
6979        signature_with_target_content(signature_value_b64, "payload")
6980            .replace("<ds:Reference Id=\"ref\"", "<ds:Reference")
6981    }
6982
6983    fn signature_with_target_content(signature_value_b64: &str, content: &str) -> String {
6984        let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6985  <target ID="target">payload</target>
6986  <ds:Signature>
6987    <ds:SignedInfo>
6988      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6989      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6990      <ds:Reference Id="ref" URI="#target">
6991        <ds:Transforms>
6992          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6993        </ds:Transforms>
6994        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
6995        <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6996      </ds:Reference>
6997    </ds:SignedInfo>
6998    <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
6999  </ds:Signature>
7000</root>"##
7001            .replace(">payload</target>", &format!(">{content}</target>"));
7002
7003        let doc = Document::parse(&xml_template).unwrap();
7004        let sig_node = doc
7005            .descendants()
7006            .find(|node| node.is_element() && node.tag_name().name() == "Signature")
7007            .unwrap();
7008        let signed_info_node = sig_node
7009            .children()
7010            .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
7011            .unwrap();
7012        let signed_info = parse_signed_info(signed_info_node).unwrap();
7013        let reference = &signed_info.references[0];
7014        let resolver = UriReferenceResolver::new(&doc);
7015        let initial_data = resolver
7016            .dereference(reference.uri.as_deref().unwrap())
7017            .unwrap();
7018        let pre_digest =
7019            crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
7020                .unwrap();
7021        let digest = compute_digest(reference.digest_method, &pre_digest);
7022        let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
7023        xml_template
7024            .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
7025            .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
7026    }
7027
7028    #[test]
7029    fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
7030        let xml = signature_with_target_reference("AQ==");
7031
7032        let result = VerifyContext::new()
7033            .verify(&xml)
7034            .expect("missing key config must be reported as verification status");
7035        assert!(
7036            matches!(
7037                result.status,
7038                DsigStatus::Invalid(FailureReason::KeyNotFound)
7039            ),
7040            "unexpected status: {:?}",
7041            result.status
7042        );
7043    }
7044
7045    #[test]
7046    fn verify_context_rejects_disallowed_uri() {
7047        let xml = minimal_signature_xml("http://example.com/external", "");
7048        let err = VerifyContext::new()
7049            .key(&RejectingKey)
7050            .verify(&xml)
7051            .expect_err("external URI should be rejected by default policy");
7052        assert!(matches!(
7053            err,
7054            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
7055                operation: "verification",
7056                ..
7057            })
7058        ));
7059    }
7060
7061    #[test]
7062    fn verify_context_bounds_effective_xml_base_components() {
7063        // External URI resolution must stop before repeatedly copying an
7064        // attacker-controlled chain of effective XML Base values.
7065        let mut xml = minimal_signature_xml("payload", "");
7066        for _ in 0..65 {
7067            xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
7068        }
7069        let resources = HashMap::new();
7070        let error = VerifyContext::new()
7071            .key(&AcceptingKey)
7072            .allowed_uri_types(UriTypeSet::ALL)
7073            .external_resources(&resources)
7074            .verify(&xml)
7075            .expect_err("XML Base component work must be bounded before lookup");
7076
7077        assert!(matches!(
7078            error,
7079            SignatureVerificationPipelineError::Policy(
7080                crate::policy::PolicyViolation::ResourceLimit {
7081                    resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
7082                    maximum: 64,
7083                    actual: 65,
7084                }
7085            )
7086        ));
7087    }
7088
7089    #[test]
7090    fn verify_context_bounds_cumulative_xml_base_resolution_bytes() {
7091        // The operation-wide byte budget charges intermediate URI copies, not
7092        // merely the small final external resource returned by the caller map.
7093        let mut xml = minimal_signature_xml("payload", "");
7094        for _ in 0..2 {
7095            xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
7096        }
7097        let resources = HashMap::new();
7098        let mut policy = crate::policy::VerificationPolicy::default();
7099        policy.resources.max_xml_base_resolution_bytes = 32;
7100        let error = VerifyContext::new()
7101            .policy(policy)
7102            .key(&AcceptingKey)
7103            .allowed_uri_types(UriTypeSet::ALL)
7104            .external_resources(&resources)
7105            .verify(&xml)
7106            .expect_err("cumulative XML Base copies must obey the operation budget");
7107
7108        assert!(matches!(
7109            error,
7110            SignatureVerificationPipelineError::Policy(
7111                crate::policy::PolicyViolation::ResourceLimit {
7112                    resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
7113                    maximum: 32,
7114                    ..
7115                }
7116            )
7117        ));
7118    }
7119
7120    #[test]
7121    fn verify_context_applies_xml_base_policy_to_signed_info_c14n() {
7122        // The SignedInfo node-set excludes its ancestors, so C14N 1.1 must
7123        // resolve their inherited xml:base values through the same operation
7124        // budget already used by Reference processing.
7125        let xml = signature_with_target_reference("AQ==")
7126            .replacen(
7127                "http://www.w3.org/2001/10/xml-exc-c14n#",
7128                "http://www.w3.org/2006/12/xml-c14n11",
7129                1,
7130            )
7131            .replace(
7132                "  <ds:Signature>",
7133                "  <outer xml:base=\"one/\"><inner xml:base=\"two/\"><ds:Signature>",
7134            )
7135            .replace("  </ds:Signature>", "  </ds:Signature></inner></outer>");
7136        let policy = crate::policy::VerificationPolicy {
7137            resources: crate::policy::ResourcePolicy {
7138                max_xml_base_components: 1,
7139                ..crate::policy::ResourcePolicy::default()
7140            },
7141            ..crate::policy::VerificationPolicy::default()
7142        };
7143
7144        let error = VerifyContext::new()
7145            .key(&AcceptingKey)
7146            .policy(policy)
7147            .verify(&xml)
7148            .expect_err("SignedInfo C14N must use the operation XML Base budget");
7149
7150        assert!(matches!(
7151            error,
7152            SignatureVerificationPipelineError::Policy(
7153                crate::policy::PolicyViolation::ResourceLimit {
7154                    resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
7155                    maximum: 1,
7156                    actual: 2,
7157                }
7158            )
7159        ));
7160    }
7161
7162    #[test]
7163    fn verify_context_classifies_signed_info_xml_base_byte_limit_as_policy() {
7164        // SignedInfo C14N 1.1 XML Base work is policy enforcement, not a
7165        // malformed canonicalization request, and must retain typed diagnostics.
7166        let xml = signature_with_target_reference("AQ==")
7167            .replacen(
7168                "http://www.w3.org/2001/10/xml-exc-c14n#",
7169                "http://www.w3.org/2006/12/xml-c14n11",
7170                1,
7171            )
7172            .replace(
7173                "  <ds:Signature>",
7174                "  <outer xml:base=\"segment/\"><ds:Signature>",
7175            )
7176            .replace("  </ds:Signature>", "  </ds:Signature></outer>");
7177        let policy = crate::policy::VerificationPolicy {
7178            resources: crate::policy::ResourcePolicy {
7179                max_xml_base_resolution_bytes: 1,
7180                ..crate::policy::ResourcePolicy::default()
7181            },
7182            ..crate::policy::VerificationPolicy::default()
7183        };
7184
7185        let error = VerifyContext::new()
7186            .key(&AcceptingKey)
7187            .policy(policy)
7188            .verify(&xml)
7189            .expect_err("SignedInfo XML Base byte exhaustion must be a policy error");
7190
7191        assert!(matches!(
7192            error,
7193            SignatureVerificationPipelineError::Policy(
7194                crate::policy::PolicyViolation::ResourceLimit {
7195                    resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
7196                    maximum: 1,
7197                    actual,
7198                }
7199            ) if actual > 1
7200        ));
7201    }
7202
7203    #[test]
7204    fn verify_context_meters_repeated_external_dereferences() {
7205        // One caller-owned entry can be referenced repeatedly. The aggregate
7206        // ceiling bounds bytes cloned and processed, not just unique map data.
7207        let payload = b"payload";
7208        let digest = base64::engine::general_purpose::STANDARD.encode(
7209            crate::xmldsig::compute_digest(DigestAlgorithm::Sha1, payload),
7210        );
7211        let reference = format!(
7212            r#"<ds:Reference URI="urn:payload"><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference>"#
7213        );
7214        let xml = format!(
7215            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>{reference}{reference}</ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature>"#
7216        );
7217        let resources = HashMap::from([("urn:payload".to_owned(), payload.to_vec())]);
7218        let policy = crate::policy::VerificationPolicy {
7219            uris: crate::policy::UriPolicy {
7220                references: UriTypeSet::ALL,
7221                ..crate::policy::UriPolicy::default()
7222            },
7223            resources: crate::policy::ResourcePolicy {
7224                max_external_resource_bytes: payload.len(),
7225                max_external_resource_total_bytes: payload.len(),
7226                ..crate::policy::ResourcePolicy::default()
7227            },
7228            ..crate::policy::VerificationPolicy::default()
7229        };
7230
7231        let error = VerifyContext::new()
7232            .key(&AcceptingKey)
7233            .policy(policy)
7234            .external_resources(&resources)
7235            .verify(&xml)
7236            .expect_err("the second dereference must exhaust the aggregate byte ceiling");
7237
7238        assert!(
7239            error
7240                .to_string()
7241                .contains("aggregate external resource bytes")
7242        );
7243    }
7244
7245    #[test]
7246    fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
7247        let xml = minimal_signature_xml("", "");
7248        let err = VerifyContext::new()
7249            .key(&RejectingKey)
7250            .allowed_uri_types(UriTypeSet::new(false, true, false))
7251            .verify(&xml)
7252            .expect_err("empty URI must be rejected when empty references are disabled");
7253        assert!(matches!(
7254            err,
7255            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
7256                operation: "verification",
7257                ..
7258            })
7259        ));
7260    }
7261
7262    #[test]
7263    fn verify_context_rejects_disallowed_transform() {
7264        let xml = minimal_signature_xml(
7265            "",
7266            r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
7267        );
7268        let err = VerifyContext::new()
7269            .key(&RejectingKey)
7270            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
7271            .verify(&xml)
7272            .expect_err("enveloped transform should be rejected by allowlist");
7273        assert!(matches!(
7274            err,
7275            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
7276                operation: "verification transform",
7277                ..
7278            })
7279        ));
7280    }
7281
7282    #[test]
7283    fn verify_context_applies_transform_allowlist_to_signed_info_c14n() {
7284        // Reference C14N remains allowlisted; only the distinct SignedInfo
7285        // canonicalization method should trigger this policy rejection.
7286        let xml = signature_with_target_reference("AQ==").replacen(
7287            "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
7288            "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"/>",
7289            1,
7290        );
7291        let error = VerifyContext::new()
7292            .key(&AcceptingKey)
7293            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
7294            .verify(&xml)
7295            .expect_err("SignedInfo C14N must obey the operation transform allowlist");
7296
7297        assert!(matches!(
7298            error,
7299            SignatureVerificationPipelineError::Policy(
7300                crate::policy::PolicyViolation::Algorithm {
7301                    operation: "verification transform",
7302                    ref algorithm,
7303                }
7304            )
7305                if algorithm == "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
7306        ));
7307    }
7308
7309    #[test]
7310    fn verify_context_applies_transform_allowlist_to_key_retrieval() {
7311        // The reference and SignedInfo both use exclusive C14N. The only XPath
7312        // operation is document-selected key retrieval and must be rejected.
7313        let xml = signature_with_target_reference("AQ==")
7314            .replacen(
7315                "</ds:Signature>",
7316                r##"<ds:KeyInfo><ds:RetrievalMethod URI="#keys" Type="http://www.w3.org/2000/09/xmldsig#X509Data"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath></ds:Transform></ds:Transforms></ds:RetrievalMethod></ds:KeyInfo></ds:Signature>"##,
7317                1,
7318            )
7319            .replacen(
7320                "</root>",
7321                r#"<holder ID="keys"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder></root>"#,
7322                1,
7323            );
7324        let error = VerifyContext::new()
7325            .key_resolver(&ConsumingKeyInfoResolver)
7326            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
7327            .verify(&xml)
7328            .expect_err("RetrievalMethod XPath must obey the operation transform allowlist");
7329
7330        assert!(matches!(
7331            error,
7332            SignatureVerificationPipelineError::Policy(
7333                crate::policy::PolicyViolation::Algorithm {
7334                    operation: "verification transform",
7335                    ref algorithm,
7336                }
7337            )
7338                if algorithm == XPATH_TRANSFORM_URI
7339        ));
7340    }
7341
7342    fn signature_with_manifest_xml(valid_manifest_digest: bool) -> String {
7343        signature_with_manifest_xml_with_manifest_mutation(valid_manifest_digest, |xml| xml)
7344    }
7345
7346    fn signature_with_manifest_xml_with_manifest_mutation<F>(
7347        valid_manifest_digest: bool,
7348        mutate_manifest: F,
7349    ) -> String
7350    where
7351        F: FnOnce(String) -> String,
7352    {
7353        const TMP_SIGNED_INFO_DIGEST: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
7354        const INVALID_MANIFEST_DIGEST: &str = "//////////////////////////8=";
7355        let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7356  <target ID="target">payload</target>
7357  <ds:Signature>
7358    <ds:SignedInfo>
7359      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7360      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
7361      <ds:Reference URI="#manifest">
7362        <ds:Transforms>
7363          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7364        </ds:Transforms>
7365        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
7366        <ds:DigestValue>SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER</ds:DigestValue>
7367      </ds:Reference>
7368    </ds:SignedInfo>
7369    <ds:SignatureValue>AQ==</ds:SignatureValue>
7370    <ds:Object>
7371      <ds:Manifest ID="manifest">
7372        <ds:Reference URI="#target">
7373          <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
7374          <ds:DigestValue>MANIFEST_DIGEST_PLACEHOLDER</ds:DigestValue>
7375        </ds:Reference>
7376      </ds:Manifest>
7377    </ds:Object>
7378  </ds:Signature>
7379</root>"##;
7380        let seed_xml = xml_template.replace(
7381            "SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER",
7382            TMP_SIGNED_INFO_DIGEST,
7383        );
7384        let doc = Document::parse(&seed_xml).unwrap();
7385        let signature_node = doc
7386            .descendants()
7387            .find(|node| {
7388                node.is_element()
7389                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
7390                    && node.tag_name().name() == "Signature"
7391            })
7392            .unwrap();
7393        let resolver = UriReferenceResolver::new(&doc);
7394        let initial_data = resolver.dereference("#target").unwrap();
7395        let manifest_pre_digest =
7396            crate::xmldsig::execute_transforms(signature_node, initial_data, &[]).unwrap();
7397        let computed_manifest_digest_b64 = base64::engine::general_purpose::STANDARD
7398            .encode(compute_digest(DigestAlgorithm::Sha1, &manifest_pre_digest));
7399        let final_manifest_digest_b64 = if valid_manifest_digest {
7400            computed_manifest_digest_b64.as_str()
7401        } else {
7402            INVALID_MANIFEST_DIGEST
7403        };
7404        let xml_with_manifest_digest = mutate_manifest(
7405            seed_xml.replace("MANIFEST_DIGEST_PLACEHOLDER", final_manifest_digest_b64),
7406        );
7407        let signed_doc = Document::parse(&xml_with_manifest_digest).unwrap();
7408        let signed_signature_node = signed_doc
7409            .descendants()
7410            .find(|node| {
7411                node.is_element()
7412                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
7413                    && node.tag_name().name() == "Signature"
7414            })
7415            .unwrap();
7416        let signed_info_node = signed_signature_node
7417            .children()
7418            .find(|node| {
7419                node.is_element()
7420                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
7421                    && node.tag_name().name() == "SignedInfo"
7422            })
7423            .unwrap();
7424        let signed_info = parse_signed_info(signed_info_node).unwrap();
7425        let object_reference = &signed_info.references[0];
7426        let signed_resolver = UriReferenceResolver::new(&signed_doc);
7427        let signed_initial_data = signed_resolver
7428            .dereference(object_reference.uri.as_deref().unwrap())
7429            .unwrap();
7430        let signed_pre_digest = crate::xmldsig::execute_transforms(
7431            signed_signature_node,
7432            signed_initial_data,
7433            &object_reference.transforms,
7434        )
7435        .unwrap();
7436        let signed_digest_b64 = base64::engine::general_purpose::STANDARD.encode(compute_digest(
7437            object_reference.digest_method,
7438            &signed_pre_digest,
7439        ));
7440
7441        xml_with_manifest_digest.replacen(TMP_SIGNED_INFO_DIGEST, &signed_digest_b64, 1)
7442    }
7443
7444    fn replace_fixture_manifest_digest(xml: &str, replacement: &str) -> String {
7445        let object_marker = "<ds:Object>";
7446        let object_start = xml
7447            .find(object_marker)
7448            .expect("fixture should contain ds:Object")
7449            + object_marker.len();
7450        let open = "<ds:DigestValue>";
7451        let close = "</ds:DigestValue>";
7452        let value_start = xml[object_start..]
7453            .find(open)
7454            .map(|offset| object_start + offset + open.len())
7455            .expect("Manifest should contain DigestValue");
7456        let value_end = xml[value_start..]
7457            .find(close)
7458            .map(|offset| value_start + offset)
7459            .expect("Manifest DigestValue must be closed");
7460
7461        format!("{}{replacement}{}", &xml[..value_start], &xml[value_end..])
7462    }
7463
7464    #[test]
7465    fn verify_context_processes_manifest_references_when_enabled() {
7466        let xml = signature_with_manifest_xml(true);
7467
7468        let result_without_manifests = VerifyContext::new()
7469            .key(&RejectingKey)
7470            .verify(&xml)
7471            .expect("manifest processing disabled should still verify SignedInfo");
7472        assert!(
7473            result_without_manifests.manifest_references.is_empty(),
7474            "manifest results must stay empty when manifest processing is disabled",
7475        );
7476        assert!(matches!(
7477            result_without_manifests.status,
7478            DsigStatus::Invalid(FailureReason::SignatureMismatch)
7479        ));
7480
7481        let malformed_manifest_xml = signature_with_manifest_xml(true).replacen(
7482            "</ds:Object>",
7483            "</ds:Object><ds:Object><ds:Manifest><ds:Foo/></ds:Manifest></ds:Object>",
7484            1,
7485        );
7486        let malformed_with_manifests_disabled = VerifyContext::new()
7487            .key(&RejectingKey)
7488            .verify(&malformed_manifest_xml)
7489            .expect("malformed Manifest must be ignored when manifest processing is disabled");
7490        assert!(
7491            malformed_with_manifests_disabled
7492                .manifest_references
7493                .is_empty(),
7494            "manifest parser must not run when process_manifests is disabled",
7495        );
7496        assert!(matches!(
7497            malformed_with_manifests_disabled.status,
7498            DsigStatus::Invalid(FailureReason::SignatureMismatch)
7499        ));
7500
7501        let result_with_manifests = VerifyContext::new()
7502            .key(&AcceptingKey)
7503            .process_manifests(true)
7504            .verify(&xml)
7505            .expect("manifest references should be processed when enabled");
7506        assert_eq!(result_with_manifests.manifest_references.len(), 1);
7507        assert_eq!(
7508            result_with_manifests.manifest_references[0].reference_set,
7509            ReferenceSet::Manifest
7510        );
7511        assert_eq!(
7512            result_with_manifests.manifest_references[0].reference_index,
7513            0
7514        );
7515        assert!(matches!(
7516            result_with_manifests.manifest_references[0].status,
7517            DsigStatus::Valid
7518        ));
7519        assert!(matches!(result_with_manifests.status, DsigStatus::Valid));
7520    }
7521
7522    #[test]
7523    fn verify_context_skips_manifest_work_when_signature_value_is_invalid() {
7524        // SignedInfo authenticates the Manifest bytes only after SignatureValue
7525        // succeeds. Malformed nested content must not consume parsing work when
7526        // the cryptographic signature itself is invalid.
7527        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7528            replace_fixture_manifest_digest(&xml, "!!!")
7529        });
7530        assert!(
7531            xml.split_once("<ds:Object>")
7532                .is_some_and(|(_, object)| object.contains("<ds:DigestValue>!!!</ds:DigestValue>")),
7533            "fixture mutation must corrupt the nested Manifest DigestValue",
7534        );
7535
7536        let result = VerifyContext::new()
7537            .key(&RejectingKey)
7538            .process_manifests(true)
7539            .verify(&xml)
7540            .expect("invalid SignatureValue must short-circuit Manifest parsing");
7541
7542        assert!(matches!(
7543            result.status,
7544            DsigStatus::Invalid(FailureReason::SignatureMismatch)
7545        ));
7546        assert!(result.manifest_references.is_empty());
7547    }
7548
7549    #[test]
7550    fn verify_context_shares_xpath_parse_budget_with_manifest_references() {
7551        // SignedInfo and every Manifest form one attacker-controlled parse unit:
7552        // splitting expressions across Reference sets must not reset the ceiling.
7553        let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
7554            .repeat(64);
7555        let transform = format!(
7556            r#"<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</ds:Transform>"#
7557        );
7558        let max_transforms = transform.repeat(16);
7559        let max_manifest_reference = format!(
7560            r##"<ds:Reference URI="#target"><ds:Transforms>{max_transforms}</ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue></ds:Reference>"##
7561        );
7562        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7563            xml.replacen(
7564                r##"<ds:Reference URI="#target">"##,
7565                &format!(
7566                    r##"<ds:Reference URI="#target"><ds:Transforms>{}</ds:Transforms>"##,
7567                    max_transforms
7568                ),
7569                1,
7570            )
7571            .replacen(
7572                "</ds:SignedInfo>",
7573                r##"<ds:Reference URI="#target"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>false()</ds:XPath></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</ds:DigestValue></ds:Reference></ds:SignedInfo>"##,
7574                1,
7575            )
7576            .replacen(
7577                "</ds:Manifest>",
7578                &format!("{}</ds:Manifest>", max_manifest_reference.repeat(3)),
7579                1,
7580            )
7581        });
7582
7583        let error = VerifyContext::new()
7584            .key(&AcceptingKey)
7585            .process_manifests(true)
7586            .verify(&xml)
7587            .expect_err("SignedInfo and Manifest References must share one XPath parse budget");
7588
7589        assert!(
7590            matches!(
7591                &error,
7592                SignatureVerificationPipelineError::Policy(
7593                    crate::policy::PolicyViolation::ResourceLimit {
7594                        resource: "XPath expressions",
7595                        ..
7596                    }
7597                )
7598            ),
7599            "unexpected error: {error:?}"
7600        );
7601    }
7602
7603    #[test]
7604    fn verify_context_processes_manifest_when_signedinfo_references_object() {
7605        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7606            xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
7607                .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
7608                .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
7609        });
7610
7611        let result = VerifyContext::new()
7612            .key(&AcceptingKey)
7613            .process_manifests(true)
7614            .verify(&xml)
7615            .expect("manifest references should be processed when SignedInfo references ds:Object");
7616        assert_eq!(
7617            result.manifest_references.len(),
7618            1,
7619            "signed ds:Object should enable processing of its direct-child ds:Manifest",
7620        );
7621        assert_eq!(
7622            result.manifest_references[0].reference_set,
7623            ReferenceSet::Manifest
7624        );
7625        assert_eq!(result.manifest_references[0].reference_index, 0);
7626        assert!(matches!(
7627            result.manifest_references[0].status,
7628            DsigStatus::Valid
7629        ));
7630    }
7631
7632    #[test]
7633    fn verify_context_skips_manifest_removed_by_enveloped_transform() {
7634        // The owning Signature contains both eligible ID targets. Subtracting
7635        // its subtree therefore removes every target node from the digest input,
7636        // so neither form authenticates the Manifest structure for processing.
7637        for target_object in [false, true] {
7638            let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7639                let xml = xml.replacen(
7640                    r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
7641                    r#"<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
7642                    1,
7643                );
7644                if target_object {
7645                    xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
7646                        .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
7647                        .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
7648                } else {
7649                    xml
7650                }
7651            });
7652
7653            let result = VerifyContext::new()
7654                .key(&AcceptingKey)
7655                .process_manifests(true)
7656                .store_pre_digest(true)
7657                .verify(&xml)
7658                .expect("an emptied reference remains a valid core digest input");
7659
7660            assert!(matches!(result.status, DsigStatus::Valid));
7661            assert_eq!(
7662                result.signed_info_references[0].pre_digest_data.as_deref(),
7663                Some([].as_slice()),
7664                "target_object={target_object} must have empty transformed bytes",
7665            );
7666            assert!(
7667                result.manifest_references.is_empty(),
7668                "target_object={target_object} must not authenticate the Manifest",
7669            );
7670        }
7671    }
7672
7673    #[test]
7674    fn verify_context_ignores_manifest_excluded_from_signed_object() {
7675        // A Reference URI authenticates only its post-transform bytes. Excluding
7676        // the Manifest subtree must not let its independently valid digest chain
7677        // masquerade as data authenticated by SignedInfo.
7678        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7679            xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
7680                .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
7681                .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
7682                .replacen(
7683                    r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
7684                    r#"<ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>not(ancestor-or-self::ds:Manifest)</ds:XPath></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
7685                    1,
7686                )
7687        });
7688
7689        let result = VerifyContext::new()
7690            .key(&AcceptingKey)
7691            .process_manifests(true)
7692            .verify(&xml)
7693            .expect("excluded Manifest content must be ignored, not parsed");
7694
7695        assert!(matches!(result.status, DsigStatus::Valid));
7696        assert!(
7697            result.manifest_references.is_empty(),
7698            "a transform-excluded Manifest is not authenticated by SignedInfo",
7699        );
7700    }
7701
7702    #[test]
7703    fn verify_context_skips_manifest_digest_work_when_signature_is_invalid() {
7704        let xml = signature_with_manifest_xml(false);
7705        let result = VerifyContext::new()
7706            .key(&RejectingKey)
7707            .process_manifests(true)
7708            .verify(&xml)
7709            .expect("invalid SignatureValue must short-circuit Manifest digest work");
7710        assert!(result.manifest_references.is_empty());
7711        assert!(matches!(
7712            result.status,
7713            DsigStatus::Invalid(FailureReason::SignatureMismatch)
7714        ));
7715    }
7716
7717    #[test]
7718    fn verify_context_manifest_digest_mismatch_is_non_fatal_with_accepting_key() {
7719        let xml = signature_with_manifest_xml(false);
7720        let result = VerifyContext::new()
7721            .key(&AcceptingKey)
7722            .process_manifests(true)
7723            .verify(&xml)
7724            .expect("manifest digest mismatches should be recorded while signature stays valid");
7725        assert_eq!(result.manifest_references.len(), 1);
7726        assert!(matches!(
7727            result.manifest_references[0].status,
7728            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
7729        ));
7730        assert!(matches!(result.status, DsigStatus::Valid));
7731    }
7732
7733    #[test]
7734    fn invalid_outer_manifest_digest_does_not_parse_nested_manifest() {
7735        // The malformed inner Manifest is not authenticated until its parent
7736        // Reference digest succeeds. A mismatch must therefore stop before
7737        // parsing or charging work for the nested structure.
7738        let xml = signature_with_manifest_xml_with_manifest_mutation(false, |xml| {
7739            xml.replacen("URI=\"#target\"", "URI=\"#inner-object\"", 1)
7740                .replacen(
7741                    "</ds:Object>",
7742                    "</ds:Object><ds:Object ID=\"inner-object\"><ds:Manifest>junk<ds:Unexpected/></ds:Manifest></ds:Object>",
7743                    1,
7744                )
7745        });
7746
7747        let result = VerifyContext::new()
7748            .key(&AcceptingKey)
7749            .process_manifests(true)
7750            .verify(&xml)
7751            .expect("an unauthenticated nested Manifest must remain opaque");
7752
7753        assert_eq!(result.manifest_references.len(), 1);
7754        assert!(matches!(
7755            result.manifest_references[0].status,
7756            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
7757        ));
7758    }
7759
7760    #[test]
7761    fn verify_context_skips_manifest_parsing_when_signedinfo_reference_fails() {
7762        // Manifest content is not authenticated after a SignedInfo reference
7763        // failure, so parsing it would spend work on untrusted nested input.
7764        let xml = signature_with_manifest_xml(true);
7765        let (signed_info_prefix, object_suffix) = xml
7766            .split_once("<ds:Object>")
7767            .expect("fixture should contain ds:Object");
7768        let open = "<ds:DigestValue>";
7769        let close = "</ds:DigestValue>";
7770        let digest_start = signed_info_prefix
7771            .find(open)
7772            .expect("SignedInfo should contain DigestValue");
7773        let digest_end = signed_info_prefix[digest_start + open.len()..]
7774            .find(close)
7775            .map(|offset| digest_start + open.len() + offset)
7776            .expect("SignedInfo DigestValue must be closed");
7777        let broken_signed_info_prefix = format!(
7778            "{}{}AAAAAAAAAAAAAAAAAAAAAAAAAAA={}{}",
7779            &signed_info_prefix[..digest_start],
7780            open,
7781            close,
7782            &signed_info_prefix[digest_end + close.len()..],
7783        );
7784        let broken_xml = format!("{broken_signed_info_prefix}<ds:Object>{object_suffix}");
7785        let result = VerifyContext::new()
7786            .key(&RejectingKey)
7787            .process_manifests(true)
7788            .verify(&broken_xml)
7789            .expect("SignedInfo digest failure should return without parsing Manifests");
7790        assert!(matches!(
7791            result.status,
7792            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
7793        ));
7794        assert!(
7795            result.manifest_references.is_empty(),
7796            "unauthenticated Manifest content must not be parsed",
7797        );
7798    }
7799
7800    #[test]
7801    fn verify_context_skips_manifest_policy_work_when_signature_is_invalid() {
7802        // A digest-valid SignedInfo reference does not authenticate Manifest
7803        // policy inputs until SignatureValue also succeeds.
7804        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7805            xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
7806        });
7807        let result = VerifyContext::new()
7808            .key(&RejectingKey)
7809            .process_manifests(true)
7810            .verify(&broken_xml)
7811            .expect("invalid SignatureValue must short-circuit Manifest policy work");
7812        assert!(result.manifest_references.is_empty());
7813        assert!(matches!(
7814            result.status,
7815            DsigStatus::Invalid(FailureReason::SignatureMismatch)
7816        ));
7817    }
7818
7819    #[test]
7820    fn verify_context_records_manifest_policy_violations_with_accepting_key() {
7821        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7822            xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
7823        });
7824        let result = VerifyContext::new()
7825            .key(&AcceptingKey)
7826            .process_manifests(true)
7827            .verify(&broken_xml)
7828            .expect("manifest policy violations should be recorded while signature stays valid");
7829        assert_eq!(result.manifest_references.len(), 1);
7830        assert!(matches!(
7831            result.manifest_references[0].status,
7832            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
7833        ));
7834        assert!(matches!(result.status, DsigStatus::Valid));
7835    }
7836
7837    #[test]
7838    fn verify_context_applies_digest_policy_to_manifest_references() {
7839        // Manifest results are authenticated extension data and must obey the
7840        // same digest allowlist as SignedInfo references.
7841        let policy = crate::policy::VerificationPolicy {
7842            manifest_processing: crate::policy::ManifestProcessing::Process,
7843            digest_algorithms: Some(HashSet::from([DigestAlgorithm::Sha1])),
7844            ..crate::policy::VerificationPolicy::default()
7845        };
7846        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
7847            let legacy = "http://www.w3.org/2000/09/xmldsig#sha1";
7848            let offset = xml
7849                .rfind(legacy)
7850                .expect("Manifest DigestMethod must be present");
7851            xml.replace_range(offset..offset + legacy.len(), DigestAlgorithm::Sha256.uri());
7852            let value_start = xml[offset..]
7853                .find("<ds:DigestValue>")
7854                .map(|relative| offset + relative + "<ds:DigestValue>".len())
7855                .expect("Manifest DigestValue must be present");
7856            let value_end = xml[value_start..]
7857                .find("</ds:DigestValue>")
7858                .map(|relative| value_start + relative)
7859                .expect("Manifest DigestValue must be closed");
7860            xml.replace_range(
7861                value_start..value_end,
7862                &base64::engine::general_purpose::STANDARD.encode([0_u8; 32]),
7863            );
7864            xml
7865        });
7866        let result = VerifyContext::new()
7867            .key(&AcceptingKey)
7868            .policy(policy)
7869            .verify(&xml)
7870            .expect("a disallowed Manifest digest is a per-reference result");
7871
7872        assert!(matches!(result.status, DsigStatus::Valid));
7873        assert!(matches!(
7874            result.manifest_references[0].status,
7875            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
7876        ));
7877    }
7878
7879    #[test]
7880    fn verify_context_applies_transform_count_policy_to_manifest_references() {
7881        // Authenticated Manifest references share the caller's per-reference
7882        // transform ceiling and fail before transform execution when exceeded.
7883        let policy = crate::policy::VerificationPolicy {
7884            manifest_processing: crate::policy::ManifestProcessing::Process,
7885            resources: crate::policy::ResourcePolicy {
7886                max_transforms_per_reference: 1,
7887                ..crate::policy::ResourcePolicy::default()
7888            },
7889            ..crate::policy::VerificationPolicy::default()
7890        };
7891        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
7892            let manifest_start = xml
7893                .find("<ds:Manifest")
7894                .expect("fixture must contain a Manifest");
7895            let manifest = xml[manifest_start..].replacen(
7896                "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
7897                concat!(
7898                    "<ds:Transforms>",
7899                    "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
7900                    "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
7901                    "</ds:Transforms>",
7902                    "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>"
7903                ),
7904                1,
7905            );
7906            xml.replace_range(manifest_start.., &manifest);
7907            xml
7908        });
7909        let result = VerifyContext::new()
7910            .key(&AcceptingKey)
7911            .policy(policy)
7912            .verify(&xml)
7913            .expect("Manifest transform policy is a per-reference result");
7914
7915        assert!(matches!(result.status, DsigStatus::Valid));
7916        assert!(matches!(
7917            result.manifest_references[0].status,
7918            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
7919        ));
7920    }
7921
7922    #[test]
7923    fn verify_context_skips_manifest_uri_work_when_signature_is_invalid() {
7924        // Missing Manifest URIs remain unauthenticated until SignatureValue
7925        // succeeds, so they cannot trigger Manifest policy processing here.
7926        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7927            xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
7928        });
7929
7930        let result = VerifyContext::new()
7931            .key(&RejectingKey)
7932            .process_manifests(true)
7933            .verify(&broken_xml)
7934            .expect("invalid SignatureValue must short-circuit Manifest URI processing");
7935        assert!(result.manifest_references.is_empty());
7936        assert!(matches!(
7937            result.status,
7938            DsigStatus::Invalid(FailureReason::SignatureMismatch)
7939        ));
7940    }
7941
7942    #[test]
7943    fn verify_context_records_manifest_missing_uri_with_accepting_key() {
7944        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7945            xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
7946        });
7947
7948        let result = VerifyContext::new()
7949            .key(&AcceptingKey)
7950            .process_manifests(true)
7951            .verify(&broken_xml)
7952            .expect("manifest missing URI should be recorded while signature stays valid");
7953        assert_eq!(result.manifest_references.len(), 1);
7954        assert_eq!(result.manifest_references[0].uri, "<omitted>");
7955        assert!(matches!(
7956            result.manifest_references[0].status,
7957            DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
7958        ));
7959        assert!(matches!(result.status, DsigStatus::Valid));
7960    }
7961
7962    #[test]
7963    fn verify_context_processes_authenticated_nested_manifests_in_object() {
7964        // XMLDSig §5.1 does not impose a direct Object-child restriction.
7965        // The original Reference authenticates this exact wrapped Manifest.
7966        // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-Manifest
7967        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7968            xml.replacen(
7969                "<ds:Manifest ID=\"manifest\">",
7970                "<wrapper><ds:Manifest ID=\"manifest\">",
7971                1,
7972            )
7973            .replacen("</ds:Manifest>", "</ds:Manifest></wrapper>", 1)
7974        });
7975
7976        let result = VerifyContext::new()
7977            .key(&AcceptingKey)
7978            .process_manifests(true)
7979            .verify(&xml)
7980            .expect("authenticated nested Manifest is processed");
7981        assert_eq!(result.manifest_references.len(), 1);
7982        assert_eq!(result.manifest_references[0].uri, "#target");
7983        assert_eq!(result.manifest_references[0].status, DsigStatus::Valid);
7984        assert!(matches!(result.status, DsigStatus::Valid));
7985    }
7986
7987    #[test]
7988    fn verify_context_reports_manifest_reference_parse_errors_explicitly() {
7989        // Malformed nested DigestValue is parsed only after the enclosing
7990        // Manifest structure has been authenticated by SignedInfo.
7991        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7992            replace_fixture_manifest_digest(&xml, "!!!")
7993        });
7994
7995        let err = VerifyContext::new()
7996            .key(&AcceptingKey)
7997            .process_manifests(true)
7998            .verify(&broken_xml)
7999            .expect_err("invalid Manifest DigestValue must map to ParseManifestReference");
8000        assert!(matches!(
8001            err,
8002            SignatureVerificationPipelineError::ParseManifestReference(_)
8003        ));
8004    }
8005
8006    #[test]
8007    fn verify_context_reports_unsupported_manifest_transform_with_declared_digest() {
8008        // Unsupported optional Manifest transforms do not invalidate core
8009        // SignedInfo, but their result must preserve the declared digest method.
8010        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
8011            let xml = xml.replacen(
8012                "<ds:Reference URI=\"#target\">",
8013                "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
8014                1,
8015            );
8016            let xml = xml.replacen(
8017                "</ds:Transforms>\n          <ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
8018                "</ds:Transforms>\n          <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>",
8019                1,
8020            );
8021            replace_fixture_manifest_digest(&xml, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
8022        });
8023        assert!(xml.contains("urn:unsupported"));
8024        assert!(xml.contains("http://www.w3.org/2001/04/xmlenc#sha256"));
8025
8026        let result = VerifyContext::new()
8027            .key(&AcceptingKey)
8028            .process_manifests(true)
8029            .verify(&xml)
8030            .expect("unsupported Manifest transform is a per-reference result");
8031        assert_eq!(result.status, DsigStatus::Valid);
8032        assert_eq!(result.manifest_references.len(), 1);
8033        assert_eq!(
8034            result.manifest_references[0].digest_algorithm,
8035            DigestAlgorithm::Sha256
8036        );
8037        assert!(matches!(
8038            result.manifest_references[0].status,
8039            DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
8040        ));
8041
8042        let restricted = VerifyContext::new()
8043            .key(&AcceptingKey)
8044            .process_manifests(true)
8045            .allowed_transforms([
8046                DEFAULT_IMPLICIT_C14N_URI,
8047                "http://www.w3.org/2001/10/xml-exc-c14n#",
8048            ])
8049            .verify(&xml)
8050            .expect("a disallowed Manifest transform is a per-reference policy result");
8051        assert!(matches!(
8052            restricted.manifest_references[0].status,
8053            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
8054        ));
8055    }
8056
8057    #[test]
8058    fn manifest_reference_limit_counts_unsupported_entries() {
8059        let references = (0..=MAX_REFERENCES_PER_SIGNATURE)
8060            .map(|index| {
8061                format!(
8062                    r##"<ds:Reference URI="#target-{index}"><ds:Transforms><ds:Transform Algorithm="urn:unsupported"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue></ds:Reference>"##
8063                )
8064            })
8065            .collect::<String>();
8066        let xml = format!(
8067            r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:Object Id="signed"><ds:Manifest>{references}</ds:Manifest></ds:Object></ds:Signature>"#
8068        );
8069        let document = XmlDocument::parse(xml).unwrap();
8070        let error = document.with_view(|view| {
8071            let signature = view.document().root_element();
8072            let object = signature.children().find(|node| node.is_element()).unwrap();
8073            let policy = crate::policy::VerificationPolicy::default();
8074            let budgets = VerificationOperationBudgets::with_transforms(
8075                &policy,
8076                TransformExecutionBudget::from_resources(&policy.resources),
8077                None,
8078            );
8079            let operation = OperationExecutionContext::new(
8080                &policy,
8081                &budgets,
8082                Some((view.identity(), view.generation())),
8083            );
8084            operation.authenticate(view.node_identity(object));
8085            let mut processed = HashSet::new();
8086            let mut remaining = MAX_REFERENCES_PER_SIGNATURE;
8087            let mut next_index = 0;
8088            let mut xpath_parse = XPathSignatureParseBudget::default();
8089            let mut state = ManifestDiscoveryState {
8090                processed: &mut processed,
8091                remaining_capacity: &mut remaining,
8092                next_reference_index: &mut next_index,
8093                xpath_parse: &mut xpath_parse,
8094            };
8095            match parse_manifest_references(signature, &operation, view, &mut state, None) {
8096                Ok(_) => panic!("unsupported references must consume the same aggregate limit"),
8097                Err(error) => error,
8098            }
8099        });
8100        assert!(matches!(
8101            error,
8102            SignatureVerificationPipelineError::InvalidStructure {
8103                reason: "signed Manifests exceed the per-signature Reference limit"
8104            }
8105        ));
8106    }
8107
8108    #[test]
8109    fn unsigned_manifest_discovery_cannot_bypass_the_filter_work_budget() {
8110        // Unsigned appended nodes still cost discovery work after Signature
8111        // authentication. Even an all-unsigned frontier must admit its scan.
8112        let document = XmlDocument::parse(format!(
8113            "<ds:Signature xmlns:ds=\"{XMLDSIG_NS}\"><ds:Object><ds:Manifest/></ds:Object></ds:Signature>"
8114        )).unwrap();
8115        document.with_view(|view| {
8116            let mut policy = crate::policy::VerificationPolicy::default();
8117            policy.resources.max_node_set_filter_work = 0;
8118            let budgets = VerificationOperationBudgets::with_transforms(
8119                &policy,
8120                TransformExecutionBudget::from_resources(&policy.resources),
8121                None,
8122            );
8123            let operation = OperationExecutionContext::new(
8124                &policy,
8125                &budgets,
8126                Some((view.identity(), view.generation())),
8127            );
8128            let mut processed = HashSet::new();
8129            let mut remaining = 1;
8130            let mut index = 0;
8131            let mut xpath = XPathSignatureParseBudget::default();
8132            let result = parse_manifest_references(
8133                view.document().root_element(),
8134                &operation,
8135                view,
8136                &mut ManifestDiscoveryState {
8137                    processed: &mut processed,
8138                    remaining_capacity: &mut remaining,
8139                    next_reference_index: &mut index,
8140                    xpath_parse: &mut xpath,
8141                },
8142                None,
8143            );
8144            assert!(result.is_err(), "unsigned scans are not free");
8145        });
8146    }
8147
8148    #[test]
8149    fn unsigned_manifest_remains_eligible_after_trust_expands() {
8150        // The second Object is not authenticated during the first discovery
8151        // pass. It must remain unprocessed so a valid reference from the first
8152        // Manifest can make its sibling Manifest eligible on the next pass.
8153        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
8154        let xml = format!(
8155            r##"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:Object Id="outer"><ds:Manifest><ds:Reference URI="#inner"><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:Manifest></ds:Object><ds:Object Id="inner"><ds:Manifest><ds:Reference URI="#payload"><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:Manifest></ds:Object></ds:Signature>"##
8156        );
8157        let document = XmlDocument::parse(xml).expect("nested Manifest fixture must parse");
8158        document.with_view(|view| {
8159            let signature = view.document().root_element();
8160            let mut objects = signature.children().filter(|node| node.is_element());
8161            let outer = objects.next().expect("outer Object");
8162            let inner = objects.next().expect("inner Object");
8163            let policy = crate::policy::VerificationPolicy::default();
8164            let budgets = VerificationOperationBudgets::with_transforms(
8165                &policy,
8166                TransformExecutionBudget::from_resources(&policy.resources),
8167                None,
8168            );
8169            let operation = OperationExecutionContext::new(
8170                &policy,
8171                &budgets,
8172                Some((view.identity(), view.generation())),
8173            );
8174            operation.authenticate(view.node_identity(outer));
8175            let mut processed = HashSet::new();
8176            let mut remaining = 2;
8177            let mut next_index = 0;
8178            let mut xpath_budget = XPathSignatureParseBudget::default();
8179
8180            let first = parse_manifest_references(
8181                signature,
8182                &operation,
8183                view,
8184                &mut ManifestDiscoveryState {
8185                    processed: &mut processed,
8186                    remaining_capacity: &mut remaining,
8187                    next_reference_index: &mut next_index,
8188                    xpath_parse: &mut xpath_budget,
8189                },
8190                None,
8191            )
8192            .expect("outer Manifest must be discovered");
8193            assert_eq!(first.references.len(), 1);
8194            assert_eq!(first.references[0].reference.uri.as_deref(), Some("#inner"));
8195
8196            operation.authenticate(view.node_identity(inner));
8197            let second = parse_manifest_references(
8198                signature,
8199                &operation,
8200                view,
8201                &mut ManifestDiscoveryState {
8202                    processed: &mut processed,
8203                    remaining_capacity: &mut remaining,
8204                    next_reference_index: &mut next_index,
8205                    xpath_parse: &mut xpath_budget,
8206                },
8207                None,
8208            )
8209            .expect("newly authenticated sibling Manifest must remain eligible");
8210            assert_eq!(second.references.len(), 1);
8211            assert_eq!(
8212                second.references[0].reference.uri.as_deref(),
8213                Some("#payload")
8214            );
8215        });
8216    }
8217
8218    #[test]
8219    fn manifest_reference_limit_includes_signed_info_references() {
8220        // The per-signature ceiling is shared by core and authenticated
8221        // Manifest references; enabling Manifest processing must not reset it.
8222        let xml = signature_with_manifest_xml(true);
8223        let reference_start = xml
8224            .find(r##"<ds:Reference URI="#manifest">"##)
8225            .expect("fixture SignedInfo must reference the Manifest");
8226        let reference_end = xml[reference_start..]
8227            .find("</ds:Reference>")
8228            .map(|offset| reference_start + offset + "</ds:Reference>".len())
8229            .expect("fixture SignedInfo Reference must be closed");
8230        let repeated = xml[reference_start..reference_end].repeat(MAX_REFERENCES_PER_SIGNATURE);
8231        let xml = format!(
8232            "{}{repeated}{}",
8233            &xml[..reference_start],
8234            &xml[reference_end..]
8235        );
8236
8237        let error = VerifyContext::new()
8238            .key(&AcceptingKey)
8239            .process_manifests(true)
8240            .verify(&xml)
8241            .expect_err("one Manifest Reference must exceed the exhausted signature-wide limit");
8242
8243        assert!(matches!(
8244            error,
8245            SignatureVerificationPipelineError::InvalidStructure {
8246                reason: "signed Manifests exceed the per-signature Reference limit"
8247            }
8248        ));
8249    }
8250
8251    #[test]
8252    fn configured_reference_limit_is_shared_with_manifests() {
8253        // Lowering the operation policy must lower the aggregate SignedInfo and
8254        // Manifest capacity rather than falling back to the crate hard limit.
8255        let policy = crate::policy::VerificationPolicy {
8256            manifest_processing: crate::policy::ManifestProcessing::Process,
8257            resources: crate::policy::ResourcePolicy {
8258                max_references: 1,
8259                ..crate::policy::ResourcePolicy::default()
8260            },
8261            ..crate::policy::VerificationPolicy::default()
8262        };
8263
8264        let error = VerifyContext::new()
8265            .key(&AcceptingKey)
8266            .policy(policy)
8267            .verify(&signature_with_manifest_xml(true))
8268            .expect_err("Manifest must exceed the caller-selected aggregate limit");
8269        assert!(matches!(
8270            error,
8271            SignatureVerificationPipelineError::InvalidStructure {
8272                reason: "signed Manifests exceed the per-signature Reference limit"
8273            }
8274        ));
8275    }
8276
8277    #[test]
8278    fn retrieval_method_materializes_single_x509_data_subtree() {
8279        for uri in [
8280            "#target",
8281            "#xpointer(id('target'))",
8282            "#xpointer(id(&quot;target&quot;))",
8283        ] {
8284            for target_xml in [
8285                r#"<ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>"#,
8286                r#"<holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>"#,
8287            ] {
8288                let xml = format!(
8289                    r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo><ds:RetrievalMethod URI="{uri}" Type="http://www.w3.org/2000/09/xmldsig#X509Data"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath></ds:Transform></ds:Transforms></ds:RetrievalMethod></ds:KeyInfo>{target_xml}</root>"#
8290                );
8291                let document = Document::parse(&xml).unwrap();
8292                let key_info_node = document
8293                    .descendants()
8294                    .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8295                    .unwrap();
8296                let mut key_info = parse_key_info(key_info_node).unwrap();
8297                let resolver = UriReferenceResolver::new(&document);
8298
8299                materialize_retrieval_methods(
8300                    &mut key_info,
8301                    &resolver,
8302                    UriTypeSet::SAME_DOCUMENT,
8303                    None,
8304                    crate::provider::default_provider(),
8305                )
8306                .expect("XPath filter must produce one X509Data-rooted node-set");
8307                assert!(matches!(
8308                    key_info.sources.as_slice(),
8309                    [super::super::parse::KeyInfoSource::X509Data(info)]
8310                        if info.subject_names == ["CN=leaf"]
8311                ));
8312            }
8313        }
8314    }
8315
8316    fn retrieval_method_xpath_signature() -> String {
8317        format!(
8318            r##"<root xmlns:ds="{XMLDSIG_NS}">
8319              <payload Id="payload">ok</payload>
8320              <ds:Signature>
8321                <ds:SignedInfo>
8322                  <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
8323                  <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
8324                  <ds:Reference URI="#payload">
8325                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
8326                    <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
8327                  </ds:Reference>
8328                </ds:SignedInfo>
8329                <ds:SignatureValue>AQ==</ds:SignatureValue>
8330                <ds:KeyInfo>
8331                  <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
8332                    <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
8333                      <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
8334                    </ds:Transform></ds:Transforms>
8335                  </ds:RetrievalMethod>
8336                </ds:KeyInfo>
8337              </ds:Signature>
8338              <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
8339            </root>"##
8340        )
8341    }
8342
8343    #[test]
8344    fn retrieval_method_xpath_uses_signature_expression_budget() {
8345        // RetrievalMethod XPath belongs to the same untrusted Signature as
8346        // Reference XPath and must not receive a separate parse allowance.
8347        let mut policy = crate::policy::VerificationPolicy::default();
8348        policy.resources.max_xpath_expressions = 0;
8349
8350        let error = VerifyContext::new()
8351            .policy(policy)
8352            .verify(&retrieval_method_xpath_signature())
8353            .expect_err("RetrievalMethod XPath must consume the signature parse budget");
8354
8355        assert!(
8356            matches!(
8357                &error,
8358                SignatureVerificationPipelineError::Policy(
8359                    crate::policy::PolicyViolation::ResourceLimit {
8360                        resource: "XPath expressions",
8361                        maximum: 0,
8362                        actual: 1,
8363                    }
8364                )
8365            ),
8366            "unexpected error: {error:?}"
8367        );
8368    }
8369
8370    #[test]
8371    fn retrieval_method_xpath_obeys_expression_byte_limit() {
8372        // Edge whitespace is semantically harmless for this restricted shape,
8373        // but its raw untrusted bytes still belong to the operation budget.
8374        let expression = "ancestor-or-self::ds:X509Data";
8375        let padded_expression = format!("  {expression}  ");
8376        let xml = retrieval_method_xpath_signature().replace(expression, &padded_expression);
8377        let mut policy = crate::policy::VerificationPolicy::default();
8378        policy.resources.max_xpath_expression_bytes = expression.len();
8379
8380        let error = VerifyContext::new()
8381            .policy(policy)
8382            .verify(&xml)
8383            .expect_err("RetrievalMethod XPath must obey the expression byte limit");
8384
8385        assert!(
8386            matches!(
8387                &error,
8388                SignatureVerificationPipelineError::Policy(
8389                    crate::policy::PolicyViolation::ResourceLimit {
8390                        resource: "XPath expression bytes",
8391                        maximum,
8392                        actual,
8393                    }
8394                ) if *maximum == expression.len() && *actual == padded_expression.len()
8395            ),
8396            "unexpected error: {error:?}"
8397        );
8398    }
8399
8400    #[test]
8401    fn retrieval_method_xpath_obeys_expression_complexity_limit() {
8402        // Recognizing a fixed safe predicate must not bypass the common XPath
8403        // complexity policy applied to every expression in the signature.
8404        let mut policy = crate::policy::VerificationPolicy::default();
8405        policy.resources.max_xpath_expression_complexity = 0;
8406
8407        let error = VerifyContext::new()
8408            .policy(policy)
8409            .verify(&retrieval_method_xpath_signature())
8410            .expect_err("RetrievalMethod XPath must obey the complexity limit");
8411
8412        assert!(
8413            matches!(
8414                &error,
8415                SignatureVerificationPipelineError::Policy(
8416                    crate::policy::PolicyViolation::ResourceLimit {
8417                        resource: "XPath expression complexity",
8418                        maximum: 0,
8419                        actual,
8420                    }
8421                ) if *actual > 0
8422            ),
8423            "unexpected error: {error:?}"
8424        );
8425    }
8426
8427    #[test]
8428    fn retrieval_method_xpath_uses_node_filter_work_budget() {
8429        // Attribute and namespace XPath nodes participate in filtering even
8430        // though the optimized X509Data locator scans only tree descendants.
8431        let mut policy = crate::policy::VerificationPolicy::default();
8432        policy.resources.max_node_set_filter_work = 4;
8433        let xml = retrieval_method_xpath_signature().replace(
8434            "<holder Id=\"target\">",
8435            "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
8436        );
8437
8438        let error = VerifyContext::new()
8439            .policy(policy)
8440            .verify(&xml)
8441            .expect_err("RetrievalMethod XPath must consume node-filter work");
8442
8443        assert!(
8444            matches!(
8445                &error,
8446                SignatureVerificationPipelineError::Policy(
8447                    crate::policy::PolicyViolation::ResourceLimit {
8448                        resource: crate::policy::resource_name::NODE_SET_FILTER_WORK,
8449                        maximum: 4,
8450                        actual,
8451                    }
8452                ) if *actual > 4
8453            ),
8454            "unexpected error: {error:?}"
8455        );
8456    }
8457
8458    #[test]
8459    fn retrieval_method_xpath_charges_every_context_to_evaluation_work() {
8460        // The optimized predicate avoids a generic XPath engine, but every
8461        // attribute and namespace context still consumes evaluation work.
8462        let mut policy = crate::policy::VerificationPolicy::default();
8463        policy.resources.max_xpath_evaluation_work = 4;
8464        let xml = retrieval_method_xpath_signature().replace(
8465            "<holder Id=\"target\">",
8466            "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
8467        );
8468
8469        let error = VerifyContext::new()
8470            .policy(policy)
8471            .verify(&xml)
8472            .expect_err("RetrievalMethod XPath must charge every evaluation context");
8473
8474        assert!(matches!(
8475            error,
8476            SignatureVerificationPipelineError::Policy(
8477                crate::policy::PolicyViolation::ResourceLimit {
8478                    resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
8479                    maximum: 4,
8480                    actual,
8481                }
8482            ) if actual > 4
8483        ));
8484    }
8485
8486    #[test]
8487    fn retrieval_method_xpath_obeys_namespace_binding_limit() {
8488        // The specialized RetrievalMethod path must retain the XPath element's
8489        // in-scope namespaces and enforce the same limit as ordinary XPath.
8490        let mut policy = crate::policy::VerificationPolicy::default();
8491        policy.resources.max_xpath_namespace_bindings = 0;
8492
8493        let error = VerifyContext::new()
8494            .policy(policy)
8495            .verify(&retrieval_method_xpath_signature())
8496            .expect_err("RetrievalMethod XPath namespaces must obey the binding limit");
8497
8498        assert!(matches!(
8499            error,
8500            SignatureVerificationPipelineError::Policy(
8501                crate::policy::PolicyViolation::ResourceLimit {
8502                    resource: crate::policy::resource_name::XPATH_NAMESPACE_BINDINGS,
8503                    maximum: 0,
8504                    actual,
8505                }
8506            ) if actual > 0
8507        ));
8508    }
8509
8510    #[test]
8511    fn retrieval_method_xpath_obeys_namespace_byte_limit() {
8512        // Prefix and URI bytes retained from the XPath namespace axis consume
8513        // the same per-expression byte budget as an ordinary XPath transform.
8514        let mut policy = crate::policy::VerificationPolicy::default();
8515        policy.resources.max_xpath_namespace_bytes = 0;
8516
8517        let error = VerifyContext::new()
8518            .policy(policy)
8519            .verify(&retrieval_method_xpath_signature())
8520            .expect_err("RetrievalMethod XPath namespaces must obey the byte limit");
8521
8522        assert!(matches!(
8523            error,
8524            SignatureVerificationPipelineError::Policy(
8525                crate::policy::PolicyViolation::ResourceLimit {
8526                    resource: crate::policy::resource_name::XPATH_NAMESPACE_BYTES,
8527                    maximum: 0,
8528                    actual,
8529                }
8530            ) if actual > 0
8531        ));
8532    }
8533
8534    #[test]
8535    fn retrieval_method_xpath_obeys_context_evaluation_limit() {
8536        // A bare fragment includes attribute and namespace XPath nodes in
8537        // addition to the four tree nodes below. Tree-only accounting would
8538        // incorrectly admit this input at the configured ceiling.
8539        let mut policy = crate::policy::VerificationPolicy::default();
8540        policy.resources.max_xpath_context_evaluations = 4;
8541        let xml = retrieval_method_xpath_signature().replace(
8542            "<holder Id=\"target\">",
8543            "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
8544        );
8545
8546        let error = VerifyContext::new()
8547            .policy(policy)
8548            .verify(&xml)
8549            .expect_err("RetrievalMethod XPath contexts must obey the evaluation limit");
8550
8551        assert!(matches!(
8552            error,
8553            SignatureVerificationPipelineError::Policy(
8554                crate::policy::PolicyViolation::ResourceLimit {
8555                    resource: crate::policy::resource_name::XPATH_CONTEXT_EVALUATIONS,
8556                    maximum: 4,
8557                    actual,
8558                }
8559            ) if actual > 4
8560        ));
8561    }
8562
8563    #[test]
8564    fn retrieval_method_materializes_direct_untransformed_x509_data() {
8565        // A typed RetrievalMethod may point directly at the XML structure it
8566        // identifies; no transform is needed when X509Data is the URI root.
8567        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8568          <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
8569          <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
8570        </root>"##;
8571        let document = Document::parse(xml).unwrap();
8572        let key_info_node = document
8573            .descendants()
8574            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8575            .unwrap();
8576        let mut key_info = parse_key_info(key_info_node).unwrap();
8577
8578        materialize_retrieval_methods(
8579            &mut key_info,
8580            &UriReferenceResolver::new(&document),
8581            UriTypeSet::SAME_DOCUMENT,
8582            None,
8583            crate::provider::default_provider(),
8584        )
8585        .expect("a direct X509Data target needs no transform");
8586        assert!(matches!(
8587            key_info.sources.as_slice(),
8588            [super::super::parse::KeyInfoSource::X509Data(info)]
8589                if info.subject_names == ["CN=leaf"]
8590        ));
8591    }
8592
8593    #[test]
8594    fn retrieval_method_respects_configured_same_document_id_semantics() {
8595        // RetrievalMethod is another consumer of same-document URIs and must
8596        // not bypass the grammar selected for normal Reference dereferencing.
8597        fn materialize(
8598            uri: &str,
8599            id: &str,
8600            semantics: crate::policy::SameDocumentIdSemantics,
8601        ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
8602            let xml = format!(
8603                r#"<root xmlns:ds="{XMLDSIG_NS}">
8604                  <ds:KeyInfo><ds:RetrievalMethod URI="{uri}" Type="{XMLDSIG_NS}X509Data"/></ds:KeyInfo>
8605                  <ds:X509Data Id="{id}"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
8606                </root>"#
8607            );
8608            let document = Document::parse(&xml).unwrap();
8609            let key_info_node = document
8610                .descendants()
8611                .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8612                .unwrap();
8613            let mut key_info = parse_key_info(key_info_node).unwrap();
8614            let resolver =
8615                UriReferenceResolver::new(&document).with_same_document_id_semantics(semantics);
8616
8617            materialize_retrieval_methods(
8618                &mut key_info,
8619                &resolver,
8620                UriTypeSet::SAME_DOCUMENT,
8621                None,
8622                crate::provider::default_provider(),
8623            )
8624        }
8625
8626        assert!(
8627            materialize(
8628                "#12345",
8629                "12345",
8630                crate::policy::SameDocumentIdSemantics::Specification,
8631            )
8632            .is_err(),
8633            "the standards mode must reject a non-NCName bare fragment"
8634        );
8635        assert!(
8636            materialize(
8637                "#visa'3d",
8638                "visa'3d",
8639                crate::policy::SameDocumentIdSemantics::XmlSecBarename,
8640            )
8641            .is_err(),
8642            "the donor barename wrapper cannot represent an apostrophe"
8643        );
8644        assert!(
8645            materialize(
8646                "#visa'3d",
8647                "visa'3d",
8648                crate::policy::SameDocumentIdSemantics::XmlSecVisa3d,
8649            )
8650            .is_ok(),
8651            "Visa3D mode resolves the registered ID without an XPointer literal"
8652        );
8653    }
8654
8655    #[test]
8656    fn raw_x509_retrieval_method_uses_inherited_xml_base() {
8657        // RetrievalMethod URI is an attribute URI reference, so XML Base uses
8658        // the effective base of the element bearing that attribute.
8659        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
8660        let xml = format!(
8661            r#"<root xml:base="https://example.test/keys/nested/" xmlns:ds="{XMLDSIG_NS}">
8662                <ds:KeyInfo><ds:RetrievalMethod URI="../signer.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>
8663            </root>"#
8664        );
8665        let document = Document::parse(&xml).unwrap();
8666        let key_info_node = document
8667            .descendants()
8668            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8669            .unwrap();
8670        let mut key_info = parse_key_info(key_info_node).unwrap();
8671        let certificate = include_bytes!(
8672            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
8673        )
8674        .to_vec();
8675        let resources = HashMap::from([(
8676            "https://example.test/keys/signer.der".to_string(),
8677            certificate,
8678        )]);
8679        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
8680
8681        materialize_retrieval_methods(
8682            &mut key_info,
8683            &resolver,
8684            UriTypeSet::ALL,
8685            None,
8686            crate::provider::default_provider(),
8687        )
8688        .expect("RetrievalMethod should resolve against inherited xml:base");
8689
8690        assert!(matches!(
8691            key_info.sources.as_slice(),
8692            [super::super::parse::KeyInfoSource::X509Data(info)]
8693                if info.certificates.len() == 1
8694        ));
8695    }
8696
8697    #[test]
8698    fn retrieval_method_requires_xpath_for_x509_data_below_uri_root() {
8699        // Without a transform the dereferenced holder, not its descendant,
8700        // is the result and therefore cannot masquerade as typed X509Data.
8701        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8702          <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
8703          <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
8704        </root>"##;
8705        let document = Document::parse(xml).unwrap();
8706        let key_info_node = document
8707            .descendants()
8708            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8709            .unwrap();
8710        let mut key_info = parse_key_info(key_info_node).unwrap();
8711
8712        let error = materialize_retrieval_methods(
8713            &mut key_info,
8714            &UriReferenceResolver::new(&document),
8715            UriTypeSet::SAME_DOCUMENT,
8716            None,
8717            crate::provider::default_provider(),
8718        )
8719        .expect_err("a wrapper target requires an explicit selection transform");
8720        assert!(matches!(
8721            error,
8722            SignatureVerificationPipelineError::InvalidStructure {
8723                reason: "untransformed X509Data RetrievalMethod must target X509Data directly"
8724            }
8725        ));
8726    }
8727
8728    #[test]
8729    fn retrieval_method_rejects_target_inside_external_x509_data_ancestor() {
8730        // XPath filtering cannot add an ancestor that was outside the URI's
8731        // dereferenced node-set, so this result is not rooted at X509Data.
8732        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8733          <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath></ds:Transform></ds:Transforms></ds:RetrievalMethod></ds:KeyInfo>
8734          <ds:X509Data><ds:X509SubjectName Id="target">CN=leaf</ds:X509SubjectName></ds:X509Data>
8735        </root>"##;
8736        let document = Document::parse(xml).unwrap();
8737        let key_info_node = document
8738            .descendants()
8739            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8740            .unwrap();
8741        let mut key_info = parse_key_info(key_info_node).unwrap();
8742
8743        let error = materialize_retrieval_methods(
8744            &mut key_info,
8745            &UriReferenceResolver::new(&document),
8746            UriTypeSet::SAME_DOCUMENT,
8747            None,
8748            crate::provider::default_provider(),
8749        )
8750        .expect_err("filter output without an X509Data root must be rejected");
8751        assert!(matches!(
8752            error,
8753            SignatureVerificationPipelineError::InvalidStructure {
8754                reason: "X509Data RetrievalMethod selected no X509Data element"
8755            }
8756        ));
8757    }
8758
8759    #[test]
8760    fn retrieval_method_rejects_ambiguous_x509_data_relation() {
8761        // A transformed result with multiple X509Data roots is not one KeyInfo child.
8762        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8763          <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath></ds:Transform></ds:Transforms></ds:RetrievalMethod></ds:KeyInfo>
8764          <holder Id="target"><ds:X509Data/><ds:X509Data/></holder>
8765        </root>"##;
8766        let document = Document::parse(xml).unwrap();
8767        let key_info_node = document
8768            .descendants()
8769            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8770            .unwrap();
8771        let mut key_info = parse_key_info(key_info_node).unwrap();
8772
8773        let error = materialize_retrieval_methods(
8774            &mut key_info,
8775            &UriReferenceResolver::new(&document),
8776            UriTypeSet::SAME_DOCUMENT,
8777            None,
8778            crate::provider::default_provider(),
8779        )
8780        .expect_err("multiple transformed X509Data roots must be rejected");
8781        assert!(matches!(
8782            error,
8783            SignatureVerificationPipelineError::InvalidStructure {
8784                reason: "X509Data RetrievalMethod selected multiple X509Data elements"
8785            }
8786        ));
8787    }
8788
8789    #[test]
8790    fn retrieval_method_materialization_preserves_key_info_order() {
8791        // Replacing the source in place keeps a later fallback behind the
8792        // retrieved key material for first-match resolvers.
8793        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8794          <ds:KeyInfo>
8795            <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath></ds:Transform></ds:Transforms></ds:RetrievalMethod>
8796            <ds:KeyName>fallback</ds:KeyName>
8797          </ds:KeyInfo>
8798          <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
8799        </root>"##;
8800        let document = Document::parse(xml).unwrap();
8801        let key_info_node = document
8802            .descendants()
8803            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8804            .unwrap();
8805        let mut key_info = parse_key_info(key_info_node).unwrap();
8806
8807        materialize_retrieval_methods(
8808            &mut key_info,
8809            &UriReferenceResolver::new(&document),
8810            UriTypeSet::SAME_DOCUMENT,
8811            None,
8812            crate::provider::default_provider(),
8813        )
8814        .unwrap();
8815        assert!(matches!(
8816            key_info.sources.as_slice(),
8817            [
8818                super::super::parse::KeyInfoSource::X509Data(_),
8819                super::super::parse::KeyInfoSource::KeyName(name)
8820            ] if name == "fallback"
8821        ));
8822    }
8823
8824    #[test]
8825    fn retrieval_method_materialization_bounds_repeated_sources() {
8826        // Repeating one allowed certificate must not multiply parsing and clones
8827        // before SignatureValue validation.
8828        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
8829        let certificate = include_bytes!(
8830            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
8831        )
8832        .to_vec();
8833        let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
8834        let mut key_info = KeyInfo {
8835            sources: (0..=64)
8836                .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
8837                    uri: "urn:certificate".into(),
8838                    resource_type: Some(RAW_X509_TYPE.into()),
8839                    transforms: RetrievalMethodTransforms::None,
8840                })
8841                .collect(),
8842        };
8843        let document = Document::parse("<root/>").unwrap();
8844        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
8845
8846        let error = materialize_retrieval_methods(
8847            &mut key_info,
8848            &resolver,
8849            UriTypeSet::ALL,
8850            None,
8851            crate::provider::default_provider(),
8852        )
8853        .expect_err("retrieval count must be bounded before materialization");
8854        assert!(matches!(
8855            error,
8856            SignatureVerificationPipelineError::InvalidStructure {
8857                reason: "KeyInfo contains too many RetrievalMethod elements"
8858            }
8859        ));
8860    }
8861
8862    #[test]
8863    fn key_info_reference_materializes_an_allowed_empty_uri() {
8864        // Empty URI is a same-document Reference URI. Element-valued
8865        // KeyInfoReference dereferencing therefore selects a KeyInfo document
8866        // element rather than consulting the external-resource map.
8867        let document = XmlDocument::parse(format!(
8868            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>root-key</ds:KeyName></ds:KeyInfo>"#
8869        ))
8870        .unwrap();
8871        let mut key_info = KeyInfo {
8872            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
8873                uri: String::new(),
8874            }],
8875        };
8876        let mut policy = crate::policy::VerificationPolicy::default();
8877        policy.key_sources.key_info_reference = true;
8878        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
8879        let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
8880        let mut budgets = RetrievalMaterializationBudgets {
8881            key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
8882                &policy.resources,
8883            )),
8884            xpath_parse: &mut xpath_parse_budget,
8885            execution: &execution_budget,
8886            resources: &policy.resources,
8887            certificate_signature_algorithms: policy
8888                .key_trust
8889                .certificate_signature_algorithms
8890                .as_ref(),
8891            xml_backend: crate::XmlBackend::default(),
8892        };
8893        let mut materialization = KeyInfoMaterializationState::default();
8894
8895        document
8896            .with_view(|view| {
8897                materialize_key_info_references_with_budgets(
8898                    &mut key_info,
8899                    &UriReferenceResolver::with_document_view(view, &[]),
8900                    &policy,
8901                    crate::provider::default_provider(),
8902                    &mut budgets,
8903                    &mut materialization,
8904                )?;
8905                Ok::<_, SignatureVerificationPipelineError>(())
8906            })
8907            .unwrap();
8908
8909        assert!(matches!(
8910            key_info.sources.as_slice(),
8911            [super::super::parse::KeyInfoSource::KeyName(name)] if name == "root-key"
8912        ));
8913    }
8914
8915    #[test]
8916    fn raw_certificate_retrieval_retains_spent_operation_work() {
8917        // Raw DER retrieval must not bypass the account used by XML KeyInfo parsing.
8918        let pair = rcgen::KeyPair::generate().expect("test key generation");
8919        let cert = rcgen::CertificateParams::new(Vec::<String>::new())
8920            .expect("empty SAN list")
8921            .self_signed(&pair)
8922            .expect("test certificate signing");
8923        let external = HashMap::from([("urn:cert".to_owned(), cert.der().to_vec())]);
8924        let document = Document::parse("<root/>").expect("test XML");
8925        let resolver = UriReferenceResolver::new(&document).with_external_resources(&external);
8926        let mut policy = crate::policy::VerificationPolicy::default();
8927        policy.resources.max_x509_work = 1;
8928        policy.uris.retrieval_methods = UriTypeSet::ALL;
8929        let usage = RefCell::new(super::super::parse::KeyInfoParseUsage::new(
8930            &policy.resources,
8931        ));
8932        usage
8933            .borrow()
8934            .certificates
8935            .memory()
8936            .charge_work(1)
8937            .expect("spend allowance");
8938        let mut xpath = XPathSignatureParseBudget::from_resources(&policy.resources);
8939        let execution = TransformExecutionBudget::from_resources(&policy.resources);
8940        let mut budgets = RetrievalMaterializationBudgets {
8941            key_info_parse: &usage,
8942            xpath_parse: &mut xpath,
8943            execution: &execution,
8944            resources: &policy.resources,
8945            certificate_signature_algorithms: None,
8946            xml_backend: crate::XmlBackend::default(),
8947        };
8948        let mut info = KeyInfo {
8949            sources: vec![super::super::parse::KeyInfoSource::RetrievalMethod {
8950                uri: "urn:cert".into(),
8951                resource_type: Some("http://www.w3.org/2000/09/xmldsig#rawX509Certificate".into()),
8952                transforms: RetrievalMethodTransforms::None,
8953            }],
8954        };
8955        let result = materialize_retrieval_methods_with_budgets(
8956            &mut info,
8957            &resolver,
8958            &policy,
8959            crate::provider::default_provider(),
8960            &mut budgets,
8961            &mut 0,
8962        );
8963        assert!(matches!(
8964            result,
8965            Err(SignatureVerificationPipelineError::Policy(
8966                crate::policy::PolicyViolation::ResourceLimitExceeded {
8967                    resource: crate::policy::resource_name::X509_WORK,
8968                    maximum: 1,
8969                }
8970            ))
8971        ));
8972    }
8973
8974    #[test]
8975    fn empty_key_info_reference_participates_in_cycle_detection() {
8976        // Empty URI resolves to the owning document's root KeyInfo. It must
8977        // recurse through the same traversal state rather than leave the
8978        // self-reference materialized and bypass cycle/depth enforcement.
8979        let document = XmlDocument::parse(format!(
8980            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#"><dsig11:KeyInfoReference URI=""/></ds:KeyInfo>"#
8981        ))
8982        .unwrap();
8983        let mut key_info = KeyInfo {
8984            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
8985                uri: String::new(),
8986            }],
8987        };
8988        let mut policy = crate::policy::VerificationPolicy::default();
8989        policy.key_sources.key_info_reference = true;
8990        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
8991        let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
8992        let mut budgets = RetrievalMaterializationBudgets {
8993            key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
8994                &policy.resources,
8995            )),
8996            xpath_parse: &mut xpath_parse_budget,
8997            execution: &execution_budget,
8998            resources: &policy.resources,
8999            certificate_signature_algorithms: policy
9000                .key_trust
9001                .certificate_signature_algorithms
9002                .as_ref(),
9003            xml_backend: crate::XmlBackend::default(),
9004        };
9005        let mut materialization = KeyInfoMaterializationState::default();
9006
9007        let error = document
9008            .with_view(|view| {
9009                materialize_key_info_references_with_budgets(
9010                    &mut key_info,
9011                    &UriReferenceResolver::with_document_view(view, &[]),
9012                    &policy,
9013                    crate::provider::default_provider(),
9014                    &mut budgets,
9015                    &mut materialization,
9016                )
9017            })
9018            .expect_err("empty-URI self-reference must be rejected as a cycle");
9019
9020        assert!(matches!(
9021            error,
9022            SignatureVerificationPipelineError::InvalidStructure {
9023                reason: "KeyInfoReference cycle detected"
9024            }
9025        ));
9026    }
9027
9028    #[test]
9029    fn key_info_materialization_shares_candidate_work_across_source_kinds() {
9030        // The reference, nested RetrievalMethod, and resulting certificate are
9031        // three units of materialization work in one operation.
9032        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
9033        let document = XmlDocument::parse(format!(
9034            r##"<root xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#"><ds:KeyInfo ID="target"><ds:RetrievalMethod URI="signer.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo></root>"##
9035        ))
9036        .unwrap();
9037        let certificate = include_bytes!(
9038            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
9039        )
9040        .to_vec();
9041        let resources = HashMap::from([("signer.der".to_owned(), certificate)]);
9042        let mut key_info = KeyInfo {
9043            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9044                uri: "#target".into(),
9045            }],
9046        };
9047        let mut policy = crate::policy::VerificationPolicy::default();
9048        policy.key_sources.key_info_reference = true;
9049        policy.uris.retrieval_methods = UriTypeSet::ALL;
9050        policy.resources.max_key_candidates = 2;
9051        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
9052        let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
9053        let mut budgets = RetrievalMaterializationBudgets {
9054            key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
9055                &policy.resources,
9056            )),
9057            xpath_parse: &mut xpath_parse_budget,
9058            execution: &execution_budget,
9059            resources: &policy.resources,
9060            certificate_signature_algorithms: policy
9061                .key_trust
9062                .certificate_signature_algorithms
9063                .as_ref(),
9064            xml_backend: crate::XmlBackend::default(),
9065        };
9066        let mut materialization = KeyInfoMaterializationState::default();
9067
9068        let error = document
9069            .with_view(|view| {
9070                let resolver = UriReferenceResolver::with_document_view(view, &[])
9071                    .with_external_resources(&resources);
9072                let mut outcome = materialize_key_info_references_with_budgets(
9073                    &mut key_info,
9074                    &resolver,
9075                    &policy,
9076                    crate::provider::default_provider(),
9077                    &mut budgets,
9078                    &mut materialization,
9079                )?;
9080                outcome.merge(materialize_retrieval_methods_with_budgets(
9081                    &mut key_info,
9082                    &resolver,
9083                    &policy,
9084                    crate::provider::default_provider(),
9085                    &mut budgets,
9086                    &mut materialization.candidate_work,
9087                )?);
9088                Ok::<_, DsigError>(outcome)
9089            })
9090            .expect_err("all materializers must share the candidate-work limit");
9091
9092        assert!(matches!(
9093            error,
9094            DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
9095                resource: crate::policy::resource_name::KEY_CANDIDATES,
9096                maximum: 2,
9097                actual: 3,
9098            })
9099        ));
9100    }
9101
9102    #[test]
9103    fn public_key_info_materialization_binds_external_resource_policy() {
9104        // Public helpers must not inherit the resolver's permissive hard-limit
9105        // budget when the operation policy selects a stricter byte ceiling.
9106        let document = Document::parse("<root/>").unwrap();
9107        let external = format!(
9108            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
9109        );
9110        let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
9111        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9112        let mut key_info = KeyInfo {
9113            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9114                uri: "key.xml".into(),
9115            }],
9116        };
9117        let mut policy = crate::policy::VerificationPolicy::default();
9118        policy.key_sources.key_info_reference = true;
9119        policy.uris.key_info_references = UriTypeSet::ALL;
9120        policy.resources.max_external_resource_bytes = 0;
9121
9122        let error = materialize_verification_key_info_references(
9123            &mut key_info,
9124            resolver,
9125            &policy,
9126            crate::provider::default_provider(),
9127            crate::XmlBackend::default(),
9128        )
9129        .expect_err("policy must reject non-empty external KeyInfo bytes");
9130
9131        assert!(matches!(
9132            error,
9133            DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
9134                resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
9135                maximum: 0,
9136                actual,
9137            }) if actual == resources["key.xml"].len()
9138        ));
9139    }
9140
9141    #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
9142    #[test]
9143    fn public_key_info_materialization_uses_the_selected_backend() {
9144        // Public materialization is a standalone parse boundary. Its backend
9145        // must be caller-selected just like the complete sign/verify pipeline.
9146        let external = format!(
9147            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
9148        );
9149        let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
9150        let document = Document::parse("<root/>").unwrap();
9151
9152        for backend in [crate::XmlBackend::Xmloxide, crate::XmlBackend::Roxmltree] {
9153            let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9154            let mut key_info = KeyInfo {
9155                sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9156                    uri: "key.xml".into(),
9157                }],
9158            };
9159            let mut policy = crate::policy::VerificationPolicy::default();
9160            policy.key_sources.key_info_reference = true;
9161            policy.uris.key_info_references = UriTypeSet::ALL;
9162
9163            materialize_verification_key_info_references(
9164                &mut key_info,
9165                resolver,
9166                &policy,
9167                crate::provider::default_provider(),
9168                backend,
9169            )
9170            .unwrap_or_else(|error| panic!("{backend:?} materialization failed: {error}"));
9171
9172            assert!(matches!(
9173                key_info.sources.as_slice(),
9174                [super::super::parse::KeyInfoSource::KeyName(name)] if name == "external"
9175            ));
9176        }
9177    }
9178
9179    #[test]
9180    fn public_key_info_materialization_defers_nested_retrieval_failure() {
9181        // Metadata expansion must preserve an earlier usable source when a
9182        // later retrieval candidate fails. The complete operation reports the
9183        // deferred error only if key selection exhausts all usable sources.
9184        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
9185        let external = format!(
9186            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>usable</ds:KeyName><ds:RetrievalMethod URI="missing.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>"#
9187        );
9188        let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
9189        let document = Document::parse("<root/>").unwrap();
9190
9191        for operation in ["signing", "verification"] {
9192            let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9193            let mut key_info = KeyInfo {
9194                sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9195                    uri: "key.xml".into(),
9196                }],
9197            };
9198            match operation {
9199                "signing" => {
9200                    let mut policy = crate::policy::SigningPolicy::default();
9201                    policy.uris.key_info_references = UriTypeSet::ALL;
9202                    policy.uris.retrieval_methods = UriTypeSet::ALL;
9203                    materialize_signing_key_info_references(
9204                        &mut key_info,
9205                        resolver,
9206                        &policy,
9207                        crate::provider::default_provider(),
9208                        crate::XmlBackend::default(),
9209                    )
9210                }
9211                "verification" => {
9212                    let mut policy = crate::policy::VerificationPolicy::default();
9213                    policy.key_sources.key_info_reference = true;
9214                    policy.uris.key_info_references = UriTypeSet::ALL;
9215                    policy.uris.retrieval_methods = UriTypeSet::ALL;
9216                    materialize_verification_key_info_references(
9217                        &mut key_info,
9218                        resolver,
9219                        &policy,
9220                        crate::provider::default_provider(),
9221                        crate::XmlBackend::default(),
9222                    )
9223                }
9224                _ => unreachable!(),
9225            }
9226            .unwrap_or_else(|error| panic!("{operation} materialization failed: {error}"));
9227
9228            assert!(matches!(
9229                key_info.sources.as_slice(),
9230                [super::super::parse::KeyInfoSource::KeyName(name)] if name == "usable"
9231            ));
9232        }
9233    }
9234
9235    #[test]
9236    fn public_key_info_materialization_binds_xpath_policy() {
9237        // Both public helpers must carry the caller's XPath parse limits into
9238        // RetrievalMethod transforms nested in an external KeyInfo document.
9239        let external = format!(
9240            r##"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}">
9241                <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
9242                    <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
9243                        <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
9244                    </ds:Transform></ds:Transforms>
9245                </ds:RetrievalMethod>
9246                <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
9247            </ds:KeyInfo>"##
9248        );
9249        let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
9250        let document = Document::parse("<root/>").unwrap();
9251
9252        for operation in ["signing", "verification"] {
9253            let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9254            let mut key_info = KeyInfo {
9255                sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9256                    uri: "key.xml".into(),
9257                }],
9258            };
9259            let error = match operation {
9260                "signing" => {
9261                    let mut policy = crate::policy::SigningPolicy::default();
9262                    policy.uris.key_info_references = UriTypeSet::ALL;
9263                    policy.resources.max_xpath_expressions = 0;
9264                    materialize_signing_key_info_references(
9265                        &mut key_info,
9266                        resolver,
9267                        &policy,
9268                        crate::provider::default_provider(),
9269                        crate::XmlBackend::default(),
9270                    )
9271                }
9272                "verification" => {
9273                    let mut policy = crate::policy::VerificationPolicy::default();
9274                    policy.key_sources.key_info_reference = true;
9275                    policy.uris.key_info_references = UriTypeSet::ALL;
9276                    policy.resources.max_xpath_expressions = 0;
9277                    materialize_verification_key_info_references(
9278                        &mut key_info,
9279                        resolver,
9280                        &policy,
9281                        crate::provider::default_provider(),
9282                        crate::XmlBackend::default(),
9283                    )
9284                }
9285                _ => unreachable!(),
9286            }
9287            .expect_err("the public helper must enforce the supplied XPath expression limit");
9288
9289            assert!(
9290                matches!(
9291                    error,
9292                    DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
9293                        resource: crate::policy::resource_name::XPATH_EXPRESSIONS,
9294                        maximum: 0,
9295                        actual: 1,
9296                    })
9297                ),
9298                "unexpected {operation} error: {error:?}"
9299            );
9300        }
9301    }
9302
9303    fn materialize_external_key_info_chain(
9304        terminal_reference: &str,
9305    ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
9306        let a = format!(
9307            r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
9308                <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
9309                <ds:KeyInfo ID="next"><dsig11:KeyInfoReference URI="b.xml#root"/></ds:KeyInfo>
9310            </doc>"##
9311        );
9312        let b = format!(
9313            r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
9314                <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
9315                <ds:KeyInfo ID="next">{terminal_reference}</ds:KeyInfo>
9316            </doc>"##
9317        );
9318        let resources = HashMap::from([
9319            ("a.xml".to_owned(), a.into_bytes()),
9320            ("b.xml".to_owned(), b.into_bytes()),
9321        ]);
9322        let document = XmlDocument::parse("<root/>").unwrap();
9323        let mut key_info = KeyInfo {
9324            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9325                uri: "a.xml#root".into(),
9326            }],
9327        };
9328        let mut policy = crate::policy::VerificationPolicy::default();
9329        policy.key_sources.key_info_reference = true;
9330        policy.uris.key_info_references = UriTypeSet::ALL;
9331        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
9332        let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
9333        let mut budgets = RetrievalMaterializationBudgets {
9334            key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
9335                &policy.resources,
9336            )),
9337            xpath_parse: &mut xpath_parse_budget,
9338            execution: &execution_budget,
9339            resources: &policy.resources,
9340            certificate_signature_algorithms: policy
9341                .key_trust
9342                .certificate_signature_algorithms
9343                .as_ref(),
9344            xml_backend: crate::XmlBackend::default(),
9345        };
9346        let mut materialization = KeyInfoMaterializationState::default();
9347
9348        document.with_view(|view| {
9349            let resolver = UriReferenceResolver::with_document_view(view, &[])
9350                .with_external_resources(&resources);
9351            materialize_key_info_references_with_budgets(
9352                &mut key_info,
9353                &resolver,
9354                &policy,
9355                crate::provider::default_provider(),
9356                &mut budgets,
9357                &mut materialization,
9358            )?;
9359            Ok::<_, SignatureVerificationPipelineError>(())
9360        })?;
9361        Ok(key_info)
9362    }
9363
9364    fn materialize_external_key_info_bytes_with_limit(
9365        encoded: Vec<u8>,
9366        maximum_document_bytes: usize,
9367    ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
9368        let resources = HashMap::from([("key.xml".to_owned(), encoded)]);
9369        let document = XmlDocument::parse("<root/>").unwrap();
9370        let mut key_info = KeyInfo {
9371            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9372                uri: "key.xml".into(),
9373            }],
9374        };
9375        let mut policy = crate::policy::VerificationPolicy::default();
9376        policy.key_sources.key_info_reference = true;
9377        policy.uris.key_info_references = UriTypeSet::ALL;
9378        policy.resources.max_xml_document_bytes = maximum_document_bytes;
9379        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
9380        let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
9381        let mut budgets = RetrievalMaterializationBudgets {
9382            key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
9383                &policy.resources,
9384            )),
9385            xpath_parse: &mut xpath_parse_budget,
9386            execution: &execution_budget,
9387            resources: &policy.resources,
9388            certificate_signature_algorithms: policy
9389                .key_trust
9390                .certificate_signature_algorithms
9391                .as_ref(),
9392            xml_backend: crate::XmlBackend::default(),
9393        };
9394        let mut materialization = KeyInfoMaterializationState::default();
9395
9396        document.with_view(|view| {
9397            let resolver = UriReferenceResolver::with_document_view(view, &[])
9398                .with_external_resources(&resources);
9399            materialize_key_info_references_with_budgets(
9400                &mut key_info,
9401                &resolver,
9402                &policy,
9403                crate::provider::default_provider(),
9404                &mut budgets,
9405                &mut materialization,
9406            )?;
9407            Ok::<_, SignatureVerificationPipelineError>(())
9408        })?;
9409        Ok(key_info)
9410    }
9411
9412    fn materialize_external_key_info_bytes(
9413        encoded: Vec<u8>,
9414    ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
9415        materialize_external_key_info_bytes_with_limit(
9416            encoded,
9417            crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
9418        )
9419    }
9420
9421    #[test]
9422    fn external_key_info_reference_decodes_utf16_xml_octets() {
9423        // External XML follows the XML encoding declaration/BOM contract, not
9424        // the UTF-8-only contract of Rust strings passed by direct callers.
9425        let xml = format!(
9426            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>utf16-key</ds:KeyName></ds:KeyInfo>"#
9427        );
9428        let mut encoded = vec![0xff, 0xfe];
9429        encoded.extend(xml.encode_utf16().flat_map(u16::to_le_bytes));
9430        let key_info = materialize_external_key_info_bytes(encoded)
9431            .expect("UTF-16 external KeyInfo must materialize");
9432        assert!(matches!(
9433            key_info.sources.as_slice(),
9434            [super::super::parse::KeyInfoSource::KeyName(name)] if name == "utf16-key"
9435        ));
9436    }
9437
9438    #[test]
9439    fn external_key_info_reference_rejects_malformed_xml_encoding() {
9440        // A BOM selecting UTF-16 must not permit a truncated code unit to reach
9441        // the XML parser under a misleading structural diagnostic.
9442        let error = materialize_external_key_info_bytes(vec![0xff, 0xfe, b'<'])
9443            .expect_err("truncated UTF-16 must be rejected during octet decoding");
9444
9445        assert!(matches!(
9446            error,
9447            SignatureVerificationPipelineError::InvalidStructure {
9448                reason: "KeyInfoReference external resource has an invalid XML encoding"
9449            }
9450        ));
9451    }
9452
9453    #[test]
9454    fn external_key_info_reference_preserves_decoded_document_limit() {
9455        // Decoding can expand a compact single-byte resource beyond the XML document policy.
9456        // That is a typed policy failure, not malformed KeyInfo structure.
9457        let encoded = b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><ds:KeyInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"><ds:KeyName>\xe9\xe9</ds:KeyName></ds:KeyInfo>".to_vec();
9458        let maximum = encoded.len();
9459        let error = materialize_external_key_info_bytes_with_limit(encoded, maximum)
9460            .expect_err("decoded XML expansion must retain resource-limit classification");
9461
9462        assert!(matches!(
9463            error,
9464            SignatureVerificationPipelineError::Policy(
9465                crate::policy::PolicyViolation::ResourceLimit {
9466                    resource: crate::policy::resource_name::XML_DOCUMENT,
9467                    maximum: observed_maximum,
9468                    actual,
9469                }
9470            ) if observed_maximum == maximum && actual > maximum
9471        ));
9472    }
9473
9474    #[test]
9475    fn key_info_reference_cycle_identity_includes_the_owning_resource() {
9476        // Equal fragment spellings in separate external documents are distinct
9477        // references and must not be rejected as a recursive cycle.
9478        let key_info = materialize_external_key_info_chain("<ds:KeyName>terminal</ds:KeyName>")
9479            .expect("cross-document duplicate fragments must materialize");
9480        assert!(matches!(
9481            key_info.sources.as_slice(),
9482            [super::super::parse::KeyInfoSource::KeyName(name)] if name == "terminal"
9483        ));
9484    }
9485
9486    #[test]
9487    fn key_info_reference_cycle_identity_survives_external_reparse() {
9488        // Returning to the same external resource must remain a cycle even
9489        // though each dereference creates a fresh owned XML document.
9490        let error =
9491            materialize_external_key_info_chain("<dsig11:KeyInfoReference URI=\"a.xml#root\"/>")
9492                .expect_err("a resource cycle must fail before exhausting depth");
9493        assert!(matches!(
9494            error,
9495            SignatureVerificationPipelineError::InvalidStructure {
9496                reason: "KeyInfoReference cycle detected"
9497            }
9498        ));
9499    }
9500
9501    #[test]
9502    fn retrieval_method_materialization_deduplicates_within_count_limit() {
9503        // Repeated references to the same raw certificate produce one parsed
9504        // key source rather than one certificate clone per XML element.
9505        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
9506        let certificate = include_bytes!(
9507            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
9508        )
9509        .to_vec();
9510        let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
9511        let mut key_info = KeyInfo {
9512            sources: (0..MAX_RETRIEVAL_METHOD_COUNT)
9513                .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
9514                    uri: "urn:certificate".into(),
9515                    resource_type: Some(RAW_X509_TYPE.into()),
9516                    transforms: RetrievalMethodTransforms::None,
9517                })
9518                .collect(),
9519        };
9520        let document = Document::parse("<root/>").unwrap();
9521        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9522
9523        materialize_retrieval_methods(
9524            &mut key_info,
9525            &resolver,
9526            UriTypeSet::ALL,
9527            None,
9528            crate::provider::default_provider(),
9529        )
9530        .unwrap();
9531        assert!(matches!(
9532            key_info.sources.as_slice(),
9533            [super::super::parse::KeyInfoSource::X509Data(info)]
9534                if info.certificates.len() == 1
9535        ));
9536    }
9537
9538    #[test]
9539    fn retrieval_method_candidate_budget_includes_embedded_key_values() {
9540        // A previously parsed KeyValue consumes the sole candidate slot, so
9541        // malformed retrieved DER must be rejected by policy before X.509 parsing.
9542        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
9543        let resources = HashMap::from([("urn:certificate".to_string(), vec![1, 2, 3])]);
9544        let mut key_info = KeyInfo {
9545            sources: vec![
9546                super::super::parse::KeyInfoSource::KeyValue(
9547                    super::super::parse::KeyValueInfo::Unsupported {
9548                        namespace: Some(XMLDSIG_NS.into()),
9549                        local_name: "FutureKeyValue".into(),
9550                    },
9551                ),
9552                super::super::parse::KeyInfoSource::RetrievalMethod {
9553                    uri: "urn:certificate".into(),
9554                    resource_type: Some(RAW_X509_TYPE.into()),
9555                    transforms: RetrievalMethodTransforms::None,
9556                },
9557            ],
9558        };
9559        let document = Document::parse("<root/>").unwrap();
9560        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9561        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
9562        let execution_budget = TransformExecutionBudget::default();
9563        let resource_policy = crate::policy::ResourcePolicy {
9564            max_key_candidates: 1,
9565            ..crate::policy::ResourcePolicy::default()
9566        };
9567        let mut budgets = RetrievalMaterializationBudgets {
9568            key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
9569                &resource_policy,
9570            )),
9571            xpath_parse: &mut xpath_parse_budget,
9572            execution: &execution_budget,
9573            resources: &resource_policy,
9574            certificate_signature_algorithms: None,
9575            xml_backend: crate::XmlBackend::default(),
9576        };
9577        let mut candidate_work = key_info.embedded_candidate_count();
9578
9579        let mut policy = crate::policy::VerificationPolicy::default();
9580        policy.uris.retrieval_methods = UriTypeSet::ALL;
9581        let error = materialize_retrieval_methods_with_budgets(
9582            &mut key_info,
9583            &resolver,
9584            &policy,
9585            crate::provider::default_provider(),
9586            &mut budgets,
9587            &mut candidate_work,
9588        )
9589        .expect_err("the retrieved certificate must exceed the aggregate candidate limit");
9590
9591        assert!(matches!(
9592            error,
9593            SignatureVerificationPipelineError::Policy(
9594                crate::policy::PolicyViolation::ResourceLimit {
9595                    resource: crate::policy::resource_name::KEY_CANDIDATES,
9596                    maximum: 1,
9597                    actual: 2,
9598                }
9599            )
9600        ));
9601    }
9602
9603    #[test]
9604    fn raw_x509_retrieval_rejects_empty_same_document_uri() {
9605        // rawX509Certificate consumes external DER octets; an empty URI denotes
9606        // the XML document and must never become a key into the external map.
9607        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
9608        let certificate = include_bytes!(
9609            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
9610        )
9611        .to_vec();
9612        let resources = HashMap::from([(String::new(), certificate)]);
9613        let mut key_info = KeyInfo {
9614            sources: vec![super::super::parse::KeyInfoSource::RetrievalMethod {
9615                uri: String::new(),
9616                resource_type: Some(RAW_X509_TYPE.into()),
9617                transforms: RetrievalMethodTransforms::None,
9618            }],
9619        };
9620        let document = Document::parse("<root/>").unwrap();
9621        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9622
9623        let error = materialize_retrieval_methods(
9624            &mut key_info,
9625            &resolver,
9626            UriTypeSet::ALL,
9627            None,
9628            crate::provider::default_provider(),
9629        )
9630        .expect_err("empty URI must retain same-document semantics");
9631        assert!(matches!(
9632            error,
9633            SignatureVerificationPipelineError::InvalidStructure {
9634                reason: "raw X509 RetrievalMethod requires an untransformed external URI"
9635            }
9636        ));
9637    }
9638
9639    #[test]
9640    fn verify_context_does_not_hide_malformed_digest_behind_unsupported_transform() {
9641        // A bad DigestValue remains a parse error even when its transform URI is unsupported.
9642        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
9643            let xml = xml.replacen(
9644                "<ds:Reference URI=\"#target\">",
9645                "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
9646                1,
9647            );
9648            replace_fixture_manifest_digest(&xml, "!!!")
9649        });
9650
9651        let error = VerifyContext::new()
9652            .key(&AcceptingKey)
9653            .process_manifests(true)
9654            .verify(&broken_xml)
9655            .expect_err("malformed Manifest digest must not become a validity result");
9656        assert!(matches!(
9657            error,
9658            SignatureVerificationPipelineError::ParseManifestReference(_)
9659        ));
9660    }
9661
9662    #[test]
9663    fn verify_context_rejects_manifest_non_whitespace_mixed_content() {
9664        // Authenticated mixed content is still structurally invalid under the
9665        // Manifest element-only grammar.
9666        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
9667            xml.replacen(
9668                "<ds:Manifest ID=\"manifest\">",
9669                "<ds:Manifest ID=\"manifest\">junk",
9670                1,
9671            )
9672        });
9673
9674        let err = VerifyContext::new()
9675            .key(&AcceptingKey)
9676            .process_manifests(true)
9677            .verify(&xml)
9678            .expect_err("Manifest mixed content must fail verification");
9679        assert!(matches!(
9680            err,
9681            SignatureVerificationPipelineError::InvalidStructure {
9682                reason: "Manifest contains non-whitespace mixed content"
9683            }
9684        ));
9685    }
9686
9687    #[test]
9688    fn verify_context_rejects_empty_manifest_children() {
9689        // An authenticated empty Manifest violates the required Reference+
9690        // content model rather than disappearing as an unsigned block.
9691        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
9692            let (prefix, rest) = xml
9693                .split_once("<ds:Manifest ID=\"manifest\">")
9694                .expect("fixture should contain Manifest");
9695            let (_, suffix) = rest
9696                .split_once("</ds:Manifest>")
9697                .expect("fixture should contain closing Manifest");
9698            format!("{prefix}<ds:Manifest ID=\"manifest\"></ds:Manifest>{suffix}")
9699        });
9700
9701        let err = VerifyContext::new()
9702            .key(&AcceptingKey)
9703            .process_manifests(true)
9704            .verify(&xml)
9705            .expect_err("empty Manifest must fail verification");
9706        assert!(matches!(
9707            err,
9708            SignatureVerificationPipelineError::InvalidStructure {
9709                reason: "Manifest must contain at least one ds:Reference element child"
9710            }
9711        ));
9712    }
9713
9714    #[test]
9715    fn verify_context_ignores_unsigned_malformed_manifest_blocks() {
9716        let xml = signature_with_manifest_xml(true).replacen(
9717            "</ds:Object>",
9718            "</ds:Object><ds:Object><ds:Manifest>junk<ds:Foo/></ds:Manifest></ds:Object>",
9719            1,
9720        );
9721        let result = VerifyContext::new()
9722            .key(&AcceptingKey)
9723            .process_manifests(true)
9724            .verify(&xml)
9725            .expect("unsigned malformed Manifest must be ignored");
9726        assert_eq!(
9727            result.manifest_references.len(),
9728            1,
9729            "only signed Manifest references must be reported",
9730        );
9731        assert!(matches!(result.status, DsigStatus::Valid));
9732    }
9733
9734    #[test]
9735    fn verify_context_skips_ambiguous_manifest_id_blocks() {
9736        let xml = signature_with_manifest_xml(true).replacen(
9737            "</ds:Object>",
9738            "</ds:Object><ds:Object><ds:Manifest ID=\"manifest\">junk<ds:Foo/></ds:Manifest></ds:Object>",
9739            1,
9740        );
9741        let err = VerifyContext::new()
9742            .key(&RejectingKey)
9743            .process_manifests(true)
9744            .verify(&xml)
9745            .expect_err("ambiguous manifest IDs should make SignedInfo #manifest dereference fail");
9746        assert!(matches!(
9747            err,
9748            SignatureVerificationPipelineError::Reference(
9749                ReferenceProcessingError::UriDereference(
9750                    crate::xmldsig::types::TransformError::ElementNotFound(id)
9751                )
9752            ) if id == "manifest"
9753        ));
9754    }
9755
9756    #[test]
9757    fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
9758        let xml = minimal_signature_xml("", "");
9759        let err = VerifyContext::new()
9760            .key(&RejectingKey)
9761            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
9762            .verify(&xml)
9763            .expect_err("implicit default C14N must be checked against allowlist");
9764        assert!(matches!(
9765            err,
9766            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
9767                operation: "verification transform",
9768                ..
9769            })
9770        ));
9771    }
9772
9773    #[test]
9774    fn verify_context_skips_resolver_when_reference_processing_fails() {
9775        let xml = minimal_signature_xml("", "");
9776        let result = VerifyContext::new()
9777            .key_resolver(&PanicResolver)
9778            .verify(&xml)
9779            .expect("reference digest mismatch should short-circuit before resolver");
9780        assert!(matches!(
9781            result.status,
9782            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
9783        ));
9784    }
9785
9786    #[test]
9787    fn verify_context_leaves_fail_fast_reference_tail_unexecuted() {
9788        // A mismatch on the first Reference must not produce graph evidence for
9789        // the second Reference, which was never dereferenced or digested.
9790        let second_reference = r#"
9791    <ds:Reference URI="">
9792      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
9793      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
9794    </ds:Reference>"#;
9795        let xml = minimal_signature_xml("", "").replacen(
9796            "  </ds:SignedInfo>",
9797            &format!("{second_reference}\n  </ds:SignedInfo>"),
9798            1,
9799        );
9800
9801        let result = VerifyContext::new()
9802            .key(&AcceptingKey)
9803            .verify(&xml)
9804            .expect("digest mismatch must remain a verification result");
9805
9806        assert_eq!(result.signed_info_references.len(), 1);
9807        assert!(matches!(
9808            result.signed_info_references[0].status,
9809            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
9810        ));
9811    }
9812
9813    #[test]
9814    fn verify_context_reports_key_not_found_when_resolver_misses() {
9815        let xml = signature_with_target_reference("AQ==");
9816        let result = VerifyContext::new()
9817            .key_resolver(&MissingKeyResolver)
9818            .verify(&xml)
9819            .expect("resolver miss should report status, not pipeline error");
9820        assert!(matches!(
9821            result.status,
9822            DsigStatus::Invalid(FailureReason::KeyNotFound)
9823        ));
9824        assert_eq!(
9825            result.signed_info_references.len(),
9826            1,
9827            "KeyNotFound path must preserve SignedInfo reference diagnostics",
9828        );
9829        assert!(matches!(
9830            result.signed_info_references[0].status,
9831            DsigStatus::Valid
9832        ));
9833    }
9834
9835    #[test]
9836    fn verification_candidate_budget_covers_preset_and_custom_resolver_paths() {
9837        // Zero is a valid deny-all ceiling. Neither an already-resolved key nor
9838        // a custom resolver may bypass the operation-wide candidate policy.
9839        let xml = signature_with_target_reference("AQ==");
9840        let mut policy = crate::policy::VerificationPolicy::default();
9841        policy.resources.max_key_candidates = 0;
9842
9843        let preset_error = VerifyContext::new()
9844            .key(&RejectingKey)
9845            .policy(policy.clone())
9846            .verify(&xml)
9847            .expect_err("a preset key consumes one candidate");
9848        assert!(matches!(
9849            preset_error,
9850            SignatureVerificationPipelineError::Policy(
9851                crate::policy::PolicyViolation::ResourceLimit {
9852                    resource: crate::policy::resource_name::KEY_CANDIDATES,
9853                    maximum: 0,
9854                    actual: 1,
9855                }
9856            )
9857        ));
9858
9859        let resolver_error = VerifyContext::new()
9860            .key_resolver(&PanicResolver)
9861            .policy(policy)
9862            .verify(&xml)
9863            .expect_err("a custom resolver requires candidate capacity before dispatch");
9864        assert!(matches!(
9865            resolver_error,
9866            SignatureVerificationPipelineError::Policy(
9867                crate::policy::PolicyViolation::ResourceLimit {
9868                    resource: crate::policy::resource_name::KEY_CANDIDATES,
9869                    maximum: 0,
9870                    actual: 1,
9871                }
9872            )
9873        ));
9874    }
9875
9876    #[test]
9877    fn verification_candidate_account_is_not_reset_for_each_signature() {
9878        // A rejected signature still spends its candidate allowance in verify_all.
9879        let xml = signature_with_target_reference("AQ==");
9880        let start = xml
9881            .find("<ds:Signature>")
9882            .expect("generated signature start");
9883        let end = xml
9884            .find("</ds:Signature>")
9885            .expect("generated signature end")
9886            + "</ds:Signature>".len();
9887        let document =
9888            XmlDocument::parse(xml.replace("</root>", &format!("{}</root>", &xml[start..end])))
9889                .expect("two signatures");
9890        let mut policy = crate::policy::VerificationPolicy::default();
9891        policy.resources.max_key_candidates = 1;
9892        let evidence = VerifyContext::new()
9893            .key(&RejectingKey)
9894            .policy(policy)
9895            .verify_all(&document)
9896            .expect("signature-local failure report");
9897        assert_eq!(evidence.signatures().len(), 2);
9898        assert!(evidence.signatures()[0].result().is_ok());
9899        assert!(matches!(
9900            evidence.signatures()[1].result(),
9901            Err(DsigError::Policy(
9902                crate::policy::PolicyViolation::ResourceLimit {
9903                    resource: crate::policy::resource_name::KEY_CANDIDATES,
9904                    maximum: 1,
9905                    actual: 2,
9906                }
9907            ))
9908        ));
9909    }
9910
9911    #[test]
9912    fn resolver_receives_the_certificate_account_spent_by_ingestion() {
9913        // The public resolver hook receives prior parsing work, not a fresh allowance.
9914        struct ProbeResolver {
9915            called: Cell<bool>,
9916        }
9917        impl KeyResolver for ProbeResolver {
9918            fn resolve<'a>(
9919                &'a self,
9920                _info: Option<&KeyInfo>,
9921                _algorithm: SignatureAlgorithm,
9922            ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
9923                panic!("core must dispatch through the budget-aware hook");
9924            }
9925            fn resolve_for_verification_with_budget<'a>(
9926                &'a self,
9927                _info: Option<&KeyInfo>,
9928                _algorithm: SignatureAlgorithm,
9929                _policy: &crate::policy::VerificationPolicy,
9930                _provider: &dyn crate::provider::CryptoProvider,
9931                budget: &mut super::super::InspectedKeyCandidateBudget,
9932            ) -> Result<Option<super::super::ResolvedVerificationKey<'a>>, DsigError> {
9933                assert!(budget.execution().work_used() > 0);
9934                budget.charge()?;
9935                self.called.set(true);
9936                Ok(Some(super::super::ResolvedVerificationKey::CallerTrusted(
9937                    Box::new(AcceptingKey),
9938                )))
9939            }
9940            fn consumes_document_key_info(&self) -> bool {
9941                true
9942            }
9943        }
9944        let pair = rcgen::KeyPair::generate().expect("test key generation");
9945        let certificate = rcgen::CertificateParams::new(Vec::<String>::new())
9946            .expect("empty SAN list")
9947            .self_signed(&pair)
9948            .expect("test certificate");
9949        let encoded = base64::engine::general_purpose::STANDARD.encode(certificate.der());
9950        let xml = signature_with_target_reference("AQ==").replace("</ds:Signature>", &format!(
9951            "<ds:KeyInfo><ds:X509Data><ds:X509Certificate>{encoded}</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>",
9952        ));
9953        let resolver = ProbeResolver {
9954            called: Cell::new(false),
9955        };
9956        let result = VerifyContext::new()
9957            .key_resolver(&resolver)
9958            .verify(&xml)
9959            .expect("shared-account resolver dispatch");
9960        assert!(matches!(result.status, DsigStatus::Valid));
9961        assert!(resolver.called.get());
9962    }
9963
9964    #[test]
9965    fn verification_candidate_budget_precedes_embedded_x509_parsing() {
9966        // The first certificate is valid and consumes the sole permitted slot;
9967        // malformed bytes in the second must never reach the X.509 parser.
9968        let first_certificate = base64::engine::general_purpose::STANDARD.encode(include_bytes!(
9969            "../../tests/fixtures/xmldsig/phaos-xmldsig-three/certs/rsa-cert.der"
9970        ));
9971        let xml = signature_with_target_reference("AQ==").replace(
9972            "</ds:SignatureValue>\n  </ds:Signature>",
9973            &format!(
9974                "</ds:SignatureValue>\n    <ds:KeyInfo><ds:X509Data><ds:X509Certificate>{first_certificate}</ds:X509Certificate><ds:X509Certificate>AQID</ds:X509Certificate></ds:X509Data></ds:KeyInfo>\n  </ds:Signature>"
9975            ),
9976        );
9977        let mut policy = crate::policy::VerificationPolicy::default();
9978        policy.resources.max_key_candidates = 1;
9979
9980        let error = VerifyContext::new()
9981            .policy(policy)
9982            .verify(&xml)
9983            .expect_err("candidate policy must run before embedded certificate parsing");
9984
9985        assert!(matches!(
9986            error,
9987            SignatureVerificationPipelineError::Policy(
9988                crate::policy::PolicyViolation::ResourceLimit {
9989                    resource: crate::policy::resource_name::KEY_CANDIDATES,
9990                    maximum: 1,
9991                    actual: 2,
9992                }
9993            )
9994        ));
9995    }
9996
9997    #[test]
9998    fn verify_context_resolver_can_ignore_malformed_keyinfo_by_default() {
9999        let base_xml = signature_with_target_reference("AQ==");
10000        let xml = base_xml
10001            .replace(
10002                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
10003                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
10004            )
10005            .replace(
10006                "</ds:SignatureValue>\n  </ds:Signature>",
10007                "</ds:SignatureValue>\n    <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n  </ds:Signature>",
10008            );
10009
10010        let result = VerifyContext::new()
10011            .key_resolver(&MissingKeyResolver)
10012            .verify(&xml)
10013            .expect("resolver path should not hard-fail on advisory malformed KeyInfo by default");
10014        assert!(matches!(
10015            result.status,
10016            DsigStatus::Invalid(FailureReason::KeyNotFound)
10017        ));
10018    }
10019
10020    #[test]
10021    fn verify_context_resolver_can_opt_in_to_keyinfo_parse_failures() {
10022        let base_xml = signature_with_target_reference("AQ==");
10023        let xml = base_xml
10024            .replace(
10025                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
10026                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
10027            )
10028            .replace(
10029                "</ds:SignatureValue>\n  </ds:Signature>",
10030                "</ds:SignatureValue>\n    <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n  </ds:Signature>",
10031            );
10032
10033        let err = VerifyContext::new()
10034            .key_resolver(&ConsumingKeyInfoResolver)
10035            .verify(&xml)
10036            .expect_err("resolver opted into KeyInfo parsing, malformed KeyInfo must fail");
10037        assert!(matches!(
10038            err,
10039            SignatureVerificationPipelineError::ParseKeyInfo(_)
10040        ));
10041    }
10042
10043    #[test]
10044    fn verify_context_ignores_unsupported_retrieval_before_valid_key_source() {
10045        // An advisory vendor RetrievalMethod cannot prevent the resolver from
10046        // reaching a later supported source in document order.
10047        let xml = signature_with_target_reference("AQ==").replace(
10048            "</ds:SignatureValue>\n  </ds:Signature>",
10049            r##"</ds:SignatureValue>
10050    <ds:KeyInfo>
10051      <ds:RetrievalMethod URI="#vendor" Type="urn:vendor:key">
10052        <ds:Transforms><ds:Transform Algorithm="urn:vendor:transform"/></ds:Transforms>
10053      </ds:RetrievalMethod>
10054      <ds:KeyName>fallback</ds:KeyName>
10055    </ds:KeyInfo>
10056  </ds:Signature>"##,
10057        );
10058
10059        let result = VerifyContext::new()
10060            .key_resolver(&FallbackKeyInfoResolver)
10061            .verify(&xml)
10062            .expect("unsupported advisory retrieval must not abort key resolution");
10063        assert_eq!(result.status, DsigStatus::Valid);
10064    }
10065
10066    #[test]
10067    fn verify_context_does_not_eagerly_fail_unused_retrieval_fallback() {
10068        // KeyInfo sources are alternatives in document order. Once an earlier
10069        // source resolves, a missing later RetrievalMethod is irrelevant.
10070        let xml = signature_with_target_reference("AQ==").replace(
10071            "</ds:SignatureValue>\n  </ds:Signature>",
10072            r#"</ds:SignatureValue>
10073    <ds:KeyInfo>
10074      <ds:KeyName>primary</ds:KeyName>
10075      <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
10076    </ds:KeyInfo>
10077  </ds:Signature>"#,
10078        );
10079
10080        let result = VerifyContext::new()
10081            .key_resolver(&EarlyKeyInfoResolver)
10082            .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
10083            .verify(&xml)
10084            .expect("an unused missing retrieval fallback must not abort verification");
10085
10086        assert_eq!(result.status, DsigStatus::Valid);
10087    }
10088
10089    #[test]
10090    fn verify_context_does_not_eagerly_fail_unused_same_document_x509_retrieval() {
10091        // Same-document X509Data retrieval is an ordered alternative just like
10092        // raw-certificate retrieval; a resolved earlier source makes it unused.
10093        let xml = signature_with_target_reference("AQ==").replace(
10094            "</ds:SignatureValue>\n  </ds:Signature>",
10095            r##"</ds:SignatureValue>
10096    <ds:KeyInfo>
10097      <ds:KeyName>primary</ds:KeyName>
10098      <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
10099    </ds:KeyInfo>
10100  </ds:Signature>"##,
10101        );
10102
10103        let result = VerifyContext::new()
10104            .key_resolver(&EarlyKeyInfoResolver)
10105            .verify(&xml)
10106            .expect("an unused missing X509Data retrieval fallback must not abort verification");
10107
10108        assert_eq!(result.status, DsigStatus::Valid);
10109    }
10110
10111    #[test]
10112    fn verify_context_reports_missing_same_document_x509_retrieval_without_fallback() {
10113        // Deferred failures retain their exact diagnostic when no alternative
10114        // source resolves the verification key.
10115        let xml = signature_with_target_reference("AQ==").replace(
10116            "</ds:SignatureValue>\n  </ds:Signature>",
10117            r##"</ds:SignatureValue>
10118    <ds:KeyInfo>
10119      <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
10120    </ds:KeyInfo>
10121  </ds:Signature>"##,
10122        );
10123
10124        let error = VerifyContext::new()
10125            .key_resolver(&ConsumingKeyInfoResolver)
10126            .verify(&xml)
10127            .expect_err("a sole missing X509Data retrieval must remain an explicit error");
10128
10129        assert!(matches!(
10130            error,
10131            SignatureVerificationPipelineError::InvalidStructure {
10132                reason: "X509Data RetrievalMethod target is missing or ambiguous"
10133            }
10134        ));
10135    }
10136
10137    #[test]
10138    fn verify_context_does_not_eagerly_parse_unused_retrieval_fallback() {
10139        // Materialization must preserve ordered fallback semantics even when
10140        // caller-supplied bytes exist but are not a certificate.
10141        let xml = signature_with_target_reference("AQ==").replace(
10142            "</ds:SignatureValue>\n  </ds:Signature>",
10143            r#"</ds:SignatureValue>
10144    <ds:KeyInfo>
10145      <ds:KeyName>primary</ds:KeyName>
10146      <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
10147    </ds:KeyInfo>
10148  </ds:Signature>"#,
10149        );
10150        let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
10151
10152        let result = VerifyContext::new()
10153            .key_resolver(&EarlyKeyInfoResolver)
10154            .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
10155            .external_resources(&resources)
10156            .verify(&xml)
10157            .expect("an unused malformed retrieval fallback must not abort verification");
10158
10159        assert_eq!(result.status, DsigStatus::Valid);
10160    }
10161
10162    #[test]
10163    fn verify_context_reports_missing_retrieval_when_no_key_source_resolves() {
10164        // Deferral changes ordering, not diagnostics: if no alternative source
10165        // resolves, the first missing retrieval remains the pipeline failure.
10166        let xml = signature_with_target_reference("AQ==").replace(
10167            "</ds:SignatureValue>\n  </ds:Signature>",
10168            r#"</ds:SignatureValue>
10169    <ds:KeyInfo>
10170      <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
10171    </ds:KeyInfo>
10172  </ds:Signature>"#,
10173        );
10174
10175        let error = VerifyContext::new()
10176            .key_resolver(&ConsumingKeyInfoResolver)
10177            .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
10178            .verify(&xml)
10179            .expect_err("a missing sole RetrievalMethod must remain an explicit error");
10180
10181        assert!(matches!(
10182            error,
10183            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
10184                crate::xmldsig::TransformError::UnsupportedUri(uri)
10185            )) if uri == "missing.der"
10186        ));
10187    }
10188
10189    #[test]
10190    fn verify_context_reports_malformed_retrieval_when_no_key_source_resolves() {
10191        // Deferral must retain the parse error when the malformed certificate
10192        // is the only candidate rather than degrading it to KeyNotFound.
10193        let xml = signature_with_target_reference("AQ==").replace(
10194            "</ds:SignatureValue>\n  </ds:Signature>",
10195            r#"</ds:SignatureValue>
10196    <ds:KeyInfo>
10197      <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
10198    </ds:KeyInfo>
10199  </ds:Signature>"#,
10200        );
10201        let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
10202
10203        let error = VerifyContext::new()
10204            .key_resolver(&ConsumingKeyInfoResolver)
10205            .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
10206            .external_resources(&resources)
10207            .verify(&xml)
10208            .expect_err("a malformed sole RetrievalMethod must remain a parse error");
10209
10210        assert!(matches!(
10211            error,
10212            SignatureVerificationPipelineError::ParseKeyInfo(_)
10213        ));
10214    }
10215
10216    #[test]
10217    fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
10218        let xml = signature_with_target_reference("@@@");
10219
10220        let err = VerifyContext::new()
10221            .key_resolver(&MissingKeyResolver)
10222            .verify(&xml)
10223            .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
10224        assert!(matches!(
10225            err,
10226            SignatureVerificationPipelineError::SignatureValueBase64(_)
10227        ));
10228    }
10229
10230    #[test]
10231    fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
10232        let xml = signature_with_target_reference("@@@");
10233
10234        let err = VerifyContext::new()
10235            .verify(&xml)
10236            .expect_err("invalid SignatureValue must remain a decode error");
10237        assert!(matches!(
10238            err,
10239            SignatureVerificationPipelineError::SignatureValueBase64(_)
10240        ));
10241    }
10242
10243    #[test]
10244    fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
10245        let references = vec![Reference {
10246            uri: None,
10247            id: None,
10248            ref_type: None,
10249            transforms: vec![],
10250            digest_method: DigestAlgorithm::Sha256,
10251            digest_value: vec![0; 32],
10252        }];
10253        let uri_types = UriTypeSet {
10254            allow_empty: false,
10255            allow_same_document: true,
10256            allow_external: false,
10257        };
10258
10259        let err = enforce_reference_policies(&references, uri_types, None)
10260            .expect_err("missing URI must fail before allow_empty policy is evaluated");
10261        assert!(matches!(
10262            err,
10263            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
10264        ));
10265    }
10266
10267    #[test]
10268    fn enforce_reference_policies_checks_only_terminal_binary_output() {
10269        let c14n = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI).unwrap();
10270        let allowed = HashSet::from([
10271            BASE64_TRANSFORM_URI.to_owned(),
10272            DEFAULT_IMPLICIT_C14N_URI.to_owned(),
10273        ]);
10274        let without_implicit_c14n = HashSet::from([BASE64_TRANSFORM_URI.to_owned()]);
10275
10276        for transforms in [
10277            vec![Transform::Base64Decode, Transform::C14n(c14n)],
10278            vec![Transform::Base64Decode, Transform::Base64Decode],
10279        ] {
10280            let reference = make_reference("", transforms, DigestAlgorithm::Sha256, vec![0; 32]);
10281            enforce_reference_policies(
10282                std::slice::from_ref(&reference),
10283                UriTypeSet::default(),
10284                Some(&allowed),
10285            )
10286            .expect("terminal binary output must not require implicit C14N");
10287        }
10288
10289        let terminal_base64 = make_reference(
10290            "",
10291            vec![Transform::Base64Decode, Transform::Base64Decode],
10292            DigestAlgorithm::Sha256,
10293            vec![0; 32],
10294        );
10295        enforce_reference_policies(
10296            std::slice::from_ref(&terminal_base64),
10297            UriTypeSet::default(),
10298            Some(&without_implicit_c14n),
10299        )
10300        .expect("terminal Base64 output must not require implicit C14N");
10301
10302        let no_transforms = make_reference("", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
10303        let error = enforce_reference_policies(
10304            std::slice::from_ref(&no_transforms),
10305            UriTypeSet::default(),
10306            Some(&without_implicit_c14n),
10307        )
10308        .expect_err("a node-set result must require allowlisted implicit C14N");
10309        assert!(matches!(
10310            error,
10311            SignatureVerificationPipelineError::Policy(
10312                crate::policy::PolicyViolation::Algorithm {
10313                    operation: "verification transform",
10314                    ref algorithm,
10315                }
10316            )
10317                if algorithm == DEFAULT_IMPLICIT_C14N_URI
10318        ));
10319
10320        let detached = make_reference("urn:payload", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
10321        enforce_reference_policies(
10322            std::slice::from_ref(&detached),
10323            UriTypeSet::ALL,
10324            Some(&without_implicit_c14n),
10325        )
10326        .expect("external octets without transforms must not require implicit C14N");
10327
10328        let external_xpath = make_reference(
10329            "urn:payload",
10330            vec![Transform::XPath(
10331                super::super::transforms::XPathExpression::new("true()"),
10332            )],
10333            DigestAlgorithm::Sha256,
10334            vec![0; 32],
10335        );
10336        let error = enforce_reference_policies(
10337            std::slice::from_ref(&external_xpath),
10338            UriTypeSet::ALL,
10339            Some(&HashSet::from([XPATH_TRANSFORM_URI.to_owned()])),
10340        )
10341        .expect_err("external XML converted to a node-set must require implicit C14N");
10342        assert!(matches!(
10343            error,
10344            SignatureVerificationPipelineError::Policy(
10345                crate::policy::PolicyViolation::Algorithm {
10346                    operation: "verification transform",
10347                    ref algorithm,
10348                }
10349            )
10350                if algorithm == DEFAULT_IMPLICIT_C14N_URI
10351        ));
10352    }
10353
10354    #[test]
10355    fn stored_pre_digest_budget_counts_repeated_external_references() {
10356        // The caller map owns one bounded payload, but diagnostic retention is
10357        // charged per Reference because every result owns its pre-digest bytes.
10358        let document =
10359            Document::parse("<ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"/>")
10360                .unwrap();
10361        let payload = vec![b'x'; 7];
10362        let digest = compute_digest(DigestAlgorithm::Sha256, &payload);
10363        let references = (0..5)
10364            .map(|_| {
10365                make_reference(
10366                    "urn:repeated",
10367                    Vec::new(),
10368                    DigestAlgorithm::Sha256,
10369                    digest.clone(),
10370                )
10371            })
10372            .collect::<Vec<_>>();
10373        let resources = HashMap::from([("urn:repeated".to_owned(), payload)]);
10374        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
10375        let transform_budget = TransformExecutionBudget::default();
10376        let canonicalized_data_budget = CanonicalizedDataBudget::with_limit(32);
10377        let execution = ReferenceExecutionContext {
10378            store_pre_digest: true,
10379            transform_options: TransformOptions::default(),
10380            transform_budget: &transform_budget,
10381            canonicalized_data_budget: &canonicalized_data_budget,
10382            provider: crate::provider::default_provider(),
10383            capture: &(),
10384        };
10385
10386        let error = process_all_references_with_options(
10387            &references,
10388            &resolver,
10389            document.root_element(),
10390            &execution,
10391        )
10392        .expect_err(
10393            "retained diagnostics must not multiply one external allocation past the aggregate cap",
10394        );
10395        assert!(matches!(
10396            error,
10397            ReferenceProcessingError::Policy(crate::policy::PolicyViolation::ResourceLimit {
10398                resource: "canonicalized bytes",
10399                maximum: 32,
10400                ..
10401            })
10402        ));
10403    }
10404
10405    #[test]
10406    fn public_reference_entries_inherit_custom_id_registration() {
10407        // Both public entry points must pass resolver registrations to XPath.
10408        let xml = format!(
10409            "<root xmlns:ds='{XMLDSIG_NS}'><data Token='target'>YWJj</data><ds:Signature/></root>"
10410        );
10411        let document = Document::parse(&xml).unwrap();
10412        let signature = document
10413            .descendants()
10414            .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
10415            .unwrap();
10416        let registrations = [crate::IdAttributeRegistration::global("Token")];
10417        let resolver = UriReferenceResolver::with_id_registrations(&document, &registrations);
10418        let expression = crate::xmldsig::transforms::XPathExpression::new(
10419            "count(id('target')) = 1 and count(ancestor-or-self::node() | id('target')) = count(ancestor-or-self::node())",
10420        );
10421        let reference = make_reference(
10422            "#target",
10423            vec![Transform::XPath(expression), Transform::Base64Decode],
10424            DigestAlgorithm::Sha256,
10425            compute_digest(DigestAlgorithm::Sha256, b"abc"),
10426        );
10427        let one = process_reference(
10428            &reference,
10429            &resolver,
10430            signature,
10431            ReferenceSet::SignedInfo,
10432            0,
10433            true,
10434        )
10435        .unwrap();
10436        assert_eq!(one.status, DsigStatus::Valid);
10437        let all = process_all_references(&[reference], &resolver, signature, true).unwrap();
10438        assert_eq!(all.results.len(), 1);
10439        assert_eq!(all.results[0].status, DsigStatus::Valid);
10440        assert!(all.first_failure.is_none());
10441    }
10442
10443    #[test]
10444    fn normalized_root_xpointers_bind_document_identity() {
10445        // Spelling changes must not discard coverage evidence for the root.
10446        let document = XmlDocument::parse("<root><item/></root>").unwrap();
10447        let budgets = VerificationOperationBudgets::with_transforms(
10448            &crate::policy::VerificationPolicy::default(),
10449            TransformExecutionBudget::default(),
10450            None,
10451        );
10452        document.with_view(|view| {
10453            let resolver = UriReferenceResolver::new(view.document());
10454            for uri in ["#xpointer(/)", "#xpointer( / )", "#xpointer(%2F)"] {
10455                let reference =
10456                    make_reference(uri, Vec::new(), DigestAlgorithm::Sha256, vec![0; 32]);
10457                assert_eq!(
10458                    budgets.resource_identity_for_reference(&reference, 0, &resolver, view, None),
10459                    OperationResourceIdentity::DocumentNode(view.root()),
10460                    "{uri}"
10461                );
10462            }
10463        });
10464    }
10465
10466    #[test]
10467    fn missing_external_identity_does_not_retain_uri_cache_copies() {
10468        // No bytes exist to fingerprint or reuse. The graph's Reference node
10469        // already carries provenance, so a missing URI must not enter the
10470        // available-resource memoization cache or be cloned with every decision.
10471        let document = XmlDocument::parse("<root/>").unwrap();
10472        let uri = format!("urn:missing:{}", "x".repeat(16384));
10473        let reference = make_reference(&uri, Vec::new(), DigestAlgorithm::Sha256, vec![0; 32]);
10474        let budgets = VerificationOperationBudgets::with_transforms(
10475            &crate::policy::VerificationPolicy::default(),
10476            TransformExecutionBudget::default(),
10477            None,
10478        );
10479        document.with_view(|view| {
10480            let resolver = UriReferenceResolver::new(view.document());
10481            let _ = budgets.resource_identity_for_reference(&reference, 0, &resolver, view, None);
10482            assert!(budgets.external_resource_identities.borrow().is_empty());
10483        });
10484    }
10485
10486    #[test]
10487    fn operation_memoizes_repeated_external_resource_identity() {
10488        // The external resource map is immutably borrowed for the operation,
10489        // so one content fingerprint safely identifies every repeated URI.
10490        let document = XmlDocument::parse("<root/>").expect("fixture must parse");
10491        let resources = HashMap::from([("urn:repeated".to_owned(), vec![b'x'; 8 * 1_024])]);
10492        let reference = make_reference(
10493            "urn:repeated",
10494            Vec::new(),
10495            DigestAlgorithm::Sha256,
10496            vec![0; 32],
10497        );
10498        let budgets = VerificationOperationBudgets::with_transforms(
10499            &crate::policy::VerificationPolicy::default(),
10500            TransformExecutionBudget::default(),
10501            None,
10502        );
10503
10504        document.with_view(|view| {
10505            let resolver =
10506                UriReferenceResolver::new(view.document()).with_external_resources(&resources);
10507            let first =
10508                budgets.resource_identity_for_reference(&reference, 0, &resolver, view, None);
10509            let second =
10510                budgets.resource_identity_for_reference(&reference, 1, &resolver, view, None);
10511
10512            assert_eq!(first, second);
10513            assert_eq!(budgets.external_resource_identities.borrow().len(), 1);
10514        });
10515    }
10516
10517    #[test]
10518    fn canonical_signed_info_obeys_policy_without_diagnostic_retention() {
10519        // SignedInfo is always materialized for crypto verification, so its
10520        // canonical bytes must consume the configured ceiling even when
10521        // diagnostics do not retain reference output.
10522        let xml = signature_with_target_reference("AQ==");
10523        let marker = "<ds:SignatureMethod";
10524        let padding = " ".repeat(1_025);
10525        let xml = xml.replacen(marker, &format!("{padding}{marker}"), 1);
10526        let policy = crate::policy::VerificationPolicy {
10527            resources: crate::policy::ResourcePolicy {
10528                max_canonicalized_bytes: 1_024,
10529                ..crate::policy::ResourcePolicy::default()
10530            },
10531            ..crate::policy::VerificationPolicy::default()
10532        };
10533
10534        let error = VerifyContext::new()
10535            .key(&AcceptingKey)
10536            .policy(policy)
10537            .verify(&xml)
10538            .expect_err("canonicalized SignedInfo must remain policy-bounded");
10539
10540        assert!(matches!(
10541            error,
10542            SignatureVerificationPipelineError::Policy(
10543                crate::policy::PolicyViolation::ResourceLimit {
10544                    resource: "canonicalized bytes",
10545                    ..
10546                }
10547            )
10548        ));
10549    }
10550
10551    #[test]
10552    fn fragmented_pq_signature_value_decodes_at_exact_wire_limit() {
10553        // Comments split the largest supported signature into tiny text nodes;
10554        // decoding must preserve bytes and reject an oversized aggregate.
10555        let parameter = super::super::pq_algorithm::PqAlgorithm::SlhDsaSha2_256f;
10556        let bytes = vec![0x5a; parameter.signature_len()];
10557        let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes);
10558        let mut xml = String::from("<SignatureValue>");
10559        for chunk in encoded.as_bytes().chunks(4) {
10560            xml.push_str(std::str::from_utf8(chunk).unwrap());
10561            xml.push_str("<!--split-->\n");
10562        }
10563        xml.push_str("</SignatureValue>");
10564        let document = Document::parse(&xml).unwrap();
10565        let algorithm = SignatureAlgorithm::PostQuantum(parameter);
10566        assert_eq!(
10567            decode_signature_value(
10568                document.root_element(),
10569                algorithm,
10570                &crate::policy::ResourcePolicy::default()
10571            )
10572            .unwrap(),
10573            bytes
10574        );
10575        let oversized = xml.replace("</SignatureValue>", "AAAA</SignatureValue>");
10576        let document = Document::parse(&oversized).unwrap();
10577        assert!(matches!(
10578            decode_signature_value(
10579                document.root_element(),
10580                algorithm,
10581                &crate::policy::ResourcePolicy::default()
10582            ),
10583            Err(SignatureVerificationPipelineError::InvalidStructure {
10584                reason: "SignatureValue exceeds maximum allowed length"
10585            })
10586        ));
10587    }
10588
10589    #[test]
10590    fn push_normalized_signature_text_rejects_form_feed() {
10591        let mut normalized = Vec::new();
10592        let mut raw_text_len = 0usize;
10593        let err = push_normalized_signature_text(
10594            "ab\u{000C}cd",
10595            &mut raw_text_len,
10596            &mut normalized,
10597            SignatureAlgorithm::RsaSha256,
10598            &crate::policy::ResourcePolicy::default(),
10599        )
10600        .expect_err("form-feed must not be treated as XML base64 whitespace");
10601        assert!(matches!(
10602            err,
10603            SignatureVerificationPipelineError::SignatureValueBase64(
10604                base64::DecodeError::InvalidByte(_, 0x0C)
10605            )
10606        ));
10607    }
10608
10609    #[test]
10610    fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
10611        let mut normalized = vec![b'A'; MAX_SIGNATURE_VALUE_LEN - 1];
10612        let mut raw_text_len = normalized.len();
10613        let err = push_normalized_signature_text(
10614            "é",
10615            &mut raw_text_len,
10616            &mut normalized,
10617            SignatureAlgorithm::RsaSha256,
10618            &crate::policy::ResourcePolicy::default(),
10619        )
10620        .expect_err("multibyte characters must not bypass byte-size limit");
10621        assert!(matches!(
10622            err,
10623            SignatureVerificationPipelineError::InvalidStructure {
10624                reason: "SignatureValue exceeds maximum allowed length"
10625            }
10626        ));
10627    }
10628
10629    // ── process_reference: happy path ────────────────────────────────
10630
10631    #[test]
10632    fn reference_with_correct_digest_passes() {
10633        // Create a simple document, compute its canonical form digest,
10634        // then verify that process_reference returns Valid status.
10635        let xml = r##"<root>
10636            <data>hello world</data>
10637            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
10638                <ds:SignedInfo/>
10639            </ds:Signature>
10640        </root>"##;
10641        let doc = Document::parse(xml).unwrap();
10642        let resolver = UriReferenceResolver::new(&doc);
10643        let sig_node = doc
10644            .descendants()
10645            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
10646            .unwrap();
10647
10648        // First, compute the expected digest by running the pipeline
10649        let initial_data = resolver.dereference("").unwrap();
10650        let transforms = vec![
10651            Transform::Enveloped,
10652            Transform::C14n(
10653                crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
10654                    .unwrap(),
10655            ),
10656        ];
10657        let pre_digest_bytes =
10658            crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
10659        let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
10660
10661        // Now build a Reference with the correct digest and verify
10662        let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
10663
10664        let result = process_reference(
10665            &reference,
10666            &resolver,
10667            sig_node,
10668            ReferenceSet::SignedInfo,
10669            0,
10670            false,
10671        )
10672        .unwrap();
10673        assert!(
10674            matches!(result.status, DsigStatus::Valid),
10675            "digest should match"
10676        );
10677        assert!(result.pre_digest_data.is_none());
10678    }
10679
10680    #[test]
10681    fn reference_with_wrong_digest_fails() {
10682        let xml = r##"<root>
10683            <data>hello</data>
10684            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
10685                <ds:SignedInfo/>
10686            </ds:Signature>
10687        </root>"##;
10688        let doc = Document::parse(xml).unwrap();
10689        let resolver = UriReferenceResolver::new(&doc);
10690        let sig_node = doc
10691            .descendants()
10692            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
10693            .unwrap();
10694
10695        let transforms = vec![Transform::Enveloped];
10696        // Wrong digest value — all zeros
10697        let wrong_digest = vec![0u8; 32];
10698        let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
10699
10700        let result = process_reference(
10701            &reference,
10702            &resolver,
10703            sig_node,
10704            ReferenceSet::SignedInfo,
10705            0,
10706            false,
10707        )
10708        .unwrap();
10709        assert!(matches!(
10710            result.status,
10711            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
10712        ));
10713    }
10714
10715    #[test]
10716    fn reference_with_wrong_digest_preserves_supplied_ref_index() {
10717        let xml = r##"<root>
10718            <data>hello</data>
10719            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
10720                <ds:SignedInfo/>
10721            </ds:Signature>
10722        </root>"##;
10723        let doc = Document::parse(xml).unwrap();
10724        let resolver = UriReferenceResolver::new(&doc);
10725        let sig_node = doc
10726            .descendants()
10727            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
10728            .unwrap();
10729
10730        let reference = make_reference(
10731            "",
10732            vec![Transform::Enveloped],
10733            DigestAlgorithm::Sha256,
10734            vec![0u8; 32],
10735        );
10736        let result = process_reference(
10737            &reference,
10738            &resolver,
10739            sig_node,
10740            ReferenceSet::SignedInfo,
10741            7,
10742            false,
10743        )
10744        .unwrap();
10745        assert!(matches!(
10746            result.status,
10747            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
10748        ));
10749    }
10750
10751    #[test]
10752    fn reference_stores_pre_digest_data() {
10753        let xml = "<root><child>text</child></root>";
10754        let doc = Document::parse(xml).unwrap();
10755        let resolver = UriReferenceResolver::new(&doc);
10756
10757        // No transforms, no enveloped — just canonicalize entire document
10758        let initial_data = resolver.dereference("").unwrap();
10759        let pre_digest =
10760            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
10761        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
10762
10763        let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
10764        let result = process_reference(
10765            &reference,
10766            &resolver,
10767            doc.root_element(),
10768            ReferenceSet::SignedInfo,
10769            0,
10770            true,
10771        )
10772        .unwrap();
10773
10774        assert!(matches!(result.status, DsigStatus::Valid));
10775        assert!(result.pre_digest_data.is_some());
10776        assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
10777    }
10778
10779    // ── process_reference: URI dereference ───────────────────────────
10780
10781    #[test]
10782    fn reference_with_id_uri() {
10783        let xml = r##"<root>
10784            <item ID="target">specific content</item>
10785            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
10786                <ds:SignedInfo/>
10787            </ds:Signature>
10788        </root>"##;
10789        let doc = Document::parse(xml).unwrap();
10790        let resolver = UriReferenceResolver::new(&doc);
10791        let sig_node = doc
10792            .descendants()
10793            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
10794            .unwrap();
10795
10796        // Compute expected digest for the #target subtree
10797        let initial_data = resolver.dereference("#target").unwrap();
10798        let transforms = vec![Transform::C14n(
10799            crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
10800                .unwrap(),
10801        )];
10802        let pre_digest =
10803            crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
10804        let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
10805
10806        let reference = make_reference(
10807            "#target",
10808            transforms,
10809            DigestAlgorithm::Sha256,
10810            expected_digest,
10811        );
10812        let result = process_reference(
10813            &reference,
10814            &resolver,
10815            sig_node,
10816            ReferenceSet::SignedInfo,
10817            0,
10818            false,
10819        )
10820        .unwrap();
10821        assert!(matches!(result.status, DsigStatus::Valid));
10822    }
10823
10824    #[test]
10825    fn reference_with_nonexistent_id_fails() {
10826        let xml = "<root><child/></root>";
10827        let doc = Document::parse(xml).unwrap();
10828        let resolver = UriReferenceResolver::new(&doc);
10829
10830        let reference =
10831            make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
10832        let result = process_reference(
10833            &reference,
10834            &resolver,
10835            doc.root_element(),
10836            ReferenceSet::SignedInfo,
10837            0,
10838            false,
10839        );
10840        assert!(result.is_err());
10841    }
10842
10843    #[test]
10844    fn reference_with_absent_uri_fails_closed() {
10845        let xml = "<root><child>text</child></root>";
10846        let doc = Document::parse(xml).unwrap();
10847        let resolver = UriReferenceResolver::new(&doc);
10848
10849        let reference = Reference {
10850            uri: None, // absent URI
10851            id: None,
10852            ref_type: None,
10853            transforms: vec![],
10854            digest_method: DigestAlgorithm::Sha256,
10855            digest_value: vec![0; 32],
10856        };
10857
10858        let result = process_reference(
10859            &reference,
10860            &resolver,
10861            doc.root_element(),
10862            ReferenceSet::SignedInfo,
10863            0,
10864            false,
10865        );
10866        assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
10867    }
10868
10869    // ── process_all_references: fail-fast ────────────────────────────
10870
10871    #[test]
10872    fn all_references_pass() {
10873        let xml = "<root><child>text</child></root>";
10874        let doc = Document::parse(xml).unwrap();
10875        let resolver = UriReferenceResolver::new(&doc);
10876
10877        // Compute correct digest
10878        let initial_data = resolver.dereference("").unwrap();
10879        let pre_digest =
10880            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
10881        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
10882
10883        let refs = vec![
10884            make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
10885            make_reference("", vec![], DigestAlgorithm::Sha256, digest),
10886        ];
10887
10888        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
10889        assert!(result.all_valid());
10890        assert_eq!(result.results.len(), 2);
10891        assert!(result.first_failure.is_none());
10892    }
10893
10894    #[test]
10895    fn reference_processing_shares_xpath_work_across_references() {
10896        // A signature-wide meter must not reset when processing the next
10897        // Reference, even though each transform chain is independently valid.
10898        let document = Document::parse("<root/>").unwrap();
10899        let resolver = UriReferenceResolver::new(&document);
10900        let transform = Transform::XPath(super::super::transforms::XPathExpression::new("true()"));
10901        let initial_data = resolver.dereference("").unwrap();
10902        let pre_digest = crate::xmldsig::execute_transforms(
10903            document.root_element(),
10904            initial_data,
10905            std::slice::from_ref(&transform),
10906        )
10907        .unwrap();
10908        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
10909        let references = vec![
10910            make_reference(
10911                "",
10912                vec![transform.clone()],
10913                DigestAlgorithm::Sha256,
10914                digest.clone(),
10915            ),
10916            make_reference("", vec![transform], DigestAlgorithm::Sha256, digest),
10917        ];
10918        let budget = TransformExecutionBudget::with_xpath_limit(12);
10919        let canonicalized_data_budget = CanonicalizedDataBudget::default();
10920        let execution = ReferenceExecutionContext {
10921            store_pre_digest: false,
10922            transform_options: TransformOptions::default(),
10923            transform_budget: &budget,
10924            canonicalized_data_budget: &canonicalized_data_budget,
10925            provider: crate::provider::default_provider(),
10926            capture: &(),
10927        };
10928
10929        let error = process_all_references_with_options(
10930            &references,
10931            &resolver,
10932            document.root_element(),
10933            &execution,
10934        )
10935        .expect_err("the second Reference must consume the first Reference's XPath work");
10936
10937        assert!(matches!(
10938            error,
10939            ReferenceProcessingError::Transform(TransformError::Policy(
10940                crate::policy::PolicyViolation::ResourceLimit {
10941                    resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
10942                    ..
10943                }
10944            ))
10945        ));
10946    }
10947
10948    #[test]
10949    fn reference_processing_shares_node_set_materialization_across_references() {
10950        // Repeated references to the same small subtree must share one owned-
10951        // string budget. Otherwise a large inherited namespace can be cloned
10952        // once per Reference even when canonicalization emits little output.
10953        let document = Document::parse(
10954            r#"<root xmlns:n="urn:0123456789"><target Id="selected">payload</target></root>"#,
10955        )
10956        .unwrap();
10957        let resolver = UriReferenceResolver::new(&document);
10958        let initial_data = resolver.dereference("#selected").unwrap();
10959        let pre_digest =
10960            crate::xmldsig::execute_transforms(document.root_element(), initial_data, &[]).unwrap();
10961        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
10962        let references = vec![
10963            make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest.clone()),
10964            make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest),
10965        ];
10966        let budget = TransformExecutionBudget::with_node_set_materialization_limit(30);
10967        let canonicalized_data_budget = CanonicalizedDataBudget::default();
10968        let execution = ReferenceExecutionContext {
10969            store_pre_digest: false,
10970            transform_options: TransformOptions::default(),
10971            transform_budget: &budget,
10972            canonicalized_data_budget: &canonicalized_data_budget,
10973            provider: crate::provider::default_provider(),
10974            capture: &(),
10975        };
10976
10977        let error = process_all_references_with_options(
10978            &references,
10979            &resolver,
10980            document.root_element(),
10981            &execution,
10982        )
10983        .expect_err("the second Reference must consume the first Reference's materialization work");
10984
10985        assert!(matches!(
10986            error,
10987            ReferenceProcessingError::UriDereference(TransformError::Policy(
10988                crate::policy::PolicyViolation::ResourceLimit {
10989                    resource: crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
10990                    ..
10991                }
10992            ))
10993        ));
10994    }
10995
10996    #[test]
10997    fn fail_fast_on_first_mismatch() {
10998        let xml = "<root><child>text</child></root>";
10999        let doc = Document::parse(xml).unwrap();
11000        let resolver = UriReferenceResolver::new(&doc);
11001
11002        let wrong_digest = vec![0u8; 32];
11003        let refs = vec![
11004            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
11005            // Second reference should NOT be processed
11006            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
11007        ];
11008
11009        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
11010        assert!(!result.all_valid());
11011        assert_eq!(result.first_failure, Some(0));
11012        // Only first reference should be in results (fail-fast)
11013        assert_eq!(result.results.len(), 1);
11014        assert!(matches!(
11015            result.results[0].status,
11016            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
11017        ));
11018    }
11019
11020    #[test]
11021    fn fail_fast_second_reference() {
11022        let xml = "<root><child>text</child></root>";
11023        let doc = Document::parse(xml).unwrap();
11024        let resolver = UriReferenceResolver::new(&doc);
11025
11026        // Compute correct digest for first ref
11027        let initial_data = resolver.dereference("").unwrap();
11028        let pre_digest =
11029            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
11030        let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
11031        let wrong_digest = vec![0u8; 32];
11032
11033        let refs = vec![
11034            make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
11035            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
11036        ];
11037
11038        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
11039        assert!(!result.all_valid());
11040        assert_eq!(result.first_failure, Some(1));
11041        // Both references should be in results
11042        assert_eq!(result.results.len(), 2);
11043        assert!(matches!(result.results[0].status, DsigStatus::Valid));
11044        assert!(matches!(
11045            result.results[1].status,
11046            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
11047        ));
11048    }
11049
11050    #[test]
11051    fn empty_references_list() {
11052        let xml = "<root/>";
11053        let doc = Document::parse(xml).unwrap();
11054        let resolver = UriReferenceResolver::new(&doc);
11055
11056        let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
11057        assert!(result.all_valid());
11058        assert!(result.results.is_empty());
11059    }
11060
11061    // ── Digest algorithms ────────────────────────────────────────────
11062
11063    #[test]
11064    fn reference_sha1_digest() {
11065        let xml = "<root>content</root>";
11066        let doc = Document::parse(xml).unwrap();
11067        let resolver = UriReferenceResolver::new(&doc);
11068
11069        let initial_data = resolver.dereference("").unwrap();
11070        let pre_digest =
11071            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
11072        let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
11073
11074        let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
11075        let result = process_reference(
11076            &reference,
11077            &resolver,
11078            doc.root_element(),
11079            ReferenceSet::SignedInfo,
11080            0,
11081            false,
11082        )
11083        .unwrap();
11084        assert!(matches!(result.status, DsigStatus::Valid));
11085        assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
11086    }
11087
11088    #[test]
11089    fn reference_sha512_digest() {
11090        let xml = "<root>content</root>";
11091        let doc = Document::parse(xml).unwrap();
11092        let resolver = UriReferenceResolver::new(&doc);
11093
11094        let initial_data = resolver.dereference("").unwrap();
11095        let pre_digest =
11096            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
11097        let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
11098
11099        let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
11100        let result = process_reference(
11101            &reference,
11102            &resolver,
11103            doc.root_element(),
11104            ReferenceSet::SignedInfo,
11105            0,
11106            false,
11107        )
11108        .unwrap();
11109        assert!(matches!(result.status, DsigStatus::Valid));
11110        assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
11111    }
11112
11113    // ── SAML-like end-to-end ─────────────────────────────────────────
11114
11115    #[test]
11116    fn saml_enveloped_reference_processing() {
11117        // Realistic SAML Response with enveloped signature
11118        let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
11119                                     xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
11120                                     ID="_resp1">
11121            <saml:Assertion ID="_assert1">
11122                <saml:Subject>user@example.com</saml:Subject>
11123            </saml:Assertion>
11124            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
11125                <ds:SignedInfo>
11126                    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
11127                    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
11128                    <ds:Reference URI="">
11129                        <ds:Transforms>
11130                            <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
11131                            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
11132                        </ds:Transforms>
11133                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
11134                        <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
11135                    </ds:Reference>
11136                </ds:SignedInfo>
11137                <ds:SignatureValue>fakesig==</ds:SignatureValue>
11138            </ds:Signature>
11139        </samlp:Response>"##;
11140        let doc = Document::parse(xml).unwrap();
11141        let resolver = UriReferenceResolver::new(&doc);
11142        let sig_node = doc
11143            .descendants()
11144            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
11145            .unwrap();
11146
11147        // Parse SignedInfo to get the Reference
11148        let signed_info_node = sig_node
11149            .children()
11150            .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
11151            .unwrap();
11152        let signed_info = parse_signed_info(signed_info_node).unwrap();
11153        let reference = &signed_info.references[0];
11154
11155        // Compute the correct digest by running the actual pipeline
11156        let initial_data = resolver.dereference("").unwrap();
11157        let pre_digest =
11158            crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
11159                .unwrap();
11160        let correct_digest = compute_digest(reference.digest_method, &pre_digest);
11161
11162        // Build a reference with the correct digest
11163        let corrected_ref = make_reference(
11164            "",
11165            reference.transforms.clone(),
11166            reference.digest_method,
11167            correct_digest,
11168        );
11169
11170        // Verify: should pass
11171        let result = process_reference(
11172            &corrected_ref,
11173            &resolver,
11174            sig_node,
11175            ReferenceSet::SignedInfo,
11176            0,
11177            true,
11178        )
11179        .unwrap();
11180        assert!(
11181            matches!(result.status, DsigStatus::Valid),
11182            "SAML reference should verify"
11183        );
11184        assert!(result.pre_digest_data.is_some());
11185
11186        // Verify the pre-digest data contains the canonicalized document without Signature
11187        let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
11188        assert!(
11189            pre_digest_str.contains("samlp:Response"),
11190            "pre-digest should contain Response"
11191        );
11192        assert!(
11193            !pre_digest_str.contains("SignatureValue"),
11194            "pre-digest should NOT contain Signature"
11195        );
11196    }
11197
11198    #[test]
11199    fn pipeline_missing_signed_info_returns_missing_element() {
11200        let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
11201
11202        let err = verify_signature_with_pem_key(xml, "dummy-key", false)
11203            .expect_err("missing SignedInfo must fail before crypto stage");
11204        assert!(matches!(
11205            err,
11206            SignatureVerificationPipelineError::MissingElement {
11207                element: "SignedInfo"
11208            }
11209        ));
11210    }
11211
11212    #[test]
11213    fn pipeline_multiple_signature_elements_are_rejected() {
11214        let xml = r#"
11215<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
11216  <ds:Signature>
11217    <ds:SignedInfo/>
11218  </ds:Signature>
11219  <ds:Signature/>
11220</root>
11221"#;
11222
11223        let err = verify_signature_with_pem_key(xml, "dummy-key", false)
11224            .expect_err("multiple signatures must fail closed");
11225        assert!(matches!(
11226            err,
11227            SignatureVerificationPipelineError::InvalidStructure {
11228                reason: "Signature must appear exactly once in document",
11229            }
11230        ));
11231    }
11232
11233    #[test]
11234    fn pipeline_start_node_limits_signature_cardinality_to_its_subtree() {
11235        // A start-node selector changes the operation root, not global ID or
11236        // reference resolution; another Signature outside the subtree is irrelevant.
11237        let xml = r#"
11238<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
11239  <scope Id="selected"><ds:Signature/></scope>
11240  <scope Id="other"><ds:Signature/></scope>
11241</root>
11242"#;
11243        let err = VerifyContext::new()
11244            .start_node_id("selected")
11245            .verify(xml)
11246            .expect_err("the selected Signature remains structurally incomplete");
11247        assert!(matches!(
11248            err,
11249            SignatureVerificationPipelineError::MissingElement {
11250                element: "SignedInfo"
11251            }
11252        ));
11253    }
11254
11255    #[test]
11256    fn pipeline_reports_keyinfo_parse_error() {
11257        let xml = r#"
11258<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
11259              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
11260  <ds:SignedInfo>
11261    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
11262    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
11263    <ds:Reference URI="">
11264      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
11265      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
11266    </ds:Reference>
11267  </ds:SignedInfo>
11268  <ds:SignatureValue>AA==</ds:SignatureValue>
11269  <ds:KeyInfo>
11270    <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
11271  </ds:KeyInfo>
11272</ds:Signature>
11273"#;
11274
11275        let err = VerifyContext::new().verify(xml).expect_err(
11276            "invalid KeyInfo must map to ParseKeyInfo when no explicit key is supplied",
11277        );
11278        assert!(matches!(
11279            err,
11280            SignatureVerificationPipelineError::ParseKeyInfo(_)
11281        ));
11282    }
11283
11284    #[test]
11285    fn pipeline_ignores_malformed_keyinfo_when_explicit_key_is_supplied() {
11286        let base_xml = signature_with_target_reference("AQ==");
11287        let xml = base_xml
11288            .replace(
11289                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
11290                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
11291            )
11292            .replace(
11293                "</ds:SignatureValue>\n  </ds:Signature>",
11294                "</ds:SignatureValue>\n    <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n  </ds:Signature>",
11295            );
11296
11297        let result = VerifyContext::new()
11298            .key(&RejectingKey)
11299            .verify(&xml)
11300            .expect("explicit key path should not fail on malformed KeyInfo");
11301        assert!(matches!(
11302            result.status,
11303            DsigStatus::Invalid(FailureReason::SignatureMismatch)
11304        ));
11305    }
11306
11307    #[test]
11308    fn pipeline_rejects_foreign_element_children_under_signature() {
11309        let base_xml = signature_with_target_reference("AQ==");
11310        let xml = base_xml
11311            .replace(
11312                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
11313                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:foo="urn:example:foo">"#,
11314            )
11315            .replace(
11316                "</ds:SignedInfo>\n    <ds:SignatureValue>",
11317                "</ds:SignedInfo>\n    <foo:Bar/>\n    <ds:SignatureValue>",
11318            );
11319
11320        let err = VerifyContext::new()
11321            .key(&RejectingKey)
11322            .verify(&xml)
11323            .expect_err("foreign element children under Signature must fail closed");
11324        assert!(matches!(
11325            err,
11326            SignatureVerificationPipelineError::InvalidStructure {
11327                reason: "Signature must contain only XMLDSIG element children",
11328            }
11329        ));
11330    }
11331
11332    #[test]
11333    fn pipeline_rejects_non_whitespace_mixed_content_under_signature() {
11334        let base_xml = signature_with_target_reference("AQ==");
11335        let xml = base_xml.replace(
11336            "</ds:SignedInfo>\n    <ds:SignatureValue>",
11337            "</ds:SignedInfo>\n    oops\n    <ds:SignatureValue>",
11338        );
11339
11340        let err = VerifyContext::new()
11341            .key(&RejectingKey)
11342            .verify(&xml)
11343            .expect_err("non-whitespace mixed content under Signature must fail closed");
11344        assert!(matches!(
11345            err,
11346            SignatureVerificationPipelineError::InvalidStructure {
11347                reason: "Signature must not contain non-whitespace mixed content",
11348            }
11349        ));
11350    }
11351
11352    #[test]
11353    fn pipeline_rejects_keyinfo_out_of_order() {
11354        let base_xml = signature_with_target_reference("AQ==");
11355        let xml = base_xml.replace(
11356            "</ds:SignatureValue>\n  </ds:Signature>",
11357            "</ds:SignatureValue>\n    <ds:Object/>\n    <ds:KeyInfo><ds:KeyName>late</ds:KeyName></ds:KeyInfo>\n  </ds:Signature>",
11358        );
11359
11360        let err = VerifyContext::new()
11361            .key(&RejectingKey)
11362            .verify(&xml)
11363            .expect_err("KeyInfo after Object must be rejected by Signature child order checks");
11364        assert!(matches!(
11365            err,
11366            SignatureVerificationPipelineError::InvalidStructure {
11367                reason: "KeyInfo must be the third element child of Signature when present"
11368            }
11369        ));
11370    }
11371
11372    #[test]
11373    fn pipeline_accepts_comments_and_processing_instructions_under_signature() {
11374        let xml = r#"
11375<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
11376  <?dbg keep ?>
11377  <!-- signature metadata -->
11378  <ds:SignedInfo>
11379    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
11380    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
11381    <ds:Reference URI="">
11382      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
11383      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
11384    </ds:Reference>
11385  </ds:SignedInfo>
11386  <!-- between required children -->
11387  <ds:SignatureValue>AA==</ds:SignatureValue>
11388</ds:Signature>
11389"#;
11390
11391        let doc = Document::parse(xml).expect("test XML must parse");
11392        let signature_node = doc.root_element();
11393        let parsed = parse_signature_children(signature_node)
11394            .expect("comment/PI nodes under Signature must be ignored");
11395
11396        assert_eq!(parsed.signed_info_node.tag_name().name(), "SignedInfo");
11397        assert_eq!(
11398            parsed.signature_value_node.tag_name().name(),
11399            "SignatureValue"
11400        );
11401        assert!(parsed.key_info_node.is_none());
11402    }
11403}