Skip to main content

xml_sec/xmldsig/
verify.rs

1//! XMLDSig reference processing and end-to-end signature verification pipeline.
2//!
3//! Implements [XMLDSig §4.3.3](https://www.w3.org/TR/xmldsig-core1/#sec-CoreValidation):
4//! for each `<Reference>` in `<SignedInfo>`, dereference the URI, apply transforms,
5//! compute the digest, and compare with the stored `<DigestValue>`.
6//!
7//! This module wires together:
8//! - [`UriReferenceResolver`] for URI dereference
9//! - [`super::transforms::execute_transforms`] for the transform pipeline
10//! - [`compute_digest`] + [`constant_time_eq`] for digest computation and comparison
11//! - [`verify_signature_with_pem_key`] for full pipeline validation (`SignedInfo` + `SignatureValue`)
12
13use crate::xml::dom::{Node, NodeId};
14use base64::Engine;
15use std::cell::{Cell, RefCell};
16use std::collections::{HashMap, HashSet};
17
18use crate::c14n::canonicalize_bounded_with_xml_base_budget;
19use crate::document::{
20    DocumentParseSettings, DocumentView, NodeIdentity, XmlDocument, XmlDocumentError,
21};
22use crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING;
23use crate::operation::{
24    OperationDecisionReason, OperationExecutionContext, OperationNodeId, OperationNodeKind,
25    OperationPlanError, OperationResourceIdentity, OperationStage,
26};
27
28#[cfg(test)]
29use super::digest::compute_digest;
30use super::digest::{DigestAlgorithm, constant_time_eq};
31#[cfg(test)]
32use super::parse::MAX_REFERENCES_PER_SIGNATURE;
33#[cfg(test)]
34use super::parse::parse_key_info;
35use super::parse::{
36    KeyInfo, MAX_X509_DATA_TOTAL_BINARY_LEN, MAX_X509_DECODED_BINARY_LEN, ParseError, Reference,
37    RetrievalMethodTransforms, SignatureAlgorithm, XMLDSIG_NS,
38};
39use super::parse::{
40    parse_reference_with_xpath_budget, parse_signed_info_with_xpath_budget,
41    parse_x509_data_dispatch_with_budget_and_provider, reference_digest_method,
42};
43use super::signature::{
44    SignatureVerificationError, verify_dsa_signature_spki, verify_ecdsa_signature_pem,
45    verify_rsa_signature_pem,
46};
47#[cfg(test)]
48use super::transforms::BASE64_TRANSFORM_URI;
49use super::transforms::{
50    DEFAULT_IMPLICIT_C14N_URI, Transform, TransformExecutionBudget, TransformOptions,
51    XPATH_TRANSFORM_URI, XPathHereSemantics, XPathSignatureParseBudget,
52    map_c14n_resource_policy_violation, transform_chain_produces_binary,
53};
54use super::types::{NodeSet, TransformError, subtree_node_id_range};
55use super::uri::{ExternalResourceMapError, UriReferenceResolver, validate_external_resource_map};
56use super::whitespace::{is_xml_whitespace_only, normalize_xml_base64_bytes};
57
58mod session;
59pub use session::*;
60mod capture;
61pub use capture::SelectedSignatureMethod;
62use capture::{CapturedOperation, ScopedCapture, VerificationCapture};
63
64const MAX_SIGNATURE_VALUE_LEN: usize = 8192;
65const MAX_RETRIEVAL_METHOD_COUNT: usize = 64;
66/// Cryptographic verifier used by [`VerifyContext`].
67///
68/// This trait intentionally has no `Send + Sync` supertraits so lightweight
69/// single-threaded verifiers can be used without additional bounds.
70pub trait VerifyingKey {
71    /// Borrow public SPKI describing this exact key after successful verification.
72    /// This is evidence only, never authorization for native verification or
73    /// private-key export. Composite handles expose their leaves instead. This
74    /// hook only borrows cached public metadata; it must not allocate, parse,
75    /// perform I/O or manufacture a different key after the decision.
76    fn association_spki(&self, _algorithm: SignatureAlgorithm) -> Option<&[u8]> {
77        None
78    }
79    /// Exact external execution domain; a key cannot be moved between token sessions.
80    fn provider_binding(&self) -> Option<&crate::provider::ProviderBinding> {
81        None
82    }
83    /// Composite resolvers may try individual keys through the selected engine.
84    /// `None` identifies a leaf handle. Wrappers must preserve per-key policy
85    /// when forwarding the visitor; no public/private key copy is required.
86    fn verify_candidate_keys(
87        &self,
88        _verify: &mut dyn FnMut(&dyn VerifyingKey) -> Result<bool, DsigError>,
89    ) -> Result<Option<bool>, DsigError> {
90        Ok(None)
91    }
92    /// Borrow public SPKI for native verification, validating method/key binding.
93    /// Opaque external keys may return `None`; native providers then fail closed.
94    fn verification_spki(
95        &self,
96        _algorithm: SignatureAlgorithm,
97    ) -> Result<Option<&[u8]>, DsigError> {
98        Ok(None)
99    }
100
101    /// Wire encoding already selected by the immutable verification policy.
102    fn ecdsa_encoding(&self) -> crate::policy::EcdsaSignatureValueEncoding {
103        crate::policy::EcdsaSignatureValueEncoding::XmlDsig
104    }
105    /// Verify with validated domain separation, rejecting unsupported contexts.
106    fn verify_with_context(
107        &self,
108        algorithm: SignatureAlgorithm,
109        context: &super::SignatureContext,
110        data: &[u8],
111        signature: &[u8],
112    ) -> Result<bool, DsigError> {
113        if !context.as_bytes().is_empty() {
114            return Err(super::SignatureVerificationError::UnsupportedAlgorithm {
115                uri: algorithm.uri().to_owned(),
116            }
117            .into());
118        }
119        self.verify(algorithm, data, signature)
120    }
121    /// Validate this key against the operation's immutable trust policy.
122    ///
123    /// Built-in keys override this hook so pre-resolved and resolver-produced
124    /// keys enforce identical strength constraints. Custom opaque keys may
125    /// retain their own policy enforcement by accepting the default no-op.
126    fn validate_policy(
127        &self,
128        _policy: &crate::policy::VerificationPolicy,
129    ) -> Result<(), DsigError> {
130        Ok(())
131    }
132
133    /// Check that `signature_value` has the wire framing required by the
134    /// declared algorithm and this key before provider dispatch.
135    ///
136    /// Key implementations with key-size-dependent framing should override
137    /// this method. The default enforces the algorithm-wide XMLDSig envelope.
138    fn validate_signature_value(
139        &self,
140        algorithm: SignatureAlgorithm,
141        signature_value: &[u8],
142    ) -> Result<bool, DsigError> {
143        Ok(super::signature::signature_value_matches_algorithm(
144            algorithm,
145            signature_value,
146        ))
147    }
148
149    /// Validate wire framing under the operation's immutable compatibility policy.
150    ///
151    /// The default delegates to [`VerifyingKey::validate_signature_value`] and
152    /// therefore implements only the standard XMLDSig framing contract. Custom
153    /// keys that support policy-selected wire formats, such as
154    /// [`crate::policy::EcdsaSignatureValueEncoding::XmlSecAsn1Der`], must
155    /// override this hook rather than accepting both formats implicitly.
156    fn validate_signature_value_with_policy(
157        &self,
158        policy: &crate::policy::VerificationPolicy,
159        algorithm: SignatureAlgorithm,
160        signature_value: &[u8],
161    ) -> Result<bool, DsigError> {
162        let _ = policy;
163        self.validate_signature_value(algorithm, signature_value)
164    }
165
166    /// Verify `signature_value` over `signed_data` with the declared algorithm.
167    fn verify(
168        &self,
169        algorithm: SignatureAlgorithm,
170        signed_data: &[u8],
171        signature_value: &[u8],
172    ) -> Result<bool, DsigError>;
173
174    /// Verify after applying operation-scoped compatibility semantics.
175    ///
176    /// The default delegates to [`VerifyingKey::verify`]. Custom keys whose
177    /// provider input depends on compatibility policy must override this hook
178    /// consistently with [`VerifyingKey::validate_signature_value_with_policy`].
179    fn verify_with_policy(
180        &self,
181        policy: &crate::policy::VerificationPolicy,
182        algorithm: SignatureAlgorithm,
183        signed_data: &[u8],
184        signature_value: &[u8],
185    ) -> Result<bool, DsigError> {
186        let _ = policy;
187        self.verify(algorithm, signed_data, signature_value)
188    }
189}
190
191/// Key resolver hook used by [`VerifyContext`] when no pre-set key is provided.
192///
193/// This trait intentionally has no `Send + Sync` supertraits; callers that need
194/// cross-thread sharing can wrap resolvers/keys in their own thread-safe types.
195pub trait KeyResolver {
196    /// Resolve using the core operation's cumulative candidate and certificate account.
197    /// The default charges one custom-resolver dispatch. Implementations searching
198    /// multiple candidates override this method, charging each inspection through
199    /// `budget` and using its execution account for certificate mechanism work.
200    fn resolve_for_verification_with_budget<'a>(
201        &'a self,
202        key_info: Option<&KeyInfo>,
203        algorithm: SignatureAlgorithm,
204        policy: &crate::policy::VerificationPolicy,
205        provider: &dyn crate::provider::CryptoProvider,
206        budget: &mut super::InspectedKeyCandidateBudget,
207    ) -> Result<Option<super::ResolvedVerificationKey<'a>>, DsigError> {
208        budget.bind_policy(policy)?;
209        budget.charge()?;
210        self.resolve_for_verification(key_info, algorithm, policy, provider)
211    }
212
213    /// Discover and explicitly authorize a key for an end-to-end operation.
214    /// The default preserves discovery only: applications that authorize keys
215    /// themselves return `ResolvedVerificationKey::CallerTrusted` explicitly.
216    fn resolve_for_verification<'a>(
217        &'a self,
218        key_info: Option<&KeyInfo>,
219        algorithm: SignatureAlgorithm,
220        policy: &crate::policy::VerificationPolicy,
221        provider: &dyn crate::provider::CryptoProvider,
222    ) -> Result<Option<super::ResolvedVerificationKey<'a>>, DsigError> {
223        Ok(self
224            .resolve_with_policy_and_provider(key_info, algorithm, policy, provider)?
225            .map(super::ResolvedVerificationKey::Candidate))
226    }
227    /// Resolve a verification key from parsed `<KeyInfo>` sources.
228    ///
229    /// Return `Ok(None)` when no suitable key could be resolved from available
230    /// key material (for example, missing `<KeyInfo>` candidates). `VerifyContext`
231    /// maps `Ok(None)` to `DsigStatus::Invalid(FailureReason::KeyNotFound)`;
232    /// reserve `Err(...)` for resolver failures.
233    fn resolve<'a>(
234        &'a self,
235        key_info: Option<&KeyInfo>,
236        algorithm: SignatureAlgorithm,
237    ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError>;
238
239    /// Resolve under the operation's immutable policy snapshot.
240    ///
241    /// Implementations that make trust or key-source decisions must override
242    /// this method. Resolver implementations that inspect multiple candidates
243    /// must enforce [`crate::policy::ResourcePolicy::max_key_candidates`] across
244    /// that internal search. The verification pipeline separately requires
245    /// capacity for the single candidate returned by any resolver. The default
246    /// preserves source-only custom resolvers whose behavior is independent of
247    /// other policy fields.
248    fn resolve_with_policy<'a>(
249        &'a self,
250        key_info: Option<&KeyInfo>,
251        algorithm: SignatureAlgorithm,
252        _policy: &crate::policy::VerificationPolicy,
253    ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
254        self.resolve(key_info, algorithm)
255    }
256
257    /// Resolve under both the operation policy and cryptographic provider.
258    ///
259    /// Resolvers that evaluate cryptographic key metadata, such as
260    /// `X509Digest`, must override this hook. The default keeps existing
261    /// policy-aware custom resolvers source-compatible.
262    fn resolve_with_policy_and_provider<'a>(
263        &'a self,
264        key_info: Option<&KeyInfo>,
265        algorithm: SignatureAlgorithm,
266        policy: &crate::policy::VerificationPolicy,
267        _provider: &dyn crate::provider::CryptoProvider,
268    ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
269        self.resolve_with_policy(key_info, algorithm, policy)
270    }
271
272    /// Return `true` when this resolver consumes document `<KeyInfo>` material.
273    ///
274    /// The verification pipeline uses this to decide whether malformed
275    /// `<KeyInfo>` should raise `DsigError::ParseKeyInfo` before resolver
276    /// execution. Resolvers that ignore document key material can keep the
277    /// default `false` to avoid fail-closed parsing on advisory `<KeyInfo>`.
278    fn consumes_document_key_info(&self) -> bool {
279        false
280    }
281}
282
283/// Allowed structural URI classes for XMLDSig reference and key-source policy.
284///
285/// External URIs resolve only from bytes supplied through
286/// [`VerifyContext::external_resources`]; allowing them never enables I/O.
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288#[must_use = "pass the policy to VerifyContext::allowed_uri_types(), or store it for reuse"]
289pub struct UriTypeSet {
290    allow_empty: bool,
291    allow_same_document: bool,
292    allow_external: bool,
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
296enum UriClass {
297    Empty,
298    SameDocument,
299    External,
300}
301
302fn classify_uri(uri: &str) -> UriClass {
303    if uri.is_empty() {
304        UriClass::Empty
305    } else if uri.starts_with('#') {
306        UriClass::SameDocument
307    } else {
308        UriClass::External
309    }
310}
311
312impl UriTypeSet {
313    /// Create a custom URI policy.
314    pub const fn new(allow_empty: bool, allow_same_document: bool, allow_external: bool) -> Self {
315        Self {
316            allow_empty,
317            allow_same_document,
318            allow_external,
319        }
320    }
321
322    /// Allow only same-document references (`""`, `#id`, `#xpointer(...)`).
323    pub const SAME_DOCUMENT: Self = Self {
324        allow_empty: true,
325        allow_same_document: true,
326        allow_external: false,
327    };
328
329    /// Allow all URI classes.
330    ///
331    /// External URIs still require an explicit caller-owned resource map.
332    pub const ALL: Self = Self {
333        allow_empty: true,
334        allow_same_document: true,
335        allow_external: true,
336    };
337
338    /// Return whether this set permits the URI's structural class.
339    pub fn allows(self, uri: &str) -> bool {
340        match classify_uri(uri) {
341            UriClass::Empty => self.allow_empty,
342            UriClass::SameDocument => self.allow_same_document,
343            UriClass::External => self.allow_external,
344        }
345    }
346}
347
348impl Default for UriTypeSet {
349    fn default() -> Self {
350        Self::SAME_DOCUMENT
351    }
352}
353
354/// Request-scoped selection of the XMLDSig operation node.
355#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
356pub enum SignatureSelection<'a> {
357    /// Require exactly one `Signature` in the complete document.
358    #[default]
359    UniqueDocumentSignature,
360    /// Select the first descendant `Signature` from the document root.
361    FirstDocumentSignature,
362    /// Select the first descendant `Signature` below the element with this ID.
363    FirstSignatureUnderId(&'a str),
364}
365
366/// Verification builder/configuration.
367#[must_use = "configure the context and call verify(), or store it for reuse"]
368#[derive(Clone)]
369pub struct VerifyContext<'a> {
370    utc_facts: Option<structured_pkix::temporal::LeapTable<'a>>,
371    decapsulation_key: Option<&'a dyn crate::provider::KeyDecapsulationKey>,
372    key: Option<&'a dyn VerifyingKey>,
373    key_resolver: Option<&'a dyn KeyResolver>,
374    policy: crate::policy::VerificationPolicy,
375    provider: &'a dyn crate::provider::CryptoProvider,
376    xml_backend: crate::XmlBackend,
377    store_pre_digest: bool,
378    external_resources: Option<&'a HashMap<String, Vec<u8>>>,
379    signature_selection: SignatureSelection<'a>,
380    id_attributes: &'a [crate::IdAttributeRegistration],
381}
382
383impl<'a> VerifyContext<'a> {
384    /// Create a context with conservative defaults.
385    ///
386    /// Defaults:
387    /// - no pre-set key, no key resolver
388    /// - manifests disabled
389    /// - same-document URIs only
390    /// - all transforms allowed
391    /// - pre-digest buffers not stored
392    pub fn new() -> Self {
393        Self {
394            utc_facts: None,
395            decapsulation_key: None,
396            key: None,
397            key_resolver: None,
398            policy: crate::policy::VerificationPolicy::default(),
399            provider: crate::provider::default_provider(),
400            xml_backend: crate::XmlBackend::default(),
401            store_pre_digest: false,
402            external_resources: None,
403            signature_selection: SignatureSelection::UniqueDocumentSignature,
404            id_attributes: &[],
405        }
406    }
407
408    /// Supply application-owned UTC history for certificate evidence. The
409    /// context borrows the table; it neither copies it nor discovers updates.
410    pub fn utc_facts(mut self, facts: structured_pkix::temporal::LeapTable<'a>) -> Self {
411        self.utc_facts = Some(facts);
412        self
413    }
414
415    /// Set a pre-resolved verification key.
416    ///
417    /// Built-in [`super::VerificationKey`] values are validated against the
418    /// same operation key-strength policy as resolver-produced keys. Custom
419    /// opaque [`VerifyingKey`] implementations retain responsibility for any
420    /// key metadata that the core cannot inspect.
421    pub fn key(mut self, key: &'a dyn VerifyingKey) -> Self {
422        self.key = Some(key);
423        self
424    }
425
426    /// Recover the HMAC secret using an explicit recipient private key.
427    /// Policy must grant the exact experimental KEM. A valid KEM/HMAC result
428    /// is not evidence of sender identity.
429    pub fn decapsulation_key(mut self, key: &'a dyn crate::provider::KeyDecapsulationKey) -> Self {
430        self.decapsulation_key = Some(key);
431        self
432    }
433
434    /// Set a key resolver fallback used when `key()` is not provided.
435    pub fn key_resolver(mut self, resolver: &'a dyn KeyResolver) -> Self {
436        self.key_resolver = Some(resolver);
437        self
438    }
439
440    /// Replace the complete immutable verification policy snapshot.
441    pub fn policy(mut self, policy: crate::policy::VerificationPolicy) -> Self {
442        self.policy = policy;
443        self
444    }
445
446    /// Select the cryptographic provider for this verification operation.
447    pub fn provider(mut self, provider: &'a dyn crate::provider::CryptoProvider) -> Self {
448        self.provider = provider;
449        self
450    }
451
452    /// Select the compiled XML parser backend for this verification operation.
453    pub fn xml_backend(mut self, backend: crate::XmlBackend) -> Self {
454        self.xml_backend = backend;
455        self
456    }
457
458    /// Enable or disable `<Manifest>` processing.
459    ///
460    /// Process references only when this Signature authenticated the original
461    /// Manifest or an ancestor through an ID-based same-document reference
462    /// using only canonicalization transforms (or implicit canonicalization).
463    /// Such Manifests may be nested or outside Signature; physical ancestry
464    /// alone never authenticates them. Authenticated Manifest references can
465    /// establish the next discovery frontier through the same operation state.
466    /// Filtering or binary transforms do not prove complete original structure.
467    /// Unsigned/unreferenced Manifests are skipped.
468    /// Whole-document same-document references such as `URI=""` or
469    /// `URI="#xpointer(/)"` do not establish an element-specific Manifest
470    /// authentication proof for this option.
471    ///
472    /// Manifests are parsed and processed only after the SignedInfo references
473    /// and SignatureValue both validate. Their digest mismatches, policy
474    /// violations, and processing failures are then reported independently in
475    /// `VerifyResult::manifest_references` and do not alter `VerifyResult::status`.
476    /// Callers that enable `process_manifests(true)` must inspect
477    /// `VerifyResult::manifest_references` in addition to `VerifyResult::status`
478    /// when interpreting `verify()` results.
479    /// Structural/parse errors in Manifest content abort `verify()` and are
480    /// returned as `Err(...)`.
481    pub fn process_manifests(mut self, enabled: bool) -> Self {
482        self.policy.manifest_processing = if enabled {
483            crate::policy::ManifestProcessing::Process
484        } else {
485            crate::policy::ManifestProcessing::Ignore
486        };
487        self
488    }
489
490    /// Restrict allowed reference URI classes.
491    pub fn allowed_uri_types(mut self, types: UriTypeSet) -> Self {
492        self.policy.uris.references = types;
493        self
494    }
495
496    /// Restrict URI classes used to retrieve key material from `<KeyInfo>`.
497    ///
498    /// This policy is independent from [`Self::allowed_uri_types`]: allowing an
499    /// external signed payload does not implicitly allow external key retrieval.
500    /// Same-document retrieval is enabled by default; external retrieval requires
501    /// an explicit opt-in and still uses only caller-supplied resources.
502    pub fn allowed_retrieval_method_uri_types(mut self, types: UriTypeSet) -> Self {
503        self.policy.uris.retrieval_methods = types;
504        self
505    }
506
507    /// Provide external URI payloads explicitly.
508    ///
509    /// The map is the complete external I/O boundary: verification never
510    /// performs network or filesystem access. External URIs must also be
511    /// enabled through [`UriTypeSet`]. Map keys are RFC 3986 resolved URI
512    /// identities: use normalized paths with dot segments removed and retain
513    /// query or fragment suffixes.
514    pub fn external_resources(mut self, resources: &'a HashMap<String, Vec<u8>>) -> Self {
515        self.external_resources = Some(resources);
516        self
517    }
518
519    /// Select the operation start node by its XML ID value.
520    ///
521    /// Verification selects the first descendant `<Signature>` in document
522    /// order. This is request context, not a policy decision, and mirrors
523    /// libxmlsec1's depth-first `xmlSecFindNode` start-node contract.
524    pub fn start_node_id(mut self, id: &'a str) -> Self {
525        self.signature_selection = SignatureSelection::FirstSignatureUnderId(id);
526        self
527    }
528
529    /// Select the first descendant `<Signature>` from the document root.
530    ///
531    /// This is the libxmlsec1 command-line operation-root contract. The library
532    /// default remains fail-closed and requires a unique document signature.
533    pub fn first_document_signature(mut self) -> Self {
534        self.signature_selection = SignatureSelection::FirstDocumentSignature;
535        self
536    }
537
538    /// Add caller-declared ID attributes for start-node and Reference lookup.
539    pub fn id_attributes(mut self, registrations: &'a [crate::IdAttributeRegistration]) -> Self {
540        self.id_attributes = registrations;
541        self
542    }
543
544    /// Allow bounded internal DTD declarations while keeping external entity
545    /// resolution disabled. This is off by default.
546    pub fn allow_internal_dtd(mut self, enabled: bool) -> Self {
547        self.policy.xml.allow_internal_dtd = enabled;
548        self
549    }
550
551    /// Restrict allowed transform and canonicalization algorithms by URI.
552    ///
553    /// Example values:
554    /// - `http://www.w3.org/2000/09/xmldsig#enveloped-signature`
555    /// - `http://www.w3.org/2001/10/xml-exc-c14n#`
556    ///
557    /// The allowlist covers explicit Reference and RetrievalMethod transforms,
558    /// the declared SignedInfo canonicalization method, and implicit default
559    /// C14N (`http://www.w3.org/TR/2001/REC-xml-c14n-20010315`) when a Reference
560    /// transform chain ends as a node set.
561    pub fn allowed_transforms<I, S>(mut self, transforms: I) -> Self
562    where
563        I: IntoIterator<Item = S>,
564        S: Into<String>,
565    {
566        self.policy.transforms.allowed_algorithms =
567            Some(transforms.into_iter().map(Into::into).collect());
568        self
569    }
570
571    /// Store pre-digest buffers for diagnostics.
572    ///
573    /// Retained reference buffers and canonicalized `<SignedInfo>` share a
574    /// non-configurable 32 MiB safety ceiling. Canonicalized `<SignedInfo>` is
575    /// charged even when diagnostic retention is disabled because signature
576    /// verification always materializes it. Overflow remains a typed policy
577    /// violation at both low-level and end-to-end entry points.
578    pub fn store_pre_digest(mut self, enabled: bool) -> Self {
579        self.store_pre_digest = enabled;
580        self
581    }
582
583    /// Select the node returned by XPath's `here()` extension function.
584    ///
585    /// The default follows XMLDSig and returns the `<XPath>` parameter.
586    /// Use [`XPathHereSemantics::XmlSecLegacy`] only for documents known to
587    /// have been generated with libxmlsec1's `<Transform>` interpretation.
588    pub fn xpath_here_semantics(mut self, semantics: XPathHereSemantics) -> Self {
589        self.policy.transforms.xpath_here_semantics = semantics;
590        self
591    }
592
593    fn allowed_transform_uris(&self) -> Option<&HashSet<String>> {
594        self.policy.transforms.allowed_algorithms.as_ref()
595    }
596
597    fn transform_options(&self) -> TransformOptions<'_> {
598        TransformOptions::default()
599            .id_attributes(self.id_attributes)
600            .allow_internal_dtd(self.policy.xml.allow_internal_dtd)
601            .xpath_here_semantics(self.policy.transforms.xpath_here_semantics)
602            .opc_relationship_edition(self.policy.transforms.opc_relationship_edition)
603    }
604
605    /// Verify one XMLDSig signature using this context.
606    ///
607    /// Returns `Ok(VerifyResult)` for both valid and invalid signatures; inspect
608    /// `VerifyResult::status` for the core `<SignedInfo>` and signature-value
609    /// outcome. When Manifest processing is enabled, inspect every
610    /// `VerifyResult::manifest_references` entry separately. `Err(...)` is
611    /// reserved for pipeline failures.
612    pub fn verify(&self, xml: &str) -> Result<VerifyResult, DsigError> {
613        verify_signature_with_context(xml, self)
614    }
615
616    /// Verify a signature against a retained owned document generation.
617    ///
618    /// The active XML input policy is revalidated against the document's parse
619    /// provenance as well as its current byte and node counts. In particular, a
620    /// strict context rejects a document that required internal DTD support.
621    pub fn verify_document(&self, document: &XmlDocument) -> Result<VerifyResult, DsigError> {
622        verify_signature_document_with_context(document, self)
623    }
624
625    /// Verify every Signature in one retained document generation.
626    ///
627    /// All signatures share the operation's work and retention budgets. A
628    /// malformed or invalid signature is retained as an individual outcome,
629    /// never silently skipped. Call [`VerifyEvidence::accepted`] with the
630    /// current document before accepting the request; a successful return
631    /// alone is not validation. [`VerifyEvidence::all_valid`] is historical
632    /// mathematical diagnostics only and does not revalidate document generation.
633    pub fn verify_all(&self, document: &XmlDocument) -> Result<VerifyEvidence, DsigError> {
634        verify_all_document(document, self, &VerificationRequest::default())
635    }
636
637    /// Discover bounded Signature identities without key or digest work.
638    ///
639    /// These are untrusted selection handles, not verification evidence. Use
640    /// them to authorize request keys even when Signatures have no XML ID.
641    pub fn signature_identities(
642        &self,
643        document: &XmlDocument,
644    ) -> Result<Vec<NodeIdentity>, DsigError> {
645        self.policy.validate()?;
646        document.validate_operation_policy(&self.policy.xml, &self.policy.resources)?;
647        document.with_view(|view| {
648            let count = count_signature_nodes(view, self.policy.resources.max_signatures)?;
649            let mut identities = Vec::with_capacity(count);
650            for node in view.document().descendants() {
651                if node.has_tag_name((XMLDSIG_NS, "Signature")) {
652                    identities.push(view.node_identity(node));
653                }
654            }
655            Ok(identities)
656        })
657    }
658
659    /// Verify all signatures against explicit caller-selected targets and keys.
660    ///
661    /// Requests are not policy: they cannot permit algorithms, URI classes or
662    /// XML features forbidden by the immutable policy snapshot.
663    pub fn verify_request(
664        &self,
665        document: &XmlDocument,
666        request: &VerificationRequest<'_>,
667    ) -> Result<VerifyEvidence, DsigError> {
668        verify_all_document(document, self, request)
669    }
670}
671
672/// Application requirement for the independently verified signatures.
673#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
674pub enum SignatureRequirement {
675    /// Every discovered signature must pass, and at least one must exist.
676    #[default]
677    All,
678    /// Explicitly tolerate other invalid signatures if this many pass.
679    AtLeast(usize),
680    /// Require exactly this many discovered signatures, all passing.
681    Exactly(usize),
682}
683
684/// Explicit key authorization for one stable, caller-selected Signature.
685pub struct CallerTrustedSignatureKey<'a> {
686    /// Signature that this key is authorized to verify.
687    pub signature: NodeIdentity,
688    /// Application-owned key; embedded KeyInfo cannot override it.
689    pub key: &'a dyn VerifyingKey,
690}
691
692/// Request data, immutably borrowed for one verification operation.
693#[derive(Default)]
694pub struct VerificationRequest<'a> {
695    /// Elements whose complete expanded content must be authenticated.
696    /// Comments and an enveloped Signature are not part of this assertion.
697    pub expected_targets: &'a [NodeIdentity],
698    /// Per-signature trusted keys. Duplicate/foreign/stale mappings fail closed.
699    pub trusted_keys: &'a [CallerTrustedSignatureKey<'a>],
700    /// Explicit external bytes; no implicit I/O or configuration discovery.
701    pub external_resources: Option<&'a HashMap<String, Vec<u8>>>,
702    /// Required multi-signature outcome; zero counts are invalid requests.
703    pub signatures: SignatureRequirement,
704    /// Opaque caller correlation value, never read from XML or used as trust.
705    pub correlation: Option<[u8; 16]>,
706}
707
708/// Outcome for one exact Signature, including pipeline failures.
709#[derive(Debug)]
710pub struct SignatureEvidence {
711    identity: NodeIdentity,
712    result: Result<VerifyResult, DsigError>,
713}
714
715impl SignatureEvidence {
716    /// Stable identity of the Signature actually processed.
717    #[must_use]
718    pub const fn identity(&self) -> NodeIdentity {
719        self.identity
720    }
721
722    /// Individual outcome. Digest matches do not imply signature validity.
723    pub const fn result(&self) -> &Result<VerifyResult, DsigError> {
724        &self.result
725    }
726}
727
728/// Bounded outcomes for all signatures in one document generation.
729#[derive(Debug)]
730#[must_use = "inspect the individual outcomes before accepting the document"]
731pub struct VerifyEvidence<V = Vec<SignatureEvidence>> {
732    document: NodeIdentity,
733    signatures: V,
734    accepted: bool,
735    correlation: Option<[u8; 16]>,
736}
737
738impl<V: std::ops::Deref<Target = [SignatureEvidence]>> VerifyEvidence<V> {
739    /// Whether the explicit request requirements hold for this document.
740    /// Foreign documents or any mutation after verification invalidate acceptance.
741    pub fn accepted(&self, document: &XmlDocument) -> Result<bool, DsigError> {
742        document.with_view(|view| {
743            view.resolve_node(self.document)?;
744            Ok(self.accepted)
745        })
746    }
747
748    /// Uninterpreted correlation value supplied by the caller.
749    #[must_use]
750    pub const fn correlation(&self) -> Option<[u8; 16]> {
751        self.correlation
752    }
753
754    /// Prove complete expanded element content under an authorized signature.
755    ///
756    /// This does not claim source-byte equality, comment coverage or protection
757    /// of the owning enveloped Signature. Arbitrary XPath/binary transforms
758    /// remain indeterminate rather than falsely covering their original input.
759    /// The exact document generation must still exist; wrapping an unsigned
760    /// sibling with a matching name cannot satisfy an identity-based assertion.
761    pub fn covers_element(
762        &self,
763        document: &XmlDocument,
764        target: NodeIdentity,
765    ) -> Result<bool, DsigError> {
766        document.with_view(|view| {
767            view.resolve_node(self.document)?;
768            let target_node = view.resolve_node(target)?;
769            if !target_node.is_element() {
770                return Err(DsigError::InvalidRequest {
771                    reason: "coverage target must be an element",
772                });
773            }
774            let target_id = target_node.id().get();
775            for signature in self.signatures() {
776                let Ok(result) = &signature.result else {
777                    continue;
778                };
779                if result.status != DsigStatus::Valid
780                    || !matches!(
781                        result.key_trust,
782                        super::KeyTrustEvidence::CallerTrusted
783                            | super::KeyTrustEvidence::ValidatedX509(_)
784                    )
785                {
786                    continue;
787                }
788                let signature_node = view.resolve_node(signature.identity)?;
789                for reference in result
790                    .signed_info_references
791                    .iter()
792                    .chain(&result.manifest_references)
793                {
794                    if reference.status != DsigStatus::Valid {
795                        continue;
796                    }
797                    let Some(root) = reference.target_identity else {
798                        continue;
799                    };
800                    if reference.coverage == ReferenceCoverage::TransformedData {
801                        continue;
802                    }
803                    let root = view.resolve_node(root)?;
804                    // The normalized arena assigns contiguous preorder IDs;
805                    // reuse the same subtree contract as node-set subtraction,
806                    // rather than walking depth for every report/target pair.
807                    if !subtree_node_id_range(root).contains(&target_id) {
808                        continue;
809                    }
810                    if reference.coverage == ReferenceCoverage::EnvelopedElementContent
811                        && subtree_node_id_range(signature_node).contains(&target_id)
812                    {
813                        continue;
814                    }
815                    return Ok(true);
816                }
817            }
818            Ok(false)
819        })
820    }
821
822    /// Outcomes in document order, including malformed signatures.
823    #[must_use]
824    pub fn signatures(&self) -> &[SignatureEvidence] {
825        &self.signatures
826    }
827
828    /// Whether a nonempty set of signatures and their processed Manifests passed.
829    ///
830    /// This is a historical mathematical outcome, not current-document
831    /// authorization. Use `accepted(document)` for generation-bound acceptance.
832    #[must_use]
833    pub fn all_valid(&self) -> bool {
834        !self.signatures.is_empty()
835            && self.signatures.iter().all(|entry| {
836                entry.result.as_ref().is_ok_and(|result| {
837                    result.status == DsigStatus::Valid
838                        && result
839                            .manifest_references
840                            .iter()
841                            .all(|reference| reference.status == DsigStatus::Valid)
842                })
843            })
844    }
845}
846
847impl Default for VerifyContext<'_> {
848    fn default() -> Self {
849        Self::new()
850    }
851}
852
853/// Per-reference verification result.
854#[derive(Debug, Clone)]
855#[non_exhaustive]
856#[must_use = "inspect status before accepting the reference result"]
857pub struct ReferenceResult {
858    /// Exact Reference node when processing a retained document.
859    pub reference_identity: Option<NodeIdentity>,
860    /// Resolved same-document target, not an untrusted URI claim.
861    pub target_identity: Option<NodeIdentity>,
862    /// SHA-256 identity of resolved external bytes before transforms. This is
863    /// provenance, not signer authorization or an original-element proof.
864    pub external_resource_fingerprint: Option<[u8; 32]>,
865    /// What can be proven about original element-content coverage.
866    pub coverage: ReferenceCoverage,
867    /// Whether this reference came from `<SignedInfo>` or `<Manifest>`.
868    pub reference_set: ReferenceSet,
869    /// Zero-based index within `reference_set`.
870    pub reference_index: usize,
871    /// URI from the `<Reference>` element (for diagnostics).
872    pub uri: String,
873    /// Digest algorithm used.
874    pub digest_algorithm: DigestAlgorithm,
875    /// Reference verification status.
876    pub status: DsigStatus,
877    /// Pre-digest bytes (populated when `store_pre_digest` is enabled).
878    pub pre_digest_data: Option<Vec<u8>>,
879}
880
881/// Conservative coverage classification for the original semantic document.
882///
883/// XMLDSig 1.1 §8.1.1 protects only transformed data. Unknown projection is
884/// deliberately not promoted to complete original-element coverage.
885/// <https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-Signed>
886#[derive(Debug, Clone, Copy, PartialEq, Eq)]
887#[non_exhaustive]
888pub enum ReferenceCoverage {
889    /// No complete original-element coverage proof (binary or arbitrary filter).
890    TransformedData,
891    /// Expanded element names, attributes and content, excluding comments.
892    ElementContent,
893    /// Element content except the owning Signature subtree and comments.
894    EnvelopedElementContent,
895}
896
897/// Origin of a processed `<Reference>`.
898#[derive(Debug, Clone, Copy, PartialEq, Eq)]
899#[non_exhaustive]
900pub enum ReferenceSet {
901    /// `<Reference>` under `<SignedInfo>`.
902    SignedInfo,
903    /// `<Reference>` under `<Object>/<Manifest>`.
904    Manifest,
905}
906
907/// Verification status.
908#[derive(Debug, Clone, Copy, PartialEq, Eq)]
909#[non_exhaustive]
910pub enum DsigStatus {
911    /// Signature/reference is cryptographically valid.
912    Valid,
913    /// Signature/reference is invalid with a concrete reason.
914    Invalid(FailureReason),
915}
916
917/// Why XMLDSig verification failed.
918#[derive(Debug, Clone, Copy, PartialEq, Eq)]
919#[non_exhaustive]
920pub enum FailureReason {
921    /// `<DigestValue>` mismatch for a `<Reference>` at `ref_index`.
922    ReferenceDigestMismatch {
923        /// Zero-based index of the failing `<Reference>` in its processed set.
924        ///
925        /// On per-reference verification entries, use
926        /// `ReferenceResult::reference_set` to distinguish the `<SignedInfo>`
927        /// and `<Manifest>` reference sets.
928        ///
929        /// When this reason appears in `VerifyResult::status` without an
930        /// accompanying `ReferenceResult`, `ref_index` always refers to the
931        /// `<SignedInfo>` reference set.
932        ref_index: usize,
933    },
934    /// `<Reference>` rejected by URI/transform allowlist policy.
935    ReferencePolicyViolation {
936        /// Zero-based index of the failing `<Reference>` in its processed set.
937        ref_index: usize,
938    },
939    /// `<Reference>` processing failed (dereference, transform, missing URI).
940    ReferenceProcessingFailure {
941        /// Zero-based index of the failing `<Reference>` in its processed set.
942        ref_index: usize,
943    },
944    /// `<SignatureValue>` does not match canonicalized `<SignedInfo>`.
945    SignatureMismatch,
946    /// No verification key was configured or could be resolved.
947    KeyNotFound,
948}
949
950/// Result of processing all `<Reference>` elements in `<SignedInfo>`.
951#[derive(Debug)]
952#[non_exhaustive]
953#[must_use = "check first_failure/results before accepting the reference set"]
954pub struct ReferencesResult {
955    /// Per-reference results (one per `<Reference>` in order).
956    /// On fail-fast, only references up to and including the failed one are present.
957    pub results: Vec<ReferenceResult>,
958    /// Index of the first failed reference, if any.
959    pub first_failure: Option<usize>,
960}
961
962impl ReferencesResult {
963    /// Whether all references passed digest verification.
964    #[must_use]
965    pub fn all_valid(&self) -> bool {
966        self.results
967            .iter()
968            .all(|result| matches!(result.status, DsigStatus::Valid))
969    }
970}
971
972/// Process a single `<Reference>`: dereference URI → apply transforms → compute
973/// digest → compare with stored `<DigestValue>`.
974///
975/// # Arguments
976///
977/// - `reference`: The parsed `<Reference>` element.
978/// - `resolver`: URI resolver for the document.
979/// - `signature_node`: The `<Signature>` element (for enveloped-signature transform).
980/// - `reference_set`: Whether this reference belongs to `<SignedInfo>` or `<Manifest>`.
981/// - `reference_index`: Zero-based index of this reference inside `reference_set`.
982/// - `store_pre_digest`: If true, store the pre-digest bytes in the result,
983///   subject to the signature-wide diagnostic retention ceiling.
984///
985/// # Errors
986///
987/// Returns `Err` for processing failures (URI dereference, transform errors).
988/// Digest mismatch is NOT an error — it produces
989/// `Ok(ReferenceResult { status: Invalid(ReferenceDigestMismatch { .. }) })`.
990pub fn process_reference(
991    reference: &Reference,
992    resolver: &UriReferenceResolver<'_>,
993    signature_node: Node<'_, '_>,
994    reference_set: ReferenceSet,
995    reference_index: usize,
996    store_pre_digest: bool,
997) -> Result<ReferenceResult, ReferenceProcessingError> {
998    let execution_budget = TransformExecutionBudget::default();
999    let canonicalized_data_budget = CanonicalizedDataBudget::default();
1000    let execution = ReferenceExecutionContext {
1001        store_pre_digest,
1002        transform_options: TransformOptions::default().id_attributes(resolver.id_registrations()),
1003        transform_budget: &execution_budget,
1004        canonicalized_data_budget: &canonicalized_data_budget,
1005        provider: crate::provider::default_provider(),
1006        capture: &(),
1007    };
1008    process_reference_with_options(
1009        reference,
1010        resolver,
1011        signature_node,
1012        reference_set,
1013        reference_index,
1014        reference_origin_node(signature_node, reference_set, reference_index),
1015        &execution,
1016    )
1017}
1018
1019fn reference_origin_node<'a, 'input>(
1020    signature_node: Node<'a, 'input>,
1021    reference_set: ReferenceSet,
1022    reference_index: usize,
1023) -> Option<Node<'a, 'input>> {
1024    let is_reference = |node: &Node<'_, '_>| {
1025        node.is_element()
1026            && node.tag_name().namespace() == Some(XMLDSIG_NS)
1027            && node.tag_name().name() == "Reference"
1028    };
1029    match reference_set {
1030        ReferenceSet::SignedInfo => signature_node
1031            .children()
1032            .find(|node| {
1033                node.is_element()
1034                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
1035                    && node.tag_name().name() == "SignedInfo"
1036            })?
1037            .children()
1038            .filter(is_reference)
1039            .nth(reference_index),
1040        ReferenceSet::Manifest => signature_node
1041            .children()
1042            .filter(|node| {
1043                node.is_element()
1044                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
1045                    && node.tag_name().name() == "Object"
1046            })
1047            .flat_map(|object| {
1048                object.children().filter(|node| {
1049                    node.is_element()
1050                        && node.tag_name().namespace() == Some(XMLDSIG_NS)
1051                        && node.tag_name().name() == "Manifest"
1052                })
1053            })
1054            .flat_map(|manifest| manifest.children().filter(is_reference))
1055            .nth(reference_index),
1056    }
1057}
1058
1059fn bind_reference_evidence(
1060    result: &mut ReferenceResult,
1061    reference: &Reference,
1062    view: DocumentView<'_>,
1063    origin: Option<Node<'_, '_>>,
1064    observed: &OperationResourceIdentity,
1065) {
1066    result.reference_identity = origin.map(|node| view.node_identity(node));
1067    if let OperationResourceIdentity::External { fingerprint, .. } = observed {
1068        result.external_resource_fingerprint = Some(*fingerprint);
1069    }
1070    if let OperationResourceIdentity::DocumentNode(target) = observed {
1071        result.target_identity = Some(*target);
1072        // A later node-set transform can reparse C14N output, so arbitrary
1073        // chains cannot retain an original subtree claim. This classification
1074        // describes proven element content, never discarded comments or the
1075        // namespace-declaration spelling normalized by canonicalization.
1076        let mut enveloped = false;
1077        let mut canonicalized = false;
1078        for transform in &reference.transforms {
1079            match transform {
1080                Transform::C14n(_) if !canonicalized => canonicalized = true,
1081                Transform::Enveloped if !canonicalized => enveloped = true,
1082                _ => return,
1083            }
1084        }
1085        result.coverage = if enveloped {
1086            ReferenceCoverage::EnvelopedElementContent
1087        } else {
1088            ReferenceCoverage::ElementContent
1089        };
1090    }
1091}
1092
1093struct ReferenceExecutionContext<'a, C = ()> {
1094    store_pre_digest: bool,
1095    transform_options: TransformOptions<'a>,
1096    transform_budget: &'a TransformExecutionBudget,
1097    canonicalized_data_budget: &'a CanonicalizedDataBudget,
1098    provider: &'a dyn crate::provider::CryptoProvider,
1099    capture: &'a C,
1100}
1101
1102struct CanonicalizedDataBudget {
1103    remaining: Cell<usize>,
1104    max_bytes: usize,
1105}
1106
1107struct VerificationOperationBudgets<'a> {
1108    key_info_parse: RefCell<super::parse::KeyInfoParseUsage>,
1109    key_candidates: RefCell<super::InspectedKeyCandidateBudget<'a>>,
1110    key_establishment: RefCell<crate::key_establishment::KeyEstablishmentUsage>,
1111    transforms: TransformExecutionBudget,
1112    canonicalized: CanonicalizedDataBudget,
1113    xpath_parse: RefCell<XPathSignatureParseBudget>,
1114    key_info_materialization: RefCell<KeyInfoMaterializationState>,
1115    external_resource_identities: RefCell<HashMap<String, OperationResourceIdentity>>,
1116    resolved_reference_resources: RefCell<HashMap<NodeId, OperationResourceIdentity>>,
1117    external_resource_set_identity: RefCell<Option<OperationResourceIdentity>>,
1118}
1119
1120impl<'a> VerificationOperationBudgets<'a> {
1121    fn with_transforms(
1122        policy: &crate::policy::VerificationPolicy,
1123        transforms: TransformExecutionBudget,
1124        utc_facts: Option<structured_pkix::temporal::LeapTable<'a>>,
1125    ) -> Self {
1126        let key_info_parse = super::parse::KeyInfoParseUsage::new(&policy.resources);
1127        let key_candidates = super::InspectedKeyCandidateBudget::with_ingestion(
1128            policy,
1129            &key_info_parse.certificates,
1130            utc_facts,
1131        );
1132        Self {
1133            key_info_parse: RefCell::new(key_info_parse),
1134            key_candidates: RefCell::new(key_candidates),
1135            key_establishment: Default::default(),
1136            transforms,
1137            canonicalized: CanonicalizedDataBudget::with_limit(
1138                policy.resources.effective_canonicalized_bytes(),
1139            ),
1140            xpath_parse: RefCell::new(XPathSignatureParseBudget::from_resources(&policy.resources)),
1141            key_info_materialization: RefCell::new(KeyInfoMaterializationState::default()),
1142            external_resource_identities: RefCell::new(HashMap::new()),
1143            resolved_reference_resources: RefCell::new(HashMap::new()),
1144            external_resource_set_identity: RefCell::new(None),
1145        }
1146    }
1147
1148    fn external_resource_set_identity(
1149        &self,
1150        resolver: &UriReferenceResolver<'_>,
1151    ) -> OperationResourceIdentity {
1152        // One resolver borrows the same immutable map for the entire request.
1153        // Hash/sort it once, not once per graph node or Signature.
1154        let mut cached = self.external_resource_set_identity.borrow_mut();
1155        cached
1156            .get_or_insert_with(|| resolver.external_resource_set_identity())
1157            .clone()
1158    }
1159
1160    fn resource_identity_for_reference(
1161        &self,
1162        reference: &Reference,
1163        index: usize,
1164        resolver: &UriReferenceResolver<'_>,
1165        view: DocumentView<'_>,
1166        origin: Option<Node<'_, '_>>,
1167    ) -> OperationResourceIdentity {
1168        let Some(uri) = reference.uri.as_deref() else {
1169            return OperationResourceIdentity::Generated("omitted-reference", index);
1170        };
1171        if uri.is_empty() || uri.starts_with('#') {
1172            return resolver
1173                .node_id_for_reference_resource(
1174                    uri,
1175                    Some(self.transforms.node_set_materialization()),
1176                )
1177                .ok()
1178                .flatten()
1179                .map(|node| OperationResourceIdentity::DocumentNode(view.node_identity_by_id(node)))
1180                .unwrap_or(OperationResourceIdentity::Generated(
1181                    "missing-document-reference",
1182                    index,
1183                ));
1184        }
1185
1186        let resolved_uri;
1187        let uri = if let Some(origin) = origin {
1188            if let Some(identity) = self.resolved_reference_resources.borrow().get(&origin.id()) {
1189                return identity.clone();
1190            }
1191            // XML Base Second Edition §4.2 resolves the external reference in
1192            // its owning element's context. Identity and execution must agree.
1193            // https://www.w3.org/TR/2009/REC-xmlbase-20090128/#resolving
1194            let Ok(resolved) = crate::c14n::xml_base::resolve_uri_from_node_with_budget(
1195                origin,
1196                uri,
1197                self.transforms.xml_base_resolution(),
1198            ) else {
1199                return OperationResourceIdentity::Generated(
1200                    "unresolved-external-reference",
1201                    index,
1202                );
1203            };
1204            resolved_uri = resolved;
1205            resolved_uri.as_str()
1206        } else {
1207            uri
1208        };
1209        if let Some(identity) = self.external_resource_identities.borrow().get(uri).cloned() {
1210            if let Some(origin) = origin {
1211                self.resolved_reference_resources
1212                    .borrow_mut()
1213                    .insert(origin.id(), identity.clone());
1214            }
1215            return identity;
1216        }
1217        let identity = resolver
1218            .external_resource_identity(uri)
1219            .unwrap_or(OperationResourceIdentity::MissingExternal);
1220        if matches!(identity, OperationResourceIdentity::External { .. }) {
1221            self.external_resource_identities
1222                .borrow_mut()
1223                .insert(uri.to_owned(), identity.clone());
1224        }
1225        if let Some(origin) = origin {
1226            self.resolved_reference_resources
1227                .borrow_mut()
1228                .insert(origin.id(), identity.clone());
1229        }
1230        identity
1231    }
1232}
1233
1234struct VerificationPlanNodes {
1235    document: OperationNodeId,
1236    key_materialization: OperationNodeId,
1237    key: OperationNodeId,
1238    digests: Vec<OperationNodeId>,
1239    canonicalization: OperationNodeId,
1240    crypto: OperationNodeId,
1241}
1242
1243fn compile_verification_operation_plan(
1244    operation: &mut OperationExecutionContext<
1245        &crate::policy::VerificationPolicy,
1246        &VerificationOperationBudgets,
1247    >,
1248    view: DocumentView<'_>,
1249    signature_node: Node<'_, '_>,
1250    references: &[Reference],
1251    resolver: &UriReferenceResolver<'_>,
1252) -> Result<VerificationPlanNodes, SignatureVerificationPipelineError> {
1253    operation
1254        .validate_document_view(view)
1255        .map_err(map_verification_plan_error)?;
1256    let document_node = operation.add_node(
1257        OperationNodeKind::Document,
1258        OperationStage::Parse,
1259        Some(OperationResourceIdentity::DocumentNode(
1260            view.node_identity(signature_node),
1261        )),
1262    );
1263    let key_materialization_resource = operation.budgets().external_resource_set_identity(resolver);
1264    let key_materialization = operation.add_node(
1265        OperationNodeKind::Key { index: 0 },
1266        OperationStage::Resolve,
1267        Some(key_materialization_resource.clone()),
1268    );
1269    let key_node = operation.add_node(
1270        OperationNodeKind::Key { index: 1 },
1271        OperationStage::Resolve,
1272        None,
1273    );
1274    operation
1275        .add_dependency(key_materialization, document_node)
1276        .map_err(map_verification_plan_error)?;
1277    operation
1278        .add_dependency(key_node, key_materialization)
1279        .map_err(map_verification_plan_error)?;
1280    let mut digests = Vec::with_capacity(references.len());
1281    for (index, reference) in references.iter().enumerate() {
1282        let resource = operation.budgets().resource_identity_for_reference(
1283            reference,
1284            index,
1285            resolver,
1286            view,
1287            reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
1288        );
1289        let digest_node = operation.add_node(
1290            OperationNodeKind::Digest { index },
1291            OperationStage::Digest,
1292            Some(resource.clone()),
1293        );
1294        operation
1295            .add_dependency(digest_node, document_node)
1296            .map_err(map_verification_plan_error)?;
1297        digests.push(digest_node);
1298    }
1299    let canonicalization = operation.add_node(
1300        OperationNodeKind::Canonicalization,
1301        OperationStage::Canonicalization,
1302        None,
1303    );
1304    for digest in &digests {
1305        operation
1306            .add_dependency(canonicalization, *digest)
1307            .map_err(map_verification_plan_error)?;
1308    }
1309    let crypto = operation.add_node(OperationNodeKind::Crypto, OperationStage::Crypto, None);
1310    operation
1311        .add_dependency(crypto, key_node)
1312        .map_err(map_verification_plan_error)?;
1313    operation
1314        .add_dependency(crypto, canonicalization)
1315        .map_err(map_verification_plan_error)?;
1316    operation.compile().map_err(map_verification_plan_error)?;
1317    Ok(VerificationPlanNodes {
1318        document: document_node,
1319        key_materialization,
1320        key: key_node,
1321        digests,
1322        canonicalization,
1323        crypto,
1324    })
1325}
1326
1327fn map_verification_plan_error(error: OperationPlanError) -> SignatureVerificationPipelineError {
1328    SignatureVerificationPipelineError::OperationPlan(error.to_string())
1329}
1330
1331impl Default for CanonicalizedDataBudget {
1332    fn default() -> Self {
1333        Self {
1334            remaining: Cell::new(CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING),
1335            max_bytes: CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
1336        }
1337    }
1338}
1339
1340impl CanonicalizedDataBudget {
1341    fn remaining(&self) -> usize {
1342        self.remaining.get()
1343    }
1344
1345    fn charge(&self, bytes: usize) -> Result<(), ReferenceProcessingError> {
1346        let available = self.remaining.get();
1347        let Some(remaining) = available.checked_sub(bytes) else {
1348            self.remaining.set(0);
1349            return Err(crate::policy::PolicyViolation::ResourceLimit {
1350                resource: crate::policy::resource_name::CANONICALIZED_BYTES,
1351                maximum: self.max_bytes,
1352                actual: self
1353                    .max_bytes
1354                    .saturating_add(bytes.saturating_sub(available)),
1355            }
1356            .into());
1357        };
1358        self.remaining.set(remaining);
1359        Ok(())
1360    }
1361
1362    fn with_limit(max_bytes: usize) -> Self {
1363        Self {
1364            remaining: Cell::new(max_bytes),
1365            max_bytes,
1366        }
1367    }
1368}
1369
1370fn process_reference_with_options<C: VerificationCapture>(
1371    reference: &Reference,
1372    resolver: &UriReferenceResolver<'_>,
1373    signature_node: Node<'_, '_>,
1374    reference_set: ReferenceSet,
1375    reference_index: usize,
1376    reference_node: Option<Node<'_, '_>>,
1377    execution: &ReferenceExecutionContext<'_, C>,
1378) -> Result<ReferenceResult, ReferenceProcessingError> {
1379    execution.capture.admit_digest(
1380        reference_node.map(|node| node.id()),
1381        reference.digest_method,
1382    )?;
1383    // 1. Dereference URI. Omitted URI is distinct from URI="" in XMLDSig and
1384    // must be rejected until caller-provided external object resolution exists.
1385    let uri = reference
1386        .uri
1387        .as_deref()
1388        .ok_or(ReferenceProcessingError::MissingUri)?;
1389    let initial_data = reference_node
1390        .map_or_else(
1391            || {
1392                resolver.dereference_with_budget(
1393                    uri,
1394                    execution.transform_budget.node_set_materialization(),
1395                )
1396            },
1397            |node| {
1398                resolver.dereference_from_with_budget(
1399                    uri,
1400                    node,
1401                    execution.transform_budget.node_set_materialization(),
1402                    execution.transform_budget.xml_base_resolution(),
1403                )
1404            },
1405        )
1406        .map_err(ReferenceProcessingError::UriDereference)?;
1407
1408    // 2. Apply transform chain
1409    let source = reference_node.unwrap_or(signature_node);
1410    let containing_signature =
1411        super::transforms::enveloped_signature_for_reference(source, &reference.transforms, || {
1412            execution.transform_budget.charge_node_filter_work(1)
1413        })
1414        .map_err(ReferenceProcessingError::Transform)?;
1415    let pre_digest_bytes = super::transforms::execute_reference_transforms_with_budget(
1416        source,
1417        containing_signature,
1418        initial_data,
1419        &reference.transforms,
1420        execution.transform_options,
1421        execution.transform_budget,
1422        execution.transform_budget.xml_parse_work(),
1423    )
1424    .map_err(ReferenceProcessingError::Transform)?;
1425
1426    // 3. Compute digest
1427    let computed_digest = super::compute_digest_with_provider(
1428        execution.provider,
1429        reference.digest_method,
1430        &pre_digest_bytes,
1431    )?;
1432    execution
1433        .capture
1434        .computed_digest(reference_node.map(|node| node.id()))?;
1435
1436    // 4. Compare with stored DigestValue (constant-time)
1437    let status = if constant_time_eq(&computed_digest, &reference.digest_value) {
1438        DsigStatus::Valid
1439    } else {
1440        DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch {
1441            ref_index: reference_index,
1442        })
1443    };
1444
1445    let pre_digest_data = if execution.store_pre_digest {
1446        execution
1447            .canonicalized_data_budget
1448            .charge(pre_digest_bytes.len())?;
1449        Some(pre_digest_bytes)
1450    } else {
1451        None
1452    };
1453
1454    Ok(ReferenceResult {
1455        reference_identity: None,
1456        target_identity: None,
1457        external_resource_fingerprint: None,
1458        coverage: ReferenceCoverage::TransformedData,
1459        reference_set,
1460        reference_index,
1461        uri: uri.to_owned(),
1462        digest_algorithm: reference.digest_method,
1463        status,
1464        pre_digest_data,
1465    })
1466}
1467
1468/// Process all `<Reference>` elements in a `<SignedInfo>`, with fail-fast
1469/// on the first digest mismatch.
1470///
1471/// Per XMLDSig spec: if any reference fails, the entire signature is invalid.
1472/// Processing stops at the first failure for efficiency.
1473///
1474/// # Errors
1475///
1476/// Returns `Err` only for processing failures (malformed XML, unsupported
1477/// transform, etc.). Digest mismatches are reported via
1478/// `ReferencesResult::first_failure`.
1479pub fn process_all_references(
1480    references: &[Reference],
1481    resolver: &UriReferenceResolver<'_>,
1482    signature_node: Node<'_, '_>,
1483    store_pre_digest: bool,
1484) -> Result<ReferencesResult, ReferenceProcessingError> {
1485    let execution_budget = TransformExecutionBudget::default();
1486    let canonicalized_data_budget = CanonicalizedDataBudget::default();
1487    let execution = ReferenceExecutionContext {
1488        store_pre_digest,
1489        transform_options: TransformOptions::default().id_attributes(resolver.id_registrations()),
1490        transform_budget: &execution_budget,
1491        canonicalized_data_budget: &canonicalized_data_budget,
1492        provider: crate::provider::default_provider(),
1493        capture: &(),
1494    };
1495    process_all_references_with_options(references, resolver, signature_node, &execution)
1496}
1497
1498fn process_all_references_with_options(
1499    references: &[Reference],
1500    resolver: &UriReferenceResolver<'_>,
1501    signature_node: Node<'_, '_>,
1502    execution: &ReferenceExecutionContext<'_>,
1503) -> Result<ReferencesResult, ReferenceProcessingError> {
1504    let mut results = Vec::with_capacity(references.len());
1505
1506    for (i, reference) in references.iter().enumerate() {
1507        let result = process_reference_with_options(
1508            reference,
1509            resolver,
1510            signature_node,
1511            ReferenceSet::SignedInfo,
1512            i,
1513            reference_origin_node(signature_node, ReferenceSet::SignedInfo, i),
1514            execution,
1515        )?;
1516        let failed = matches!(result.status, DsigStatus::Invalid(_));
1517        results.push(result);
1518
1519        if failed {
1520            return Ok(ReferencesResult {
1521                results,
1522                first_failure: Some(i),
1523            });
1524        }
1525    }
1526
1527    Ok(ReferencesResult {
1528        results,
1529        first_failure: None,
1530    })
1531}
1532
1533/// Errors during reference processing.
1534///
1535/// Distinct from digest mismatch (which is a validation result, not a processing error).
1536#[derive(Debug, thiserror::Error)]
1537#[non_exhaustive]
1538pub enum ReferenceProcessingError {
1539    /// The current shared execution refused original digest-receipt retention.
1540    #[error("shared reference execution: {0}")]
1541    SharedExecution(#[from] structured_asn1::memory::MemoryError),
1542    /// The immutable verification policy rejected reference processing.
1543    #[error("verification policy violation: {0}")]
1544    Policy(#[from] crate::policy::PolicyViolation),
1545
1546    /// The selected provider could not compute the declared digest.
1547    #[error("cryptographic provider error: {0}")]
1548    Provider(#[from] crate::provider::ProviderError),
1549
1550    /// `<Reference>` omitted the `URI` attribute, which we do not resolve implicitly.
1551    #[error("reference URI is required; omitted URI references are not supported")]
1552    MissingUri,
1553
1554    /// URI dereference failed.
1555    #[error("URI dereference failed: {0}")]
1556    UriDereference(#[source] super::types::TransformError),
1557
1558    /// Transform execution failed.
1559    #[error("transform failed: {0}")]
1560    Transform(#[source] super::types::TransformError),
1561}
1562
1563impl ReferenceProcessingError {
1564    fn into_policy_violation(self) -> Result<crate::policy::PolicyViolation, Self> {
1565        match self {
1566            Self::Policy(error)
1567            | Self::UriDereference(TransformError::Policy(error))
1568            | Self::Transform(TransformError::Policy(error)) => Ok(error),
1569            error => Err(error),
1570        }
1571    }
1572}
1573
1574/// End-to-end XMLDSig verification result for one `<Signature>`.
1575#[derive(Debug)]
1576#[non_exhaustive]
1577#[must_use = "inspect status before accepting the document"]
1578pub struct VerifyResult {
1579    /// Key authorization, independent of `status`; neither alone authorizes data.
1580    pub key_trust: super::KeyTrustEvidence,
1581    /// Core XMLDSig status for the `<SignedInfo>` references and signature value.
1582    ///
1583    /// Manifest reference failures do not alter this field; inspect
1584    /// [`Self::manifest_references`] before accepting Manifest-backed data.
1585    pub status: DsigStatus,
1586    /// `<Reference>` verification results from `<SignedInfo>`.
1587    /// On fail-fast, this includes references up to and including
1588    /// the first digest mismatch only.
1589    pub signed_info_references: Vec<ReferenceResult>,
1590    /// `<Manifest>` reference results.
1591    /// Populated only when `VerifyContext::process_manifests(true)` is enabled
1592    /// and core signature validation succeeds.
1593    /// Includes references whose original Manifest or ancestor is authenticated
1594    /// by this Signature's structure-preserving references. Manifests may be
1595    /// nested or outside Signature; DOM ancestry alone never authenticates them.
1596    /// Each entry has an independent status that does not alter [`Self::status`].
1597    /// Callers must inspect every entry before accepting Manifest-backed data.
1598    /// Unsigned/unreferenced Manifest blocks are skipped, so an
1599    /// empty list does not imply that no Manifest elements existed in `verify()` input.
1600    pub manifest_references: Vec<ReferenceResult>,
1601    /// Canonicalized `<SignedInfo>` bytes when `store_pre_digest` is enabled
1602    /// and verification reaches SignedInfo canonicalization.
1603    pub canonicalized_signed_info: Option<Vec<u8>>,
1604}
1605
1606/// Errors while running end-to-end XMLDSig verification.
1607#[derive(Debug, thiserror::Error)]
1608#[non_exhaustive]
1609pub enum DsigError {
1610    /// The current shared execution refused work or storage admission.
1611    #[error("shared verification execution: {0}")]
1612    SharedExecution(#[from] structured_asn1::memory::MemoryError),
1613    /// Key establishment failed before the consuming MAC.
1614    #[error(transparent)]
1615    KeyEstablishment(#[from] crate::key_establishment::KeyEstablishmentError),
1616    /// Caller-supplied request identities or requirements are inconsistent.
1617    #[error("invalid verification request: {reason}")]
1618    InvalidRequest {
1619        /// Concrete invariant violated before signature execution.
1620        reason: &'static str,
1621    },
1622    /// The compiled verification policy rejected an operation input.
1623    #[error("verification policy violation: {0}")]
1624    Policy(#[from] crate::policy::PolicyViolation),
1625
1626    /// A query-funded algorithm rejection; the URI retains its charge until drop.
1627    #[error("verification policy violation: {operation} policy rejects algorithm {algorithm}")]
1628    AccountedPolicyAlgorithm {
1629        /// Operation evaluating the algorithm.
1630        operation: &'static str,
1631        /// Rejected algorithm URI, owned by the current query's account.
1632        algorithm: structured_asn1::memory::OwnedText,
1633    },
1634
1635    /// The selected provider cannot execute the requested operation.
1636    #[error("cryptographic provider error: {0}")]
1637    Provider(#[from] crate::provider::ProviderError),
1638
1639    /// XML parsing failed.
1640    #[error("XML parse error: {0}")]
1641    XmlParse(#[from] crate::xml::dom::ParseError),
1642
1643    /// The owned XML document boundary rejected the document or identity.
1644    #[error("XML document error: {0}")]
1645    Document(#[from] crate::document::XmlDocumentError),
1646
1647    /// Required signature element is missing.
1648    #[error("missing required element: <{element}>")]
1649    MissingElement {
1650        /// Name of the missing element.
1651        element: &'static str,
1652    },
1653
1654    /// Signature element tree shape violates XMLDSig structure requirements.
1655    #[error("invalid Signature structure: {reason}")]
1656    InvalidStructure {
1657        /// Validation failure reason.
1658        reason: &'static str,
1659    },
1660
1661    /// The authenticated dependency graph could not be compiled or executed.
1662    #[error("invalid operation plan: {0}")]
1663    OperationPlan(String),
1664
1665    /// The requested operation start node is absent or has a duplicate ID.
1666    #[error("selected node ID is missing or ambiguous: {id}")]
1667    SelectedNodeUnavailable {
1668        /// Caller-provided XML ID value.
1669        id: String,
1670    },
1671
1672    /// `<SignedInfo>` parsing failed.
1673    #[error("failed to parse SignedInfo: {0}")]
1674    ParseSignedInfo(super::parse::ParseError),
1675
1676    /// `<KeyInfo>` parsing failed.
1677    #[error("failed to parse KeyInfo: {0}")]
1678    ParseKeyInfo(#[source] super::parse::ParseError),
1679
1680    /// Configuration-driven key resolution failed.
1681    #[error("key resolution failed: {0}")]
1682    KeyResolution(#[from] super::keys::KeyResolutionError),
1683
1684    /// `<Object>/<Manifest>/<Reference>` parsing failed.
1685    #[error("failed to parse Manifest reference: {0}")]
1686    ParseManifestReference(#[source] ParseError),
1687
1688    /// Reference processing failed.
1689    #[error("reference processing failed: {0}")]
1690    Reference(ReferenceProcessingError),
1691
1692    /// SignedInfo canonicalization failed.
1693    #[error("SignedInfo canonicalization failed: {0}")]
1694    Canonicalization(#[from] crate::c14n::C14nError),
1695
1696    /// SignatureValue base64 decoding failed.
1697    #[error("invalid SignatureValue base64: {0}")]
1698    SignatureValueBase64(#[from] base64::DecodeError),
1699
1700    /// Cryptographic verification failed before validity decision.
1701    #[error("signature verification failed: {0}")]
1702    Crypto(#[from] SignatureVerificationError),
1703}
1704
1705impl From<super::parse::ParseError> for DsigError {
1706    fn from(error: super::parse::ParseError) -> Self {
1707        match error {
1708            super::parse::ParseError::Policy(error) => Self::Policy(error),
1709            super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1710                Self::Policy(error)
1711            }
1712            error => Self::ParseSignedInfo(error),
1713        }
1714    }
1715}
1716
1717fn map_key_info_parse_error(error: super::parse::ParseError) -> DsigError {
1718    match error {
1719        super::parse::ParseError::Policy(error)
1720        | super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1721            DsigError::Policy(error)
1722        }
1723        error => DsigError::ParseKeyInfo(error),
1724    }
1725}
1726
1727fn map_manifest_parse_error(error: super::parse::ParseError) -> DsigError {
1728    match error {
1729        super::parse::ParseError::Policy(error)
1730        | super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1731            DsigError::Policy(error)
1732        }
1733        error => DsigError::ParseManifestReference(error),
1734    }
1735}
1736
1737impl From<ReferenceProcessingError> for DsigError {
1738    fn from(error: ReferenceProcessingError) -> Self {
1739        match error.into_policy_violation() {
1740            Ok(error) => Self::Policy(error),
1741            Err(error) => Self::Reference(error),
1742        }
1743    }
1744}
1745
1746type SignatureVerificationPipelineError = DsigError;
1747
1748impl From<OperationPlanError> for DsigError {
1749    fn from(error: OperationPlanError) -> Self {
1750        Self::OperationPlan(error.to_string())
1751    }
1752}
1753
1754/// Verify one XMLDSig `<Signature>` end-to-end with a PEM public key.
1755///
1756/// Pipeline:
1757/// 1. Parse `<Signature>` children and enforce structural constraints
1758/// 2. Parse `<SignedInfo>`
1759/// 3. Validate all `<Reference>` digests (fail-fast)
1760/// 4. Canonicalize `<SignedInfo>`
1761/// 5. Base64-decode `<SignatureValue>`
1762/// 6. Verify signature bytes against canonicalized `<SignedInfo>` using the provided PEM key
1763///
1764/// If any `<Reference>` digest mismatches, returns `Ok` with
1765/// `status == Invalid(ReferenceDigestMismatch { .. })`.
1766///
1767/// This API uses only the provided PEM key and does not parse embedded
1768/// `<KeyInfo>` key material for key selection/validation. Consequently,
1769/// malformed optional `<KeyInfo>` does not produce `DsigError::ParseKeyInfo`
1770/// on this API path.
1771///
1772/// Structural constraints enforced by this API:
1773/// - The document must contain exactly one XMLDSig `<Signature>` element.
1774/// - `<SignedInfo>` must be the first element child of `<Signature>` and appear once.
1775/// - `<SignatureValue>` must be the second element child of `<Signature>` and appear once.
1776/// - `<KeyInfo>` is optional and, when present, must be the third element child.
1777/// - Only XMLDSig namespace element children are allowed under `<Signature>`.
1778/// - Non-whitespace mixed text content under `<Signature>` is rejected.
1779/// - After `<SignedInfo>`, `<SignatureValue>`, and optional `<KeyInfo>`, only `<Object>` elements are allowed.
1780/// - `<SignatureValue>` must not contain nested element children.
1781pub fn verify_signature_with_pem_key(
1782    xml: &str,
1783    public_key_pem: &str,
1784    store_pre_digest: bool,
1785) -> Result<VerifyResult, DsigError> {
1786    struct PemVerifyingKey<'a> {
1787        public_key_pem: &'a str,
1788    }
1789
1790    impl VerifyingKey for PemVerifyingKey<'_> {
1791        fn verify_with_context(
1792            &self,
1793            algorithm: SignatureAlgorithm,
1794            context: &super::SignatureContext,
1795            signed_data: &[u8],
1796            signature_value: &[u8],
1797        ) -> Result<bool, DsigError> {
1798            if context.as_bytes().is_empty() {
1799                return self.verify(algorithm, signed_data, signature_value);
1800            }
1801            if algorithm.context_element().is_none() {
1802                return Err(SignatureVerificationError::UnsupportedAlgorithm {
1803                    uri: algorithm.uri().to_owned(),
1804                }
1805                .into());
1806            }
1807            let (rest, pem) = x509_parser::pem::parse_x509_pem(self.public_key_pem.as_bytes())
1808                .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
1809            if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
1810                return Err(SignatureVerificationError::InvalidKeyPem.into());
1811            }
1812            Ok(super::modern::verify_with_context(
1813                algorithm,
1814                &pem.contents,
1815                context,
1816                signed_data,
1817                signature_value,
1818            )?)
1819        }
1820
1821        fn verify(
1822            &self,
1823            algorithm: SignatureAlgorithm,
1824            signed_data: &[u8],
1825            signature_value: &[u8],
1826        ) -> Result<bool, DsigError> {
1827            verify_with_algorithm(algorithm, self.public_key_pem, signed_data, signature_value)
1828        }
1829    }
1830
1831    let key = PemVerifyingKey { public_key_pem };
1832    VerifyContext::new()
1833        .key(&key)
1834        .store_pre_digest(store_pre_digest)
1835        .verify(xml)
1836}
1837
1838fn verify_signature_with_context(
1839    xml: &str,
1840    ctx: &VerifyContext<'_>,
1841) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1842    ctx.policy.validate()?;
1843    ctx.policy.signature_constraints.require_targets(false)?;
1844    ctx.policy.resources.validate_xml_document_len(xml.len())?;
1845    let execution_budget = TransformExecutionBudget::from_resources(&ctx.policy.resources)
1846        .with_xml_backend(ctx.xml_backend);
1847    let settings = DocumentParseSettings::from_policy(&ctx.policy.xml, &ctx.policy.resources)
1848        .with_backend(ctx.xml_backend);
1849    let document = XmlDocument::parse_with_settings_and_budget(
1850        xml.to_owned(),
1851        settings,
1852        execution_budget.xml_parse_work(),
1853    )
1854    .map_err(|error| map_document_parse_error(error, settings))?;
1855    verify_signature_document_with_context_and_transforms(&document, ctx, execution_budget)
1856}
1857
1858fn map_document_parse_error(error: XmlDocumentError, settings: DocumentParseSettings) -> DsigError {
1859    match error.into_policy_violation(settings) {
1860        Ok(error) => DsigError::Policy(error),
1861        Err(XmlDocumentError::Parse(error)) => DsigError::XmlParse(error),
1862        Err(error) => DsigError::Document(error),
1863    }
1864}
1865
1866#[cfg(test)]
1867mod xml_parse_budget_tests {
1868    use super::*;
1869
1870    #[test]
1871    fn verification_initial_parse_uses_the_policy_work_budget() {
1872        // Even a structurally invalid signature must not reach parsing when
1873        // the immutable operation snapshot denies all XML parse work.
1874        let xml = "<root/>";
1875        let mut policy = crate::policy::VerificationPolicy::default();
1876        policy.resources.max_xml_parse_work_bytes = 0;
1877
1878        let error = VerifyContext::new()
1879            .policy(policy)
1880            .verify(xml)
1881            .expect_err("a zero parse-work budget must reject the input parse");
1882
1883        assert!(matches!(
1884            error,
1885            DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1886                resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
1887                maximum: 0,
1888                actual,
1889            }) if actual == xml.len()
1890        ));
1891    }
1892}
1893
1894fn verify_signature_document_with_context(
1895    document: &XmlDocument,
1896    ctx: &VerifyContext<'_>,
1897) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1898    document.validate_operation_policy(&ctx.policy.xml, &ctx.policy.resources)?;
1899    verify_signature_document_with_context_and_transforms(
1900        document,
1901        ctx,
1902        TransformExecutionBudget::from_resources(&ctx.policy.resources)
1903            .with_xml_backend(ctx.xml_backend),
1904    )
1905}
1906
1907fn count_signature_nodes(view: DocumentView<'_>, maximum: usize) -> Result<usize, DsigError> {
1908    let mut count = 0;
1909    for node in view.document().descendants() {
1910        if !node.has_tag_name((XMLDSIG_NS, "Signature")) {
1911            continue;
1912        }
1913        if count == maximum {
1914            return Err(crate::policy::PolicyViolation::ResourceLimit {
1915                resource: crate::policy::resource_name::VERIFICATION_SIGNATURES,
1916                maximum,
1917                actual: count + 1,
1918            }
1919            .into());
1920        }
1921        count += 1;
1922    }
1923    Ok(count)
1924}
1925
1926trait SignatureBuffer: std::ops::Deref<Target = [SignatureEvidence]> {
1927    fn push_signature(
1928        &mut self,
1929        evidence: SignatureEvidence,
1930    ) -> Result<(), structured_asn1::memory::MemoryError>;
1931}
1932
1933impl SignatureBuffer for Vec<SignatureEvidence> {
1934    fn push_signature(
1935        &mut self,
1936        evidence: SignatureEvidence,
1937    ) -> Result<(), structured_asn1::memory::MemoryError> {
1938        self.push(evidence);
1939        Ok(())
1940    }
1941}
1942
1943impl<W: structured_asn1::memory::Work> SignatureBuffer
1944    for structured_asn1::memory::OwnedVec<SignatureEvidence, W>
1945{
1946    fn push_signature(
1947        &mut self,
1948        evidence: SignatureEvidence,
1949    ) -> Result<(), structured_asn1::memory::MemoryError> {
1950        self.try_push(evidence)
1951    }
1952}
1953
1954trait OperationRetention<P, B, C> {
1955    fn retain(
1956        &mut self,
1957        operation: OperationExecutionContext<P, B>,
1958        capture: C,
1959    ) -> Result<(), structured_asn1::memory::MemoryError>;
1960}
1961
1962impl<P, B, C> OperationRetention<P, B, C> for () {
1963    fn retain(
1964        &mut self,
1965        operation: OperationExecutionContext<P, B>,
1966        _capture: C,
1967    ) -> Result<(), structured_asn1::memory::MemoryError> {
1968        drop(operation);
1969        Ok(())
1970    }
1971}
1972
1973impl<P, B, C, W: structured_asn1::memory::Work> OperationRetention<P, B, C>
1974    for structured_asn1::memory::OwnedVec<CapturedOperation<P, B, C>, W>
1975{
1976    fn retain(
1977        &mut self,
1978        operation: OperationExecutionContext<P, B>,
1979        capture: C,
1980    ) -> Result<(), structured_asn1::memory::MemoryError> {
1981        self.try_push(CapturedOperation { operation, capture })
1982    }
1983}
1984
1985trait RequestStorage {
1986    type Signatures: SignatureBuffer;
1987    type Capture: VerificationCapture;
1988    // Actual reference types preserve their lifetimes without the implied
1989    // 'static bound of a lifetime-GAT projection under the scoped callback.
1990    type Operations<P, B>: OperationRetention<P, B, Self::Capture>;
1991
1992    fn capture(&self) -> Self::Capture;
1993
1994    fn admit_request(
1995        &self,
1996        document: &XmlDocument,
1997    ) -> Result<(), structured_asn1::memory::MemoryError>;
1998    fn signatures(
1999        &self,
2000        count: usize,
2001    ) -> Result<Self::Signatures, structured_asn1::memory::MemoryError>;
2002    fn operations<P, B>(
2003        &self,
2004        count: usize,
2005    ) -> Result<Self::Operations<P, B>, structured_asn1::memory::MemoryError>;
2006}
2007
2008struct CollectedRequestStorage;
2009
2010impl RequestStorage for CollectedRequestStorage {
2011    type Signatures = Vec<SignatureEvidence>;
2012    type Capture = ();
2013    type Operations<P, B> = ();
2014
2015    fn capture(&self) {}
2016
2017    fn admit_request(
2018        &self,
2019        _document: &XmlDocument,
2020    ) -> Result<(), structured_asn1::memory::MemoryError> {
2021        Ok(())
2022    }
2023    fn signatures(
2024        &self,
2025        count: usize,
2026    ) -> Result<Self::Signatures, structured_asn1::memory::MemoryError> {
2027        Ok(Vec::with_capacity(count))
2028    }
2029    fn operations<P, B>(
2030        &self,
2031        _count: usize,
2032    ) -> Result<Self::Operations<P, B>, structured_asn1::memory::MemoryError> {
2033        Ok(())
2034    }
2035}
2036
2037struct ScopedRequestStorage<'work, W: structured_asn1::memory::Work>(&'work W);
2038
2039impl<'work, W: structured_asn1::memory::Work> RequestStorage for ScopedRequestStorage<'work, W> {
2040    type Signatures = structured_asn1::memory::OwnedVec<SignatureEvidence, W>;
2041    type Capture = ScopedCapture<'work, W>;
2042    type Operations<P, B> =
2043        structured_asn1::memory::OwnedVec<CapturedOperation<P, B, ScopedCapture<'work, W>>, W>;
2044
2045    fn capture(&self) -> Self::Capture {
2046        ScopedCapture::new(self.0)
2047    }
2048
2049    fn admit_request(
2050        &self,
2051        document: &XmlDocument,
2052    ) -> Result<(), structured_asn1::memory::MemoryError> {
2053        let units = document
2054            .with_view(|view| {
2055                u64::try_from(view.node_count())
2056                    .ok()
2057                    .and_then(|count| count.checked_mul(2))
2058                    .and_then(|count| count.checked_add(1))
2059            })
2060            .ok_or(structured_asn1::memory::MemoryError::WorkExceeded)?;
2061        self.0.charge_work(units)
2062    }
2063    fn signatures(
2064        &self,
2065        count: usize,
2066    ) -> Result<Self::Signatures, structured_asn1::memory::MemoryError> {
2067        let mut values = self.0.vector();
2068        values.try_reserve_exact(count)?;
2069        Ok(values)
2070    }
2071    fn operations<P, B>(
2072        &self,
2073        count: usize,
2074    ) -> Result<Self::Operations<P, B>, structured_asn1::memory::MemoryError> {
2075        let mut values = self.0.vector();
2076        values.try_reserve_exact(count)?;
2077        Ok(values)
2078    }
2079}
2080
2081fn verify_all_document(
2082    document: &XmlDocument,
2083    ctx: &VerifyContext<'_>,
2084    request: &VerificationRequest<'_>,
2085) -> Result<VerifyEvidence, DsigError> {
2086    with_verified_document(
2087        document,
2088        ctx,
2089        request,
2090        CollectedRequestStorage,
2091        |_, _, _, evidence, (), _| Ok(evidence),
2092    )
2093}
2094
2095fn with_verified_document<'inputs, R, S: RequestStorage>(
2096    document: &XmlDocument,
2097    ctx: &VerifyContext<'inputs>,
2098    request: &VerificationRequest<'inputs>,
2099    storage: S,
2100    continuation: impl for<'view, 'scope> FnOnce(
2101        DocumentView<'view>,
2102        &'scope VerifyContext<'inputs>,
2103        &'scope VerificationOperationBudgets<'inputs>,
2104        VerifyEvidence<S::Signatures>,
2105        S::Operations<
2106            &'scope crate::policy::VerificationPolicy,
2107            &'scope VerificationOperationBudgets<'inputs>,
2108        >,
2109        UriReferenceResolver<'view>,
2110    ) -> Result<R, DsigError>,
2111) -> Result<R, DsigError> {
2112    storage.admit_request(document)?;
2113    ctx.policy.validate()?;
2114    document.validate_operation_policy(&ctx.policy.xml, &ctx.policy.resources)?;
2115    ctx.policy
2116        .signature_constraints
2117        .require_targets(!request.expected_targets.is_empty())?;
2118    if request.external_resources.is_some() && ctx.external_resources.is_some() {
2119        return Err(DsigError::InvalidRequest {
2120            reason: "external resources configured in both context and request",
2121        });
2122    }
2123    // RFC 5280 §6.1.1: validation time is one operation input, including
2124    // alternate certification paths and all Signatures in a batch.
2125    // https://www.rfc-editor.org/rfc/rfc5280#section-6.1.1
2126    let capture_time =
2127        ctx.policy.key_trust.verify_x509_chains && ctx.policy.key_trust.verification_time.is_none();
2128    let captured_context;
2129    let ctx = if capture_time || request.external_resources.is_some() {
2130        captured_context = {
2131            let mut captured = ctx.clone();
2132            if let Some(resources) = request.external_resources {
2133                captured.external_resources = Some(resources);
2134            }
2135            if capture_time {
2136                captured.policy.key_trust.verification_time = Some(std::time::SystemTime::now());
2137            }
2138            captured
2139        };
2140        &captured_context
2141    } else {
2142        ctx
2143    };
2144    let budgets = VerificationOperationBudgets::with_transforms(
2145        &ctx.policy,
2146        TransformExecutionBudget::from_resources(&ctx.policy.resources)
2147            .with_xml_backend(ctx.xml_backend),
2148        ctx.utc_facts,
2149    );
2150    document.with_view(|view| {
2151        match request.signatures {
2152            SignatureRequirement::AtLeast(0) | SignatureRequirement::Exactly(0) => {
2153                return Err(DsigError::InvalidRequest {
2154                    reason: "signature requirement must be nonzero",
2155                });
2156            }
2157            _ => {}
2158        }
2159        if request.expected_targets.len() > ctx.policy.resources.max_references {
2160            return Err(DsigError::InvalidRequest {
2161                reason: "too many expected coverage targets",
2162            });
2163        }
2164        if request.trusted_keys.len() > ctx.policy.resources.max_signatures {
2165            return Err(DsigError::InvalidRequest {
2166                reason: "too many trusted signature keys",
2167            });
2168        }
2169        for (index, target) in request.expected_targets.iter().copied().enumerate() {
2170            if !view.resolve_node(target)?.is_element() {
2171                return Err(DsigError::InvalidRequest {
2172                    reason: "coverage target must be an element",
2173                });
2174            }
2175            if request.expected_targets[..index].contains(&target) {
2176                return Err(DsigError::InvalidRequest {
2177                    reason: "duplicate expected coverage target",
2178                });
2179            }
2180        }
2181        for (index, entry) in request.trusted_keys.iter().enumerate() {
2182            if !view
2183                .resolve_node(entry.signature)?
2184                .has_tag_name((XMLDSIG_NS, "Signature"))
2185            {
2186                return Err(DsigError::InvalidRequest {
2187                    reason: "trusted key target must be a Signature",
2188                });
2189            }
2190            if request.trusted_keys[..index]
2191                .iter()
2192                .any(|prior| prior.signature == entry.signature)
2193            {
2194                return Err(DsigError::InvalidRequest {
2195                    reason: "ambiguous trusted keys for one Signature",
2196                });
2197            }
2198        }
2199        let is_signature = |node: &Node<'_, '_>| node.has_tag_name((XMLDSIG_NS, "Signature"));
2200        let count = count_signature_nodes(view, ctx.policy.resources.max_signatures)?;
2201        // Cardinality is checked before allocating the report or calling a
2202        // resolver. No Vec of backend handles or cloned XML is needed.
2203        let mut signatures = storage.signatures(count)?;
2204        let mut operations = storage.operations(count)?;
2205        let resolver = UriReferenceResolver::with_document_view(view, ctx.id_attributes)
2206            .with_same_document_id_semantics(ctx.policy.transforms.same_document_id_semantics)
2207            .with_external_resource_limits(
2208                ctx.policy.resources.max_external_resource_bytes,
2209                ctx.policy.resources.max_external_resource_total_bytes,
2210            );
2211        let resolver = match ctx.external_resources {
2212            Some(resources) => resolver.with_external_resources(resources),
2213            None => resolver,
2214        };
2215        for signature in view.document().descendants().filter(is_signature) {
2216            // Authentication is signature-local; work accounting is request-
2217            // wide. A valid first signature cannot authenticate a Manifest
2218            // belonging to an invalid second signature.
2219            let mut operation = OperationExecutionContext::new(
2220                &ctx.policy,
2221                &budgets,
2222                Some((view.identity(), view.generation())),
2223            );
2224            let signature_identity = view.node_identity(signature);
2225            let key = request
2226                .trusted_keys
2227                .iter()
2228                .find(|entry| entry.signature == signature_identity)
2229                .map(|entry| entry.key)
2230                .or(ctx.key);
2231            let capture = storage.capture();
2232            let result = verify_signature_node(
2233                view,
2234                ctx,
2235                key,
2236                &mut operation,
2237                &resolver,
2238                signature,
2239                &capture,
2240            );
2241            signatures.push_signature(SignatureEvidence {
2242                identity: view.node_identity(signature),
2243                result,
2244            })?;
2245            operations.retain(operation, capture)?;
2246        }
2247        let mut evidence = VerifyEvidence {
2248            document: view.root(),
2249            signatures,
2250            accepted: false,
2251            correlation: request.correlation,
2252        };
2253        let valid = evidence
2254            .signatures
2255            .iter()
2256            .filter(|entry| {
2257                entry.result.as_ref().is_ok_and(|result| {
2258                    result.status == DsigStatus::Valid
2259                        && result
2260                            .manifest_references
2261                            .iter()
2262                            .all(|reference| reference.status == DsigStatus::Valid)
2263                })
2264            })
2265            .count();
2266        evidence.accepted = match request.signatures {
2267            SignatureRequirement::All => valid > 0 && valid == count,
2268            SignatureRequirement::AtLeast(minimum) => valid >= minimum,
2269            SignatureRequirement::Exactly(required) => valid == required && count == required,
2270        };
2271        for target in request.expected_targets {
2272            if !evidence.covers_element(document, *target)? {
2273                evidence.accepted = false;
2274                break;
2275            }
2276        }
2277        continuation(view, ctx, &budgets, evidence, operations, resolver)
2278    })
2279}
2280
2281fn verify_signature_document_with_context_and_transforms(
2282    document: &XmlDocument,
2283    ctx: &VerifyContext<'_>,
2284    transforms: TransformExecutionBudget,
2285) -> Result<VerifyResult, SignatureVerificationPipelineError> {
2286    // RFC 5280 §6.1.1 makes the validation time an input to path validation.
2287    // Capture it once, not separately for each source or alternate path:
2288    // https://www.rfc-editor.org/rfc/rfc5280#section-6.1.1
2289    let captured_context;
2290    let ctx = if ctx.policy.key_trust.verify_x509_chains
2291        && ctx.policy.key_trust.verification_time.is_none()
2292    {
2293        captured_context = {
2294            let mut context = ctx.clone();
2295            context.policy.key_trust.verification_time = Some(std::time::SystemTime::now());
2296            context
2297        };
2298        &captured_context
2299    } else {
2300        ctx
2301    };
2302    let budgets =
2303        VerificationOperationBudgets::with_transforms(&ctx.policy, transforms, ctx.utc_facts);
2304    let mut operation = OperationExecutionContext::new(
2305        &ctx.policy,
2306        &budgets,
2307        Some((document.identity(), document.generation())),
2308    );
2309    document.with_view(|view| verify_signature_view(view, ctx, &mut operation))
2310}
2311
2312fn verify_signature_view<'a>(
2313    view: DocumentView<'a>,
2314    ctx: &VerifyContext<'_>,
2315    operation: &mut OperationExecutionContext<
2316        &crate::policy::VerificationPolicy,
2317        &VerificationOperationBudgets,
2318    >,
2319) -> Result<VerifyResult, SignatureVerificationPipelineError> {
2320    ctx.policy.validate()?;
2321    ctx.policy.signature_constraints.require_targets(false)?;
2322    let doc = view.document();
2323    let resolver = UriReferenceResolver::with_document_view(view, ctx.id_attributes)
2324        .with_same_document_id_semantics(ctx.policy.transforms.same_document_id_semantics)
2325        .with_external_resource_limits(
2326            ctx.policy.resources.max_external_resource_bytes,
2327            ctx.policy.resources.max_external_resource_total_bytes,
2328        );
2329    let resolver = match ctx.external_resources {
2330        Some(resources) => resolver.with_external_resources(resources),
2331        None => resolver,
2332    };
2333    let start_node = match ctx.signature_selection {
2334        SignatureSelection::FirstSignatureUnderId(id) => {
2335            resolver.node_for_id(id).ok_or_else(|| {
2336                SignatureVerificationPipelineError::SelectedNodeUnavailable { id: id.to_owned() }
2337            })?
2338        }
2339        SignatureSelection::UniqueDocumentSignature
2340        | SignatureSelection::FirstDocumentSignature => doc.root(),
2341    };
2342    let mut signatures = start_node.descendants().filter(|node| {
2343        node.is_element()
2344            && node.tag_name().name() == "Signature"
2345            && node.tag_name().namespace() == Some(XMLDSIG_NS)
2346    });
2347    let signature_node = match (signatures.next(), ctx.signature_selection) {
2348        (None, _) => {
2349            return Err(SignatureVerificationPipelineError::MissingElement {
2350                element: "Signature",
2351            });
2352        }
2353        // libxmlsec1 treats --node-id as an operation start node and performs
2354        // a depth-first xmlSecFindNode lookup from there. Without a selector,
2355        // the library API retains its fail-closed document-wide cardinality.
2356        (
2357            Some(node),
2358            SignatureSelection::FirstDocumentSignature
2359            | SignatureSelection::FirstSignatureUnderId(_),
2360        ) => node,
2361        (Some(node), SignatureSelection::UniqueDocumentSignature)
2362            if signatures.next().is_none() =>
2363        {
2364            node
2365        }
2366        (Some(_), SignatureSelection::UniqueDocumentSignature) => {
2367            return Err(SignatureVerificationPipelineError::InvalidStructure {
2368                reason: "Signature must appear exactly once in document",
2369            });
2370        }
2371    };
2372
2373    verify_signature_node(
2374        view,
2375        ctx,
2376        ctx.key,
2377        operation,
2378        &resolver,
2379        signature_node,
2380        &(),
2381    )
2382}
2383
2384fn verify_signature_node<'a, C: VerificationCapture>(
2385    view: DocumentView<'a>,
2386    ctx: &VerifyContext<'_>,
2387    key: Option<&dyn VerifyingKey>,
2388    operation: &mut OperationExecutionContext<
2389        &crate::policy::VerificationPolicy,
2390        &VerificationOperationBudgets,
2391    >,
2392    resolver: &UriReferenceResolver<'a>,
2393    signature_node: Node<'a, 'a>,
2394    capture: &C,
2395) -> Result<VerifyResult, DsigError> {
2396    if ctx.policy.resources.max_signatures == 0 {
2397        return Err(crate::policy::PolicyViolation::ResourceLimit {
2398            resource: crate::policy::resource_name::VERIFICATION_SIGNATURES,
2399            maximum: 0,
2400            actual: 1,
2401        }
2402        .into());
2403    }
2404    let doc = view.document();
2405    let signature_children = parse_signature_children(signature_node)?;
2406    ctx.policy
2407        .signature_constraints
2408        .check(signature_node, &operation.budgets().transforms)?;
2409    if ctx.decapsulation_key.is_some() {
2410        // Public-key encapsulation establishes a recipient secret, not sender
2411        // identity. Reject an impossible trust requirement before key work.
2412        if ctx.policy.key_trust.mode != crate::policy::VerificationTrustMode::CryptographicOnly {
2413            return Err(crate::policy::PolicyViolation::KeyTrust {
2414                reason: "KEM/HMAC does not authenticate sender identity",
2415            }
2416            .into());
2417        }
2418    }
2419    let signed_info_node = signature_children.signed_info_node;
2420    let should_parse_key_info = ctx.decapsulation_key.is_none()
2421        && match (key, ctx.key_resolver) {
2422            (Some(_), _) => false,
2423            (None, Some(resolver)) => resolver.consumes_document_key_info(),
2424            (None, None) => true,
2425        };
2426    let mut key_info = if should_parse_key_info {
2427        signature_children
2428            .key_info_node
2429            .map(|node| {
2430                super::parse::validate_x509_digest_policy(node, &|algorithm| {
2431                    ctx.policy.check_digest_algorithm(algorithm)
2432                })?;
2433                super::parse::parse_key_info_in_session(
2434                    node,
2435                    ctx.provider,
2436                    operation.budgets().transforms.xml_base_resolution(),
2437                    &ctx.policy.resources,
2438                    None,
2439                    ctx.policy
2440                        .key_trust
2441                        .certificate_signature_algorithms
2442                        .as_ref(),
2443                    &mut operation.budgets().key_info_parse.borrow_mut(),
2444                )
2445            })
2446            .transpose()
2447            .map_err(map_key_info_parse_error)?
2448    } else {
2449        None
2450    };
2451
2452    let signed_info = parse_signed_info_with_xpath_budget(
2453        signed_info_node,
2454        &mut operation.budgets().xpath_parse.borrow_mut(),
2455        &ctx.policy.resources,
2456    )?;
2457    capture.method(view, signed_info_node, signed_info.signature_method)?;
2458    if signed_info.references.len() > ctx.policy.resources.max_references {
2459        return Err(crate::policy::PolicyViolation::ResourceLimit {
2460            resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
2461            maximum: ctx.policy.resources.max_references,
2462            actual: signed_info.references.len(),
2463        }
2464        .into());
2465    }
2466    for reference in &signed_info.references {
2467        if reference.transforms.len() > ctx.policy.resources.max_transforms_per_reference {
2468            return Err(crate::policy::PolicyViolation::ResourceLimit {
2469                resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
2470                maximum: ctx.policy.resources.max_transforms_per_reference,
2471                actual: reference.transforms.len(),
2472            }
2473            .into());
2474        }
2475    }
2476    ctx.policy
2477        .check_signature_algorithm(signed_info.signature_method)?;
2478    // Refuse an inadmissible authenticator before reference resolution/transforms.
2479    // XMLDSig 1.1 section 4.4.2 explicitly requires max(80, hash_bits/2),
2480    // independently of data; section 6.3.1 separately requires octet alignment.
2481    // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-SignatureMethod
2482    if let Some(full_output_bits) = signed_info.signature_method.hmac_output_bits() {
2483        ctx.policy.hmac.validate_output(
2484            signed_info.signature_method,
2485            signed_info
2486                .hmac_output_length_bits
2487                .unwrap_or(full_output_bits),
2488        )?;
2489    }
2490    for reference in &signed_info.references {
2491        ctx.policy.check_digest_algorithm(reference.digest_method)?;
2492        super::transforms::validate_relationship_chain(
2493            &reference.transforms,
2494            ctx.policy.transforms.opc_relationship_edition,
2495        )
2496        .map_err(ReferenceProcessingError::Transform)?;
2497    }
2498    enforce_reference_policies(
2499        &signed_info.references,
2500        ctx.policy.uris.references,
2501        ctx.allowed_transform_uris(),
2502    )?;
2503    enforce_transform_allowed(ctx.allowed_transform_uris(), signed_info.c14n_method.uri())?;
2504
2505    if let Some(resources) = ctx.external_resources {
2506        validate_external_resource_map(
2507            resources,
2508            ctx.policy.resources.max_external_resource_bytes,
2509            ctx.policy.resources.max_external_resource_total_bytes,
2510        )
2511        .map_err(|error| match error {
2512            ExternalResourceMapError::Policy(error) => error.into(),
2513            ExternalResourceMapError::TotalLengthOverflow => {
2514                SignatureVerificationPipelineError::InvalidStructure {
2515                    reason: "external resource total length overflow",
2516                }
2517            }
2518        })?;
2519    }
2520    let remaining_reference_capacity = ctx
2521        .policy
2522        .resources
2523        .max_references
2524        .checked_sub(signed_info.references.len())
2525        .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2526            reason: "SignedInfo exceeds the per-signature Reference limit",
2527        })?;
2528    let plan_nodes = compile_verification_operation_plan(
2529        operation,
2530        view,
2531        signature_node,
2532        &signed_info.references,
2533        resolver,
2534    )?;
2535    let signature_identity =
2536        OperationResourceIdentity::DocumentNode(view.node_identity(signature_node));
2537    operation.run_with_resource(plan_nodes.document, &signature_identity, || {
2538        operation
2539            .validate_document_view(view)
2540            .map_err(map_verification_plan_error)
2541    })?;
2542    let observed_key_resources = operation.budgets().external_resource_set_identity(resolver);
2543    let retrieval_materialization = operation.run_with_resource(
2544        plan_nodes.key_materialization,
2545        &observed_key_resources,
2546        || {
2547            let budgets = operation.budgets();
2548            let mut materialization = budgets.key_info_materialization.borrow_mut();
2549            materialization.collect_encapsulation = true;
2550            materialization.encapsulation = None;
2551            if let Some(node) = signature_children.key_info_node {
2552                // A pinned key ignores lookup hints, not mechanism instructions.
2553                // Apply the same bounded reference discovery before key work.
2554                if ctx.decapsulation_key.is_some() {
2555                    discover_encapsulation_with_budget(
2556                        node,
2557                        resolver,
2558                        &ctx.policy.resources,
2559                        &budgets.transforms,
2560                        Some((&ctx.policy, ctx.xml_backend)),
2561                        Some(&mut materialization),
2562                    )?;
2563                } else if !should_parse_key_info {
2564                    if has_encapsulation_with_budget(
2565                        node,
2566                        resolver,
2567                        &ctx.policy.resources,
2568                        &budgets.transforms,
2569                        Some((&ctx.policy, ctx.xml_backend)),
2570                    )? {
2571                        return Err(crate::key_establishment::KeyEstablishmentError::Structure(
2572                            "mechanism and explicit recipient key must be supplied together",
2573                        )
2574                        .into());
2575                    }
2576                } else {
2577                    materialization.observe_encapsulation(node, resolver, 0)?;
2578                }
2579            }
2580            if let Some(info) = key_info.as_mut() {
2581                let mut xpath_parse = budgets.xpath_parse.borrow_mut();
2582                let mut retrieval_budgets = RetrievalMaterializationBudgets {
2583                    key_info_parse: &budgets.key_info_parse,
2584                    xpath_parse: &mut xpath_parse,
2585                    execution: &budgets.transforms,
2586                    resources: &ctx.policy.resources,
2587                    certificate_signature_algorithms: ctx
2588                        .policy
2589                        .key_trust
2590                        .certificate_signature_algorithms
2591                        .as_ref(),
2592                    xml_backend: ctx.xml_backend,
2593                };
2594                let mut outcome = materialize_key_info_references_with_budgets(
2595                    info,
2596                    resolver,
2597                    &ctx.policy,
2598                    ctx.provider,
2599                    &mut retrieval_budgets,
2600                    &mut materialization,
2601                )?;
2602                outcome.merge(materialize_retrieval_methods_with_budgets(
2603                    info,
2604                    resolver,
2605                    &ctx.policy,
2606                    ctx.provider,
2607                    &mut retrieval_budgets,
2608                    &mut materialization.candidate_work,
2609                )?);
2610                Ok::<_, SignatureVerificationPipelineError>(outcome)
2611            } else {
2612                Ok(RetrievalMaterialization::default())
2613            }
2614        },
2615    )?;
2616    let encapsulation = operation
2617        .budgets()
2618        .key_info_materialization
2619        .borrow_mut()
2620        .encapsulation
2621        .take();
2622    if encapsulation.is_some() != ctx.decapsulation_key.is_some() {
2623        return Err(crate::key_establishment::KeyEstablishmentError::Structure(
2624            "mechanism and explicit recipient key must be supplied together",
2625        )
2626        .into());
2627    }
2628    if let Some(mechanism) = &encapsulation {
2629        ctx.policy
2630            .key_establishment
2631            .check_encapsulation(mechanism.algorithm)?;
2632        if ctx
2633            .decapsulation_key
2634            .is_some_and(|key| key.algorithm() != mechanism.algorithm)
2635        {
2636            return Err(crate::key_establishment::KeyEstablishmentError::Structure(
2637                "recipient parameter set mismatch",
2638            )
2639            .into());
2640        }
2641    }
2642    let mut reference_results = Vec::with_capacity(signed_info.references.len());
2643    let mut first_failure = None;
2644    for (index, (node, reference)) in plan_nodes
2645        .digests
2646        .iter()
2647        .copied()
2648        .zip(&signed_info.references)
2649        .enumerate()
2650    {
2651        let observed = operation.budgets().resource_identity_for_reference(
2652            reference,
2653            index,
2654            resolver,
2655            view,
2656            reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2657        );
2658        let mut result = operation.run_with_resource(node, &observed, || {
2659            let budgets = operation.budgets();
2660            let execution = ReferenceExecutionContext {
2661                store_pre_digest: ctx.store_pre_digest,
2662                transform_options: ctx.transform_options(),
2663                transform_budget: &budgets.transforms,
2664                canonicalized_data_budget: &budgets.canonicalized,
2665                provider: ctx.provider,
2666                capture,
2667            };
2668            process_reference_with_options(
2669                reference,
2670                resolver,
2671                signature_node,
2672                ReferenceSet::SignedInfo,
2673                index,
2674                reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2675                &execution,
2676            )
2677            .map_err(SignatureVerificationPipelineError::from)
2678        })?;
2679        bind_reference_evidence(
2680            &mut result,
2681            reference,
2682            view,
2683            reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2684            &observed,
2685        );
2686        let accepted = result.status == DsigStatus::Valid;
2687        operation.set_outcome(
2688            node,
2689            accepted,
2690            if accepted {
2691                OperationDecisionReason::ReferenceDigestVerified
2692            } else {
2693                OperationDecisionReason::ReferenceDigestRejected
2694            },
2695        );
2696        if result.status == DsigStatus::Valid
2697            && let Some(reference) = signed_info.references.get(result.reference_index)
2698            && reference
2699                .transforms
2700                .iter()
2701                .all(transform_preserves_manifest_structure)
2702            && let Some(uri) = reference.uri.as_deref()
2703            && let Ok(Some(target)) = resolver.node_id_for_same_document_reference(uri)
2704        {
2705            let identity = view.node_identity_by_id(target);
2706            operation.authenticate(identity);
2707            debug_assert!(operation.is_authenticated(identity));
2708        }
2709        reference_results.push(result);
2710        if !accepted {
2711            first_failure = Some(index);
2712            break;
2713        }
2714    }
2715    debug_assert!(
2716        plan_nodes.digests[reference_results.len()..]
2717            .iter()
2718            .all(|node| !operation.is_executed(*node))
2719    );
2720    let references = ReferencesResult {
2721        results: reference_results,
2722        first_failure,
2723    };
2724
2725    if let Some(first_failure) = references.first_failure {
2726        debug_assert!(operation.first_failure().is_some());
2727        let status = references.results[first_failure].status;
2728        return Ok(VerifyResult {
2729            key_trust: super::KeyTrustEvidence::NotEvaluated,
2730            status,
2731            signed_info_references: references.results,
2732            manifest_references: Vec::new(),
2733            canonicalized_signed_info: None,
2734        });
2735    }
2736
2737    let canonical_signed_info = operation.run(plan_nodes.canonicalization, || {
2738        let signed_info_subtree: HashSet<_> = signed_info_node
2739            .descendants()
2740            .map(|node: Node<'_, '_>| node.id())
2741            .collect();
2742        let mut canonical_signed_info = Vec::new();
2743        let signed_info_limit = operation
2744            .budgets()
2745            .canonicalized
2746            .remaining()
2747            .min(operation.budgets().transforms.remaining_c14n_output());
2748        canonicalize_bounded_with_xml_base_budget(
2749            doc,
2750            Some(&|node| signed_info_subtree.contains(&node.id())),
2751            &signed_info.c14n_method,
2752            signed_info_limit,
2753            operation.budgets().transforms.xml_base_resolution(),
2754            &mut canonical_signed_info,
2755        )
2756        .map_err(|error| {
2757            if let Some(violation) = map_c14n_resource_policy_violation(
2758                &error,
2759                crate::policy::resource_name::CANONICALIZED_BYTES,
2760                operation.budgets().canonicalized.max_bytes,
2761            ) {
2762                SignatureVerificationPipelineError::Policy(violation)
2763            } else {
2764                SignatureVerificationPipelineError::Canonicalization(error)
2765            }
2766        })?;
2767        operation
2768            .budgets()
2769            .transforms
2770            .charge_c14n_output(canonical_signed_info.len())
2771            .map_err(ReferenceProcessingError::Transform)?;
2772        operation
2773            .budgets()
2774            .canonicalized
2775            .charge(canonical_signed_info.len())?;
2776        Ok::<_, SignatureVerificationPipelineError>(canonical_signed_info)
2777    })?;
2778
2779    let signature_value = decode_signature_value(
2780        signature_children.signature_value_node,
2781        signed_info.signature_method,
2782        &ctx.policy.resources,
2783    )?;
2784    if let Some(full_output_bits) = signed_info.signature_method.hmac_output_bits() {
2785        let expected_bits = signed_info
2786            .hmac_output_length_bits
2787            .unwrap_or(full_output_bits);
2788        if signature_value.len() != expected_bits / 8 {
2789            return Err(SignatureVerificationPipelineError::InvalidStructure {
2790                reason: "SignatureValue length does not match HMACOutputLength",
2791            });
2792        }
2793    }
2794    let resolved_key = operation.run(plan_nodes.key, || {
2795        if let Some(recipient) = ctx.decapsulation_key {
2796            if key.is_some() || ctx.key_resolver.is_some() {
2797                return Err(crate::key_establishment::KeyEstablishmentError::Structure(
2798                    "ambiguous verification key request",
2799                )
2800                .into());
2801            }
2802            require_verifying_key_candidate_capacity(&ctx.policy)?;
2803            if !ctx.policy.key_sources.preset_key
2804                || signed_info.signature_method.hmac_output_bits().is_none()
2805            {
2806                return Err(crate::key_establishment::KeyEstablishmentError::Structure(
2807                    "encapsulation requires permitted explicit key and HMAC",
2808                )
2809                .into());
2810            }
2811            ctx.policy.hmac.validate_key_bits(256)?;
2812            let mechanism = encapsulation.as_ref().ok_or(
2813                crate::key_establishment::KeyEstablishmentError::Structure("missing mechanism"),
2814            )?;
2815            operation.budgets().key_candidates.borrow_mut().charge()?;
2816            operation
2817                .budgets()
2818                .key_establishment
2819                .borrow_mut()
2820                .commit_all(&ctx.policy.key_establishment, 0, 0, 32)?;
2821            let secret = operation
2822                .budgets()
2823                .key_establishment
2824                .borrow_mut()
2825                .decapsulate(
2826                    &ctx.policy.key_establishment,
2827                    ctx.provider,
2828                    recipient,
2829                    mechanism.algorithm,
2830                    &mechanism.ciphertext[..mechanism.algorithm.ciphertext_len()],
2831                )?;
2832            let hmac = super::HmacVerificationKey::new(secret.to_vec())?;
2833            return Ok(Some(ResolvedVerifyingKey::Established(hmac)));
2834        }
2835        resolve_verifying_key(
2836            ctx,
2837            key,
2838            key_info.as_ref(),
2839            signed_info.signature_method,
2840            &mut operation.budgets().key_candidates.borrow_mut(),
2841        )
2842    })?;
2843    let Some(resolved_key) = resolved_key else {
2844        operation.set_outcome(
2845            plan_nodes.key,
2846            false,
2847            OperationDecisionReason::KeyUnavailable,
2848        );
2849        if let Some(error) = retrieval_materialization.deferred_error {
2850            return Err(error);
2851        }
2852        return Ok(VerifyResult {
2853            key_trust: super::KeyTrustEvidence::NotEvaluated,
2854            status: DsigStatus::Invalid(FailureReason::KeyNotFound),
2855            signed_info_references: references.results,
2856            manifest_references: Vec::new(),
2857            canonicalized_signed_info: if ctx.store_pre_digest {
2858                Some(canonical_signed_info)
2859            } else {
2860                None
2861            },
2862        });
2863    };
2864    operation.set_outcome(plan_nodes.key, true, OperationDecisionReason::KeyResolved);
2865    let verifier = resolved_key.as_ref();
2866    let signature_valid = operation.run(plan_nodes.crypto, || {
2867        verifier.validate_policy(&ctx.policy)?;
2868        if !verifier.validate_signature_value_with_policy(
2869            &ctx.policy,
2870            signed_info.signature_method,
2871            &signature_value,
2872        )? {
2873            return Ok::<_, SignatureVerificationPipelineError>(false);
2874        }
2875        ctx.provider
2876            .require_capability(crate::provider::ProviderCapability::Verify(
2877                signed_info.signature_method,
2878            ))?;
2879        let candidate_refusal = RefCell::new(None);
2880        let policy_verifier = PolicyVerifyingKey {
2881            key: verifier,
2882            policy: &ctx.policy,
2883            candidates: &operation.budgets().key_candidates,
2884            candidate_refusal: &candidate_refusal,
2885        };
2886        let result = capture.verify(
2887            ctx.provider,
2888            &policy_verifier,
2889            crate::provider::SignatureVerificationInput {
2890                algorithm: signed_info.signature_method,
2891                context: &signed_info.signature_context,
2892                data: &canonical_signed_info,
2893                signature: &signature_value,
2894            },
2895        );
2896        if let Some(error) = candidate_refusal.into_inner() {
2897            return Err(error.into());
2898        }
2899        result
2900    })?;
2901
2902    if !signature_valid {
2903        operation.set_outcome(
2904            plan_nodes.crypto,
2905            false,
2906            OperationDecisionReason::SignatureRejected,
2907        );
2908        return Ok(VerifyResult {
2909            key_trust: resolved_key.evidence(&ctx.policy)?,
2910            status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
2911            signed_info_references: references.results,
2912            manifest_references: Vec::new(),
2913            canonicalized_signed_info: if ctx.store_pre_digest {
2914                Some(canonical_signed_info)
2915            } else {
2916                None
2917            },
2918        });
2919    }
2920    operation.set_outcome(
2921        plan_nodes.crypto,
2922        true,
2923        OperationDecisionReason::SignatureVerified,
2924    );
2925    // Manifest syntax and references are authenticated content. Delay parsing
2926    // until SignatureValue succeeds, then extend the same operation graph with
2927    // the newly trusted dependency subgraph and its evidence node.
2928    let (manifest_references, manifest_digests) =
2929        if ctx.policy.manifest_processing == crate::policy::ManifestProcessing::Process {
2930            process_authenticated_manifest_references(
2931                operation,
2932                view,
2933                signature_node,
2934                resolver,
2935                ManifestVerification {
2936                    context: ctx,
2937                    capture,
2938                },
2939                remaining_reference_capacity,
2940                plan_nodes.crypto,
2941            )?
2942        } else {
2943            (Vec::new(), Vec::new())
2944        };
2945    operation.extend();
2946    let evidence = operation.add_node(OperationNodeKind::Evidence, OperationStage::Evidence, None);
2947    operation
2948        .add_dependency(evidence, plan_nodes.crypto)
2949        .map_err(map_verification_plan_error)?;
2950    for digest in manifest_digests {
2951        operation
2952            .add_dependency(evidence, digest)
2953            .map_err(map_verification_plan_error)?;
2954    }
2955    operation.compile().map_err(map_verification_plan_error)?;
2956    operation.run(evidence, || {
2957        Ok::<_, SignatureVerificationPipelineError>(VerifyResult {
2958            key_trust: resolved_key.evidence(&ctx.policy)?,
2959            status: DsigStatus::Valid,
2960            signed_info_references: references.results,
2961            manifest_references,
2962            canonicalized_signed_info: if ctx.store_pre_digest {
2963                Some(canonical_signed_info)
2964            } else {
2965                None
2966            },
2967        })
2968    })
2969}
2970
2971struct PolicyVerifyingKey<'a, 'inputs> {
2972    key: &'a dyn VerifyingKey,
2973    policy: &'a crate::policy::VerificationPolicy,
2974    candidates: &'a RefCell<super::InspectedKeyCandidateBudget<'inputs>>,
2975    candidate_refusal: &'a RefCell<Option<crate::policy::PolicyViolation>>,
2976}
2977
2978impl VerifyingKey for PolicyVerifyingKey<'_, '_> {
2979    fn association_spki(&self, algorithm: SignatureAlgorithm) -> Option<&[u8]> {
2980        self.key.association_spki(algorithm)
2981    }
2982    fn provider_binding(&self) -> Option<&crate::provider::ProviderBinding> {
2983        self.key.provider_binding()
2984    }
2985    fn verify_candidate_keys(
2986        &self,
2987        verify: &mut dyn FnMut(&dyn VerifyingKey) -> Result<bool, DsigError>,
2988    ) -> Result<Option<bool>, DsigError> {
2989        self.key.verify_candidate_keys(&mut |key| {
2990            if let Some(error) = self.candidate_refusal.borrow().as_ref() {
2991                return Err(error.clone().into());
2992            }
2993            // The resolver already admitted this handle. Every child exposed
2994            // by it is another attempt in that same operation-wide budget.
2995            if let Err(error) = self.candidates.borrow_mut().charge_policy(1) {
2996                *self.candidate_refusal.borrow_mut() = Some(error.clone());
2997                return Err(error.into());
2998            }
2999            key.validate_policy(self.policy)?;
3000            verify(&PolicyVerifyingKey {
3001                key,
3002                policy: self.policy,
3003                candidates: self.candidates,
3004                candidate_refusal: self.candidate_refusal,
3005            })
3006        })
3007    }
3008    fn verification_spki(&self, algorithm: SignatureAlgorithm) -> Result<Option<&[u8]>, DsigError> {
3009        self.key.verification_spki(algorithm)
3010    }
3011
3012    fn ecdsa_encoding(&self) -> crate::policy::EcdsaSignatureValueEncoding {
3013        self.policy.ecdsa_signature_value_encoding
3014    }
3015    fn verify_with_context(
3016        &self,
3017        algorithm: SignatureAlgorithm,
3018        context: &super::SignatureContext,
3019        data: &[u8],
3020        signature: &[u8],
3021    ) -> Result<bool, DsigError> {
3022        if context.as_bytes().is_empty() {
3023            self.verify(algorithm, data, signature)
3024        } else {
3025            self.key
3026                .verify_with_context(algorithm, context, data, signature)
3027        }
3028    }
3029    fn validate_policy(&self, policy: &crate::policy::VerificationPolicy) -> Result<(), DsigError> {
3030        self.key.validate_policy(policy)
3031    }
3032
3033    fn validate_signature_value(
3034        &self,
3035        algorithm: SignatureAlgorithm,
3036        signature_value: &[u8],
3037    ) -> Result<bool, DsigError> {
3038        self.key
3039            .validate_signature_value_with_policy(self.policy, algorithm, signature_value)
3040    }
3041
3042    fn verify(
3043        &self,
3044        algorithm: SignatureAlgorithm,
3045        signed_data: &[u8],
3046        signature_value: &[u8],
3047    ) -> Result<bool, DsigError> {
3048        self.key
3049            .verify_with_policy(self.policy, algorithm, signed_data, signature_value)
3050    }
3051}
3052
3053#[derive(Debug, Default)]
3054struct RetrievalMaterialization {
3055    deferred_error: Option<SignatureVerificationPipelineError>,
3056}
3057
3058impl RetrievalMaterialization {
3059    fn merge(&mut self, other: Self) {
3060        if self.deferred_error.is_none() {
3061            self.deferred_error = other.deferred_error;
3062        }
3063    }
3064}
3065
3066struct RetrievalMaterializationBudgets<'a> {
3067    key_info_parse: &'a RefCell<super::parse::KeyInfoParseUsage>,
3068    certificate_signature_algorithms: Option<&'a crate::policy::CertificateSignatureAlgorithms>,
3069    xpath_parse: &'a mut XPathSignatureParseBudget,
3070    execution: &'a TransformExecutionBudget,
3071    resources: &'a crate::policy::ResourcePolicy,
3072    xml_backend: crate::XmlBackend,
3073}
3074
3075#[derive(Default)]
3076struct KeyInfoMaterializationState {
3077    active: HashSet<(super::uri::TraversalDocumentIdentity, String)>,
3078    candidate_work: usize,
3079    collect_encapsulation: bool,
3080    encapsulation_template: bool,
3081    retain_encapsulation_target: bool,
3082    encapsulation_depth: usize,
3083    reference_depth_base: usize,
3084    encapsulation_target: Option<(super::uri::TraversalDocumentIdentity, NodeId, NodeId)>,
3085    encapsulation: Option<ResolvedEncapsulation>,
3086}
3087
3088struct ResolvedEncapsulation {
3089    algorithm: crate::provider::KeyEncapsulationAlgorithm,
3090    ciphertext: [u8; 1568],
3091}
3092
3093impl KeyInfoMaterializationState {
3094    fn observe_encapsulation(
3095        &mut self,
3096        node: Node<'_, '_>,
3097        resolver: &UriReferenceResolver<'_>,
3098        depth: usize,
3099    ) -> Result<(), SignatureVerificationPipelineError> {
3100        if !self.collect_encapsulation {
3101            return Ok(());
3102        }
3103        // XMLDSig 1.1 §4.5.10 references KeyInfo, including its extension
3104        // children. Collect metadata in the gated reference traversal, not a
3105        // separate unbounded scan; external document nodes cannot escape it.
3106        // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-KeyInfoReference
3107        let Some(mechanism) =
3108            crate::key_establishment::direct_encapsulation(node, self.encapsulation_template)
3109                .map_err(|_| SignatureVerificationPipelineError::InvalidStructure {
3110                    reason: "invalid encapsulation mechanism in resolved KeyInfo",
3111                })?
3112        else {
3113            return Ok(());
3114        };
3115        self.record_encapsulation(mechanism, resolver, depth)
3116    }
3117
3118    fn record_encapsulation(
3119        &mut self,
3120        mechanism: crate::key_establishment::ParsedEncapsulation<'_, '_>,
3121        resolver: &UriReferenceResolver<'_>,
3122        depth: usize,
3123    ) -> Result<(), SignatureVerificationPipelineError> {
3124        if self.encapsulation.is_some() {
3125            return Err(SignatureVerificationPipelineError::InvalidStructure {
3126                reason: "multiple encapsulation mechanisms in resolved KeyInfo",
3127            });
3128        }
3129        if self.retain_encapsulation_target {
3130            self.encapsulation_depth = depth;
3131            self.encapsulation_target = Some((
3132                resolver.traversal_document_identity(),
3133                mechanism.cipher_value.id(),
3134                mechanism.key_info.id(),
3135            ));
3136        }
3137        self.encapsulation = Some(ResolvedEncapsulation {
3138            algorithm: mechanism.algorithm,
3139            ciphertext: mechanism.into_ciphertext(),
3140        });
3141        Ok(())
3142    }
3143}
3144
3145trait KeyInfoReferencePolicy {
3146    fn key_establishment(&self) -> &crate::policy::KeyEstablishmentPolicy;
3147    fn check_digest_algorithm(
3148        &self,
3149        algorithm: DigestAlgorithm,
3150    ) -> Result<(), crate::policy::PolicyViolation>;
3151    fn certificate_signature_algorithms(
3152        &self,
3153    ) -> Option<&crate::policy::CertificateSignatureAlgorithms>;
3154    fn resources(&self) -> &crate::policy::ResourcePolicy;
3155    fn xml(&self) -> &crate::policy::XmlInputPolicy;
3156    fn key_info_reference_uris(&self) -> UriTypeSet;
3157    fn retrieval_method_uris(&self) -> UriTypeSet;
3158    fn key_info_reference_source_enabled(&self) -> bool;
3159    fn allowed_transforms(&self) -> Option<&HashSet<String>>;
3160}
3161
3162impl KeyInfoReferencePolicy for crate::policy::SigningPolicy {
3163    fn key_establishment(&self) -> &crate::policy::KeyEstablishmentPolicy {
3164        &self.key_establishment
3165    }
3166    fn check_digest_algorithm(
3167        &self,
3168        algorithm: DigestAlgorithm,
3169    ) -> Result<(), crate::policy::PolicyViolation> {
3170        self.check_digest_algorithm(algorithm)
3171    }
3172    fn certificate_signature_algorithms(
3173        &self,
3174    ) -> Option<&crate::policy::CertificateSignatureAlgorithms> {
3175        None
3176    }
3177    fn resources(&self) -> &crate::policy::ResourcePolicy {
3178        &self.resources
3179    }
3180
3181    fn xml(&self) -> &crate::policy::XmlInputPolicy {
3182        &self.xml
3183    }
3184
3185    fn key_info_reference_uris(&self) -> UriTypeSet {
3186        self.uris.key_info_references
3187    }
3188
3189    fn retrieval_method_uris(&self) -> UriTypeSet {
3190        self.uris.retrieval_methods
3191    }
3192
3193    fn key_info_reference_source_enabled(&self) -> bool {
3194        true
3195    }
3196
3197    fn allowed_transforms(&self) -> Option<&HashSet<String>> {
3198        self.transforms.allowed_algorithms.as_ref()
3199    }
3200}
3201
3202impl KeyInfoReferencePolicy for crate::policy::VerificationPolicy {
3203    fn key_establishment(&self) -> &crate::policy::KeyEstablishmentPolicy {
3204        &self.key_establishment
3205    }
3206    fn check_digest_algorithm(
3207        &self,
3208        algorithm: DigestAlgorithm,
3209    ) -> Result<(), crate::policy::PolicyViolation> {
3210        self.check_digest_algorithm(algorithm)
3211    }
3212    fn certificate_signature_algorithms(
3213        &self,
3214    ) -> Option<&crate::policy::CertificateSignatureAlgorithms> {
3215        self.key_trust.certificate_signature_algorithms.as_ref()
3216    }
3217    fn resources(&self) -> &crate::policy::ResourcePolicy {
3218        &self.resources
3219    }
3220
3221    fn xml(&self) -> &crate::policy::XmlInputPolicy {
3222        &self.xml
3223    }
3224
3225    fn key_info_reference_uris(&self) -> UriTypeSet {
3226        self.uris.key_info_references
3227    }
3228
3229    fn retrieval_method_uris(&self) -> UriTypeSet {
3230        self.uris.retrieval_methods
3231    }
3232
3233    fn key_info_reference_source_enabled(&self) -> bool {
3234        self.key_sources.key_info_reference
3235    }
3236
3237    fn allowed_transforms(&self) -> Option<&HashSet<String>> {
3238        self.transforms.allowed_algorithms.as_ref()
3239    }
3240}
3241
3242struct KeyInfoReferenceMaterializationContext<'a, 'budget, P> {
3243    policy: &'a P,
3244    provider: &'a dyn crate::provider::CryptoProvider,
3245    budgets: &'a mut RetrievalMaterializationBudgets<'budget>,
3246}
3247
3248fn materialize_key_info_references_with_budgets<P: KeyInfoReferencePolicy>(
3249    key_info: &mut KeyInfo,
3250    resolver: &UriReferenceResolver<'_>,
3251    policy: &P,
3252    provider: &dyn crate::provider::CryptoProvider,
3253    budgets: &mut RetrievalMaterializationBudgets<'_>,
3254    materialization: &mut KeyInfoMaterializationState,
3255) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
3256    fn visit<P: KeyInfoReferencePolicy>(
3257        key_info: &mut KeyInfo,
3258        resolver: &UriReferenceResolver<'_>,
3259        context: &mut KeyInfoReferenceMaterializationContext<'_, '_, P>,
3260        materialization: &mut KeyInfoMaterializationState,
3261        depth: usize,
3262    ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
3263        let mut materialized = Vec::new();
3264        let mut outcome = RetrievalMaterialization::default();
3265        for source in std::mem::take(&mut key_info.sources) {
3266            let source_work = match &source {
3267                super::parse::KeyInfoSource::X509Data(info) => info.certificates.len().max(1),
3268                _ => 1,
3269            };
3270            materialization.candidate_work =
3271                materialization.candidate_work.saturating_add(source_work);
3272            context
3273                .policy
3274                .resources()
3275                .validate_key_candidates(materialization.candidate_work)?;
3276
3277            let super::parse::KeyInfoSource::KeyInfoReference { uri } = source else {
3278                materialized.push(source);
3279                continue;
3280            };
3281            if !context.policy.key_info_reference_source_enabled() {
3282                return Err(crate::policy::PolicyViolation::KeyTrust {
3283                    reason: "KeyInfoReference key sources are disabled",
3284                }
3285                .into());
3286            }
3287            if !context.policy.key_info_reference_uris().allows(&uri) {
3288                return Err(crate::policy::PolicyViolation::Uri {
3289                    operation: "KeyInfoReference",
3290                    reason: "URI class is disabled",
3291                }
3292                .into());
3293            }
3294            let next_depth = depth.saturating_add(1);
3295            context
3296                .policy
3297                .resources()
3298                .validate_key_info_reference_depth(next_depth)?;
3299            let cycle_key = (resolver.traversal_document_identity(), uri.clone());
3300            if !materialization.active.insert(cycle_key.clone()) {
3301                return Err(SignatureVerificationPipelineError::InvalidStructure {
3302                    reason: "KeyInfoReference cycle detected",
3303                });
3304            }
3305
3306            let is_same_document = uri.is_empty() || uri.starts_with('#');
3307            let (mut referenced, mut nested_outcome) = if is_same_document {
3308                let node = resolver
3309                    .node_for_same_document_reference(&uri)
3310                    .map_err(|error| {
3311                        SignatureVerificationPipelineError::from(
3312                            ReferenceProcessingError::UriDereference(error),
3313                        )
3314                    })?
3315                    .ok_or(SignatureVerificationPipelineError::InvalidStructure {
3316                        reason: "KeyInfoReference target is missing or ambiguous",
3317                    })?;
3318                if !node.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
3319                    return Err(SignatureVerificationPipelineError::InvalidStructure {
3320                        reason: "KeyInfoReference target must be KeyInfo",
3321                    });
3322                }
3323                materialization.observe_encapsulation(node, resolver, next_depth)?;
3324                if let Some(mechanism) = &materialization.encapsulation {
3325                    context
3326                        .policy
3327                        .key_establishment()
3328                        .check_encapsulation(mechanism.algorithm)?;
3329                }
3330                super::parse::validate_x509_digest_policy(node, &|algorithm| {
3331                    context.policy.check_digest_algorithm(algorithm)
3332                })
3333                .map_err(map_key_info_parse_error)?;
3334                (
3335                    super::parse::parse_key_info_in_session(
3336                        node,
3337                        context.provider,
3338                        context.budgets.execution.xml_base_resolution(),
3339                        context.policy.resources(),
3340                        None,
3341                        context.policy.certificate_signature_algorithms(),
3342                        &mut context.budgets.key_info_parse.borrow_mut(),
3343                    )
3344                    .map_err(map_key_info_parse_error)?,
3345                    RetrievalMaterialization::default(),
3346                )
3347            } else {
3348                // The fragment selects a node inside the retrieved document; it
3349                // is not part of either the caller-owned resource identity or
3350                // the base URI used by references nested in that document.
3351                let (resource_uri, fragment) = uri
3352                    .split_once('#')
3353                    .map_or((uri.as_str(), None), |(resource, fragment)| {
3354                        (resource, (!fragment.is_empty()).then_some(fragment))
3355                    });
3356                let bytes = resolver
3357                    .external_resource(resource_uri)
3358                    .map_err(|error| {
3359                        SignatureVerificationPipelineError::from(
3360                            ReferenceProcessingError::UriDereference(error),
3361                        )
3362                    })?
3363                    .ok_or(SignatureVerificationPipelineError::InvalidStructure {
3364                        reason: "KeyInfoReference external resource is unavailable",
3365                    })?;
3366                // External KeyInfo bytes share the operation's work allowance even if decoding
3367                // fails before a document exists. Parsing the decoded text is a separate pass.
3368                context
3369                    .budgets
3370                    .execution
3371                    .xml_parse_work()
3372                    .charge_policy(bytes.len())?;
3373                let xml = crate::encoding::decode_xml_octets(
3374                    bytes,
3375                    context.policy.resources().max_xml_document_bytes,
3376                )
3377                .map_err(map_key_info_xml_decode_error)?;
3378                let settings = DocumentParseSettings::from_policy(
3379                    context.policy.xml(),
3380                    context.policy.resources(),
3381                )
3382                .with_backend(context.budgets.xml_backend);
3383                let document = XmlDocument::parse_with_settings_and_budget(
3384                    xml.into_owned(),
3385                    settings,
3386                    context.budgets.execution.xml_parse_work(),
3387                )
3388                .map_err(|error| map_document_parse_error(error, settings))?;
3389                document.with_view(|view| {
3390                    let external_resolver = resolver.for_external_document_view(view, resource_uri);
3391                    let target = match fragment {
3392                        Some(fragment) => external_resolver
3393                            .node_for_same_document_reference(&format!("#{fragment}"))
3394                            .map_err(|error| {
3395                                SignatureVerificationPipelineError::from(
3396                                    ReferenceProcessingError::UriDereference(error),
3397                                )
3398                            })?
3399                            .ok_or(SignatureVerificationPipelineError::InvalidStructure {
3400                                reason: "KeyInfoReference external target is missing or ambiguous",
3401                            })?,
3402                        _ => view.document().root_element(),
3403                    };
3404                    if !target.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
3405                        return Err(SignatureVerificationPipelineError::InvalidStructure {
3406                            reason: "KeyInfoReference external target must be KeyInfo",
3407                        });
3408                    }
3409                    materialization.observe_encapsulation(
3410                        target,
3411                        &external_resolver,
3412                        next_depth,
3413                    )?;
3414                    if let Some(mechanism) = &materialization.encapsulation {
3415                        context
3416                            .policy
3417                            .key_establishment()
3418                            .check_encapsulation(mechanism.algorithm)?;
3419                    }
3420                    super::parse::validate_x509_digest_policy(target, &|algorithm| {
3421                        context.policy.check_digest_algorithm(algorithm)
3422                    })
3423                    .map_err(map_key_info_parse_error)?;
3424                    let mut referenced = super::parse::parse_key_info_in_session(
3425                        target,
3426                        context.provider,
3427                        context.budgets.execution.xml_base_resolution(),
3428                        context.policy.resources(),
3429                        Some(resource_uri),
3430                        context.policy.certificate_signature_algorithms(),
3431                        &mut context.budgets.key_info_parse.borrow_mut(),
3432                    )
3433                    .map_err(map_key_info_parse_error)?;
3434                    let mut nested_outcome = visit(
3435                        &mut referenced,
3436                        &external_resolver,
3437                        context,
3438                        materialization,
3439                        next_depth,
3440                    )?;
3441                    nested_outcome.merge(materialize_retrieval_methods_with_budgets(
3442                        &mut referenced,
3443                        &external_resolver,
3444                        context.policy,
3445                        context.provider,
3446                        context.budgets,
3447                        &mut materialization.candidate_work,
3448                    )?);
3449                    // RetrievalMethod nodes are meaningful only in the document
3450                    // whose resolver owns their same-document URI context.
3451                    referenced.sources.retain(|source| {
3452                        !matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. })
3453                    });
3454                    Ok((referenced, nested_outcome))
3455                })?
3456            };
3457            if is_same_document {
3458                nested_outcome.merge(visit(
3459                    &mut referenced,
3460                    resolver,
3461                    context,
3462                    materialization,
3463                    next_depth,
3464                )?);
3465            }
3466            outcome.merge(nested_outcome);
3467            materialization.active.remove(&cycle_key);
3468            materialized.extend(referenced.sources);
3469        }
3470        key_info.sources = materialized;
3471        context
3472            .policy
3473            .resources()
3474            .validate_key_candidates(key_info.embedded_candidate_count())?;
3475        Ok(outcome)
3476    }
3477
3478    let mut context = KeyInfoReferenceMaterializationContext {
3479        policy,
3480        provider,
3481        budgets,
3482    };
3483    let depth = materialization.reference_depth_base;
3484    visit(key_info, resolver, &mut context, materialization, depth)
3485}
3486
3487fn map_key_info_xml_decode_error(
3488    error: crate::encoding::XmlEncodingError,
3489) -> SignatureVerificationPipelineError {
3490    match error {
3491        crate::encoding::XmlEncodingError::DecodedLimit { maximum, actual } => {
3492            SignatureVerificationPipelineError::Policy(
3493                crate::policy::PolicyViolation::ResourceLimit {
3494                    resource: crate::policy::resource_name::XML_DOCUMENT,
3495                    maximum,
3496                    actual,
3497                },
3498            )
3499        }
3500        _ => SignatureVerificationPipelineError::InvalidStructure {
3501            reason: "KeyInfoReference external resource has an invalid XML encoding",
3502        },
3503    }
3504}
3505
3506/// Resolved signature hints and experimental recipient mechanism metadata.
3507/// No key hint establishes trust or permission. The mutation handle is private
3508/// and bound to the original document, never an external document's local ID.
3509pub struct SignatureKeyInfoMetadata {
3510    /// Materialized recipient hints for KEM, ordinary hints otherwise.
3511    pub key_info: KeyInfo,
3512    /// Exact KEM parameter set, if the resolved graph contains one mechanism.
3513    pub encapsulation: Option<crate::provider::KeyEncapsulationAlgorithm>,
3514    pub(crate) cipher_value: Option<NodeId>,
3515}
3516
3517/// Discover operation instructions in local KeyInfo references without consuming
3518/// unrelated key hints. A positive result requires full metadata validation.
3519pub fn has_same_document_encapsulation(
3520    node: Node<'_, '_>,
3521    resolver: &UriReferenceResolver<'_>,
3522    resources: &crate::policy::ResourcePolicy,
3523) -> Result<bool, DsigError> {
3524    resources.validate()?;
3525    let execution = TransformExecutionBudget::from_resources(resources);
3526    has_same_document_encapsulation_with_budget(node, resolver, resources, &execution)
3527}
3528
3529pub(crate) fn has_same_document_encapsulation_with_budget(
3530    node: Node<'_, '_>,
3531    resolver: &UriReferenceResolver<'_>,
3532    resources: &crate::policy::ResourcePolicy,
3533    execution: &TransformExecutionBudget,
3534) -> Result<bool, DsigError> {
3535    has_encapsulation_with_budget::<crate::policy::SigningPolicy>(
3536        node, resolver, resources, execution, None,
3537    )
3538}
3539
3540pub(crate) fn has_signing_encapsulation_with_budget(
3541    node: Node<'_, '_>,
3542    resolver: &UriReferenceResolver<'_>,
3543    policy: &crate::policy::SigningPolicy,
3544    execution: &TransformExecutionBudget,
3545    backend: crate::XmlBackend,
3546) -> Result<bool, DsigError> {
3547    has_encapsulation_with_budget(
3548        node,
3549        resolver,
3550        &policy.resources,
3551        execution,
3552        Some((policy, backend)),
3553    )
3554}
3555
3556fn has_encapsulation_with_budget<P: KeyInfoReferencePolicy>(
3557    node: Node<'_, '_>,
3558    resolver: &UriReferenceResolver<'_>,
3559    resources: &crate::policy::ResourcePolicy,
3560    execution: &TransformExecutionBudget,
3561    external: Option<(&P, crate::XmlBackend)>,
3562) -> Result<bool, DsigError> {
3563    discover_encapsulation_with_budget(node, resolver, resources, execution, external, None)
3564}
3565
3566fn discover_encapsulation_with_budget<P: KeyInfoReferencePolicy>(
3567    node: Node<'_, '_>,
3568    resolver: &UriReferenceResolver<'_>,
3569    resources: &crate::policy::ResourcePolicy,
3570    execution: &TransformExecutionBudget,
3571    external: Option<(&P, crate::XmlBackend)>,
3572    materialization: Option<&mut KeyInfoMaterializationState>,
3573) -> Result<bool, DsigError> {
3574    if resolver.node_for_node_id(node.id()) != Some(node) {
3575        return Err(DsigError::InvalidStructure {
3576            reason: "KeyInfo and resolver belong to different documents",
3577        });
3578    }
3579    struct ExternalProbePath<'a> {
3580        uri: &'a str,
3581        parent: Option<&'a ExternalProbePath<'a>>,
3582    }
3583    struct ProbeContext<'a, P> {
3584        resources: &'a crate::policy::ResourcePolicy,
3585        external: Option<(&'a P, crate::XmlBackend)>,
3586        document_base: Option<&'a str>,
3587        external_path: Option<&'a ExternalProbePath<'a>>,
3588    }
3589    struct DiscoveryState<'a> {
3590        path: [Option<(usize, NodeId)>; crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING + 1],
3591        work: usize,
3592        materialization: Option<&'a mut KeyInfoMaterializationState>,
3593    }
3594    fn visit<P: KeyInfoReferencePolicy>(
3595        node: Node<'_, '_>,
3596        resolver: &UriReferenceResolver<'_>,
3597        context: &ProbeContext<'_, P>,
3598        state: &mut DiscoveryState<'_>,
3599        depth: usize,
3600        execution: &TransformExecutionBudget,
3601    ) -> Result<bool, DsigError> {
3602        let ProbeContext {
3603            resources,
3604            external,
3605            document_base,
3606            external_path,
3607        } = *context;
3608        resources.validate_key_info_reference_depth(depth)?;
3609        if depth > crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING {
3610            return Err(DsigError::InvalidStructure {
3611                reason: "KeyInfoReference safety depth exceeded",
3612            });
3613        }
3614        // Every document on the active stack remains alive for this traversal;
3615        // arena identity distinguishes equal node IDs in different documents.
3616        let identity = (std::ptr::from_ref(node.document()) as usize, node.id());
3617        if state.path[..depth].contains(&Some(identity)) {
3618            if state.materialization.is_some() {
3619                return Err(DsigError::InvalidStructure {
3620                    reason: "KeyInfoReference cycle detected",
3621                });
3622            }
3623            return Ok(false);
3624        }
3625        state.path[depth] = Some(identity);
3626        let mut found = false;
3627        for child in node.children().filter(Node::is_element) {
3628            let work = match state.materialization.as_deref_mut() {
3629                Some(materialization) => &mut materialization.candidate_work,
3630                None => &mut state.work,
3631            };
3632            *work += 1;
3633            resources.validate_key_candidates(*work)?;
3634            if child.has_tag_name((
3635                crate::key_establishment::ENCAPSULATION_NS,
3636                "EncapsulationMechanism",
3637            )) {
3638                if state.materialization.is_none() {
3639                    return Ok(true);
3640                }
3641                if let Some(materialization) = state.materialization.as_deref_mut() {
3642                    let mechanism = crate::key_establishment::parse_encapsulation(
3643                        child,
3644                        materialization.encapsulation_template,
3645                    )
3646                    .map_err(|_| DsigError::InvalidStructure {
3647                        reason: "invalid encapsulation mechanism in resolved KeyInfo",
3648                    })?;
3649                    materialization.record_encapsulation(mechanism, resolver, depth)?;
3650                    if let (Some(mechanism), Some((policy, _))) =
3651                        (&materialization.encapsulation, external)
3652                    {
3653                        policy
3654                            .key_establishment()
3655                            .check_encapsulation(mechanism.algorithm)?;
3656                    }
3657                }
3658                found = true;
3659                continue;
3660            }
3661            if !child.has_tag_name(("http://www.w3.org/2009/xmldsig11#", "KeyInfoReference")) {
3662                continue;
3663            }
3664            let Some(uri) = child.attribute("URI") else {
3665                continue;
3666            };
3667            if uri.len() > super::parse::MAX_KEY_NAME_TEXT_LEN {
3668                continue;
3669            }
3670            if state.materialization.is_some()
3671                && let Some((policy, _)) = external
3672                && !policy.key_info_reference_source_enabled()
3673            {
3674                return Err(crate::policy::PolicyViolation::KeyTrust {
3675                    reason: "KeyInfoReference key sources are disabled",
3676                }
3677                .into());
3678            }
3679            if !uri.is_empty() && !uri.starts_with('#') {
3680                let Some((policy, backend)) = external else {
3681                    continue;
3682                };
3683                // XMLDSig 1.1 section 4.5.10 gives KeyInfoReference the same URI
3684                // dereferencing behavior as Reference, including external XML.
3685                // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-KeyInfoReference
3686                let resolved =
3687                    crate::c14n::xml_base::resolve_uri_from_node_with_document_base_with_budget(
3688                        child,
3689                        uri,
3690                        document_base,
3691                        execution.xml_base_resolution(),
3692                    )
3693                    .map_err(|error| {
3694                        ReferenceProcessingError::UriDereference(
3695                            super::uri::map_xml_base_resolution_error(error),
3696                        )
3697                    })?;
3698                if !policy.key_info_reference_uris().allows(&resolved) {
3699                    if state.materialization.is_some() {
3700                        return Err(crate::policy::PolicyViolation::Uri {
3701                            operation: "KeyInfoReference",
3702                            reason: "URI class is disabled",
3703                        }
3704                        .into());
3705                    }
3706                    continue;
3707                }
3708                // Resource identity, not a reparsed arena ID, detects external
3709                // cycles. The active URI chain borrows stack frames, no clones.
3710                let mut ancestor = external_path;
3711                while let Some(entry) = ancestor {
3712                    if entry.uri == resolved {
3713                        break;
3714                    }
3715                    ancestor = entry.parent;
3716                }
3717                if ancestor.is_some() {
3718                    if state.materialization.is_some() {
3719                        return Err(DsigError::InvalidStructure {
3720                            reason: "KeyInfoReference cycle detected",
3721                        });
3722                    }
3723                    continue;
3724                }
3725                let external_entry = ExternalProbePath {
3726                    uri: &resolved,
3727                    parent: external_path,
3728                };
3729                let (resource, fragment) = resolved
3730                    .split_once('#')
3731                    .map_or((resolved.as_str(), None), |(resource, fragment)| {
3732                        (resource, Some(fragment))
3733                    });
3734                resources.validate_key_info_reference_depth(depth + 1)?;
3735                if depth == crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING {
3736                    return Err(DsigError::InvalidStructure {
3737                        reason: "KeyInfoReference safety depth exceeded",
3738                    });
3739                }
3740                let bytes = match resolver.external_resource(resource) {
3741                    Ok(Some(bytes)) => bytes,
3742                    Err(TransformError::Policy(violation)) => {
3743                        return Err(DsigError::Policy(violation));
3744                    }
3745                    _ => continue,
3746                };
3747                execution.xml_parse_work().charge_policy(bytes.len())?;
3748                let xml =
3749                    crate::encoding::decode_xml_octets(bytes, resources.max_xml_document_bytes)
3750                        .map_err(map_key_info_xml_decode_error)?;
3751                let settings = DocumentParseSettings::from_policy(policy.xml(), resources)
3752                    .with_backend(backend);
3753                let document = XmlDocument::parse_with_settings_and_budget(
3754                    xml.into_owned(),
3755                    settings,
3756                    execution.xml_parse_work(),
3757                )
3758                .map_err(|error| map_document_parse_error(error, settings))?;
3759                let target_found = document.with_view(|view| {
3760                    let external_resolver = resolver.for_external_document_view(view, resource);
3761                    let target = match fragment {
3762                        Some(fragment) if !fragment.is_empty() => external_resolver
3763                            .node_for_same_document_reference_with_budget(
3764                                &resolved[resource.len()..],
3765                                Some(execution.node_set_materialization()),
3766                            )
3767                            .map_err(ReferenceProcessingError::UriDereference)?,
3768                        _ => Some(view.document().root_element()),
3769                    };
3770                    let Some(target) = target else {
3771                        return Ok(false);
3772                    };
3773                    if !target.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
3774                        return Ok(false);
3775                    }
3776                    visit(
3777                        target,
3778                        &external_resolver,
3779                        &ProbeContext {
3780                            document_base: Some(resource),
3781                            external_path: Some(&external_entry),
3782                            resources,
3783                            external,
3784                        },
3785                        state,
3786                        depth + 1,
3787                        execution,
3788                    )
3789                })?;
3790                if target_found {
3791                    if state.materialization.is_none() {
3792                        return Ok(true);
3793                    }
3794                    found = true;
3795                }
3796                continue;
3797            }
3798            // Unconsumed key hints are not validity requirements. Mechanism
3799            // discovery checks only reachable KeyInfo instructions, not key
3800            // candidates that an explicitly supplied recipient never uses.
3801            // The target type is defined by XMLDSig 1.1 section 4.5.10.
3802            // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-KeyInfoReference
3803            if state.materialization.is_some()
3804                && let Some((policy, _)) = external
3805                && !policy.key_info_reference_uris().allows(uri)
3806            {
3807                return Err(crate::policy::PolicyViolation::Uri {
3808                    operation: "KeyInfoReference",
3809                    reason: "URI class is disabled",
3810                }
3811                .into());
3812            }
3813            let target = match resolver.node_for_same_document_reference_with_budget(
3814                uri,
3815                Some(execution.node_set_materialization()),
3816            ) {
3817                Ok(Some(target)) => target,
3818                Err(TransformError::Policy(violation)) => return Err(DsigError::Policy(violation)),
3819                _ => continue,
3820            };
3821            if target.has_tag_name((XMLDSIG_NS, "KeyInfo"))
3822                && visit(target, resolver, context, state, depth + 1, execution)?
3823            {
3824                if state.materialization.is_none() {
3825                    return Ok(true);
3826                }
3827                found = true;
3828            }
3829        }
3830        Ok(found)
3831    }
3832    visit(
3833        node,
3834        resolver,
3835        &ProbeContext {
3836            resources,
3837            external,
3838            document_base: None,
3839            external_path: None,
3840        },
3841        &mut DiscoveryState {
3842            path: [None; crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING + 1],
3843            work: 0,
3844            materialization,
3845        },
3846        0,
3847        execution,
3848    )
3849}
3850
3851fn inspect_signature_key_info_with_budgets<P: KeyInfoReferencePolicy>(
3852    node: Node<'_, '_>,
3853    resolver: &UriReferenceResolver<'_>,
3854    policy: &P,
3855    provider: &dyn crate::provider::CryptoProvider,
3856    budgets: &mut RetrievalMaterializationBudgets<'_>,
3857    template: bool,
3858    retain_recipient_hints: bool,
3859) -> Result<SignatureKeyInfoMetadata, DsigError> {
3860    if resolver.node_for_node_id(node.id()) != Some(node) {
3861        return Err(DsigError::InvalidStructure {
3862            reason: "KeyInfo and resolver belong to different documents",
3863        });
3864    }
3865    let mut materialization = KeyInfoMaterializationState {
3866        collect_encapsulation: true,
3867        encapsulation_template: template,
3868        retain_encapsulation_target: true,
3869        ..Default::default()
3870    };
3871    materialization.observe_encapsulation(node, resolver, 0)?;
3872    if let Some(mechanism) = &materialization.encapsulation {
3873        policy
3874            .key_establishment()
3875            .check_encapsulation(mechanism.algorithm)?;
3876    }
3877    let mut key_info = super::parse::parse_key_info_in_session(
3878        node,
3879        provider,
3880        budgets.execution.xml_base_resolution(),
3881        policy.resources(),
3882        None,
3883        policy.certificate_signature_algorithms(),
3884        &mut budgets.key_info_parse.borrow_mut(),
3885    )
3886    .map_err(map_key_info_parse_error)?;
3887    materialize_key_info_references_with_budgets(
3888        &mut key_info,
3889        resolver,
3890        policy,
3891        provider,
3892        budgets,
3893        &mut materialization,
3894    )?;
3895    let encapsulation = materialization
3896        .encapsulation
3897        .as_ref()
3898        .map(|value| value.algorithm);
3899    let cipher_value =
3900        if let Some((resource, cipher, recipient)) = materialization.encapsulation_target.take() {
3901            // Product mutation boundary: immutable external resources cannot be
3902            // updated by signing, and foreign node IDs must not address this DOM.
3903            if resource != resolver.traversal_document_identity() {
3904                return Err(DsigError::InvalidStructure {
3905                    reason: "recipient metadata requires its original document context",
3906                });
3907            }
3908            if retain_recipient_hints {
3909                let recipient =
3910                    resolver
3911                        .node_for_node_id(recipient)
3912                        .ok_or(DsigError::InvalidStructure {
3913                            reason: "recipient KeyInfo target is unavailable",
3914                        })?;
3915                key_info = super::parse::parse_key_info_in_session(
3916                    recipient,
3917                    provider,
3918                    budgets.execution.xml_base_resolution(),
3919                    policy.resources(),
3920                    None,
3921                    policy.certificate_signature_algorithms(),
3922                    &mut budgets.key_info_parse.borrow_mut(),
3923                )
3924                .map_err(map_key_info_parse_error)?;
3925                // Recipient references use the same cumulative candidate, parse and
3926                // external-resource allowances as the containing mechanism graph.
3927                materialization.reference_depth_base = materialization.encapsulation_depth;
3928                materialize_key_info_references_with_budgets(
3929                    &mut key_info,
3930                    resolver,
3931                    policy,
3932                    provider,
3933                    budgets,
3934                    &mut materialization,
3935                )?;
3936            }
3937            Some(cipher)
3938        } else {
3939            None
3940        };
3941    Ok(SignatureKeyInfoMetadata {
3942        key_info,
3943        encapsulation,
3944        cipher_value,
3945    })
3946}
3947
3948pub(crate) fn inspect_signing_key_info_with_budgets(
3949    node: Node<'_, '_>,
3950    resolver: &UriReferenceResolver<'_>,
3951    policy: &crate::policy::SigningPolicy,
3952    provider: &dyn crate::provider::CryptoProvider,
3953    shared_budgets: (&TransformExecutionBudget, &mut XPathSignatureParseBudget),
3954    xml_backend: crate::XmlBackend,
3955) -> Result<SignatureKeyInfoMetadata, DsigError> {
3956    inspect_signature_key_info_with_budgets(
3957        node,
3958        resolver,
3959        policy,
3960        provider,
3961        &mut RetrievalMaterializationBudgets {
3962            key_info_parse: &RefCell::new(super::parse::KeyInfoParseUsage::new(&policy.resources)),
3963            xpath_parse: shared_budgets.1,
3964            execution: shared_budgets.0,
3965            resources: &policy.resources,
3966            certificate_signature_algorithms: None,
3967            xml_backend,
3968        },
3969        true,
3970        false,
3971    )
3972}
3973
3974fn inspect_signature_key_info_for_policy<P: KeyInfoReferencePolicy>(
3975    node: Node<'_, '_>,
3976    resolver: UriReferenceResolver<'_>,
3977    policy: &P,
3978    provider: &dyn crate::provider::CryptoProvider,
3979    xml_backend: crate::XmlBackend,
3980    template: bool,
3981) -> Result<SignatureKeyInfoMetadata, DsigError> {
3982    let resolver = resolver.with_external_resource_limits(
3983        policy.resources().max_external_resource_bytes,
3984        policy.resources().max_external_resource_total_bytes,
3985    );
3986    let mut xpath_parse = XPathSignatureParseBudget::from_resources(policy.resources());
3987    let execution = TransformExecutionBudget::from_resources(policy.resources());
3988    inspect_signature_key_info_with_budgets(
3989        node,
3990        &resolver,
3991        policy,
3992        provider,
3993        &mut RetrievalMaterializationBudgets {
3994            key_info_parse: &RefCell::new(super::parse::KeyInfoParseUsage::new(policy.resources())),
3995            xpath_parse: &mut xpath_parse,
3996            execution: &execution,
3997            resources: policy.resources(),
3998            certificate_signature_algorithms: policy.certificate_signature_algorithms(),
3999            xml_backend,
4000        },
4001        template,
4002        true,
4003    )
4004}
4005
4006/// Inspect signing hints and KEM targets through the bounded KeyInfoReference
4007/// graph. KEM targets must belong to the document that will be mutated.
4008pub fn inspect_signing_key_info(
4009    node: Node<'_, '_>,
4010    resolver: UriReferenceResolver<'_>,
4011    policy: &crate::policy::SigningPolicy,
4012    provider: &dyn crate::provider::CryptoProvider,
4013    xml_backend: crate::XmlBackend,
4014) -> Result<SignatureKeyInfoMetadata, DsigError> {
4015    policy.validate()?;
4016    inspect_signature_key_info_for_policy(node, resolver, policy, provider, xml_backend, true)
4017}
4018
4019/// Inspect verification hints and KEM metadata with the same source, URI,
4020/// candidate, depth and cycle gates as core verification.
4021/// This original-document metadata API requires KEM targets in that document;
4022/// core verification also supports caller-supplied external KEM resources.
4023pub fn inspect_verification_key_info(
4024    node: Node<'_, '_>,
4025    resolver: UriReferenceResolver<'_>,
4026    policy: &crate::policy::VerificationPolicy,
4027    provider: &dyn crate::provider::CryptoProvider,
4028    xml_backend: crate::XmlBackend,
4029) -> Result<SignatureKeyInfoMetadata, DsigError> {
4030    policy.validate()?;
4031    inspect_signature_key_info_for_policy(node, resolver, policy, provider, xml_backend, false)
4032}
4033
4034/// Inspect operation instructions for a caller-pinned key, leaving unrelated
4035/// lookup hints unconsumed. Discovery and materialization share one allowance.
4036/// Like `inspect_verification_key_info`, this original-document metadata API
4037/// rejects external KEM targets; it never reports a permitted external mechanism
4038/// as absent. Use core verification with a decapsulation key for those targets.
4039pub fn inspect_pinned_verification_key_info(
4040    node: Node<'_, '_>,
4041    resolver: UriReferenceResolver<'_>,
4042    policy: &crate::policy::VerificationPolicy,
4043    provider: &dyn crate::provider::CryptoProvider,
4044    xml_backend: crate::XmlBackend,
4045) -> Result<Option<SignatureKeyInfoMetadata>, DsigError> {
4046    policy.validate()?;
4047    let resolver = resolver.with_external_resource_limits(
4048        policy.resources.max_external_resource_bytes,
4049        policy.resources.max_external_resource_total_bytes,
4050    );
4051    let execution = TransformExecutionBudget::from_resources(&policy.resources);
4052    if !has_encapsulation_with_budget(
4053        node,
4054        &resolver,
4055        &policy.resources,
4056        &execution,
4057        Some((policy, xml_backend)),
4058    )? {
4059        return Ok(None);
4060    }
4061    let mut xpath_parse = XPathSignatureParseBudget::from_resources(&policy.resources);
4062    inspect_signature_key_info_with_budgets(
4063        node,
4064        &resolver,
4065        policy,
4066        provider,
4067        &mut RetrievalMaterializationBudgets {
4068            key_info_parse: &RefCell::new(super::parse::KeyInfoParseUsage::new(&policy.resources)),
4069            xpath_parse: &mut xpath_parse,
4070            execution: &execution,
4071            resources: &policy.resources,
4072            certificate_signature_algorithms: policy.certificate_signature_algorithms(),
4073            xml_backend,
4074        },
4075        false,
4076        true,
4077    )
4078    .map(Some)
4079}
4080
4081fn materialize_key_info_references_for_policy<P: KeyInfoReferencePolicy>(
4082    key_info: &mut KeyInfo,
4083    resolver: UriReferenceResolver<'_>,
4084    policy: &P,
4085    provider: &dyn crate::provider::CryptoProvider,
4086    xml_backend: crate::XmlBackend,
4087) -> Result<(), DsigError> {
4088    let resolver = resolver.with_external_resource_limits(
4089        policy.resources().max_external_resource_bytes,
4090        policy.resources().max_external_resource_total_bytes,
4091    );
4092    let mut xpath_parse_budget = XPathSignatureParseBudget::from_resources(policy.resources());
4093    let execution_budget = TransformExecutionBudget::from_resources(policy.resources());
4094    let mut budgets = RetrievalMaterializationBudgets {
4095        key_info_parse: &RefCell::new(super::parse::KeyInfoParseUsage::new(policy.resources())),
4096        xpath_parse: &mut xpath_parse_budget,
4097        execution: &execution_budget,
4098        resources: policy.resources(),
4099        certificate_signature_algorithms: policy.certificate_signature_algorithms(),
4100        xml_backend,
4101    };
4102    let mut materialization = KeyInfoMaterializationState::default();
4103    // Candidate-local retrieval failures remain deferred: this metadata-only
4104    // entry point cannot know whether a later key-selection stage needs them.
4105    materialize_key_info_references_with_budgets(
4106        key_info,
4107        &resolver,
4108        policy,
4109        provider,
4110        &mut budgets,
4111        &mut materialization,
4112    )?;
4113    Ok(())
4114}
4115
4116/// Resolve and recursively expand policy-allowed `KeyInfoReference` sources
4117/// while preparing caller-supplied key material for signing.
4118///
4119/// The recursive reference traversal shares the verifier's candidate-work,
4120/// depth, cycle, target, XML parsing, and external-resource bounds. Retrieval
4121/// methods nested in an external referenced document are materialized while
4122/// that document's URI context is active; retrieval methods in the caller's
4123/// document remain the responsibility of the complete signing pipeline.
4124/// candidate failures are deferred so an earlier usable key source can still
4125/// be selected; the complete signing pipeline reports the retained failure if
4126/// no source resolves to a key. URI classes remain controlled by the signing
4127/// policy's `uris` field; external
4128/// bytes must be attached to `resolver` explicitly by the caller. The resolver
4129/// is consumed so this operation can bind a fresh aggregate external-resource
4130/// budget to the supplied policy. `xml_backend` is used for every recursively
4131/// referenced XML document, preserving the caller's parser semantics.
4132pub fn materialize_signing_key_info_references(
4133    key_info: &mut KeyInfo,
4134    resolver: UriReferenceResolver<'_>,
4135    policy: &crate::policy::SigningPolicy,
4136    provider: &dyn crate::provider::CryptoProvider,
4137    xml_backend: crate::XmlBackend,
4138) -> Result<(), DsigError> {
4139    policy.validate()?;
4140    materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
4141}
4142
4143/// Resolve and recursively expand policy-allowed `KeyInfoReference` sources
4144/// before verification key selection.
4145///
4146/// In addition to URI and resource policy, this entry point enforces the
4147/// verification policy's `key_sources.key_info_reference` trust gate. Retrieval
4148/// methods nested in an external referenced document are materialized in that
4149/// document's URI context; retrieval methods in the caller's document remain
4150/// the responsibility of the complete verification pipeline. Candidate-local
4151/// retrieval failures remain deferred so usable sibling sources survive; the
4152/// complete pipeline reports one only when key selection has no fallback.
4153/// External bytes must be attached to `resolver` explicitly by the caller. The resolver is
4154/// consumed so this operation can bind a fresh aggregate external-resource
4155/// budget to the supplied policy. `xml_backend` is used for every recursively
4156/// referenced XML document, preserving the caller's parser semantics.
4157pub fn materialize_verification_key_info_references(
4158    key_info: &mut KeyInfo,
4159    resolver: UriReferenceResolver<'_>,
4160    policy: &crate::policy::VerificationPolicy,
4161    provider: &dyn crate::provider::CryptoProvider,
4162    xml_backend: crate::XmlBackend,
4163) -> Result<(), DsigError> {
4164    policy.validate()?;
4165    materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
4166}
4167
4168fn materialize_retrieval_methods_with_budgets<P: KeyInfoReferencePolicy>(
4169    key_info: &mut KeyInfo,
4170    resolver: &UriReferenceResolver<'_>,
4171    policy: &P,
4172    provider: &dyn crate::provider::CryptoProvider,
4173    budgets: &mut RetrievalMaterializationBudgets<'_>,
4174    candidate_work: &mut usize,
4175) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
4176    let allowed_uri_types = policy.retrieval_method_uris();
4177    let allowed_transforms = policy.allowed_transforms();
4178    let retrieval_count = key_info
4179        .sources
4180        .iter()
4181        .filter(|source| matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. }))
4182        .count();
4183    if retrieval_count > MAX_RETRIEVAL_METHOD_COUNT {
4184        return Err(SignatureVerificationPipelineError::InvalidStructure {
4185            reason: "KeyInfo contains too many RetrievalMethod elements",
4186        });
4187    }
4188
4189    let mut total_binary_len = existing_x509_binary_len(key_info)?;
4190    let mut seen = HashSet::new();
4191    let mut materialized = Vec::with_capacity(key_info.sources.len());
4192    let mut outcome = RetrievalMaterialization::default();
4193    for source in std::mem::take(&mut key_info.sources) {
4194        let super::parse::KeyInfoSource::RetrievalMethod {
4195            uri: resolved_uri,
4196            resource_type,
4197            transforms,
4198        } = source
4199        else {
4200            materialized.push(source);
4201            continue;
4202        };
4203
4204        let identity = (
4205            resolved_uri.clone(),
4206            resource_type.clone(),
4207            transforms.clone(),
4208        );
4209        if !seen.insert(identity) {
4210            continue;
4211        }
4212
4213        if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#rawX509Certificate")
4214        {
4215            if transforms != RetrievalMethodTransforms::None
4216                || classify_uri(&resolved_uri) != UriClass::External
4217            {
4218                return Err(SignatureVerificationPipelineError::InvalidStructure {
4219                    reason: "raw X509 RetrievalMethod requires an untransformed external URI",
4220                });
4221            }
4222            if !allowed_uri_types.allows(&resolved_uri) {
4223                return Err(crate::policy::PolicyViolation::Uri {
4224                    operation: "verification",
4225                    reason: "retrieval method URI class is not permitted",
4226                }
4227                .into());
4228            }
4229            let certificate = resolver.external_resource(&resolved_uri).map_err(|error| {
4230                SignatureVerificationPipelineError::from(ReferenceProcessingError::Transform(error))
4231            })?;
4232            let Some(certificate) = certificate else {
4233                outcome.deferred_error.get_or_insert_with(|| {
4234                    SignatureVerificationPipelineError::Reference(
4235                        ReferenceProcessingError::Transform(super::TransformError::UnsupportedUri(
4236                            resolved_uri.clone(),
4237                        )),
4238                    )
4239                });
4240                materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
4241                    uri: resolved_uri,
4242                    resource_type,
4243                    transforms,
4244                });
4245                continue;
4246            };
4247            *candidate_work = candidate_work.saturating_add(1);
4248            budgets.resources.validate_key_candidates(*candidate_work)?;
4249            if certificate.len() > MAX_X509_DECODED_BINARY_LEN {
4250                return Err(SignatureVerificationPipelineError::InvalidStructure {
4251                    reason: "raw X509 RetrievalMethod certificate exceeds maximum allowed length",
4252                });
4253            }
4254            add_retrieval_binary_usage(&mut total_binary_len, certificate.len())?;
4255            let parsed = match super::parse::parse_x509_certificate_in(
4256                certificate,
4257                &budgets.key_info_parse.borrow().certificates,
4258            ) {
4259                Ok(parsed) => parsed,
4260                Err(error) => {
4261                    let error = map_key_info_parse_error(error);
4262                    if matches!(error, SignatureVerificationPipelineError::Policy(_)) {
4263                        return Err(error);
4264                    }
4265                    outcome.deferred_error.get_or_insert(error);
4266                    materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
4267                        uri: resolved_uri,
4268                        resource_type,
4269                        transforms,
4270                    });
4271                    continue;
4272                }
4273            };
4274            materialized.push(super::parse::KeyInfoSource::X509Data(
4275                super::parse::X509DataInfo {
4276                    certificates: vec![certificate.to_vec()],
4277                    parsed_certificates: vec![parsed],
4278                    certificate_chain: vec![0],
4279                    ..super::parse::X509DataInfo::default()
4280                },
4281            ));
4282        } else if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#X509Data") {
4283            if !allowed_uri_types.allows(&resolved_uri) {
4284                return Err(crate::policy::PolicyViolation::Uri {
4285                    operation: "verification",
4286                    reason: "retrieval method URI class is not permitted",
4287                }
4288                .into());
4289            }
4290            let target = resolver
4291                .node_for_same_document_reference(&resolved_uri)
4292                .map_err(ReferenceProcessingError::Transform)?;
4293            let Some(target) = target else {
4294                outcome.deferred_error.get_or_insert(
4295                    SignatureVerificationPipelineError::InvalidStructure {
4296                        reason: "X509Data RetrievalMethod target is missing or ambiguous",
4297                    },
4298                );
4299                materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
4300                    uri: resolved_uri,
4301                    resource_type,
4302                    transforms,
4303                });
4304                continue;
4305            };
4306            let node = match transforms {
4307                RetrievalMethodTransforms::None
4308                    if target.has_tag_name((XMLDSIG_NS, "X509Data")) =>
4309                {
4310                    target
4311                }
4312                RetrievalMethodTransforms::None => {
4313                    return Err(SignatureVerificationPipelineError::InvalidStructure {
4314                        reason: "untransformed X509Data RetrievalMethod must target X509Data directly",
4315                    });
4316                }
4317                RetrievalMethodTransforms::X509DataNodeSetFilter {
4318                    expression,
4319                    namespaces,
4320                } => {
4321                    enforce_transform_allowed(allowed_transforms, XPATH_TRANSFORM_URI)?;
4322                    budgets
4323                        .xpath_parse
4324                        .validate_expression(&expression)
4325                        .map_err(ReferenceProcessingError::Transform)?;
4326                    budgets
4327                        .xpath_parse
4328                        .validate_namespaces(&namespaces)
4329                        .map_err(ReferenceProcessingError::Transform)?;
4330                    select_retrieved_x509_data_root(target, budgets.execution)?
4331                }
4332                RetrievalMethodTransforms::Unsupported => {
4333                    return Err(SignatureVerificationPipelineError::InvalidStructure {
4334                        reason: "X509Data RetrievalMethod contains unsupported transforms",
4335                    });
4336                }
4337            };
4338            super::parse::validate_x509_digest_policy(node, &|algorithm| {
4339                policy.check_digest_algorithm(algorithm)
4340            })
4341            .map_err(map_key_info_parse_error)?;
4342            let data = parse_x509_data_dispatch_with_budget_and_provider(
4343                node,
4344                &mut total_binary_len,
4345                candidate_work,
4346                provider,
4347                budgets.resources,
4348                budgets.certificate_signature_algorithms,
4349                &budgets.key_info_parse.borrow().certificates,
4350            )
4351            .map_err(map_key_info_parse_error)?;
4352            materialized.push(super::parse::KeyInfoSource::X509Data(data));
4353        } else {
4354            materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
4355                uri: resolved_uri,
4356                resource_type,
4357                transforms,
4358            });
4359        }
4360    }
4361    key_info.sources = materialized;
4362    Ok(outcome)
4363}
4364
4365fn select_retrieved_x509_data_root<'a, 'input>(
4366    target: Node<'a, 'input>,
4367    execution_budget: &TransformExecutionBudget,
4368) -> Result<Node<'a, 'input>, SignatureVerificationPipelineError> {
4369    // XMLDSig XPath filtering evaluates the predicate for every node in the
4370    // dereferenced node-set, including attribute and namespace nodes. The
4371    // element-only scan below selects X509Data but must not undercount that
4372    // XPath context cardinality.
4373    let context_nodes = NodeSet::ensure_subtree_materialization_fits_with_budget(
4374        target,
4375        false,
4376        execution_budget.node_set_materialization(),
4377    )
4378    .map_err(ReferenceProcessingError::Transform)?;
4379    execution_budget
4380        .validate_xpath_context_evaluations(context_nodes)
4381        .map_err(ReferenceProcessingError::Transform)?;
4382    execution_budget
4383        .charge_xpath_work(context_nodes)
4384        .map_err(ReferenceProcessingError::Transform)?;
4385    execution_budget
4386        .charge_node_filter_work(context_nodes)
4387        .map_err(ReferenceProcessingError::Transform)?;
4388    let mut root = None;
4389    for candidate in target.descendants() {
4390        if !candidate.is_element()
4391            || candidate.tag_name().namespace() != Some(XMLDSIG_NS)
4392            || candidate.tag_name().name() != "X509Data"
4393        {
4394            continue;
4395        }
4396        if root.replace(candidate).is_some() {
4397            return Err(SignatureVerificationPipelineError::InvalidStructure {
4398                reason: "X509Data RetrievalMethod selected multiple X509Data elements",
4399            });
4400        }
4401    }
4402    root.ok_or(SignatureVerificationPipelineError::InvalidStructure {
4403        reason: "X509Data RetrievalMethod selected no X509Data element",
4404    })
4405}
4406
4407#[cfg(test)]
4408fn materialize_retrieval_methods(
4409    key_info: &mut KeyInfo,
4410    resolver: &UriReferenceResolver<'_>,
4411    allowed_uri_types: UriTypeSet,
4412    allowed_transforms: Option<&HashSet<String>>,
4413    provider: &dyn crate::provider::CryptoProvider,
4414) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
4415    let mut xpath_parse_budget = XPathSignatureParseBudget::default();
4416    let execution_budget = TransformExecutionBudget::default();
4417    let resources = crate::policy::ResourcePolicy::default();
4418    let mut budgets = RetrievalMaterializationBudgets {
4419        key_info_parse: &RefCell::new(super::parse::KeyInfoParseUsage::new(&resources)),
4420        xpath_parse: &mut xpath_parse_budget,
4421        execution: &execution_budget,
4422        resources: &resources,
4423        certificate_signature_algorithms: None,
4424        xml_backend: crate::XmlBackend::default(),
4425    };
4426    let mut candidate_work = key_info.embedded_candidate_count();
4427    let mut policy = crate::policy::VerificationPolicy::default();
4428    policy.uris.retrieval_methods = allowed_uri_types;
4429    policy.transforms.allowed_algorithms = allowed_transforms.cloned();
4430    materialize_retrieval_methods_with_budgets(
4431        key_info,
4432        resolver,
4433        &policy,
4434        provider,
4435        &mut budgets,
4436        &mut candidate_work,
4437    )
4438}
4439
4440fn existing_x509_binary_len(
4441    key_info: &KeyInfo,
4442) -> Result<usize, SignatureVerificationPipelineError> {
4443    let mut total = 0usize;
4444    for source in &key_info.sources {
4445        if let super::parse::KeyInfoSource::X509Data(info) = source {
4446            for len in info
4447                .certificates
4448                .iter()
4449                .chain(&info.skis)
4450                .chain(&info.crls)
4451                .map(Vec::len)
4452                .chain(info.digests.iter().map(|(_, digest)| digest.len()))
4453            {
4454                add_retrieval_binary_usage(&mut total, len)?;
4455            }
4456        }
4457    }
4458    Ok(total)
4459}
4460
4461fn add_retrieval_binary_usage(
4462    total: &mut usize,
4463    delta: usize,
4464) -> Result<(), SignatureVerificationPipelineError> {
4465    *total =
4466        total
4467            .checked_add(delta)
4468            .ok_or(SignatureVerificationPipelineError::InvalidStructure {
4469                reason: "RetrievalMethod X509Data binary length overflow",
4470            })?;
4471    if *total > MAX_X509_DATA_TOTAL_BINARY_LEN {
4472        return Err(SignatureVerificationPipelineError::InvalidStructure {
4473            reason: "RetrievalMethod X509Data exceeds maximum aggregate binary length",
4474        });
4475    }
4476    Ok(())
4477}
4478
4479fn manifest_reference_failure_reason(
4480    error: ReferenceProcessingError,
4481    ref_index: usize,
4482) -> FailureReason {
4483    match error.into_policy_violation() {
4484        Ok(_) => FailureReason::ReferencePolicyViolation { ref_index },
4485        Err(_) => FailureReason::ReferenceProcessingFailure { ref_index },
4486    }
4487}
4488
4489struct CompiledManifestReference {
4490    index: usize,
4491    reference: Reference,
4492    reference_node_id: NodeId,
4493}
4494
4495struct CompiledManifestInvalid {
4496    result: ReferenceResult,
4497}
4498
4499enum ScheduledManifestReference {
4500    Parsed {
4501        node: OperationNodeId,
4502        reference: CompiledManifestReference,
4503    },
4504    Invalid {
4505        node: OperationNodeId,
4506        invalid: CompiledManifestInvalid,
4507    },
4508}
4509
4510impl ScheduledManifestReference {
4511    fn index(&self) -> usize {
4512        match self {
4513            Self::Parsed { reference, .. } => reference.index,
4514            Self::Invalid { invalid, .. } => invalid.result.reference_index,
4515        }
4516    }
4517}
4518
4519struct ManifestVerification<'context, 'inputs, C> {
4520    context: &'context VerifyContext<'inputs>,
4521    capture: &'context C,
4522}
4523
4524fn process_authenticated_manifest_references<C: VerificationCapture>(
4525    operation: &mut OperationExecutionContext<
4526        &crate::policy::VerificationPolicy,
4527        &VerificationOperationBudgets,
4528    >,
4529    view: DocumentView<'_>,
4530    signature_node: Node<'_, '_>,
4531    resolver: &UriReferenceResolver<'_>,
4532    verification: ManifestVerification<'_, '_, C>,
4533    mut remaining_reference_capacity: usize,
4534    crypto: OperationNodeId,
4535) -> Result<(Vec<ReferenceResult>, Vec<OperationNodeId>), SignatureVerificationPipelineError> {
4536    let ctx = verification.context;
4537    let capture = verification.capture;
4538    let mut processed_manifests = HashSet::new();
4539    let mut next_reference_index = 0usize;
4540    let mut frontier_index = 0usize;
4541    let mut completed_digests = Vec::new();
4542    let mut results = Vec::new();
4543    loop {
4544        operation.extend();
4545        let discovery = operation.add_node(
4546            OperationNodeKind::Manifest {
4547                index: frontier_index,
4548            },
4549            OperationStage::AuthenticatedDependency,
4550            None,
4551        );
4552        operation
4553            .add_dependency(discovery, crypto)
4554            .map_err(map_verification_plan_error)?;
4555        for digest in &completed_digests {
4556            operation
4557                .add_dependency(discovery, *digest)
4558                .map_err(map_verification_plan_error)?;
4559        }
4560        operation.compile().map_err(map_verification_plan_error)?;
4561        let parsed = operation.run(discovery, || {
4562            let mut xpath_parse = operation.budgets().xpath_parse.borrow_mut();
4563            let mut state = ManifestDiscoveryState {
4564                processed: &mut processed_manifests,
4565                remaining_capacity: &mut remaining_reference_capacity,
4566                next_reference_index: &mut next_reference_index,
4567                xpath_parse: &mut xpath_parse,
4568            };
4569            parse_manifest_references(
4570                signature_node,
4571                operation,
4572                view,
4573                &mut state,
4574                ctx.allowed_transform_uris(),
4575            )
4576        })?;
4577        let discovered = parsed.references.len() + parsed.invalid_results.len();
4578        if discovered == 0 {
4579            break;
4580        }
4581
4582        operation.extend();
4583        let mut scheduled = Vec::with_capacity(discovered);
4584        for item in parsed.references {
4585            let resource = operation.budgets().resource_identity_for_reference(
4586                &item.reference,
4587                item.index,
4588                resolver,
4589                view,
4590                resolver.node_for_node_id(item.reference_node_id),
4591            );
4592            let node = operation.add_node(
4593                OperationNodeKind::Digest { index: item.index },
4594                OperationStage::AuthenticatedDependency,
4595                Some(resource.clone()),
4596            );
4597            operation
4598                .add_dependency(node, discovery)
4599                .map_err(map_verification_plan_error)?;
4600            scheduled.push(ScheduledManifestReference::Parsed {
4601                node,
4602                reference: item,
4603            });
4604        }
4605        for item in parsed.invalid_results {
4606            let node = operation.add_node(
4607                OperationNodeKind::Digest {
4608                    index: item.result.reference_index,
4609                },
4610                OperationStage::AuthenticatedDependency,
4611                None,
4612            );
4613            operation
4614                .add_dependency(node, discovery)
4615                .map_err(map_verification_plan_error)?;
4616            scheduled.push(ScheduledManifestReference::Invalid {
4617                node,
4618                invalid: item,
4619            });
4620        }
4621        scheduled.sort_by_key(ScheduledManifestReference::index);
4622        operation.compile().map_err(map_verification_plan_error)?;
4623
4624        for scheduled_reference in scheduled {
4625            let (node, result) = match scheduled_reference {
4626                ScheduledManifestReference::Invalid { node, invalid } => {
4627                    let result = operation.run(node, || {
4628                        Ok::<_, SignatureVerificationPipelineError>(invalid.result)
4629                    })?;
4630                    (node, result)
4631                }
4632                ScheduledManifestReference::Parsed {
4633                    node,
4634                    reference: compiled,
4635                } => {
4636                    let observed = operation.budgets().resource_identity_for_reference(
4637                        &compiled.reference,
4638                        compiled.index,
4639                        resolver,
4640                        view,
4641                        resolver.node_for_node_id(compiled.reference_node_id),
4642                    );
4643                    let result = operation.run_with_resource(node, &observed, || {
4644                        let reference = &compiled.reference;
4645                        let budgets = operation.budgets();
4646                        let execution = ReferenceExecutionContext {
4647                            store_pre_digest: ctx.store_pre_digest,
4648                            transform_options: ctx.transform_options(),
4649                            transform_budget: &budgets.transforms,
4650                            canonicalized_data_budget: &budgets.canonicalized,
4651                            provider: ctx.provider,
4652                            capture,
4653                        };
4654                        let mut result = if execution.transform_budget.remaining_c14n_output() == 0
4655                            || super::transforms::validate_relationship_chain(
4656                                &reference.transforms,
4657                                ctx.policy.transforms.opc_relationship_edition,
4658                            )
4659                            .is_err()
4660                            || reference.transforms.len()
4661                                > ctx.policy.resources.max_transforms_per_reference
4662                            || ctx
4663                                .policy
4664                                .check_digest_algorithm(reference.digest_method)
4665                                .is_err()
4666                        {
4667                            manifest_reference_invalid_result(
4668                                reference,
4669                                compiled.index,
4670                                FailureReason::ReferencePolicyViolation {
4671                                    ref_index: compiled.index,
4672                                },
4673                            )
4674                        } else {
4675                            match enforce_reference_policies(
4676                                std::slice::from_ref(reference),
4677                                ctx.policy.uris.references,
4678                                ctx.allowed_transform_uris(),
4679                            ) {
4680                                Ok(()) => process_reference_with_options(
4681                                    reference,
4682                                    resolver,
4683                                    signature_node,
4684                                    ReferenceSet::Manifest,
4685                                    compiled.index,
4686                                    resolver.node_for_node_id(compiled.reference_node_id),
4687                                    &execution,
4688                                )
4689                                .unwrap_or_else(|error| {
4690                                    manifest_reference_invalid_result(
4691                                        reference,
4692                                        compiled.index,
4693                                        manifest_reference_failure_reason(error, compiled.index),
4694                                    )
4695                                }),
4696                                Err(SignatureVerificationPipelineError::Policy(_)) => {
4697                                    manifest_reference_invalid_result(
4698                                        reference,
4699                                        compiled.index,
4700                                        FailureReason::ReferencePolicyViolation {
4701                                            ref_index: compiled.index,
4702                                        },
4703                                    )
4704                                }
4705                                Err(_) => manifest_reference_invalid_result(
4706                                    reference,
4707                                    compiled.index,
4708                                    FailureReason::ReferenceProcessingFailure {
4709                                        ref_index: compiled.index,
4710                                    },
4711                                ),
4712                            }
4713                        };
4714                        bind_reference_evidence(
4715                            &mut result,
4716                            reference,
4717                            view,
4718                            resolver.node_for_node_id(compiled.reference_node_id),
4719                            &observed,
4720                        );
4721                        if result.status == DsigStatus::Valid
4722                            && reference
4723                                .transforms
4724                                .iter()
4725                                .all(transform_preserves_manifest_structure)
4726                            && let Some(uri) = reference.uri.as_deref()
4727                            && let Ok(Some(target)) =
4728                                resolver.node_id_for_same_document_reference(uri)
4729                        {
4730                            operation.authenticate(view.node_identity_by_id(target));
4731                        }
4732                        Ok::<_, SignatureVerificationPipelineError>(result)
4733                    })?;
4734                    (node, result)
4735                }
4736            };
4737            let accepted = result.status == DsigStatus::Valid;
4738            operation.set_outcome(
4739                node,
4740                accepted,
4741                if accepted {
4742                    OperationDecisionReason::ReferenceDigestVerified
4743                } else {
4744                    OperationDecisionReason::ReferenceDigestRejected
4745                },
4746            );
4747            completed_digests.push(node);
4748            results.push(result);
4749        }
4750        frontier_index += 1;
4751    }
4752    results.sort_by_key(|result| result.reference_index);
4753    Ok((results, completed_digests))
4754}
4755
4756fn manifest_reference_invalid_result(
4757    reference: &Reference,
4758    index: usize,
4759    reason: FailureReason,
4760) -> ReferenceResult {
4761    ReferenceResult {
4762        reference_identity: None,
4763        target_identity: None,
4764        external_resource_fingerprint: None,
4765        coverage: ReferenceCoverage::TransformedData,
4766        reference_set: ReferenceSet::Manifest,
4767        reference_index: index,
4768        uri: reference
4769            .uri
4770            .clone()
4771            .unwrap_or_else(|| "<omitted>".to_owned()),
4772        digest_algorithm: reference.digest_method,
4773        status: DsigStatus::Invalid(reason),
4774        pre_digest_data: None,
4775    }
4776}
4777
4778fn parse_manifest_references(
4779    signature_node: Node<'_, '_>,
4780    operation: &OperationExecutionContext<
4781        &crate::policy::VerificationPolicy,
4782        &VerificationOperationBudgets,
4783    >,
4784    view: DocumentView<'_>,
4785    state: &mut ManifestDiscoveryState<'_>,
4786    allowed_transforms: Option<&HashSet<String>>,
4787) -> Result<ParsedManifestReferences, SignatureVerificationPipelineError> {
4788    let mut references = Vec::new();
4789    let mut invalid = Vec::new();
4790    // XMLDSig 1.1 §5.1 binds a Manifest through references, independently of
4791    // physical Signature/Object ancestry. Use only this operation's existing
4792    // authentication state; another Signature's successes cannot admit it.
4793    // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-Manifest
4794    let document = signature_node.document();
4795    let scan = &operation.budgets().transforms;
4796    for manifest_node in document.descendants() {
4797        scan.charge_node_filter_work(1)
4798            .map_err(ReferenceProcessingError::Transform)?;
4799        if !manifest_node.has_tag_name((XMLDSIG_NS, "Manifest"))
4800            || state.processed.contains(&manifest_node.id())
4801        {
4802            continue;
4803        }
4804        let mut manifest_is_signed = false;
4805        for ancestor in manifest_node.ancestors() {
4806            scan.charge_node_filter_work(1)
4807                .map_err(ReferenceProcessingError::Transform)?;
4808            if operation.is_authenticated(view.node_identity(ancestor)) {
4809                manifest_is_signed = true;
4810                break;
4811            }
4812        }
4813        // Leave unauthenticated Manifests unmarked so a verified outer
4814        // Manifest can make them eligible on the next discovery pass.
4815        if !manifest_is_signed {
4816            continue;
4817        }
4818        if !state.processed.insert(manifest_node.id()) {
4819            continue;
4820        }
4821        let mut manifest_children = Vec::new();
4822        for child in manifest_node.children() {
4823            if child.is_text()
4824                && child.text().is_some_and(|text| {
4825                    text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
4826                })
4827            {
4828                return Err(SignatureVerificationPipelineError::InvalidStructure {
4829                    reason: "Manifest contains non-whitespace mixed content",
4830                });
4831            }
4832            if child.is_element() {
4833                manifest_children.push(child);
4834            }
4835        }
4836        if manifest_children.is_empty() {
4837            return Err(SignatureVerificationPipelineError::InvalidStructure {
4838                reason: "Manifest must contain at least one ds:Reference element child",
4839            });
4840        }
4841        for child in manifest_children {
4842            if child.tag_name().namespace() != Some(XMLDSIG_NS)
4843                || child.tag_name().name() != "Reference"
4844            {
4845                return Err(SignatureVerificationPipelineError::InvalidStructure {
4846                    reason: "Manifest must contain only ds:Reference element children",
4847                });
4848            }
4849            if *state.remaining_capacity == 0 {
4850                return Err(SignatureVerificationPipelineError::InvalidStructure {
4851                    reason: "signed Manifests exceed the per-signature Reference limit",
4852                });
4853            }
4854            *state.remaining_capacity -= 1;
4855            let reference_index = *state.next_reference_index;
4856            *state.next_reference_index += 1;
4857            match parse_reference_with_xpath_budget(child, state.xpath_parse) {
4858                Ok(reference) => references.push(CompiledManifestReference {
4859                    index: reference_index,
4860                    reference,
4861                    reference_node_id: child.id(),
4862                }),
4863                Err(ParseError::Transform(super::TransformError::UnsupportedTransform(uri))) => {
4864                    let digest_algorithm =
4865                        reference_digest_method(child).map_err(map_manifest_parse_error)?;
4866                    let reason =
4867                        if allowed_transforms.is_some_and(|allowed| !allowed.contains(&uri)) {
4868                            FailureReason::ReferencePolicyViolation {
4869                                ref_index: reference_index,
4870                            }
4871                        } else {
4872                            FailureReason::ReferenceProcessingFailure {
4873                                ref_index: reference_index,
4874                            }
4875                        };
4876                    invalid.push(CompiledManifestInvalid {
4877                        result: ReferenceResult {
4878                            reference_identity: Some(view.node_identity(child)),
4879                            target_identity: None,
4880                            external_resource_fingerprint: None,
4881                            coverage: ReferenceCoverage::TransformedData,
4882                            reference_set: ReferenceSet::Manifest,
4883                            reference_index,
4884                            uri: child.attribute("URI").unwrap_or("<omitted>").to_owned(),
4885                            digest_algorithm,
4886                            status: DsigStatus::Invalid(reason),
4887                            pre_digest_data: None,
4888                        },
4889                    });
4890                }
4891                Err(error) => return Err(map_manifest_parse_error(error)),
4892            }
4893        }
4894    }
4895    Ok(ParsedManifestReferences {
4896        references,
4897        invalid_results: invalid,
4898    })
4899}
4900
4901struct ParsedManifestReferences {
4902    references: Vec<CompiledManifestReference>,
4903    invalid_results: Vec<CompiledManifestInvalid>,
4904}
4905
4906struct ManifestDiscoveryState<'a> {
4907    processed: &'a mut HashSet<NodeId>,
4908    remaining_capacity: &'a mut usize,
4909    next_reference_index: &'a mut usize,
4910    xpath_parse: &'a mut XPathSignatureParseBudget,
4911}
4912
4913fn transform_preserves_manifest_structure(transform: &Transform) -> bool {
4914    match transform {
4915        Transform::C14n(_) => true,
4916        // Both eligible ID targets are descendants of the owning Signature.
4917        // Enveloped subtraction therefore removes their intersection with that
4918        // Signature subtree, even though the Signature node itself is not in the
4919        // dereferenced node set.
4920        Transform::Enveloped
4921        | Transform::XpathExcludeAllSignatures
4922        | Transform::XPath(_)
4923        | Transform::XPathFilter2(_)
4924        | Transform::Base64Decode
4925        | Transform::Relationship(_) => false,
4926    }
4927}
4928
4929#[expect(
4930    clippy::large_enum_variant,
4931    reason = "single bounded authorization result avoids heap allocation"
4932)]
4933enum ResolvedVerifyingKey<'a> {
4934    Established(super::HmacVerificationKey),
4935    Borrowed(&'a dyn VerifyingKey),
4936    Owned(super::ResolvedVerificationKey<'a>),
4937}
4938
4939impl ResolvedVerifyingKey<'_> {
4940    fn evidence(
4941        &self,
4942        policy: &crate::policy::VerificationPolicy,
4943    ) -> Result<super::KeyTrustEvidence, DsigError> {
4944        match self {
4945            Self::Established(_) => {
4946                if policy.key_trust.mode != crate::policy::VerificationTrustMode::CryptographicOnly
4947                {
4948                    return Err(crate::policy::PolicyViolation::KeyTrust {
4949                        reason: "KEM/HMAC does not authenticate sender identity",
4950                    }
4951                    .into());
4952                }
4953                Ok(super::KeyTrustEvidence::NotEstablished)
4954            }
4955            Self::Borrowed(_)
4956                if policy.key_trust.mode
4957                    == crate::policy::VerificationTrustMode::CryptographicOnly =>
4958            {
4959                Ok(super::KeyTrustEvidence::NotEstablished)
4960            }
4961            Self::Borrowed(_) => Ok(super::KeyTrustEvidence::CallerTrusted),
4962            Self::Owned(key) => key.authorize(policy),
4963        }
4964    }
4965    fn as_ref(&self) -> &dyn VerifyingKey {
4966        match self {
4967            Self::Established(key) => key,
4968            Self::Borrowed(key) => *key,
4969            Self::Owned(key) => key.as_ref(),
4970        }
4971    }
4972}
4973
4974fn resolve_verifying_key<'k>(
4975    ctx: &VerifyContext<'k>,
4976    key: Option<&'k dyn VerifyingKey>,
4977    key_info: Option<&KeyInfo>,
4978    algorithm: SignatureAlgorithm,
4979    budget: &mut super::InspectedKeyCandidateBudget,
4980) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
4981    if let Some(key) = key {
4982        if !ctx.policy.key_sources.preset_key {
4983            return Err(crate::policy::PolicyViolation::KeyTrust {
4984                reason: "pre-resolved verification keys are disabled",
4985            }
4986            .into());
4987        }
4988        require_verifying_key_candidate_capacity(&ctx.policy)?;
4989        budget.charge()?;
4990        return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
4991    }
4992    if let Some(resolver) = ctx.key_resolver {
4993        require_verifying_key_candidate_capacity(&ctx.policy)?;
4994        let resolved = resolver.resolve_for_verification_with_budget(
4995            key_info,
4996            algorithm,
4997            &ctx.policy,
4998            ctx.provider,
4999            budget,
5000        )?;
5001        if let Some(key) = &resolved {
5002            key.authorize(&ctx.policy)?;
5003        }
5004        return Ok(resolved.map(ResolvedVerifyingKey::Owned));
5005    }
5006    Ok(None)
5007}
5008
5009fn require_verifying_key_candidate_capacity(
5010    policy: &crate::policy::VerificationPolicy,
5011) -> Result<(), SignatureVerificationPipelineError> {
5012    policy
5013        .resources
5014        .validate_key_candidates(1)
5015        .map_err(Into::into)
5016}
5017
5018fn enforce_reference_policies(
5019    references: &[Reference],
5020    allowed_uri_types: UriTypeSet,
5021    allowed_transforms: Option<&HashSet<String>>,
5022) -> Result<(), SignatureVerificationPipelineError> {
5023    for reference in references {
5024        let uri = reference
5025            .uri
5026            .as_deref()
5027            .ok_or(SignatureVerificationPipelineError::Reference(
5028                ReferenceProcessingError::MissingUri,
5029            ))?;
5030        if !allowed_uri_types.allows(uri) {
5031            return Err(crate::policy::PolicyViolation::Uri {
5032                operation: "verification",
5033                reason: "reference URI class is not permitted",
5034            }
5035            .into());
5036        }
5037
5038        if let Some(allowed) = allowed_transforms {
5039            for transform in &reference.transforms {
5040                let transform_uri = transform.algorithm_uri();
5041                enforce_transform_allowed(Some(allowed), transform_uri)?;
5042            }
5043
5044            // External dereference has an octet-stream data type independent of
5045            // whether the caller supplied the resource. Every transform then
5046            // determines the next type, including implicit binary-to-node-set
5047            // adapters before XML-level transforms.
5048            let produces_binary = transform_chain_produces_binary(
5049                classify_uri(uri) == UriClass::External,
5050                &reference.transforms,
5051            );
5052            if !produces_binary {
5053                enforce_transform_allowed(Some(allowed), DEFAULT_IMPLICIT_C14N_URI)?;
5054            }
5055        }
5056    }
5057    Ok(())
5058}
5059
5060fn enforce_transform_allowed(
5061    allowed_transforms: Option<&HashSet<String>>,
5062    algorithm: &str,
5063) -> Result<(), SignatureVerificationPipelineError> {
5064    if allowed_transforms.is_some_and(|allowed| !allowed.contains(algorithm)) {
5065        return Err(crate::policy::PolicyViolation::Algorithm {
5066            operation: "verification transform",
5067            algorithm: algorithm.to_owned(),
5068        }
5069        .into());
5070    }
5071    Ok(())
5072}
5073
5074/// Query-funded form: the attacker-sized URI is copied into `work`'s storage.
5075fn enforce_current_transform_allowed<W: structured_asn1::memory::Work>(
5076    allowed_transforms: Option<&HashSet<String>>,
5077    algorithm: &str,
5078    work: &W,
5079) -> Result<(), SignatureVerificationPipelineError> {
5080    use crate::xml::dom::storage::Storage as _;
5081    if allowed_transforms.is_some_and(|allowed| !allowed.contains(algorithm)) {
5082        return Err(
5083            match crate::xml::dom::storage::Accounted(work).formatted(format_args!("{algorithm}")) {
5084                Ok(algorithm) => DsigError::AccountedPolicyAlgorithm {
5085                    operation: "verification transform",
5086                    algorithm: algorithm.into_owned(),
5087                },
5088                Err(crate::xml::dom::ParseError::SharedExecution(cause)) => cause.into(),
5089                Err(cause) => cause.into(),
5090            },
5091        );
5092    }
5093    Ok(())
5094}
5095
5096#[derive(Debug, Clone, Copy)]
5097pub(super) struct SignatureChildNodes<'a, 'input> {
5098    signed_info_node: Node<'a, 'input>,
5099    signature_value_node: Node<'a, 'input>,
5100    pub(super) key_info_node: Option<Node<'a, 'input>>,
5101}
5102
5103pub(super) fn parse_signature_children<'a, 'input>(
5104    signature_node: Node<'a, 'input>,
5105) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
5106    let mut signed_info_node: Option<Node<'_, '_>> = None;
5107    let mut signature_value_node: Option<Node<'_, '_>> = None;
5108    let mut key_info_node: Option<Node<'_, '_>> = None;
5109    let mut signed_info_index: Option<usize> = None;
5110    let mut signature_value_index: Option<usize> = None;
5111    let mut key_info_index: Option<usize> = None;
5112    let mut first_unexpected_dsig_index: Option<usize> = None;
5113
5114    let mut element_index = 0usize;
5115    for child in signature_node.children() {
5116        if child.is_text() {
5117            if child
5118                .text()
5119                .is_some_and(|text| !is_xml_whitespace_only(text))
5120            {
5121                return Err(SignatureVerificationPipelineError::InvalidStructure {
5122                    reason: "Signature must not contain non-whitespace mixed content",
5123                });
5124            }
5125            continue;
5126        }
5127        if !child.is_element() {
5128            continue;
5129        }
5130
5131        element_index += 1;
5132        if child.tag_name().namespace() != Some(XMLDSIG_NS) {
5133            return Err(SignatureVerificationPipelineError::InvalidStructure {
5134                reason: "Signature must contain only XMLDSIG element children",
5135            });
5136        }
5137        match child.tag_name().name() {
5138            "SignedInfo" => {
5139                if signed_info_node.is_some() {
5140                    return Err(SignatureVerificationPipelineError::InvalidStructure {
5141                        reason: "SignedInfo must appear exactly once under Signature",
5142                    });
5143                }
5144                signed_info_node = Some(child);
5145                signed_info_index = Some(element_index);
5146            }
5147            "SignatureValue" => {
5148                if signature_value_node.is_some() {
5149                    return Err(SignatureVerificationPipelineError::InvalidStructure {
5150                        reason: "SignatureValue must appear exactly once under Signature",
5151                    });
5152                }
5153                signature_value_node = Some(child);
5154                signature_value_index = Some(element_index);
5155            }
5156            "KeyInfo" => {
5157                if key_info_node.is_some() {
5158                    return Err(SignatureVerificationPipelineError::InvalidStructure {
5159                        reason: "KeyInfo must appear at most once under Signature",
5160                    });
5161                }
5162                key_info_node = Some(child);
5163                key_info_index = Some(element_index);
5164            }
5165            "Object" => {
5166                // Valid Object elements are allowed only after SignedInfo, SignatureValue,
5167                // and optional KeyInfo; this is enforced via first_unexpected_dsig_index.
5168            }
5169            _ => {
5170                if first_unexpected_dsig_index.is_none() {
5171                    first_unexpected_dsig_index = Some(element_index);
5172                }
5173            }
5174        }
5175    }
5176
5177    let signed_info_node =
5178        signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
5179            element: "SignedInfo",
5180        })?;
5181    let signature_value_node =
5182        signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
5183            element: "SignatureValue",
5184        })?;
5185    if signed_info_index != Some(1) {
5186        return Err(SignatureVerificationPipelineError::InvalidStructure {
5187            reason: "SignedInfo must be the first element child of Signature",
5188        });
5189    }
5190    if signature_value_index != Some(2) {
5191        return Err(SignatureVerificationPipelineError::InvalidStructure {
5192            reason: "SignatureValue must be the second element child of Signature",
5193        });
5194    }
5195    if let Some(index) = key_info_index
5196        && index != 3
5197    {
5198        return Err(SignatureVerificationPipelineError::InvalidStructure {
5199            reason: "KeyInfo must be the third element child of Signature when present",
5200        });
5201    }
5202
5203    let allowed_prefix_end = key_info_index.unwrap_or(2);
5204    if let Some(unexpected_index) = first_unexpected_dsig_index {
5205        return Err(SignatureVerificationPipelineError::InvalidStructure {
5206            reason: if unexpected_index > allowed_prefix_end {
5207                "After SignedInfo, SignatureValue, and optional KeyInfo, Signature may contain only Object elements"
5208            } else {
5209                "Signature may contain SignedInfo first, SignatureValue second, optional KeyInfo third, and Object elements thereafter"
5210            },
5211        });
5212    }
5213
5214    Ok(SignatureChildNodes {
5215        signed_info_node,
5216        signature_value_node,
5217        key_info_node,
5218    })
5219}
5220
5221fn decode_signature_value(
5222    signature_value_node: Node<'_, '_>,
5223    algorithm: SignatureAlgorithm,
5224    resources: &crate::policy::ResourcePolicy,
5225) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
5226    if matches!(algorithm, SignatureAlgorithm::PostQuantum(_)) {
5227        let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
5228        // PQ signatures can be tens of kilobytes: borrow their XML text and
5229        // stream directly into the decoded allocation, without a second
5230        // full-size normalized Base64 buffer.
5231        let payload = super::whitespace::XmlBase64Payload::bounded(
5232            signature_value_node,
5233            raw_maximum,
5234            maximum,
5235        )
5236        .map_err(
5237            |reason| SignatureVerificationPipelineError::InvalidStructure {
5238                reason: match reason {
5239                    "unexpected nested element" => {
5240                        "SignatureValue must not contain element children"
5241                    }
5242                    "maximum allowed text length" => {
5243                        "SignatureValue exceeds maximum allowed text length"
5244                    }
5245                    "maximum allowed base64 length" => {
5246                        "SignatureValue exceeds maximum allowed length"
5247                    }
5248                    _ => "SignatureValue contains malformed Base64",
5249                },
5250            },
5251        )?;
5252        return payload.decode().map_err(|_| {
5253            SignatureVerificationPipelineError::InvalidStructure {
5254                reason: "SignatureValue contains malformed Base64",
5255            }
5256        });
5257    }
5258    if signature_value_node
5259        .children()
5260        .any(|child| child.is_element())
5261    {
5262        return Err(SignatureVerificationPipelineError::InvalidStructure {
5263            reason: "SignatureValue must not contain element children",
5264        });
5265    }
5266
5267    // Count bounded borrowed text first: comments may split a
5268    // signature into many tiny text nodes. Reserving per node would repeatedly
5269    // copy the accumulated payload; one exact reservation keeps this linear.
5270    let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
5271    let mut text_len = 0usize;
5272    let mut normalized_len = 0usize;
5273    for child in signature_value_node
5274        .children()
5275        .filter(|child| child.is_text())
5276    {
5277        if let Some(text) = child.text() {
5278            if text.len() > raw_maximum - text_len {
5279                return Err(SignatureVerificationPipelineError::InvalidStructure {
5280                    reason: "SignatureValue exceeds maximum allowed text length",
5281                });
5282            }
5283            text_len += text.len();
5284            for byte in text.bytes() {
5285                if matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
5286                    continue;
5287                }
5288                if normalized_len == maximum {
5289                    return Err(SignatureVerificationPipelineError::InvalidStructure {
5290                        reason: "SignatureValue exceeds maximum allowed length",
5291                    });
5292                }
5293                normalized_len += 1;
5294            }
5295        }
5296    }
5297    let mut normalized = Vec::with_capacity(normalized_len);
5298    let mut raw_text_len = 0usize;
5299    for child in signature_value_node
5300        .children()
5301        .filter(|child| child.is_text())
5302    {
5303        if let Some(text) = child.text() {
5304            push_normalized_signature_text(
5305                text,
5306                &mut raw_text_len,
5307                &mut normalized,
5308                algorithm,
5309                resources,
5310            )?;
5311        }
5312    }
5313
5314    Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
5315}
5316
5317fn push_normalized_signature_text(
5318    text: &str,
5319    raw_text_len: &mut usize,
5320    normalized: &mut Vec<u8>,
5321    algorithm: SignatureAlgorithm,
5322    resources: &crate::policy::ResourcePolicy,
5323) -> Result<(), SignatureVerificationPipelineError> {
5324    let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
5325    if text.len() > raw_maximum - *raw_text_len {
5326        return Err(SignatureVerificationPipelineError::InvalidStructure {
5327            reason: "SignatureValue exceeds maximum allowed text length",
5328        });
5329    }
5330    *raw_text_len += text.len();
5331    let additional = text
5332        .bytes()
5333        .filter(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
5334        .count();
5335    if additional > maximum - normalized.len() {
5336        return Err(SignatureVerificationPipelineError::InvalidStructure {
5337            reason: "SignatureValue exceeds maximum allowed length",
5338        });
5339    }
5340
5341    normalize_xml_base64_bytes(text.as_bytes(), normalized, |_| true).map_err(|err| {
5342        SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(
5343            err.normalized_offset,
5344            err.invalid_byte,
5345        ))
5346    })?;
5347
5348    Ok(())
5349}
5350
5351fn signature_value_limits(
5352    algorithm: SignatureAlgorithm,
5353    resources: &crate::policy::ResourcePolicy,
5354) -> (usize, usize) {
5355    // FIPS 205 table 2 includes signatures up to 49,856 octets. The old
5356    // RSA-oriented encoded-text ceiling is not their wire contract.
5357    // https://doi.org/10.6028/NIST.FIPS.205
5358    // XSD 1.0 section 3.2.16 fixes base64Binary whitespace to collapse:
5359    // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#base64Binary
5360    // Raw text is bounded by the operation's XML budget, not the wire width.
5361    let encoded = match algorithm {
5362        SignatureAlgorithm::PostQuantum(parameter) => parameter.signature_len().div_ceil(3) * 4,
5363        _ => MAX_SIGNATURE_VALUE_LEN,
5364    };
5365    (encoded, resources.max_xml_document_bytes)
5366}
5367
5368fn verify_with_algorithm(
5369    algorithm: SignatureAlgorithm,
5370    public_key_pem: &str,
5371    signed_data: &[u8],
5372    signature_value: &[u8],
5373) -> Result<bool, SignatureVerificationPipelineError> {
5374    match algorithm {
5375        #[cfg(feature = "legacy-algorithms")]
5376        SignatureAlgorithm::RsaMd5 | SignatureAlgorithm::RsaRipemd160 => Ok(
5377            verify_rsa_signature_pem(algorithm, public_key_pem, signed_data, signature_value)?,
5378        ),
5379        #[cfg(feature = "legacy-algorithms")]
5380        SignatureAlgorithm::EcdsaRipemd160 => Ok(verify_ecdsa_signature_pem(
5381            algorithm,
5382            public_key_pem,
5383            signed_data,
5384            signature_value,
5385        )?),
5386        #[cfg(feature = "legacy-algorithms")]
5387        SignatureAlgorithm::HmacMd5 | SignatureAlgorithm::HmacRipemd160 => {
5388            Err(SignatureVerificationError::UnsupportedAlgorithm {
5389                uri: algorithm.uri().to_owned(),
5390            }
5391            .into())
5392        }
5393        SignatureAlgorithm::Ed25519
5394        | SignatureAlgorithm::Ed25519Ctx
5395        | SignatureAlgorithm::Ed25519Ph
5396        | SignatureAlgorithm::Ed448
5397        | SignatureAlgorithm::Ed448Ph
5398        | SignatureAlgorithm::PostQuantum(_) => {
5399            let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
5400                .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
5401            if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
5402                return Err(SignatureVerificationError::InvalidKeyPem.into());
5403            }
5404            Ok(super::modern::verify(
5405                algorithm,
5406                &pem.contents,
5407                signed_data,
5408                signature_value,
5409            )?)
5410        }
5411        SignatureAlgorithm::DsaSha1 | SignatureAlgorithm::DsaSha256 => {
5412            let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
5413                .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
5414            if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
5415                return Err(SignatureVerificationError::InvalidKeyPem.into());
5416            }
5417            Ok(verify_dsa_signature_spki(
5418                algorithm,
5419                &pem.contents,
5420                signed_data,
5421                signature_value,
5422            )?)
5423        }
5424        SignatureAlgorithm::HmacSha1
5425        | SignatureAlgorithm::HmacSha224
5426        | SignatureAlgorithm::HmacSha256
5427        | SignatureAlgorithm::HmacSha384
5428        | SignatureAlgorithm::HmacSha512 => Err(SignatureVerificationError::UnsupportedAlgorithm {
5429            uri: algorithm.uri().to_string(),
5430        }
5431        .into()),
5432        SignatureAlgorithm::RsaSha1
5433        | SignatureAlgorithm::RsaSha224
5434        | SignatureAlgorithm::RsaSha256
5435        | SignatureAlgorithm::RsaSha384
5436        | SignatureAlgorithm::RsaSha512
5437        | SignatureAlgorithm::RsaPssSha1
5438        | SignatureAlgorithm::RsaPssSha224
5439        | SignatureAlgorithm::RsaPssSha256
5440        | SignatureAlgorithm::RsaPssSha384
5441        | SignatureAlgorithm::RsaPssSha512
5442        | SignatureAlgorithm::RsaPssSha3_224
5443        | SignatureAlgorithm::RsaPssSha3_256
5444        | SignatureAlgorithm::RsaPssSha3_384
5445        | SignatureAlgorithm::RsaPssSha3_512
5446        | SignatureAlgorithm::RsaPss(_) => Ok(verify_rsa_signature_pem(
5447            algorithm,
5448            public_key_pem,
5449            signed_data,
5450            signature_value,
5451        )?),
5452        SignatureAlgorithm::EcdsaSha1
5453        | SignatureAlgorithm::EcdsaSha224
5454        | SignatureAlgorithm::EcdsaSha256
5455        | SignatureAlgorithm::EcdsaSha384
5456        | SignatureAlgorithm::EcdsaSha512
5457        | SignatureAlgorithm::EcdsaSha3_224
5458        | SignatureAlgorithm::EcdsaSha3_256
5459        | SignatureAlgorithm::EcdsaSha3_384
5460        | SignatureAlgorithm::EcdsaSha3_512 => {
5461            // Malformed ECDSA signature bytes are treated as a verification miss
5462            // (Ok(false)) instead of a pipeline error; only key/algorithm and
5463            // crypto-operation failures propagate as Err.
5464            match verify_ecdsa_signature_pem(
5465                algorithm,
5466                public_key_pem,
5467                signed_data,
5468                signature_value,
5469            ) {
5470                Ok(valid) => Ok(valid),
5471                Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
5472                Err(error) => Err(error.into()),
5473            }
5474        }
5475    }
5476}
5477
5478#[cfg(test)]
5479#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
5480mod tests {
5481    use super::*;
5482    use crate::c14n::C14nAlgorithm;
5483    use crate::xml::dom::Document;
5484    use crate::xmldsig::TransformError;
5485    use crate::xmldsig::digest::DigestAlgorithm;
5486    use crate::xmldsig::parse::{Reference, parse_signed_info};
5487    use crate::xmldsig::transforms::Transform;
5488    use crate::xmldsig::uri::UriReferenceResolver;
5489    use base64::Engine;
5490
5491    // ── Helpers ──────────────────────────────────────────────────────
5492
5493    /// Build a Reference with given URI, transforms, digest method, and expected digest.
5494    fn make_reference(
5495        uri: &str,
5496        transforms: Vec<Transform>,
5497        digest_method: DigestAlgorithm,
5498        digest_value: Vec<u8>,
5499    ) -> Reference {
5500        Reference {
5501            uri: Some(uri.to_string()),
5502            id: None,
5503            ref_type: None,
5504            transforms,
5505            digest_method,
5506            digest_value,
5507        }
5508    }
5509
5510    #[test]
5511    fn operation_borrows_caller_utc_facts_without_policy_rebinding()
5512    -> Result<(), Box<dyn std::error::Error>> {
5513        // Synthetic caller facts test transport, not real-world UTC history.
5514        // Operation capture shares the original slice and never infers facts
5515        // from a different policy or creates a replacement timeline.
5516        let transitions = [structured_pkix::temporal::Transition::new(2_678_400, 1)];
5517        let facts = structured_pkix::temporal::LeapTable::new(0, 6_000_000, &transitions)
5518            .expect("valid synthetic UTC facts");
5519        let ctx = VerifyContext::new().utc_facts(facts);
5520        let budgets = VerificationOperationBudgets::with_transforms(
5521            &ctx.policy,
5522            TransformExecutionBudget::from_resources(&ctx.policy.resources),
5523            ctx.utc_facts,
5524        );
5525        let mut budget = budgets.key_candidates.borrow_mut();
5526        assert_eq!(
5527            budget
5528                .utc_facts()
5529                .ok_or("operation UTC facts")?
5530                .transitions()
5531                .as_ptr(),
5532            transitions.as_ptr()
5533        );
5534        budget.bind_policy(&ctx.policy)?;
5535        assert_eq!(
5536            budget
5537                .utc_facts()
5538                .ok_or("retained UTC facts")?
5539                .transitions()
5540                .as_ptr(),
5541            transitions.as_ptr()
5542        );
5543        let standalone =
5544            super::super::InspectedKeyCandidateBudget::new_with_utc_facts(&ctx.policy, facts);
5545        assert_eq!(
5546            standalone
5547                .utc_facts()
5548                .ok_or("standalone UTC facts")?
5549                .transitions()
5550                .as_ptr(),
5551            transitions.as_ptr()
5552        );
5553        assert!(
5554            super::super::InspectedKeyCandidateBudget::new(&ctx.policy)
5555                .utc_facts()
5556                .is_none()
5557        );
5558        Ok(())
5559    }
5560
5561    #[test]
5562    fn reference_resolution_uses_each_elements_effective_xml_base() {
5563        // Equal lexical URIs under different xml:base values identify distinct
5564        // caller-owned resources and must not collide in the resolver.
5565        let first = b"first payload";
5566        let second = b"second payload";
5567        let first_digest = base64::engine::general_purpose::STANDARD
5568            .encode(compute_digest(DigestAlgorithm::Sha256, first));
5569        let second_digest = base64::engine::general_purpose::STANDARD
5570            .encode(compute_digest(DigestAlgorithm::Sha256, second));
5571        let xml = format!(
5572            r#"<root xml:base="https://example.test/base/" xmlns:ds="{XMLDSIG_NS}">
5573                <ds:Signature><ds:SignedInfo>
5574                    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5575                    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5576                    <ds:Reference xml:base="one/" URI="payload.bin">
5577                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5578                        <ds:DigestValue>{first_digest}</ds:DigestValue>
5579                    </ds:Reference>
5580                    <ds:Reference xml:base="../two/" URI="payload.bin">
5581                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5582                        <ds:DigestValue>{second_digest}</ds:DigestValue>
5583                    </ds:Reference>
5584                </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>
5585            </root>"#
5586        );
5587        let document = Document::parse(&xml).unwrap();
5588        let signature = document
5589            .descendants()
5590            .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
5591            .unwrap();
5592        let signed_info_node = signature
5593            .children()
5594            .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
5595            .unwrap();
5596        let signed_info = parse_signed_info(signed_info_node).unwrap();
5597        let resources = HashMap::from([
5598            (
5599                "https://example.test/base/one/payload.bin".into(),
5600                first.to_vec(),
5601            ),
5602            (
5603                "https://example.test/two/payload.bin".into(),
5604                second.to_vec(),
5605            ),
5606        ]);
5607        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5608
5609        let result = process_all_references(&signed_info.references, &resolver, signature, false)
5610            .expect("each Reference should resolve against its own effective base");
5611
5612        assert!(result.all_valid());
5613    }
5614
5615    #[test]
5616    fn internal_dtd_opt_in_applies_to_detached_xml_transforms() {
5617        // The parse policy covers every XML document in one verification
5618        // pipeline, including caller-owned octets converted to a node-set.
5619        let detached = b"<!DOCTYPE payload [<!ELEMENT payload (#PCDATA)>]><payload>ok</payload>";
5620        let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
5621            DigestAlgorithm::Sha256,
5622            b"<payload>ok</payload>",
5623        ));
5624        let xml = format!(
5625            r#"<root xmlns:ds="{XMLDSIG_NS}">
5626  <ds:Signature>
5627    <ds:SignedInfo>
5628      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5629      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5630      <ds:Reference URI="urn:detached-dtd">
5631        <ds:Transforms>
5632          <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
5633        </ds:Transforms>
5634        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5635        <ds:DigestValue>{digest}</ds:DigestValue>
5636      </ds:Reference>
5637    </ds:SignedInfo>
5638    <ds:SignatureValue>AQ==</ds:SignatureValue>
5639  </ds:Signature>
5640</root>"#
5641        );
5642        let resources = HashMap::from([("urn:detached-dtd".to_owned(), detached.to_vec())]);
5643        let key = AcceptingKey;
5644
5645        let default_error = VerifyContext::new()
5646            .key(&key)
5647            .allowed_uri_types(UriTypeSet::ALL)
5648            .external_resources(&resources)
5649            .verify(&xml)
5650            .expect_err("internal DTD parsing must remain disabled by default");
5651        assert!(matches!(
5652            default_error,
5653            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
5654                crate::xmldsig::TransformError::XmlParse(_)
5655            ))
5656        ));
5657
5658        let result = VerifyContext::new()
5659            .key(&key)
5660            .allowed_uri_types(UriTypeSet::ALL)
5661            .external_resources(&resources)
5662            .allow_internal_dtd(true)
5663            .verify(&xml)
5664            .expect("the explicit DTD opt-in must cover detached XML transforms");
5665
5666        assert_eq!(result.status, DsigStatus::Valid);
5667
5668        let external_entity = br#"<!DOCTYPE payload [
5669            <!ENTITY ext SYSTEM "file:///etc/passwd">
5670        ]><payload>&ext;</payload>"#;
5671        let external_entity_resources =
5672            HashMap::from([("urn:detached-dtd".to_owned(), external_entity.to_vec())]);
5673        let external_entity_error = VerifyContext::new()
5674            .key(&key)
5675            .allowed_uri_types(UriTypeSet::ALL)
5676            .external_resources(&external_entity_resources)
5677            .allow_internal_dtd(true)
5678            .verify(&xml)
5679            .expect_err("the internal-DTD opt-in must not resolve external entities");
5680        assert!(matches!(
5681            external_entity_error,
5682            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
5683                crate::xmldsig::TransformError::XmlParse(_)
5684            ))
5685        ));
5686    }
5687
5688    #[test]
5689    fn owned_verification_revalidates_internal_dtd_provenance() {
5690        // A document accepted by one permissive operation must not bypass a
5691        // later verification context's stricter XML input policy.
5692        let document = XmlDocument::parse_with_settings(
5693            "<!DOCTYPE root [<!ENTITY value \"ok\">]><root>&value;</root>".into(),
5694            DocumentParseSettings::new(
5695                true,
5696                crate::hard_limits::XML_DOCUMENT_NODE_CEILING,
5697                crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
5698            ),
5699        )
5700        .expect("explicitly permitted DTD fixture must parse");
5701
5702        assert!(matches!(
5703            VerifyContext::new().verify_document(&document),
5704            Err(DsigError::Policy(
5705                crate::policy::PolicyViolation::XmlInput {
5706                    reason: "owned document requires internal DTD support"
5707                }
5708            ))
5709        ));
5710        assert!(!matches!(
5711            VerifyContext::new()
5712                .allow_internal_dtd(true)
5713                .verify_document(&document),
5714            Err(DsigError::Policy(
5715                crate::policy::PolicyViolation::XmlInput { .. }
5716            ))
5717        ));
5718    }
5719
5720    #[test]
5721    fn verification_policy_bounds_reference_canonicalization() {
5722        // Reference transforms and SignedInfo canonicalization are one operation;
5723        // references must not fall back to the transform hard-limit budget.
5724        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
5725        let xml = format!(
5726            r#"<root xmlns:ds="{XMLDSIG_NS}"><payload>{}</payload><ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI=""><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"#,
5727            "payload".repeat(16)
5728        );
5729        let policy = crate::policy::VerificationPolicy {
5730            resources: crate::policy::ResourcePolicy {
5731                max_canonicalized_bytes: 64,
5732                ..crate::policy::ResourcePolicy::default()
5733            },
5734            ..crate::policy::VerificationPolicy::default()
5735        };
5736
5737        let error = VerifyContext::new()
5738            .key(&AcceptingKey)
5739            .policy(policy)
5740            .verify(&xml)
5741            .expect_err("reference canonicalization must consume the policy budget");
5742
5743        assert!(
5744            matches!(
5745                error,
5746                SignatureVerificationPipelineError::Policy(
5747                    crate::policy::PolicyViolation::ResourceLimit {
5748                        resource: crate::policy::resource_name::CANONICALIZED_BYTES,
5749                        maximum: 64,
5750                        ..
5751                    }
5752                )
5753            ),
5754            "unexpected error: {error:?}"
5755        );
5756    }
5757
5758    #[test]
5759    fn verification_policy_bounds_document_bytes_before_parsing() {
5760        // A small node count does not bound parser work when one text node is
5761        // large, so the byte ceiling must reject before structural inspection.
5762        let xml = format!("<root>{}</root>", "x".repeat(1_024));
5763        let policy = crate::policy::VerificationPolicy {
5764            resources: crate::policy::ResourcePolicy {
5765                max_xml_document_bytes: xml.len() - 1,
5766                ..crate::policy::ResourcePolicy::default()
5767            },
5768            ..crate::policy::VerificationPolicy::default()
5769        };
5770
5771        assert!(matches!(
5772            VerifyContext::new().policy(policy).verify(&xml),
5773            Err(SignatureVerificationPipelineError::Policy(
5774                crate::policy::PolicyViolation::ResourceLimit {
5775                    resource: crate::policy::resource_name::XML_DOCUMENT,
5776                    maximum,
5777                    actual,
5778                }
5779            )) if maximum == xml.len() - 1 && actual == xml.len()
5780        ));
5781    }
5782
5783    #[test]
5784    fn verification_entry_points_enforce_policy_depth() {
5785        // Both borrowed XML and a retained document parsed under wider defaults
5786        // must be rejected before signature selection traverses the tree.
5787        let xml = "<root><child><leaf/></child></root>";
5788        let policy = crate::policy::VerificationPolicy {
5789            resources: crate::policy::ResourcePolicy {
5790                max_xml_depth: 2,
5791                ..crate::policy::ResourcePolicy::default()
5792            },
5793            ..crate::policy::VerificationPolicy::default()
5794        };
5795        let document = XmlDocument::parse(xml).expect("wide retained fixture must parse");
5796
5797        assert!(matches!(
5798            VerifyContext::new().policy(policy.clone()).verify(xml),
5799            Err(DsigError::Policy(
5800                crate::policy::PolicyViolation::ResourceLimit {
5801                    resource: crate::policy::resource_name::XML_DEPTH,
5802                    maximum: 2,
5803                    actual: 3,
5804                }
5805            ))
5806        ));
5807        assert!(matches!(
5808            VerifyContext::new()
5809                .policy(policy)
5810                .verify_document(&document),
5811            Err(DsigError::Policy(
5812                crate::policy::PolicyViolation::ResourceLimit {
5813                    resource: crate::policy::resource_name::XML_DEPTH,
5814                    maximum: 2,
5815                    actual: 3,
5816                }
5817            ))
5818        ));
5819    }
5820
5821    #[test]
5822    fn verification_policy_bounds_base64_transform_input() {
5823        // The operation snapshot must reach the transform executor rather than
5824        // silently falling back to its implementation-wide default budget.
5825        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
5826        let xml = format!(
5827            r##"<root xmlns:ds="{XMLDSIG_NS}"><payload ID="payload">QUJDRA==</payload><ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#payload"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#base64"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"##
5828        );
5829        let policy = crate::policy::VerificationPolicy {
5830            resources: crate::policy::ResourcePolicy {
5831                max_base64_transform_input_bytes: 4,
5832                ..crate::policy::ResourcePolicy::default()
5833            },
5834            ..crate::policy::VerificationPolicy::default()
5835        };
5836
5837        let error = VerifyContext::new()
5838            .key(&AcceptingKey)
5839            .policy(policy)
5840            .verify(&xml)
5841            .expect_err("Base64 input must use the operation policy ceiling");
5842
5843        assert!(matches!(
5844            error,
5845            SignatureVerificationPipelineError::Policy(
5846                crate::policy::PolicyViolation::ResourceLimit {
5847                    resource: crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
5848                    maximum: 4,
5849                    ..
5850                }
5851            )
5852        ));
5853    }
5854
5855    #[test]
5856    fn verification_policy_bounds_cumulative_base64_transform_output() {
5857        // References share one operation budget. Validating each decoded value
5858        // against the full ceiling would let a signature multiply output work.
5859        let first_digest = base64::engine::general_purpose::STANDARD
5860            .encode(compute_digest(DigestAlgorithm::Sha256, b"a"));
5861        let second_digest = base64::engine::general_purpose::STANDARD
5862            .encode(compute_digest(DigestAlgorithm::Sha256, b"b"));
5863        let xml = format!(
5864            r##"<root xmlns:ds="{XMLDSIG_NS}"><first ID="first">YQ==</first><second ID="second">Yg==</second><ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#first"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#base64"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{first_digest}</ds:DigestValue></ds:Reference><ds:Reference URI="#second"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#base64"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{second_digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"##
5865        );
5866        let policy = crate::policy::VerificationPolicy {
5867            resources: crate::policy::ResourcePolicy {
5868                max_base64_transform_input_bytes: 8,
5869                max_base64_transform_output_bytes: 1,
5870                ..crate::policy::ResourcePolicy::default()
5871            },
5872            ..crate::policy::VerificationPolicy::default()
5873        };
5874
5875        let error = VerifyContext::new()
5876            .key(&AcceptingKey)
5877            .policy(policy)
5878            .verify(&xml)
5879            .expect_err("references must share the Base64 output allowance");
5880
5881        assert!(matches!(
5882            error,
5883            SignatureVerificationPipelineError::Policy(
5884                crate::policy::PolicyViolation::ResourceLimit {
5885                    resource: crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
5886                    maximum: 1,
5887                    actual: 2,
5888                }
5889            )
5890        ));
5891    }
5892
5893    #[test]
5894    fn verification_policy_bounds_xpath_source_before_compilation() {
5895        // XPath parser limits are part of the same operation snapshot as the
5896        // evaluator limits; parsing cannot use a separate hard-coded budget.
5897        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
5898        let xml = format!(
5899            r#"<root xmlns:ds="{XMLDSIG_NS}"><ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI=""><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>true()</ds:XPath></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"#
5900        );
5901        let policy = crate::policy::VerificationPolicy {
5902            resources: crate::policy::ResourcePolicy {
5903                max_xpath_expression_bytes: 4,
5904                ..crate::policy::ResourcePolicy::default()
5905            },
5906            ..crate::policy::VerificationPolicy::default()
5907        };
5908
5909        let error = VerifyContext::new()
5910            .key(&AcceptingKey)
5911            .policy(policy)
5912            .verify(&xml)
5913            .expect_err("XPath source must use the operation policy ceiling");
5914
5915        assert!(matches!(
5916            error,
5917            SignatureVerificationPipelineError::Policy(
5918                crate::policy::PolicyViolation::ResourceLimit {
5919                    resource: crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
5920                    maximum: 4,
5921                    ..
5922                }
5923            )
5924        ));
5925    }
5926
5927    #[test]
5928    fn verification_policy_shares_canonicalization_budget_with_signed_info() {
5929        // Reference transforms and SignedInfo canonicalization are one operation.
5930        // Each output fits independently, but their aggregate must not receive
5931        // two separate copies of the configured canonicalization allowance.
5932        let payload_text = "x".repeat(700);
5933        let canonical_payload = format!("<payload ID=\"payload\">{payload_text}</payload>");
5934        let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
5935            DigestAlgorithm::Sha256,
5936            canonical_payload.as_bytes(),
5937        ));
5938        let xml = format!(
5939            r##"<root xmlns:ds="{XMLDSIG_NS}">{canonical_payload}<ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#payload"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"##
5940        );
5941        let policy = crate::policy::VerificationPolicy {
5942            resources: crate::policy::ResourcePolicy {
5943                max_canonicalized_bytes: 1_024,
5944                ..crate::policy::ResourcePolicy::default()
5945            },
5946            ..crate::policy::VerificationPolicy::default()
5947        };
5948
5949        let error = VerifyContext::new()
5950            .key(&AcceptingKey)
5951            .policy(policy)
5952            .verify(&xml)
5953            .expect_err("SignedInfo must consume the remaining operation C14N budget");
5954
5955        assert!(
5956            matches!(
5957                &error,
5958                SignatureVerificationPipelineError::Policy(
5959                    crate::policy::PolicyViolation::ResourceLimit {
5960                        resource: "canonicalized bytes",
5961                        maximum: 1_024,
5962                        ..
5963                    }
5964                )
5965            ),
5966            "unexpected error: {error:?}"
5967        );
5968    }
5969
5970    #[test]
5971    fn manifest_processing_stops_after_c14n_budget_exhaustion() {
5972        // A failed bounded render consumes the remaining operation allowance.
5973        // Later Manifest references, including cheap binary ones, must not run.
5974        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
5975        let xml = format!(
5976            r##"<root xmlns:ds="{XMLDSIG_NS}"><payload Id="payload">too large</payload><ds:Signature><ds:Object Id="signed-object"><ds:Manifest><ds:Reference URI="#payload"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference><ds:Reference URI="urn:small"><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:Manifest></ds:Object></ds:Signature></root>"##
5977        );
5978        let resources = HashMap::from([("urn:small".to_owned(), b"small".to_vec())]);
5979        let transform_budget = TransformExecutionBudget::with_c14n_limit(8);
5980        let ctx = VerifyContext::new()
5981            .allowed_uri_types(UriTypeSet::ALL)
5982            .external_resources(&resources);
5983        let document = XmlDocument::parse(xml).expect("test signature must parse");
5984        let results = document
5985            .with_view(|view| {
5986                let signature = view
5987                    .document()
5988                    .descendants()
5989                    .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
5990                    .expect("test signature must contain Signature");
5991                let object = signature
5992                    .children()
5993                    .find(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
5994                    .expect("test signature must contain Object");
5995                let resolver = UriReferenceResolver::with_document_view(view, &[])
5996                    .with_external_resources(&resources);
5997                let budgets = VerificationOperationBudgets::with_transforms(
5998                    &ctx.policy,
5999                    transform_budget,
6000                    ctx.utc_facts,
6001                );
6002                let mut operation = OperationExecutionContext::new(
6003                    &ctx.policy,
6004                    &budgets,
6005                    Some((view.identity(), view.generation())),
6006                );
6007                let crypto =
6008                    operation.add_node(OperationNodeKind::Crypto, OperationStage::Crypto, None);
6009                operation.compile().expect("base plan");
6010                operation
6011                    .run(crypto, || Ok::<_, SignatureVerificationPipelineError>(()))
6012                    .expect("crypto gate");
6013                operation.authenticate(view.node_identity(object));
6014                process_authenticated_manifest_references(
6015                    &mut operation,
6016                    view,
6017                    signature,
6018                    &resolver,
6019                    ManifestVerification {
6020                        context: &ctx,
6021                        capture: &(),
6022                    },
6023                    2,
6024                    crypto,
6025                )
6026                .map(|(results, _)| results)
6027            })
6028            .expect("resource exhaustion is reported per Manifest reference");
6029
6030        assert_eq!(results.len(), 2);
6031        assert!(results.iter().all(|result| {
6032            matches!(
6033                result.status,
6034                DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { .. })
6035            )
6036        }));
6037    }
6038
6039    #[test]
6040    fn verification_policy_bounds_detached_xml_nodes() {
6041        // Caller-owned detached octets become a second XML document during a
6042        // node-set transform and must inherit the same operation node ceiling.
6043        let detached = format!("<payload>{}</payload>", "<n/>".repeat(32));
6044        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
6045        let xml = format!(
6046            r#"<root xmlns:ds="{XMLDSIG_NS}"><ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="urn:detached-nodes"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>true()</ds:XPath></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature></root>"#
6047        );
6048        let resources = HashMap::from([("urn:detached-nodes".to_owned(), detached.into_bytes())]);
6049        let policy = crate::policy::VerificationPolicy {
6050            uris: crate::policy::UriPolicy {
6051                references: UriTypeSet::ALL,
6052                ..crate::policy::UriPolicy::default()
6053            },
6054            resources: crate::policy::ResourcePolicy {
6055                max_xml_nodes: 24,
6056                ..crate::policy::ResourcePolicy::default()
6057            },
6058            ..crate::policy::VerificationPolicy::default()
6059        };
6060
6061        let error = VerifyContext::new()
6062            .key(&AcceptingKey)
6063            .policy(policy)
6064            .external_resources(&resources)
6065            .verify(&xml)
6066            .expect_err("detached XML must inherit the policy node ceiling");
6067
6068        assert!(
6069            matches!(
6070                error,
6071                SignatureVerificationPipelineError::Policy(
6072                    crate::policy::PolicyViolation::ResourceLimit {
6073                        resource: crate::policy::resource_name::XML_NODES,
6074                        maximum: 24,
6075                        ..
6076                    }
6077                )
6078            ),
6079            "unexpected error: {error:?}"
6080        );
6081    }
6082
6083    #[test]
6084    fn query_only_reference_resolves_against_relative_xml_base() {
6085        // A query-only URI replaces the inherited base query without changing
6086        // its relative path; no absolute document base is required by XML Base.
6087        let payload = b"query-selected payload";
6088        let digest = base64::engine::general_purpose::STANDARD
6089            .encode(compute_digest(DigestAlgorithm::Sha256, payload));
6090        let xml = format!(
6091            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo>
6092                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6093                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6094                <ds:Reference xml:base="a/b?old" URI="?new">
6095                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6096                    <ds:DigestValue>{digest}</ds:DigestValue>
6097                </ds:Reference>
6098            </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>"#
6099        );
6100        let document = Document::parse(&xml).unwrap();
6101        let signature = document.root_element();
6102        let signed_info_node = signature
6103            .children()
6104            .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
6105            .unwrap();
6106        let signed_info = parse_signed_info(signed_info_node).unwrap();
6107        let resources = HashMap::from([("a/b?new".to_string(), payload.to_vec())]);
6108        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6109
6110        let result = process_all_references(&signed_info.references, &resolver, signature, false)
6111            .expect("query-only URI must resolve against the complete relative base path");
6112
6113        assert!(result.all_valid());
6114    }
6115
6116    #[test]
6117    fn manifest_reference_resolution_uses_its_effective_xml_base() {
6118        // Manifest references carry their own XML Base context and must not
6119        // accidentally reuse the SignedInfo or Signature element context.
6120        let payload = b"manifest payload";
6121        let digest = base64::engine::general_purpose::STANDARD
6122            .encode(compute_digest(DigestAlgorithm::Sha256, payload));
6123        let xml = format!(
6124            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
6125                <ds:Object><ds:Manifest xml:base="manifests/">
6126                    <ds:Reference URI="payload.bin">
6127                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6128                        <ds:DigestValue>{digest}</ds:DigestValue>
6129                    </ds:Reference>
6130                </ds:Manifest></ds:Object>
6131            </ds:Signature>"#
6132        );
6133        let document = Document::parse(&xml).unwrap();
6134        let signature = document.root_element();
6135        let reference_node = signature
6136            .descendants()
6137            .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
6138            .unwrap();
6139        let reference = super::super::parse::parse_reference(reference_node).unwrap();
6140        let resources = HashMap::from([(
6141            "https://example.test/manifests/payload.bin".to_string(),
6142            payload.to_vec(),
6143        )]);
6144        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6145
6146        let result = process_reference(
6147            &reference,
6148            &resolver,
6149            signature,
6150            ReferenceSet::Manifest,
6151            0,
6152            false,
6153        )
6154        .expect("Manifest Reference should inherit its own XML Base context");
6155
6156        assert_eq!(result.status, DsigStatus::Valid);
6157    }
6158
6159    #[test]
6160    fn manifest_reference_index_ignores_nested_manifest_descendants() {
6161        // The public Manifest index follows Signature/Object/Manifest structure;
6162        // wrapper descendants must not steal an index and supply another base URI.
6163        let payload = b"direct manifest payload";
6164        let digest = base64::engine::general_purpose::STANDARD
6165            .encode(compute_digest(DigestAlgorithm::Sha256, payload));
6166        let xml = format!(
6167            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
6168                <ds:Object><wrapper><ds:Manifest xml:base="nested/">
6169                    <ds:Reference URI="payload.bin">
6170                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6171                        <ds:DigestValue>{digest}</ds:DigestValue>
6172                    </ds:Reference>
6173                </ds:Manifest></wrapper></ds:Object>
6174                <ds:Object><ds:Manifest xml:base="direct/">
6175                    <ds:Reference URI="payload.bin">
6176                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6177                        <ds:DigestValue>{digest}</ds:DigestValue>
6178                    </ds:Reference>
6179                </ds:Manifest></ds:Object>
6180            </ds:Signature>"#
6181        );
6182        let document = Document::parse(&xml).unwrap();
6183        let signature = document.root_element();
6184        let direct_reference_node = signature
6185            .children()
6186            .filter(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
6187            .nth(1)
6188            .unwrap()
6189            .children()
6190            .find(|node| node.has_tag_name((XMLDSIG_NS, "Manifest")))
6191            .unwrap()
6192            .children()
6193            .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
6194            .unwrap();
6195        let reference = super::super::parse::parse_reference(direct_reference_node).unwrap();
6196        let resources = HashMap::from([(
6197            "https://example.test/direct/payload.bin".to_string(),
6198            payload.to_vec(),
6199        )]);
6200        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
6201
6202        let result = process_reference(
6203            &reference,
6204            &resolver,
6205            signature,
6206            ReferenceSet::Manifest,
6207            0,
6208            false,
6209        )
6210        .expect("Manifest index must select the direct Object/Manifest reference");
6211
6212        assert_eq!(result.status, DsigStatus::Valid);
6213    }
6214
6215    struct RejectingKey;
6216
6217    impl VerifyingKey for RejectingKey {
6218        fn verify(
6219            &self,
6220            _algorithm: SignatureAlgorithm,
6221            _signed_data: &[u8],
6222            _signature_value: &[u8],
6223        ) -> Result<bool, SignatureVerificationPipelineError> {
6224            Ok(false)
6225        }
6226    }
6227
6228    struct AcceptingKey;
6229
6230    impl VerifyingKey for AcceptingKey {
6231        fn verify(
6232            &self,
6233            _algorithm: SignatureAlgorithm,
6234            _signed_data: &[u8],
6235            _signature_value: &[u8],
6236        ) -> Result<bool, SignatureVerificationPipelineError> {
6237            Ok(true)
6238        }
6239    }
6240
6241    struct PanicResolver;
6242
6243    struct UntrustedTestResolver;
6244
6245    impl KeyResolver for UntrustedTestResolver {
6246        fn resolve<'a>(
6247            &'a self,
6248            _key_info: Option<&KeyInfo>,
6249            _algorithm: SignatureAlgorithm,
6250        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
6251            Ok(Some(Box::new(AcceptingKey)))
6252        }
6253    }
6254
6255    #[test]
6256    fn verification_requires_explicit_key_authorization() {
6257        // A resolver finding a working key must not authorize its signer.
6258        let xml = signature_with_target_reference("AQ==");
6259        let error = VerifyContext::new()
6260            .key_resolver(&UntrustedTestResolver)
6261            .verify(&xml)
6262            .expect_err("default verification must reject an untrusted resolver candidate");
6263        assert!(matches!(
6264            error,
6265            DsigError::Policy(crate::policy::PolicyViolation::KeyTrust { .. })
6266        ));
6267    }
6268
6269    #[test]
6270    fn cryptographic_only_is_explicit_and_never_reports_trust() {
6271        // Opting out of signer authorization must remain visible in the result.
6272        let mut policy = crate::policy::VerificationPolicy::default();
6273        policy.key_trust.mode = crate::policy::VerificationTrustMode::CryptographicOnly;
6274        let result = VerifyContext::new()
6275            .policy(policy)
6276            .key_resolver(&UntrustedTestResolver)
6277            .verify(&signature_with_target_reference("AQ=="))
6278            .expect("explicit mathematical mode permits an untrusted candidate");
6279        assert_eq!(result.status, DsigStatus::Valid);
6280        assert_eq!(
6281            result.key_trust,
6282            super::super::KeyTrustEvidence::NotEstablished
6283        );
6284    }
6285
6286    #[test]
6287    fn caller_key_authorization_does_not_authenticate_an_invalid_signature() {
6288        // A trusted key is not evidence that the signature itself verified.
6289        let result = VerifyContext::new()
6290            .key(&RejectingKey)
6291            .verify(&signature_with_target_reference("AQ=="))
6292            .expect("an invalid mathematical signature is a completed result");
6293        assert_eq!(
6294            result.status,
6295            DsigStatus::Invalid(FailureReason::SignatureMismatch)
6296        );
6297        assert_eq!(
6298            result.key_trust,
6299            super::super::KeyTrustEvidence::CallerTrusted
6300        );
6301    }
6302
6303    impl KeyResolver for PanicResolver {
6304        fn resolve<'a>(
6305            &'a self,
6306            _key_info: Option<&KeyInfo>,
6307            _algorithm: SignatureAlgorithm,
6308        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
6309        {
6310            panic!("resolver should not be called when references already fail");
6311        }
6312    }
6313
6314    struct MissingKeyResolver;
6315
6316    impl KeyResolver for MissingKeyResolver {
6317        fn resolve<'a>(
6318            &'a self,
6319            _key_info: Option<&KeyInfo>,
6320            _algorithm: SignatureAlgorithm,
6321        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
6322        {
6323            Ok(None)
6324        }
6325    }
6326
6327    struct ConsumingKeyInfoResolver;
6328
6329    impl KeyResolver for ConsumingKeyInfoResolver {
6330        fn resolve<'a>(
6331            &'a self,
6332            _key_info: Option<&KeyInfo>,
6333            _algorithm: SignatureAlgorithm,
6334        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
6335        {
6336            Ok(None)
6337        }
6338
6339        fn consumes_document_key_info(&self) -> bool {
6340            true
6341        }
6342    }
6343
6344    struct FallbackKeyInfoResolver;
6345
6346    impl KeyResolver for FallbackKeyInfoResolver {
6347        fn resolve_for_verification<'a>(
6348            &'a self,
6349            key_info: Option<&KeyInfo>,
6350            algorithm: SignatureAlgorithm,
6351            _policy: &crate::policy::VerificationPolicy,
6352            _provider: &dyn crate::provider::CryptoProvider,
6353        ) -> Result<Option<super::super::ResolvedVerificationKey<'a>>, DsigError> {
6354            // This test resolver explicitly authorizes its application-owned key.
6355            self.resolve(key_info, algorithm)
6356                .map(|key| key.map(super::super::ResolvedVerificationKey::CallerTrusted))
6357        }
6358
6359        fn resolve<'a>(
6360            &'a self,
6361            key_info: Option<&KeyInfo>,
6362            _algorithm: SignatureAlgorithm,
6363        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
6364        {
6365            let sources = &key_info.expect("KeyInfo must be parsed").sources;
6366            assert!(matches!(
6367                sources.as_slice(),
6368                [
6369                    super::super::parse::KeyInfoSource::RetrievalMethod { .. },
6370                    super::super::parse::KeyInfoSource::KeyName(name),
6371                ] if name == "fallback"
6372            ));
6373            Ok(Some(Box::new(AcceptingKey)))
6374        }
6375
6376        fn consumes_document_key_info(&self) -> bool {
6377            true
6378        }
6379    }
6380
6381    struct EarlyKeyInfoResolver;
6382
6383    impl KeyResolver for EarlyKeyInfoResolver {
6384        fn resolve_for_verification<'a>(
6385            &'a self,
6386            key_info: Option<&KeyInfo>,
6387            algorithm: SignatureAlgorithm,
6388            _policy: &crate::policy::VerificationPolicy,
6389            _provider: &dyn crate::provider::CryptoProvider,
6390        ) -> Result<Option<super::super::ResolvedVerificationKey<'a>>, DsigError> {
6391            // Only the selected primary application key is authorized here.
6392            self.resolve(key_info, algorithm)
6393                .map(|key| key.map(super::super::ResolvedVerificationKey::CallerTrusted))
6394        }
6395
6396        fn resolve<'a>(
6397            &'a self,
6398            key_info: Option<&KeyInfo>,
6399            _algorithm: SignatureAlgorithm,
6400        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
6401        {
6402            let sources = &key_info.expect("KeyInfo must be parsed").sources;
6403            assert!(matches!(
6404                sources.as_slice(),
6405                [
6406                    super::super::parse::KeyInfoSource::KeyName(name),
6407                    super::super::parse::KeyInfoSource::RetrievalMethod { .. },
6408                ] if name == "primary"
6409            ));
6410            Ok(Some(Box::new(AcceptingKey)))
6411        }
6412
6413        fn consumes_document_key_info(&self) -> bool {
6414            true
6415        }
6416    }
6417
6418    fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
6419        format!(
6420            r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6421  <ds:SignedInfo>
6422    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6423    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6424    <ds:Reference URI="{reference_uri}">
6425      {transforms_xml}
6426      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
6427      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6428    </ds:Reference>
6429  </ds:SignedInfo>
6430  <ds:SignatureValue>AQ==</ds:SignatureValue>
6431</ds:Signature>"#
6432        )
6433    }
6434
6435    #[test]
6436    fn retained_verification_preserves_original_authenticated_operation() {
6437        // Keep the original per-Signature graph/authentication state, not a new
6438        // set reconstructed from detached digest-result booleans.
6439        use structured_asn1::memory::{Memory, MemoryAccount};
6440        let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(16384, 100000)));
6441        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6442        let context = VerifyContext::new().key(&AcceptingKey);
6443        with_verified_document(
6444            &document,
6445            &context,
6446            &VerificationRequest::default(),
6447            ScopedRequestStorage(&work),
6448            |view, _, budgets, evidence, operations, _| {
6449                assert_eq!(evidence.signatures().len(), 1);
6450                assert_eq!(operations.len(), 1);
6451                let target = view.node_for_id("target", &[]).unwrap();
6452                assert!(operations[0].operation.is_authenticated(target));
6453                assert!(std::ptr::eq(*operations[0].operation.budgets(), budgets));
6454                assert!(budgets.canonicalized.remaining() < budgets.canonicalized.max_bytes);
6455                Ok(())
6456            },
6457        )
6458        .unwrap();
6459    }
6460
6461    #[test]
6462    fn retained_verification_accepts_borrowed_keys_and_admits_storage_first() {
6463        // The scoped storage projection must not impose 'static on request
6464        // keys. Refused report/state storage cannot reach the provider.
6465        use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
6466        struct BorrowedKey<'a>(&'a Cell<usize>);
6467        impl VerifyingKey for BorrowedKey<'_> {
6468            fn validate_signature_value(
6469                &self,
6470                _: SignatureAlgorithm,
6471                _: &[u8],
6472            ) -> Result<bool, DsigError> {
6473                Ok(true)
6474            }
6475            fn verify(&self, _: SignatureAlgorithm, _: &[u8], _: &[u8]) -> Result<bool, DsigError> {
6476                self.0.set(self.0.get() + 1);
6477                Ok(true)
6478            }
6479        }
6480        let calls = Cell::new(0);
6481        let key = BorrowedKey(&calls);
6482        let context = VerifyContext::new().key(&key);
6483        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6484        let denied = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 100000)));
6485        let result: Result<(), DsigError> = with_verified_document(
6486            &document,
6487            &context,
6488            &VerificationRequest::default(),
6489            ScopedRequestStorage(&denied),
6490            |_, _, _, _, _, _| panic!("refused storage must not reach the continuation"),
6491        );
6492        assert!(matches!(
6493            result,
6494            Err(DsigError::SharedExecution(MemoryError::LimitExceeded))
6495        ));
6496        assert_eq!(calls.get(), 0);
6497        let admitted = Memory::new(std::sync::Arc::new(MemoryAccount::new(16384, 100000)));
6498        with_verified_document(
6499            &document,
6500            &context,
6501            &VerificationRequest::default(),
6502            ScopedRequestStorage(&admitted),
6503            |_, _, _, evidence, operations, _| {
6504                assert!(evidence.all_valid());
6505                assert_eq!(operations.len(), 1);
6506                Ok(())
6507            },
6508        )
6509        .unwrap();
6510        assert_eq!(calls.get(), 1);
6511    }
6512
6513    #[test]
6514    fn verified_continuation_keeps_the_consumed_operation_budget() {
6515        // Extension work continues after core verification with the very same
6516        // canonicalization allowance, not a fresh budget built from policy.
6517        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6518        let context = VerifyContext::new().key(&AcceptingKey);
6519        let called = Cell::new(false);
6520        let evidence = with_verified_document(
6521            &document,
6522            &context,
6523            &VerificationRequest::default(),
6524            CollectedRequestStorage,
6525            |view, _captured_context, budgets, evidence, (), _| {
6526                called.set(true);
6527                assert_eq!(view.root(), evidence.document);
6528                assert!(evidence.signatures()[0].result().is_ok());
6529                let remaining = budgets.canonicalized.remaining();
6530                assert!(remaining < budgets.canonicalized.max_bytes);
6531                budgets.canonicalized.charge(remaining).unwrap();
6532                assert!(budgets.canonicalized.charge(1).is_err());
6533                Ok(evidence)
6534            },
6535        )
6536        .unwrap();
6537        assert!(called.get());
6538        assert_eq!(evidence.signatures().len(), 1);
6539    }
6540
6541    #[test]
6542    fn public_session_catalogue_replays_the_original_reference() {
6543        // The public continuation must retain the selected operation, expose
6544        // original Reference/parent identities, and stream the verified bytes.
6545        use structured_asn1::memory::{Memory, MemoryAccount};
6546        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6547        let context = VerifyContext::new().key(&AcceptingKey);
6548        let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
6549        with_verified_request(
6550            &document,
6551            &context,
6552            &VerificationRequest::default(),
6553            &work,
6554            |session| {
6555                let signature = session.evidence().signatures()[0].identity();
6556                let selected = session.select_signature(signature)?;
6557                let mut count = 0;
6558                let completeness = selected
6559                    .visit_authenticated_references(|reference| {
6560                        count += 1;
6561                        assert!(matches!(reference.parent, ReferenceParent::SignedInfo(_)));
6562                        assert_eq!(reference.uri, Some("#target"));
6563                        let mut bytes = Vec::new();
6564                        let outcome = selected
6565                            .replay_reference(
6566                                reference.identity,
6567                                ReplayMode::ReferenceOutput,
6568                                &mut |chunk: &[u8]| {
6569                                    bytes.extend_from_slice(chunk);
6570                                    Ok::<_, ()>(())
6571                                },
6572                            )
6573                            .unwrap();
6574                        assert_eq!(outcome, ReplayOutcome::Complete);
6575                        assert_eq!(bytes, b"<target ID=\"target\">payload</target>");
6576                        Ok::<_, ()>(())
6577                    })
6578                    .unwrap();
6579                assert_eq!(completeness, CatalogueCompleteness::Complete);
6580                assert_eq!(count, 1);
6581                Ok(())
6582            },
6583        )
6584        .unwrap();
6585    }
6586
6587    #[test]
6588    fn public_session_resolves_original_attribute_and_fragmented_simple_content() {
6589        // Both schema lexical sources must resolve through the same original
6590        // ID index. Fragmented text must not drop text after comments or CDATA.
6591        use structured_asn1::memory::{Memory, MemoryAccount};
6592        let document = XmlDocument::parse(signature_with_target_content(
6593            "AQ==",
6594            "<property URI=\"#ref\"/><content>#r<!--split--><![CDATA[ef]]></content>",
6595        ))
6596        .unwrap();
6597        let context = VerifyContext::new().key(&AcceptingKey);
6598        let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
6599        with_verified_request(
6600            &document,
6601            &context,
6602            &VerificationRequest::default(),
6603            &work,
6604            |session| {
6605                let selected =
6606                    session.select_signature(session.evidence().signatures()[0].identity())?;
6607                for (name, source) in [
6608                    (
6609                        "property",
6610                        LexicalSource::Attribute {
6611                            namespace: None,
6612                            local_name: "URI",
6613                        },
6614                    ),
6615                    ("content", LexicalSource::SimpleContent),
6616                ] {
6617                    let node = session
6618                        .view()
6619                        .document()
6620                        .descendants()
6621                        .find(|node| node.has_tag_name(name))
6622                        .unwrap();
6623                    let ReferenceResolution::Reference(reference) =
6624                        selected.resolve_reference(session.view().node_identity(node), source)?
6625                    else {
6626                        panic!("original Reference must resolve");
6627                    };
6628                    assert_eq!(reference.uri, Some("#target"));
6629                }
6630                Ok(())
6631            },
6632        )
6633        .unwrap();
6634    }
6635
6636    #[test]
6637    fn public_session_manifest_instruction_survives_old_digest_mismatch() {
6638        // Renewal covers the authenticated instruction with a new digest. It
6639        // must not require the old target digest to match first (EN §5.5.3).
6640        use structured_asn1::memory::{Memory, MemoryAccount};
6641        let document = XmlDocument::parse(signature_with_manifest_xml(false)).unwrap();
6642        let context = VerifyContext::new()
6643            .key(&AcceptingKey)
6644            .process_manifests(true);
6645        let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
6646        with_verified_request(
6647            &document,
6648            &context,
6649            &VerificationRequest::default(),
6650            &work,
6651            |session| {
6652                let selected =
6653                    session.select_signature(session.evidence().signatures()[0].identity())?;
6654                let mut manifests = 0;
6655                let completeness = selected
6656                    .visit_authenticated_references(|reference| {
6657                        if let ReferenceParent::Manifest(_) = reference.parent {
6658                            manifests += 1;
6659                            assert!(matches!(
6660                                reference.result.unwrap().status,
6661                                DsigStatus::Invalid(_)
6662                            ));
6663                            let mut calls = 0;
6664                            let outcome = selected
6665                                .replay_reference(
6666                                    reference.identity,
6667                                    ReplayMode::ReferenceOutput,
6668                                    &mut |_: &[u8]| {
6669                                        calls += 1;
6670                                        Ok::<_, ()>(())
6671                                    },
6672                                )
6673                                .unwrap();
6674                            assert_eq!(outcome, ReplayOutcome::Complete);
6675                            assert!(calls > 0);
6676                        }
6677                        Ok::<_, ()>(())
6678                    })
6679                    .unwrap();
6680                assert_eq!(manifests, 1);
6681                assert_eq!(completeness, CatalogueCompleteness::Complete);
6682                Ok(())
6683            },
6684        )
6685        .unwrap();
6686    }
6687
6688    #[test]
6689    fn public_session_replay_keeps_original_receiver_failure_sticky() {
6690        // No later C14N fragment may overwrite a typed caller refusal or invoke
6691        // the receiver a second time after the first error.
6692        use structured_asn1::memory::{Memory, MemoryAccount};
6693        #[derive(Debug, PartialEq)]
6694        struct Refusal(u32);
6695        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6696        let context = VerifyContext::new().key(&AcceptingKey);
6697        let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
6698        with_verified_request(
6699            &document,
6700            &context,
6701            &VerificationRequest::default(),
6702            &work,
6703            |session| {
6704                let selected =
6705                    session.select_signature(session.evidence().signatures()[0].identity())?;
6706                selected
6707                    .visit_authenticated_references(|reference| {
6708                        let mut calls = 0;
6709                        let result = selected.replay_reference(
6710                            reference.identity,
6711                            ReplayMode::ReferenceOutput,
6712                            &mut |_: &[u8]| {
6713                                calls += 1;
6714                                Err(Refusal(7))
6715                            },
6716                        );
6717                        assert!(matches!(result, Err(ReplayError::Receiver(Refusal(7)))));
6718                        assert_eq!(calls, 1);
6719                        Ok::<_, ()>(())
6720                    })
6721                    .unwrap();
6722                Ok(())
6723            },
6724        )
6725        .unwrap();
6726    }
6727
6728    #[test]
6729    fn verify_all_retains_each_signature_without_vacuous_success() {
6730        // Malformed signatures remain individually visible; an empty document
6731        // never satisfies a request merely because there were no failures.
6732        let document = XmlDocument::parse(format!(
6733            "<root xmlns:ds=\"{XMLDSIG_NS}\"><ds:Signature/><ds:Signature/></root>"
6734        ))
6735        .unwrap();
6736        let evidence = VerifyContext::new().verify_all(&document).unwrap();
6737        assert_eq!(evidence.signatures().len(), 2);
6738        assert!(
6739            evidence
6740                .signatures()
6741                .iter()
6742                .all(|entry| entry.result().is_err())
6743        );
6744        assert!(!evidence.all_valid());
6745        let empty = XmlDocument::parse("<root/>").unwrap();
6746        assert!(!VerifyContext::new().verify_all(&empty).unwrap().all_valid());
6747    }
6748
6749    #[test]
6750    fn verify_all_checks_cardinality_before_running_keys() {
6751        // The cardinality policy bounds report allocation and crypto work,
6752        // including malformed signatures that never reach core validation.
6753        let document = XmlDocument::parse(format!(
6754            "<root xmlns:ds=\"{XMLDSIG_NS}\"><ds:Signature/><ds:Signature/></root>"
6755        ))
6756        .unwrap();
6757        let mut policy = crate::policy::VerificationPolicy::default();
6758        policy.resources.max_signatures = 1;
6759        assert!(matches!(
6760            VerifyContext::new().policy(policy).verify_all(&document),
6761            Err(DsigError::Policy(
6762                crate::policy::PolicyViolation::ResourceLimit {
6763                    resource: "verification signatures",
6764                    maximum: 1,
6765                    actual: 2,
6766                }
6767            ))
6768        ));
6769    }
6770
6771    #[test]
6772    fn verification_signature_limit_also_gates_the_single_api() {
6773        // The same compiled resource policy must apply to both public paths.
6774        let mut policy = crate::policy::VerificationPolicy::default();
6775        policy.resources.max_signatures = 0;
6776        assert!(matches!(
6777            VerifyContext::new()
6778                .policy(policy)
6779                .key(&AcceptingKey)
6780                .verify(&signature_with_target_reference("AQ==")),
6781            Err(DsigError::Policy(
6782                crate::policy::PolicyViolation::ResourceLimit {
6783                    resource: "verification signatures",
6784                    maximum: 0,
6785                    actual: 1,
6786                }
6787            ))
6788        ));
6789    }
6790
6791    #[test]
6792    fn verify_all_preserves_generation_and_rejects_wrapped_siblings() {
6793        // A signature over one element never authenticates a lookalike sibling.
6794        let xml = signature_with_target_reference("AQ==")
6795            .replace("</root>", "<target ID=\"unsigned\">payload</target></root>");
6796        let document = XmlDocument::parse(xml).unwrap();
6797        let (signed, unsigned) = document.with_view(|view| {
6798            (
6799                view.node_for_id("target", &[]).unwrap(),
6800                view.node_for_id("unsigned", &[]).unwrap(),
6801            )
6802        });
6803        let evidence = VerifyContext::new()
6804            .key(&AcceptingKey)
6805            .verify_all(&document)
6806            .unwrap();
6807        assert!(evidence.all_valid());
6808        assert!(evidence.covers_element(&document, signed).unwrap());
6809        assert!(!evidence.covers_element(&document, unsigned).unwrap());
6810        let foreign = XmlDocument::parse("<target/>").unwrap();
6811        let target = foreign.with_view(|view| view.root_element());
6812        assert!(matches!(
6813            evidence.covers_element(&document, target),
6814            Err(DsigError::Document(XmlDocumentError::ForeignIdentity))
6815        ));
6816    }
6817
6818    #[test]
6819    fn verify_request_rejects_duplicate_targets_before_crypto() {
6820        // Ambiguous request mapping is not a successful duplicate assertion.
6821        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6822        let target = document.with_view(|view| view.node_for_id("target", &[]).unwrap());
6823        let targets = [target, target];
6824        let request = VerificationRequest {
6825            expected_targets: &targets,
6826            ..VerificationRequest::default()
6827        };
6828        assert!(matches!(
6829            VerifyContext::new().verify_request(&document, &request),
6830            Err(DsigError::InvalidRequest {
6831                reason: "duplicate expected coverage target"
6832            })
6833        ));
6834    }
6835
6836    #[test]
6837    fn verify_all_shares_canonicalization_retention_budget() {
6838        // Repeating a Signature may not reset the retained-data allowance.
6839        let xml = signature_with_target_reference("AQ==");
6840        let single = VerifyContext::new()
6841            .key(&AcceptingKey)
6842            .store_pre_digest(true)
6843            .verify(&xml)
6844            .unwrap();
6845        let bytes = single.canonicalized_signed_info.unwrap().len()
6846            + single.signed_info_references[0]
6847                .pre_digest_data
6848                .as_ref()
6849                .unwrap()
6850                .len();
6851        let parsed = Document::parse(&xml).unwrap();
6852        let signature = parsed
6853            .descendants()
6854            .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
6855            .unwrap();
6856        let xml = xml.replace("</root>", &format!("{}</root>", &xml[signature.range()]));
6857        let document = XmlDocument::parse(xml).unwrap();
6858        let mut policy = crate::policy::VerificationPolicy::default();
6859        policy.resources.max_canonicalized_bytes = bytes * 2 - 1;
6860        let evidence = VerifyContext::new()
6861            .key(&AcceptingKey)
6862            .store_pre_digest(true)
6863            .policy(policy)
6864            .verify_all(&document)
6865            .unwrap();
6866        assert_eq!(evidence.signatures.len(), 2);
6867        assert_eq!(
6868            evidence.signatures[0].result().as_ref().unwrap().status,
6869            DsigStatus::Valid
6870        );
6871        assert!(matches!(
6872            evidence.signatures[1].result(),
6873            Err(DsigError::Policy(
6874                crate::policy::PolicyViolation::ResourceLimit {
6875                    resource: "canonicalized bytes",
6876                    ..
6877                }
6878            ))
6879        ));
6880        assert!(!evidence.accepted(&document).unwrap());
6881    }
6882
6883    #[test]
6884    fn verify_all_manifest_coverage_requires_its_authenticated_digest() {
6885        // A valid enclosing Signature authenticates the Manifest declaration,
6886        // not a target whose separately checked Manifest digest mismatches.
6887        for valid in [true, false] {
6888            let document = XmlDocument::parse(signature_with_manifest_xml(valid)).unwrap();
6889            let target = document.with_view(|view| view.node_for_id("target", &[]).unwrap());
6890            let evidence = VerifyContext::new()
6891                .key(&AcceptingKey)
6892                .process_manifests(true)
6893                .verify_all(&document)
6894                .unwrap();
6895            let result = evidence.signatures()[0].result().as_ref().unwrap();
6896            assert_eq!(result.manifest_references.len(), 1);
6897            assert!(result.manifest_references[0].reference_identity.is_some());
6898            assert_eq!(evidence.covers_element(&document, target).unwrap(), valid);
6899            assert_eq!(evidence.accepted(&document).unwrap(), valid);
6900        }
6901    }
6902
6903    #[test]
6904    fn verify_all_filtered_digest_does_not_claim_original_subtree() {
6905        // A projection may discard content even when its local digest matches.
6906        // No arbitrary filter is promoted to complete original-node coverage.
6907        let mut reference = make_reference(
6908            "#target",
6909            vec![Transform::XpathExcludeAllSignatures],
6910            DigestAlgorithm::Sha256,
6911            vec![],
6912        );
6913        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
6914        document.with_view(|view| {
6915            let target = view.node_for_id("target", &[]).unwrap();
6916            let mut result = manifest_reference_invalid_result(
6917                &reference,
6918                0,
6919                FailureReason::ReferenceProcessingFailure { ref_index: 0 },
6920            );
6921            result.status = DsigStatus::Valid;
6922            bind_reference_evidence(
6923                &mut result,
6924                &reference,
6925                view,
6926                None,
6927                &OperationResourceIdentity::DocumentNode(target),
6928            );
6929            assert_eq!(result.target_identity, Some(target));
6930            assert_eq!(result.coverage, ReferenceCoverage::TransformedData);
6931            reference.transforms = vec![Transform::C14n(crate::c14n::C14nAlgorithm::new(
6932                crate::c14n::C14nMode::Exclusive1_0,
6933                false,
6934            ))];
6935            bind_reference_evidence(
6936                &mut result,
6937                &reference,
6938                view,
6939                None,
6940                &OperationResourceIdentity::DocumentNode(target),
6941            );
6942            assert_eq!(result.coverage, ReferenceCoverage::ElementContent);
6943        });
6944    }
6945
6946    #[test]
6947    fn verify_request_rejects_conflicting_resource_owners() {
6948        // There is no implicit override between request bytes and old builder
6949        // configuration: ownership must be explicit before resolver work.
6950        let document = XmlDocument::parse("<root/>").unwrap();
6951        let resources = HashMap::new();
6952        let request = VerificationRequest {
6953            external_resources: Some(&resources),
6954            ..VerificationRequest::default()
6955        };
6956        assert!(matches!(
6957            VerifyContext::new()
6958                .external_resources(&resources)
6959                .verify_request(&document, &request),
6960            Err(DsigError::InvalidRequest {
6961                reason: "external resources configured in both context and request"
6962            })
6963        ));
6964    }
6965
6966    fn signature_with_target_reference(signature_value_b64: &str) -> String {
6967        signature_with_target_content(signature_value_b64, "payload")
6968            .replace("<ds:Reference Id=\"ref\"", "<ds:Reference")
6969    }
6970
6971    fn signature_with_target_content(signature_value_b64: &str, content: &str) -> String {
6972        let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6973  <target ID="target">payload</target>
6974  <ds:Signature>
6975    <ds:SignedInfo>
6976      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6977      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6978      <ds:Reference Id="ref" URI="#target">
6979        <ds:Transforms>
6980          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6981        </ds:Transforms>
6982        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
6983        <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6984      </ds:Reference>
6985    </ds:SignedInfo>
6986    <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
6987  </ds:Signature>
6988</root>"##
6989            .replace(">payload</target>", &format!(">{content}</target>"));
6990
6991        let doc = Document::parse(&xml_template).unwrap();
6992        let sig_node = doc
6993            .descendants()
6994            .find(|node| node.is_element() && node.tag_name().name() == "Signature")
6995            .unwrap();
6996        let signed_info_node = sig_node
6997            .children()
6998            .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
6999            .unwrap();
7000        let signed_info = parse_signed_info(signed_info_node).unwrap();
7001        let reference = &signed_info.references[0];
7002        let resolver = UriReferenceResolver::new(&doc);
7003        let initial_data = resolver
7004            .dereference(reference.uri.as_deref().unwrap())
7005            .unwrap();
7006        let pre_digest =
7007            crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
7008                .unwrap();
7009        let digest = compute_digest(reference.digest_method, &pre_digest);
7010        let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
7011        xml_template
7012            .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
7013            .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
7014    }
7015
7016    #[test]
7017    fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
7018        let xml = signature_with_target_reference("AQ==");
7019
7020        let result = VerifyContext::new()
7021            .verify(&xml)
7022            .expect("missing key config must be reported as verification status");
7023        assert!(
7024            matches!(
7025                result.status,
7026                DsigStatus::Invalid(FailureReason::KeyNotFound)
7027            ),
7028            "unexpected status: {:?}",
7029            result.status
7030        );
7031    }
7032
7033    #[test]
7034    fn verify_context_rejects_disallowed_uri() {
7035        let xml = minimal_signature_xml("http://example.com/external", "");
7036        let err = VerifyContext::new()
7037            .key(&RejectingKey)
7038            .verify(&xml)
7039            .expect_err("external URI should be rejected by default policy");
7040        assert!(matches!(
7041            err,
7042            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
7043                operation: "verification",
7044                ..
7045            })
7046        ));
7047    }
7048
7049    #[test]
7050    fn verify_context_bounds_effective_xml_base_components() {
7051        // External URI resolution must stop before repeatedly copying an
7052        // attacker-controlled chain of effective XML Base values.
7053        let mut xml = minimal_signature_xml("payload", "");
7054        for _ in 0..65 {
7055            xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
7056        }
7057        let resources = HashMap::new();
7058        let error = VerifyContext::new()
7059            .key(&AcceptingKey)
7060            .allowed_uri_types(UriTypeSet::ALL)
7061            .external_resources(&resources)
7062            .verify(&xml)
7063            .expect_err("XML Base component work must be bounded before lookup");
7064
7065        assert!(matches!(
7066            error,
7067            SignatureVerificationPipelineError::Policy(
7068                crate::policy::PolicyViolation::ResourceLimit {
7069                    resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
7070                    maximum: 64,
7071                    actual: 65,
7072                }
7073            )
7074        ));
7075    }
7076
7077    #[test]
7078    fn verify_context_bounds_cumulative_xml_base_resolution_bytes() {
7079        // The operation-wide byte budget charges intermediate URI copies, not
7080        // merely the small final external resource returned by the caller map.
7081        let mut xml = minimal_signature_xml("payload", "");
7082        for _ in 0..2 {
7083            xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
7084        }
7085        let resources = HashMap::new();
7086        let mut policy = crate::policy::VerificationPolicy::default();
7087        policy.resources.max_xml_base_resolution_bytes = 32;
7088        let error = VerifyContext::new()
7089            .policy(policy)
7090            .key(&AcceptingKey)
7091            .allowed_uri_types(UriTypeSet::ALL)
7092            .external_resources(&resources)
7093            .verify(&xml)
7094            .expect_err("cumulative XML Base copies must obey the operation budget");
7095
7096        assert!(matches!(
7097            error,
7098            SignatureVerificationPipelineError::Policy(
7099                crate::policy::PolicyViolation::ResourceLimit {
7100                    resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
7101                    maximum: 32,
7102                    ..
7103                }
7104            )
7105        ));
7106    }
7107
7108    #[test]
7109    fn verify_context_applies_xml_base_policy_to_signed_info_c14n() {
7110        // The SignedInfo node-set excludes its ancestors, so C14N 1.1 must
7111        // resolve their inherited xml:base values through the same operation
7112        // budget already used by Reference processing.
7113        let xml = signature_with_target_reference("AQ==")
7114            .replacen(
7115                "http://www.w3.org/2001/10/xml-exc-c14n#",
7116                "http://www.w3.org/2006/12/xml-c14n11",
7117                1,
7118            )
7119            .replace(
7120                "  <ds:Signature>",
7121                "  <outer xml:base=\"one/\"><inner xml:base=\"two/\"><ds:Signature>",
7122            )
7123            .replace("  </ds:Signature>", "  </ds:Signature></inner></outer>");
7124        let policy = crate::policy::VerificationPolicy {
7125            resources: crate::policy::ResourcePolicy {
7126                max_xml_base_components: 1,
7127                ..crate::policy::ResourcePolicy::default()
7128            },
7129            ..crate::policy::VerificationPolicy::default()
7130        };
7131
7132        let error = VerifyContext::new()
7133            .key(&AcceptingKey)
7134            .policy(policy)
7135            .verify(&xml)
7136            .expect_err("SignedInfo C14N must use the operation XML Base budget");
7137
7138        assert!(matches!(
7139            error,
7140            SignatureVerificationPipelineError::Policy(
7141                crate::policy::PolicyViolation::ResourceLimit {
7142                    resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
7143                    maximum: 1,
7144                    actual: 2,
7145                }
7146            )
7147        ));
7148    }
7149
7150    #[test]
7151    fn verify_context_classifies_signed_info_xml_base_byte_limit_as_policy() {
7152        // SignedInfo C14N 1.1 XML Base work is policy enforcement, not a
7153        // malformed canonicalization request, and must retain typed diagnostics.
7154        let xml = signature_with_target_reference("AQ==")
7155            .replacen(
7156                "http://www.w3.org/2001/10/xml-exc-c14n#",
7157                "http://www.w3.org/2006/12/xml-c14n11",
7158                1,
7159            )
7160            .replace(
7161                "  <ds:Signature>",
7162                "  <outer xml:base=\"segment/\"><ds:Signature>",
7163            )
7164            .replace("  </ds:Signature>", "  </ds:Signature></outer>");
7165        let policy = crate::policy::VerificationPolicy {
7166            resources: crate::policy::ResourcePolicy {
7167                max_xml_base_resolution_bytes: 1,
7168                ..crate::policy::ResourcePolicy::default()
7169            },
7170            ..crate::policy::VerificationPolicy::default()
7171        };
7172
7173        let error = VerifyContext::new()
7174            .key(&AcceptingKey)
7175            .policy(policy)
7176            .verify(&xml)
7177            .expect_err("SignedInfo XML Base byte exhaustion must be a policy error");
7178
7179        assert!(matches!(
7180            error,
7181            SignatureVerificationPipelineError::Policy(
7182                crate::policy::PolicyViolation::ResourceLimit {
7183                    resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
7184                    maximum: 1,
7185                    actual,
7186                }
7187            ) if actual > 1
7188        ));
7189    }
7190
7191    #[test]
7192    fn verify_context_meters_repeated_external_dereferences() {
7193        // One caller-owned entry can be referenced repeatedly. The aggregate
7194        // ceiling bounds bytes cloned and processed, not just unique map data.
7195        let payload = b"payload";
7196        let digest = base64::engine::general_purpose::STANDARD.encode(
7197            crate::xmldsig::compute_digest(DigestAlgorithm::Sha1, payload),
7198        );
7199        let reference = format!(
7200            r#"<ds:Reference URI="urn:payload"><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference>"#
7201        );
7202        let xml = format!(
7203            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>{reference}{reference}</ds:SignedInfo><ds:SignatureValue>AQ==</ds:SignatureValue></ds:Signature>"#
7204        );
7205        let resources = HashMap::from([("urn:payload".to_owned(), payload.to_vec())]);
7206        let policy = crate::policy::VerificationPolicy {
7207            uris: crate::policy::UriPolicy {
7208                references: UriTypeSet::ALL,
7209                ..crate::policy::UriPolicy::default()
7210            },
7211            resources: crate::policy::ResourcePolicy {
7212                max_external_resource_bytes: payload.len(),
7213                max_external_resource_total_bytes: payload.len(),
7214                ..crate::policy::ResourcePolicy::default()
7215            },
7216            ..crate::policy::VerificationPolicy::default()
7217        };
7218
7219        let error = VerifyContext::new()
7220            .key(&AcceptingKey)
7221            .policy(policy)
7222            .external_resources(&resources)
7223            .verify(&xml)
7224            .expect_err("the second dereference must exhaust the aggregate byte ceiling");
7225
7226        assert!(
7227            error
7228                .to_string()
7229                .contains("aggregate external resource bytes")
7230        );
7231    }
7232
7233    #[test]
7234    fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
7235        let xml = minimal_signature_xml("", "");
7236        let err = VerifyContext::new()
7237            .key(&RejectingKey)
7238            .allowed_uri_types(UriTypeSet::new(false, true, false))
7239            .verify(&xml)
7240            .expect_err("empty URI must be rejected when empty references are disabled");
7241        assert!(matches!(
7242            err,
7243            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
7244                operation: "verification",
7245                ..
7246            })
7247        ));
7248    }
7249
7250    #[test]
7251    fn verify_context_rejects_disallowed_transform() {
7252        let xml = minimal_signature_xml(
7253            "",
7254            r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
7255        );
7256        let err = VerifyContext::new()
7257            .key(&RejectingKey)
7258            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
7259            .verify(&xml)
7260            .expect_err("enveloped transform should be rejected by allowlist");
7261        assert!(matches!(
7262            err,
7263            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
7264                operation: "verification transform",
7265                ..
7266            })
7267        ));
7268    }
7269
7270    #[test]
7271    fn verify_context_applies_transform_allowlist_to_signed_info_c14n() {
7272        // Reference C14N remains allowlisted; only the distinct SignedInfo
7273        // canonicalization method should trigger this policy rejection.
7274        let xml = signature_with_target_reference("AQ==").replacen(
7275            "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
7276            "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"/>",
7277            1,
7278        );
7279        let error = VerifyContext::new()
7280            .key(&AcceptingKey)
7281            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
7282            .verify(&xml)
7283            .expect_err("SignedInfo C14N must obey the operation transform allowlist");
7284
7285        assert!(matches!(
7286            error,
7287            SignatureVerificationPipelineError::Policy(
7288                crate::policy::PolicyViolation::Algorithm {
7289                    operation: "verification transform",
7290                    ref algorithm,
7291                }
7292            )
7293                if algorithm == "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
7294        ));
7295    }
7296
7297    #[test]
7298    fn verify_context_applies_transform_allowlist_to_key_retrieval() {
7299        // The reference and SignedInfo both use exclusive C14N. The only XPath
7300        // operation is document-selected key retrieval and must be rejected.
7301        let xml = signature_with_target_reference("AQ==")
7302            .replacen(
7303                "</ds:Signature>",
7304                r##"<ds:KeyInfo><ds:RetrievalMethod URI="#keys" Type="http://www.w3.org/2000/09/xmldsig#X509Data"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath></ds:Transform></ds:Transforms></ds:RetrievalMethod></ds:KeyInfo></ds:Signature>"##,
7305                1,
7306            )
7307            .replacen(
7308                "</root>",
7309                r#"<holder ID="keys"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder></root>"#,
7310                1,
7311            );
7312        let error = VerifyContext::new()
7313            .key_resolver(&ConsumingKeyInfoResolver)
7314            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
7315            .verify(&xml)
7316            .expect_err("RetrievalMethod XPath must obey the operation transform allowlist");
7317
7318        assert!(matches!(
7319            error,
7320            SignatureVerificationPipelineError::Policy(
7321                crate::policy::PolicyViolation::Algorithm {
7322                    operation: "verification transform",
7323                    ref algorithm,
7324                }
7325            )
7326                if algorithm == XPATH_TRANSFORM_URI
7327        ));
7328    }
7329
7330    fn signature_with_manifest_xml(valid_manifest_digest: bool) -> String {
7331        signature_with_manifest_xml_with_manifest_mutation(valid_manifest_digest, |xml| xml)
7332    }
7333
7334    fn signature_with_manifest_xml_with_manifest_mutation<F>(
7335        valid_manifest_digest: bool,
7336        mutate_manifest: F,
7337    ) -> String
7338    where
7339        F: FnOnce(String) -> String,
7340    {
7341        const TMP_SIGNED_INFO_DIGEST: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
7342        const INVALID_MANIFEST_DIGEST: &str = "//////////////////////////8=";
7343        let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7344  <target ID="target">payload</target>
7345  <ds:Signature>
7346    <ds:SignedInfo>
7347      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7348      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
7349      <ds:Reference URI="#manifest">
7350        <ds:Transforms>
7351          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7352        </ds:Transforms>
7353        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
7354        <ds:DigestValue>SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER</ds:DigestValue>
7355      </ds:Reference>
7356    </ds:SignedInfo>
7357    <ds:SignatureValue>AQ==</ds:SignatureValue>
7358    <ds:Object>
7359      <ds:Manifest ID="manifest">
7360        <ds:Reference URI="#target">
7361          <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
7362          <ds:DigestValue>MANIFEST_DIGEST_PLACEHOLDER</ds:DigestValue>
7363        </ds:Reference>
7364      </ds:Manifest>
7365    </ds:Object>
7366  </ds:Signature>
7367</root>"##;
7368        let seed_xml = xml_template.replace(
7369            "SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER",
7370            TMP_SIGNED_INFO_DIGEST,
7371        );
7372        let doc = Document::parse(&seed_xml).unwrap();
7373        let signature_node = doc
7374            .descendants()
7375            .find(|node| {
7376                node.is_element()
7377                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
7378                    && node.tag_name().name() == "Signature"
7379            })
7380            .unwrap();
7381        let resolver = UriReferenceResolver::new(&doc);
7382        let initial_data = resolver.dereference("#target").unwrap();
7383        let manifest_pre_digest =
7384            crate::xmldsig::execute_transforms(signature_node, initial_data, &[]).unwrap();
7385        let computed_manifest_digest_b64 = base64::engine::general_purpose::STANDARD
7386            .encode(compute_digest(DigestAlgorithm::Sha1, &manifest_pre_digest));
7387        let final_manifest_digest_b64 = if valid_manifest_digest {
7388            computed_manifest_digest_b64.as_str()
7389        } else {
7390            INVALID_MANIFEST_DIGEST
7391        };
7392        let xml_with_manifest_digest = mutate_manifest(
7393            seed_xml.replace("MANIFEST_DIGEST_PLACEHOLDER", final_manifest_digest_b64),
7394        );
7395        let signed_doc = Document::parse(&xml_with_manifest_digest).unwrap();
7396        let signed_signature_node = signed_doc
7397            .descendants()
7398            .find(|node| {
7399                node.is_element()
7400                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
7401                    && node.tag_name().name() == "Signature"
7402            })
7403            .unwrap();
7404        let signed_info_node = signed_signature_node
7405            .children()
7406            .find(|node| {
7407                node.is_element()
7408                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
7409                    && node.tag_name().name() == "SignedInfo"
7410            })
7411            .unwrap();
7412        let signed_info = parse_signed_info(signed_info_node).unwrap();
7413        let object_reference = &signed_info.references[0];
7414        let signed_resolver = UriReferenceResolver::new(&signed_doc);
7415        let signed_initial_data = signed_resolver
7416            .dereference(object_reference.uri.as_deref().unwrap())
7417            .unwrap();
7418        let signed_pre_digest = crate::xmldsig::execute_transforms(
7419            signed_signature_node,
7420            signed_initial_data,
7421            &object_reference.transforms,
7422        )
7423        .unwrap();
7424        let signed_digest_b64 = base64::engine::general_purpose::STANDARD.encode(compute_digest(
7425            object_reference.digest_method,
7426            &signed_pre_digest,
7427        ));
7428
7429        xml_with_manifest_digest.replacen(TMP_SIGNED_INFO_DIGEST, &signed_digest_b64, 1)
7430    }
7431
7432    fn replace_fixture_manifest_digest(xml: &str, replacement: &str) -> String {
7433        let object_marker = "<ds:Object>";
7434        let object_start = xml
7435            .find(object_marker)
7436            .expect("fixture should contain ds:Object")
7437            + object_marker.len();
7438        let open = "<ds:DigestValue>";
7439        let close = "</ds:DigestValue>";
7440        let value_start = xml[object_start..]
7441            .find(open)
7442            .map(|offset| object_start + offset + open.len())
7443            .expect("Manifest should contain DigestValue");
7444        let value_end = xml[value_start..]
7445            .find(close)
7446            .map(|offset| value_start + offset)
7447            .expect("Manifest DigestValue must be closed");
7448
7449        format!("{}{replacement}{}", &xml[..value_start], &xml[value_end..])
7450    }
7451
7452    #[test]
7453    fn verify_context_processes_manifest_references_when_enabled() {
7454        let xml = signature_with_manifest_xml(true);
7455
7456        let result_without_manifests = VerifyContext::new()
7457            .key(&RejectingKey)
7458            .verify(&xml)
7459            .expect("manifest processing disabled should still verify SignedInfo");
7460        assert!(
7461            result_without_manifests.manifest_references.is_empty(),
7462            "manifest results must stay empty when manifest processing is disabled",
7463        );
7464        assert!(matches!(
7465            result_without_manifests.status,
7466            DsigStatus::Invalid(FailureReason::SignatureMismatch)
7467        ));
7468
7469        let malformed_manifest_xml = signature_with_manifest_xml(true).replacen(
7470            "</ds:Object>",
7471            "</ds:Object><ds:Object><ds:Manifest><ds:Foo/></ds:Manifest></ds:Object>",
7472            1,
7473        );
7474        let malformed_with_manifests_disabled = VerifyContext::new()
7475            .key(&RejectingKey)
7476            .verify(&malformed_manifest_xml)
7477            .expect("malformed Manifest must be ignored when manifest processing is disabled");
7478        assert!(
7479            malformed_with_manifests_disabled
7480                .manifest_references
7481                .is_empty(),
7482            "manifest parser must not run when process_manifests is disabled",
7483        );
7484        assert!(matches!(
7485            malformed_with_manifests_disabled.status,
7486            DsigStatus::Invalid(FailureReason::SignatureMismatch)
7487        ));
7488
7489        let result_with_manifests = VerifyContext::new()
7490            .key(&AcceptingKey)
7491            .process_manifests(true)
7492            .verify(&xml)
7493            .expect("manifest references should be processed when enabled");
7494        assert_eq!(result_with_manifests.manifest_references.len(), 1);
7495        assert_eq!(
7496            result_with_manifests.manifest_references[0].reference_set,
7497            ReferenceSet::Manifest
7498        );
7499        assert_eq!(
7500            result_with_manifests.manifest_references[0].reference_index,
7501            0
7502        );
7503        assert!(matches!(
7504            result_with_manifests.manifest_references[0].status,
7505            DsigStatus::Valid
7506        ));
7507        assert!(matches!(result_with_manifests.status, DsigStatus::Valid));
7508    }
7509
7510    #[test]
7511    fn verify_context_skips_manifest_work_when_signature_value_is_invalid() {
7512        // SignedInfo authenticates the Manifest bytes only after SignatureValue
7513        // succeeds. Malformed nested content must not consume parsing work when
7514        // the cryptographic signature itself is invalid.
7515        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7516            replace_fixture_manifest_digest(&xml, "!!!")
7517        });
7518        assert!(
7519            xml.split_once("<ds:Object>")
7520                .is_some_and(|(_, object)| object.contains("<ds:DigestValue>!!!</ds:DigestValue>")),
7521            "fixture mutation must corrupt the nested Manifest DigestValue",
7522        );
7523
7524        let result = VerifyContext::new()
7525            .key(&RejectingKey)
7526            .process_manifests(true)
7527            .verify(&xml)
7528            .expect("invalid SignatureValue must short-circuit Manifest parsing");
7529
7530        assert!(matches!(
7531            result.status,
7532            DsigStatus::Invalid(FailureReason::SignatureMismatch)
7533        ));
7534        assert!(result.manifest_references.is_empty());
7535    }
7536
7537    #[test]
7538    fn verify_context_shares_xpath_parse_budget_with_manifest_references() {
7539        // SignedInfo and every Manifest form one attacker-controlled parse unit:
7540        // splitting expressions across Reference sets must not reset the ceiling.
7541        let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
7542            .repeat(64);
7543        let transform = format!(
7544            r#"<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</ds:Transform>"#
7545        );
7546        let max_transforms = transform.repeat(16);
7547        let max_manifest_reference = format!(
7548            r##"<ds:Reference URI="#target"><ds:Transforms>{max_transforms}</ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue></ds:Reference>"##
7549        );
7550        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7551            xml.replacen(
7552                r##"<ds:Reference URI="#target">"##,
7553                &format!(
7554                    r##"<ds:Reference URI="#target"><ds:Transforms>{}</ds:Transforms>"##,
7555                    max_transforms
7556                ),
7557                1,
7558            )
7559            .replacen(
7560                "</ds:SignedInfo>",
7561                r##"<ds:Reference URI="#target"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>false()</ds:XPath></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</ds:DigestValue></ds:Reference></ds:SignedInfo>"##,
7562                1,
7563            )
7564            .replacen(
7565                "</ds:Manifest>",
7566                &format!("{}</ds:Manifest>", max_manifest_reference.repeat(3)),
7567                1,
7568            )
7569        });
7570
7571        let error = VerifyContext::new()
7572            .key(&AcceptingKey)
7573            .process_manifests(true)
7574            .verify(&xml)
7575            .expect_err("SignedInfo and Manifest References must share one XPath parse budget");
7576
7577        assert!(
7578            matches!(
7579                &error,
7580                SignatureVerificationPipelineError::Policy(
7581                    crate::policy::PolicyViolation::ResourceLimit {
7582                        resource: "XPath expressions",
7583                        ..
7584                    }
7585                )
7586            ),
7587            "unexpected error: {error:?}"
7588        );
7589    }
7590
7591    #[test]
7592    fn verify_context_processes_manifest_when_signedinfo_references_object() {
7593        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7594            xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
7595                .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
7596                .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
7597        });
7598
7599        let result = VerifyContext::new()
7600            .key(&AcceptingKey)
7601            .process_manifests(true)
7602            .verify(&xml)
7603            .expect("manifest references should be processed when SignedInfo references ds:Object");
7604        assert_eq!(
7605            result.manifest_references.len(),
7606            1,
7607            "signed ds:Object should enable processing of its direct-child ds:Manifest",
7608        );
7609        assert_eq!(
7610            result.manifest_references[0].reference_set,
7611            ReferenceSet::Manifest
7612        );
7613        assert_eq!(result.manifest_references[0].reference_index, 0);
7614        assert!(matches!(
7615            result.manifest_references[0].status,
7616            DsigStatus::Valid
7617        ));
7618    }
7619
7620    #[test]
7621    fn verify_context_skips_manifest_removed_by_enveloped_transform() {
7622        // The owning Signature contains both eligible ID targets. Subtracting
7623        // its subtree therefore removes every target node from the digest input,
7624        // so neither form authenticates the Manifest structure for processing.
7625        for target_object in [false, true] {
7626            let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7627                let xml = xml.replacen(
7628                    r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
7629                    r#"<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
7630                    1,
7631                );
7632                if target_object {
7633                    xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
7634                        .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
7635                        .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
7636                } else {
7637                    xml
7638                }
7639            });
7640
7641            let result = VerifyContext::new()
7642                .key(&AcceptingKey)
7643                .process_manifests(true)
7644                .store_pre_digest(true)
7645                .verify(&xml)
7646                .expect("an emptied reference remains a valid core digest input");
7647
7648            assert!(matches!(result.status, DsigStatus::Valid));
7649            assert_eq!(
7650                result.signed_info_references[0].pre_digest_data.as_deref(),
7651                Some([].as_slice()),
7652                "target_object={target_object} must have empty transformed bytes",
7653            );
7654            assert!(
7655                result.manifest_references.is_empty(),
7656                "target_object={target_object} must not authenticate the Manifest",
7657            );
7658        }
7659    }
7660
7661    #[test]
7662    fn verify_context_ignores_manifest_excluded_from_signed_object() {
7663        // A Reference URI authenticates only its post-transform bytes. Excluding
7664        // the Manifest subtree must not let its independently valid digest chain
7665        // masquerade as data authenticated by SignedInfo.
7666        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7667            xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
7668                .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
7669                .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
7670                .replacen(
7671                    r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
7672                    r#"<ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>not(ancestor-or-self::ds:Manifest)</ds:XPath></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
7673                    1,
7674                )
7675        });
7676
7677        let result = VerifyContext::new()
7678            .key(&AcceptingKey)
7679            .process_manifests(true)
7680            .verify(&xml)
7681            .expect("excluded Manifest content must be ignored, not parsed");
7682
7683        assert!(matches!(result.status, DsigStatus::Valid));
7684        assert!(
7685            result.manifest_references.is_empty(),
7686            "a transform-excluded Manifest is not authenticated by SignedInfo",
7687        );
7688    }
7689
7690    #[test]
7691    fn verify_context_skips_manifest_digest_work_when_signature_is_invalid() {
7692        let xml = signature_with_manifest_xml(false);
7693        let result = VerifyContext::new()
7694            .key(&RejectingKey)
7695            .process_manifests(true)
7696            .verify(&xml)
7697            .expect("invalid SignatureValue must short-circuit Manifest digest work");
7698        assert!(result.manifest_references.is_empty());
7699        assert!(matches!(
7700            result.status,
7701            DsigStatus::Invalid(FailureReason::SignatureMismatch)
7702        ));
7703    }
7704
7705    #[test]
7706    fn verify_context_manifest_digest_mismatch_is_non_fatal_with_accepting_key() {
7707        let xml = signature_with_manifest_xml(false);
7708        let result = VerifyContext::new()
7709            .key(&AcceptingKey)
7710            .process_manifests(true)
7711            .verify(&xml)
7712            .expect("manifest digest mismatches should be recorded while signature stays valid");
7713        assert_eq!(result.manifest_references.len(), 1);
7714        assert!(matches!(
7715            result.manifest_references[0].status,
7716            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
7717        ));
7718        assert!(matches!(result.status, DsigStatus::Valid));
7719    }
7720
7721    #[test]
7722    fn invalid_outer_manifest_digest_does_not_parse_nested_manifest() {
7723        // The malformed inner Manifest is not authenticated until its parent
7724        // Reference digest succeeds. A mismatch must therefore stop before
7725        // parsing or charging work for the nested structure.
7726        let xml = signature_with_manifest_xml_with_manifest_mutation(false, |xml| {
7727            xml.replacen("URI=\"#target\"", "URI=\"#inner-object\"", 1)
7728                .replacen(
7729                    "</ds:Object>",
7730                    "</ds:Object><ds:Object ID=\"inner-object\"><ds:Manifest>junk<ds:Unexpected/></ds:Manifest></ds:Object>",
7731                    1,
7732                )
7733        });
7734
7735        let result = VerifyContext::new()
7736            .key(&AcceptingKey)
7737            .process_manifests(true)
7738            .verify(&xml)
7739            .expect("an unauthenticated nested Manifest must remain opaque");
7740
7741        assert_eq!(result.manifest_references.len(), 1);
7742        assert!(matches!(
7743            result.manifest_references[0].status,
7744            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
7745        ));
7746    }
7747
7748    #[test]
7749    fn verify_context_skips_manifest_parsing_when_signedinfo_reference_fails() {
7750        // Manifest content is not authenticated after a SignedInfo reference
7751        // failure, so parsing it would spend work on untrusted nested input.
7752        let xml = signature_with_manifest_xml(true);
7753        let (signed_info_prefix, object_suffix) = xml
7754            .split_once("<ds:Object>")
7755            .expect("fixture should contain ds:Object");
7756        let open = "<ds:DigestValue>";
7757        let close = "</ds:DigestValue>";
7758        let digest_start = signed_info_prefix
7759            .find(open)
7760            .expect("SignedInfo should contain DigestValue");
7761        let digest_end = signed_info_prefix[digest_start + open.len()..]
7762            .find(close)
7763            .map(|offset| digest_start + open.len() + offset)
7764            .expect("SignedInfo DigestValue must be closed");
7765        let broken_signed_info_prefix = format!(
7766            "{}{}AAAAAAAAAAAAAAAAAAAAAAAAAAA={}{}",
7767            &signed_info_prefix[..digest_start],
7768            open,
7769            close,
7770            &signed_info_prefix[digest_end + close.len()..],
7771        );
7772        let broken_xml = format!("{broken_signed_info_prefix}<ds:Object>{object_suffix}");
7773        let result = VerifyContext::new()
7774            .key(&RejectingKey)
7775            .process_manifests(true)
7776            .verify(&broken_xml)
7777            .expect("SignedInfo digest failure should return without parsing Manifests");
7778        assert!(matches!(
7779            result.status,
7780            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
7781        ));
7782        assert!(
7783            result.manifest_references.is_empty(),
7784            "unauthenticated Manifest content must not be parsed",
7785        );
7786    }
7787
7788    #[test]
7789    fn verify_context_skips_manifest_policy_work_when_signature_is_invalid() {
7790        // A digest-valid SignedInfo reference does not authenticate Manifest
7791        // policy inputs until SignatureValue also succeeds.
7792        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7793            xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
7794        });
7795        let result = VerifyContext::new()
7796            .key(&RejectingKey)
7797            .process_manifests(true)
7798            .verify(&broken_xml)
7799            .expect("invalid SignatureValue must short-circuit Manifest policy work");
7800        assert!(result.manifest_references.is_empty());
7801        assert!(matches!(
7802            result.status,
7803            DsigStatus::Invalid(FailureReason::SignatureMismatch)
7804        ));
7805    }
7806
7807    #[test]
7808    fn verify_context_records_manifest_policy_violations_with_accepting_key() {
7809        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7810            xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
7811        });
7812        let result = VerifyContext::new()
7813            .key(&AcceptingKey)
7814            .process_manifests(true)
7815            .verify(&broken_xml)
7816            .expect("manifest policy violations should be recorded while signature stays valid");
7817        assert_eq!(result.manifest_references.len(), 1);
7818        assert!(matches!(
7819            result.manifest_references[0].status,
7820            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
7821        ));
7822        assert!(matches!(result.status, DsigStatus::Valid));
7823    }
7824
7825    #[test]
7826    fn verify_context_applies_digest_policy_to_manifest_references() {
7827        // Manifest results are authenticated extension data and must obey the
7828        // same digest allowlist as SignedInfo references.
7829        let policy = crate::policy::VerificationPolicy {
7830            manifest_processing: crate::policy::ManifestProcessing::Process,
7831            digest_algorithms: Some(HashSet::from([DigestAlgorithm::Sha1])),
7832            ..crate::policy::VerificationPolicy::default()
7833        };
7834        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
7835            let legacy = "http://www.w3.org/2000/09/xmldsig#sha1";
7836            let offset = xml
7837                .rfind(legacy)
7838                .expect("Manifest DigestMethod must be present");
7839            xml.replace_range(offset..offset + legacy.len(), DigestAlgorithm::Sha256.uri());
7840            let value_start = xml[offset..]
7841                .find("<ds:DigestValue>")
7842                .map(|relative| offset + relative + "<ds:DigestValue>".len())
7843                .expect("Manifest DigestValue must be present");
7844            let value_end = xml[value_start..]
7845                .find("</ds:DigestValue>")
7846                .map(|relative| value_start + relative)
7847                .expect("Manifest DigestValue must be closed");
7848            xml.replace_range(
7849                value_start..value_end,
7850                &base64::engine::general_purpose::STANDARD.encode([0_u8; 32]),
7851            );
7852            xml
7853        });
7854        let result = VerifyContext::new()
7855            .key(&AcceptingKey)
7856            .policy(policy)
7857            .verify(&xml)
7858            .expect("a disallowed Manifest digest is a per-reference result");
7859
7860        assert!(matches!(result.status, DsigStatus::Valid));
7861        assert!(matches!(
7862            result.manifest_references[0].status,
7863            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
7864        ));
7865    }
7866
7867    #[test]
7868    fn verify_context_applies_transform_count_policy_to_manifest_references() {
7869        // Authenticated Manifest references share the caller's per-reference
7870        // transform ceiling and fail before transform execution when exceeded.
7871        let policy = crate::policy::VerificationPolicy {
7872            manifest_processing: crate::policy::ManifestProcessing::Process,
7873            resources: crate::policy::ResourcePolicy {
7874                max_transforms_per_reference: 1,
7875                ..crate::policy::ResourcePolicy::default()
7876            },
7877            ..crate::policy::VerificationPolicy::default()
7878        };
7879        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
7880            let manifest_start = xml
7881                .find("<ds:Manifest")
7882                .expect("fixture must contain a Manifest");
7883            let manifest = xml[manifest_start..].replacen(
7884                "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
7885                concat!(
7886                    "<ds:Transforms>",
7887                    "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
7888                    "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
7889                    "</ds:Transforms>",
7890                    "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>"
7891                ),
7892                1,
7893            );
7894            xml.replace_range(manifest_start.., &manifest);
7895            xml
7896        });
7897        let result = VerifyContext::new()
7898            .key(&AcceptingKey)
7899            .policy(policy)
7900            .verify(&xml)
7901            .expect("Manifest transform policy is a per-reference result");
7902
7903        assert!(matches!(result.status, DsigStatus::Valid));
7904        assert!(matches!(
7905            result.manifest_references[0].status,
7906            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
7907        ));
7908    }
7909
7910    #[test]
7911    fn verify_context_skips_manifest_uri_work_when_signature_is_invalid() {
7912        // Missing Manifest URIs remain unauthenticated until SignatureValue
7913        // succeeds, so they cannot trigger Manifest policy processing here.
7914        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7915            xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
7916        });
7917
7918        let result = VerifyContext::new()
7919            .key(&RejectingKey)
7920            .process_manifests(true)
7921            .verify(&broken_xml)
7922            .expect("invalid SignatureValue must short-circuit Manifest URI processing");
7923        assert!(result.manifest_references.is_empty());
7924        assert!(matches!(
7925            result.status,
7926            DsigStatus::Invalid(FailureReason::SignatureMismatch)
7927        ));
7928    }
7929
7930    #[test]
7931    fn verify_context_records_manifest_missing_uri_with_accepting_key() {
7932        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7933            xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
7934        });
7935
7936        let result = VerifyContext::new()
7937            .key(&AcceptingKey)
7938            .process_manifests(true)
7939            .verify(&broken_xml)
7940            .expect("manifest missing URI should be recorded while signature stays valid");
7941        assert_eq!(result.manifest_references.len(), 1);
7942        assert_eq!(result.manifest_references[0].uri, "<omitted>");
7943        assert!(matches!(
7944            result.manifest_references[0].status,
7945            DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
7946        ));
7947        assert!(matches!(result.status, DsigStatus::Valid));
7948    }
7949
7950    #[test]
7951    fn verify_context_processes_authenticated_nested_manifests_in_object() {
7952        // XMLDSig §5.1 does not impose a direct Object-child restriction.
7953        // The original Reference authenticates this exact wrapped Manifest.
7954        // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-Manifest
7955        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7956            xml.replacen(
7957                "<ds:Manifest ID=\"manifest\">",
7958                "<wrapper><ds:Manifest ID=\"manifest\">",
7959                1,
7960            )
7961            .replacen("</ds:Manifest>", "</ds:Manifest></wrapper>", 1)
7962        });
7963
7964        let result = VerifyContext::new()
7965            .key(&AcceptingKey)
7966            .process_manifests(true)
7967            .verify(&xml)
7968            .expect("authenticated nested Manifest is processed");
7969        assert_eq!(result.manifest_references.len(), 1);
7970        assert_eq!(result.manifest_references[0].uri, "#target");
7971        assert_eq!(result.manifest_references[0].status, DsigStatus::Valid);
7972        assert!(matches!(result.status, DsigStatus::Valid));
7973    }
7974
7975    #[test]
7976    fn verify_context_reports_manifest_reference_parse_errors_explicitly() {
7977        // Malformed nested DigestValue is parsed only after the enclosing
7978        // Manifest structure has been authenticated by SignedInfo.
7979        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7980            replace_fixture_manifest_digest(&xml, "!!!")
7981        });
7982
7983        let err = VerifyContext::new()
7984            .key(&AcceptingKey)
7985            .process_manifests(true)
7986            .verify(&broken_xml)
7987            .expect_err("invalid Manifest DigestValue must map to ParseManifestReference");
7988        assert!(matches!(
7989            err,
7990            SignatureVerificationPipelineError::ParseManifestReference(_)
7991        ));
7992    }
7993
7994    #[test]
7995    fn verify_context_reports_unsupported_manifest_transform_with_declared_digest() {
7996        // Unsupported optional Manifest transforms do not invalidate core
7997        // SignedInfo, but their result must preserve the declared digest method.
7998        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7999            let xml = xml.replacen(
8000                "<ds:Reference URI=\"#target\">",
8001                "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
8002                1,
8003            );
8004            let xml = xml.replacen(
8005                "</ds:Transforms>\n          <ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
8006                "</ds:Transforms>\n          <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>",
8007                1,
8008            );
8009            replace_fixture_manifest_digest(&xml, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
8010        });
8011        assert!(xml.contains("urn:unsupported"));
8012        assert!(xml.contains("http://www.w3.org/2001/04/xmlenc#sha256"));
8013
8014        let result = VerifyContext::new()
8015            .key(&AcceptingKey)
8016            .process_manifests(true)
8017            .verify(&xml)
8018            .expect("unsupported Manifest transform is a per-reference result");
8019        assert_eq!(result.status, DsigStatus::Valid);
8020        assert_eq!(result.manifest_references.len(), 1);
8021        assert_eq!(
8022            result.manifest_references[0].digest_algorithm,
8023            DigestAlgorithm::Sha256
8024        );
8025        assert!(matches!(
8026            result.manifest_references[0].status,
8027            DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
8028        ));
8029
8030        let restricted = VerifyContext::new()
8031            .key(&AcceptingKey)
8032            .process_manifests(true)
8033            .allowed_transforms([
8034                DEFAULT_IMPLICIT_C14N_URI,
8035                "http://www.w3.org/2001/10/xml-exc-c14n#",
8036            ])
8037            .verify(&xml)
8038            .expect("a disallowed Manifest transform is a per-reference policy result");
8039        assert!(matches!(
8040            restricted.manifest_references[0].status,
8041            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
8042        ));
8043    }
8044
8045    #[test]
8046    fn manifest_reference_limit_counts_unsupported_entries() {
8047        let references = (0..=MAX_REFERENCES_PER_SIGNATURE)
8048            .map(|index| {
8049                format!(
8050                    r##"<ds:Reference URI="#target-{index}"><ds:Transforms><ds:Transform Algorithm="urn:unsupported"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue></ds:Reference>"##
8051                )
8052            })
8053            .collect::<String>();
8054        let xml = format!(
8055            r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:Object Id="signed"><ds:Manifest>{references}</ds:Manifest></ds:Object></ds:Signature>"#
8056        );
8057        let document = XmlDocument::parse(xml).unwrap();
8058        let error = document.with_view(|view| {
8059            let signature = view.document().root_element();
8060            let object = signature.children().find(|node| node.is_element()).unwrap();
8061            let policy = crate::policy::VerificationPolicy::default();
8062            let budgets = VerificationOperationBudgets::with_transforms(
8063                &policy,
8064                TransformExecutionBudget::from_resources(&policy.resources),
8065                None,
8066            );
8067            let operation = OperationExecutionContext::new(
8068                &policy,
8069                &budgets,
8070                Some((view.identity(), view.generation())),
8071            );
8072            operation.authenticate(view.node_identity(object));
8073            let mut processed = HashSet::new();
8074            let mut remaining = MAX_REFERENCES_PER_SIGNATURE;
8075            let mut next_index = 0;
8076            let mut xpath_parse = XPathSignatureParseBudget::default();
8077            let mut state = ManifestDiscoveryState {
8078                processed: &mut processed,
8079                remaining_capacity: &mut remaining,
8080                next_reference_index: &mut next_index,
8081                xpath_parse: &mut xpath_parse,
8082            };
8083            match parse_manifest_references(signature, &operation, view, &mut state, None) {
8084                Ok(_) => panic!("unsupported references must consume the same aggregate limit"),
8085                Err(error) => error,
8086            }
8087        });
8088        assert!(matches!(
8089            error,
8090            SignatureVerificationPipelineError::InvalidStructure {
8091                reason: "signed Manifests exceed the per-signature Reference limit"
8092            }
8093        ));
8094    }
8095
8096    #[test]
8097    fn unsigned_manifest_discovery_cannot_bypass_the_filter_work_budget() {
8098        // Unsigned appended nodes still cost discovery work after Signature
8099        // authentication. Even an all-unsigned frontier must admit its scan.
8100        let document = XmlDocument::parse(format!(
8101            "<ds:Signature xmlns:ds=\"{XMLDSIG_NS}\"><ds:Object><ds:Manifest/></ds:Object></ds:Signature>"
8102        )).unwrap();
8103        document.with_view(|view| {
8104            let mut policy = crate::policy::VerificationPolicy::default();
8105            policy.resources.max_node_set_filter_work = 0;
8106            let budgets = VerificationOperationBudgets::with_transforms(
8107                &policy,
8108                TransformExecutionBudget::from_resources(&policy.resources),
8109                None,
8110            );
8111            let operation = OperationExecutionContext::new(
8112                &policy,
8113                &budgets,
8114                Some((view.identity(), view.generation())),
8115            );
8116            let mut processed = HashSet::new();
8117            let mut remaining = 1;
8118            let mut index = 0;
8119            let mut xpath = XPathSignatureParseBudget::default();
8120            let result = parse_manifest_references(
8121                view.document().root_element(),
8122                &operation,
8123                view,
8124                &mut ManifestDiscoveryState {
8125                    processed: &mut processed,
8126                    remaining_capacity: &mut remaining,
8127                    next_reference_index: &mut index,
8128                    xpath_parse: &mut xpath,
8129                },
8130                None,
8131            );
8132            assert!(result.is_err(), "unsigned scans are not free");
8133        });
8134    }
8135
8136    #[test]
8137    fn unsigned_manifest_remains_eligible_after_trust_expands() {
8138        // The second Object is not authenticated during the first discovery
8139        // pass. It must remain unprocessed so a valid reference from the first
8140        // Manifest can make its sibling Manifest eligible on the next pass.
8141        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
8142        let xml = format!(
8143            r##"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:Object Id="outer"><ds:Manifest><ds:Reference URI="#inner"><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:Manifest></ds:Object><ds:Object Id="inner"><ds:Manifest><ds:Reference URI="#payload"><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:Manifest></ds:Object></ds:Signature>"##
8144        );
8145        let document = XmlDocument::parse(xml).expect("nested Manifest fixture must parse");
8146        document.with_view(|view| {
8147            let signature = view.document().root_element();
8148            let mut objects = signature.children().filter(|node| node.is_element());
8149            let outer = objects.next().expect("outer Object");
8150            let inner = objects.next().expect("inner Object");
8151            let policy = crate::policy::VerificationPolicy::default();
8152            let budgets = VerificationOperationBudgets::with_transforms(
8153                &policy,
8154                TransformExecutionBudget::from_resources(&policy.resources),
8155                None,
8156            );
8157            let operation = OperationExecutionContext::new(
8158                &policy,
8159                &budgets,
8160                Some((view.identity(), view.generation())),
8161            );
8162            operation.authenticate(view.node_identity(outer));
8163            let mut processed = HashSet::new();
8164            let mut remaining = 2;
8165            let mut next_index = 0;
8166            let mut xpath_budget = XPathSignatureParseBudget::default();
8167
8168            let first = parse_manifest_references(
8169                signature,
8170                &operation,
8171                view,
8172                &mut ManifestDiscoveryState {
8173                    processed: &mut processed,
8174                    remaining_capacity: &mut remaining,
8175                    next_reference_index: &mut next_index,
8176                    xpath_parse: &mut xpath_budget,
8177                },
8178                None,
8179            )
8180            .expect("outer Manifest must be discovered");
8181            assert_eq!(first.references.len(), 1);
8182            assert_eq!(first.references[0].reference.uri.as_deref(), Some("#inner"));
8183
8184            operation.authenticate(view.node_identity(inner));
8185            let second = parse_manifest_references(
8186                signature,
8187                &operation,
8188                view,
8189                &mut ManifestDiscoveryState {
8190                    processed: &mut processed,
8191                    remaining_capacity: &mut remaining,
8192                    next_reference_index: &mut next_index,
8193                    xpath_parse: &mut xpath_budget,
8194                },
8195                None,
8196            )
8197            .expect("newly authenticated sibling Manifest must remain eligible");
8198            assert_eq!(second.references.len(), 1);
8199            assert_eq!(
8200                second.references[0].reference.uri.as_deref(),
8201                Some("#payload")
8202            );
8203        });
8204    }
8205
8206    #[test]
8207    fn manifest_reference_limit_includes_signed_info_references() {
8208        // The per-signature ceiling is shared by core and authenticated
8209        // Manifest references; enabling Manifest processing must not reset it.
8210        let xml = signature_with_manifest_xml(true);
8211        let reference_start = xml
8212            .find(r##"<ds:Reference URI="#manifest">"##)
8213            .expect("fixture SignedInfo must reference the Manifest");
8214        let reference_end = xml[reference_start..]
8215            .find("</ds:Reference>")
8216            .map(|offset| reference_start + offset + "</ds:Reference>".len())
8217            .expect("fixture SignedInfo Reference must be closed");
8218        let repeated = xml[reference_start..reference_end].repeat(MAX_REFERENCES_PER_SIGNATURE);
8219        let xml = format!(
8220            "{}{repeated}{}",
8221            &xml[..reference_start],
8222            &xml[reference_end..]
8223        );
8224
8225        let error = VerifyContext::new()
8226            .key(&AcceptingKey)
8227            .process_manifests(true)
8228            .verify(&xml)
8229            .expect_err("one Manifest Reference must exceed the exhausted signature-wide limit");
8230
8231        assert!(matches!(
8232            error,
8233            SignatureVerificationPipelineError::InvalidStructure {
8234                reason: "signed Manifests exceed the per-signature Reference limit"
8235            }
8236        ));
8237    }
8238
8239    #[test]
8240    fn configured_reference_limit_is_shared_with_manifests() {
8241        // Lowering the operation policy must lower the aggregate SignedInfo and
8242        // Manifest capacity rather than falling back to the crate hard limit.
8243        let policy = crate::policy::VerificationPolicy {
8244            manifest_processing: crate::policy::ManifestProcessing::Process,
8245            resources: crate::policy::ResourcePolicy {
8246                max_references: 1,
8247                ..crate::policy::ResourcePolicy::default()
8248            },
8249            ..crate::policy::VerificationPolicy::default()
8250        };
8251
8252        let error = VerifyContext::new()
8253            .key(&AcceptingKey)
8254            .policy(policy)
8255            .verify(&signature_with_manifest_xml(true))
8256            .expect_err("Manifest must exceed the caller-selected aggregate limit");
8257        assert!(matches!(
8258            error,
8259            SignatureVerificationPipelineError::InvalidStructure {
8260                reason: "signed Manifests exceed the per-signature Reference limit"
8261            }
8262        ));
8263    }
8264
8265    #[test]
8266    fn retrieval_method_materializes_single_x509_data_subtree() {
8267        for uri in [
8268            "#target",
8269            "#xpointer(id('target'))",
8270            "#xpointer(id(&quot;target&quot;))",
8271        ] {
8272            for target_xml in [
8273                r#"<ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>"#,
8274                r#"<holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>"#,
8275            ] {
8276                let xml = format!(
8277                    r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:KeyInfo><ds:RetrievalMethod URI="{uri}" Type="http://www.w3.org/2000/09/xmldsig#X509Data"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath></ds:Transform></ds:Transforms></ds:RetrievalMethod></ds:KeyInfo>{target_xml}</root>"#
8278                );
8279                let document = Document::parse(&xml).unwrap();
8280                let key_info_node = document
8281                    .descendants()
8282                    .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8283                    .unwrap();
8284                let mut key_info = parse_key_info(key_info_node).unwrap();
8285                let resolver = UriReferenceResolver::new(&document);
8286
8287                materialize_retrieval_methods(
8288                    &mut key_info,
8289                    &resolver,
8290                    UriTypeSet::SAME_DOCUMENT,
8291                    None,
8292                    crate::provider::default_provider(),
8293                )
8294                .expect("XPath filter must produce one X509Data-rooted node-set");
8295                assert!(matches!(
8296                    key_info.sources.as_slice(),
8297                    [super::super::parse::KeyInfoSource::X509Data(info)]
8298                        if info.subject_names == ["CN=leaf"]
8299                ));
8300            }
8301        }
8302    }
8303
8304    fn retrieval_method_xpath_signature() -> String {
8305        format!(
8306            r##"<root xmlns:ds="{XMLDSIG_NS}">
8307              <payload Id="payload">ok</payload>
8308              <ds:Signature>
8309                <ds:SignedInfo>
8310                  <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
8311                  <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
8312                  <ds:Reference URI="#payload">
8313                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
8314                    <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
8315                  </ds:Reference>
8316                </ds:SignedInfo>
8317                <ds:SignatureValue>AQ==</ds:SignatureValue>
8318                <ds:KeyInfo>
8319                  <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
8320                    <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
8321                      <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
8322                    </ds:Transform></ds:Transforms>
8323                  </ds:RetrievalMethod>
8324                </ds:KeyInfo>
8325              </ds:Signature>
8326              <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
8327            </root>"##
8328        )
8329    }
8330
8331    #[test]
8332    fn retrieval_method_xpath_uses_signature_expression_budget() {
8333        // RetrievalMethod XPath belongs to the same untrusted Signature as
8334        // Reference XPath and must not receive a separate parse allowance.
8335        let mut policy = crate::policy::VerificationPolicy::default();
8336        policy.resources.max_xpath_expressions = 0;
8337
8338        let error = VerifyContext::new()
8339            .policy(policy)
8340            .verify(&retrieval_method_xpath_signature())
8341            .expect_err("RetrievalMethod XPath must consume the signature parse budget");
8342
8343        assert!(
8344            matches!(
8345                &error,
8346                SignatureVerificationPipelineError::Policy(
8347                    crate::policy::PolicyViolation::ResourceLimit {
8348                        resource: "XPath expressions",
8349                        maximum: 0,
8350                        actual: 1,
8351                    }
8352                )
8353            ),
8354            "unexpected error: {error:?}"
8355        );
8356    }
8357
8358    #[test]
8359    fn retrieval_method_xpath_obeys_expression_byte_limit() {
8360        // Edge whitespace is semantically harmless for this restricted shape,
8361        // but its raw untrusted bytes still belong to the operation budget.
8362        let expression = "ancestor-or-self::ds:X509Data";
8363        let padded_expression = format!("  {expression}  ");
8364        let xml = retrieval_method_xpath_signature().replace(expression, &padded_expression);
8365        let mut policy = crate::policy::VerificationPolicy::default();
8366        policy.resources.max_xpath_expression_bytes = expression.len();
8367
8368        let error = VerifyContext::new()
8369            .policy(policy)
8370            .verify(&xml)
8371            .expect_err("RetrievalMethod XPath must obey the expression byte limit");
8372
8373        assert!(
8374            matches!(
8375                &error,
8376                SignatureVerificationPipelineError::Policy(
8377                    crate::policy::PolicyViolation::ResourceLimit {
8378                        resource: "XPath expression bytes",
8379                        maximum,
8380                        actual,
8381                    }
8382                ) if *maximum == expression.len() && *actual == padded_expression.len()
8383            ),
8384            "unexpected error: {error:?}"
8385        );
8386    }
8387
8388    #[test]
8389    fn retrieval_method_xpath_obeys_expression_complexity_limit() {
8390        // Recognizing a fixed safe predicate must not bypass the common XPath
8391        // complexity policy applied to every expression in the signature.
8392        let mut policy = crate::policy::VerificationPolicy::default();
8393        policy.resources.max_xpath_expression_complexity = 0;
8394
8395        let error = VerifyContext::new()
8396            .policy(policy)
8397            .verify(&retrieval_method_xpath_signature())
8398            .expect_err("RetrievalMethod XPath must obey the complexity limit");
8399
8400        assert!(
8401            matches!(
8402                &error,
8403                SignatureVerificationPipelineError::Policy(
8404                    crate::policy::PolicyViolation::ResourceLimit {
8405                        resource: "XPath expression complexity",
8406                        maximum: 0,
8407                        actual,
8408                    }
8409                ) if *actual > 0
8410            ),
8411            "unexpected error: {error:?}"
8412        );
8413    }
8414
8415    #[test]
8416    fn retrieval_method_xpath_uses_node_filter_work_budget() {
8417        // Attribute and namespace XPath nodes participate in filtering even
8418        // though the optimized X509Data locator scans only tree descendants.
8419        let mut policy = crate::policy::VerificationPolicy::default();
8420        policy.resources.max_node_set_filter_work = 4;
8421        let xml = retrieval_method_xpath_signature().replace(
8422            "<holder Id=\"target\">",
8423            "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
8424        );
8425
8426        let error = VerifyContext::new()
8427            .policy(policy)
8428            .verify(&xml)
8429            .expect_err("RetrievalMethod XPath must consume node-filter work");
8430
8431        assert!(
8432            matches!(
8433                &error,
8434                SignatureVerificationPipelineError::Policy(
8435                    crate::policy::PolicyViolation::ResourceLimit {
8436                        resource: crate::policy::resource_name::NODE_SET_FILTER_WORK,
8437                        maximum: 4,
8438                        actual,
8439                    }
8440                ) if *actual > 4
8441            ),
8442            "unexpected error: {error:?}"
8443        );
8444    }
8445
8446    #[test]
8447    fn retrieval_method_xpath_charges_every_context_to_evaluation_work() {
8448        // The optimized predicate avoids a generic XPath engine, but every
8449        // attribute and namespace context still consumes evaluation work.
8450        let mut policy = crate::policy::VerificationPolicy::default();
8451        policy.resources.max_xpath_evaluation_work = 4;
8452        let xml = retrieval_method_xpath_signature().replace(
8453            "<holder Id=\"target\">",
8454            "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
8455        );
8456
8457        let error = VerifyContext::new()
8458            .policy(policy)
8459            .verify(&xml)
8460            .expect_err("RetrievalMethod XPath must charge every evaluation context");
8461
8462        assert!(matches!(
8463            error,
8464            SignatureVerificationPipelineError::Policy(
8465                crate::policy::PolicyViolation::ResourceLimit {
8466                    resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
8467                    maximum: 4,
8468                    actual,
8469                }
8470            ) if actual > 4
8471        ));
8472    }
8473
8474    #[test]
8475    fn retrieval_method_xpath_obeys_namespace_binding_limit() {
8476        // The specialized RetrievalMethod path must retain the XPath element's
8477        // in-scope namespaces and enforce the same limit as ordinary XPath.
8478        let mut policy = crate::policy::VerificationPolicy::default();
8479        policy.resources.max_xpath_namespace_bindings = 0;
8480
8481        let error = VerifyContext::new()
8482            .policy(policy)
8483            .verify(&retrieval_method_xpath_signature())
8484            .expect_err("RetrievalMethod XPath namespaces must obey the binding limit");
8485
8486        assert!(matches!(
8487            error,
8488            SignatureVerificationPipelineError::Policy(
8489                crate::policy::PolicyViolation::ResourceLimit {
8490                    resource: crate::policy::resource_name::XPATH_NAMESPACE_BINDINGS,
8491                    maximum: 0,
8492                    actual,
8493                }
8494            ) if actual > 0
8495        ));
8496    }
8497
8498    #[test]
8499    fn retrieval_method_xpath_obeys_namespace_byte_limit() {
8500        // Prefix and URI bytes retained from the XPath namespace axis consume
8501        // the same per-expression byte budget as an ordinary XPath transform.
8502        let mut policy = crate::policy::VerificationPolicy::default();
8503        policy.resources.max_xpath_namespace_bytes = 0;
8504
8505        let error = VerifyContext::new()
8506            .policy(policy)
8507            .verify(&retrieval_method_xpath_signature())
8508            .expect_err("RetrievalMethod XPath namespaces must obey the byte limit");
8509
8510        assert!(matches!(
8511            error,
8512            SignatureVerificationPipelineError::Policy(
8513                crate::policy::PolicyViolation::ResourceLimit {
8514                    resource: crate::policy::resource_name::XPATH_NAMESPACE_BYTES,
8515                    maximum: 0,
8516                    actual,
8517                }
8518            ) if actual > 0
8519        ));
8520    }
8521
8522    #[test]
8523    fn retrieval_method_xpath_obeys_context_evaluation_limit() {
8524        // A bare fragment includes attribute and namespace XPath nodes in
8525        // addition to the four tree nodes below. Tree-only accounting would
8526        // incorrectly admit this input at the configured ceiling.
8527        let mut policy = crate::policy::VerificationPolicy::default();
8528        policy.resources.max_xpath_context_evaluations = 4;
8529        let xml = retrieval_method_xpath_signature().replace(
8530            "<holder Id=\"target\">",
8531            "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
8532        );
8533
8534        let error = VerifyContext::new()
8535            .policy(policy)
8536            .verify(&xml)
8537            .expect_err("RetrievalMethod XPath contexts must obey the evaluation limit");
8538
8539        assert!(matches!(
8540            error,
8541            SignatureVerificationPipelineError::Policy(
8542                crate::policy::PolicyViolation::ResourceLimit {
8543                    resource: crate::policy::resource_name::XPATH_CONTEXT_EVALUATIONS,
8544                    maximum: 4,
8545                    actual,
8546                }
8547            ) if actual > 4
8548        ));
8549    }
8550
8551    #[test]
8552    fn retrieval_method_materializes_direct_untransformed_x509_data() {
8553        // A typed RetrievalMethod may point directly at the XML structure it
8554        // identifies; no transform is needed when X509Data is the URI root.
8555        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8556          <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
8557          <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
8558        </root>"##;
8559        let document = Document::parse(xml).unwrap();
8560        let key_info_node = document
8561            .descendants()
8562            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8563            .unwrap();
8564        let mut key_info = parse_key_info(key_info_node).unwrap();
8565
8566        materialize_retrieval_methods(
8567            &mut key_info,
8568            &UriReferenceResolver::new(&document),
8569            UriTypeSet::SAME_DOCUMENT,
8570            None,
8571            crate::provider::default_provider(),
8572        )
8573        .expect("a direct X509Data target needs no transform");
8574        assert!(matches!(
8575            key_info.sources.as_slice(),
8576            [super::super::parse::KeyInfoSource::X509Data(info)]
8577                if info.subject_names == ["CN=leaf"]
8578        ));
8579    }
8580
8581    #[test]
8582    fn retrieval_method_respects_configured_same_document_id_semantics() {
8583        // RetrievalMethod is another consumer of same-document URIs and must
8584        // not bypass the grammar selected for normal Reference dereferencing.
8585        fn materialize(
8586            uri: &str,
8587            id: &str,
8588            semantics: crate::policy::SameDocumentIdSemantics,
8589        ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
8590            let xml = format!(
8591                r#"<root xmlns:ds="{XMLDSIG_NS}">
8592                  <ds:KeyInfo><ds:RetrievalMethod URI="{uri}" Type="{XMLDSIG_NS}X509Data"/></ds:KeyInfo>
8593                  <ds:X509Data Id="{id}"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
8594                </root>"#
8595            );
8596            let document = Document::parse(&xml).unwrap();
8597            let key_info_node = document
8598                .descendants()
8599                .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8600                .unwrap();
8601            let mut key_info = parse_key_info(key_info_node).unwrap();
8602            let resolver =
8603                UriReferenceResolver::new(&document).with_same_document_id_semantics(semantics);
8604
8605            materialize_retrieval_methods(
8606                &mut key_info,
8607                &resolver,
8608                UriTypeSet::SAME_DOCUMENT,
8609                None,
8610                crate::provider::default_provider(),
8611            )
8612        }
8613
8614        assert!(
8615            materialize(
8616                "#12345",
8617                "12345",
8618                crate::policy::SameDocumentIdSemantics::Specification,
8619            )
8620            .is_err(),
8621            "the standards mode must reject a non-NCName bare fragment"
8622        );
8623        assert!(
8624            materialize(
8625                "#visa'3d",
8626                "visa'3d",
8627                crate::policy::SameDocumentIdSemantics::XmlSecBarename,
8628            )
8629            .is_err(),
8630            "the donor barename wrapper cannot represent an apostrophe"
8631        );
8632        assert!(
8633            materialize(
8634                "#visa'3d",
8635                "visa'3d",
8636                crate::policy::SameDocumentIdSemantics::XmlSecVisa3d,
8637            )
8638            .is_ok(),
8639            "Visa3D mode resolves the registered ID without an XPointer literal"
8640        );
8641    }
8642
8643    #[test]
8644    fn raw_x509_retrieval_method_uses_inherited_xml_base() {
8645        // RetrievalMethod URI is an attribute URI reference, so XML Base uses
8646        // the effective base of the element bearing that attribute.
8647        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
8648        let xml = format!(
8649            r#"<root xml:base="https://example.test/keys/nested/" xmlns:ds="{XMLDSIG_NS}">
8650                <ds:KeyInfo><ds:RetrievalMethod URI="../signer.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>
8651            </root>"#
8652        );
8653        let document = Document::parse(&xml).unwrap();
8654        let key_info_node = document
8655            .descendants()
8656            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8657            .unwrap();
8658        let mut key_info = parse_key_info(key_info_node).unwrap();
8659        let certificate = include_bytes!(
8660            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
8661        )
8662        .to_vec();
8663        let resources = HashMap::from([(
8664            "https://example.test/keys/signer.der".to_string(),
8665            certificate,
8666        )]);
8667        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
8668
8669        materialize_retrieval_methods(
8670            &mut key_info,
8671            &resolver,
8672            UriTypeSet::ALL,
8673            None,
8674            crate::provider::default_provider(),
8675        )
8676        .expect("RetrievalMethod should resolve against inherited xml:base");
8677
8678        assert!(matches!(
8679            key_info.sources.as_slice(),
8680            [super::super::parse::KeyInfoSource::X509Data(info)]
8681                if info.certificates.len() == 1
8682        ));
8683    }
8684
8685    #[test]
8686    fn retrieval_method_requires_xpath_for_x509_data_below_uri_root() {
8687        // Without a transform the dereferenced holder, not its descendant,
8688        // is the result and therefore cannot masquerade as typed X509Data.
8689        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8690          <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
8691          <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
8692        </root>"##;
8693        let document = Document::parse(xml).unwrap();
8694        let key_info_node = document
8695            .descendants()
8696            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8697            .unwrap();
8698        let mut key_info = parse_key_info(key_info_node).unwrap();
8699
8700        let error = materialize_retrieval_methods(
8701            &mut key_info,
8702            &UriReferenceResolver::new(&document),
8703            UriTypeSet::SAME_DOCUMENT,
8704            None,
8705            crate::provider::default_provider(),
8706        )
8707        .expect_err("a wrapper target requires an explicit selection transform");
8708        assert!(matches!(
8709            error,
8710            SignatureVerificationPipelineError::InvalidStructure {
8711                reason: "untransformed X509Data RetrievalMethod must target X509Data directly"
8712            }
8713        ));
8714    }
8715
8716    #[test]
8717    fn retrieval_method_rejects_target_inside_external_x509_data_ancestor() {
8718        // XPath filtering cannot add an ancestor that was outside the URI's
8719        // dereferenced node-set, so this result is not rooted at X509Data.
8720        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8721          <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath></ds:Transform></ds:Transforms></ds:RetrievalMethod></ds:KeyInfo>
8722          <ds:X509Data><ds:X509SubjectName Id="target">CN=leaf</ds:X509SubjectName></ds:X509Data>
8723        </root>"##;
8724        let document = Document::parse(xml).unwrap();
8725        let key_info_node = document
8726            .descendants()
8727            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8728            .unwrap();
8729        let mut key_info = parse_key_info(key_info_node).unwrap();
8730
8731        let error = materialize_retrieval_methods(
8732            &mut key_info,
8733            &UriReferenceResolver::new(&document),
8734            UriTypeSet::SAME_DOCUMENT,
8735            None,
8736            crate::provider::default_provider(),
8737        )
8738        .expect_err("filter output without an X509Data root must be rejected");
8739        assert!(matches!(
8740            error,
8741            SignatureVerificationPipelineError::InvalidStructure {
8742                reason: "X509Data RetrievalMethod selected no X509Data element"
8743            }
8744        ));
8745    }
8746
8747    #[test]
8748    fn retrieval_method_rejects_ambiguous_x509_data_relation() {
8749        // A transformed result with multiple X509Data roots is not one KeyInfo child.
8750        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8751          <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath></ds:Transform></ds:Transforms></ds:RetrievalMethod></ds:KeyInfo>
8752          <holder Id="target"><ds:X509Data/><ds:X509Data/></holder>
8753        </root>"##;
8754        let document = Document::parse(xml).unwrap();
8755        let key_info_node = document
8756            .descendants()
8757            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8758            .unwrap();
8759        let mut key_info = parse_key_info(key_info_node).unwrap();
8760
8761        let error = materialize_retrieval_methods(
8762            &mut key_info,
8763            &UriReferenceResolver::new(&document),
8764            UriTypeSet::SAME_DOCUMENT,
8765            None,
8766            crate::provider::default_provider(),
8767        )
8768        .expect_err("multiple transformed X509Data roots must be rejected");
8769        assert!(matches!(
8770            error,
8771            SignatureVerificationPipelineError::InvalidStructure {
8772                reason: "X509Data RetrievalMethod selected multiple X509Data elements"
8773            }
8774        ));
8775    }
8776
8777    #[test]
8778    fn retrieval_method_materialization_preserves_key_info_order() {
8779        // Replacing the source in place keeps a later fallback behind the
8780        // retrieved key material for first-match resolvers.
8781        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8782          <ds:KeyInfo>
8783            <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath></ds:Transform></ds:Transforms></ds:RetrievalMethod>
8784            <ds:KeyName>fallback</ds:KeyName>
8785          </ds:KeyInfo>
8786          <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
8787        </root>"##;
8788        let document = Document::parse(xml).unwrap();
8789        let key_info_node = document
8790            .descendants()
8791            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
8792            .unwrap();
8793        let mut key_info = parse_key_info(key_info_node).unwrap();
8794
8795        materialize_retrieval_methods(
8796            &mut key_info,
8797            &UriReferenceResolver::new(&document),
8798            UriTypeSet::SAME_DOCUMENT,
8799            None,
8800            crate::provider::default_provider(),
8801        )
8802        .unwrap();
8803        assert!(matches!(
8804            key_info.sources.as_slice(),
8805            [
8806                super::super::parse::KeyInfoSource::X509Data(_),
8807                super::super::parse::KeyInfoSource::KeyName(name)
8808            ] if name == "fallback"
8809        ));
8810    }
8811
8812    #[test]
8813    fn retrieval_method_materialization_bounds_repeated_sources() {
8814        // Repeating one allowed certificate must not multiply parsing and clones
8815        // before SignatureValue validation.
8816        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
8817        let certificate = include_bytes!(
8818            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
8819        )
8820        .to_vec();
8821        let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
8822        let mut key_info = KeyInfo {
8823            sources: (0..=64)
8824                .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
8825                    uri: "urn:certificate".into(),
8826                    resource_type: Some(RAW_X509_TYPE.into()),
8827                    transforms: RetrievalMethodTransforms::None,
8828                })
8829                .collect(),
8830        };
8831        let document = Document::parse("<root/>").unwrap();
8832        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
8833
8834        let error = materialize_retrieval_methods(
8835            &mut key_info,
8836            &resolver,
8837            UriTypeSet::ALL,
8838            None,
8839            crate::provider::default_provider(),
8840        )
8841        .expect_err("retrieval count must be bounded before materialization");
8842        assert!(matches!(
8843            error,
8844            SignatureVerificationPipelineError::InvalidStructure {
8845                reason: "KeyInfo contains too many RetrievalMethod elements"
8846            }
8847        ));
8848    }
8849
8850    #[test]
8851    fn key_info_reference_materializes_an_allowed_empty_uri() {
8852        // Empty URI is a same-document Reference URI. Element-valued
8853        // KeyInfoReference dereferencing therefore selects a KeyInfo document
8854        // element rather than consulting the external-resource map.
8855        let document = XmlDocument::parse(format!(
8856            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>root-key</ds:KeyName></ds:KeyInfo>"#
8857        ))
8858        .unwrap();
8859        let mut key_info = KeyInfo {
8860            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
8861                uri: String::new(),
8862            }],
8863        };
8864        let mut policy = crate::policy::VerificationPolicy::default();
8865        policy.key_sources.key_info_reference = true;
8866        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
8867        let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
8868        let mut budgets = RetrievalMaterializationBudgets {
8869            key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
8870                &policy.resources,
8871            )),
8872            xpath_parse: &mut xpath_parse_budget,
8873            execution: &execution_budget,
8874            resources: &policy.resources,
8875            certificate_signature_algorithms: policy
8876                .key_trust
8877                .certificate_signature_algorithms
8878                .as_ref(),
8879            xml_backend: crate::XmlBackend::default(),
8880        };
8881        let mut materialization = KeyInfoMaterializationState::default();
8882
8883        document
8884            .with_view(|view| {
8885                materialize_key_info_references_with_budgets(
8886                    &mut key_info,
8887                    &UriReferenceResolver::with_document_view(view, &[]),
8888                    &policy,
8889                    crate::provider::default_provider(),
8890                    &mut budgets,
8891                    &mut materialization,
8892                )?;
8893                Ok::<_, SignatureVerificationPipelineError>(())
8894            })
8895            .unwrap();
8896
8897        assert!(matches!(
8898            key_info.sources.as_slice(),
8899            [super::super::parse::KeyInfoSource::KeyName(name)] if name == "root-key"
8900        ));
8901    }
8902
8903    #[test]
8904    fn raw_certificate_retrieval_retains_spent_operation_work() {
8905        // Raw DER retrieval must not bypass the account used by XML KeyInfo parsing.
8906        let pair = rcgen::KeyPair::generate().expect("test key generation");
8907        let cert = rcgen::CertificateParams::new(Vec::<String>::new())
8908            .expect("empty SAN list")
8909            .self_signed(&pair)
8910            .expect("test certificate signing");
8911        let external = HashMap::from([("urn:cert".to_owned(), cert.der().to_vec())]);
8912        let document = Document::parse("<root/>").expect("test XML");
8913        let resolver = UriReferenceResolver::new(&document).with_external_resources(&external);
8914        let mut policy = crate::policy::VerificationPolicy::default();
8915        policy.resources.max_x509_work = 1;
8916        policy.uris.retrieval_methods = UriTypeSet::ALL;
8917        let usage = RefCell::new(super::super::parse::KeyInfoParseUsage::new(
8918            &policy.resources,
8919        ));
8920        usage
8921            .borrow()
8922            .certificates
8923            .memory()
8924            .charge_work(1)
8925            .expect("spend allowance");
8926        let mut xpath = XPathSignatureParseBudget::from_resources(&policy.resources);
8927        let execution = TransformExecutionBudget::from_resources(&policy.resources);
8928        let mut budgets = RetrievalMaterializationBudgets {
8929            key_info_parse: &usage,
8930            xpath_parse: &mut xpath,
8931            execution: &execution,
8932            resources: &policy.resources,
8933            certificate_signature_algorithms: None,
8934            xml_backend: crate::XmlBackend::default(),
8935        };
8936        let mut info = KeyInfo {
8937            sources: vec![super::super::parse::KeyInfoSource::RetrievalMethod {
8938                uri: "urn:cert".into(),
8939                resource_type: Some("http://www.w3.org/2000/09/xmldsig#rawX509Certificate".into()),
8940                transforms: RetrievalMethodTransforms::None,
8941            }],
8942        };
8943        let result = materialize_retrieval_methods_with_budgets(
8944            &mut info,
8945            &resolver,
8946            &policy,
8947            crate::provider::default_provider(),
8948            &mut budgets,
8949            &mut 0,
8950        );
8951        assert!(matches!(
8952            result,
8953            Err(SignatureVerificationPipelineError::Policy(
8954                crate::policy::PolicyViolation::ResourceLimitExceeded {
8955                    resource: crate::policy::resource_name::X509_WORK,
8956                    maximum: 1,
8957                }
8958            ))
8959        ));
8960    }
8961
8962    #[test]
8963    fn empty_key_info_reference_participates_in_cycle_detection() {
8964        // Empty URI resolves to the owning document's root KeyInfo. It must
8965        // recurse through the same traversal state rather than leave the
8966        // self-reference materialized and bypass cycle/depth enforcement.
8967        let document = XmlDocument::parse(format!(
8968            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#"><dsig11:KeyInfoReference URI=""/></ds:KeyInfo>"#
8969        ))
8970        .unwrap();
8971        let mut key_info = KeyInfo {
8972            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
8973                uri: String::new(),
8974            }],
8975        };
8976        let mut policy = crate::policy::VerificationPolicy::default();
8977        policy.key_sources.key_info_reference = true;
8978        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
8979        let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
8980        let mut budgets = RetrievalMaterializationBudgets {
8981            key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
8982                &policy.resources,
8983            )),
8984            xpath_parse: &mut xpath_parse_budget,
8985            execution: &execution_budget,
8986            resources: &policy.resources,
8987            certificate_signature_algorithms: policy
8988                .key_trust
8989                .certificate_signature_algorithms
8990                .as_ref(),
8991            xml_backend: crate::XmlBackend::default(),
8992        };
8993        let mut materialization = KeyInfoMaterializationState::default();
8994
8995        let error = document
8996            .with_view(|view| {
8997                materialize_key_info_references_with_budgets(
8998                    &mut key_info,
8999                    &UriReferenceResolver::with_document_view(view, &[]),
9000                    &policy,
9001                    crate::provider::default_provider(),
9002                    &mut budgets,
9003                    &mut materialization,
9004                )
9005            })
9006            .expect_err("empty-URI self-reference must be rejected as a cycle");
9007
9008        assert!(matches!(
9009            error,
9010            SignatureVerificationPipelineError::InvalidStructure {
9011                reason: "KeyInfoReference cycle detected"
9012            }
9013        ));
9014    }
9015
9016    #[test]
9017    fn key_info_materialization_shares_candidate_work_across_source_kinds() {
9018        // The reference, nested RetrievalMethod, and resulting certificate are
9019        // three units of materialization work in one operation.
9020        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
9021        let document = XmlDocument::parse(format!(
9022            r##"<root xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#"><ds:KeyInfo ID="target"><ds:RetrievalMethod URI="signer.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo></root>"##
9023        ))
9024        .unwrap();
9025        let certificate = include_bytes!(
9026            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
9027        )
9028        .to_vec();
9029        let resources = HashMap::from([("signer.der".to_owned(), certificate)]);
9030        let mut key_info = KeyInfo {
9031            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9032                uri: "#target".into(),
9033            }],
9034        };
9035        let mut policy = crate::policy::VerificationPolicy::default();
9036        policy.key_sources.key_info_reference = true;
9037        policy.uris.retrieval_methods = UriTypeSet::ALL;
9038        policy.resources.max_key_candidates = 2;
9039        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
9040        let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
9041        let mut budgets = RetrievalMaterializationBudgets {
9042            key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
9043                &policy.resources,
9044            )),
9045            xpath_parse: &mut xpath_parse_budget,
9046            execution: &execution_budget,
9047            resources: &policy.resources,
9048            certificate_signature_algorithms: policy
9049                .key_trust
9050                .certificate_signature_algorithms
9051                .as_ref(),
9052            xml_backend: crate::XmlBackend::default(),
9053        };
9054        let mut materialization = KeyInfoMaterializationState::default();
9055
9056        let error = document
9057            .with_view(|view| {
9058                let resolver = UriReferenceResolver::with_document_view(view, &[])
9059                    .with_external_resources(&resources);
9060                let mut outcome = materialize_key_info_references_with_budgets(
9061                    &mut key_info,
9062                    &resolver,
9063                    &policy,
9064                    crate::provider::default_provider(),
9065                    &mut budgets,
9066                    &mut materialization,
9067                )?;
9068                outcome.merge(materialize_retrieval_methods_with_budgets(
9069                    &mut key_info,
9070                    &resolver,
9071                    &policy,
9072                    crate::provider::default_provider(),
9073                    &mut budgets,
9074                    &mut materialization.candidate_work,
9075                )?);
9076                Ok::<_, DsigError>(outcome)
9077            })
9078            .expect_err("all materializers must share the candidate-work limit");
9079
9080        assert!(matches!(
9081            error,
9082            DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
9083                resource: crate::policy::resource_name::KEY_CANDIDATES,
9084                maximum: 2,
9085                actual: 3,
9086            })
9087        ));
9088    }
9089
9090    #[test]
9091    fn public_key_info_materialization_binds_external_resource_policy() {
9092        // Public helpers must not inherit the resolver's permissive hard-limit
9093        // budget when the operation policy selects a stricter byte ceiling.
9094        let document = Document::parse("<root/>").unwrap();
9095        let external = format!(
9096            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
9097        );
9098        let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
9099        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9100        let mut key_info = KeyInfo {
9101            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9102                uri: "key.xml".into(),
9103            }],
9104        };
9105        let mut policy = crate::policy::VerificationPolicy::default();
9106        policy.key_sources.key_info_reference = true;
9107        policy.uris.key_info_references = UriTypeSet::ALL;
9108        policy.resources.max_external_resource_bytes = 0;
9109
9110        let error = materialize_verification_key_info_references(
9111            &mut key_info,
9112            resolver,
9113            &policy,
9114            crate::provider::default_provider(),
9115            crate::XmlBackend::default(),
9116        )
9117        .expect_err("policy must reject non-empty external KeyInfo bytes");
9118
9119        assert!(matches!(
9120            error,
9121            DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
9122                resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
9123                maximum: 0,
9124                actual,
9125            }) if actual == resources["key.xml"].len()
9126        ));
9127    }
9128
9129    #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
9130    #[test]
9131    fn public_key_info_materialization_uses_the_selected_backend() {
9132        // Public materialization is a standalone parse boundary. Its backend
9133        // must be caller-selected just like the complete sign/verify pipeline.
9134        let external = format!(
9135            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
9136        );
9137        let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
9138        let document = Document::parse("<root/>").unwrap();
9139
9140        for backend in [crate::XmlBackend::Xmloxide, crate::XmlBackend::Roxmltree] {
9141            let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9142            let mut key_info = KeyInfo {
9143                sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9144                    uri: "key.xml".into(),
9145                }],
9146            };
9147            let mut policy = crate::policy::VerificationPolicy::default();
9148            policy.key_sources.key_info_reference = true;
9149            policy.uris.key_info_references = UriTypeSet::ALL;
9150
9151            materialize_verification_key_info_references(
9152                &mut key_info,
9153                resolver,
9154                &policy,
9155                crate::provider::default_provider(),
9156                backend,
9157            )
9158            .unwrap_or_else(|error| panic!("{backend:?} materialization failed: {error}"));
9159
9160            assert!(matches!(
9161                key_info.sources.as_slice(),
9162                [super::super::parse::KeyInfoSource::KeyName(name)] if name == "external"
9163            ));
9164        }
9165    }
9166
9167    #[test]
9168    fn public_key_info_materialization_defers_nested_retrieval_failure() {
9169        // Metadata expansion must preserve an earlier usable source when a
9170        // later retrieval candidate fails. The complete operation reports the
9171        // deferred error only if key selection exhausts all usable sources.
9172        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
9173        let external = format!(
9174            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>usable</ds:KeyName><ds:RetrievalMethod URI="missing.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>"#
9175        );
9176        let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
9177        let document = Document::parse("<root/>").unwrap();
9178
9179        for operation in ["signing", "verification"] {
9180            let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9181            let mut key_info = KeyInfo {
9182                sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9183                    uri: "key.xml".into(),
9184                }],
9185            };
9186            match operation {
9187                "signing" => {
9188                    let mut policy = crate::policy::SigningPolicy::default();
9189                    policy.uris.key_info_references = UriTypeSet::ALL;
9190                    policy.uris.retrieval_methods = UriTypeSet::ALL;
9191                    materialize_signing_key_info_references(
9192                        &mut key_info,
9193                        resolver,
9194                        &policy,
9195                        crate::provider::default_provider(),
9196                        crate::XmlBackend::default(),
9197                    )
9198                }
9199                "verification" => {
9200                    let mut policy = crate::policy::VerificationPolicy::default();
9201                    policy.key_sources.key_info_reference = true;
9202                    policy.uris.key_info_references = UriTypeSet::ALL;
9203                    policy.uris.retrieval_methods = UriTypeSet::ALL;
9204                    materialize_verification_key_info_references(
9205                        &mut key_info,
9206                        resolver,
9207                        &policy,
9208                        crate::provider::default_provider(),
9209                        crate::XmlBackend::default(),
9210                    )
9211                }
9212                _ => unreachable!(),
9213            }
9214            .unwrap_or_else(|error| panic!("{operation} materialization failed: {error}"));
9215
9216            assert!(matches!(
9217                key_info.sources.as_slice(),
9218                [super::super::parse::KeyInfoSource::KeyName(name)] if name == "usable"
9219            ));
9220        }
9221    }
9222
9223    #[test]
9224    fn public_key_info_materialization_binds_xpath_policy() {
9225        // Both public helpers must carry the caller's XPath parse limits into
9226        // RetrievalMethod transforms nested in an external KeyInfo document.
9227        let external = format!(
9228            r##"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}">
9229                <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
9230                    <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
9231                        <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
9232                    </ds:Transform></ds:Transforms>
9233                </ds:RetrievalMethod>
9234                <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
9235            </ds:KeyInfo>"##
9236        );
9237        let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
9238        let document = Document::parse("<root/>").unwrap();
9239
9240        for operation in ["signing", "verification"] {
9241            let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9242            let mut key_info = KeyInfo {
9243                sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9244                    uri: "key.xml".into(),
9245                }],
9246            };
9247            let error = match operation {
9248                "signing" => {
9249                    let mut policy = crate::policy::SigningPolicy::default();
9250                    policy.uris.key_info_references = UriTypeSet::ALL;
9251                    policy.resources.max_xpath_expressions = 0;
9252                    materialize_signing_key_info_references(
9253                        &mut key_info,
9254                        resolver,
9255                        &policy,
9256                        crate::provider::default_provider(),
9257                        crate::XmlBackend::default(),
9258                    )
9259                }
9260                "verification" => {
9261                    let mut policy = crate::policy::VerificationPolicy::default();
9262                    policy.key_sources.key_info_reference = true;
9263                    policy.uris.key_info_references = UriTypeSet::ALL;
9264                    policy.resources.max_xpath_expressions = 0;
9265                    materialize_verification_key_info_references(
9266                        &mut key_info,
9267                        resolver,
9268                        &policy,
9269                        crate::provider::default_provider(),
9270                        crate::XmlBackend::default(),
9271                    )
9272                }
9273                _ => unreachable!(),
9274            }
9275            .expect_err("the public helper must enforce the supplied XPath expression limit");
9276
9277            assert!(
9278                matches!(
9279                    error,
9280                    DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
9281                        resource: crate::policy::resource_name::XPATH_EXPRESSIONS,
9282                        maximum: 0,
9283                        actual: 1,
9284                    })
9285                ),
9286                "unexpected {operation} error: {error:?}"
9287            );
9288        }
9289    }
9290
9291    fn materialize_external_key_info_chain(
9292        terminal_reference: &str,
9293    ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
9294        let a = format!(
9295            r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
9296                <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
9297                <ds:KeyInfo ID="next"><dsig11:KeyInfoReference URI="b.xml#root"/></ds:KeyInfo>
9298            </doc>"##
9299        );
9300        let b = format!(
9301            r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
9302                <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
9303                <ds:KeyInfo ID="next">{terminal_reference}</ds:KeyInfo>
9304            </doc>"##
9305        );
9306        let resources = HashMap::from([
9307            ("a.xml".to_owned(), a.into_bytes()),
9308            ("b.xml".to_owned(), b.into_bytes()),
9309        ]);
9310        let document = XmlDocument::parse("<root/>").unwrap();
9311        let mut key_info = KeyInfo {
9312            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9313                uri: "a.xml#root".into(),
9314            }],
9315        };
9316        let mut policy = crate::policy::VerificationPolicy::default();
9317        policy.key_sources.key_info_reference = true;
9318        policy.uris.key_info_references = UriTypeSet::ALL;
9319        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
9320        let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
9321        let mut budgets = RetrievalMaterializationBudgets {
9322            key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
9323                &policy.resources,
9324            )),
9325            xpath_parse: &mut xpath_parse_budget,
9326            execution: &execution_budget,
9327            resources: &policy.resources,
9328            certificate_signature_algorithms: policy
9329                .key_trust
9330                .certificate_signature_algorithms
9331                .as_ref(),
9332            xml_backend: crate::XmlBackend::default(),
9333        };
9334        let mut materialization = KeyInfoMaterializationState::default();
9335
9336        document.with_view(|view| {
9337            let resolver = UriReferenceResolver::with_document_view(view, &[])
9338                .with_external_resources(&resources);
9339            materialize_key_info_references_with_budgets(
9340                &mut key_info,
9341                &resolver,
9342                &policy,
9343                crate::provider::default_provider(),
9344                &mut budgets,
9345                &mut materialization,
9346            )?;
9347            Ok::<_, SignatureVerificationPipelineError>(())
9348        })?;
9349        Ok(key_info)
9350    }
9351
9352    fn materialize_external_key_info_bytes_with_limit(
9353        encoded: Vec<u8>,
9354        maximum_document_bytes: usize,
9355    ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
9356        let resources = HashMap::from([("key.xml".to_owned(), encoded)]);
9357        let document = XmlDocument::parse("<root/>").unwrap();
9358        let mut key_info = KeyInfo {
9359            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
9360                uri: "key.xml".into(),
9361            }],
9362        };
9363        let mut policy = crate::policy::VerificationPolicy::default();
9364        policy.key_sources.key_info_reference = true;
9365        policy.uris.key_info_references = UriTypeSet::ALL;
9366        policy.resources.max_xml_document_bytes = maximum_document_bytes;
9367        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
9368        let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
9369        let mut budgets = RetrievalMaterializationBudgets {
9370            key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
9371                &policy.resources,
9372            )),
9373            xpath_parse: &mut xpath_parse_budget,
9374            execution: &execution_budget,
9375            resources: &policy.resources,
9376            certificate_signature_algorithms: policy
9377                .key_trust
9378                .certificate_signature_algorithms
9379                .as_ref(),
9380            xml_backend: crate::XmlBackend::default(),
9381        };
9382        let mut materialization = KeyInfoMaterializationState::default();
9383
9384        document.with_view(|view| {
9385            let resolver = UriReferenceResolver::with_document_view(view, &[])
9386                .with_external_resources(&resources);
9387            materialize_key_info_references_with_budgets(
9388                &mut key_info,
9389                &resolver,
9390                &policy,
9391                crate::provider::default_provider(),
9392                &mut budgets,
9393                &mut materialization,
9394            )?;
9395            Ok::<_, SignatureVerificationPipelineError>(())
9396        })?;
9397        Ok(key_info)
9398    }
9399
9400    fn materialize_external_key_info_bytes(
9401        encoded: Vec<u8>,
9402    ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
9403        materialize_external_key_info_bytes_with_limit(
9404            encoded,
9405            crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
9406        )
9407    }
9408
9409    #[test]
9410    fn external_key_info_reference_decodes_utf16_xml_octets() {
9411        // External XML follows the XML encoding declaration/BOM contract, not
9412        // the UTF-8-only contract of Rust strings passed by direct callers.
9413        let xml = format!(
9414            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>utf16-key</ds:KeyName></ds:KeyInfo>"#
9415        );
9416        let mut encoded = vec![0xff, 0xfe];
9417        encoded.extend(xml.encode_utf16().flat_map(u16::to_le_bytes));
9418        let key_info = materialize_external_key_info_bytes(encoded)
9419            .expect("UTF-16 external KeyInfo must materialize");
9420        assert!(matches!(
9421            key_info.sources.as_slice(),
9422            [super::super::parse::KeyInfoSource::KeyName(name)] if name == "utf16-key"
9423        ));
9424    }
9425
9426    #[test]
9427    fn external_key_info_reference_rejects_malformed_xml_encoding() {
9428        // A BOM selecting UTF-16 must not permit a truncated code unit to reach
9429        // the XML parser under a misleading structural diagnostic.
9430        let error = materialize_external_key_info_bytes(vec![0xff, 0xfe, b'<'])
9431            .expect_err("truncated UTF-16 must be rejected during octet decoding");
9432
9433        assert!(matches!(
9434            error,
9435            SignatureVerificationPipelineError::InvalidStructure {
9436                reason: "KeyInfoReference external resource has an invalid XML encoding"
9437            }
9438        ));
9439    }
9440
9441    #[test]
9442    fn external_key_info_reference_preserves_decoded_document_limit() {
9443        // Decoding can expand a compact single-byte resource beyond the XML document policy.
9444        // That is a typed policy failure, not malformed KeyInfo structure.
9445        let encoded = b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><ds:KeyInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"><ds:KeyName>\xe9\xe9</ds:KeyName></ds:KeyInfo>".to_vec();
9446        let maximum = encoded.len();
9447        let error = materialize_external_key_info_bytes_with_limit(encoded, maximum)
9448            .expect_err("decoded XML expansion must retain resource-limit classification");
9449
9450        assert!(matches!(
9451            error,
9452            SignatureVerificationPipelineError::Policy(
9453                crate::policy::PolicyViolation::ResourceLimit {
9454                    resource: crate::policy::resource_name::XML_DOCUMENT,
9455                    maximum: observed_maximum,
9456                    actual,
9457                }
9458            ) if observed_maximum == maximum && actual > maximum
9459        ));
9460    }
9461
9462    #[test]
9463    fn key_info_reference_cycle_identity_includes_the_owning_resource() {
9464        // Equal fragment spellings in separate external documents are distinct
9465        // references and must not be rejected as a recursive cycle.
9466        let key_info = materialize_external_key_info_chain("<ds:KeyName>terminal</ds:KeyName>")
9467            .expect("cross-document duplicate fragments must materialize");
9468        assert!(matches!(
9469            key_info.sources.as_slice(),
9470            [super::super::parse::KeyInfoSource::KeyName(name)] if name == "terminal"
9471        ));
9472    }
9473
9474    #[test]
9475    fn key_info_reference_cycle_identity_survives_external_reparse() {
9476        // Returning to the same external resource must remain a cycle even
9477        // though each dereference creates a fresh owned XML document.
9478        let error =
9479            materialize_external_key_info_chain("<dsig11:KeyInfoReference URI=\"a.xml#root\"/>")
9480                .expect_err("a resource cycle must fail before exhausting depth");
9481        assert!(matches!(
9482            error,
9483            SignatureVerificationPipelineError::InvalidStructure {
9484                reason: "KeyInfoReference cycle detected"
9485            }
9486        ));
9487    }
9488
9489    #[test]
9490    fn retrieval_method_materialization_deduplicates_within_count_limit() {
9491        // Repeated references to the same raw certificate produce one parsed
9492        // key source rather than one certificate clone per XML element.
9493        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
9494        let certificate = include_bytes!(
9495            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
9496        )
9497        .to_vec();
9498        let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
9499        let mut key_info = KeyInfo {
9500            sources: (0..MAX_RETRIEVAL_METHOD_COUNT)
9501                .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
9502                    uri: "urn:certificate".into(),
9503                    resource_type: Some(RAW_X509_TYPE.into()),
9504                    transforms: RetrievalMethodTransforms::None,
9505                })
9506                .collect(),
9507        };
9508        let document = Document::parse("<root/>").unwrap();
9509        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9510
9511        materialize_retrieval_methods(
9512            &mut key_info,
9513            &resolver,
9514            UriTypeSet::ALL,
9515            None,
9516            crate::provider::default_provider(),
9517        )
9518        .unwrap();
9519        assert!(matches!(
9520            key_info.sources.as_slice(),
9521            [super::super::parse::KeyInfoSource::X509Data(info)]
9522                if info.certificates.len() == 1
9523        ));
9524    }
9525
9526    #[test]
9527    fn retrieval_method_candidate_budget_includes_embedded_key_values() {
9528        // A previously parsed KeyValue consumes the sole candidate slot, so
9529        // malformed retrieved DER must be rejected by policy before X.509 parsing.
9530        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
9531        let resources = HashMap::from([("urn:certificate".to_string(), vec![1, 2, 3])]);
9532        let mut key_info = KeyInfo {
9533            sources: vec![
9534                super::super::parse::KeyInfoSource::KeyValue(
9535                    super::super::parse::KeyValueInfo::Unsupported {
9536                        namespace: Some(XMLDSIG_NS.into()),
9537                        local_name: "FutureKeyValue".into(),
9538                    },
9539                ),
9540                super::super::parse::KeyInfoSource::RetrievalMethod {
9541                    uri: "urn:certificate".into(),
9542                    resource_type: Some(RAW_X509_TYPE.into()),
9543                    transforms: RetrievalMethodTransforms::None,
9544                },
9545            ],
9546        };
9547        let document = Document::parse("<root/>").unwrap();
9548        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9549        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
9550        let execution_budget = TransformExecutionBudget::default();
9551        let resource_policy = crate::policy::ResourcePolicy {
9552            max_key_candidates: 1,
9553            ..crate::policy::ResourcePolicy::default()
9554        };
9555        let mut budgets = RetrievalMaterializationBudgets {
9556            key_info_parse: &RefCell::new(super::super::parse::KeyInfoParseUsage::new(
9557                &resource_policy,
9558            )),
9559            xpath_parse: &mut xpath_parse_budget,
9560            execution: &execution_budget,
9561            resources: &resource_policy,
9562            certificate_signature_algorithms: None,
9563            xml_backend: crate::XmlBackend::default(),
9564        };
9565        let mut candidate_work = key_info.embedded_candidate_count();
9566
9567        let mut policy = crate::policy::VerificationPolicy::default();
9568        policy.uris.retrieval_methods = UriTypeSet::ALL;
9569        let error = materialize_retrieval_methods_with_budgets(
9570            &mut key_info,
9571            &resolver,
9572            &policy,
9573            crate::provider::default_provider(),
9574            &mut budgets,
9575            &mut candidate_work,
9576        )
9577        .expect_err("the retrieved certificate must exceed the aggregate candidate limit");
9578
9579        assert!(matches!(
9580            error,
9581            SignatureVerificationPipelineError::Policy(
9582                crate::policy::PolicyViolation::ResourceLimit {
9583                    resource: crate::policy::resource_name::KEY_CANDIDATES,
9584                    maximum: 1,
9585                    actual: 2,
9586                }
9587            )
9588        ));
9589    }
9590
9591    #[test]
9592    fn raw_x509_retrieval_rejects_empty_same_document_uri() {
9593        // rawX509Certificate consumes external DER octets; an empty URI denotes
9594        // the XML document and must never become a key into the external map.
9595        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
9596        let certificate = include_bytes!(
9597            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
9598        )
9599        .to_vec();
9600        let resources = HashMap::from([(String::new(), certificate)]);
9601        let mut key_info = KeyInfo {
9602            sources: vec![super::super::parse::KeyInfoSource::RetrievalMethod {
9603                uri: String::new(),
9604                resource_type: Some(RAW_X509_TYPE.into()),
9605                transforms: RetrievalMethodTransforms::None,
9606            }],
9607        };
9608        let document = Document::parse("<root/>").unwrap();
9609        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
9610
9611        let error = materialize_retrieval_methods(
9612            &mut key_info,
9613            &resolver,
9614            UriTypeSet::ALL,
9615            None,
9616            crate::provider::default_provider(),
9617        )
9618        .expect_err("empty URI must retain same-document semantics");
9619        assert!(matches!(
9620            error,
9621            SignatureVerificationPipelineError::InvalidStructure {
9622                reason: "raw X509 RetrievalMethod requires an untransformed external URI"
9623            }
9624        ));
9625    }
9626
9627    #[test]
9628    fn verify_context_does_not_hide_malformed_digest_behind_unsupported_transform() {
9629        // A bad DigestValue remains a parse error even when its transform URI is unsupported.
9630        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
9631            let xml = xml.replacen(
9632                "<ds:Reference URI=\"#target\">",
9633                "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
9634                1,
9635            );
9636            replace_fixture_manifest_digest(&xml, "!!!")
9637        });
9638
9639        let error = VerifyContext::new()
9640            .key(&AcceptingKey)
9641            .process_manifests(true)
9642            .verify(&broken_xml)
9643            .expect_err("malformed Manifest digest must not become a validity result");
9644        assert!(matches!(
9645            error,
9646            SignatureVerificationPipelineError::ParseManifestReference(_)
9647        ));
9648    }
9649
9650    #[test]
9651    fn verify_context_rejects_manifest_non_whitespace_mixed_content() {
9652        // Authenticated mixed content is still structurally invalid under the
9653        // Manifest element-only grammar.
9654        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
9655            xml.replacen(
9656                "<ds:Manifest ID=\"manifest\">",
9657                "<ds:Manifest ID=\"manifest\">junk",
9658                1,
9659            )
9660        });
9661
9662        let err = VerifyContext::new()
9663            .key(&AcceptingKey)
9664            .process_manifests(true)
9665            .verify(&xml)
9666            .expect_err("Manifest mixed content must fail verification");
9667        assert!(matches!(
9668            err,
9669            SignatureVerificationPipelineError::InvalidStructure {
9670                reason: "Manifest contains non-whitespace mixed content"
9671            }
9672        ));
9673    }
9674
9675    #[test]
9676    fn verify_context_rejects_empty_manifest_children() {
9677        // An authenticated empty Manifest violates the required Reference+
9678        // content model rather than disappearing as an unsigned block.
9679        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
9680            let (prefix, rest) = xml
9681                .split_once("<ds:Manifest ID=\"manifest\">")
9682                .expect("fixture should contain Manifest");
9683            let (_, suffix) = rest
9684                .split_once("</ds:Manifest>")
9685                .expect("fixture should contain closing Manifest");
9686            format!("{prefix}<ds:Manifest ID=\"manifest\"></ds:Manifest>{suffix}")
9687        });
9688
9689        let err = VerifyContext::new()
9690            .key(&AcceptingKey)
9691            .process_manifests(true)
9692            .verify(&xml)
9693            .expect_err("empty Manifest must fail verification");
9694        assert!(matches!(
9695            err,
9696            SignatureVerificationPipelineError::InvalidStructure {
9697                reason: "Manifest must contain at least one ds:Reference element child"
9698            }
9699        ));
9700    }
9701
9702    #[test]
9703    fn verify_context_ignores_unsigned_malformed_manifest_blocks() {
9704        let xml = signature_with_manifest_xml(true).replacen(
9705            "</ds:Object>",
9706            "</ds:Object><ds:Object><ds:Manifest>junk<ds:Foo/></ds:Manifest></ds:Object>",
9707            1,
9708        );
9709        let result = VerifyContext::new()
9710            .key(&AcceptingKey)
9711            .process_manifests(true)
9712            .verify(&xml)
9713            .expect("unsigned malformed Manifest must be ignored");
9714        assert_eq!(
9715            result.manifest_references.len(),
9716            1,
9717            "only signed Manifest references must be reported",
9718        );
9719        assert!(matches!(result.status, DsigStatus::Valid));
9720    }
9721
9722    #[test]
9723    fn verify_context_skips_ambiguous_manifest_id_blocks() {
9724        let xml = signature_with_manifest_xml(true).replacen(
9725            "</ds:Object>",
9726            "</ds:Object><ds:Object><ds:Manifest ID=\"manifest\">junk<ds:Foo/></ds:Manifest></ds:Object>",
9727            1,
9728        );
9729        let err = VerifyContext::new()
9730            .key(&RejectingKey)
9731            .process_manifests(true)
9732            .verify(&xml)
9733            .expect_err("ambiguous manifest IDs should make SignedInfo #manifest dereference fail");
9734        assert!(matches!(
9735            err,
9736            SignatureVerificationPipelineError::Reference(
9737                ReferenceProcessingError::UriDereference(
9738                    crate::xmldsig::types::TransformError::ElementNotFound(id)
9739                )
9740            ) if id == "manifest"
9741        ));
9742    }
9743
9744    #[test]
9745    fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
9746        let xml = minimal_signature_xml("", "");
9747        let err = VerifyContext::new()
9748            .key(&RejectingKey)
9749            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
9750            .verify(&xml)
9751            .expect_err("implicit default C14N must be checked against allowlist");
9752        assert!(matches!(
9753            err,
9754            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
9755                operation: "verification transform",
9756                ..
9757            })
9758        ));
9759    }
9760
9761    #[test]
9762    fn verify_context_skips_resolver_when_reference_processing_fails() {
9763        let xml = minimal_signature_xml("", "");
9764        let result = VerifyContext::new()
9765            .key_resolver(&PanicResolver)
9766            .verify(&xml)
9767            .expect("reference digest mismatch should short-circuit before resolver");
9768        assert!(matches!(
9769            result.status,
9770            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
9771        ));
9772    }
9773
9774    #[test]
9775    fn verify_context_leaves_fail_fast_reference_tail_unexecuted() {
9776        // A mismatch on the first Reference must not produce graph evidence for
9777        // the second Reference, which was never dereferenced or digested.
9778        let second_reference = r#"
9779    <ds:Reference URI="">
9780      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
9781      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
9782    </ds:Reference>"#;
9783        let xml = minimal_signature_xml("", "").replacen(
9784            "  </ds:SignedInfo>",
9785            &format!("{second_reference}\n  </ds:SignedInfo>"),
9786            1,
9787        );
9788
9789        let result = VerifyContext::new()
9790            .key(&AcceptingKey)
9791            .verify(&xml)
9792            .expect("digest mismatch must remain a verification result");
9793
9794        assert_eq!(result.signed_info_references.len(), 1);
9795        assert!(matches!(
9796            result.signed_info_references[0].status,
9797            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
9798        ));
9799    }
9800
9801    #[test]
9802    fn verify_context_reports_key_not_found_when_resolver_misses() {
9803        let xml = signature_with_target_reference("AQ==");
9804        let result = VerifyContext::new()
9805            .key_resolver(&MissingKeyResolver)
9806            .verify(&xml)
9807            .expect("resolver miss should report status, not pipeline error");
9808        assert!(matches!(
9809            result.status,
9810            DsigStatus::Invalid(FailureReason::KeyNotFound)
9811        ));
9812        assert_eq!(
9813            result.signed_info_references.len(),
9814            1,
9815            "KeyNotFound path must preserve SignedInfo reference diagnostics",
9816        );
9817        assert!(matches!(
9818            result.signed_info_references[0].status,
9819            DsigStatus::Valid
9820        ));
9821    }
9822
9823    #[test]
9824    fn verification_candidate_budget_covers_preset_and_custom_resolver_paths() {
9825        // Zero is a valid deny-all ceiling. Neither an already-resolved key nor
9826        // a custom resolver may bypass the operation-wide candidate policy.
9827        let xml = signature_with_target_reference("AQ==");
9828        let mut policy = crate::policy::VerificationPolicy::default();
9829        policy.resources.max_key_candidates = 0;
9830
9831        let preset_error = VerifyContext::new()
9832            .key(&RejectingKey)
9833            .policy(policy.clone())
9834            .verify(&xml)
9835            .expect_err("a preset key consumes one candidate");
9836        assert!(matches!(
9837            preset_error,
9838            SignatureVerificationPipelineError::Policy(
9839                crate::policy::PolicyViolation::ResourceLimit {
9840                    resource: crate::policy::resource_name::KEY_CANDIDATES,
9841                    maximum: 0,
9842                    actual: 1,
9843                }
9844            )
9845        ));
9846
9847        let resolver_error = VerifyContext::new()
9848            .key_resolver(&PanicResolver)
9849            .policy(policy)
9850            .verify(&xml)
9851            .expect_err("a custom resolver requires candidate capacity before dispatch");
9852        assert!(matches!(
9853            resolver_error,
9854            SignatureVerificationPipelineError::Policy(
9855                crate::policy::PolicyViolation::ResourceLimit {
9856                    resource: crate::policy::resource_name::KEY_CANDIDATES,
9857                    maximum: 0,
9858                    actual: 1,
9859                }
9860            )
9861        ));
9862    }
9863
9864    #[test]
9865    fn verification_candidate_account_is_not_reset_for_each_signature() {
9866        // A rejected signature still spends its candidate allowance in verify_all.
9867        let xml = signature_with_target_reference("AQ==");
9868        let start = xml
9869            .find("<ds:Signature>")
9870            .expect("generated signature start");
9871        let end = xml
9872            .find("</ds:Signature>")
9873            .expect("generated signature end")
9874            + "</ds:Signature>".len();
9875        let document =
9876            XmlDocument::parse(xml.replace("</root>", &format!("{}</root>", &xml[start..end])))
9877                .expect("two signatures");
9878        let mut policy = crate::policy::VerificationPolicy::default();
9879        policy.resources.max_key_candidates = 1;
9880        let evidence = VerifyContext::new()
9881            .key(&RejectingKey)
9882            .policy(policy)
9883            .verify_all(&document)
9884            .expect("signature-local failure report");
9885        assert_eq!(evidence.signatures().len(), 2);
9886        assert!(evidence.signatures()[0].result().is_ok());
9887        assert!(matches!(
9888            evidence.signatures()[1].result(),
9889            Err(DsigError::Policy(
9890                crate::policy::PolicyViolation::ResourceLimit {
9891                    resource: crate::policy::resource_name::KEY_CANDIDATES,
9892                    maximum: 1,
9893                    actual: 2,
9894                }
9895            ))
9896        ));
9897    }
9898
9899    #[test]
9900    fn resolver_receives_the_certificate_account_spent_by_ingestion() {
9901        // The public resolver hook receives prior parsing work, not a fresh allowance.
9902        struct ProbeResolver {
9903            called: Cell<bool>,
9904        }
9905        impl KeyResolver for ProbeResolver {
9906            fn resolve<'a>(
9907                &'a self,
9908                _info: Option<&KeyInfo>,
9909                _algorithm: SignatureAlgorithm,
9910            ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
9911                panic!("core must dispatch through the budget-aware hook");
9912            }
9913            fn resolve_for_verification_with_budget<'a>(
9914                &'a self,
9915                _info: Option<&KeyInfo>,
9916                _algorithm: SignatureAlgorithm,
9917                _policy: &crate::policy::VerificationPolicy,
9918                _provider: &dyn crate::provider::CryptoProvider,
9919                budget: &mut super::super::InspectedKeyCandidateBudget,
9920            ) -> Result<Option<super::super::ResolvedVerificationKey<'a>>, DsigError> {
9921                assert!(budget.execution().work_used() > 0);
9922                budget.charge()?;
9923                self.called.set(true);
9924                Ok(Some(super::super::ResolvedVerificationKey::CallerTrusted(
9925                    Box::new(AcceptingKey),
9926                )))
9927            }
9928            fn consumes_document_key_info(&self) -> bool {
9929                true
9930            }
9931        }
9932        let pair = rcgen::KeyPair::generate().expect("test key generation");
9933        let certificate = rcgen::CertificateParams::new(Vec::<String>::new())
9934            .expect("empty SAN list")
9935            .self_signed(&pair)
9936            .expect("test certificate");
9937        let encoded = base64::engine::general_purpose::STANDARD.encode(certificate.der());
9938        let xml = signature_with_target_reference("AQ==").replace("</ds:Signature>", &format!(
9939            "<ds:KeyInfo><ds:X509Data><ds:X509Certificate>{encoded}</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>",
9940        ));
9941        let resolver = ProbeResolver {
9942            called: Cell::new(false),
9943        };
9944        let result = VerifyContext::new()
9945            .key_resolver(&resolver)
9946            .verify(&xml)
9947            .expect("shared-account resolver dispatch");
9948        assert!(matches!(result.status, DsigStatus::Valid));
9949        assert!(resolver.called.get());
9950    }
9951
9952    #[test]
9953    fn verification_candidate_budget_precedes_embedded_x509_parsing() {
9954        // The first certificate is valid and consumes the sole permitted slot;
9955        // malformed bytes in the second must never reach the X.509 parser.
9956        let first_certificate = base64::engine::general_purpose::STANDARD.encode(include_bytes!(
9957            "../../tests/fixtures/xmldsig/phaos-xmldsig-three/certs/rsa-cert.der"
9958        ));
9959        let xml = signature_with_target_reference("AQ==").replace(
9960            "</ds:SignatureValue>\n  </ds:Signature>",
9961            &format!(
9962                "</ds:SignatureValue>\n    <ds:KeyInfo><ds:X509Data><ds:X509Certificate>{first_certificate}</ds:X509Certificate><ds:X509Certificate>AQID</ds:X509Certificate></ds:X509Data></ds:KeyInfo>\n  </ds:Signature>"
9963            ),
9964        );
9965        let mut policy = crate::policy::VerificationPolicy::default();
9966        policy.resources.max_key_candidates = 1;
9967
9968        let error = VerifyContext::new()
9969            .policy(policy)
9970            .verify(&xml)
9971            .expect_err("candidate policy must run before embedded certificate parsing");
9972
9973        assert!(matches!(
9974            error,
9975            SignatureVerificationPipelineError::Policy(
9976                crate::policy::PolicyViolation::ResourceLimit {
9977                    resource: crate::policy::resource_name::KEY_CANDIDATES,
9978                    maximum: 1,
9979                    actual: 2,
9980                }
9981            )
9982        ));
9983    }
9984
9985    #[test]
9986    fn verify_context_resolver_can_ignore_malformed_keyinfo_by_default() {
9987        let base_xml = signature_with_target_reference("AQ==");
9988        let xml = base_xml
9989            .replace(
9990                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
9991                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
9992            )
9993            .replace(
9994                "</ds:SignatureValue>\n  </ds:Signature>",
9995                "</ds:SignatureValue>\n    <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n  </ds:Signature>",
9996            );
9997
9998        let result = VerifyContext::new()
9999            .key_resolver(&MissingKeyResolver)
10000            .verify(&xml)
10001            .expect("resolver path should not hard-fail on advisory malformed KeyInfo by default");
10002        assert!(matches!(
10003            result.status,
10004            DsigStatus::Invalid(FailureReason::KeyNotFound)
10005        ));
10006    }
10007
10008    #[test]
10009    fn verify_context_resolver_can_opt_in_to_keyinfo_parse_failures() {
10010        let base_xml = signature_with_target_reference("AQ==");
10011        let xml = base_xml
10012            .replace(
10013                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
10014                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
10015            )
10016            .replace(
10017                "</ds:SignatureValue>\n  </ds:Signature>",
10018                "</ds:SignatureValue>\n    <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n  </ds:Signature>",
10019            );
10020
10021        let err = VerifyContext::new()
10022            .key_resolver(&ConsumingKeyInfoResolver)
10023            .verify(&xml)
10024            .expect_err("resolver opted into KeyInfo parsing, malformed KeyInfo must fail");
10025        assert!(matches!(
10026            err,
10027            SignatureVerificationPipelineError::ParseKeyInfo(_)
10028        ));
10029    }
10030
10031    #[test]
10032    fn verify_context_ignores_unsupported_retrieval_before_valid_key_source() {
10033        // An advisory vendor RetrievalMethod cannot prevent the resolver from
10034        // reaching a later supported source in document order.
10035        let xml = signature_with_target_reference("AQ==").replace(
10036            "</ds:SignatureValue>\n  </ds:Signature>",
10037            r##"</ds:SignatureValue>
10038    <ds:KeyInfo>
10039      <ds:RetrievalMethod URI="#vendor" Type="urn:vendor:key">
10040        <ds:Transforms><ds:Transform Algorithm="urn:vendor:transform"/></ds:Transforms>
10041      </ds:RetrievalMethod>
10042      <ds:KeyName>fallback</ds:KeyName>
10043    </ds:KeyInfo>
10044  </ds:Signature>"##,
10045        );
10046
10047        let result = VerifyContext::new()
10048            .key_resolver(&FallbackKeyInfoResolver)
10049            .verify(&xml)
10050            .expect("unsupported advisory retrieval must not abort key resolution");
10051        assert_eq!(result.status, DsigStatus::Valid);
10052    }
10053
10054    #[test]
10055    fn verify_context_does_not_eagerly_fail_unused_retrieval_fallback() {
10056        // KeyInfo sources are alternatives in document order. Once an earlier
10057        // source resolves, a missing later RetrievalMethod is irrelevant.
10058        let xml = signature_with_target_reference("AQ==").replace(
10059            "</ds:SignatureValue>\n  </ds:Signature>",
10060            r#"</ds:SignatureValue>
10061    <ds:KeyInfo>
10062      <ds:KeyName>primary</ds:KeyName>
10063      <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
10064    </ds:KeyInfo>
10065  </ds:Signature>"#,
10066        );
10067
10068        let result = VerifyContext::new()
10069            .key_resolver(&EarlyKeyInfoResolver)
10070            .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
10071            .verify(&xml)
10072            .expect("an unused missing retrieval fallback must not abort verification");
10073
10074        assert_eq!(result.status, DsigStatus::Valid);
10075    }
10076
10077    #[test]
10078    fn verify_context_does_not_eagerly_fail_unused_same_document_x509_retrieval() {
10079        // Same-document X509Data retrieval is an ordered alternative just like
10080        // raw-certificate retrieval; a resolved earlier source makes it unused.
10081        let xml = signature_with_target_reference("AQ==").replace(
10082            "</ds:SignatureValue>\n  </ds:Signature>",
10083            r##"</ds:SignatureValue>
10084    <ds:KeyInfo>
10085      <ds:KeyName>primary</ds:KeyName>
10086      <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
10087    </ds:KeyInfo>
10088  </ds:Signature>"##,
10089        );
10090
10091        let result = VerifyContext::new()
10092            .key_resolver(&EarlyKeyInfoResolver)
10093            .verify(&xml)
10094            .expect("an unused missing X509Data retrieval fallback must not abort verification");
10095
10096        assert_eq!(result.status, DsigStatus::Valid);
10097    }
10098
10099    #[test]
10100    fn verify_context_reports_missing_same_document_x509_retrieval_without_fallback() {
10101        // Deferred failures retain their exact diagnostic when no alternative
10102        // source resolves the verification key.
10103        let xml = signature_with_target_reference("AQ==").replace(
10104            "</ds:SignatureValue>\n  </ds:Signature>",
10105            r##"</ds:SignatureValue>
10106    <ds:KeyInfo>
10107      <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
10108    </ds:KeyInfo>
10109  </ds:Signature>"##,
10110        );
10111
10112        let error = VerifyContext::new()
10113            .key_resolver(&ConsumingKeyInfoResolver)
10114            .verify(&xml)
10115            .expect_err("a sole missing X509Data retrieval must remain an explicit error");
10116
10117        assert!(matches!(
10118            error,
10119            SignatureVerificationPipelineError::InvalidStructure {
10120                reason: "X509Data RetrievalMethod target is missing or ambiguous"
10121            }
10122        ));
10123    }
10124
10125    #[test]
10126    fn verify_context_does_not_eagerly_parse_unused_retrieval_fallback() {
10127        // Materialization must preserve ordered fallback semantics even when
10128        // caller-supplied bytes exist but are not a certificate.
10129        let xml = signature_with_target_reference("AQ==").replace(
10130            "</ds:SignatureValue>\n  </ds:Signature>",
10131            r#"</ds:SignatureValue>
10132    <ds:KeyInfo>
10133      <ds:KeyName>primary</ds:KeyName>
10134      <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
10135    </ds:KeyInfo>
10136  </ds:Signature>"#,
10137        );
10138        let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
10139
10140        let result = VerifyContext::new()
10141            .key_resolver(&EarlyKeyInfoResolver)
10142            .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
10143            .external_resources(&resources)
10144            .verify(&xml)
10145            .expect("an unused malformed retrieval fallback must not abort verification");
10146
10147        assert_eq!(result.status, DsigStatus::Valid);
10148    }
10149
10150    #[test]
10151    fn verify_context_reports_missing_retrieval_when_no_key_source_resolves() {
10152        // Deferral changes ordering, not diagnostics: if no alternative source
10153        // resolves, the first missing retrieval remains the pipeline failure.
10154        let xml = signature_with_target_reference("AQ==").replace(
10155            "</ds:SignatureValue>\n  </ds:Signature>",
10156            r#"</ds:SignatureValue>
10157    <ds:KeyInfo>
10158      <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
10159    </ds:KeyInfo>
10160  </ds:Signature>"#,
10161        );
10162
10163        let error = VerifyContext::new()
10164            .key_resolver(&ConsumingKeyInfoResolver)
10165            .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
10166            .verify(&xml)
10167            .expect_err("a missing sole RetrievalMethod must remain an explicit error");
10168
10169        assert!(matches!(
10170            error,
10171            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
10172                crate::xmldsig::TransformError::UnsupportedUri(uri)
10173            )) if uri == "missing.der"
10174        ));
10175    }
10176
10177    #[test]
10178    fn verify_context_reports_malformed_retrieval_when_no_key_source_resolves() {
10179        // Deferral must retain the parse error when the malformed certificate
10180        // is the only candidate rather than degrading it to KeyNotFound.
10181        let xml = signature_with_target_reference("AQ==").replace(
10182            "</ds:SignatureValue>\n  </ds:Signature>",
10183            r#"</ds:SignatureValue>
10184    <ds:KeyInfo>
10185      <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
10186    </ds:KeyInfo>
10187  </ds:Signature>"#,
10188        );
10189        let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
10190
10191        let error = VerifyContext::new()
10192            .key_resolver(&ConsumingKeyInfoResolver)
10193            .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
10194            .external_resources(&resources)
10195            .verify(&xml)
10196            .expect_err("a malformed sole RetrievalMethod must remain a parse error");
10197
10198        assert!(matches!(
10199            error,
10200            SignatureVerificationPipelineError::ParseKeyInfo(_)
10201        ));
10202    }
10203
10204    #[test]
10205    fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
10206        let xml = signature_with_target_reference("@@@");
10207
10208        let err = VerifyContext::new()
10209            .key_resolver(&MissingKeyResolver)
10210            .verify(&xml)
10211            .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
10212        assert!(matches!(
10213            err,
10214            SignatureVerificationPipelineError::SignatureValueBase64(_)
10215        ));
10216    }
10217
10218    #[test]
10219    fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
10220        let xml = signature_with_target_reference("@@@");
10221
10222        let err = VerifyContext::new()
10223            .verify(&xml)
10224            .expect_err("invalid SignatureValue must remain a decode error");
10225        assert!(matches!(
10226            err,
10227            SignatureVerificationPipelineError::SignatureValueBase64(_)
10228        ));
10229    }
10230
10231    #[test]
10232    fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
10233        let references = vec![Reference {
10234            uri: None,
10235            id: None,
10236            ref_type: None,
10237            transforms: vec![],
10238            digest_method: DigestAlgorithm::Sha256,
10239            digest_value: vec![0; 32],
10240        }];
10241        let uri_types = UriTypeSet {
10242            allow_empty: false,
10243            allow_same_document: true,
10244            allow_external: false,
10245        };
10246
10247        let err = enforce_reference_policies(&references, uri_types, None)
10248            .expect_err("missing URI must fail before allow_empty policy is evaluated");
10249        assert!(matches!(
10250            err,
10251            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
10252        ));
10253    }
10254
10255    #[test]
10256    fn enforce_reference_policies_checks_only_terminal_binary_output() {
10257        let c14n = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI).unwrap();
10258        let allowed = HashSet::from([
10259            BASE64_TRANSFORM_URI.to_owned(),
10260            DEFAULT_IMPLICIT_C14N_URI.to_owned(),
10261        ]);
10262        let without_implicit_c14n = HashSet::from([BASE64_TRANSFORM_URI.to_owned()]);
10263
10264        for transforms in [
10265            vec![Transform::Base64Decode, Transform::C14n(c14n)],
10266            vec![Transform::Base64Decode, Transform::Base64Decode],
10267        ] {
10268            let reference = make_reference("", transforms, DigestAlgorithm::Sha256, vec![0; 32]);
10269            enforce_reference_policies(
10270                std::slice::from_ref(&reference),
10271                UriTypeSet::default(),
10272                Some(&allowed),
10273            )
10274            .expect("terminal binary output must not require implicit C14N");
10275        }
10276
10277        let terminal_base64 = make_reference(
10278            "",
10279            vec![Transform::Base64Decode, Transform::Base64Decode],
10280            DigestAlgorithm::Sha256,
10281            vec![0; 32],
10282        );
10283        enforce_reference_policies(
10284            std::slice::from_ref(&terminal_base64),
10285            UriTypeSet::default(),
10286            Some(&without_implicit_c14n),
10287        )
10288        .expect("terminal Base64 output must not require implicit C14N");
10289
10290        let no_transforms = make_reference("", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
10291        let error = enforce_reference_policies(
10292            std::slice::from_ref(&no_transforms),
10293            UriTypeSet::default(),
10294            Some(&without_implicit_c14n),
10295        )
10296        .expect_err("a node-set result must require allowlisted implicit C14N");
10297        assert!(matches!(
10298            error,
10299            SignatureVerificationPipelineError::Policy(
10300                crate::policy::PolicyViolation::Algorithm {
10301                    operation: "verification transform",
10302                    ref algorithm,
10303                }
10304            )
10305                if algorithm == DEFAULT_IMPLICIT_C14N_URI
10306        ));
10307
10308        let detached = make_reference("urn:payload", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
10309        enforce_reference_policies(
10310            std::slice::from_ref(&detached),
10311            UriTypeSet::ALL,
10312            Some(&without_implicit_c14n),
10313        )
10314        .expect("external octets without transforms must not require implicit C14N");
10315
10316        let external_xpath = make_reference(
10317            "urn:payload",
10318            vec![Transform::XPath(
10319                super::super::transforms::XPathExpression::new("true()"),
10320            )],
10321            DigestAlgorithm::Sha256,
10322            vec![0; 32],
10323        );
10324        let error = enforce_reference_policies(
10325            std::slice::from_ref(&external_xpath),
10326            UriTypeSet::ALL,
10327            Some(&HashSet::from([XPATH_TRANSFORM_URI.to_owned()])),
10328        )
10329        .expect_err("external XML converted to a node-set must require implicit C14N");
10330        assert!(matches!(
10331            error,
10332            SignatureVerificationPipelineError::Policy(
10333                crate::policy::PolicyViolation::Algorithm {
10334                    operation: "verification transform",
10335                    ref algorithm,
10336                }
10337            )
10338                if algorithm == DEFAULT_IMPLICIT_C14N_URI
10339        ));
10340    }
10341
10342    #[test]
10343    fn stored_pre_digest_budget_counts_repeated_external_references() {
10344        // The caller map owns one bounded payload, but diagnostic retention is
10345        // charged per Reference because every result owns its pre-digest bytes.
10346        let document =
10347            Document::parse("<ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"/>")
10348                .unwrap();
10349        let payload = vec![b'x'; 7];
10350        let digest = compute_digest(DigestAlgorithm::Sha256, &payload);
10351        let references = (0..5)
10352            .map(|_| {
10353                make_reference(
10354                    "urn:repeated",
10355                    Vec::new(),
10356                    DigestAlgorithm::Sha256,
10357                    digest.clone(),
10358                )
10359            })
10360            .collect::<Vec<_>>();
10361        let resources = HashMap::from([("urn:repeated".to_owned(), payload)]);
10362        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
10363        let transform_budget = TransformExecutionBudget::default();
10364        let canonicalized_data_budget = CanonicalizedDataBudget::with_limit(32);
10365        let execution = ReferenceExecutionContext {
10366            store_pre_digest: true,
10367            transform_options: TransformOptions::default(),
10368            transform_budget: &transform_budget,
10369            canonicalized_data_budget: &canonicalized_data_budget,
10370            provider: crate::provider::default_provider(),
10371            capture: &(),
10372        };
10373
10374        let error = process_all_references_with_options(
10375            &references,
10376            &resolver,
10377            document.root_element(),
10378            &execution,
10379        )
10380        .expect_err(
10381            "retained diagnostics must not multiply one external allocation past the aggregate cap",
10382        );
10383        assert!(matches!(
10384            error,
10385            ReferenceProcessingError::Policy(crate::policy::PolicyViolation::ResourceLimit {
10386                resource: "canonicalized bytes",
10387                maximum: 32,
10388                ..
10389            })
10390        ));
10391    }
10392
10393    #[test]
10394    fn public_reference_entries_inherit_custom_id_registration() {
10395        // Both public entry points must pass resolver registrations to XPath.
10396        let xml = format!(
10397            "<root xmlns:ds='{XMLDSIG_NS}'><data Token='target'>YWJj</data><ds:Signature/></root>"
10398        );
10399        let document = Document::parse(&xml).unwrap();
10400        let signature = document
10401            .descendants()
10402            .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
10403            .unwrap();
10404        let registrations = [crate::IdAttributeRegistration::global("Token")];
10405        let resolver = UriReferenceResolver::with_id_registrations(&document, &registrations);
10406        let expression = crate::xmldsig::transforms::XPathExpression::new(
10407            "count(id('target')) = 1 and count(ancestor-or-self::node() | id('target')) = count(ancestor-or-self::node())",
10408        );
10409        let reference = make_reference(
10410            "#target",
10411            vec![Transform::XPath(expression), Transform::Base64Decode],
10412            DigestAlgorithm::Sha256,
10413            compute_digest(DigestAlgorithm::Sha256, b"abc"),
10414        );
10415        let one = process_reference(
10416            &reference,
10417            &resolver,
10418            signature,
10419            ReferenceSet::SignedInfo,
10420            0,
10421            true,
10422        )
10423        .unwrap();
10424        assert_eq!(one.status, DsigStatus::Valid);
10425        let all = process_all_references(&[reference], &resolver, signature, true).unwrap();
10426        assert_eq!(all.results.len(), 1);
10427        assert_eq!(all.results[0].status, DsigStatus::Valid);
10428        assert!(all.first_failure.is_none());
10429    }
10430
10431    #[test]
10432    fn normalized_root_xpointers_bind_document_identity() {
10433        // Spelling changes must not discard coverage evidence for the root.
10434        let document = XmlDocument::parse("<root><item/></root>").unwrap();
10435        let budgets = VerificationOperationBudgets::with_transforms(
10436            &crate::policy::VerificationPolicy::default(),
10437            TransformExecutionBudget::default(),
10438            None,
10439        );
10440        document.with_view(|view| {
10441            let resolver = UriReferenceResolver::new(view.document());
10442            for uri in ["#xpointer(/)", "#xpointer( / )", "#xpointer(%2F)"] {
10443                let reference =
10444                    make_reference(uri, Vec::new(), DigestAlgorithm::Sha256, vec![0; 32]);
10445                assert_eq!(
10446                    budgets.resource_identity_for_reference(&reference, 0, &resolver, view, None),
10447                    OperationResourceIdentity::DocumentNode(view.root()),
10448                    "{uri}"
10449                );
10450            }
10451        });
10452    }
10453
10454    #[test]
10455    fn missing_external_identity_does_not_retain_uri_cache_copies() {
10456        // No bytes exist to fingerprint or reuse. The graph's Reference node
10457        // already carries provenance, so a missing URI must not enter the
10458        // available-resource memoization cache or be cloned with every decision.
10459        let document = XmlDocument::parse("<root/>").unwrap();
10460        let uri = format!("urn:missing:{}", "x".repeat(16384));
10461        let reference = make_reference(&uri, Vec::new(), DigestAlgorithm::Sha256, vec![0; 32]);
10462        let budgets = VerificationOperationBudgets::with_transforms(
10463            &crate::policy::VerificationPolicy::default(),
10464            TransformExecutionBudget::default(),
10465            None,
10466        );
10467        document.with_view(|view| {
10468            let resolver = UriReferenceResolver::new(view.document());
10469            let _ = budgets.resource_identity_for_reference(&reference, 0, &resolver, view, None);
10470            assert!(budgets.external_resource_identities.borrow().is_empty());
10471        });
10472    }
10473
10474    #[test]
10475    fn operation_memoizes_repeated_external_resource_identity() {
10476        // The external resource map is immutably borrowed for the operation,
10477        // so one content fingerprint safely identifies every repeated URI.
10478        let document = XmlDocument::parse("<root/>").expect("fixture must parse");
10479        let resources = HashMap::from([("urn:repeated".to_owned(), vec![b'x'; 8 * 1_024])]);
10480        let reference = make_reference(
10481            "urn:repeated",
10482            Vec::new(),
10483            DigestAlgorithm::Sha256,
10484            vec![0; 32],
10485        );
10486        let budgets = VerificationOperationBudgets::with_transforms(
10487            &crate::policy::VerificationPolicy::default(),
10488            TransformExecutionBudget::default(),
10489            None,
10490        );
10491
10492        document.with_view(|view| {
10493            let resolver =
10494                UriReferenceResolver::new(view.document()).with_external_resources(&resources);
10495            let first =
10496                budgets.resource_identity_for_reference(&reference, 0, &resolver, view, None);
10497            let second =
10498                budgets.resource_identity_for_reference(&reference, 1, &resolver, view, None);
10499
10500            assert_eq!(first, second);
10501            assert_eq!(budgets.external_resource_identities.borrow().len(), 1);
10502        });
10503    }
10504
10505    #[test]
10506    fn canonical_signed_info_obeys_policy_without_diagnostic_retention() {
10507        // SignedInfo is always materialized for crypto verification, so its
10508        // canonical bytes must consume the configured ceiling even when
10509        // diagnostics do not retain reference output.
10510        let xml = signature_with_target_reference("AQ==");
10511        let marker = "<ds:SignatureMethod";
10512        let padding = " ".repeat(1_025);
10513        let xml = xml.replacen(marker, &format!("{padding}{marker}"), 1);
10514        let policy = crate::policy::VerificationPolicy {
10515            resources: crate::policy::ResourcePolicy {
10516                max_canonicalized_bytes: 1_024,
10517                ..crate::policy::ResourcePolicy::default()
10518            },
10519            ..crate::policy::VerificationPolicy::default()
10520        };
10521
10522        let error = VerifyContext::new()
10523            .key(&AcceptingKey)
10524            .policy(policy)
10525            .verify(&xml)
10526            .expect_err("canonicalized SignedInfo must remain policy-bounded");
10527
10528        assert!(matches!(
10529            error,
10530            SignatureVerificationPipelineError::Policy(
10531                crate::policy::PolicyViolation::ResourceLimit {
10532                    resource: "canonicalized bytes",
10533                    ..
10534                }
10535            )
10536        ));
10537    }
10538
10539    #[test]
10540    fn fragmented_pq_signature_value_decodes_at_exact_wire_limit() {
10541        // Comments split the largest supported signature into tiny text nodes;
10542        // decoding must preserve bytes and reject an oversized aggregate.
10543        let parameter = super::super::pq_algorithm::PqAlgorithm::SlhDsaSha2_256f;
10544        let bytes = vec![0x5a; parameter.signature_len()];
10545        let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes);
10546        let mut xml = String::from("<SignatureValue>");
10547        for chunk in encoded.as_bytes().chunks(4) {
10548            xml.push_str(std::str::from_utf8(chunk).unwrap());
10549            xml.push_str("<!--split-->\n");
10550        }
10551        xml.push_str("</SignatureValue>");
10552        let document = Document::parse(&xml).unwrap();
10553        let algorithm = SignatureAlgorithm::PostQuantum(parameter);
10554        assert_eq!(
10555            decode_signature_value(
10556                document.root_element(),
10557                algorithm,
10558                &crate::policy::ResourcePolicy::default()
10559            )
10560            .unwrap(),
10561            bytes
10562        );
10563        let oversized = xml.replace("</SignatureValue>", "AAAA</SignatureValue>");
10564        let document = Document::parse(&oversized).unwrap();
10565        assert!(matches!(
10566            decode_signature_value(
10567                document.root_element(),
10568                algorithm,
10569                &crate::policy::ResourcePolicy::default()
10570            ),
10571            Err(SignatureVerificationPipelineError::InvalidStructure {
10572                reason: "SignatureValue exceeds maximum allowed length"
10573            })
10574        ));
10575    }
10576
10577    #[test]
10578    fn push_normalized_signature_text_rejects_form_feed() {
10579        let mut normalized = Vec::new();
10580        let mut raw_text_len = 0usize;
10581        let err = push_normalized_signature_text(
10582            "ab\u{000C}cd",
10583            &mut raw_text_len,
10584            &mut normalized,
10585            SignatureAlgorithm::RsaSha256,
10586            &crate::policy::ResourcePolicy::default(),
10587        )
10588        .expect_err("form-feed must not be treated as XML base64 whitespace");
10589        assert!(matches!(
10590            err,
10591            SignatureVerificationPipelineError::SignatureValueBase64(
10592                base64::DecodeError::InvalidByte(_, 0x0C)
10593            )
10594        ));
10595    }
10596
10597    #[test]
10598    fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
10599        let mut normalized = vec![b'A'; MAX_SIGNATURE_VALUE_LEN - 1];
10600        let mut raw_text_len = normalized.len();
10601        let err = push_normalized_signature_text(
10602            "é",
10603            &mut raw_text_len,
10604            &mut normalized,
10605            SignatureAlgorithm::RsaSha256,
10606            &crate::policy::ResourcePolicy::default(),
10607        )
10608        .expect_err("multibyte characters must not bypass byte-size limit");
10609        assert!(matches!(
10610            err,
10611            SignatureVerificationPipelineError::InvalidStructure {
10612                reason: "SignatureValue exceeds maximum allowed length"
10613            }
10614        ));
10615    }
10616
10617    // ── process_reference: happy path ────────────────────────────────
10618
10619    #[test]
10620    fn reference_with_correct_digest_passes() {
10621        // Create a simple document, compute its canonical form digest,
10622        // then verify that process_reference returns Valid status.
10623        let xml = r##"<root>
10624            <data>hello world</data>
10625            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
10626                <ds:SignedInfo/>
10627            </ds:Signature>
10628        </root>"##;
10629        let doc = Document::parse(xml).unwrap();
10630        let resolver = UriReferenceResolver::new(&doc);
10631        let sig_node = doc
10632            .descendants()
10633            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
10634            .unwrap();
10635
10636        // First, compute the expected digest by running the pipeline
10637        let initial_data = resolver.dereference("").unwrap();
10638        let transforms = vec![
10639            Transform::Enveloped,
10640            Transform::C14n(
10641                crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
10642                    .unwrap(),
10643            ),
10644        ];
10645        let pre_digest_bytes =
10646            crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
10647        let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
10648
10649        // Now build a Reference with the correct digest and verify
10650        let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
10651
10652        let result = process_reference(
10653            &reference,
10654            &resolver,
10655            sig_node,
10656            ReferenceSet::SignedInfo,
10657            0,
10658            false,
10659        )
10660        .unwrap();
10661        assert!(
10662            matches!(result.status, DsigStatus::Valid),
10663            "digest should match"
10664        );
10665        assert!(result.pre_digest_data.is_none());
10666    }
10667
10668    #[test]
10669    fn reference_with_wrong_digest_fails() {
10670        let xml = r##"<root>
10671            <data>hello</data>
10672            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
10673                <ds:SignedInfo/>
10674            </ds:Signature>
10675        </root>"##;
10676        let doc = Document::parse(xml).unwrap();
10677        let resolver = UriReferenceResolver::new(&doc);
10678        let sig_node = doc
10679            .descendants()
10680            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
10681            .unwrap();
10682
10683        let transforms = vec![Transform::Enveloped];
10684        // Wrong digest value — all zeros
10685        let wrong_digest = vec![0u8; 32];
10686        let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
10687
10688        let result = process_reference(
10689            &reference,
10690            &resolver,
10691            sig_node,
10692            ReferenceSet::SignedInfo,
10693            0,
10694            false,
10695        )
10696        .unwrap();
10697        assert!(matches!(
10698            result.status,
10699            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
10700        ));
10701    }
10702
10703    #[test]
10704    fn reference_with_wrong_digest_preserves_supplied_ref_index() {
10705        let xml = r##"<root>
10706            <data>hello</data>
10707            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
10708                <ds:SignedInfo/>
10709            </ds:Signature>
10710        </root>"##;
10711        let doc = Document::parse(xml).unwrap();
10712        let resolver = UriReferenceResolver::new(&doc);
10713        let sig_node = doc
10714            .descendants()
10715            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
10716            .unwrap();
10717
10718        let reference = make_reference(
10719            "",
10720            vec![Transform::Enveloped],
10721            DigestAlgorithm::Sha256,
10722            vec![0u8; 32],
10723        );
10724        let result = process_reference(
10725            &reference,
10726            &resolver,
10727            sig_node,
10728            ReferenceSet::SignedInfo,
10729            7,
10730            false,
10731        )
10732        .unwrap();
10733        assert!(matches!(
10734            result.status,
10735            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
10736        ));
10737    }
10738
10739    #[test]
10740    fn reference_stores_pre_digest_data() {
10741        let xml = "<root><child>text</child></root>";
10742        let doc = Document::parse(xml).unwrap();
10743        let resolver = UriReferenceResolver::new(&doc);
10744
10745        // No transforms, no enveloped — just canonicalize entire document
10746        let initial_data = resolver.dereference("").unwrap();
10747        let pre_digest =
10748            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
10749        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
10750
10751        let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
10752        let result = process_reference(
10753            &reference,
10754            &resolver,
10755            doc.root_element(),
10756            ReferenceSet::SignedInfo,
10757            0,
10758            true,
10759        )
10760        .unwrap();
10761
10762        assert!(matches!(result.status, DsigStatus::Valid));
10763        assert!(result.pre_digest_data.is_some());
10764        assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
10765    }
10766
10767    // ── process_reference: URI dereference ───────────────────────────
10768
10769    #[test]
10770    fn reference_with_id_uri() {
10771        let xml = r##"<root>
10772            <item ID="target">specific content</item>
10773            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
10774                <ds:SignedInfo/>
10775            </ds:Signature>
10776        </root>"##;
10777        let doc = Document::parse(xml).unwrap();
10778        let resolver = UriReferenceResolver::new(&doc);
10779        let sig_node = doc
10780            .descendants()
10781            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
10782            .unwrap();
10783
10784        // Compute expected digest for the #target subtree
10785        let initial_data = resolver.dereference("#target").unwrap();
10786        let transforms = vec![Transform::C14n(
10787            crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
10788                .unwrap(),
10789        )];
10790        let pre_digest =
10791            crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
10792        let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
10793
10794        let reference = make_reference(
10795            "#target",
10796            transforms,
10797            DigestAlgorithm::Sha256,
10798            expected_digest,
10799        );
10800        let result = process_reference(
10801            &reference,
10802            &resolver,
10803            sig_node,
10804            ReferenceSet::SignedInfo,
10805            0,
10806            false,
10807        )
10808        .unwrap();
10809        assert!(matches!(result.status, DsigStatus::Valid));
10810    }
10811
10812    #[test]
10813    fn reference_with_nonexistent_id_fails() {
10814        let xml = "<root><child/></root>";
10815        let doc = Document::parse(xml).unwrap();
10816        let resolver = UriReferenceResolver::new(&doc);
10817
10818        let reference =
10819            make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
10820        let result = process_reference(
10821            &reference,
10822            &resolver,
10823            doc.root_element(),
10824            ReferenceSet::SignedInfo,
10825            0,
10826            false,
10827        );
10828        assert!(result.is_err());
10829    }
10830
10831    #[test]
10832    fn reference_with_absent_uri_fails_closed() {
10833        let xml = "<root><child>text</child></root>";
10834        let doc = Document::parse(xml).unwrap();
10835        let resolver = UriReferenceResolver::new(&doc);
10836
10837        let reference = Reference {
10838            uri: None, // absent URI
10839            id: None,
10840            ref_type: None,
10841            transforms: vec![],
10842            digest_method: DigestAlgorithm::Sha256,
10843            digest_value: vec![0; 32],
10844        };
10845
10846        let result = process_reference(
10847            &reference,
10848            &resolver,
10849            doc.root_element(),
10850            ReferenceSet::SignedInfo,
10851            0,
10852            false,
10853        );
10854        assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
10855    }
10856
10857    // ── process_all_references: fail-fast ────────────────────────────
10858
10859    #[test]
10860    fn all_references_pass() {
10861        let xml = "<root><child>text</child></root>";
10862        let doc = Document::parse(xml).unwrap();
10863        let resolver = UriReferenceResolver::new(&doc);
10864
10865        // Compute correct digest
10866        let initial_data = resolver.dereference("").unwrap();
10867        let pre_digest =
10868            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
10869        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
10870
10871        let refs = vec![
10872            make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
10873            make_reference("", vec![], DigestAlgorithm::Sha256, digest),
10874        ];
10875
10876        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
10877        assert!(result.all_valid());
10878        assert_eq!(result.results.len(), 2);
10879        assert!(result.first_failure.is_none());
10880    }
10881
10882    #[test]
10883    fn reference_processing_shares_xpath_work_across_references() {
10884        // A signature-wide meter must not reset when processing the next
10885        // Reference, even though each transform chain is independently valid.
10886        let document = Document::parse("<root/>").unwrap();
10887        let resolver = UriReferenceResolver::new(&document);
10888        let transform = Transform::XPath(super::super::transforms::XPathExpression::new("true()"));
10889        let initial_data = resolver.dereference("").unwrap();
10890        let pre_digest = crate::xmldsig::execute_transforms(
10891            document.root_element(),
10892            initial_data,
10893            std::slice::from_ref(&transform),
10894        )
10895        .unwrap();
10896        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
10897        let references = vec![
10898            make_reference(
10899                "",
10900                vec![transform.clone()],
10901                DigestAlgorithm::Sha256,
10902                digest.clone(),
10903            ),
10904            make_reference("", vec![transform], DigestAlgorithm::Sha256, digest),
10905        ];
10906        let budget = TransformExecutionBudget::with_xpath_limit(12);
10907        let canonicalized_data_budget = CanonicalizedDataBudget::default();
10908        let execution = ReferenceExecutionContext {
10909            store_pre_digest: false,
10910            transform_options: TransformOptions::default(),
10911            transform_budget: &budget,
10912            canonicalized_data_budget: &canonicalized_data_budget,
10913            provider: crate::provider::default_provider(),
10914            capture: &(),
10915        };
10916
10917        let error = process_all_references_with_options(
10918            &references,
10919            &resolver,
10920            document.root_element(),
10921            &execution,
10922        )
10923        .expect_err("the second Reference must consume the first Reference's XPath work");
10924
10925        assert!(matches!(
10926            error,
10927            ReferenceProcessingError::Transform(TransformError::Policy(
10928                crate::policy::PolicyViolation::ResourceLimit {
10929                    resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
10930                    ..
10931                }
10932            ))
10933        ));
10934    }
10935
10936    #[test]
10937    fn reference_processing_shares_node_set_materialization_across_references() {
10938        // Repeated references to the same small subtree must share one owned-
10939        // string budget. Otherwise a large inherited namespace can be cloned
10940        // once per Reference even when canonicalization emits little output.
10941        let document = Document::parse(
10942            r#"<root xmlns:n="urn:0123456789"><target Id="selected">payload</target></root>"#,
10943        )
10944        .unwrap();
10945        let resolver = UriReferenceResolver::new(&document);
10946        let initial_data = resolver.dereference("#selected").unwrap();
10947        let pre_digest =
10948            crate::xmldsig::execute_transforms(document.root_element(), initial_data, &[]).unwrap();
10949        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
10950        let references = vec![
10951            make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest.clone()),
10952            make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest),
10953        ];
10954        let budget = TransformExecutionBudget::with_node_set_materialization_limit(30);
10955        let canonicalized_data_budget = CanonicalizedDataBudget::default();
10956        let execution = ReferenceExecutionContext {
10957            store_pre_digest: false,
10958            transform_options: TransformOptions::default(),
10959            transform_budget: &budget,
10960            canonicalized_data_budget: &canonicalized_data_budget,
10961            provider: crate::provider::default_provider(),
10962            capture: &(),
10963        };
10964
10965        let error = process_all_references_with_options(
10966            &references,
10967            &resolver,
10968            document.root_element(),
10969            &execution,
10970        )
10971        .expect_err("the second Reference must consume the first Reference's materialization work");
10972
10973        assert!(matches!(
10974            error,
10975            ReferenceProcessingError::UriDereference(TransformError::Policy(
10976                crate::policy::PolicyViolation::ResourceLimit {
10977                    resource: crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
10978                    ..
10979                }
10980            ))
10981        ));
10982    }
10983
10984    #[test]
10985    fn fail_fast_on_first_mismatch() {
10986        let xml = "<root><child>text</child></root>";
10987        let doc = Document::parse(xml).unwrap();
10988        let resolver = UriReferenceResolver::new(&doc);
10989
10990        let wrong_digest = vec![0u8; 32];
10991        let refs = vec![
10992            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
10993            // Second reference should NOT be processed
10994            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
10995        ];
10996
10997        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
10998        assert!(!result.all_valid());
10999        assert_eq!(result.first_failure, Some(0));
11000        // Only first reference should be in results (fail-fast)
11001        assert_eq!(result.results.len(), 1);
11002        assert!(matches!(
11003            result.results[0].status,
11004            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
11005        ));
11006    }
11007
11008    #[test]
11009    fn fail_fast_second_reference() {
11010        let xml = "<root><child>text</child></root>";
11011        let doc = Document::parse(xml).unwrap();
11012        let resolver = UriReferenceResolver::new(&doc);
11013
11014        // Compute correct digest for first ref
11015        let initial_data = resolver.dereference("").unwrap();
11016        let pre_digest =
11017            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
11018        let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
11019        let wrong_digest = vec![0u8; 32];
11020
11021        let refs = vec![
11022            make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
11023            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
11024        ];
11025
11026        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
11027        assert!(!result.all_valid());
11028        assert_eq!(result.first_failure, Some(1));
11029        // Both references should be in results
11030        assert_eq!(result.results.len(), 2);
11031        assert!(matches!(result.results[0].status, DsigStatus::Valid));
11032        assert!(matches!(
11033            result.results[1].status,
11034            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
11035        ));
11036    }
11037
11038    #[test]
11039    fn empty_references_list() {
11040        let xml = "<root/>";
11041        let doc = Document::parse(xml).unwrap();
11042        let resolver = UriReferenceResolver::new(&doc);
11043
11044        let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
11045        assert!(result.all_valid());
11046        assert!(result.results.is_empty());
11047    }
11048
11049    // ── Digest algorithms ────────────────────────────────────────────
11050
11051    #[test]
11052    fn reference_sha1_digest() {
11053        let xml = "<root>content</root>";
11054        let doc = Document::parse(xml).unwrap();
11055        let resolver = UriReferenceResolver::new(&doc);
11056
11057        let initial_data = resolver.dereference("").unwrap();
11058        let pre_digest =
11059            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
11060        let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
11061
11062        let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
11063        let result = process_reference(
11064            &reference,
11065            &resolver,
11066            doc.root_element(),
11067            ReferenceSet::SignedInfo,
11068            0,
11069            false,
11070        )
11071        .unwrap();
11072        assert!(matches!(result.status, DsigStatus::Valid));
11073        assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
11074    }
11075
11076    #[test]
11077    fn reference_sha512_digest() {
11078        let xml = "<root>content</root>";
11079        let doc = Document::parse(xml).unwrap();
11080        let resolver = UriReferenceResolver::new(&doc);
11081
11082        let initial_data = resolver.dereference("").unwrap();
11083        let pre_digest =
11084            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
11085        let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
11086
11087        let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
11088        let result = process_reference(
11089            &reference,
11090            &resolver,
11091            doc.root_element(),
11092            ReferenceSet::SignedInfo,
11093            0,
11094            false,
11095        )
11096        .unwrap();
11097        assert!(matches!(result.status, DsigStatus::Valid));
11098        assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
11099    }
11100
11101    // ── SAML-like end-to-end ─────────────────────────────────────────
11102
11103    #[test]
11104    fn saml_enveloped_reference_processing() {
11105        // Realistic SAML Response with enveloped signature
11106        let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
11107                                     xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
11108                                     ID="_resp1">
11109            <saml:Assertion ID="_assert1">
11110                <saml:Subject>user@example.com</saml:Subject>
11111            </saml:Assertion>
11112            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
11113                <ds:SignedInfo>
11114                    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
11115                    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
11116                    <ds:Reference URI="">
11117                        <ds:Transforms>
11118                            <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
11119                            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
11120                        </ds:Transforms>
11121                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
11122                        <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
11123                    </ds:Reference>
11124                </ds:SignedInfo>
11125                <ds:SignatureValue>fakesig==</ds:SignatureValue>
11126            </ds:Signature>
11127        </samlp:Response>"##;
11128        let doc = Document::parse(xml).unwrap();
11129        let resolver = UriReferenceResolver::new(&doc);
11130        let sig_node = doc
11131            .descendants()
11132            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
11133            .unwrap();
11134
11135        // Parse SignedInfo to get the Reference
11136        let signed_info_node = sig_node
11137            .children()
11138            .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
11139            .unwrap();
11140        let signed_info = parse_signed_info(signed_info_node).unwrap();
11141        let reference = &signed_info.references[0];
11142
11143        // Compute the correct digest by running the actual pipeline
11144        let initial_data = resolver.dereference("").unwrap();
11145        let pre_digest =
11146            crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
11147                .unwrap();
11148        let correct_digest = compute_digest(reference.digest_method, &pre_digest);
11149
11150        // Build a reference with the correct digest
11151        let corrected_ref = make_reference(
11152            "",
11153            reference.transforms.clone(),
11154            reference.digest_method,
11155            correct_digest,
11156        );
11157
11158        // Verify: should pass
11159        let result = process_reference(
11160            &corrected_ref,
11161            &resolver,
11162            sig_node,
11163            ReferenceSet::SignedInfo,
11164            0,
11165            true,
11166        )
11167        .unwrap();
11168        assert!(
11169            matches!(result.status, DsigStatus::Valid),
11170            "SAML reference should verify"
11171        );
11172        assert!(result.pre_digest_data.is_some());
11173
11174        // Verify the pre-digest data contains the canonicalized document without Signature
11175        let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
11176        assert!(
11177            pre_digest_str.contains("samlp:Response"),
11178            "pre-digest should contain Response"
11179        );
11180        assert!(
11181            !pre_digest_str.contains("SignatureValue"),
11182            "pre-digest should NOT contain Signature"
11183        );
11184    }
11185
11186    #[test]
11187    fn pipeline_missing_signed_info_returns_missing_element() {
11188        let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
11189
11190        let err = verify_signature_with_pem_key(xml, "dummy-key", false)
11191            .expect_err("missing SignedInfo must fail before crypto stage");
11192        assert!(matches!(
11193            err,
11194            SignatureVerificationPipelineError::MissingElement {
11195                element: "SignedInfo"
11196            }
11197        ));
11198    }
11199
11200    #[test]
11201    fn pipeline_multiple_signature_elements_are_rejected() {
11202        let xml = r#"
11203<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
11204  <ds:Signature>
11205    <ds:SignedInfo/>
11206  </ds:Signature>
11207  <ds:Signature/>
11208</root>
11209"#;
11210
11211        let err = verify_signature_with_pem_key(xml, "dummy-key", false)
11212            .expect_err("multiple signatures must fail closed");
11213        assert!(matches!(
11214            err,
11215            SignatureVerificationPipelineError::InvalidStructure {
11216                reason: "Signature must appear exactly once in document",
11217            }
11218        ));
11219    }
11220
11221    #[test]
11222    fn pipeline_start_node_limits_signature_cardinality_to_its_subtree() {
11223        // A start-node selector changes the operation root, not global ID or
11224        // reference resolution; another Signature outside the subtree is irrelevant.
11225        let xml = r#"
11226<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
11227  <scope Id="selected"><ds:Signature/></scope>
11228  <scope Id="other"><ds:Signature/></scope>
11229</root>
11230"#;
11231        let err = VerifyContext::new()
11232            .start_node_id("selected")
11233            .verify(xml)
11234            .expect_err("the selected Signature remains structurally incomplete");
11235        assert!(matches!(
11236            err,
11237            SignatureVerificationPipelineError::MissingElement {
11238                element: "SignedInfo"
11239            }
11240        ));
11241    }
11242
11243    #[test]
11244    fn pipeline_reports_keyinfo_parse_error() {
11245        let xml = r#"
11246<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
11247              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
11248  <ds:SignedInfo>
11249    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
11250    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
11251    <ds:Reference URI="">
11252      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
11253      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
11254    </ds:Reference>
11255  </ds:SignedInfo>
11256  <ds:SignatureValue>AA==</ds:SignatureValue>
11257  <ds:KeyInfo>
11258    <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
11259  </ds:KeyInfo>
11260</ds:Signature>
11261"#;
11262
11263        let err = VerifyContext::new().verify(xml).expect_err(
11264            "invalid KeyInfo must map to ParseKeyInfo when no explicit key is supplied",
11265        );
11266        assert!(matches!(
11267            err,
11268            SignatureVerificationPipelineError::ParseKeyInfo(_)
11269        ));
11270    }
11271
11272    #[test]
11273    fn pipeline_ignores_malformed_keyinfo_when_explicit_key_is_supplied() {
11274        let base_xml = signature_with_target_reference("AQ==");
11275        let xml = base_xml
11276            .replace(
11277                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
11278                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
11279            )
11280            .replace(
11281                "</ds:SignatureValue>\n  </ds:Signature>",
11282                "</ds:SignatureValue>\n    <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n  </ds:Signature>",
11283            );
11284
11285        let result = VerifyContext::new()
11286            .key(&RejectingKey)
11287            .verify(&xml)
11288            .expect("explicit key path should not fail on malformed KeyInfo");
11289        assert!(matches!(
11290            result.status,
11291            DsigStatus::Invalid(FailureReason::SignatureMismatch)
11292        ));
11293    }
11294
11295    #[test]
11296    fn pipeline_rejects_foreign_element_children_under_signature() {
11297        let base_xml = signature_with_target_reference("AQ==");
11298        let xml = base_xml
11299            .replace(
11300                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
11301                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:foo="urn:example:foo">"#,
11302            )
11303            .replace(
11304                "</ds:SignedInfo>\n    <ds:SignatureValue>",
11305                "</ds:SignedInfo>\n    <foo:Bar/>\n    <ds:SignatureValue>",
11306            );
11307
11308        let err = VerifyContext::new()
11309            .key(&RejectingKey)
11310            .verify(&xml)
11311            .expect_err("foreign element children under Signature must fail closed");
11312        assert!(matches!(
11313            err,
11314            SignatureVerificationPipelineError::InvalidStructure {
11315                reason: "Signature must contain only XMLDSIG element children",
11316            }
11317        ));
11318    }
11319
11320    #[test]
11321    fn pipeline_rejects_non_whitespace_mixed_content_under_signature() {
11322        let base_xml = signature_with_target_reference("AQ==");
11323        let xml = base_xml.replace(
11324            "</ds:SignedInfo>\n    <ds:SignatureValue>",
11325            "</ds:SignedInfo>\n    oops\n    <ds:SignatureValue>",
11326        );
11327
11328        let err = VerifyContext::new()
11329            .key(&RejectingKey)
11330            .verify(&xml)
11331            .expect_err("non-whitespace mixed content under Signature must fail closed");
11332        assert!(matches!(
11333            err,
11334            SignatureVerificationPipelineError::InvalidStructure {
11335                reason: "Signature must not contain non-whitespace mixed content",
11336            }
11337        ));
11338    }
11339
11340    #[test]
11341    fn pipeline_rejects_keyinfo_out_of_order() {
11342        let base_xml = signature_with_target_reference("AQ==");
11343        let xml = base_xml.replace(
11344            "</ds:SignatureValue>\n  </ds:Signature>",
11345            "</ds:SignatureValue>\n    <ds:Object/>\n    <ds:KeyInfo><ds:KeyName>late</ds:KeyName></ds:KeyInfo>\n  </ds:Signature>",
11346        );
11347
11348        let err = VerifyContext::new()
11349            .key(&RejectingKey)
11350            .verify(&xml)
11351            .expect_err("KeyInfo after Object must be rejected by Signature child order checks");
11352        assert!(matches!(
11353            err,
11354            SignatureVerificationPipelineError::InvalidStructure {
11355                reason: "KeyInfo must be the third element child of Signature when present"
11356            }
11357        ));
11358    }
11359
11360    #[test]
11361    fn pipeline_accepts_comments_and_processing_instructions_under_signature() {
11362        let xml = r#"
11363<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
11364  <?dbg keep ?>
11365  <!-- signature metadata -->
11366  <ds:SignedInfo>
11367    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
11368    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
11369    <ds:Reference URI="">
11370      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
11371      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
11372    </ds:Reference>
11373  </ds:SignedInfo>
11374  <!-- between required children -->
11375  <ds:SignatureValue>AA==</ds:SignatureValue>
11376</ds:Signature>
11377"#;
11378
11379        let doc = Document::parse(xml).expect("test XML must parse");
11380        let signature_node = doc.root_element();
11381        let parsed = parse_signature_children(signature_node)
11382            .expect("comment/PI nodes under Signature must be ignored");
11383
11384        assert_eq!(parsed.signed_info_node.tag_name().name(), "SignedInfo");
11385        assert_eq!(
11386            parsed.signature_value_node.tag_name().name(),
11387            "SignatureValue"
11388        );
11389        assert!(parsed.key_info_node.is_none());
11390    }
11391}