Skip to main content

xml_sec/xmldsig/
verify.rs

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