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                parse_key_info_with_policy_budgets(
2118                    node,
2119                    ctx.provider,
2120                    operation.budgets().transforms.xml_base_resolution(),
2121                    &ctx.policy.resources,
2122                    ctx.policy
2123                        .key_trust
2124                        .certificate_signature_algorithms
2125                        .as_ref(),
2126                )
2127            })
2128            .transpose()
2129            .map_err(map_key_info_parse_error)?
2130    } else {
2131        None
2132    };
2133
2134    let signed_info = parse_signed_info_with_xpath_budget(
2135        signed_info_node,
2136        &mut operation.budgets().xpath_parse.borrow_mut(),
2137        &ctx.policy.resources,
2138    )?;
2139    if signed_info.references.len() > ctx.policy.resources.max_references {
2140        return Err(crate::policy::PolicyViolation::ResourceLimit {
2141            resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
2142            maximum: ctx.policy.resources.max_references,
2143            actual: signed_info.references.len(),
2144        }
2145        .into());
2146    }
2147    for reference in &signed_info.references {
2148        if reference.transforms.len() > ctx.policy.resources.max_transforms_per_reference {
2149            return Err(crate::policy::PolicyViolation::ResourceLimit {
2150                resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
2151                maximum: ctx.policy.resources.max_transforms_per_reference,
2152                actual: reference.transforms.len(),
2153            }
2154            .into());
2155        }
2156    }
2157    ctx.policy
2158        .check_signature_algorithm(signed_info.signature_method)?;
2159    // Refuse an inadmissible authenticator before reference resolution/transforms.
2160    // XMLDSig 1.1 section 4.4.2 explicitly requires max(80, hash_bits/2),
2161    // independently of data; section 6.3.1 separately requires octet alignment.
2162    // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-SignatureMethod
2163    if let Some(full_output_bits) = signed_info.signature_method.hmac_output_bits() {
2164        ctx.policy.hmac.validate_output(
2165            signed_info.signature_method,
2166            signed_info
2167                .hmac_output_length_bits
2168                .unwrap_or(full_output_bits),
2169        )?;
2170    }
2171    for reference in &signed_info.references {
2172        if ctx
2173            .policy
2174            .digest_algorithms
2175            .as_ref()
2176            .is_some_and(|allowed| !allowed.contains(&reference.digest_method))
2177        {
2178            return Err(crate::policy::PolicyViolation::Algorithm {
2179                operation: "verification",
2180                algorithm: reference.digest_method.uri().to_string(),
2181            }
2182            .into());
2183        }
2184    }
2185    enforce_reference_policies(
2186        &signed_info.references,
2187        ctx.policy.uris.references,
2188        ctx.allowed_transform_uris(),
2189    )?;
2190    enforce_transform_allowed(ctx.allowed_transform_uris(), signed_info.c14n_method.uri())?;
2191
2192    if let Some(resources) = ctx.external_resources {
2193        validate_external_resource_map(
2194            resources,
2195            ctx.policy.resources.max_external_resource_bytes,
2196            ctx.policy.resources.max_external_resource_total_bytes,
2197        )
2198        .map_err(|error| match error {
2199            ExternalResourceMapError::Policy(error) => error.into(),
2200            ExternalResourceMapError::TotalLengthOverflow => {
2201                SignatureVerificationPipelineError::InvalidStructure {
2202                    reason: "external resource total length overflow",
2203                }
2204            }
2205        })?;
2206    }
2207    let remaining_reference_capacity = ctx
2208        .policy
2209        .resources
2210        .max_references
2211        .checked_sub(signed_info.references.len())
2212        .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2213            reason: "SignedInfo exceeds the per-signature Reference limit",
2214        })?;
2215    let plan_nodes = compile_verification_operation_plan(
2216        operation,
2217        view,
2218        signature_node,
2219        &signed_info.references,
2220        resolver,
2221    )?;
2222    let signature_identity =
2223        OperationResourceIdentity::DocumentNode(view.node_identity(signature_node));
2224    operation.run_with_resource(plan_nodes.document, &signature_identity, || {
2225        operation
2226            .validate_document_view(view)
2227            .map_err(map_verification_plan_error)
2228    })?;
2229    let observed_key_resources = operation.budgets().external_resource_set_identity(resolver);
2230    let retrieval_materialization = operation.run_with_resource(
2231        plan_nodes.key_materialization,
2232        &observed_key_resources,
2233        || {
2234            if let Some(info) = key_info.as_mut() {
2235                let budgets = operation.budgets();
2236                let mut xpath_parse = budgets.xpath_parse.borrow_mut();
2237                let mut retrieval_budgets = RetrievalMaterializationBudgets {
2238                    xpath_parse: &mut xpath_parse,
2239                    execution: &budgets.transforms,
2240                    resources: &ctx.policy.resources,
2241                    certificate_signature_algorithms: ctx
2242                        .policy
2243                        .key_trust
2244                        .certificate_signature_algorithms
2245                        .as_ref(),
2246                    xml_backend: ctx.xml_backend,
2247                };
2248                let mut materialization = budgets.key_info_materialization.borrow_mut();
2249                let mut outcome = materialize_key_info_references_with_budgets(
2250                    info,
2251                    resolver,
2252                    &ctx.policy,
2253                    ctx.provider,
2254                    &mut retrieval_budgets,
2255                    &mut materialization,
2256                )?;
2257                outcome.merge(materialize_retrieval_methods_with_budgets(
2258                    info,
2259                    resolver,
2260                    ctx.policy.uris.retrieval_methods,
2261                    ctx.allowed_transform_uris(),
2262                    ctx.provider,
2263                    &mut retrieval_budgets,
2264                    &mut materialization.candidate_work,
2265                )?);
2266                Ok::<_, SignatureVerificationPipelineError>(outcome)
2267            } else {
2268                Ok(RetrievalMaterialization::default())
2269            }
2270        },
2271    )?;
2272    let mut reference_results = Vec::with_capacity(signed_info.references.len());
2273    let mut first_failure = None;
2274    for (index, (node, reference)) in plan_nodes
2275        .digests
2276        .iter()
2277        .copied()
2278        .zip(&signed_info.references)
2279        .enumerate()
2280    {
2281        let observed = operation.budgets().resource_identity_for_reference(
2282            reference,
2283            index,
2284            resolver,
2285            view,
2286            reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2287        );
2288        let mut result = operation.run_with_resource(node, &observed, || {
2289            let budgets = operation.budgets();
2290            let execution = ReferenceExecutionContext {
2291                store_pre_digest: ctx.store_pre_digest,
2292                transform_options: ctx.transform_options(),
2293                transform_budget: &budgets.transforms,
2294                canonicalized_data_budget: &budgets.canonicalized,
2295                provider: ctx.provider,
2296            };
2297            process_reference_with_options(
2298                reference,
2299                resolver,
2300                signature_node,
2301                ReferenceSet::SignedInfo,
2302                index,
2303                reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2304                &execution,
2305            )
2306            .map_err(SignatureVerificationPipelineError::from)
2307        })?;
2308        bind_reference_evidence(
2309            &mut result,
2310            reference,
2311            view,
2312            reference_origin_node(signature_node, ReferenceSet::SignedInfo, index),
2313            &observed,
2314        );
2315        let accepted = result.status == DsigStatus::Valid;
2316        operation.set_outcome(
2317            node,
2318            accepted,
2319            if accepted {
2320                OperationDecisionReason::ReferenceDigestVerified
2321            } else {
2322                OperationDecisionReason::ReferenceDigestRejected
2323            },
2324        );
2325        if result.status == DsigStatus::Valid
2326            && let Some(reference) = signed_info.references.get(result.reference_index)
2327            && reference
2328                .transforms
2329                .iter()
2330                .all(transform_preserves_manifest_structure)
2331            && let Some(uri) = reference.uri.as_deref()
2332            && let Ok(Some(target)) = resolver.node_id_for_same_document_reference(uri)
2333        {
2334            let identity = view.node_identity_by_id(target);
2335            operation.authenticate(identity);
2336            debug_assert!(operation.is_authenticated(identity));
2337        }
2338        reference_results.push(result);
2339        if !accepted {
2340            first_failure = Some(index);
2341            break;
2342        }
2343    }
2344    debug_assert!(
2345        plan_nodes.digests[reference_results.len()..]
2346            .iter()
2347            .all(|node| !operation.is_executed(*node))
2348    );
2349    let references = ReferencesResult {
2350        results: reference_results,
2351        first_failure,
2352    };
2353
2354    if let Some(first_failure) = references.first_failure {
2355        debug_assert!(operation.first_failure().is_some());
2356        let status = references.results[first_failure].status;
2357        return Ok(VerifyResult {
2358            key_trust: super::KeyTrustEvidence::NotEvaluated,
2359            status,
2360            signed_info_references: references.results,
2361            manifest_references: Vec::new(),
2362            canonicalized_signed_info: None,
2363        });
2364    }
2365
2366    let canonical_signed_info = operation.run(plan_nodes.canonicalization, || {
2367        let signed_info_subtree: HashSet<_> = signed_info_node
2368            .descendants()
2369            .map(|node: Node<'_, '_>| node.id())
2370            .collect();
2371        let mut canonical_signed_info = Vec::new();
2372        let signed_info_limit = operation
2373            .budgets()
2374            .canonicalized
2375            .remaining()
2376            .min(operation.budgets().transforms.remaining_c14n_output());
2377        canonicalize_bounded_with_xml_base_budget(
2378            doc,
2379            Some(&|node| signed_info_subtree.contains(&node.id())),
2380            &signed_info.c14n_method,
2381            signed_info_limit,
2382            operation.budgets().transforms.xml_base_resolution(),
2383            &mut canonical_signed_info,
2384        )
2385        .map_err(|error| {
2386            if let Some(violation) = map_c14n_resource_policy_violation(
2387                &error,
2388                crate::policy::resource_name::CANONICALIZED_BYTES,
2389                operation.budgets().canonicalized.max_bytes,
2390            ) {
2391                SignatureVerificationPipelineError::Policy(violation)
2392            } else {
2393                SignatureVerificationPipelineError::Canonicalization(error)
2394            }
2395        })?;
2396        operation
2397            .budgets()
2398            .transforms
2399            .charge_c14n_output(canonical_signed_info.len())
2400            .map_err(ReferenceProcessingError::Transform)?;
2401        operation
2402            .budgets()
2403            .canonicalized
2404            .charge(canonical_signed_info.len())?;
2405        Ok::<_, SignatureVerificationPipelineError>(canonical_signed_info)
2406    })?;
2407
2408    let signature_value = decode_signature_value(
2409        signature_children.signature_value_node,
2410        signed_info.signature_method,
2411        &ctx.policy.resources,
2412    )?;
2413    if let Some(full_output_bits) = signed_info.signature_method.hmac_output_bits() {
2414        let expected_bits = signed_info
2415            .hmac_output_length_bits
2416            .unwrap_or(full_output_bits);
2417        if signature_value.len() != expected_bits / 8 {
2418            return Err(SignatureVerificationPipelineError::InvalidStructure {
2419                reason: "SignatureValue length does not match HMACOutputLength",
2420            });
2421        }
2422    }
2423    let resolved_key = operation.run(plan_nodes.key, || {
2424        resolve_verifying_key(ctx, key, key_info.as_ref(), signed_info.signature_method)
2425    })?;
2426    let Some(resolved_key) = resolved_key else {
2427        operation.set_outcome(
2428            plan_nodes.key,
2429            false,
2430            OperationDecisionReason::KeyUnavailable,
2431        );
2432        if let Some(error) = retrieval_materialization.deferred_error {
2433            return Err(error);
2434        }
2435        return Ok(VerifyResult {
2436            key_trust: super::KeyTrustEvidence::NotEvaluated,
2437            status: DsigStatus::Invalid(FailureReason::KeyNotFound),
2438            signed_info_references: references.results,
2439            manifest_references: Vec::new(),
2440            canonicalized_signed_info: if ctx.store_pre_digest {
2441                Some(canonical_signed_info)
2442            } else {
2443                None
2444            },
2445        });
2446    };
2447    operation.set_outcome(plan_nodes.key, true, OperationDecisionReason::KeyResolved);
2448    let verifier = resolved_key.as_ref();
2449    let signature_valid = operation.run(plan_nodes.crypto, || {
2450        verifier.validate_policy(&ctx.policy)?;
2451        if !verifier.validate_signature_value_with_policy(
2452            &ctx.policy,
2453            signed_info.signature_method,
2454            &signature_value,
2455        )? {
2456            return Ok::<_, SignatureVerificationPipelineError>(false);
2457        }
2458        ctx.provider
2459            .require_capability(crate::provider::ProviderCapability::Verify(
2460                signed_info.signature_method,
2461            ))?;
2462        let policy_verifier = PolicyVerifyingKey {
2463            key: verifier,
2464            policy: &ctx.policy,
2465        };
2466        ctx.provider.verify_with_context(
2467            &policy_verifier,
2468            signed_info.signature_method,
2469            &signed_info.signature_context,
2470            &canonical_signed_info,
2471            &signature_value,
2472        )
2473    })?;
2474
2475    if !signature_valid {
2476        operation.set_outcome(
2477            plan_nodes.crypto,
2478            false,
2479            OperationDecisionReason::SignatureRejected,
2480        );
2481        return Ok(VerifyResult {
2482            key_trust: resolved_key.evidence(&ctx.policy)?,
2483            status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
2484            signed_info_references: references.results,
2485            manifest_references: Vec::new(),
2486            canonicalized_signed_info: if ctx.store_pre_digest {
2487                Some(canonical_signed_info)
2488            } else {
2489                None
2490            },
2491        });
2492    }
2493    operation.set_outcome(
2494        plan_nodes.crypto,
2495        true,
2496        OperationDecisionReason::SignatureVerified,
2497    );
2498    // Manifest syntax and references are authenticated content. Delay parsing
2499    // until SignatureValue succeeds, then extend the same operation graph with
2500    // the newly trusted dependency subgraph and its evidence node.
2501    let (manifest_references, manifest_digests) =
2502        if ctx.policy.manifest_processing == crate::policy::ManifestProcessing::Process {
2503            process_authenticated_manifest_references(
2504                operation,
2505                view,
2506                signature_node,
2507                resolver,
2508                ctx,
2509                remaining_reference_capacity,
2510                plan_nodes.crypto,
2511            )?
2512        } else {
2513            (Vec::new(), Vec::new())
2514        };
2515    operation.extend();
2516    let evidence = operation.add_node(OperationNodeKind::Evidence, OperationStage::Evidence, None);
2517    operation
2518        .add_dependency(evidence, plan_nodes.crypto)
2519        .map_err(map_verification_plan_error)?;
2520    for digest in manifest_digests {
2521        operation
2522            .add_dependency(evidence, digest)
2523            .map_err(map_verification_plan_error)?;
2524    }
2525    operation.compile().map_err(map_verification_plan_error)?;
2526    operation.run(evidence, || {
2527        Ok::<_, SignatureVerificationPipelineError>(VerifyResult {
2528            key_trust: resolved_key.evidence(&ctx.policy)?,
2529            status: DsigStatus::Valid,
2530            signed_info_references: references.results,
2531            manifest_references,
2532            canonicalized_signed_info: if ctx.store_pre_digest {
2533                Some(canonical_signed_info)
2534            } else {
2535                None
2536            },
2537        })
2538    })
2539}
2540
2541struct PolicyVerifyingKey<'a> {
2542    key: &'a dyn VerifyingKey,
2543    policy: &'a crate::policy::VerificationPolicy,
2544}
2545
2546impl VerifyingKey for PolicyVerifyingKey<'_> {
2547    fn verify_candidate_keys(
2548        &self,
2549        verify: &mut dyn FnMut(&dyn VerifyingKey) -> Result<bool, DsigError>,
2550    ) -> Result<Option<bool>, DsigError> {
2551        self.key.verify_candidate_keys(&mut |key| {
2552            key.validate_policy(self.policy)?;
2553            verify(&PolicyVerifyingKey {
2554                key,
2555                policy: self.policy,
2556            })
2557        })
2558    }
2559    fn verification_spki(&self, algorithm: SignatureAlgorithm) -> Result<Option<&[u8]>, DsigError> {
2560        self.key.verification_spki(algorithm)
2561    }
2562
2563    fn ecdsa_encoding(&self) -> crate::policy::EcdsaSignatureValueEncoding {
2564        self.policy.ecdsa_signature_value_encoding
2565    }
2566    fn verify_with_context(
2567        &self,
2568        algorithm: SignatureAlgorithm,
2569        context: &super::SignatureContext,
2570        data: &[u8],
2571        signature: &[u8],
2572    ) -> Result<bool, DsigError> {
2573        if context.as_bytes().is_empty() {
2574            self.verify(algorithm, data, signature)
2575        } else {
2576            self.key
2577                .verify_with_context(algorithm, context, data, signature)
2578        }
2579    }
2580    fn validate_policy(&self, policy: &crate::policy::VerificationPolicy) -> Result<(), DsigError> {
2581        self.key.validate_policy(policy)
2582    }
2583
2584    fn validate_signature_value(
2585        &self,
2586        algorithm: SignatureAlgorithm,
2587        signature_value: &[u8],
2588    ) -> Result<bool, DsigError> {
2589        self.key
2590            .validate_signature_value_with_policy(self.policy, algorithm, signature_value)
2591    }
2592
2593    fn verify(
2594        &self,
2595        algorithm: SignatureAlgorithm,
2596        signed_data: &[u8],
2597        signature_value: &[u8],
2598    ) -> Result<bool, DsigError> {
2599        self.key
2600            .verify_with_policy(self.policy, algorithm, signed_data, signature_value)
2601    }
2602}
2603
2604#[derive(Debug, Default)]
2605struct RetrievalMaterialization {
2606    deferred_error: Option<SignatureVerificationPipelineError>,
2607}
2608
2609impl RetrievalMaterialization {
2610    fn merge(&mut self, other: Self) {
2611        if self.deferred_error.is_none() {
2612            self.deferred_error = other.deferred_error;
2613        }
2614    }
2615}
2616
2617struct RetrievalMaterializationBudgets<'a> {
2618    certificate_signature_algorithms: Option<&'a crate::policy::CertificateSignatureAlgorithms>,
2619    xpath_parse: &'a mut XPathSignatureParseBudget,
2620    execution: &'a TransformExecutionBudget,
2621    resources: &'a crate::policy::ResourcePolicy,
2622    xml_backend: crate::XmlBackend,
2623}
2624
2625#[derive(Default)]
2626struct KeyInfoMaterializationState {
2627    active: HashSet<(super::uri::TraversalDocumentIdentity, String)>,
2628    candidate_work: usize,
2629}
2630
2631trait KeyInfoReferencePolicy {
2632    fn certificate_signature_algorithms(
2633        &self,
2634    ) -> Option<&crate::policy::CertificateSignatureAlgorithms>;
2635    fn resources(&self) -> &crate::policy::ResourcePolicy;
2636    fn xml(&self) -> &crate::policy::XmlInputPolicy;
2637    fn key_info_reference_uris(&self) -> UriTypeSet;
2638    fn retrieval_method_uris(&self) -> UriTypeSet;
2639    fn key_info_reference_source_enabled(&self) -> bool;
2640    fn allowed_transforms(&self) -> Option<&HashSet<String>>;
2641}
2642
2643impl KeyInfoReferencePolicy for crate::policy::SigningPolicy {
2644    fn certificate_signature_algorithms(
2645        &self,
2646    ) -> Option<&crate::policy::CertificateSignatureAlgorithms> {
2647        None
2648    }
2649    fn resources(&self) -> &crate::policy::ResourcePolicy {
2650        &self.resources
2651    }
2652
2653    fn xml(&self) -> &crate::policy::XmlInputPolicy {
2654        &self.xml
2655    }
2656
2657    fn key_info_reference_uris(&self) -> UriTypeSet {
2658        self.uris.key_info_references
2659    }
2660
2661    fn retrieval_method_uris(&self) -> UriTypeSet {
2662        self.uris.retrieval_methods
2663    }
2664
2665    fn key_info_reference_source_enabled(&self) -> bool {
2666        true
2667    }
2668
2669    fn allowed_transforms(&self) -> Option<&HashSet<String>> {
2670        self.transforms.allowed_algorithms.as_ref()
2671    }
2672}
2673
2674impl KeyInfoReferencePolicy for crate::policy::VerificationPolicy {
2675    fn certificate_signature_algorithms(
2676        &self,
2677    ) -> Option<&crate::policy::CertificateSignatureAlgorithms> {
2678        self.key_trust.certificate_signature_algorithms.as_ref()
2679    }
2680    fn resources(&self) -> &crate::policy::ResourcePolicy {
2681        &self.resources
2682    }
2683
2684    fn xml(&self) -> &crate::policy::XmlInputPolicy {
2685        &self.xml
2686    }
2687
2688    fn key_info_reference_uris(&self) -> UriTypeSet {
2689        self.uris.key_info_references
2690    }
2691
2692    fn retrieval_method_uris(&self) -> UriTypeSet {
2693        self.uris.retrieval_methods
2694    }
2695
2696    fn key_info_reference_source_enabled(&self) -> bool {
2697        self.key_sources.key_info_reference
2698    }
2699
2700    fn allowed_transforms(&self) -> Option<&HashSet<String>> {
2701        self.transforms.allowed_algorithms.as_ref()
2702    }
2703}
2704
2705struct KeyInfoReferenceMaterializationContext<'a, 'budget, P> {
2706    policy: &'a P,
2707    provider: &'a dyn crate::provider::CryptoProvider,
2708    budgets: &'a mut RetrievalMaterializationBudgets<'budget>,
2709}
2710
2711fn materialize_key_info_references_with_budgets<P: KeyInfoReferencePolicy>(
2712    key_info: &mut KeyInfo,
2713    resolver: &UriReferenceResolver<'_>,
2714    policy: &P,
2715    provider: &dyn crate::provider::CryptoProvider,
2716    budgets: &mut RetrievalMaterializationBudgets<'_>,
2717    materialization: &mut KeyInfoMaterializationState,
2718) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
2719    fn visit<P: KeyInfoReferencePolicy>(
2720        key_info: &mut KeyInfo,
2721        resolver: &UriReferenceResolver<'_>,
2722        context: &mut KeyInfoReferenceMaterializationContext<'_, '_, P>,
2723        materialization: &mut KeyInfoMaterializationState,
2724        depth: usize,
2725    ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
2726        let mut materialized = Vec::new();
2727        let mut outcome = RetrievalMaterialization::default();
2728        for source in std::mem::take(&mut key_info.sources) {
2729            let source_work = match &source {
2730                super::parse::KeyInfoSource::X509Data(info) => info.certificates.len().max(1),
2731                _ => 1,
2732            };
2733            materialization.candidate_work =
2734                materialization.candidate_work.saturating_add(source_work);
2735            context
2736                .policy
2737                .resources()
2738                .validate_key_candidates(materialization.candidate_work)?;
2739
2740            let super::parse::KeyInfoSource::KeyInfoReference { uri } = source else {
2741                materialized.push(source);
2742                continue;
2743            };
2744            if !context.policy.key_info_reference_source_enabled() {
2745                return Err(crate::policy::PolicyViolation::KeyTrust {
2746                    reason: "KeyInfoReference key sources are disabled",
2747                }
2748                .into());
2749            }
2750            if !context.policy.key_info_reference_uris().allows(&uri) {
2751                return Err(crate::policy::PolicyViolation::Uri {
2752                    operation: "KeyInfoReference",
2753                    reason: "URI class is disabled",
2754                }
2755                .into());
2756            }
2757            let next_depth = depth.saturating_add(1);
2758            context
2759                .policy
2760                .resources()
2761                .validate_key_info_reference_depth(next_depth)?;
2762            let cycle_key = (resolver.traversal_document_identity(), uri.clone());
2763            if !materialization.active.insert(cycle_key.clone()) {
2764                return Err(SignatureVerificationPipelineError::InvalidStructure {
2765                    reason: "KeyInfoReference cycle detected",
2766                });
2767            }
2768
2769            let is_same_document = uri.is_empty() || uri.starts_with('#');
2770            let (mut referenced, mut nested_outcome) = if is_same_document {
2771                let node = resolver
2772                    .node_for_same_document_reference(&uri)
2773                    .map_err(|error| {
2774                        SignatureVerificationPipelineError::from(
2775                            ReferenceProcessingError::UriDereference(error),
2776                        )
2777                    })?
2778                    .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2779                        reason: "KeyInfoReference target is missing or ambiguous",
2780                    })?;
2781                if !node.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
2782                    return Err(SignatureVerificationPipelineError::InvalidStructure {
2783                        reason: "KeyInfoReference target must be KeyInfo",
2784                    });
2785                }
2786                (
2787                    parse_key_info_with_policy_budgets(
2788                        node,
2789                        context.provider,
2790                        context.budgets.execution.xml_base_resolution(),
2791                        context.policy.resources(),
2792                        context.policy.certificate_signature_algorithms(),
2793                    )
2794                    .map_err(map_key_info_parse_error)?,
2795                    RetrievalMaterialization::default(),
2796                )
2797            } else {
2798                // The fragment selects a node inside the retrieved document; it
2799                // is not part of either the caller-owned resource identity or
2800                // the base URI used by references nested in that document.
2801                let (resource_uri, fragment) = uri
2802                    .split_once('#')
2803                    .map_or((uri.as_str(), None), |(resource, fragment)| {
2804                        (resource, (!fragment.is_empty()).then_some(fragment))
2805                    });
2806                let bytes = resolver
2807                    .external_resource(resource_uri)
2808                    .map_err(|error| {
2809                        SignatureVerificationPipelineError::from(
2810                            ReferenceProcessingError::UriDereference(error),
2811                        )
2812                    })?
2813                    .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2814                        reason: "KeyInfoReference external resource is unavailable",
2815                    })?;
2816                // External KeyInfo bytes share the operation's work allowance even if decoding
2817                // fails before a document exists. Parsing the decoded text is a separate pass.
2818                context
2819                    .budgets
2820                    .execution
2821                    .xml_parse_work()
2822                    .charge_policy(bytes.len())?;
2823                let xml = crate::encoding::decode_xml_octets(
2824                    bytes,
2825                    context.policy.resources().max_xml_document_bytes,
2826                )
2827                .map_err(map_key_info_xml_decode_error)?;
2828                let settings = DocumentParseSettings::from_policy(
2829                    context.policy.xml(),
2830                    context.policy.resources(),
2831                )
2832                .with_backend(context.budgets.xml_backend);
2833                let document = XmlDocument::parse_with_settings_and_budget(
2834                    xml.into_owned(),
2835                    settings,
2836                    context.budgets.execution.xml_parse_work(),
2837                )
2838                .map_err(|error| map_document_parse_error(error, settings))?;
2839                document.with_view(|view| {
2840                    let external_resolver = resolver.for_external_document_view(view, resource_uri);
2841                    let target = match fragment {
2842                        Some(fragment) => external_resolver
2843                            .node_for_same_document_reference(&format!("#{fragment}"))
2844                            .map_err(|error| {
2845                                SignatureVerificationPipelineError::from(
2846                                    ReferenceProcessingError::UriDereference(error),
2847                                )
2848                            })?
2849                            .ok_or(SignatureVerificationPipelineError::InvalidStructure {
2850                                reason: "KeyInfoReference external target is missing or ambiguous",
2851                            })?,
2852                        _ => view.document().root_element(),
2853                    };
2854                    if !target.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
2855                        return Err(SignatureVerificationPipelineError::InvalidStructure {
2856                            reason: "KeyInfoReference external target must be KeyInfo",
2857                        });
2858                    }
2859                    let mut referenced = parse_key_info_with_policy_budgets_and_document_base(
2860                        target,
2861                        context.provider,
2862                        context.budgets.execution.xml_base_resolution(),
2863                        context.policy.resources(),
2864                        Some(resource_uri),
2865                        context.policy.certificate_signature_algorithms(),
2866                    )
2867                    .map_err(map_key_info_parse_error)?;
2868                    let mut nested_outcome = visit(
2869                        &mut referenced,
2870                        &external_resolver,
2871                        context,
2872                        materialization,
2873                        next_depth,
2874                    )?;
2875                    nested_outcome.merge(materialize_retrieval_methods_with_budgets(
2876                        &mut referenced,
2877                        &external_resolver,
2878                        context.policy.retrieval_method_uris(),
2879                        context.policy.allowed_transforms(),
2880                        context.provider,
2881                        context.budgets,
2882                        &mut materialization.candidate_work,
2883                    )?);
2884                    // RetrievalMethod nodes are meaningful only in the document
2885                    // whose resolver owns their same-document URI context.
2886                    referenced.sources.retain(|source| {
2887                        !matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. })
2888                    });
2889                    Ok((referenced, nested_outcome))
2890                })?
2891            };
2892            if is_same_document {
2893                nested_outcome.merge(visit(
2894                    &mut referenced,
2895                    resolver,
2896                    context,
2897                    materialization,
2898                    next_depth,
2899                )?);
2900            }
2901            outcome.merge(nested_outcome);
2902            materialization.active.remove(&cycle_key);
2903            materialized.extend(referenced.sources);
2904        }
2905        key_info.sources = materialized;
2906        context
2907            .policy
2908            .resources()
2909            .validate_key_candidates(key_info.embedded_candidate_count())?;
2910        Ok(outcome)
2911    }
2912
2913    let mut context = KeyInfoReferenceMaterializationContext {
2914        policy,
2915        provider,
2916        budgets,
2917    };
2918    visit(key_info, resolver, &mut context, materialization, 0)
2919}
2920
2921fn map_key_info_xml_decode_error(
2922    error: crate::encoding::XmlEncodingError,
2923) -> SignatureVerificationPipelineError {
2924    match error {
2925        crate::encoding::XmlEncodingError::DecodedLimit { maximum, actual } => {
2926            SignatureVerificationPipelineError::Policy(
2927                crate::policy::PolicyViolation::ResourceLimit {
2928                    resource: crate::policy::resource_name::XML_DOCUMENT,
2929                    maximum,
2930                    actual,
2931                },
2932            )
2933        }
2934        _ => SignatureVerificationPipelineError::InvalidStructure {
2935            reason: "KeyInfoReference external resource has an invalid XML encoding",
2936        },
2937    }
2938}
2939
2940fn materialize_key_info_references_for_policy<P: KeyInfoReferencePolicy>(
2941    key_info: &mut KeyInfo,
2942    resolver: UriReferenceResolver<'_>,
2943    policy: &P,
2944    provider: &dyn crate::provider::CryptoProvider,
2945    xml_backend: crate::XmlBackend,
2946) -> Result<(), DsigError> {
2947    let resolver = resolver.with_external_resource_limits(
2948        policy.resources().max_external_resource_bytes,
2949        policy.resources().max_external_resource_total_bytes,
2950    );
2951    let mut xpath_parse_budget = XPathSignatureParseBudget::from_resources(policy.resources());
2952    let execution_budget = TransformExecutionBudget::from_resources(policy.resources());
2953    let mut budgets = RetrievalMaterializationBudgets {
2954        xpath_parse: &mut xpath_parse_budget,
2955        execution: &execution_budget,
2956        resources: policy.resources(),
2957        certificate_signature_algorithms: policy.certificate_signature_algorithms(),
2958        xml_backend,
2959    };
2960    let mut materialization = KeyInfoMaterializationState::default();
2961    // Candidate-local retrieval failures remain deferred: this metadata-only
2962    // entry point cannot know whether a later key-selection stage needs them.
2963    materialize_key_info_references_with_budgets(
2964        key_info,
2965        &resolver,
2966        policy,
2967        provider,
2968        &mut budgets,
2969        &mut materialization,
2970    )?;
2971    Ok(())
2972}
2973
2974/// Resolve and recursively expand policy-allowed `KeyInfoReference` sources
2975/// while preparing caller-supplied key material for signing.
2976///
2977/// The recursive reference traversal shares the verifier's candidate-work,
2978/// depth, cycle, target, XML parsing, and external-resource bounds. Retrieval
2979/// methods nested in an external referenced document are materialized while
2980/// that document's URI context is active; retrieval methods in the caller's
2981/// document remain the responsibility of the complete signing pipeline.
2982/// candidate failures are deferred so an earlier usable key source can still
2983/// be selected; the complete signing pipeline reports the retained failure if
2984/// no source resolves to a key. URI classes remain controlled by the signing
2985/// policy's `uris` field; external
2986/// bytes must be attached to `resolver` explicitly by the caller. The resolver
2987/// is consumed so this operation can bind a fresh aggregate external-resource
2988/// budget to the supplied policy. `xml_backend` is used for every recursively
2989/// referenced XML document, preserving the caller's parser semantics.
2990pub fn materialize_signing_key_info_references(
2991    key_info: &mut KeyInfo,
2992    resolver: UriReferenceResolver<'_>,
2993    policy: &crate::policy::SigningPolicy,
2994    provider: &dyn crate::provider::CryptoProvider,
2995    xml_backend: crate::XmlBackend,
2996) -> Result<(), DsigError> {
2997    policy.validate()?;
2998    materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
2999}
3000
3001/// Resolve and recursively expand policy-allowed `KeyInfoReference` sources
3002/// before verification key selection.
3003///
3004/// In addition to URI and resource policy, this entry point enforces the
3005/// verification policy's `key_sources.key_info_reference` trust gate. Retrieval
3006/// methods nested in an external referenced document are materialized in that
3007/// document's URI context; retrieval methods in the caller's document remain
3008/// the responsibility of the complete verification pipeline. Candidate-local
3009/// retrieval failures remain deferred so usable sibling sources survive; the
3010/// complete pipeline reports one only when key selection has no fallback.
3011/// External bytes must be attached to `resolver` explicitly by the caller. The resolver is
3012/// consumed so this operation can bind a fresh aggregate external-resource
3013/// budget to the supplied policy. `xml_backend` is used for every recursively
3014/// referenced XML document, preserving the caller's parser semantics.
3015pub fn materialize_verification_key_info_references(
3016    key_info: &mut KeyInfo,
3017    resolver: UriReferenceResolver<'_>,
3018    policy: &crate::policy::VerificationPolicy,
3019    provider: &dyn crate::provider::CryptoProvider,
3020    xml_backend: crate::XmlBackend,
3021) -> Result<(), DsigError> {
3022    policy.validate()?;
3023    materialize_key_info_references_for_policy(key_info, resolver, policy, provider, xml_backend)
3024}
3025
3026fn materialize_retrieval_methods_with_budgets(
3027    key_info: &mut KeyInfo,
3028    resolver: &UriReferenceResolver<'_>,
3029    allowed_uri_types: UriTypeSet,
3030    allowed_transforms: Option<&HashSet<String>>,
3031    provider: &dyn crate::provider::CryptoProvider,
3032    budgets: &mut RetrievalMaterializationBudgets<'_>,
3033    candidate_work: &mut usize,
3034) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
3035    let retrieval_count = key_info
3036        .sources
3037        .iter()
3038        .filter(|source| matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. }))
3039        .count();
3040    if retrieval_count > MAX_RETRIEVAL_METHOD_COUNT {
3041        return Err(SignatureVerificationPipelineError::InvalidStructure {
3042            reason: "KeyInfo contains too many RetrievalMethod elements",
3043        });
3044    }
3045
3046    let mut total_binary_len = existing_x509_binary_len(key_info)?;
3047    let mut seen = HashSet::new();
3048    let mut materialized = Vec::with_capacity(key_info.sources.len());
3049    let mut outcome = RetrievalMaterialization::default();
3050    for source in std::mem::take(&mut key_info.sources) {
3051        let super::parse::KeyInfoSource::RetrievalMethod {
3052            uri: resolved_uri,
3053            resource_type,
3054            transforms,
3055        } = source
3056        else {
3057            materialized.push(source);
3058            continue;
3059        };
3060
3061        let identity = (
3062            resolved_uri.clone(),
3063            resource_type.clone(),
3064            transforms.clone(),
3065        );
3066        if !seen.insert(identity) {
3067            continue;
3068        }
3069
3070        if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#rawX509Certificate")
3071        {
3072            if transforms != RetrievalMethodTransforms::None
3073                || classify_uri(&resolved_uri) != UriClass::External
3074            {
3075                return Err(SignatureVerificationPipelineError::InvalidStructure {
3076                    reason: "raw X509 RetrievalMethod requires an untransformed external URI",
3077                });
3078            }
3079            if !allowed_uri_types.allows(&resolved_uri) {
3080                return Err(crate::policy::PolicyViolation::Uri {
3081                    operation: "verification",
3082                    reason: "retrieval method URI class is not permitted",
3083                }
3084                .into());
3085            }
3086            let certificate = resolver.external_resource(&resolved_uri).map_err(|error| {
3087                SignatureVerificationPipelineError::from(ReferenceProcessingError::Transform(error))
3088            })?;
3089            let Some(certificate) = certificate else {
3090                outcome.deferred_error.get_or_insert_with(|| {
3091                    SignatureVerificationPipelineError::Reference(
3092                        ReferenceProcessingError::Transform(super::TransformError::UnsupportedUri(
3093                            resolved_uri.clone(),
3094                        )),
3095                    )
3096                });
3097                materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
3098                    uri: resolved_uri,
3099                    resource_type,
3100                    transforms,
3101                });
3102                continue;
3103            };
3104            *candidate_work = candidate_work.saturating_add(1);
3105            budgets.resources.validate_key_candidates(*candidate_work)?;
3106            if certificate.len() > MAX_X509_DECODED_BINARY_LEN {
3107                return Err(SignatureVerificationPipelineError::InvalidStructure {
3108                    reason: "raw X509 RetrievalMethod certificate exceeds maximum allowed length",
3109                });
3110            }
3111            add_retrieval_binary_usage(&mut total_binary_len, certificate.len())?;
3112            let parsed = match parse_x509_certificate(certificate) {
3113                Ok(parsed) => parsed,
3114                Err(error) => {
3115                    let error = map_key_info_parse_error(error);
3116                    if matches!(error, SignatureVerificationPipelineError::Policy(_)) {
3117                        return Err(error);
3118                    }
3119                    outcome.deferred_error.get_or_insert(error);
3120                    materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
3121                        uri: resolved_uri,
3122                        resource_type,
3123                        transforms,
3124                    });
3125                    continue;
3126                }
3127            };
3128            materialized.push(super::parse::KeyInfoSource::X509Data(
3129                super::parse::X509DataInfo {
3130                    certificates: vec![certificate.to_vec()],
3131                    parsed_certificates: vec![parsed],
3132                    certificate_chain: vec![0],
3133                    ..super::parse::X509DataInfo::default()
3134                },
3135            ));
3136        } else if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#X509Data") {
3137            if !allowed_uri_types.allows(&resolved_uri) {
3138                return Err(crate::policy::PolicyViolation::Uri {
3139                    operation: "verification",
3140                    reason: "retrieval method URI class is not permitted",
3141                }
3142                .into());
3143            }
3144            let target = resolver
3145                .node_for_same_document_reference(&resolved_uri)
3146                .map_err(ReferenceProcessingError::Transform)?;
3147            let Some(target) = target else {
3148                outcome.deferred_error.get_or_insert(
3149                    SignatureVerificationPipelineError::InvalidStructure {
3150                        reason: "X509Data RetrievalMethod target is missing or ambiguous",
3151                    },
3152                );
3153                materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
3154                    uri: resolved_uri,
3155                    resource_type,
3156                    transforms,
3157                });
3158                continue;
3159            };
3160            let node = match transforms {
3161                RetrievalMethodTransforms::None
3162                    if target.has_tag_name((XMLDSIG_NS, "X509Data")) =>
3163                {
3164                    target
3165                }
3166                RetrievalMethodTransforms::None => {
3167                    return Err(SignatureVerificationPipelineError::InvalidStructure {
3168                        reason: "untransformed X509Data RetrievalMethod must target X509Data directly",
3169                    });
3170                }
3171                RetrievalMethodTransforms::X509DataNodeSetFilter {
3172                    expression,
3173                    namespaces,
3174                } => {
3175                    enforce_transform_allowed(allowed_transforms, XPATH_TRANSFORM_URI)?;
3176                    budgets
3177                        .xpath_parse
3178                        .validate_expression(&expression)
3179                        .map_err(ReferenceProcessingError::Transform)?;
3180                    budgets
3181                        .xpath_parse
3182                        .validate_namespaces(&namespaces)
3183                        .map_err(ReferenceProcessingError::Transform)?;
3184                    select_retrieved_x509_data_root(target, budgets.execution)?
3185                }
3186                RetrievalMethodTransforms::Unsupported => {
3187                    return Err(SignatureVerificationPipelineError::InvalidStructure {
3188                        reason: "X509Data RetrievalMethod contains unsupported transforms",
3189                    });
3190                }
3191            };
3192            let data = parse_x509_data_dispatch_with_budget_and_provider(
3193                node,
3194                &mut total_binary_len,
3195                candidate_work,
3196                provider,
3197                budgets.resources,
3198                budgets.certificate_signature_algorithms,
3199            )
3200            .map_err(map_key_info_parse_error)?;
3201            materialized.push(super::parse::KeyInfoSource::X509Data(data));
3202        } else {
3203            materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
3204                uri: resolved_uri,
3205                resource_type,
3206                transforms,
3207            });
3208        }
3209    }
3210    key_info.sources = materialized;
3211    Ok(outcome)
3212}
3213
3214fn select_retrieved_x509_data_root<'a, 'input>(
3215    target: Node<'a, 'input>,
3216    execution_budget: &TransformExecutionBudget,
3217) -> Result<Node<'a, 'input>, SignatureVerificationPipelineError> {
3218    // XMLDSig XPath filtering evaluates the predicate for every node in the
3219    // dereferenced node-set, including attribute and namespace nodes. The
3220    // element-only scan below selects X509Data but must not undercount that
3221    // XPath context cardinality.
3222    let context_nodes = NodeSet::ensure_subtree_materialization_fits_with_budget(
3223        target,
3224        false,
3225        execution_budget.node_set_materialization(),
3226    )
3227    .map_err(ReferenceProcessingError::Transform)?;
3228    execution_budget
3229        .validate_xpath_context_evaluations(context_nodes)
3230        .map_err(ReferenceProcessingError::Transform)?;
3231    execution_budget
3232        .charge_xpath_work(context_nodes)
3233        .map_err(ReferenceProcessingError::Transform)?;
3234    execution_budget
3235        .charge_node_filter_work(context_nodes)
3236        .map_err(ReferenceProcessingError::Transform)?;
3237    let mut root = None;
3238    for candidate in target.descendants() {
3239        if !candidate.is_element()
3240            || candidate.tag_name().namespace() != Some(XMLDSIG_NS)
3241            || candidate.tag_name().name() != "X509Data"
3242        {
3243            continue;
3244        }
3245        if root.replace(candidate).is_some() {
3246            return Err(SignatureVerificationPipelineError::InvalidStructure {
3247                reason: "X509Data RetrievalMethod selected multiple X509Data elements",
3248            });
3249        }
3250    }
3251    root.ok_or(SignatureVerificationPipelineError::InvalidStructure {
3252        reason: "X509Data RetrievalMethod selected no X509Data element",
3253    })
3254}
3255
3256#[cfg(test)]
3257fn materialize_retrieval_methods(
3258    key_info: &mut KeyInfo,
3259    resolver: &UriReferenceResolver<'_>,
3260    allowed_uri_types: UriTypeSet,
3261    allowed_transforms: Option<&HashSet<String>>,
3262    provider: &dyn crate::provider::CryptoProvider,
3263) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
3264    let mut xpath_parse_budget = XPathSignatureParseBudget::default();
3265    let execution_budget = TransformExecutionBudget::default();
3266    let resources = crate::policy::ResourcePolicy::default();
3267    let mut budgets = RetrievalMaterializationBudgets {
3268        xpath_parse: &mut xpath_parse_budget,
3269        execution: &execution_budget,
3270        resources: &resources,
3271        certificate_signature_algorithms: None,
3272        xml_backend: crate::XmlBackend::default(),
3273    };
3274    let mut candidate_work = key_info.embedded_candidate_count();
3275    materialize_retrieval_methods_with_budgets(
3276        key_info,
3277        resolver,
3278        allowed_uri_types,
3279        allowed_transforms,
3280        provider,
3281        &mut budgets,
3282        &mut candidate_work,
3283    )
3284}
3285
3286fn existing_x509_binary_len(
3287    key_info: &KeyInfo,
3288) -> Result<usize, SignatureVerificationPipelineError> {
3289    let mut total = 0usize;
3290    for source in &key_info.sources {
3291        if let super::parse::KeyInfoSource::X509Data(info) = source {
3292            for len in info
3293                .certificates
3294                .iter()
3295                .chain(&info.skis)
3296                .chain(&info.crls)
3297                .map(Vec::len)
3298                .chain(info.digests.iter().map(|(_, digest)| digest.len()))
3299            {
3300                add_retrieval_binary_usage(&mut total, len)?;
3301            }
3302        }
3303    }
3304    Ok(total)
3305}
3306
3307fn add_retrieval_binary_usage(
3308    total: &mut usize,
3309    delta: usize,
3310) -> Result<(), SignatureVerificationPipelineError> {
3311    *total =
3312        total
3313            .checked_add(delta)
3314            .ok_or(SignatureVerificationPipelineError::InvalidStructure {
3315                reason: "RetrievalMethod X509Data binary length overflow",
3316            })?;
3317    if *total > MAX_X509_DATA_TOTAL_BINARY_LEN {
3318        return Err(SignatureVerificationPipelineError::InvalidStructure {
3319            reason: "RetrievalMethod X509Data exceeds maximum aggregate binary length",
3320        });
3321    }
3322    Ok(())
3323}
3324
3325fn manifest_reference_failure_reason(
3326    error: ReferenceProcessingError,
3327    ref_index: usize,
3328) -> FailureReason {
3329    match error.into_policy_violation() {
3330        Ok(_) => FailureReason::ReferencePolicyViolation { ref_index },
3331        Err(_) => FailureReason::ReferenceProcessingFailure { ref_index },
3332    }
3333}
3334
3335struct CompiledManifestReference {
3336    index: usize,
3337    reference: Reference,
3338    reference_node_id: NodeId,
3339}
3340
3341struct CompiledManifestInvalid {
3342    result: ReferenceResult,
3343}
3344
3345enum ScheduledManifestReference {
3346    Parsed {
3347        node: OperationNodeId,
3348        reference: CompiledManifestReference,
3349    },
3350    Invalid {
3351        node: OperationNodeId,
3352        invalid: CompiledManifestInvalid,
3353    },
3354}
3355
3356impl ScheduledManifestReference {
3357    fn index(&self) -> usize {
3358        match self {
3359            Self::Parsed { reference, .. } => reference.index,
3360            Self::Invalid { invalid, .. } => invalid.result.reference_index,
3361        }
3362    }
3363}
3364
3365fn process_authenticated_manifest_references(
3366    operation: &mut OperationExecutionContext<
3367        &crate::policy::VerificationPolicy,
3368        &VerificationOperationBudgets,
3369    >,
3370    view: DocumentView<'_>,
3371    signature_node: Node<'_, '_>,
3372    resolver: &UriReferenceResolver<'_>,
3373    ctx: &VerifyContext<'_>,
3374    mut remaining_reference_capacity: usize,
3375    crypto: OperationNodeId,
3376) -> Result<(Vec<ReferenceResult>, Vec<OperationNodeId>), SignatureVerificationPipelineError> {
3377    let mut processed_manifests = HashSet::new();
3378    let mut next_reference_index = 0usize;
3379    let mut frontier_index = 0usize;
3380    let mut completed_digests = Vec::new();
3381    let mut results = Vec::new();
3382    loop {
3383        operation.extend();
3384        let discovery = operation.add_node(
3385            OperationNodeKind::Manifest {
3386                index: frontier_index,
3387            },
3388            OperationStage::AuthenticatedDependency,
3389            None,
3390        );
3391        operation
3392            .add_dependency(discovery, crypto)
3393            .map_err(map_verification_plan_error)?;
3394        for digest in &completed_digests {
3395            operation
3396                .add_dependency(discovery, *digest)
3397                .map_err(map_verification_plan_error)?;
3398        }
3399        operation.compile().map_err(map_verification_plan_error)?;
3400        let parsed = operation.run(discovery, || {
3401            let mut xpath_parse = operation.budgets().xpath_parse.borrow_mut();
3402            let mut state = ManifestDiscoveryState {
3403                processed: &mut processed_manifests,
3404                remaining_capacity: &mut remaining_reference_capacity,
3405                next_reference_index: &mut next_reference_index,
3406                xpath_parse: &mut xpath_parse,
3407            };
3408            parse_manifest_references(
3409                signature_node,
3410                operation,
3411                view,
3412                &mut state,
3413                ctx.allowed_transform_uris(),
3414            )
3415        })?;
3416        let discovered = parsed.references.len() + parsed.invalid_results.len();
3417        if discovered == 0 {
3418            break;
3419        }
3420
3421        operation.extend();
3422        let mut scheduled = Vec::with_capacity(discovered);
3423        for item in parsed.references {
3424            let resource = operation.budgets().resource_identity_for_reference(
3425                &item.reference,
3426                item.index,
3427                resolver,
3428                view,
3429                resolver.node_for_node_id(item.reference_node_id),
3430            );
3431            let node = operation.add_node(
3432                OperationNodeKind::Digest { index: item.index },
3433                OperationStage::AuthenticatedDependency,
3434                Some(resource.clone()),
3435            );
3436            operation
3437                .add_dependency(node, discovery)
3438                .map_err(map_verification_plan_error)?;
3439            scheduled.push(ScheduledManifestReference::Parsed {
3440                node,
3441                reference: item,
3442            });
3443        }
3444        for item in parsed.invalid_results {
3445            let node = operation.add_node(
3446                OperationNodeKind::Digest {
3447                    index: item.result.reference_index,
3448                },
3449                OperationStage::AuthenticatedDependency,
3450                None,
3451            );
3452            operation
3453                .add_dependency(node, discovery)
3454                .map_err(map_verification_plan_error)?;
3455            scheduled.push(ScheduledManifestReference::Invalid {
3456                node,
3457                invalid: item,
3458            });
3459        }
3460        scheduled.sort_by_key(ScheduledManifestReference::index);
3461        operation.compile().map_err(map_verification_plan_error)?;
3462
3463        for scheduled_reference in scheduled {
3464            let (node, result) = match scheduled_reference {
3465                ScheduledManifestReference::Invalid { node, invalid } => {
3466                    let result = operation.run(node, || {
3467                        Ok::<_, SignatureVerificationPipelineError>(invalid.result)
3468                    })?;
3469                    (node, result)
3470                }
3471                ScheduledManifestReference::Parsed {
3472                    node,
3473                    reference: compiled,
3474                } => {
3475                    let observed = operation.budgets().resource_identity_for_reference(
3476                        &compiled.reference,
3477                        compiled.index,
3478                        resolver,
3479                        view,
3480                        resolver.node_for_node_id(compiled.reference_node_id),
3481                    );
3482                    let result = operation.run_with_resource(node, &observed, || {
3483                        let reference = &compiled.reference;
3484                        let budgets = operation.budgets();
3485                        let execution = ReferenceExecutionContext {
3486                            store_pre_digest: ctx.store_pre_digest,
3487                            transform_options: ctx.transform_options(),
3488                            transform_budget: &budgets.transforms,
3489                            canonicalized_data_budget: &budgets.canonicalized,
3490                            provider: ctx.provider,
3491                        };
3492                        let mut result = if execution.transform_budget.remaining_c14n_output() == 0
3493                            || reference.transforms.len()
3494                                > ctx.policy.resources.max_transforms_per_reference
3495                            || ctx
3496                                .policy
3497                                .digest_algorithms
3498                                .as_ref()
3499                                .is_some_and(|allowed| !allowed.contains(&reference.digest_method))
3500                        {
3501                            manifest_reference_invalid_result(
3502                                reference,
3503                                compiled.index,
3504                                FailureReason::ReferencePolicyViolation {
3505                                    ref_index: compiled.index,
3506                                },
3507                            )
3508                        } else {
3509                            match enforce_reference_policies(
3510                                std::slice::from_ref(reference),
3511                                ctx.policy.uris.references,
3512                                ctx.allowed_transform_uris(),
3513                            ) {
3514                                Ok(()) => process_reference_with_options(
3515                                    reference,
3516                                    resolver,
3517                                    signature_node,
3518                                    ReferenceSet::Manifest,
3519                                    compiled.index,
3520                                    resolver.node_for_node_id(compiled.reference_node_id),
3521                                    &execution,
3522                                )
3523                                .unwrap_or_else(|error| {
3524                                    manifest_reference_invalid_result(
3525                                        reference,
3526                                        compiled.index,
3527                                        manifest_reference_failure_reason(error, compiled.index),
3528                                    )
3529                                }),
3530                                Err(SignatureVerificationPipelineError::Policy(_)) => {
3531                                    manifest_reference_invalid_result(
3532                                        reference,
3533                                        compiled.index,
3534                                        FailureReason::ReferencePolicyViolation {
3535                                            ref_index: compiled.index,
3536                                        },
3537                                    )
3538                                }
3539                                Err(_) => manifest_reference_invalid_result(
3540                                    reference,
3541                                    compiled.index,
3542                                    FailureReason::ReferenceProcessingFailure {
3543                                        ref_index: compiled.index,
3544                                    },
3545                                ),
3546                            }
3547                        };
3548                        bind_reference_evidence(
3549                            &mut result,
3550                            reference,
3551                            view,
3552                            resolver.node_for_node_id(compiled.reference_node_id),
3553                            &observed,
3554                        );
3555                        if result.status == DsigStatus::Valid
3556                            && reference
3557                                .transforms
3558                                .iter()
3559                                .all(transform_preserves_manifest_structure)
3560                            && let Some(uri) = reference.uri.as_deref()
3561                            && let Ok(Some(target)) =
3562                                resolver.node_id_for_same_document_reference(uri)
3563                        {
3564                            operation.authenticate(view.node_identity_by_id(target));
3565                        }
3566                        Ok::<_, SignatureVerificationPipelineError>(result)
3567                    })?;
3568                    (node, result)
3569                }
3570            };
3571            let accepted = result.status == DsigStatus::Valid;
3572            operation.set_outcome(
3573                node,
3574                accepted,
3575                if accepted {
3576                    OperationDecisionReason::ReferenceDigestVerified
3577                } else {
3578                    OperationDecisionReason::ReferenceDigestRejected
3579                },
3580            );
3581            completed_digests.push(node);
3582            results.push(result);
3583        }
3584        frontier_index += 1;
3585    }
3586    results.sort_by_key(|result| result.reference_index);
3587    Ok((results, completed_digests))
3588}
3589
3590fn manifest_reference_invalid_result(
3591    reference: &Reference,
3592    index: usize,
3593    reason: FailureReason,
3594) -> ReferenceResult {
3595    ReferenceResult {
3596        reference_identity: None,
3597        target_identity: None,
3598        external_resource_fingerprint: None,
3599        coverage: ReferenceCoverage::TransformedData,
3600        reference_set: ReferenceSet::Manifest,
3601        reference_index: index,
3602        uri: reference
3603            .uri
3604            .clone()
3605            .unwrap_or_else(|| "<omitted>".to_owned()),
3606        digest_algorithm: reference.digest_method,
3607        status: DsigStatus::Invalid(reason),
3608        pre_digest_data: None,
3609    }
3610}
3611
3612fn parse_manifest_references(
3613    signature_node: Node<'_, '_>,
3614    operation: &OperationExecutionContext<
3615        &crate::policy::VerificationPolicy,
3616        &VerificationOperationBudgets,
3617    >,
3618    view: DocumentView<'_>,
3619    state: &mut ManifestDiscoveryState<'_>,
3620    allowed_transforms: Option<&HashSet<String>>,
3621) -> Result<ParsedManifestReferences, SignatureVerificationPipelineError> {
3622    let mut references = Vec::new();
3623    let mut invalid = Vec::new();
3624    for object_node in signature_node.children().filter(|node| {
3625        node.is_element()
3626            && node.tag_name().namespace() == Some(XMLDSIG_NS)
3627            && node.tag_name().name() == "Object"
3628    }) {
3629        let object_is_signed = operation.is_authenticated(view.node_identity(object_node));
3630        for manifest_node in object_node.children().filter(|node| {
3631            node.is_element()
3632                && node.tag_name().namespace() == Some(XMLDSIG_NS)
3633                && node.tag_name().name() == "Manifest"
3634        }) {
3635            let manifest_is_signed = operation.is_authenticated(view.node_identity(manifest_node));
3636            // Leave unauthenticated Manifests unmarked so a verified outer
3637            // Manifest can make them eligible on the next discovery pass.
3638            if !object_is_signed && !manifest_is_signed {
3639                continue;
3640            }
3641            if !state.processed.insert(manifest_node.id()) {
3642                continue;
3643            }
3644            let mut manifest_children = Vec::new();
3645            for child in manifest_node.children() {
3646                if child.is_text()
3647                    && child.text().is_some_and(|text| {
3648                        text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
3649                    })
3650                {
3651                    return Err(SignatureVerificationPipelineError::InvalidStructure {
3652                        reason: "Manifest contains non-whitespace mixed content",
3653                    });
3654                }
3655                if child.is_element() {
3656                    manifest_children.push(child);
3657                }
3658            }
3659            if manifest_children.is_empty() {
3660                return Err(SignatureVerificationPipelineError::InvalidStructure {
3661                    reason: "Manifest must contain at least one ds:Reference element child",
3662                });
3663            }
3664            for child in manifest_children {
3665                if child.tag_name().namespace() != Some(XMLDSIG_NS)
3666                    || child.tag_name().name() != "Reference"
3667                {
3668                    return Err(SignatureVerificationPipelineError::InvalidStructure {
3669                        reason: "Manifest must contain only ds:Reference element children",
3670                    });
3671                }
3672                if *state.remaining_capacity == 0 {
3673                    return Err(SignatureVerificationPipelineError::InvalidStructure {
3674                        reason: "signed Manifests exceed the per-signature Reference limit",
3675                    });
3676                }
3677                *state.remaining_capacity -= 1;
3678                let reference_index = *state.next_reference_index;
3679                *state.next_reference_index += 1;
3680                match parse_reference_with_xpath_budget(child, state.xpath_parse) {
3681                    Ok(reference) => references.push(CompiledManifestReference {
3682                        index: reference_index,
3683                        reference,
3684                        reference_node_id: child.id(),
3685                    }),
3686                    Err(ParseError::Transform(super::TransformError::UnsupportedTransform(
3687                        uri,
3688                    ))) => {
3689                        let digest_algorithm =
3690                            reference_digest_method(child).map_err(map_manifest_parse_error)?;
3691                        let reason =
3692                            if allowed_transforms.is_some_and(|allowed| !allowed.contains(&uri)) {
3693                                FailureReason::ReferencePolicyViolation {
3694                                    ref_index: reference_index,
3695                                }
3696                            } else {
3697                                FailureReason::ReferenceProcessingFailure {
3698                                    ref_index: reference_index,
3699                                }
3700                            };
3701                        invalid.push(CompiledManifestInvalid {
3702                            result: ReferenceResult {
3703                                reference_identity: Some(view.node_identity(child)),
3704                                target_identity: None,
3705                                external_resource_fingerprint: None,
3706                                coverage: ReferenceCoverage::TransformedData,
3707                                reference_set: ReferenceSet::Manifest,
3708                                reference_index,
3709                                uri: child.attribute("URI").unwrap_or("<omitted>").to_owned(),
3710                                digest_algorithm,
3711                                status: DsigStatus::Invalid(reason),
3712                                pre_digest_data: None,
3713                            },
3714                        });
3715                    }
3716                    Err(error) => return Err(map_manifest_parse_error(error)),
3717                }
3718            }
3719        }
3720    }
3721    Ok(ParsedManifestReferences {
3722        references,
3723        invalid_results: invalid,
3724    })
3725}
3726
3727struct ParsedManifestReferences {
3728    references: Vec<CompiledManifestReference>,
3729    invalid_results: Vec<CompiledManifestInvalid>,
3730}
3731
3732struct ManifestDiscoveryState<'a> {
3733    processed: &'a mut HashSet<NodeId>,
3734    remaining_capacity: &'a mut usize,
3735    next_reference_index: &'a mut usize,
3736    xpath_parse: &'a mut XPathSignatureParseBudget,
3737}
3738
3739fn transform_preserves_manifest_structure(transform: &Transform) -> bool {
3740    match transform {
3741        Transform::C14n(_) => true,
3742        // Both eligible ID targets are descendants of the owning Signature.
3743        // Enveloped subtraction therefore removes their intersection with that
3744        // Signature subtree, even though the Signature node itself is not in the
3745        // dereferenced node set.
3746        Transform::Enveloped
3747        | Transform::XpathExcludeAllSignatures
3748        | Transform::XPath(_)
3749        | Transform::XPathFilter2(_)
3750        | Transform::Base64Decode => false,
3751    }
3752}
3753
3754#[expect(
3755    clippy::large_enum_variant,
3756    reason = "single bounded authorization result avoids heap allocation"
3757)]
3758enum ResolvedVerifyingKey<'a> {
3759    Borrowed(&'a dyn VerifyingKey),
3760    Owned(super::ResolvedVerificationKey<'a>),
3761}
3762
3763impl ResolvedVerifyingKey<'_> {
3764    fn evidence(
3765        &self,
3766        policy: &crate::policy::VerificationPolicy,
3767    ) -> Result<super::KeyTrustEvidence, DsigError> {
3768        match self {
3769            Self::Borrowed(_)
3770                if policy.key_trust.mode
3771                    == crate::policy::VerificationTrustMode::CryptographicOnly =>
3772            {
3773                Ok(super::KeyTrustEvidence::NotEstablished)
3774            }
3775            Self::Borrowed(_) => Ok(super::KeyTrustEvidence::CallerTrusted),
3776            Self::Owned(key) => key.authorize(policy),
3777        }
3778    }
3779    fn as_ref(&self) -> &dyn VerifyingKey {
3780        match self {
3781            Self::Borrowed(key) => *key,
3782            Self::Owned(key) => key.as_ref(),
3783        }
3784    }
3785}
3786
3787fn resolve_verifying_key<'k>(
3788    ctx: &VerifyContext<'k>,
3789    key: Option<&'k dyn VerifyingKey>,
3790    key_info: Option<&KeyInfo>,
3791    algorithm: SignatureAlgorithm,
3792) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
3793    if let Some(key) = key {
3794        if !ctx.policy.key_sources.preset_key {
3795            return Err(crate::policy::PolicyViolation::KeyTrust {
3796                reason: "pre-resolved verification keys are disabled",
3797            }
3798            .into());
3799        }
3800        require_verifying_key_candidate_capacity(&ctx.policy)?;
3801        return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
3802    }
3803    if let Some(resolver) = ctx.key_resolver {
3804        require_verifying_key_candidate_capacity(&ctx.policy)?;
3805        let resolved =
3806            resolver.resolve_for_verification(key_info, algorithm, &ctx.policy, ctx.provider)?;
3807        if let Some(key) = &resolved {
3808            key.authorize(&ctx.policy)?;
3809        }
3810        return Ok(resolved.map(ResolvedVerifyingKey::Owned));
3811    }
3812    Ok(None)
3813}
3814
3815fn require_verifying_key_candidate_capacity(
3816    policy: &crate::policy::VerificationPolicy,
3817) -> Result<(), SignatureVerificationPipelineError> {
3818    policy
3819        .resources
3820        .validate_key_candidates(1)
3821        .map_err(Into::into)
3822}
3823
3824fn enforce_reference_policies(
3825    references: &[Reference],
3826    allowed_uri_types: UriTypeSet,
3827    allowed_transforms: Option<&HashSet<String>>,
3828) -> Result<(), SignatureVerificationPipelineError> {
3829    for reference in references {
3830        let uri = reference
3831            .uri
3832            .as_deref()
3833            .ok_or(SignatureVerificationPipelineError::Reference(
3834                ReferenceProcessingError::MissingUri,
3835            ))?;
3836        if !allowed_uri_types.allows(uri) {
3837            return Err(crate::policy::PolicyViolation::Uri {
3838                operation: "verification",
3839                reason: "reference URI class is not permitted",
3840            }
3841            .into());
3842        }
3843
3844        if let Some(allowed) = allowed_transforms {
3845            for transform in &reference.transforms {
3846                let transform_uri = transform.algorithm_uri();
3847                enforce_transform_allowed(Some(allowed), transform_uri)?;
3848            }
3849
3850            // External dereference has an octet-stream data type independent of
3851            // whether the caller supplied the resource. Every transform then
3852            // determines the next type, including implicit binary-to-node-set
3853            // adapters before XML-level transforms.
3854            let produces_binary = transform_chain_produces_binary(
3855                classify_uri(uri) == UriClass::External,
3856                &reference.transforms,
3857            );
3858            if !produces_binary {
3859                enforce_transform_allowed(Some(allowed), DEFAULT_IMPLICIT_C14N_URI)?;
3860            }
3861        }
3862    }
3863    Ok(())
3864}
3865
3866fn enforce_transform_allowed(
3867    allowed_transforms: Option<&HashSet<String>>,
3868    algorithm: &str,
3869) -> Result<(), SignatureVerificationPipelineError> {
3870    if allowed_transforms.is_some_and(|allowed| !allowed.contains(algorithm)) {
3871        return Err(crate::policy::PolicyViolation::Algorithm {
3872            operation: "verification transform",
3873            algorithm: algorithm.to_owned(),
3874        }
3875        .into());
3876    }
3877    Ok(())
3878}
3879
3880#[derive(Debug, Clone, Copy)]
3881pub(super) struct SignatureChildNodes<'a, 'input> {
3882    signed_info_node: Node<'a, 'input>,
3883    signature_value_node: Node<'a, 'input>,
3884    key_info_node: Option<Node<'a, 'input>>,
3885}
3886
3887pub(super) fn parse_signature_children<'a, 'input>(
3888    signature_node: Node<'a, 'input>,
3889) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
3890    let mut signed_info_node: Option<Node<'_, '_>> = None;
3891    let mut signature_value_node: Option<Node<'_, '_>> = None;
3892    let mut key_info_node: Option<Node<'_, '_>> = None;
3893    let mut signed_info_index: Option<usize> = None;
3894    let mut signature_value_index: Option<usize> = None;
3895    let mut key_info_index: Option<usize> = None;
3896    let mut first_unexpected_dsig_index: Option<usize> = None;
3897
3898    let mut element_index = 0usize;
3899    for child in signature_node.children() {
3900        if child.is_text() {
3901            if child
3902                .text()
3903                .is_some_and(|text| !is_xml_whitespace_only(text))
3904            {
3905                return Err(SignatureVerificationPipelineError::InvalidStructure {
3906                    reason: "Signature must not contain non-whitespace mixed content",
3907                });
3908            }
3909            continue;
3910        }
3911        if !child.is_element() {
3912            continue;
3913        }
3914
3915        element_index += 1;
3916        if child.tag_name().namespace() != Some(XMLDSIG_NS) {
3917            return Err(SignatureVerificationPipelineError::InvalidStructure {
3918                reason: "Signature must contain only XMLDSIG element children",
3919            });
3920        }
3921        match child.tag_name().name() {
3922            "SignedInfo" => {
3923                if signed_info_node.is_some() {
3924                    return Err(SignatureVerificationPipelineError::InvalidStructure {
3925                        reason: "SignedInfo must appear exactly once under Signature",
3926                    });
3927                }
3928                signed_info_node = Some(child);
3929                signed_info_index = Some(element_index);
3930            }
3931            "SignatureValue" => {
3932                if signature_value_node.is_some() {
3933                    return Err(SignatureVerificationPipelineError::InvalidStructure {
3934                        reason: "SignatureValue must appear exactly once under Signature",
3935                    });
3936                }
3937                signature_value_node = Some(child);
3938                signature_value_index = Some(element_index);
3939            }
3940            "KeyInfo" => {
3941                if key_info_node.is_some() {
3942                    return Err(SignatureVerificationPipelineError::InvalidStructure {
3943                        reason: "KeyInfo must appear at most once under Signature",
3944                    });
3945                }
3946                key_info_node = Some(child);
3947                key_info_index = Some(element_index);
3948            }
3949            "Object" => {
3950                // Valid Object elements are allowed only after SignedInfo, SignatureValue,
3951                // and optional KeyInfo; this is enforced via first_unexpected_dsig_index.
3952            }
3953            _ => {
3954                if first_unexpected_dsig_index.is_none() {
3955                    first_unexpected_dsig_index = Some(element_index);
3956                }
3957            }
3958        }
3959    }
3960
3961    let signed_info_node =
3962        signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
3963            element: "SignedInfo",
3964        })?;
3965    let signature_value_node =
3966        signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
3967            element: "SignatureValue",
3968        })?;
3969    if signed_info_index != Some(1) {
3970        return Err(SignatureVerificationPipelineError::InvalidStructure {
3971            reason: "SignedInfo must be the first element child of Signature",
3972        });
3973    }
3974    if signature_value_index != Some(2) {
3975        return Err(SignatureVerificationPipelineError::InvalidStructure {
3976            reason: "SignatureValue must be the second element child of Signature",
3977        });
3978    }
3979    if let Some(index) = key_info_index
3980        && index != 3
3981    {
3982        return Err(SignatureVerificationPipelineError::InvalidStructure {
3983            reason: "KeyInfo must be the third element child of Signature when present",
3984        });
3985    }
3986
3987    let allowed_prefix_end = key_info_index.unwrap_or(2);
3988    if let Some(unexpected_index) = first_unexpected_dsig_index {
3989        return Err(SignatureVerificationPipelineError::InvalidStructure {
3990            reason: if unexpected_index > allowed_prefix_end {
3991                "After SignedInfo, SignatureValue, and optional KeyInfo, Signature may contain only Object elements"
3992            } else {
3993                "Signature may contain SignedInfo first, SignatureValue second, optional KeyInfo third, and Object elements thereafter"
3994            },
3995        });
3996    }
3997
3998    Ok(SignatureChildNodes {
3999        signed_info_node,
4000        signature_value_node,
4001        key_info_node,
4002    })
4003}
4004
4005fn decode_signature_value(
4006    signature_value_node: Node<'_, '_>,
4007    algorithm: SignatureAlgorithm,
4008    resources: &crate::policy::ResourcePolicy,
4009) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
4010    if matches!(algorithm, SignatureAlgorithm::PostQuantum(_)) {
4011        let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
4012        // PQ signatures can be tens of kilobytes: borrow their XML text and
4013        // stream directly into the decoded allocation, without a second
4014        // full-size normalized Base64 buffer.
4015        let payload = super::whitespace::XmlBase64Payload::bounded(
4016            signature_value_node,
4017            raw_maximum,
4018            maximum,
4019        )
4020        .map_err(
4021            |reason| SignatureVerificationPipelineError::InvalidStructure {
4022                reason: match reason {
4023                    "unexpected nested element" => {
4024                        "SignatureValue must not contain element children"
4025                    }
4026                    "maximum allowed text length" => {
4027                        "SignatureValue exceeds maximum allowed text length"
4028                    }
4029                    "maximum allowed base64 length" => {
4030                        "SignatureValue exceeds maximum allowed length"
4031                    }
4032                    _ => "SignatureValue contains malformed Base64",
4033                },
4034            },
4035        )?;
4036        return payload.decode().map_err(|_| {
4037            SignatureVerificationPipelineError::InvalidStructure {
4038                reason: "SignatureValue contains malformed Base64",
4039            }
4040        });
4041    }
4042    if signature_value_node
4043        .children()
4044        .any(|child| child.is_element())
4045    {
4046        return Err(SignatureVerificationPipelineError::InvalidStructure {
4047            reason: "SignatureValue must not contain element children",
4048        });
4049    }
4050
4051    // Count bounded borrowed text first: comments may split a
4052    // signature into many tiny text nodes. Reserving per node would repeatedly
4053    // copy the accumulated payload; one exact reservation keeps this linear.
4054    let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
4055    let mut text_len = 0usize;
4056    let mut normalized_len = 0usize;
4057    for child in signature_value_node
4058        .children()
4059        .filter(|child| child.is_text())
4060    {
4061        if let Some(text) = child.text() {
4062            if text.len() > raw_maximum - text_len {
4063                return Err(SignatureVerificationPipelineError::InvalidStructure {
4064                    reason: "SignatureValue exceeds maximum allowed text length",
4065                });
4066            }
4067            text_len += text.len();
4068            for byte in text.bytes() {
4069                if matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
4070                    continue;
4071                }
4072                if normalized_len == maximum {
4073                    return Err(SignatureVerificationPipelineError::InvalidStructure {
4074                        reason: "SignatureValue exceeds maximum allowed length",
4075                    });
4076                }
4077                normalized_len += 1;
4078            }
4079        }
4080    }
4081    let mut normalized = Vec::with_capacity(normalized_len);
4082    let mut raw_text_len = 0usize;
4083    for child in signature_value_node
4084        .children()
4085        .filter(|child| child.is_text())
4086    {
4087        if let Some(text) = child.text() {
4088            push_normalized_signature_text(
4089                text,
4090                &mut raw_text_len,
4091                &mut normalized,
4092                algorithm,
4093                resources,
4094            )?;
4095        }
4096    }
4097
4098    Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
4099}
4100
4101fn push_normalized_signature_text(
4102    text: &str,
4103    raw_text_len: &mut usize,
4104    normalized: &mut Vec<u8>,
4105    algorithm: SignatureAlgorithm,
4106    resources: &crate::policy::ResourcePolicy,
4107) -> Result<(), SignatureVerificationPipelineError> {
4108    let (maximum, raw_maximum) = signature_value_limits(algorithm, resources);
4109    if text.len() > raw_maximum - *raw_text_len {
4110        return Err(SignatureVerificationPipelineError::InvalidStructure {
4111            reason: "SignatureValue exceeds maximum allowed text length",
4112        });
4113    }
4114    *raw_text_len += text.len();
4115    let additional = text
4116        .bytes()
4117        .filter(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
4118        .count();
4119    if additional > maximum - normalized.len() {
4120        return Err(SignatureVerificationPipelineError::InvalidStructure {
4121            reason: "SignatureValue exceeds maximum allowed length",
4122        });
4123    }
4124
4125    normalize_xml_base64_bytes(text.as_bytes(), normalized, |_| true).map_err(|err| {
4126        SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(
4127            err.normalized_offset,
4128            err.invalid_byte,
4129        ))
4130    })?;
4131
4132    Ok(())
4133}
4134
4135fn signature_value_limits(
4136    algorithm: SignatureAlgorithm,
4137    resources: &crate::policy::ResourcePolicy,
4138) -> (usize, usize) {
4139    // FIPS 205 table 2 includes signatures up to 49,856 octets. The old
4140    // RSA-oriented encoded-text ceiling is not their wire contract.
4141    // https://doi.org/10.6028/NIST.FIPS.205
4142    // XSD 1.0 section 3.2.16 fixes base64Binary whitespace to collapse:
4143    // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#base64Binary
4144    // Raw text is bounded by the operation's XML budget, not the wire width.
4145    let encoded = match algorithm {
4146        SignatureAlgorithm::PostQuantum(parameter) => parameter.signature_len().div_ceil(3) * 4,
4147        _ => MAX_SIGNATURE_VALUE_LEN,
4148    };
4149    (encoded, resources.max_xml_document_bytes)
4150}
4151
4152fn verify_with_algorithm(
4153    algorithm: SignatureAlgorithm,
4154    public_key_pem: &str,
4155    signed_data: &[u8],
4156    signature_value: &[u8],
4157) -> Result<bool, SignatureVerificationPipelineError> {
4158    match algorithm {
4159        SignatureAlgorithm::Ed25519
4160        | SignatureAlgorithm::Ed25519Ctx
4161        | SignatureAlgorithm::Ed25519Ph
4162        | SignatureAlgorithm::Ed448
4163        | SignatureAlgorithm::Ed448Ph
4164        | SignatureAlgorithm::PostQuantum(_) => {
4165            let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
4166                .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
4167            if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
4168                return Err(SignatureVerificationError::InvalidKeyPem.into());
4169            }
4170            Ok(super::modern::verify(
4171                algorithm,
4172                &pem.contents,
4173                signed_data,
4174                signature_value,
4175            )?)
4176        }
4177        SignatureAlgorithm::DsaSha1 | SignatureAlgorithm::DsaSha256 => {
4178            let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
4179                .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
4180            if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
4181                return Err(SignatureVerificationError::InvalidKeyPem.into());
4182            }
4183            Ok(verify_dsa_signature_spki(
4184                algorithm,
4185                &pem.contents,
4186                signed_data,
4187                signature_value,
4188            )?)
4189        }
4190        SignatureAlgorithm::HmacSha1
4191        | SignatureAlgorithm::HmacSha224
4192        | SignatureAlgorithm::HmacSha256
4193        | SignatureAlgorithm::HmacSha384
4194        | SignatureAlgorithm::HmacSha512 => Err(SignatureVerificationError::UnsupportedAlgorithm {
4195            uri: algorithm.uri().to_string(),
4196        }
4197        .into()),
4198        SignatureAlgorithm::RsaSha1
4199        | SignatureAlgorithm::RsaSha224
4200        | SignatureAlgorithm::RsaSha256
4201        | SignatureAlgorithm::RsaSha384
4202        | SignatureAlgorithm::RsaSha512 => Ok(verify_rsa_signature_pem(
4203            algorithm,
4204            public_key_pem,
4205            signed_data,
4206            signature_value,
4207        )?),
4208        SignatureAlgorithm::EcdsaSha1
4209        | SignatureAlgorithm::EcdsaSha224
4210        | SignatureAlgorithm::EcdsaSha256
4211        | SignatureAlgorithm::EcdsaSha384
4212        | SignatureAlgorithm::EcdsaSha512
4213        | SignatureAlgorithm::EcdsaSha3_224
4214        | SignatureAlgorithm::EcdsaSha3_256
4215        | SignatureAlgorithm::EcdsaSha3_384
4216        | SignatureAlgorithm::EcdsaSha3_512 => {
4217            // Malformed ECDSA signature bytes are treated as a verification miss
4218            // (Ok(false)) instead of a pipeline error; only key/algorithm and
4219            // crypto-operation failures propagate as Err.
4220            match verify_ecdsa_signature_pem(
4221                algorithm,
4222                public_key_pem,
4223                signed_data,
4224                signature_value,
4225            ) {
4226                Ok(valid) => Ok(valid),
4227                Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
4228                Err(error) => Err(error.into()),
4229            }
4230        }
4231    }
4232}
4233
4234#[cfg(test)]
4235#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
4236mod tests {
4237    use super::*;
4238    use crate::c14n::C14nAlgorithm;
4239    use crate::xml::dom::Document;
4240    use crate::xmldsig::TransformError;
4241    use crate::xmldsig::digest::DigestAlgorithm;
4242    use crate::xmldsig::parse::{Reference, parse_signed_info};
4243    use crate::xmldsig::transforms::Transform;
4244    use crate::xmldsig::uri::UriReferenceResolver;
4245    use base64::Engine;
4246
4247    // ── Helpers ──────────────────────────────────────────────────────
4248
4249    /// Build a Reference with given URI, transforms, digest method, and expected digest.
4250    fn make_reference(
4251        uri: &str,
4252        transforms: Vec<Transform>,
4253        digest_method: DigestAlgorithm,
4254        digest_value: Vec<u8>,
4255    ) -> Reference {
4256        Reference {
4257            uri: Some(uri.to_string()),
4258            id: None,
4259            ref_type: None,
4260            transforms,
4261            digest_method,
4262            digest_value,
4263        }
4264    }
4265
4266    #[test]
4267    fn reference_resolution_uses_each_elements_effective_xml_base() {
4268        // Equal lexical URIs under different xml:base values identify distinct
4269        // caller-owned resources and must not collide in the resolver.
4270        let first = b"first payload";
4271        let second = b"second payload";
4272        let first_digest = base64::engine::general_purpose::STANDARD
4273            .encode(compute_digest(DigestAlgorithm::Sha256, first));
4274        let second_digest = base64::engine::general_purpose::STANDARD
4275            .encode(compute_digest(DigestAlgorithm::Sha256, second));
4276        let xml = format!(
4277            r#"<root xml:base="https://example.test/base/" xmlns:ds="{XMLDSIG_NS}">
4278                <ds:Signature><ds:SignedInfo>
4279                    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4280                    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4281                    <ds:Reference xml:base="one/" URI="payload.bin">
4282                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4283                        <ds:DigestValue>{first_digest}</ds:DigestValue>
4284                    </ds:Reference>
4285                    <ds:Reference xml:base="../two/" URI="payload.bin">
4286                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4287                        <ds:DigestValue>{second_digest}</ds:DigestValue>
4288                    </ds:Reference>
4289                </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>
4290            </root>"#
4291        );
4292        let document = Document::parse(&xml).unwrap();
4293        let signature = document
4294            .descendants()
4295            .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
4296            .unwrap();
4297        let signed_info_node = signature
4298            .children()
4299            .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
4300            .unwrap();
4301        let signed_info = parse_signed_info(signed_info_node).unwrap();
4302        let resources = HashMap::from([
4303            (
4304                "https://example.test/base/one/payload.bin".into(),
4305                first.to_vec(),
4306            ),
4307            (
4308                "https://example.test/two/payload.bin".into(),
4309                second.to_vec(),
4310            ),
4311        ]);
4312        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4313
4314        let result = process_all_references(&signed_info.references, &resolver, signature, false)
4315            .expect("each Reference should resolve against its own effective base");
4316
4317        assert!(result.all_valid());
4318    }
4319
4320    #[test]
4321    fn internal_dtd_opt_in_applies_to_detached_xml_transforms() {
4322        // The parse policy covers every XML document in one verification
4323        // pipeline, including caller-owned octets converted to a node-set.
4324        let detached = b"<!DOCTYPE payload [<!ELEMENT payload (#PCDATA)>]><payload>ok</payload>";
4325        let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
4326            DigestAlgorithm::Sha256,
4327            b"<payload>ok</payload>",
4328        ));
4329        let xml = format!(
4330            r#"<root xmlns:ds="{XMLDSIG_NS}">
4331  <ds:Signature>
4332    <ds:SignedInfo>
4333      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4334      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4335      <ds:Reference URI="urn:detached-dtd">
4336        <ds:Transforms>
4337          <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
4338        </ds:Transforms>
4339        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4340        <ds:DigestValue>{digest}</ds:DigestValue>
4341      </ds:Reference>
4342    </ds:SignedInfo>
4343    <ds:SignatureValue>AQ==</ds:SignatureValue>
4344  </ds:Signature>
4345</root>"#
4346        );
4347        let resources = HashMap::from([("urn:detached-dtd".to_owned(), detached.to_vec())]);
4348        let key = AcceptingKey;
4349
4350        let default_error = VerifyContext::new()
4351            .key(&key)
4352            .allowed_uri_types(UriTypeSet::ALL)
4353            .external_resources(&resources)
4354            .verify(&xml)
4355            .expect_err("internal DTD parsing must remain disabled by default");
4356        assert!(matches!(
4357            default_error,
4358            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
4359                crate::xmldsig::TransformError::XmlParse(_)
4360            ))
4361        ));
4362
4363        let result = VerifyContext::new()
4364            .key(&key)
4365            .allowed_uri_types(UriTypeSet::ALL)
4366            .external_resources(&resources)
4367            .allow_internal_dtd(true)
4368            .verify(&xml)
4369            .expect("the explicit DTD opt-in must cover detached XML transforms");
4370
4371        assert_eq!(result.status, DsigStatus::Valid);
4372
4373        let external_entity = br#"<!DOCTYPE payload [
4374            <!ENTITY ext SYSTEM "file:///etc/passwd">
4375        ]><payload>&ext;</payload>"#;
4376        let external_entity_resources =
4377            HashMap::from([("urn:detached-dtd".to_owned(), external_entity.to_vec())]);
4378        let external_entity_error = VerifyContext::new()
4379            .key(&key)
4380            .allowed_uri_types(UriTypeSet::ALL)
4381            .external_resources(&external_entity_resources)
4382            .allow_internal_dtd(true)
4383            .verify(&xml)
4384            .expect_err("the internal-DTD opt-in must not resolve external entities");
4385        assert!(matches!(
4386            external_entity_error,
4387            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
4388                crate::xmldsig::TransformError::XmlParse(_)
4389            ))
4390        ));
4391    }
4392
4393    #[test]
4394    fn owned_verification_revalidates_internal_dtd_provenance() {
4395        // A document accepted by one permissive operation must not bypass a
4396        // later verification context's stricter XML input policy.
4397        let document = XmlDocument::parse_with_settings(
4398            "<!DOCTYPE root [<!ENTITY value \"ok\">]><root>&value;</root>".into(),
4399            DocumentParseSettings::new(
4400                true,
4401                crate::hard_limits::XML_DOCUMENT_NODE_CEILING,
4402                crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
4403            ),
4404        )
4405        .expect("explicitly permitted DTD fixture must parse");
4406
4407        assert!(matches!(
4408            VerifyContext::new().verify_document(&document),
4409            Err(DsigError::Policy(
4410                crate::policy::PolicyViolation::XmlInput {
4411                    reason: "owned document requires internal DTD support"
4412                }
4413            ))
4414        ));
4415        assert!(!matches!(
4416            VerifyContext::new()
4417                .allow_internal_dtd(true)
4418                .verify_document(&document),
4419            Err(DsigError::Policy(
4420                crate::policy::PolicyViolation::XmlInput { .. }
4421            ))
4422        ));
4423    }
4424
4425    #[test]
4426    fn verification_policy_bounds_reference_canonicalization() {
4427        // Reference transforms and SignedInfo canonicalization are one operation;
4428        // references must not fall back to the transform hard-limit budget.
4429        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4430        let xml = format!(
4431            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>"#,
4432            "payload".repeat(16)
4433        );
4434        let policy = crate::policy::VerificationPolicy {
4435            resources: crate::policy::ResourcePolicy {
4436                max_canonicalized_bytes: 64,
4437                ..crate::policy::ResourcePolicy::default()
4438            },
4439            ..crate::policy::VerificationPolicy::default()
4440        };
4441
4442        let error = VerifyContext::new()
4443            .key(&AcceptingKey)
4444            .policy(policy)
4445            .verify(&xml)
4446            .expect_err("reference canonicalization must consume the policy budget");
4447
4448        assert!(
4449            matches!(
4450                error,
4451                SignatureVerificationPipelineError::Policy(
4452                    crate::policy::PolicyViolation::ResourceLimit {
4453                        resource: crate::policy::resource_name::CANONICALIZED_BYTES,
4454                        maximum: 64,
4455                        ..
4456                    }
4457                )
4458            ),
4459            "unexpected error: {error:?}"
4460        );
4461    }
4462
4463    #[test]
4464    fn verification_policy_bounds_document_bytes_before_parsing() {
4465        // A small node count does not bound parser work when one text node is
4466        // large, so the byte ceiling must reject before structural inspection.
4467        let xml = format!("<root>{}</root>", "x".repeat(1_024));
4468        let policy = crate::policy::VerificationPolicy {
4469            resources: crate::policy::ResourcePolicy {
4470                max_xml_document_bytes: xml.len() - 1,
4471                ..crate::policy::ResourcePolicy::default()
4472            },
4473            ..crate::policy::VerificationPolicy::default()
4474        };
4475
4476        assert!(matches!(
4477            VerifyContext::new().policy(policy).verify(&xml),
4478            Err(SignatureVerificationPipelineError::Policy(
4479                crate::policy::PolicyViolation::ResourceLimit {
4480                    resource: crate::policy::resource_name::XML_DOCUMENT,
4481                    maximum,
4482                    actual,
4483                }
4484            )) if maximum == xml.len() - 1 && actual == xml.len()
4485        ));
4486    }
4487
4488    #[test]
4489    fn verification_entry_points_enforce_policy_depth() {
4490        // Both borrowed XML and a retained document parsed under wider defaults
4491        // must be rejected before signature selection traverses the tree.
4492        let xml = "<root><child><leaf/></child></root>";
4493        let policy = crate::policy::VerificationPolicy {
4494            resources: crate::policy::ResourcePolicy {
4495                max_xml_depth: 2,
4496                ..crate::policy::ResourcePolicy::default()
4497            },
4498            ..crate::policy::VerificationPolicy::default()
4499        };
4500        let document = XmlDocument::parse(xml).expect("wide retained fixture must parse");
4501
4502        assert!(matches!(
4503            VerifyContext::new().policy(policy.clone()).verify(xml),
4504            Err(DsigError::Policy(
4505                crate::policy::PolicyViolation::ResourceLimit {
4506                    resource: crate::policy::resource_name::XML_DEPTH,
4507                    maximum: 2,
4508                    actual: 3,
4509                }
4510            ))
4511        ));
4512        assert!(matches!(
4513            VerifyContext::new()
4514                .policy(policy)
4515                .verify_document(&document),
4516            Err(DsigError::Policy(
4517                crate::policy::PolicyViolation::ResourceLimit {
4518                    resource: crate::policy::resource_name::XML_DEPTH,
4519                    maximum: 2,
4520                    actual: 3,
4521                }
4522            ))
4523        ));
4524    }
4525
4526    #[test]
4527    fn verification_policy_bounds_base64_transform_input() {
4528        // The operation snapshot must reach the transform executor rather than
4529        // silently falling back to its implementation-wide default budget.
4530        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4531        let xml = format!(
4532            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>"##
4533        );
4534        let policy = crate::policy::VerificationPolicy {
4535            resources: crate::policy::ResourcePolicy {
4536                max_base64_transform_input_bytes: 4,
4537                ..crate::policy::ResourcePolicy::default()
4538            },
4539            ..crate::policy::VerificationPolicy::default()
4540        };
4541
4542        let error = VerifyContext::new()
4543            .key(&AcceptingKey)
4544            .policy(policy)
4545            .verify(&xml)
4546            .expect_err("Base64 input must use the operation policy ceiling");
4547
4548        assert!(matches!(
4549            error,
4550            SignatureVerificationPipelineError::Policy(
4551                crate::policy::PolicyViolation::ResourceLimit {
4552                    resource: crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
4553                    maximum: 4,
4554                    ..
4555                }
4556            )
4557        ));
4558    }
4559
4560    #[test]
4561    fn verification_policy_bounds_cumulative_base64_transform_output() {
4562        // References share one operation budget. Validating each decoded value
4563        // against the full ceiling would let a signature multiply output work.
4564        let first_digest = base64::engine::general_purpose::STANDARD
4565            .encode(compute_digest(DigestAlgorithm::Sha256, b"a"));
4566        let second_digest = base64::engine::general_purpose::STANDARD
4567            .encode(compute_digest(DigestAlgorithm::Sha256, b"b"));
4568        let xml = format!(
4569            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>"##
4570        );
4571        let policy = crate::policy::VerificationPolicy {
4572            resources: crate::policy::ResourcePolicy {
4573                max_base64_transform_input_bytes: 8,
4574                max_base64_transform_output_bytes: 1,
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("references must share the Base64 output allowance");
4585
4586        assert!(matches!(
4587            error,
4588            SignatureVerificationPipelineError::Policy(
4589                crate::policy::PolicyViolation::ResourceLimit {
4590                    resource: crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
4591                    maximum: 1,
4592                    actual: 2,
4593                }
4594            )
4595        ));
4596    }
4597
4598    #[test]
4599    fn verification_policy_bounds_xpath_source_before_compilation() {
4600        // XPath parser limits are part of the same operation snapshot as the
4601        // evaluator limits; parsing cannot use a separate hard-coded budget.
4602        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4603        let xml = format!(
4604            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>"#
4605        );
4606        let policy = crate::policy::VerificationPolicy {
4607            resources: crate::policy::ResourcePolicy {
4608                max_xpath_expression_bytes: 4,
4609                ..crate::policy::ResourcePolicy::default()
4610            },
4611            ..crate::policy::VerificationPolicy::default()
4612        };
4613
4614        let error = VerifyContext::new()
4615            .key(&AcceptingKey)
4616            .policy(policy)
4617            .verify(&xml)
4618            .expect_err("XPath source must use the operation policy ceiling");
4619
4620        assert!(matches!(
4621            error,
4622            SignatureVerificationPipelineError::Policy(
4623                crate::policy::PolicyViolation::ResourceLimit {
4624                    resource: crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
4625                    maximum: 4,
4626                    ..
4627                }
4628            )
4629        ));
4630    }
4631
4632    #[test]
4633    fn verification_policy_shares_canonicalization_budget_with_signed_info() {
4634        // Reference transforms and SignedInfo canonicalization are one operation.
4635        // Each output fits independently, but their aggregate must not receive
4636        // two separate copies of the configured canonicalization allowance.
4637        let payload_text = "x".repeat(700);
4638        let canonical_payload = format!("<payload ID=\"payload\">{payload_text}</payload>");
4639        let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
4640            DigestAlgorithm::Sha256,
4641            canonical_payload.as_bytes(),
4642        ));
4643        let xml = format!(
4644            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>"##
4645        );
4646        let policy = crate::policy::VerificationPolicy {
4647            resources: crate::policy::ResourcePolicy {
4648                max_canonicalized_bytes: 1_024,
4649                ..crate::policy::ResourcePolicy::default()
4650            },
4651            ..crate::policy::VerificationPolicy::default()
4652        };
4653
4654        let error = VerifyContext::new()
4655            .key(&AcceptingKey)
4656            .policy(policy)
4657            .verify(&xml)
4658            .expect_err("SignedInfo must consume the remaining operation C14N budget");
4659
4660        assert!(
4661            matches!(
4662                &error,
4663                SignatureVerificationPipelineError::Policy(
4664                    crate::policy::PolicyViolation::ResourceLimit {
4665                        resource: "canonicalized bytes",
4666                        maximum: 1_024,
4667                        ..
4668                    }
4669                )
4670            ),
4671            "unexpected error: {error:?}"
4672        );
4673    }
4674
4675    #[test]
4676    fn manifest_processing_stops_after_c14n_budget_exhaustion() {
4677        // A failed bounded render consumes the remaining operation allowance.
4678        // Later Manifest references, including cheap binary ones, must not run.
4679        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4680        let xml = format!(
4681            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>"##
4682        );
4683        let resources = HashMap::from([("urn:small".to_owned(), b"small".to_vec())]);
4684        let transform_budget = TransformExecutionBudget::with_c14n_limit(8);
4685        let ctx = VerifyContext::new()
4686            .allowed_uri_types(UriTypeSet::ALL)
4687            .external_resources(&resources);
4688        let document = XmlDocument::parse(xml).expect("test signature must parse");
4689        let results = document
4690            .with_view(|view| {
4691                let signature = view
4692                    .document()
4693                    .descendants()
4694                    .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
4695                    .expect("test signature must contain Signature");
4696                let object = signature
4697                    .children()
4698                    .find(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
4699                    .expect("test signature must contain Object");
4700                let resolver = UriReferenceResolver::with_document_view(view, &[])
4701                    .with_external_resources(&resources);
4702                let budgets =
4703                    VerificationOperationBudgets::with_transforms(&ctx.policy, transform_budget);
4704                let mut operation = OperationExecutionContext::new(
4705                    &ctx.policy,
4706                    &budgets,
4707                    Some((view.identity(), view.generation())),
4708                );
4709                let crypto =
4710                    operation.add_node(OperationNodeKind::Crypto, OperationStage::Crypto, None);
4711                operation.compile().expect("base plan");
4712                operation
4713                    .run(crypto, || Ok::<_, SignatureVerificationPipelineError>(()))
4714                    .expect("crypto gate");
4715                operation.authenticate(view.node_identity(object));
4716                process_authenticated_manifest_references(
4717                    &mut operation,
4718                    view,
4719                    signature,
4720                    &resolver,
4721                    &ctx,
4722                    2,
4723                    crypto,
4724                )
4725                .map(|(results, _)| results)
4726            })
4727            .expect("resource exhaustion is reported per Manifest reference");
4728
4729        assert_eq!(results.len(), 2);
4730        assert!(results.iter().all(|result| {
4731            matches!(
4732                result.status,
4733                DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { .. })
4734            )
4735        }));
4736    }
4737
4738    #[test]
4739    fn verification_policy_bounds_detached_xml_nodes() {
4740        // Caller-owned detached octets become a second XML document during a
4741        // node-set transform and must inherit the same operation node ceiling.
4742        let detached = format!("<payload>{}</payload>", "<n/>".repeat(32));
4743        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4744        let xml = format!(
4745            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>"#
4746        );
4747        let resources = HashMap::from([("urn:detached-nodes".to_owned(), detached.into_bytes())]);
4748        let policy = crate::policy::VerificationPolicy {
4749            uris: crate::policy::UriPolicy {
4750                references: UriTypeSet::ALL,
4751                ..crate::policy::UriPolicy::default()
4752            },
4753            resources: crate::policy::ResourcePolicy {
4754                max_xml_nodes: 24,
4755                ..crate::policy::ResourcePolicy::default()
4756            },
4757            ..crate::policy::VerificationPolicy::default()
4758        };
4759
4760        let error = VerifyContext::new()
4761            .key(&AcceptingKey)
4762            .policy(policy)
4763            .external_resources(&resources)
4764            .verify(&xml)
4765            .expect_err("detached XML must inherit the policy node ceiling");
4766
4767        assert!(
4768            matches!(
4769                error,
4770                SignatureVerificationPipelineError::Policy(
4771                    crate::policy::PolicyViolation::ResourceLimit {
4772                        resource: crate::policy::resource_name::XML_NODES,
4773                        maximum: 24,
4774                        ..
4775                    }
4776                )
4777            ),
4778            "unexpected error: {error:?}"
4779        );
4780    }
4781
4782    #[test]
4783    fn query_only_reference_resolves_against_relative_xml_base() {
4784        // A query-only URI replaces the inherited base query without changing
4785        // its relative path; no absolute document base is required by XML Base.
4786        let payload = b"query-selected payload";
4787        let digest = base64::engine::general_purpose::STANDARD
4788            .encode(compute_digest(DigestAlgorithm::Sha256, payload));
4789        let xml = format!(
4790            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo>
4791                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4792                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4793                <ds:Reference xml:base="a/b?old" URI="?new">
4794                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4795                    <ds:DigestValue>{digest}</ds:DigestValue>
4796                </ds:Reference>
4797            </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>"#
4798        );
4799        let document = Document::parse(&xml).unwrap();
4800        let signature = document.root_element();
4801        let signed_info_node = signature
4802            .children()
4803            .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
4804            .unwrap();
4805        let signed_info = parse_signed_info(signed_info_node).unwrap();
4806        let resources = HashMap::from([("a/b?new".to_string(), payload.to_vec())]);
4807        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4808
4809        let result = process_all_references(&signed_info.references, &resolver, signature, false)
4810            .expect("query-only URI must resolve against the complete relative base path");
4811
4812        assert!(result.all_valid());
4813    }
4814
4815    #[test]
4816    fn manifest_reference_resolution_uses_its_effective_xml_base() {
4817        // Manifest references carry their own XML Base context and must not
4818        // accidentally reuse the SignedInfo or Signature element context.
4819        let payload = b"manifest payload";
4820        let digest = base64::engine::general_purpose::STANDARD
4821            .encode(compute_digest(DigestAlgorithm::Sha256, payload));
4822        let xml = format!(
4823            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
4824                <ds:Object><ds:Manifest xml:base="manifests/">
4825                    <ds:Reference URI="payload.bin">
4826                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4827                        <ds:DigestValue>{digest}</ds:DigestValue>
4828                    </ds:Reference>
4829                </ds:Manifest></ds:Object>
4830            </ds:Signature>"#
4831        );
4832        let document = Document::parse(&xml).unwrap();
4833        let signature = document.root_element();
4834        let reference_node = signature
4835            .descendants()
4836            .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
4837            .unwrap();
4838        let reference = super::super::parse::parse_reference(reference_node).unwrap();
4839        let resources = HashMap::from([(
4840            "https://example.test/manifests/payload.bin".to_string(),
4841            payload.to_vec(),
4842        )]);
4843        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4844
4845        let result = process_reference(
4846            &reference,
4847            &resolver,
4848            signature,
4849            ReferenceSet::Manifest,
4850            0,
4851            false,
4852        )
4853        .expect("Manifest Reference should inherit its own XML Base context");
4854
4855        assert_eq!(result.status, DsigStatus::Valid);
4856    }
4857
4858    #[test]
4859    fn manifest_reference_index_ignores_nested_manifest_descendants() {
4860        // The public Manifest index follows Signature/Object/Manifest structure;
4861        // wrapper descendants must not steal an index and supply another base URI.
4862        let payload = b"direct manifest payload";
4863        let digest = base64::engine::general_purpose::STANDARD
4864            .encode(compute_digest(DigestAlgorithm::Sha256, payload));
4865        let xml = format!(
4866            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
4867                <ds:Object><wrapper><ds:Manifest xml:base="nested/">
4868                    <ds:Reference URI="payload.bin">
4869                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4870                        <ds:DigestValue>{digest}</ds:DigestValue>
4871                    </ds:Reference>
4872                </ds:Manifest></wrapper></ds:Object>
4873                <ds:Object><ds:Manifest xml:base="direct/">
4874                    <ds:Reference URI="payload.bin">
4875                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4876                        <ds:DigestValue>{digest}</ds:DigestValue>
4877                    </ds:Reference>
4878                </ds:Manifest></ds:Object>
4879            </ds:Signature>"#
4880        );
4881        let document = Document::parse(&xml).unwrap();
4882        let signature = document.root_element();
4883        let direct_reference_node = signature
4884            .children()
4885            .filter(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
4886            .nth(1)
4887            .unwrap()
4888            .children()
4889            .find(|node| node.has_tag_name((XMLDSIG_NS, "Manifest")))
4890            .unwrap()
4891            .children()
4892            .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
4893            .unwrap();
4894        let reference = super::super::parse::parse_reference(direct_reference_node).unwrap();
4895        let resources = HashMap::from([(
4896            "https://example.test/direct/payload.bin".to_string(),
4897            payload.to_vec(),
4898        )]);
4899        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4900
4901        let result = process_reference(
4902            &reference,
4903            &resolver,
4904            signature,
4905            ReferenceSet::Manifest,
4906            0,
4907            false,
4908        )
4909        .expect("Manifest index must select the direct Object/Manifest reference");
4910
4911        assert_eq!(result.status, DsigStatus::Valid);
4912    }
4913
4914    struct RejectingKey;
4915
4916    impl VerifyingKey for RejectingKey {
4917        fn verify(
4918            &self,
4919            _algorithm: SignatureAlgorithm,
4920            _signed_data: &[u8],
4921            _signature_value: &[u8],
4922        ) -> Result<bool, SignatureVerificationPipelineError> {
4923            Ok(false)
4924        }
4925    }
4926
4927    struct AcceptingKey;
4928
4929    impl VerifyingKey for AcceptingKey {
4930        fn verify(
4931            &self,
4932            _algorithm: SignatureAlgorithm,
4933            _signed_data: &[u8],
4934            _signature_value: &[u8],
4935        ) -> Result<bool, SignatureVerificationPipelineError> {
4936            Ok(true)
4937        }
4938    }
4939
4940    struct PanicResolver;
4941
4942    struct UntrustedTestResolver;
4943
4944    impl KeyResolver for UntrustedTestResolver {
4945        fn resolve<'a>(
4946            &'a self,
4947            _key_info: Option<&KeyInfo>,
4948            _algorithm: SignatureAlgorithm,
4949        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
4950            Ok(Some(Box::new(AcceptingKey)))
4951        }
4952    }
4953
4954    #[test]
4955    fn verification_requires_explicit_key_authorization() {
4956        // A resolver finding a working key must not authorize its signer.
4957        let xml = signature_with_target_reference("AQ==");
4958        let error = VerifyContext::new()
4959            .key_resolver(&UntrustedTestResolver)
4960            .verify(&xml)
4961            .expect_err("default verification must reject an untrusted resolver candidate");
4962        assert!(matches!(
4963            error,
4964            DsigError::Policy(crate::policy::PolicyViolation::KeyTrust { .. })
4965        ));
4966    }
4967
4968    #[test]
4969    fn cryptographic_only_is_explicit_and_never_reports_trust() {
4970        // Opting out of signer authorization must remain visible in the result.
4971        let mut policy = crate::policy::VerificationPolicy::default();
4972        policy.key_trust.mode = crate::policy::VerificationTrustMode::CryptographicOnly;
4973        let result = VerifyContext::new()
4974            .policy(policy)
4975            .key_resolver(&UntrustedTestResolver)
4976            .verify(&signature_with_target_reference("AQ=="))
4977            .expect("explicit mathematical mode permits an untrusted candidate");
4978        assert_eq!(result.status, DsigStatus::Valid);
4979        assert_eq!(
4980            result.key_trust,
4981            super::super::KeyTrustEvidence::NotEstablished
4982        );
4983    }
4984
4985    #[test]
4986    fn caller_key_authorization_does_not_authenticate_an_invalid_signature() {
4987        // A trusted key is not evidence that the signature itself verified.
4988        let result = VerifyContext::new()
4989            .key(&RejectingKey)
4990            .verify(&signature_with_target_reference("AQ=="))
4991            .expect("an invalid mathematical signature is a completed result");
4992        assert_eq!(
4993            result.status,
4994            DsigStatus::Invalid(FailureReason::SignatureMismatch)
4995        );
4996        assert_eq!(
4997            result.key_trust,
4998            super::super::KeyTrustEvidence::CallerTrusted
4999        );
5000    }
5001
5002    impl KeyResolver for PanicResolver {
5003        fn resolve<'a>(
5004            &'a self,
5005            _key_info: Option<&KeyInfo>,
5006            _algorithm: SignatureAlgorithm,
5007        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
5008        {
5009            panic!("resolver should not be called when references already fail");
5010        }
5011    }
5012
5013    struct MissingKeyResolver;
5014
5015    impl KeyResolver for MissingKeyResolver {
5016        fn resolve<'a>(
5017            &'a self,
5018            _key_info: Option<&KeyInfo>,
5019            _algorithm: SignatureAlgorithm,
5020        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
5021        {
5022            Ok(None)
5023        }
5024    }
5025
5026    struct ConsumingKeyInfoResolver;
5027
5028    impl KeyResolver for ConsumingKeyInfoResolver {
5029        fn resolve<'a>(
5030            &'a self,
5031            _key_info: Option<&KeyInfo>,
5032            _algorithm: SignatureAlgorithm,
5033        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
5034        {
5035            Ok(None)
5036        }
5037
5038        fn consumes_document_key_info(&self) -> bool {
5039            true
5040        }
5041    }
5042
5043    struct FallbackKeyInfoResolver;
5044
5045    impl KeyResolver for FallbackKeyInfoResolver {
5046        fn resolve_for_verification<'a>(
5047            &'a self,
5048            key_info: Option<&KeyInfo>,
5049            algorithm: SignatureAlgorithm,
5050            _policy: &crate::policy::VerificationPolicy,
5051            _provider: &dyn crate::provider::CryptoProvider,
5052        ) -> Result<Option<super::super::ResolvedVerificationKey<'a>>, DsigError> {
5053            // This test resolver explicitly authorizes its application-owned key.
5054            self.resolve(key_info, algorithm)
5055                .map(|key| key.map(super::super::ResolvedVerificationKey::CallerTrusted))
5056        }
5057
5058        fn resolve<'a>(
5059            &'a self,
5060            key_info: Option<&KeyInfo>,
5061            _algorithm: SignatureAlgorithm,
5062        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
5063        {
5064            let sources = &key_info.expect("KeyInfo must be parsed").sources;
5065            assert!(matches!(
5066                sources.as_slice(),
5067                [
5068                    super::super::parse::KeyInfoSource::RetrievalMethod { .. },
5069                    super::super::parse::KeyInfoSource::KeyName(name),
5070                ] if name == "fallback"
5071            ));
5072            Ok(Some(Box::new(AcceptingKey)))
5073        }
5074
5075        fn consumes_document_key_info(&self) -> bool {
5076            true
5077        }
5078    }
5079
5080    struct EarlyKeyInfoResolver;
5081
5082    impl KeyResolver for EarlyKeyInfoResolver {
5083        fn resolve_for_verification<'a>(
5084            &'a self,
5085            key_info: Option<&KeyInfo>,
5086            algorithm: SignatureAlgorithm,
5087            _policy: &crate::policy::VerificationPolicy,
5088            _provider: &dyn crate::provider::CryptoProvider,
5089        ) -> Result<Option<super::super::ResolvedVerificationKey<'a>>, DsigError> {
5090            // Only the selected primary application key is authorized here.
5091            self.resolve(key_info, algorithm)
5092                .map(|key| key.map(super::super::ResolvedVerificationKey::CallerTrusted))
5093        }
5094
5095        fn resolve<'a>(
5096            &'a self,
5097            key_info: Option<&KeyInfo>,
5098            _algorithm: SignatureAlgorithm,
5099        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
5100        {
5101            let sources = &key_info.expect("KeyInfo must be parsed").sources;
5102            assert!(matches!(
5103                sources.as_slice(),
5104                [
5105                    super::super::parse::KeyInfoSource::KeyName(name),
5106                    super::super::parse::KeyInfoSource::RetrievalMethod { .. },
5107                ] if name == "primary"
5108            ));
5109            Ok(Some(Box::new(AcceptingKey)))
5110        }
5111
5112        fn consumes_document_key_info(&self) -> bool {
5113            true
5114        }
5115    }
5116
5117    fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
5118        format!(
5119            r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5120  <ds:SignedInfo>
5121    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5122    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5123    <ds:Reference URI="{reference_uri}">
5124      {transforms_xml}
5125      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
5126      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
5127    </ds:Reference>
5128  </ds:SignedInfo>
5129  <ds:SignatureValue>AQ==</ds:SignatureValue>
5130</ds:Signature>"#
5131        )
5132    }
5133
5134    #[test]
5135    fn verify_all_retains_each_signature_without_vacuous_success() {
5136        // Malformed signatures remain individually visible; an empty document
5137        // never satisfies a request merely because there were no failures.
5138        let document = XmlDocument::parse(format!(
5139            "<root xmlns:ds=\"{XMLDSIG_NS}\"><ds:Signature/><ds:Signature/></root>"
5140        ))
5141        .unwrap();
5142        let evidence = VerifyContext::new().verify_all(&document).unwrap();
5143        assert_eq!(evidence.signatures().len(), 2);
5144        assert!(
5145            evidence
5146                .signatures()
5147                .iter()
5148                .all(|entry| entry.result().is_err())
5149        );
5150        assert!(!evidence.all_valid());
5151        let empty = XmlDocument::parse("<root/>").unwrap();
5152        assert!(!VerifyContext::new().verify_all(&empty).unwrap().all_valid());
5153    }
5154
5155    #[test]
5156    fn verify_all_checks_cardinality_before_running_keys() {
5157        // The cardinality policy bounds report allocation and crypto work,
5158        // including malformed signatures that never reach core validation.
5159        let document = XmlDocument::parse(format!(
5160            "<root xmlns:ds=\"{XMLDSIG_NS}\"><ds:Signature/><ds:Signature/></root>"
5161        ))
5162        .unwrap();
5163        let mut policy = crate::policy::VerificationPolicy::default();
5164        policy.resources.max_signatures = 1;
5165        assert!(matches!(
5166            VerifyContext::new().policy(policy).verify_all(&document),
5167            Err(DsigError::Policy(
5168                crate::policy::PolicyViolation::ResourceLimit {
5169                    resource: "verification signatures",
5170                    maximum: 1,
5171                    actual: 2,
5172                }
5173            ))
5174        ));
5175    }
5176
5177    #[test]
5178    fn verification_signature_limit_also_gates_the_single_api() {
5179        // The same compiled resource policy must apply to both public paths.
5180        let mut policy = crate::policy::VerificationPolicy::default();
5181        policy.resources.max_signatures = 0;
5182        assert!(matches!(
5183            VerifyContext::new()
5184                .policy(policy)
5185                .key(&AcceptingKey)
5186                .verify(&signature_with_target_reference("AQ==")),
5187            Err(DsigError::Policy(
5188                crate::policy::PolicyViolation::ResourceLimit {
5189                    resource: "verification signatures",
5190                    maximum: 0,
5191                    actual: 1,
5192                }
5193            ))
5194        ));
5195    }
5196
5197    #[test]
5198    fn verify_all_preserves_generation_and_rejects_wrapped_siblings() {
5199        // A signature over one element never authenticates a lookalike sibling.
5200        let xml = signature_with_target_reference("AQ==")
5201            .replace("</root>", "<target ID=\"unsigned\">payload</target></root>");
5202        let document = XmlDocument::parse(xml).unwrap();
5203        let (signed, unsigned) = document.with_view(|view| {
5204            (
5205                view.node_for_id("target", &[]).unwrap(),
5206                view.node_for_id("unsigned", &[]).unwrap(),
5207            )
5208        });
5209        let evidence = VerifyContext::new()
5210            .key(&AcceptingKey)
5211            .verify_all(&document)
5212            .unwrap();
5213        assert!(evidence.all_valid());
5214        assert!(evidence.covers_element(&document, signed).unwrap());
5215        assert!(!evidence.covers_element(&document, unsigned).unwrap());
5216        let foreign = XmlDocument::parse("<target/>").unwrap();
5217        let target = foreign.with_view(|view| view.root_element());
5218        assert!(matches!(
5219            evidence.covers_element(&document, target),
5220            Err(DsigError::Document(XmlDocumentError::ForeignIdentity))
5221        ));
5222    }
5223
5224    #[test]
5225    fn verify_request_rejects_duplicate_targets_before_crypto() {
5226        // Ambiguous request mapping is not a successful duplicate assertion.
5227        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
5228        let target = document.with_view(|view| view.node_for_id("target", &[]).unwrap());
5229        let targets = [target, target];
5230        let request = VerificationRequest {
5231            expected_targets: &targets,
5232            ..VerificationRequest::default()
5233        };
5234        assert!(matches!(
5235            VerifyContext::new().verify_request(&document, &request),
5236            Err(DsigError::InvalidRequest {
5237                reason: "duplicate expected coverage target"
5238            })
5239        ));
5240    }
5241
5242    #[test]
5243    fn verify_all_shares_canonicalization_retention_budget() {
5244        // Repeating a Signature may not reset the retained-data allowance.
5245        let xml = signature_with_target_reference("AQ==");
5246        let single = VerifyContext::new()
5247            .key(&AcceptingKey)
5248            .store_pre_digest(true)
5249            .verify(&xml)
5250            .unwrap();
5251        let bytes = single.canonicalized_signed_info.unwrap().len()
5252            + single.signed_info_references[0]
5253                .pre_digest_data
5254                .as_ref()
5255                .unwrap()
5256                .len();
5257        let parsed = Document::parse(&xml).unwrap();
5258        let signature = parsed
5259            .descendants()
5260            .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
5261            .unwrap();
5262        let xml = xml.replace("</root>", &format!("{}</root>", &xml[signature.range()]));
5263        let document = XmlDocument::parse(xml).unwrap();
5264        let mut policy = crate::policy::VerificationPolicy::default();
5265        policy.resources.max_canonicalized_bytes = bytes * 2 - 1;
5266        let evidence = VerifyContext::new()
5267            .key(&AcceptingKey)
5268            .store_pre_digest(true)
5269            .policy(policy)
5270            .verify_all(&document)
5271            .unwrap();
5272        assert_eq!(evidence.signatures.len(), 2);
5273        assert_eq!(
5274            evidence.signatures[0].result().as_ref().unwrap().status,
5275            DsigStatus::Valid
5276        );
5277        assert!(matches!(
5278            evidence.signatures[1].result(),
5279            Err(DsigError::Policy(
5280                crate::policy::PolicyViolation::ResourceLimit {
5281                    resource: "canonicalized bytes",
5282                    ..
5283                }
5284            ))
5285        ));
5286        assert!(!evidence.accepted(&document).unwrap());
5287    }
5288
5289    #[test]
5290    fn verify_all_manifest_coverage_requires_its_authenticated_digest() {
5291        // A valid enclosing Signature authenticates the Manifest declaration,
5292        // not a target whose separately checked Manifest digest mismatches.
5293        for valid in [true, false] {
5294            let document = XmlDocument::parse(signature_with_manifest_xml(valid)).unwrap();
5295            let target = document.with_view(|view| view.node_for_id("target", &[]).unwrap());
5296            let evidence = VerifyContext::new()
5297                .key(&AcceptingKey)
5298                .process_manifests(true)
5299                .verify_all(&document)
5300                .unwrap();
5301            let result = evidence.signatures()[0].result().as_ref().unwrap();
5302            assert_eq!(result.manifest_references.len(), 1);
5303            assert!(result.manifest_references[0].reference_identity.is_some());
5304            assert_eq!(evidence.covers_element(&document, target).unwrap(), valid);
5305            assert_eq!(evidence.accepted(&document).unwrap(), valid);
5306        }
5307    }
5308
5309    #[test]
5310    fn verify_all_filtered_digest_does_not_claim_original_subtree() {
5311        // A projection may discard content even when its local digest matches.
5312        // No arbitrary filter is promoted to complete original-node coverage.
5313        let mut reference = make_reference(
5314            "#target",
5315            vec![Transform::XpathExcludeAllSignatures],
5316            DigestAlgorithm::Sha256,
5317            vec![],
5318        );
5319        let document = XmlDocument::parse(signature_with_target_reference("AQ==")).unwrap();
5320        document.with_view(|view| {
5321            let target = view.node_for_id("target", &[]).unwrap();
5322            let mut result = manifest_reference_invalid_result(
5323                &reference,
5324                0,
5325                FailureReason::ReferenceProcessingFailure { ref_index: 0 },
5326            );
5327            result.status = DsigStatus::Valid;
5328            bind_reference_evidence(
5329                &mut result,
5330                &reference,
5331                view,
5332                None,
5333                &OperationResourceIdentity::DocumentNode(target),
5334            );
5335            assert_eq!(result.target_identity, Some(target));
5336            assert_eq!(result.coverage, ReferenceCoverage::TransformedData);
5337            reference.transforms = vec![Transform::C14n(crate::c14n::C14nAlgorithm::new(
5338                crate::c14n::C14nMode::Exclusive1_0,
5339                false,
5340            ))];
5341            bind_reference_evidence(
5342                &mut result,
5343                &reference,
5344                view,
5345                None,
5346                &OperationResourceIdentity::DocumentNode(target),
5347            );
5348            assert_eq!(result.coverage, ReferenceCoverage::ElementContent);
5349        });
5350    }
5351
5352    #[test]
5353    fn verify_request_rejects_conflicting_resource_owners() {
5354        // There is no implicit override between request bytes and old builder
5355        // configuration: ownership must be explicit before resolver work.
5356        let document = XmlDocument::parse("<root/>").unwrap();
5357        let resources = HashMap::new();
5358        let request = VerificationRequest {
5359            external_resources: Some(&resources),
5360            ..VerificationRequest::default()
5361        };
5362        assert!(matches!(
5363            VerifyContext::new()
5364                .external_resources(&resources)
5365                .verify_request(&document, &request),
5366            Err(DsigError::InvalidRequest {
5367                reason: "external resources configured in both context and request"
5368            })
5369        ));
5370    }
5371
5372    fn signature_with_target_reference(signature_value_b64: &str) -> String {
5373        let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5374  <target ID="target">payload</target>
5375  <ds:Signature>
5376    <ds:SignedInfo>
5377      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5378      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5379      <ds:Reference URI="#target">
5380        <ds:Transforms>
5381          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5382        </ds:Transforms>
5383        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
5384        <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
5385      </ds:Reference>
5386    </ds:SignedInfo>
5387    <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
5388  </ds:Signature>
5389</root>"##;
5390
5391        let doc = Document::parse(xml_template).unwrap();
5392        let sig_node = doc
5393            .descendants()
5394            .find(|node| node.is_element() && node.tag_name().name() == "Signature")
5395            .unwrap();
5396        let signed_info_node = sig_node
5397            .children()
5398            .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
5399            .unwrap();
5400        let signed_info = parse_signed_info(signed_info_node).unwrap();
5401        let reference = &signed_info.references[0];
5402        let resolver = UriReferenceResolver::new(&doc);
5403        let initial_data = resolver
5404            .dereference(reference.uri.as_deref().unwrap())
5405            .unwrap();
5406        let pre_digest =
5407            crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
5408                .unwrap();
5409        let digest = compute_digest(reference.digest_method, &pre_digest);
5410        let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
5411        xml_template
5412            .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
5413            .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
5414    }
5415
5416    #[test]
5417    fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
5418        let xml = signature_with_target_reference("AQ==");
5419
5420        let result = VerifyContext::new()
5421            .verify(&xml)
5422            .expect("missing key config must be reported as verification status");
5423        assert!(
5424            matches!(
5425                result.status,
5426                DsigStatus::Invalid(FailureReason::KeyNotFound)
5427            ),
5428            "unexpected status: {:?}",
5429            result.status
5430        );
5431    }
5432
5433    #[test]
5434    fn verify_context_rejects_disallowed_uri() {
5435        let xml = minimal_signature_xml("http://example.com/external", "");
5436        let err = VerifyContext::new()
5437            .key(&RejectingKey)
5438            .verify(&xml)
5439            .expect_err("external URI should be rejected by default policy");
5440        assert!(matches!(
5441            err,
5442            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
5443                operation: "verification",
5444                ..
5445            })
5446        ));
5447    }
5448
5449    #[test]
5450    fn verify_context_bounds_effective_xml_base_components() {
5451        // External URI resolution must stop before repeatedly copying an
5452        // attacker-controlled chain of effective XML Base values.
5453        let mut xml = minimal_signature_xml("payload", "");
5454        for _ in 0..65 {
5455            xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
5456        }
5457        let resources = HashMap::new();
5458        let error = VerifyContext::new()
5459            .key(&AcceptingKey)
5460            .allowed_uri_types(UriTypeSet::ALL)
5461            .external_resources(&resources)
5462            .verify(&xml)
5463            .expect_err("XML Base component work must be bounded before lookup");
5464
5465        assert!(matches!(
5466            error,
5467            SignatureVerificationPipelineError::Policy(
5468                crate::policy::PolicyViolation::ResourceLimit {
5469                    resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
5470                    maximum: 64,
5471                    actual: 65,
5472                }
5473            )
5474        ));
5475    }
5476
5477    #[test]
5478    fn verify_context_bounds_cumulative_xml_base_resolution_bytes() {
5479        // The operation-wide byte budget charges intermediate URI copies, not
5480        // merely the small final external resource returned by the caller map.
5481        let mut xml = minimal_signature_xml("payload", "");
5482        for _ in 0..2 {
5483            xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
5484        }
5485        let resources = HashMap::new();
5486        let mut policy = crate::policy::VerificationPolicy::default();
5487        policy.resources.max_xml_base_resolution_bytes = 32;
5488        let error = VerifyContext::new()
5489            .policy(policy)
5490            .key(&AcceptingKey)
5491            .allowed_uri_types(UriTypeSet::ALL)
5492            .external_resources(&resources)
5493            .verify(&xml)
5494            .expect_err("cumulative XML Base copies must obey the operation budget");
5495
5496        assert!(matches!(
5497            error,
5498            SignatureVerificationPipelineError::Policy(
5499                crate::policy::PolicyViolation::ResourceLimit {
5500                    resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
5501                    maximum: 32,
5502                    ..
5503                }
5504            )
5505        ));
5506    }
5507
5508    #[test]
5509    fn verify_context_applies_xml_base_policy_to_signed_info_c14n() {
5510        // The SignedInfo node-set excludes its ancestors, so C14N 1.1 must
5511        // resolve their inherited xml:base values through the same operation
5512        // budget already used by Reference processing.
5513        let xml = signature_with_target_reference("AQ==")
5514            .replacen(
5515                "http://www.w3.org/2001/10/xml-exc-c14n#",
5516                "http://www.w3.org/2006/12/xml-c14n11",
5517                1,
5518            )
5519            .replace(
5520                "  <ds:Signature>",
5521                "  <outer xml:base=\"one/\"><inner xml:base=\"two/\"><ds:Signature>",
5522            )
5523            .replace("  </ds:Signature>", "  </ds:Signature></inner></outer>");
5524        let policy = crate::policy::VerificationPolicy {
5525            resources: crate::policy::ResourcePolicy {
5526                max_xml_base_components: 1,
5527                ..crate::policy::ResourcePolicy::default()
5528            },
5529            ..crate::policy::VerificationPolicy::default()
5530        };
5531
5532        let error = VerifyContext::new()
5533            .key(&AcceptingKey)
5534            .policy(policy)
5535            .verify(&xml)
5536            .expect_err("SignedInfo C14N must use the operation XML Base budget");
5537
5538        assert!(matches!(
5539            error,
5540            SignatureVerificationPipelineError::Policy(
5541                crate::policy::PolicyViolation::ResourceLimit {
5542                    resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
5543                    maximum: 1,
5544                    actual: 2,
5545                }
5546            )
5547        ));
5548    }
5549
5550    #[test]
5551    fn verify_context_classifies_signed_info_xml_base_byte_limit_as_policy() {
5552        // SignedInfo C14N 1.1 XML Base work is policy enforcement, not a
5553        // malformed canonicalization request, and must retain typed diagnostics.
5554        let xml = signature_with_target_reference("AQ==")
5555            .replacen(
5556                "http://www.w3.org/2001/10/xml-exc-c14n#",
5557                "http://www.w3.org/2006/12/xml-c14n11",
5558                1,
5559            )
5560            .replace(
5561                "  <ds:Signature>",
5562                "  <outer xml:base=\"segment/\"><ds:Signature>",
5563            )
5564            .replace("  </ds:Signature>", "  </ds:Signature></outer>");
5565        let policy = crate::policy::VerificationPolicy {
5566            resources: crate::policy::ResourcePolicy {
5567                max_xml_base_resolution_bytes: 1,
5568                ..crate::policy::ResourcePolicy::default()
5569            },
5570            ..crate::policy::VerificationPolicy::default()
5571        };
5572
5573        let error = VerifyContext::new()
5574            .key(&AcceptingKey)
5575            .policy(policy)
5576            .verify(&xml)
5577            .expect_err("SignedInfo XML Base byte exhaustion must be a policy error");
5578
5579        assert!(matches!(
5580            error,
5581            SignatureVerificationPipelineError::Policy(
5582                crate::policy::PolicyViolation::ResourceLimit {
5583                    resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
5584                    maximum: 1,
5585                    actual,
5586                }
5587            ) if actual > 1
5588        ));
5589    }
5590
5591    #[test]
5592    fn verify_context_meters_repeated_external_dereferences() {
5593        // One caller-owned entry can be referenced repeatedly. The aggregate
5594        // ceiling bounds bytes cloned and processed, not just unique map data.
5595        let payload = b"payload";
5596        let digest = base64::engine::general_purpose::STANDARD.encode(
5597            crate::xmldsig::compute_digest(DigestAlgorithm::Sha1, payload),
5598        );
5599        let reference = format!(
5600            r#"<ds:Reference URI="urn:payload"><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference>"#
5601        );
5602        let xml = format!(
5603            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>"#
5604        );
5605        let resources = HashMap::from([("urn:payload".to_owned(), payload.to_vec())]);
5606        let policy = crate::policy::VerificationPolicy {
5607            uris: crate::policy::UriPolicy {
5608                references: UriTypeSet::ALL,
5609                ..crate::policy::UriPolicy::default()
5610            },
5611            resources: crate::policy::ResourcePolicy {
5612                max_external_resource_bytes: payload.len(),
5613                max_external_resource_total_bytes: payload.len(),
5614                ..crate::policy::ResourcePolicy::default()
5615            },
5616            ..crate::policy::VerificationPolicy::default()
5617        };
5618
5619        let error = VerifyContext::new()
5620            .key(&AcceptingKey)
5621            .policy(policy)
5622            .external_resources(&resources)
5623            .verify(&xml)
5624            .expect_err("the second dereference must exhaust the aggregate byte ceiling");
5625
5626        assert!(
5627            error
5628                .to_string()
5629                .contains("aggregate external resource bytes")
5630        );
5631    }
5632
5633    #[test]
5634    fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
5635        let xml = minimal_signature_xml("", "");
5636        let err = VerifyContext::new()
5637            .key(&RejectingKey)
5638            .allowed_uri_types(UriTypeSet::new(false, true, false))
5639            .verify(&xml)
5640            .expect_err("empty URI must be rejected when empty references are disabled");
5641        assert!(matches!(
5642            err,
5643            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
5644                operation: "verification",
5645                ..
5646            })
5647        ));
5648    }
5649
5650    #[test]
5651    fn verify_context_rejects_disallowed_transform() {
5652        let xml = minimal_signature_xml(
5653            "",
5654            r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
5655        );
5656        let err = VerifyContext::new()
5657            .key(&RejectingKey)
5658            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
5659            .verify(&xml)
5660            .expect_err("enveloped transform should be rejected by allowlist");
5661        assert!(matches!(
5662            err,
5663            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
5664                operation: "verification transform",
5665                ..
5666            })
5667        ));
5668    }
5669
5670    #[test]
5671    fn verify_context_applies_transform_allowlist_to_signed_info_c14n() {
5672        // Reference C14N remains allowlisted; only the distinct SignedInfo
5673        // canonicalization method should trigger this policy rejection.
5674        let xml = signature_with_target_reference("AQ==").replacen(
5675            "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
5676            "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"/>",
5677            1,
5678        );
5679        let error = VerifyContext::new()
5680            .key(&AcceptingKey)
5681            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
5682            .verify(&xml)
5683            .expect_err("SignedInfo C14N must obey the operation transform allowlist");
5684
5685        assert!(matches!(
5686            error,
5687            SignatureVerificationPipelineError::Policy(
5688                crate::policy::PolicyViolation::Algorithm {
5689                    operation: "verification transform",
5690                    ref algorithm,
5691                }
5692            )
5693                if algorithm == "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
5694        ));
5695    }
5696
5697    #[test]
5698    fn verify_context_applies_transform_allowlist_to_key_retrieval() {
5699        // The reference and SignedInfo both use exclusive C14N. The only XPath
5700        // operation is document-selected key retrieval and must be rejected.
5701        let xml = signature_with_target_reference("AQ==")
5702            .replacen(
5703                "</ds:Signature>",
5704                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>"##,
5705                1,
5706            )
5707            .replacen(
5708                "</root>",
5709                r#"<holder ID="keys"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder></root>"#,
5710                1,
5711            );
5712        let error = VerifyContext::new()
5713            .key_resolver(&ConsumingKeyInfoResolver)
5714            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
5715            .verify(&xml)
5716            .expect_err("RetrievalMethod XPath must obey the operation transform allowlist");
5717
5718        assert!(matches!(
5719            error,
5720            SignatureVerificationPipelineError::Policy(
5721                crate::policy::PolicyViolation::Algorithm {
5722                    operation: "verification transform",
5723                    ref algorithm,
5724                }
5725            )
5726                if algorithm == XPATH_TRANSFORM_URI
5727        ));
5728    }
5729
5730    fn signature_with_manifest_xml(valid_manifest_digest: bool) -> String {
5731        signature_with_manifest_xml_with_manifest_mutation(valid_manifest_digest, |xml| xml)
5732    }
5733
5734    fn signature_with_manifest_xml_with_manifest_mutation<F>(
5735        valid_manifest_digest: bool,
5736        mutate_manifest: F,
5737    ) -> String
5738    where
5739        F: FnOnce(String) -> String,
5740    {
5741        const TMP_SIGNED_INFO_DIGEST: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
5742        const INVALID_MANIFEST_DIGEST: &str = "//////////////////////////8=";
5743        let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5744  <target ID="target">payload</target>
5745  <ds:Signature>
5746    <ds:SignedInfo>
5747      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5748      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5749      <ds:Reference URI="#manifest">
5750        <ds:Transforms>
5751          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5752        </ds:Transforms>
5753        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
5754        <ds:DigestValue>SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER</ds:DigestValue>
5755      </ds:Reference>
5756    </ds:SignedInfo>
5757    <ds:SignatureValue>AQ==</ds:SignatureValue>
5758    <ds:Object>
5759      <ds:Manifest ID="manifest">
5760        <ds:Reference URI="#target">
5761          <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
5762          <ds:DigestValue>MANIFEST_DIGEST_PLACEHOLDER</ds:DigestValue>
5763        </ds:Reference>
5764      </ds:Manifest>
5765    </ds:Object>
5766  </ds:Signature>
5767</root>"##;
5768        let seed_xml = xml_template.replace(
5769            "SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER",
5770            TMP_SIGNED_INFO_DIGEST,
5771        );
5772        let doc = Document::parse(&seed_xml).unwrap();
5773        let signature_node = doc
5774            .descendants()
5775            .find(|node| {
5776                node.is_element()
5777                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
5778                    && node.tag_name().name() == "Signature"
5779            })
5780            .unwrap();
5781        let resolver = UriReferenceResolver::new(&doc);
5782        let initial_data = resolver.dereference("#target").unwrap();
5783        let manifest_pre_digest =
5784            crate::xmldsig::execute_transforms(signature_node, initial_data, &[]).unwrap();
5785        let computed_manifest_digest_b64 = base64::engine::general_purpose::STANDARD
5786            .encode(compute_digest(DigestAlgorithm::Sha1, &manifest_pre_digest));
5787        let final_manifest_digest_b64 = if valid_manifest_digest {
5788            computed_manifest_digest_b64.as_str()
5789        } else {
5790            INVALID_MANIFEST_DIGEST
5791        };
5792        let xml_with_manifest_digest = mutate_manifest(
5793            seed_xml.replace("MANIFEST_DIGEST_PLACEHOLDER", final_manifest_digest_b64),
5794        );
5795        let signed_doc = Document::parse(&xml_with_manifest_digest).unwrap();
5796        let signed_signature_node = signed_doc
5797            .descendants()
5798            .find(|node| {
5799                node.is_element()
5800                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
5801                    && node.tag_name().name() == "Signature"
5802            })
5803            .unwrap();
5804        let signed_info_node = signed_signature_node
5805            .children()
5806            .find(|node| {
5807                node.is_element()
5808                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
5809                    && node.tag_name().name() == "SignedInfo"
5810            })
5811            .unwrap();
5812        let signed_info = parse_signed_info(signed_info_node).unwrap();
5813        let object_reference = &signed_info.references[0];
5814        let signed_resolver = UriReferenceResolver::new(&signed_doc);
5815        let signed_initial_data = signed_resolver
5816            .dereference(object_reference.uri.as_deref().unwrap())
5817            .unwrap();
5818        let signed_pre_digest = crate::xmldsig::execute_transforms(
5819            signed_signature_node,
5820            signed_initial_data,
5821            &object_reference.transforms,
5822        )
5823        .unwrap();
5824        let signed_digest_b64 = base64::engine::general_purpose::STANDARD.encode(compute_digest(
5825            object_reference.digest_method,
5826            &signed_pre_digest,
5827        ));
5828
5829        xml_with_manifest_digest.replacen(TMP_SIGNED_INFO_DIGEST, &signed_digest_b64, 1)
5830    }
5831
5832    fn replace_fixture_manifest_digest(xml: &str, replacement: &str) -> String {
5833        let object_marker = "<ds:Object>";
5834        let object_start = xml
5835            .find(object_marker)
5836            .expect("fixture should contain ds:Object")
5837            + object_marker.len();
5838        let open = "<ds:DigestValue>";
5839        let close = "</ds:DigestValue>";
5840        let value_start = xml[object_start..]
5841            .find(open)
5842            .map(|offset| object_start + offset + open.len())
5843            .expect("Manifest should contain DigestValue");
5844        let value_end = xml[value_start..]
5845            .find(close)
5846            .map(|offset| value_start + offset)
5847            .expect("Manifest DigestValue must be closed");
5848
5849        format!("{}{replacement}{}", &xml[..value_start], &xml[value_end..])
5850    }
5851
5852    #[test]
5853    fn verify_context_processes_manifest_references_when_enabled() {
5854        let xml = signature_with_manifest_xml(true);
5855
5856        let result_without_manifests = VerifyContext::new()
5857            .key(&RejectingKey)
5858            .verify(&xml)
5859            .expect("manifest processing disabled should still verify SignedInfo");
5860        assert!(
5861            result_without_manifests.manifest_references.is_empty(),
5862            "manifest results must stay empty when manifest processing is disabled",
5863        );
5864        assert!(matches!(
5865            result_without_manifests.status,
5866            DsigStatus::Invalid(FailureReason::SignatureMismatch)
5867        ));
5868
5869        let malformed_manifest_xml = signature_with_manifest_xml(true).replacen(
5870            "</ds:Object>",
5871            "</ds:Object><ds:Object><ds:Manifest><ds:Foo/></ds:Manifest></ds:Object>",
5872            1,
5873        );
5874        let malformed_with_manifests_disabled = VerifyContext::new()
5875            .key(&RejectingKey)
5876            .verify(&malformed_manifest_xml)
5877            .expect("malformed Manifest must be ignored when manifest processing is disabled");
5878        assert!(
5879            malformed_with_manifests_disabled
5880                .manifest_references
5881                .is_empty(),
5882            "manifest parser must not run when process_manifests is disabled",
5883        );
5884        assert!(matches!(
5885            malformed_with_manifests_disabled.status,
5886            DsigStatus::Invalid(FailureReason::SignatureMismatch)
5887        ));
5888
5889        let result_with_manifests = VerifyContext::new()
5890            .key(&AcceptingKey)
5891            .process_manifests(true)
5892            .verify(&xml)
5893            .expect("manifest references should be processed when enabled");
5894        assert_eq!(result_with_manifests.manifest_references.len(), 1);
5895        assert_eq!(
5896            result_with_manifests.manifest_references[0].reference_set,
5897            ReferenceSet::Manifest
5898        );
5899        assert_eq!(
5900            result_with_manifests.manifest_references[0].reference_index,
5901            0
5902        );
5903        assert!(matches!(
5904            result_with_manifests.manifest_references[0].status,
5905            DsigStatus::Valid
5906        ));
5907        assert!(matches!(result_with_manifests.status, DsigStatus::Valid));
5908    }
5909
5910    #[test]
5911    fn verify_context_skips_manifest_work_when_signature_value_is_invalid() {
5912        // SignedInfo authenticates the Manifest bytes only after SignatureValue
5913        // succeeds. Malformed nested content must not consume parsing work when
5914        // the cryptographic signature itself is invalid.
5915        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5916            replace_fixture_manifest_digest(&xml, "!!!")
5917        });
5918        assert!(
5919            xml.split_once("<ds:Object>")
5920                .is_some_and(|(_, object)| object.contains("<ds:DigestValue>!!!</ds:DigestValue>")),
5921            "fixture mutation must corrupt the nested Manifest DigestValue",
5922        );
5923
5924        let result = VerifyContext::new()
5925            .key(&RejectingKey)
5926            .process_manifests(true)
5927            .verify(&xml)
5928            .expect("invalid SignatureValue must short-circuit Manifest parsing");
5929
5930        assert!(matches!(
5931            result.status,
5932            DsigStatus::Invalid(FailureReason::SignatureMismatch)
5933        ));
5934        assert!(result.manifest_references.is_empty());
5935    }
5936
5937    #[test]
5938    fn verify_context_shares_xpath_parse_budget_with_manifest_references() {
5939        // SignedInfo and every Manifest form one attacker-controlled parse unit:
5940        // splitting expressions across Reference sets must not reset the ceiling.
5941        let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
5942            .repeat(64);
5943        let transform = format!(
5944            r#"<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</ds:Transform>"#
5945        );
5946        let max_transforms = transform.repeat(16);
5947        let max_manifest_reference = format!(
5948            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>"##
5949        );
5950        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5951            xml.replacen(
5952                r##"<ds:Reference URI="#target">"##,
5953                &format!(
5954                    r##"<ds:Reference URI="#target"><ds:Transforms>{}</ds:Transforms>"##,
5955                    max_transforms
5956                ),
5957                1,
5958            )
5959            .replacen(
5960                "</ds:SignedInfo>",
5961                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>"##,
5962                1,
5963            )
5964            .replacen(
5965                "</ds:Manifest>",
5966                &format!("{}</ds:Manifest>", max_manifest_reference.repeat(3)),
5967                1,
5968            )
5969        });
5970
5971        let error = VerifyContext::new()
5972            .key(&AcceptingKey)
5973            .process_manifests(true)
5974            .verify(&xml)
5975            .expect_err("SignedInfo and Manifest References must share one XPath parse budget");
5976
5977        assert!(
5978            matches!(
5979                &error,
5980                SignatureVerificationPipelineError::Policy(
5981                    crate::policy::PolicyViolation::ResourceLimit {
5982                        resource: "XPath expressions",
5983                        ..
5984                    }
5985                )
5986            ),
5987            "unexpected error: {error:?}"
5988        );
5989    }
5990
5991    #[test]
5992    fn verify_context_processes_manifest_when_signedinfo_references_object() {
5993        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
5994            xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
5995                .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
5996                .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
5997        });
5998
5999        let result = VerifyContext::new()
6000            .key(&AcceptingKey)
6001            .process_manifests(true)
6002            .verify(&xml)
6003            .expect("manifest references should be processed when SignedInfo references ds:Object");
6004        assert_eq!(
6005            result.manifest_references.len(),
6006            1,
6007            "signed ds:Object should enable processing of its direct-child ds:Manifest",
6008        );
6009        assert_eq!(
6010            result.manifest_references[0].reference_set,
6011            ReferenceSet::Manifest
6012        );
6013        assert_eq!(result.manifest_references[0].reference_index, 0);
6014        assert!(matches!(
6015            result.manifest_references[0].status,
6016            DsigStatus::Valid
6017        ));
6018    }
6019
6020    #[test]
6021    fn verify_context_skips_manifest_removed_by_enveloped_transform() {
6022        // The owning Signature contains both eligible ID targets. Subtracting
6023        // its subtree therefore removes every target node from the digest input,
6024        // so neither form authenticates the Manifest structure for processing.
6025        for target_object in [false, true] {
6026            let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6027                let xml = xml.replacen(
6028                    r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
6029                    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#"/>"#,
6030                    1,
6031                );
6032                if target_object {
6033                    xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
6034                        .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
6035                        .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
6036                } else {
6037                    xml
6038                }
6039            });
6040
6041            let result = VerifyContext::new()
6042                .key(&AcceptingKey)
6043                .process_manifests(true)
6044                .store_pre_digest(true)
6045                .verify(&xml)
6046                .expect("an emptied reference remains a valid core digest input");
6047
6048            assert!(matches!(result.status, DsigStatus::Valid));
6049            assert_eq!(
6050                result.signed_info_references[0].pre_digest_data.as_deref(),
6051                Some([].as_slice()),
6052                "target_object={target_object} must have empty transformed bytes",
6053            );
6054            assert!(
6055                result.manifest_references.is_empty(),
6056                "target_object={target_object} must not authenticate the Manifest",
6057            );
6058        }
6059    }
6060
6061    #[test]
6062    fn verify_context_ignores_manifest_excluded_from_signed_object() {
6063        // A Reference URI authenticates only its post-transform bytes. Excluding
6064        // the Manifest subtree must not let its independently valid digest chain
6065        // masquerade as data authenticated by SignedInfo.
6066        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6067            xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
6068                .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
6069                .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
6070                .replacen(
6071                    r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
6072                    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#"/>"#,
6073                    1,
6074                )
6075        });
6076
6077        let result = VerifyContext::new()
6078            .key(&AcceptingKey)
6079            .process_manifests(true)
6080            .verify(&xml)
6081            .expect("excluded Manifest content must be ignored, not parsed");
6082
6083        assert!(matches!(result.status, DsigStatus::Valid));
6084        assert!(
6085            result.manifest_references.is_empty(),
6086            "a transform-excluded Manifest is not authenticated by SignedInfo",
6087        );
6088    }
6089
6090    #[test]
6091    fn verify_context_skips_manifest_digest_work_when_signature_is_invalid() {
6092        let xml = signature_with_manifest_xml(false);
6093        let result = VerifyContext::new()
6094            .key(&RejectingKey)
6095            .process_manifests(true)
6096            .verify(&xml)
6097            .expect("invalid SignatureValue must short-circuit Manifest digest work");
6098        assert!(result.manifest_references.is_empty());
6099        assert!(matches!(
6100            result.status,
6101            DsigStatus::Invalid(FailureReason::SignatureMismatch)
6102        ));
6103    }
6104
6105    #[test]
6106    fn verify_context_manifest_digest_mismatch_is_non_fatal_with_accepting_key() {
6107        let xml = signature_with_manifest_xml(false);
6108        let result = VerifyContext::new()
6109            .key(&AcceptingKey)
6110            .process_manifests(true)
6111            .verify(&xml)
6112            .expect("manifest digest mismatches should be recorded while signature stays valid");
6113        assert_eq!(result.manifest_references.len(), 1);
6114        assert!(matches!(
6115            result.manifest_references[0].status,
6116            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
6117        ));
6118        assert!(matches!(result.status, DsigStatus::Valid));
6119    }
6120
6121    #[test]
6122    fn invalid_outer_manifest_digest_does_not_parse_nested_manifest() {
6123        // The malformed inner Manifest is not authenticated until its parent
6124        // Reference digest succeeds. A mismatch must therefore stop before
6125        // parsing or charging work for the nested structure.
6126        let xml = signature_with_manifest_xml_with_manifest_mutation(false, |xml| {
6127            xml.replacen("URI=\"#target\"", "URI=\"#inner-object\"", 1)
6128                .replacen(
6129                    "</ds:Object>",
6130                    "</ds:Object><ds:Object ID=\"inner-object\"><ds:Manifest>junk<ds:Unexpected/></ds:Manifest></ds:Object>",
6131                    1,
6132                )
6133        });
6134
6135        let result = VerifyContext::new()
6136            .key(&AcceptingKey)
6137            .process_manifests(true)
6138            .verify(&xml)
6139            .expect("an unauthenticated nested Manifest must remain opaque");
6140
6141        assert_eq!(result.manifest_references.len(), 1);
6142        assert!(matches!(
6143            result.manifest_references[0].status,
6144            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
6145        ));
6146    }
6147
6148    #[test]
6149    fn verify_context_skips_manifest_parsing_when_signedinfo_reference_fails() {
6150        // Manifest content is not authenticated after a SignedInfo reference
6151        // failure, so parsing it would spend work on untrusted nested input.
6152        let xml = signature_with_manifest_xml(true);
6153        let (signed_info_prefix, object_suffix) = xml
6154            .split_once("<ds:Object>")
6155            .expect("fixture should contain ds:Object");
6156        let open = "<ds:DigestValue>";
6157        let close = "</ds:DigestValue>";
6158        let digest_start = signed_info_prefix
6159            .find(open)
6160            .expect("SignedInfo should contain DigestValue");
6161        let digest_end = signed_info_prefix[digest_start + open.len()..]
6162            .find(close)
6163            .map(|offset| digest_start + open.len() + offset)
6164            .expect("SignedInfo DigestValue must be closed");
6165        let broken_signed_info_prefix = format!(
6166            "{}{}AAAAAAAAAAAAAAAAAAAAAAAAAAA={}{}",
6167            &signed_info_prefix[..digest_start],
6168            open,
6169            close,
6170            &signed_info_prefix[digest_end + close.len()..],
6171        );
6172        let broken_xml = format!("{broken_signed_info_prefix}<ds:Object>{object_suffix}");
6173        let result = VerifyContext::new()
6174            .key(&RejectingKey)
6175            .process_manifests(true)
6176            .verify(&broken_xml)
6177            .expect("SignedInfo digest failure should return without parsing Manifests");
6178        assert!(matches!(
6179            result.status,
6180            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
6181        ));
6182        assert!(
6183            result.manifest_references.is_empty(),
6184            "unauthenticated Manifest content must not be parsed",
6185        );
6186    }
6187
6188    #[test]
6189    fn verify_context_skips_manifest_policy_work_when_signature_is_invalid() {
6190        // A digest-valid SignedInfo reference does not authenticate Manifest
6191        // policy inputs until SignatureValue also succeeds.
6192        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6193            xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
6194        });
6195        let result = VerifyContext::new()
6196            .key(&RejectingKey)
6197            .process_manifests(true)
6198            .verify(&broken_xml)
6199            .expect("invalid SignatureValue must short-circuit Manifest policy work");
6200        assert!(result.manifest_references.is_empty());
6201        assert!(matches!(
6202            result.status,
6203            DsigStatus::Invalid(FailureReason::SignatureMismatch)
6204        ));
6205    }
6206
6207    #[test]
6208    fn verify_context_records_manifest_policy_violations_with_accepting_key() {
6209        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6210            xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
6211        });
6212        let result = VerifyContext::new()
6213            .key(&AcceptingKey)
6214            .process_manifests(true)
6215            .verify(&broken_xml)
6216            .expect("manifest policy violations should be recorded while signature stays valid");
6217        assert_eq!(result.manifest_references.len(), 1);
6218        assert!(matches!(
6219            result.manifest_references[0].status,
6220            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
6221        ));
6222        assert!(matches!(result.status, DsigStatus::Valid));
6223    }
6224
6225    #[test]
6226    fn verify_context_applies_digest_policy_to_manifest_references() {
6227        // Manifest results are authenticated extension data and must obey the
6228        // same digest allowlist as SignedInfo references.
6229        let policy = crate::policy::VerificationPolicy {
6230            manifest_processing: crate::policy::ManifestProcessing::Process,
6231            digest_algorithms: Some(HashSet::from([DigestAlgorithm::Sha1])),
6232            ..crate::policy::VerificationPolicy::default()
6233        };
6234        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
6235            let legacy = "http://www.w3.org/2000/09/xmldsig#sha1";
6236            let offset = xml
6237                .rfind(legacy)
6238                .expect("Manifest DigestMethod must be present");
6239            xml.replace_range(offset..offset + legacy.len(), DigestAlgorithm::Sha256.uri());
6240            let value_start = xml[offset..]
6241                .find("<ds:DigestValue>")
6242                .map(|relative| offset + relative + "<ds:DigestValue>".len())
6243                .expect("Manifest DigestValue must be present");
6244            let value_end = xml[value_start..]
6245                .find("</ds:DigestValue>")
6246                .map(|relative| value_start + relative)
6247                .expect("Manifest DigestValue must be closed");
6248            xml.replace_range(
6249                value_start..value_end,
6250                &base64::engine::general_purpose::STANDARD.encode([0_u8; 32]),
6251            );
6252            xml
6253        });
6254        let result = VerifyContext::new()
6255            .key(&AcceptingKey)
6256            .policy(policy)
6257            .verify(&xml)
6258            .expect("a disallowed Manifest digest is a per-reference result");
6259
6260        assert!(matches!(result.status, DsigStatus::Valid));
6261        assert!(matches!(
6262            result.manifest_references[0].status,
6263            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
6264        ));
6265    }
6266
6267    #[test]
6268    fn verify_context_applies_transform_count_policy_to_manifest_references() {
6269        // Authenticated Manifest references share the caller's per-reference
6270        // transform ceiling and fail before transform execution when exceeded.
6271        let policy = crate::policy::VerificationPolicy {
6272            manifest_processing: crate::policy::ManifestProcessing::Process,
6273            resources: crate::policy::ResourcePolicy {
6274                max_transforms_per_reference: 1,
6275                ..crate::policy::ResourcePolicy::default()
6276            },
6277            ..crate::policy::VerificationPolicy::default()
6278        };
6279        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
6280            let manifest_start = xml
6281                .find("<ds:Manifest")
6282                .expect("fixture must contain a Manifest");
6283            let manifest = xml[manifest_start..].replacen(
6284                "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
6285                concat!(
6286                    "<ds:Transforms>",
6287                    "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
6288                    "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
6289                    "</ds:Transforms>",
6290                    "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>"
6291                ),
6292                1,
6293            );
6294            xml.replace_range(manifest_start.., &manifest);
6295            xml
6296        });
6297        let result = VerifyContext::new()
6298            .key(&AcceptingKey)
6299            .policy(policy)
6300            .verify(&xml)
6301            .expect("Manifest transform policy is a per-reference result");
6302
6303        assert!(matches!(result.status, DsigStatus::Valid));
6304        assert!(matches!(
6305            result.manifest_references[0].status,
6306            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
6307        ));
6308    }
6309
6310    #[test]
6311    fn verify_context_skips_manifest_uri_work_when_signature_is_invalid() {
6312        // Missing Manifest URIs remain unauthenticated until SignatureValue
6313        // succeeds, so they cannot trigger Manifest policy processing here.
6314        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6315            xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
6316        });
6317
6318        let result = VerifyContext::new()
6319            .key(&RejectingKey)
6320            .process_manifests(true)
6321            .verify(&broken_xml)
6322            .expect("invalid SignatureValue must short-circuit Manifest URI processing");
6323        assert!(result.manifest_references.is_empty());
6324        assert!(matches!(
6325            result.status,
6326            DsigStatus::Invalid(FailureReason::SignatureMismatch)
6327        ));
6328    }
6329
6330    #[test]
6331    fn verify_context_records_manifest_missing_uri_with_accepting_key() {
6332        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6333            xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
6334        });
6335
6336        let result = VerifyContext::new()
6337            .key(&AcceptingKey)
6338            .process_manifests(true)
6339            .verify(&broken_xml)
6340            .expect("manifest missing URI should be recorded while signature stays valid");
6341        assert_eq!(result.manifest_references.len(), 1);
6342        assert_eq!(result.manifest_references[0].uri, "<omitted>");
6343        assert!(matches!(
6344            result.manifest_references[0].status,
6345            DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
6346        ));
6347        assert!(matches!(result.status, DsigStatus::Valid));
6348    }
6349
6350    #[test]
6351    fn verify_context_ignores_nested_manifests_in_object() {
6352        // A digest-valid Manifest below a wrapper is outside the strict direct-
6353        // child processing profile and must not appear in diagnostics.
6354        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6355            xml.replacen(
6356                "<ds:Manifest ID=\"manifest\">",
6357                "<wrapper><ds:Manifest ID=\"manifest\">",
6358                1,
6359            )
6360            .replacen("</ds:Manifest>", "</ds:Manifest></wrapper>", 1)
6361        });
6362
6363        let result = VerifyContext::new()
6364            .key(&AcceptingKey)
6365            .process_manifests(true)
6366            .verify(&xml)
6367            .expect("nested Manifest nodes are ignored in strict mode");
6368        assert!(
6369            result.manifest_references.is_empty(),
6370            "only direct ds:Manifest children of ds:Object must be processed"
6371        );
6372        assert!(matches!(result.status, DsigStatus::Valid));
6373    }
6374
6375    #[test]
6376    fn verify_context_reports_manifest_reference_parse_errors_explicitly() {
6377        // Malformed nested DigestValue is parsed only after the enclosing
6378        // Manifest structure has been authenticated by SignedInfo.
6379        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6380            replace_fixture_manifest_digest(&xml, "!!!")
6381        });
6382
6383        let err = VerifyContext::new()
6384            .key(&AcceptingKey)
6385            .process_manifests(true)
6386            .verify(&broken_xml)
6387            .expect_err("invalid Manifest DigestValue must map to ParseManifestReference");
6388        assert!(matches!(
6389            err,
6390            SignatureVerificationPipelineError::ParseManifestReference(_)
6391        ));
6392    }
6393
6394    #[test]
6395    fn verify_context_reports_unsupported_manifest_transform_with_declared_digest() {
6396        // Unsupported optional Manifest transforms do not invalidate core
6397        // SignedInfo, but their result must preserve the declared digest method.
6398        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
6399            let xml = xml.replacen(
6400                "<ds:Reference URI=\"#target\">",
6401                "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
6402                1,
6403            );
6404            let xml = xml.replacen(
6405                "</ds:Transforms>\n          <ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
6406                "</ds:Transforms>\n          <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>",
6407                1,
6408            );
6409            replace_fixture_manifest_digest(&xml, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
6410        });
6411        assert!(xml.contains("urn:unsupported"));
6412        assert!(xml.contains("http://www.w3.org/2001/04/xmlenc#sha256"));
6413
6414        let result = VerifyContext::new()
6415            .key(&AcceptingKey)
6416            .process_manifests(true)
6417            .verify(&xml)
6418            .expect("unsupported Manifest transform is a per-reference result");
6419        assert_eq!(result.status, DsigStatus::Valid);
6420        assert_eq!(result.manifest_references.len(), 1);
6421        assert_eq!(
6422            result.manifest_references[0].digest_algorithm,
6423            DigestAlgorithm::Sha256
6424        );
6425        assert!(matches!(
6426            result.manifest_references[0].status,
6427            DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
6428        ));
6429
6430        let restricted = VerifyContext::new()
6431            .key(&AcceptingKey)
6432            .process_manifests(true)
6433            .allowed_transforms([
6434                DEFAULT_IMPLICIT_C14N_URI,
6435                "http://www.w3.org/2001/10/xml-exc-c14n#",
6436            ])
6437            .verify(&xml)
6438            .expect("a disallowed Manifest transform is a per-reference policy result");
6439        assert!(matches!(
6440            restricted.manifest_references[0].status,
6441            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
6442        ));
6443    }
6444
6445    #[test]
6446    fn manifest_reference_limit_counts_unsupported_entries() {
6447        let references = (0..=MAX_REFERENCES_PER_SIGNATURE)
6448            .map(|index| {
6449                format!(
6450                    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>"##
6451                )
6452            })
6453            .collect::<String>();
6454        let xml = format!(
6455            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>"#
6456        );
6457        let document = XmlDocument::parse(xml).unwrap();
6458        let error = document.with_view(|view| {
6459            let signature = view.document().root_element();
6460            let object = signature.children().find(|node| node.is_element()).unwrap();
6461            let policy = crate::policy::VerificationPolicy::default();
6462            let budgets = VerificationOperationBudgets::with_transforms(
6463                &policy,
6464                TransformExecutionBudget::from_resources(&policy.resources),
6465            );
6466            let operation = OperationExecutionContext::new(
6467                &policy,
6468                &budgets,
6469                Some((view.identity(), view.generation())),
6470            );
6471            operation.authenticate(view.node_identity(object));
6472            let mut processed = HashSet::new();
6473            let mut remaining = MAX_REFERENCES_PER_SIGNATURE;
6474            let mut next_index = 0;
6475            let mut xpath_parse = XPathSignatureParseBudget::default();
6476            let mut state = ManifestDiscoveryState {
6477                processed: &mut processed,
6478                remaining_capacity: &mut remaining,
6479                next_reference_index: &mut next_index,
6480                xpath_parse: &mut xpath_parse,
6481            };
6482            match parse_manifest_references(signature, &operation, view, &mut state, None) {
6483                Ok(_) => panic!("unsupported references must consume the same aggregate limit"),
6484                Err(error) => error,
6485            }
6486        });
6487        assert!(matches!(
6488            error,
6489            SignatureVerificationPipelineError::InvalidStructure {
6490                reason: "signed Manifests exceed the per-signature Reference limit"
6491            }
6492        ));
6493    }
6494
6495    #[test]
6496    fn unsigned_manifest_remains_eligible_after_trust_expands() {
6497        // The second Object is not authenticated during the first discovery
6498        // pass. It must remain unprocessed so a valid reference from the first
6499        // Manifest can make its sibling Manifest eligible on the next pass.
6500        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
6501        let xml = format!(
6502            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>"##
6503        );
6504        let document = XmlDocument::parse(xml).expect("nested Manifest fixture must parse");
6505        document.with_view(|view| {
6506            let signature = view.document().root_element();
6507            let mut objects = signature.children().filter(|node| node.is_element());
6508            let outer = objects.next().expect("outer Object");
6509            let inner = objects.next().expect("inner Object");
6510            let policy = crate::policy::VerificationPolicy::default();
6511            let budgets = VerificationOperationBudgets::with_transforms(
6512                &policy,
6513                TransformExecutionBudget::from_resources(&policy.resources),
6514            );
6515            let operation = OperationExecutionContext::new(
6516                &policy,
6517                &budgets,
6518                Some((view.identity(), view.generation())),
6519            );
6520            operation.authenticate(view.node_identity(outer));
6521            let mut processed = HashSet::new();
6522            let mut remaining = 2;
6523            let mut next_index = 0;
6524            let mut xpath_budget = XPathSignatureParseBudget::default();
6525
6526            let first = parse_manifest_references(
6527                signature,
6528                &operation,
6529                view,
6530                &mut ManifestDiscoveryState {
6531                    processed: &mut processed,
6532                    remaining_capacity: &mut remaining,
6533                    next_reference_index: &mut next_index,
6534                    xpath_parse: &mut xpath_budget,
6535                },
6536                None,
6537            )
6538            .expect("outer Manifest must be discovered");
6539            assert_eq!(first.references.len(), 1);
6540            assert_eq!(first.references[0].reference.uri.as_deref(), Some("#inner"));
6541
6542            operation.authenticate(view.node_identity(inner));
6543            let second = parse_manifest_references(
6544                signature,
6545                &operation,
6546                view,
6547                &mut ManifestDiscoveryState {
6548                    processed: &mut processed,
6549                    remaining_capacity: &mut remaining,
6550                    next_reference_index: &mut next_index,
6551                    xpath_parse: &mut xpath_budget,
6552                },
6553                None,
6554            )
6555            .expect("newly authenticated sibling Manifest must remain eligible");
6556            assert_eq!(second.references.len(), 1);
6557            assert_eq!(
6558                second.references[0].reference.uri.as_deref(),
6559                Some("#payload")
6560            );
6561        });
6562    }
6563
6564    #[test]
6565    fn manifest_reference_limit_includes_signed_info_references() {
6566        // The per-signature ceiling is shared by core and authenticated
6567        // Manifest references; enabling Manifest processing must not reset it.
6568        let xml = signature_with_manifest_xml(true);
6569        let reference_start = xml
6570            .find(r##"<ds:Reference URI="#manifest">"##)
6571            .expect("fixture SignedInfo must reference the Manifest");
6572        let reference_end = xml[reference_start..]
6573            .find("</ds:Reference>")
6574            .map(|offset| reference_start + offset + "</ds:Reference>".len())
6575            .expect("fixture SignedInfo Reference must be closed");
6576        let repeated = xml[reference_start..reference_end].repeat(MAX_REFERENCES_PER_SIGNATURE);
6577        let xml = format!(
6578            "{}{repeated}{}",
6579            &xml[..reference_start],
6580            &xml[reference_end..]
6581        );
6582
6583        let error = VerifyContext::new()
6584            .key(&AcceptingKey)
6585            .process_manifests(true)
6586            .verify(&xml)
6587            .expect_err("one Manifest Reference must exceed the exhausted signature-wide limit");
6588
6589        assert!(matches!(
6590            error,
6591            SignatureVerificationPipelineError::InvalidStructure {
6592                reason: "signed Manifests exceed the per-signature Reference limit"
6593            }
6594        ));
6595    }
6596
6597    #[test]
6598    fn configured_reference_limit_is_shared_with_manifests() {
6599        // Lowering the operation policy must lower the aggregate SignedInfo and
6600        // Manifest capacity rather than falling back to the crate hard limit.
6601        let policy = crate::policy::VerificationPolicy {
6602            manifest_processing: crate::policy::ManifestProcessing::Process,
6603            resources: crate::policy::ResourcePolicy {
6604                max_references: 1,
6605                ..crate::policy::ResourcePolicy::default()
6606            },
6607            ..crate::policy::VerificationPolicy::default()
6608        };
6609
6610        let error = VerifyContext::new()
6611            .key(&AcceptingKey)
6612            .policy(policy)
6613            .verify(&signature_with_manifest_xml(true))
6614            .expect_err("Manifest must exceed the caller-selected aggregate limit");
6615        assert!(matches!(
6616            error,
6617            SignatureVerificationPipelineError::InvalidStructure {
6618                reason: "signed Manifests exceed the per-signature Reference limit"
6619            }
6620        ));
6621    }
6622
6623    #[test]
6624    fn retrieval_method_materializes_single_x509_data_subtree() {
6625        for uri in [
6626            "#target",
6627            "#xpointer(id('target'))",
6628            "#xpointer(id(&quot;target&quot;))",
6629        ] {
6630            for target_xml in [
6631                r#"<ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>"#,
6632                r#"<holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>"#,
6633            ] {
6634                let xml = format!(
6635                    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>"#
6636                );
6637                let document = Document::parse(&xml).unwrap();
6638                let key_info_node = document
6639                    .descendants()
6640                    .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
6641                    .unwrap();
6642                let mut key_info = parse_key_info(key_info_node).unwrap();
6643                let resolver = UriReferenceResolver::new(&document);
6644
6645                materialize_retrieval_methods(
6646                    &mut key_info,
6647                    &resolver,
6648                    UriTypeSet::SAME_DOCUMENT,
6649                    None,
6650                    crate::provider::default_provider(),
6651                )
6652                .expect("XPath filter must produce one X509Data-rooted node-set");
6653                assert!(matches!(
6654                    key_info.sources.as_slice(),
6655                    [super::super::parse::KeyInfoSource::X509Data(info)]
6656                        if info.subject_names == ["CN=leaf"]
6657                ));
6658            }
6659        }
6660    }
6661
6662    fn retrieval_method_xpath_signature() -> String {
6663        format!(
6664            r##"<root xmlns:ds="{XMLDSIG_NS}">
6665              <payload Id="payload">ok</payload>
6666              <ds:Signature>
6667                <ds:SignedInfo>
6668                  <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6669                  <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6670                  <ds:Reference URI="#payload">
6671                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6672                    <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6673                  </ds:Reference>
6674                </ds:SignedInfo>
6675                <ds:SignatureValue>AQ==</ds:SignatureValue>
6676                <ds:KeyInfo>
6677                  <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
6678                    <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
6679                      <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
6680                    </ds:Transform></ds:Transforms>
6681                  </ds:RetrievalMethod>
6682                </ds:KeyInfo>
6683              </ds:Signature>
6684              <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
6685            </root>"##
6686        )
6687    }
6688
6689    #[test]
6690    fn retrieval_method_xpath_uses_signature_expression_budget() {
6691        // RetrievalMethod XPath belongs to the same untrusted Signature as
6692        // Reference XPath and must not receive a separate parse allowance.
6693        let mut policy = crate::policy::VerificationPolicy::default();
6694        policy.resources.max_xpath_expressions = 0;
6695
6696        let error = VerifyContext::new()
6697            .policy(policy)
6698            .verify(&retrieval_method_xpath_signature())
6699            .expect_err("RetrievalMethod XPath must consume the signature parse budget");
6700
6701        assert!(
6702            matches!(
6703                &error,
6704                SignatureVerificationPipelineError::Policy(
6705                    crate::policy::PolicyViolation::ResourceLimit {
6706                        resource: "XPath expressions",
6707                        maximum: 0,
6708                        actual: 1,
6709                    }
6710                )
6711            ),
6712            "unexpected error: {error:?}"
6713        );
6714    }
6715
6716    #[test]
6717    fn retrieval_method_xpath_obeys_expression_byte_limit() {
6718        // Edge whitespace is semantically harmless for this restricted shape,
6719        // but its raw untrusted bytes still belong to the operation budget.
6720        let expression = "ancestor-or-self::ds:X509Data";
6721        let padded_expression = format!("  {expression}  ");
6722        let xml = retrieval_method_xpath_signature().replace(expression, &padded_expression);
6723        let mut policy = crate::policy::VerificationPolicy::default();
6724        policy.resources.max_xpath_expression_bytes = expression.len();
6725
6726        let error = VerifyContext::new()
6727            .policy(policy)
6728            .verify(&xml)
6729            .expect_err("RetrievalMethod XPath must obey the expression byte limit");
6730
6731        assert!(
6732            matches!(
6733                &error,
6734                SignatureVerificationPipelineError::Policy(
6735                    crate::policy::PolicyViolation::ResourceLimit {
6736                        resource: "XPath expression bytes",
6737                        maximum,
6738                        actual,
6739                    }
6740                ) if *maximum == expression.len() && *actual == padded_expression.len()
6741            ),
6742            "unexpected error: {error:?}"
6743        );
6744    }
6745
6746    #[test]
6747    fn retrieval_method_xpath_obeys_expression_complexity_limit() {
6748        // Recognizing a fixed safe predicate must not bypass the common XPath
6749        // complexity policy applied to every expression in the signature.
6750        let mut policy = crate::policy::VerificationPolicy::default();
6751        policy.resources.max_xpath_expression_complexity = 0;
6752
6753        let error = VerifyContext::new()
6754            .policy(policy)
6755            .verify(&retrieval_method_xpath_signature())
6756            .expect_err("RetrievalMethod XPath must obey the complexity limit");
6757
6758        assert!(
6759            matches!(
6760                &error,
6761                SignatureVerificationPipelineError::Policy(
6762                    crate::policy::PolicyViolation::ResourceLimit {
6763                        resource: "XPath expression complexity",
6764                        maximum: 0,
6765                        actual,
6766                    }
6767                ) if *actual > 0
6768            ),
6769            "unexpected error: {error:?}"
6770        );
6771    }
6772
6773    #[test]
6774    fn retrieval_method_xpath_uses_node_filter_work_budget() {
6775        // Attribute and namespace XPath nodes participate in filtering even
6776        // though the optimized X509Data locator scans only tree descendants.
6777        let mut policy = crate::policy::VerificationPolicy::default();
6778        policy.resources.max_node_set_filter_work = 4;
6779        let xml = retrieval_method_xpath_signature().replace(
6780            "<holder Id=\"target\">",
6781            "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
6782        );
6783
6784        let error = VerifyContext::new()
6785            .policy(policy)
6786            .verify(&xml)
6787            .expect_err("RetrievalMethod XPath must consume node-filter work");
6788
6789        assert!(
6790            matches!(
6791                &error,
6792                SignatureVerificationPipelineError::Policy(
6793                    crate::policy::PolicyViolation::ResourceLimit {
6794                        resource: crate::policy::resource_name::NODE_SET_FILTER_WORK,
6795                        maximum: 4,
6796                        actual,
6797                    }
6798                ) if *actual > 4
6799            ),
6800            "unexpected error: {error:?}"
6801        );
6802    }
6803
6804    #[test]
6805    fn retrieval_method_xpath_charges_every_context_to_evaluation_work() {
6806        // The optimized predicate avoids a generic XPath engine, but every
6807        // attribute and namespace context still consumes evaluation work.
6808        let mut policy = crate::policy::VerificationPolicy::default();
6809        policy.resources.max_xpath_evaluation_work = 4;
6810        let xml = retrieval_method_xpath_signature().replace(
6811            "<holder Id=\"target\">",
6812            "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
6813        );
6814
6815        let error = VerifyContext::new()
6816            .policy(policy)
6817            .verify(&xml)
6818            .expect_err("RetrievalMethod XPath must charge every evaluation context");
6819
6820        assert!(matches!(
6821            error,
6822            SignatureVerificationPipelineError::Policy(
6823                crate::policy::PolicyViolation::ResourceLimit {
6824                    resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
6825                    maximum: 4,
6826                    actual,
6827                }
6828            ) if actual > 4
6829        ));
6830    }
6831
6832    #[test]
6833    fn retrieval_method_xpath_obeys_namespace_binding_limit() {
6834        // The specialized RetrievalMethod path must retain the XPath element's
6835        // in-scope namespaces and enforce the same limit as ordinary XPath.
6836        let mut policy = crate::policy::VerificationPolicy::default();
6837        policy.resources.max_xpath_namespace_bindings = 0;
6838
6839        let error = VerifyContext::new()
6840            .policy(policy)
6841            .verify(&retrieval_method_xpath_signature())
6842            .expect_err("RetrievalMethod XPath namespaces must obey the binding limit");
6843
6844        assert!(matches!(
6845            error,
6846            SignatureVerificationPipelineError::Policy(
6847                crate::policy::PolicyViolation::ResourceLimit {
6848                    resource: crate::policy::resource_name::XPATH_NAMESPACE_BINDINGS,
6849                    maximum: 0,
6850                    actual,
6851                }
6852            ) if actual > 0
6853        ));
6854    }
6855
6856    #[test]
6857    fn retrieval_method_xpath_obeys_namespace_byte_limit() {
6858        // Prefix and URI bytes retained from the XPath namespace axis consume
6859        // the same per-expression byte budget as an ordinary XPath transform.
6860        let mut policy = crate::policy::VerificationPolicy::default();
6861        policy.resources.max_xpath_namespace_bytes = 0;
6862
6863        let error = VerifyContext::new()
6864            .policy(policy)
6865            .verify(&retrieval_method_xpath_signature())
6866            .expect_err("RetrievalMethod XPath namespaces must obey the byte limit");
6867
6868        assert!(matches!(
6869            error,
6870            SignatureVerificationPipelineError::Policy(
6871                crate::policy::PolicyViolation::ResourceLimit {
6872                    resource: crate::policy::resource_name::XPATH_NAMESPACE_BYTES,
6873                    maximum: 0,
6874                    actual,
6875                }
6876            ) if actual > 0
6877        ));
6878    }
6879
6880    #[test]
6881    fn retrieval_method_xpath_obeys_context_evaluation_limit() {
6882        // A bare fragment includes attribute and namespace XPath nodes in
6883        // addition to the four tree nodes below. Tree-only accounting would
6884        // incorrectly admit this input at the configured ceiling.
6885        let mut policy = crate::policy::VerificationPolicy::default();
6886        policy.resources.max_xpath_context_evaluations = 4;
6887        let xml = retrieval_method_xpath_signature().replace(
6888            "<holder Id=\"target\">",
6889            "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
6890        );
6891
6892        let error = VerifyContext::new()
6893            .policy(policy)
6894            .verify(&xml)
6895            .expect_err("RetrievalMethod XPath contexts must obey the evaluation limit");
6896
6897        assert!(matches!(
6898            error,
6899            SignatureVerificationPipelineError::Policy(
6900                crate::policy::PolicyViolation::ResourceLimit {
6901                    resource: crate::policy::resource_name::XPATH_CONTEXT_EVALUATIONS,
6902                    maximum: 4,
6903                    actual,
6904                }
6905            ) if actual > 4
6906        ));
6907    }
6908
6909    #[test]
6910    fn retrieval_method_materializes_direct_untransformed_x509_data() {
6911        // A typed RetrievalMethod may point directly at the XML structure it
6912        // identifies; no transform is needed when X509Data is the URI root.
6913        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6914          <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
6915          <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
6916        </root>"##;
6917        let document = Document::parse(xml).unwrap();
6918        let key_info_node = document
6919            .descendants()
6920            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
6921            .unwrap();
6922        let mut key_info = parse_key_info(key_info_node).unwrap();
6923
6924        materialize_retrieval_methods(
6925            &mut key_info,
6926            &UriReferenceResolver::new(&document),
6927            UriTypeSet::SAME_DOCUMENT,
6928            None,
6929            crate::provider::default_provider(),
6930        )
6931        .expect("a direct X509Data target needs no transform");
6932        assert!(matches!(
6933            key_info.sources.as_slice(),
6934            [super::super::parse::KeyInfoSource::X509Data(info)]
6935                if info.subject_names == ["CN=leaf"]
6936        ));
6937    }
6938
6939    #[test]
6940    fn retrieval_method_respects_configured_same_document_id_semantics() {
6941        // RetrievalMethod is another consumer of same-document URIs and must
6942        // not bypass the grammar selected for normal Reference dereferencing.
6943        fn materialize(
6944            uri: &str,
6945            id: &str,
6946            semantics: crate::policy::SameDocumentIdSemantics,
6947        ) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
6948            let xml = format!(
6949                r#"<root xmlns:ds="{XMLDSIG_NS}">
6950                  <ds:KeyInfo><ds:RetrievalMethod URI="{uri}" Type="{XMLDSIG_NS}X509Data"/></ds:KeyInfo>
6951                  <ds:X509Data Id="{id}"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
6952                </root>"#
6953            );
6954            let document = Document::parse(&xml).unwrap();
6955            let key_info_node = document
6956                .descendants()
6957                .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
6958                .unwrap();
6959            let mut key_info = parse_key_info(key_info_node).unwrap();
6960            let resolver =
6961                UriReferenceResolver::new(&document).with_same_document_id_semantics(semantics);
6962
6963            materialize_retrieval_methods(
6964                &mut key_info,
6965                &resolver,
6966                UriTypeSet::SAME_DOCUMENT,
6967                None,
6968                crate::provider::default_provider(),
6969            )
6970        }
6971
6972        assert!(
6973            materialize(
6974                "#12345",
6975                "12345",
6976                crate::policy::SameDocumentIdSemantics::Specification,
6977            )
6978            .is_err(),
6979            "the standards mode must reject a non-NCName bare fragment"
6980        );
6981        assert!(
6982            materialize(
6983                "#visa'3d",
6984                "visa'3d",
6985                crate::policy::SameDocumentIdSemantics::XmlSecBarename,
6986            )
6987            .is_err(),
6988            "the donor barename wrapper cannot represent an apostrophe"
6989        );
6990        assert!(
6991            materialize(
6992                "#visa'3d",
6993                "visa'3d",
6994                crate::policy::SameDocumentIdSemantics::XmlSecVisa3d,
6995            )
6996            .is_ok(),
6997            "Visa3D mode resolves the registered ID without an XPointer literal"
6998        );
6999    }
7000
7001    #[test]
7002    fn raw_x509_retrieval_method_uses_inherited_xml_base() {
7003        // RetrievalMethod URI is an attribute URI reference, so XML Base uses
7004        // the effective base of the element bearing that attribute.
7005        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7006        let xml = format!(
7007            r#"<root xml:base="https://example.test/keys/nested/" xmlns:ds="{XMLDSIG_NS}">
7008                <ds:KeyInfo><ds:RetrievalMethod URI="../signer.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>
7009            </root>"#
7010        );
7011        let document = Document::parse(&xml).unwrap();
7012        let key_info_node = document
7013            .descendants()
7014            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
7015            .unwrap();
7016        let mut key_info = parse_key_info(key_info_node).unwrap();
7017        let certificate = include_bytes!(
7018            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
7019        )
7020        .to_vec();
7021        let resources = HashMap::from([(
7022            "https://example.test/keys/signer.der".to_string(),
7023            certificate,
7024        )]);
7025        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7026
7027        materialize_retrieval_methods(
7028            &mut key_info,
7029            &resolver,
7030            UriTypeSet::ALL,
7031            None,
7032            crate::provider::default_provider(),
7033        )
7034        .expect("RetrievalMethod should resolve against inherited xml:base");
7035
7036        assert!(matches!(
7037            key_info.sources.as_slice(),
7038            [super::super::parse::KeyInfoSource::X509Data(info)]
7039                if info.certificates.len() == 1
7040        ));
7041    }
7042
7043    #[test]
7044    fn retrieval_method_requires_xpath_for_x509_data_below_uri_root() {
7045        // Without a transform the dereferenced holder, not its descendant,
7046        // is the result and therefore cannot masquerade as typed X509Data.
7047        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7048          <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
7049          <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
7050        </root>"##;
7051        let document = Document::parse(xml).unwrap();
7052        let key_info_node = document
7053            .descendants()
7054            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
7055            .unwrap();
7056        let mut key_info = parse_key_info(key_info_node).unwrap();
7057
7058        let error = materialize_retrieval_methods(
7059            &mut key_info,
7060            &UriReferenceResolver::new(&document),
7061            UriTypeSet::SAME_DOCUMENT,
7062            None,
7063            crate::provider::default_provider(),
7064        )
7065        .expect_err("a wrapper target requires an explicit selection transform");
7066        assert!(matches!(
7067            error,
7068            SignatureVerificationPipelineError::InvalidStructure {
7069                reason: "untransformed X509Data RetrievalMethod must target X509Data directly"
7070            }
7071        ));
7072    }
7073
7074    #[test]
7075    fn retrieval_method_rejects_target_inside_external_x509_data_ancestor() {
7076        // XPath filtering cannot add an ancestor that was outside the URI's
7077        // dereferenced node-set, so this result is not rooted at X509Data.
7078        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7079          <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>
7080          <ds:X509Data><ds:X509SubjectName Id="target">CN=leaf</ds:X509SubjectName></ds:X509Data>
7081        </root>"##;
7082        let document = Document::parse(xml).unwrap();
7083        let key_info_node = document
7084            .descendants()
7085            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
7086            .unwrap();
7087        let mut key_info = parse_key_info(key_info_node).unwrap();
7088
7089        let error = materialize_retrieval_methods(
7090            &mut key_info,
7091            &UriReferenceResolver::new(&document),
7092            UriTypeSet::SAME_DOCUMENT,
7093            None,
7094            crate::provider::default_provider(),
7095        )
7096        .expect_err("filter output without an X509Data root must be rejected");
7097        assert!(matches!(
7098            error,
7099            SignatureVerificationPipelineError::InvalidStructure {
7100                reason: "X509Data RetrievalMethod selected no X509Data element"
7101            }
7102        ));
7103    }
7104
7105    #[test]
7106    fn retrieval_method_rejects_ambiguous_x509_data_relation() {
7107        // A transformed result with multiple X509Data roots is not one KeyInfo child.
7108        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7109          <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>
7110          <holder Id="target"><ds:X509Data/><ds:X509Data/></holder>
7111        </root>"##;
7112        let document = Document::parse(xml).unwrap();
7113        let key_info_node = document
7114            .descendants()
7115            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
7116            .unwrap();
7117        let mut key_info = parse_key_info(key_info_node).unwrap();
7118
7119        let error = materialize_retrieval_methods(
7120            &mut key_info,
7121            &UriReferenceResolver::new(&document),
7122            UriTypeSet::SAME_DOCUMENT,
7123            None,
7124            crate::provider::default_provider(),
7125        )
7126        .expect_err("multiple transformed X509Data roots must be rejected");
7127        assert!(matches!(
7128            error,
7129            SignatureVerificationPipelineError::InvalidStructure {
7130                reason: "X509Data RetrievalMethod selected multiple X509Data elements"
7131            }
7132        ));
7133    }
7134
7135    #[test]
7136    fn retrieval_method_materialization_preserves_key_info_order() {
7137        // Replacing the source in place keeps a later fallback behind the
7138        // retrieved key material for first-match resolvers.
7139        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7140          <ds:KeyInfo>
7141            <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>
7142            <ds:KeyName>fallback</ds:KeyName>
7143          </ds:KeyInfo>
7144          <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
7145        </root>"##;
7146        let document = Document::parse(xml).unwrap();
7147        let key_info_node = document
7148            .descendants()
7149            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
7150            .unwrap();
7151        let mut key_info = parse_key_info(key_info_node).unwrap();
7152
7153        materialize_retrieval_methods(
7154            &mut key_info,
7155            &UriReferenceResolver::new(&document),
7156            UriTypeSet::SAME_DOCUMENT,
7157            None,
7158            crate::provider::default_provider(),
7159        )
7160        .unwrap();
7161        assert!(matches!(
7162            key_info.sources.as_slice(),
7163            [
7164                super::super::parse::KeyInfoSource::X509Data(_),
7165                super::super::parse::KeyInfoSource::KeyName(name)
7166            ] if name == "fallback"
7167        ));
7168    }
7169
7170    #[test]
7171    fn retrieval_method_materialization_bounds_repeated_sources() {
7172        // Repeating one allowed certificate must not multiply parsing and clones
7173        // before SignatureValue validation.
7174        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7175        let certificate = include_bytes!(
7176            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
7177        )
7178        .to_vec();
7179        let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
7180        let mut key_info = KeyInfo {
7181            sources: (0..=64)
7182                .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
7183                    uri: "urn:certificate".into(),
7184                    resource_type: Some(RAW_X509_TYPE.into()),
7185                    transforms: RetrievalMethodTransforms::None,
7186                })
7187                .collect(),
7188        };
7189        let document = Document::parse("<root/>").unwrap();
7190        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7191
7192        let error = materialize_retrieval_methods(
7193            &mut key_info,
7194            &resolver,
7195            UriTypeSet::ALL,
7196            None,
7197            crate::provider::default_provider(),
7198        )
7199        .expect_err("retrieval count must be bounded before materialization");
7200        assert!(matches!(
7201            error,
7202            SignatureVerificationPipelineError::InvalidStructure {
7203                reason: "KeyInfo contains too many RetrievalMethod elements"
7204            }
7205        ));
7206    }
7207
7208    #[test]
7209    fn key_info_reference_materializes_an_allowed_empty_uri() {
7210        // Empty URI is a same-document Reference URI. Element-valued
7211        // KeyInfoReference dereferencing therefore selects a KeyInfo document
7212        // element rather than consulting the external-resource map.
7213        let document = XmlDocument::parse(format!(
7214            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>root-key</ds:KeyName></ds:KeyInfo>"#
7215        ))
7216        .unwrap();
7217        let mut key_info = KeyInfo {
7218            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7219                uri: String::new(),
7220            }],
7221        };
7222        let mut policy = crate::policy::VerificationPolicy::default();
7223        policy.key_sources.key_info_reference = true;
7224        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7225        let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
7226        let mut budgets = RetrievalMaterializationBudgets {
7227            xpath_parse: &mut xpath_parse_budget,
7228            execution: &execution_budget,
7229            resources: &policy.resources,
7230            certificate_signature_algorithms: policy
7231                .key_trust
7232                .certificate_signature_algorithms
7233                .as_ref(),
7234            xml_backend: crate::XmlBackend::default(),
7235        };
7236        let mut materialization = KeyInfoMaterializationState::default();
7237
7238        document
7239            .with_view(|view| {
7240                materialize_key_info_references_with_budgets(
7241                    &mut key_info,
7242                    &UriReferenceResolver::with_document_view(view, &[]),
7243                    &policy,
7244                    crate::provider::default_provider(),
7245                    &mut budgets,
7246                    &mut materialization,
7247                )?;
7248                Ok::<_, SignatureVerificationPipelineError>(())
7249            })
7250            .unwrap();
7251
7252        assert!(matches!(
7253            key_info.sources.as_slice(),
7254            [super::super::parse::KeyInfoSource::KeyName(name)] if name == "root-key"
7255        ));
7256    }
7257
7258    #[test]
7259    fn empty_key_info_reference_participates_in_cycle_detection() {
7260        // Empty URI resolves to the owning document's root KeyInfo. It must
7261        // recurse through the same traversal state rather than leave the
7262        // self-reference materialized and bypass cycle/depth enforcement.
7263        let document = XmlDocument::parse(format!(
7264            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#"><dsig11:KeyInfoReference URI=""/></ds:KeyInfo>"#
7265        ))
7266        .unwrap();
7267        let mut key_info = KeyInfo {
7268            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7269                uri: String::new(),
7270            }],
7271        };
7272        let mut policy = crate::policy::VerificationPolicy::default();
7273        policy.key_sources.key_info_reference = true;
7274        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7275        let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
7276        let mut budgets = RetrievalMaterializationBudgets {
7277            xpath_parse: &mut xpath_parse_budget,
7278            execution: &execution_budget,
7279            resources: &policy.resources,
7280            certificate_signature_algorithms: policy
7281                .key_trust
7282                .certificate_signature_algorithms
7283                .as_ref(),
7284            xml_backend: crate::XmlBackend::default(),
7285        };
7286        let mut materialization = KeyInfoMaterializationState::default();
7287
7288        let error = document
7289            .with_view(|view| {
7290                materialize_key_info_references_with_budgets(
7291                    &mut key_info,
7292                    &UriReferenceResolver::with_document_view(view, &[]),
7293                    &policy,
7294                    crate::provider::default_provider(),
7295                    &mut budgets,
7296                    &mut materialization,
7297                )
7298            })
7299            .expect_err("empty-URI self-reference must be rejected as a cycle");
7300
7301        assert!(matches!(
7302            error,
7303            SignatureVerificationPipelineError::InvalidStructure {
7304                reason: "KeyInfoReference cycle detected"
7305            }
7306        ));
7307    }
7308
7309    #[test]
7310    fn key_info_materialization_shares_candidate_work_across_source_kinds() {
7311        // The reference, nested RetrievalMethod, and resulting certificate are
7312        // three units of materialization work in one operation.
7313        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7314        let document = XmlDocument::parse(format!(
7315            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>"##
7316        ))
7317        .unwrap();
7318        let certificate = include_bytes!(
7319            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
7320        )
7321        .to_vec();
7322        let resources = HashMap::from([("signer.der".to_owned(), certificate)]);
7323        let mut key_info = KeyInfo {
7324            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7325                uri: "#target".into(),
7326            }],
7327        };
7328        let mut policy = crate::policy::VerificationPolicy::default();
7329        policy.key_sources.key_info_reference = true;
7330        policy.uris.retrieval_methods = UriTypeSet::ALL;
7331        policy.resources.max_key_candidates = 2;
7332        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7333        let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
7334        let mut budgets = RetrievalMaterializationBudgets {
7335            xpath_parse: &mut xpath_parse_budget,
7336            execution: &execution_budget,
7337            resources: &policy.resources,
7338            certificate_signature_algorithms: policy
7339                .key_trust
7340                .certificate_signature_algorithms
7341                .as_ref(),
7342            xml_backend: crate::XmlBackend::default(),
7343        };
7344        let mut materialization = KeyInfoMaterializationState::default();
7345
7346        let error = document
7347            .with_view(|view| {
7348                let resolver = UriReferenceResolver::with_document_view(view, &[])
7349                    .with_external_resources(&resources);
7350                let mut outcome = materialize_key_info_references_with_budgets(
7351                    &mut key_info,
7352                    &resolver,
7353                    &policy,
7354                    crate::provider::default_provider(),
7355                    &mut budgets,
7356                    &mut materialization,
7357                )?;
7358                outcome.merge(materialize_retrieval_methods_with_budgets(
7359                    &mut key_info,
7360                    &resolver,
7361                    policy.uris.retrieval_methods,
7362                    policy.transforms.allowed_algorithms.as_ref(),
7363                    crate::provider::default_provider(),
7364                    &mut budgets,
7365                    &mut materialization.candidate_work,
7366                )?);
7367                Ok::<_, DsigError>(outcome)
7368            })
7369            .expect_err("all materializers must share the candidate-work limit");
7370
7371        assert!(matches!(
7372            error,
7373            DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
7374                resource: crate::policy::resource_name::KEY_CANDIDATES,
7375                maximum: 2,
7376                actual: 3,
7377            })
7378        ));
7379    }
7380
7381    #[test]
7382    fn public_key_info_materialization_binds_external_resource_policy() {
7383        // Public helpers must not inherit the resolver's permissive hard-limit
7384        // budget when the operation policy selects a stricter byte ceiling.
7385        let document = Document::parse("<root/>").unwrap();
7386        let external = format!(
7387            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
7388        );
7389        let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
7390        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7391        let mut key_info = KeyInfo {
7392            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7393                uri: "key.xml".into(),
7394            }],
7395        };
7396        let mut policy = crate::policy::VerificationPolicy::default();
7397        policy.key_sources.key_info_reference = true;
7398        policy.uris.key_info_references = UriTypeSet::ALL;
7399        policy.resources.max_external_resource_bytes = 0;
7400
7401        let error = materialize_verification_key_info_references(
7402            &mut key_info,
7403            resolver,
7404            &policy,
7405            crate::provider::default_provider(),
7406            crate::XmlBackend::default(),
7407        )
7408        .expect_err("policy must reject non-empty external KeyInfo bytes");
7409
7410        assert!(matches!(
7411            error,
7412            DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
7413                resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
7414                maximum: 0,
7415                actual,
7416            }) if actual == resources["key.xml"].len()
7417        ));
7418    }
7419
7420    #[cfg(all(feature = "xml-backend-roxmltree", feature = "xml-backend-xmloxide"))]
7421    #[test]
7422    fn public_key_info_materialization_uses_the_selected_backend() {
7423        // Public materialization is a standalone parse boundary. Its backend
7424        // must be caller-selected just like the complete sign/verify pipeline.
7425        let external = format!(
7426            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>external</ds:KeyName></ds:KeyInfo>"#
7427        );
7428        let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
7429        let document = Document::parse("<root/>").unwrap();
7430
7431        for backend in [crate::XmlBackend::Xmloxide, crate::XmlBackend::Roxmltree] {
7432            let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7433            let mut key_info = KeyInfo {
7434                sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7435                    uri: "key.xml".into(),
7436                }],
7437            };
7438            let mut policy = crate::policy::VerificationPolicy::default();
7439            policy.key_sources.key_info_reference = true;
7440            policy.uris.key_info_references = UriTypeSet::ALL;
7441
7442            materialize_verification_key_info_references(
7443                &mut key_info,
7444                resolver,
7445                &policy,
7446                crate::provider::default_provider(),
7447                backend,
7448            )
7449            .unwrap_or_else(|error| panic!("{backend:?} materialization failed: {error}"));
7450
7451            assert!(matches!(
7452                key_info.sources.as_slice(),
7453                [super::super::parse::KeyInfoSource::KeyName(name)] if name == "external"
7454            ));
7455        }
7456    }
7457
7458    #[test]
7459    fn public_key_info_materialization_defers_nested_retrieval_failure() {
7460        // Metadata expansion must preserve an earlier usable source when a
7461        // later retrieval candidate fails. The complete operation reports the
7462        // deferred error only if key selection exhausts all usable sources.
7463        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7464        let external = format!(
7465            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>usable</ds:KeyName><ds:RetrievalMethod URI="missing.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>"#
7466        );
7467        let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
7468        let document = Document::parse("<root/>").unwrap();
7469
7470        for operation in ["signing", "verification"] {
7471            let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7472            let mut key_info = KeyInfo {
7473                sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7474                    uri: "key.xml".into(),
7475                }],
7476            };
7477            match operation {
7478                "signing" => {
7479                    let mut policy = crate::policy::SigningPolicy::default();
7480                    policy.uris.key_info_references = UriTypeSet::ALL;
7481                    policy.uris.retrieval_methods = UriTypeSet::ALL;
7482                    materialize_signing_key_info_references(
7483                        &mut key_info,
7484                        resolver,
7485                        &policy,
7486                        crate::provider::default_provider(),
7487                        crate::XmlBackend::default(),
7488                    )
7489                }
7490                "verification" => {
7491                    let mut policy = crate::policy::VerificationPolicy::default();
7492                    policy.key_sources.key_info_reference = true;
7493                    policy.uris.key_info_references = UriTypeSet::ALL;
7494                    policy.uris.retrieval_methods = UriTypeSet::ALL;
7495                    materialize_verification_key_info_references(
7496                        &mut key_info,
7497                        resolver,
7498                        &policy,
7499                        crate::provider::default_provider(),
7500                        crate::XmlBackend::default(),
7501                    )
7502                }
7503                _ => unreachable!(),
7504            }
7505            .unwrap_or_else(|error| panic!("{operation} materialization failed: {error}"));
7506
7507            assert!(matches!(
7508                key_info.sources.as_slice(),
7509                [super::super::parse::KeyInfoSource::KeyName(name)] if name == "usable"
7510            ));
7511        }
7512    }
7513
7514    #[test]
7515    fn public_key_info_materialization_binds_xpath_policy() {
7516        // Both public helpers must carry the caller's XPath parse limits into
7517        // RetrievalMethod transforms nested in an external KeyInfo document.
7518        let external = format!(
7519            r##"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}">
7520                <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
7521                    <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
7522                        <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
7523                    </ds:Transform></ds:Transforms>
7524                </ds:RetrievalMethod>
7525                <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
7526            </ds:KeyInfo>"##
7527        );
7528        let resources = HashMap::from([("key.xml".to_owned(), external.into_bytes())]);
7529        let document = Document::parse("<root/>").unwrap();
7530
7531        for operation in ["signing", "verification"] {
7532            let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7533            let mut key_info = KeyInfo {
7534                sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7535                    uri: "key.xml".into(),
7536                }],
7537            };
7538            let error = match operation {
7539                "signing" => {
7540                    let mut policy = crate::policy::SigningPolicy::default();
7541                    policy.uris.key_info_references = UriTypeSet::ALL;
7542                    policy.resources.max_xpath_expressions = 0;
7543                    materialize_signing_key_info_references(
7544                        &mut key_info,
7545                        resolver,
7546                        &policy,
7547                        crate::provider::default_provider(),
7548                        crate::XmlBackend::default(),
7549                    )
7550                }
7551                "verification" => {
7552                    let mut policy = crate::policy::VerificationPolicy::default();
7553                    policy.key_sources.key_info_reference = true;
7554                    policy.uris.key_info_references = UriTypeSet::ALL;
7555                    policy.resources.max_xpath_expressions = 0;
7556                    materialize_verification_key_info_references(
7557                        &mut key_info,
7558                        resolver,
7559                        &policy,
7560                        crate::provider::default_provider(),
7561                        crate::XmlBackend::default(),
7562                    )
7563                }
7564                _ => unreachable!(),
7565            }
7566            .expect_err("the public helper must enforce the supplied XPath expression limit");
7567
7568            assert!(
7569                matches!(
7570                    error,
7571                    DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
7572                        resource: crate::policy::resource_name::XPATH_EXPRESSIONS,
7573                        maximum: 0,
7574                        actual: 1,
7575                    })
7576                ),
7577                "unexpected {operation} error: {error:?}"
7578            );
7579        }
7580    }
7581
7582    fn materialize_external_key_info_chain(
7583        terminal_reference: &str,
7584    ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
7585        let a = format!(
7586            r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
7587                <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
7588                <ds:KeyInfo ID="next"><dsig11:KeyInfoReference URI="b.xml#root"/></ds:KeyInfo>
7589            </doc>"##
7590        );
7591        let b = format!(
7592            r##"<doc xmlns:ds="{XMLDSIG_NS}" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
7593                <ds:KeyInfo ID="root"><dsig11:KeyInfoReference URI="#next"/></ds:KeyInfo>
7594                <ds:KeyInfo ID="next">{terminal_reference}</ds:KeyInfo>
7595            </doc>"##
7596        );
7597        let resources = HashMap::from([
7598            ("a.xml".to_owned(), a.into_bytes()),
7599            ("b.xml".to_owned(), b.into_bytes()),
7600        ]);
7601        let document = XmlDocument::parse("<root/>").unwrap();
7602        let mut key_info = KeyInfo {
7603            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7604                uri: "a.xml#root".into(),
7605            }],
7606        };
7607        let mut policy = crate::policy::VerificationPolicy::default();
7608        policy.key_sources.key_info_reference = true;
7609        policy.uris.key_info_references = UriTypeSet::ALL;
7610        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7611        let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
7612        let mut budgets = RetrievalMaterializationBudgets {
7613            xpath_parse: &mut xpath_parse_budget,
7614            execution: &execution_budget,
7615            resources: &policy.resources,
7616            certificate_signature_algorithms: policy
7617                .key_trust
7618                .certificate_signature_algorithms
7619                .as_ref(),
7620            xml_backend: crate::XmlBackend::default(),
7621        };
7622        let mut materialization = KeyInfoMaterializationState::default();
7623
7624        document.with_view(|view| {
7625            let resolver = UriReferenceResolver::with_document_view(view, &[])
7626                .with_external_resources(&resources);
7627            materialize_key_info_references_with_budgets(
7628                &mut key_info,
7629                &resolver,
7630                &policy,
7631                crate::provider::default_provider(),
7632                &mut budgets,
7633                &mut materialization,
7634            )?;
7635            Ok::<_, SignatureVerificationPipelineError>(())
7636        })?;
7637        Ok(key_info)
7638    }
7639
7640    fn materialize_external_key_info_bytes_with_limit(
7641        encoded: Vec<u8>,
7642        maximum_document_bytes: usize,
7643    ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
7644        let resources = HashMap::from([("key.xml".to_owned(), encoded)]);
7645        let document = XmlDocument::parse("<root/>").unwrap();
7646        let mut key_info = KeyInfo {
7647            sources: vec![super::super::parse::KeyInfoSource::KeyInfoReference {
7648                uri: "key.xml".into(),
7649            }],
7650        };
7651        let mut policy = crate::policy::VerificationPolicy::default();
7652        policy.key_sources.key_info_reference = true;
7653        policy.uris.key_info_references = UriTypeSet::ALL;
7654        policy.resources.max_xml_document_bytes = maximum_document_bytes;
7655        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7656        let execution_budget = TransformExecutionBudget::from_resources(&policy.resources);
7657        let mut budgets = RetrievalMaterializationBudgets {
7658            xpath_parse: &mut xpath_parse_budget,
7659            execution: &execution_budget,
7660            resources: &policy.resources,
7661            certificate_signature_algorithms: policy
7662                .key_trust
7663                .certificate_signature_algorithms
7664                .as_ref(),
7665            xml_backend: crate::XmlBackend::default(),
7666        };
7667        let mut materialization = KeyInfoMaterializationState::default();
7668
7669        document.with_view(|view| {
7670            let resolver = UriReferenceResolver::with_document_view(view, &[])
7671                .with_external_resources(&resources);
7672            materialize_key_info_references_with_budgets(
7673                &mut key_info,
7674                &resolver,
7675                &policy,
7676                crate::provider::default_provider(),
7677                &mut budgets,
7678                &mut materialization,
7679            )?;
7680            Ok::<_, SignatureVerificationPipelineError>(())
7681        })?;
7682        Ok(key_info)
7683    }
7684
7685    fn materialize_external_key_info_bytes(
7686        encoded: Vec<u8>,
7687    ) -> Result<KeyInfo, SignatureVerificationPipelineError> {
7688        materialize_external_key_info_bytes_with_limit(
7689            encoded,
7690            crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
7691        )
7692    }
7693
7694    #[test]
7695    fn external_key_info_reference_decodes_utf16_xml_octets() {
7696        // External XML follows the XML encoding declaration/BOM contract, not
7697        // the UTF-8-only contract of Rust strings passed by direct callers.
7698        let xml = format!(
7699            r#"<ds:KeyInfo xmlns:ds="{XMLDSIG_NS}"><ds:KeyName>utf16-key</ds:KeyName></ds:KeyInfo>"#
7700        );
7701        let mut encoded = vec![0xff, 0xfe];
7702        encoded.extend(xml.encode_utf16().flat_map(u16::to_le_bytes));
7703        let key_info = materialize_external_key_info_bytes(encoded)
7704            .expect("UTF-16 external KeyInfo must materialize");
7705        assert!(matches!(
7706            key_info.sources.as_slice(),
7707            [super::super::parse::KeyInfoSource::KeyName(name)] if name == "utf16-key"
7708        ));
7709    }
7710
7711    #[test]
7712    fn external_key_info_reference_rejects_malformed_xml_encoding() {
7713        // A BOM selecting UTF-16 must not permit a truncated code unit to reach
7714        // the XML parser under a misleading structural diagnostic.
7715        let error = materialize_external_key_info_bytes(vec![0xff, 0xfe, b'<'])
7716            .expect_err("truncated UTF-16 must be rejected during octet decoding");
7717
7718        assert!(matches!(
7719            error,
7720            SignatureVerificationPipelineError::InvalidStructure {
7721                reason: "KeyInfoReference external resource has an invalid XML encoding"
7722            }
7723        ));
7724    }
7725
7726    #[test]
7727    fn external_key_info_reference_preserves_decoded_document_limit() {
7728        // Decoding can expand a compact single-byte resource beyond the XML document policy.
7729        // That is a typed policy failure, not malformed KeyInfo structure.
7730        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();
7731        let maximum = encoded.len();
7732        let error = materialize_external_key_info_bytes_with_limit(encoded, maximum)
7733            .expect_err("decoded XML expansion must retain resource-limit classification");
7734
7735        assert!(matches!(
7736            error,
7737            SignatureVerificationPipelineError::Policy(
7738                crate::policy::PolicyViolation::ResourceLimit {
7739                    resource: crate::policy::resource_name::XML_DOCUMENT,
7740                    maximum: observed_maximum,
7741                    actual,
7742                }
7743            ) if observed_maximum == maximum && actual > maximum
7744        ));
7745    }
7746
7747    #[test]
7748    fn key_info_reference_cycle_identity_includes_the_owning_resource() {
7749        // Equal fragment spellings in separate external documents are distinct
7750        // references and must not be rejected as a recursive cycle.
7751        let key_info = materialize_external_key_info_chain("<ds:KeyName>terminal</ds:KeyName>")
7752            .expect("cross-document duplicate fragments must materialize");
7753        assert!(matches!(
7754            key_info.sources.as_slice(),
7755            [super::super::parse::KeyInfoSource::KeyName(name)] if name == "terminal"
7756        ));
7757    }
7758
7759    #[test]
7760    fn key_info_reference_cycle_identity_survives_external_reparse() {
7761        // Returning to the same external resource must remain a cycle even
7762        // though each dereference creates a fresh owned XML document.
7763        let error =
7764            materialize_external_key_info_chain("<dsig11:KeyInfoReference URI=\"a.xml#root\"/>")
7765                .expect_err("a resource cycle must fail before exhausting depth");
7766        assert!(matches!(
7767            error,
7768            SignatureVerificationPipelineError::InvalidStructure {
7769                reason: "KeyInfoReference cycle detected"
7770            }
7771        ));
7772    }
7773
7774    #[test]
7775    fn retrieval_method_materialization_deduplicates_within_count_limit() {
7776        // Repeated references to the same raw certificate produce one parsed
7777        // key source rather than one certificate clone per XML element.
7778        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7779        let certificate = include_bytes!(
7780            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
7781        )
7782        .to_vec();
7783        let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
7784        let mut key_info = KeyInfo {
7785            sources: (0..MAX_RETRIEVAL_METHOD_COUNT)
7786                .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
7787                    uri: "urn:certificate".into(),
7788                    resource_type: Some(RAW_X509_TYPE.into()),
7789                    transforms: RetrievalMethodTransforms::None,
7790                })
7791                .collect(),
7792        };
7793        let document = Document::parse("<root/>").unwrap();
7794        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7795
7796        materialize_retrieval_methods(
7797            &mut key_info,
7798            &resolver,
7799            UriTypeSet::ALL,
7800            None,
7801            crate::provider::default_provider(),
7802        )
7803        .unwrap();
7804        assert!(matches!(
7805            key_info.sources.as_slice(),
7806            [super::super::parse::KeyInfoSource::X509Data(info)]
7807                if info.certificates.len() == 1
7808        ));
7809    }
7810
7811    #[test]
7812    fn retrieval_method_candidate_budget_includes_embedded_key_values() {
7813        // A previously parsed KeyValue consumes the sole candidate slot, so
7814        // malformed retrieved DER must be rejected by policy before X.509 parsing.
7815        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7816        let resources = HashMap::from([("urn:certificate".to_string(), vec![1, 2, 3])]);
7817        let mut key_info = KeyInfo {
7818            sources: vec![
7819                super::super::parse::KeyInfoSource::KeyValue(
7820                    super::super::parse::KeyValueInfo::Unsupported {
7821                        namespace: Some(XMLDSIG_NS.into()),
7822                        local_name: "FutureKeyValue".into(),
7823                    },
7824                ),
7825                super::super::parse::KeyInfoSource::RetrievalMethod {
7826                    uri: "urn:certificate".into(),
7827                    resource_type: Some(RAW_X509_TYPE.into()),
7828                    transforms: RetrievalMethodTransforms::None,
7829                },
7830            ],
7831        };
7832        let document = Document::parse("<root/>").unwrap();
7833        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7834        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
7835        let execution_budget = TransformExecutionBudget::default();
7836        let resource_policy = crate::policy::ResourcePolicy {
7837            max_key_candidates: 1,
7838            ..crate::policy::ResourcePolicy::default()
7839        };
7840        let mut budgets = RetrievalMaterializationBudgets {
7841            xpath_parse: &mut xpath_parse_budget,
7842            execution: &execution_budget,
7843            resources: &resource_policy,
7844            certificate_signature_algorithms: None,
7845            xml_backend: crate::XmlBackend::default(),
7846        };
7847        let mut candidate_work = key_info.embedded_candidate_count();
7848
7849        let error = materialize_retrieval_methods_with_budgets(
7850            &mut key_info,
7851            &resolver,
7852            UriTypeSet::ALL,
7853            None,
7854            crate::provider::default_provider(),
7855            &mut budgets,
7856            &mut candidate_work,
7857        )
7858        .expect_err("the retrieved certificate must exceed the aggregate candidate limit");
7859
7860        assert!(matches!(
7861            error,
7862            SignatureVerificationPipelineError::Policy(
7863                crate::policy::PolicyViolation::ResourceLimit {
7864                    resource: crate::policy::resource_name::KEY_CANDIDATES,
7865                    maximum: 1,
7866                    actual: 2,
7867                }
7868            )
7869        ));
7870    }
7871
7872    #[test]
7873    fn raw_x509_retrieval_rejects_empty_same_document_uri() {
7874        // rawX509Certificate consumes external DER octets; an empty URI denotes
7875        // the XML document and must never become a key into the external map.
7876        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
7877        let certificate = include_bytes!(
7878            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
7879        )
7880        .to_vec();
7881        let resources = HashMap::from([(String::new(), certificate)]);
7882        let mut key_info = KeyInfo {
7883            sources: vec![super::super::parse::KeyInfoSource::RetrievalMethod {
7884                uri: String::new(),
7885                resource_type: Some(RAW_X509_TYPE.into()),
7886                transforms: RetrievalMethodTransforms::None,
7887            }],
7888        };
7889        let document = Document::parse("<root/>").unwrap();
7890        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
7891
7892        let error = materialize_retrieval_methods(
7893            &mut key_info,
7894            &resolver,
7895            UriTypeSet::ALL,
7896            None,
7897            crate::provider::default_provider(),
7898        )
7899        .expect_err("empty URI must retain same-document semantics");
7900        assert!(matches!(
7901            error,
7902            SignatureVerificationPipelineError::InvalidStructure {
7903                reason: "raw X509 RetrievalMethod requires an untransformed external URI"
7904            }
7905        ));
7906    }
7907
7908    #[test]
7909    fn verify_context_does_not_hide_malformed_digest_behind_unsupported_transform() {
7910        // A bad DigestValue remains a parse error even when its transform URI is unsupported.
7911        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7912            let xml = xml.replacen(
7913                "<ds:Reference URI=\"#target\">",
7914                "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
7915                1,
7916            );
7917            replace_fixture_manifest_digest(&xml, "!!!")
7918        });
7919
7920        let error = VerifyContext::new()
7921            .key(&AcceptingKey)
7922            .process_manifests(true)
7923            .verify(&broken_xml)
7924            .expect_err("malformed Manifest digest must not become a validity result");
7925        assert!(matches!(
7926            error,
7927            SignatureVerificationPipelineError::ParseManifestReference(_)
7928        ));
7929    }
7930
7931    #[test]
7932    fn verify_context_rejects_manifest_non_whitespace_mixed_content() {
7933        // Authenticated mixed content is still structurally invalid under the
7934        // Manifest element-only grammar.
7935        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7936            xml.replacen(
7937                "<ds:Manifest ID=\"manifest\">",
7938                "<ds:Manifest ID=\"manifest\">junk",
7939                1,
7940            )
7941        });
7942
7943        let err = VerifyContext::new()
7944            .key(&AcceptingKey)
7945            .process_manifests(true)
7946            .verify(&xml)
7947            .expect_err("Manifest mixed content must fail verification");
7948        assert!(matches!(
7949            err,
7950            SignatureVerificationPipelineError::InvalidStructure {
7951                reason: "Manifest contains non-whitespace mixed content"
7952            }
7953        ));
7954    }
7955
7956    #[test]
7957    fn verify_context_rejects_empty_manifest_children() {
7958        // An authenticated empty Manifest violates the required Reference+
7959        // content model rather than disappearing as an unsigned block.
7960        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
7961            let (prefix, rest) = xml
7962                .split_once("<ds:Manifest ID=\"manifest\">")
7963                .expect("fixture should contain Manifest");
7964            let (_, suffix) = rest
7965                .split_once("</ds:Manifest>")
7966                .expect("fixture should contain closing Manifest");
7967            format!("{prefix}<ds:Manifest ID=\"manifest\"></ds:Manifest>{suffix}")
7968        });
7969
7970        let err = VerifyContext::new()
7971            .key(&AcceptingKey)
7972            .process_manifests(true)
7973            .verify(&xml)
7974            .expect_err("empty Manifest must fail verification");
7975        assert!(matches!(
7976            err,
7977            SignatureVerificationPipelineError::InvalidStructure {
7978                reason: "Manifest must contain at least one ds:Reference element child"
7979            }
7980        ));
7981    }
7982
7983    #[test]
7984    fn verify_context_ignores_unsigned_malformed_manifest_blocks() {
7985        let xml = signature_with_manifest_xml(true).replacen(
7986            "</ds:Object>",
7987            "</ds:Object><ds:Object><ds:Manifest>junk<ds:Foo/></ds:Manifest></ds:Object>",
7988            1,
7989        );
7990        let result = VerifyContext::new()
7991            .key(&AcceptingKey)
7992            .process_manifests(true)
7993            .verify(&xml)
7994            .expect("unsigned malformed Manifest must be ignored");
7995        assert_eq!(
7996            result.manifest_references.len(),
7997            1,
7998            "only signed Manifest references must be reported",
7999        );
8000        assert!(matches!(result.status, DsigStatus::Valid));
8001    }
8002
8003    #[test]
8004    fn verify_context_skips_ambiguous_manifest_id_blocks() {
8005        let xml = signature_with_manifest_xml(true).replacen(
8006            "</ds:Object>",
8007            "</ds:Object><ds:Object><ds:Manifest ID=\"manifest\">junk<ds:Foo/></ds:Manifest></ds:Object>",
8008            1,
8009        );
8010        let err = VerifyContext::new()
8011            .key(&RejectingKey)
8012            .process_manifests(true)
8013            .verify(&xml)
8014            .expect_err("ambiguous manifest IDs should make SignedInfo #manifest dereference fail");
8015        assert!(matches!(
8016            err,
8017            SignatureVerificationPipelineError::Reference(
8018                ReferenceProcessingError::UriDereference(
8019                    crate::xmldsig::types::TransformError::ElementNotFound(id)
8020                )
8021            ) if id == "manifest"
8022        ));
8023    }
8024
8025    #[test]
8026    fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
8027        let xml = minimal_signature_xml("", "");
8028        let err = VerifyContext::new()
8029            .key(&RejectingKey)
8030            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
8031            .verify(&xml)
8032            .expect_err("implicit default C14N must be checked against allowlist");
8033        assert!(matches!(
8034            err,
8035            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
8036                operation: "verification transform",
8037                ..
8038            })
8039        ));
8040    }
8041
8042    #[test]
8043    fn verify_context_skips_resolver_when_reference_processing_fails() {
8044        let xml = minimal_signature_xml("", "");
8045        let result = VerifyContext::new()
8046            .key_resolver(&PanicResolver)
8047            .verify(&xml)
8048            .expect("reference digest mismatch should short-circuit before resolver");
8049        assert!(matches!(
8050            result.status,
8051            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
8052        ));
8053    }
8054
8055    #[test]
8056    fn verify_context_leaves_fail_fast_reference_tail_unexecuted() {
8057        // A mismatch on the first Reference must not produce graph evidence for
8058        // the second Reference, which was never dereferenced or digested.
8059        let second_reference = r#"
8060    <ds:Reference URI="">
8061      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
8062      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
8063    </ds:Reference>"#;
8064        let xml = minimal_signature_xml("", "").replacen(
8065            "  </ds:SignedInfo>",
8066            &format!("{second_reference}\n  </ds:SignedInfo>"),
8067            1,
8068        );
8069
8070        let result = VerifyContext::new()
8071            .key(&AcceptingKey)
8072            .verify(&xml)
8073            .expect("digest mismatch must remain a verification result");
8074
8075        assert_eq!(result.signed_info_references.len(), 1);
8076        assert!(matches!(
8077            result.signed_info_references[0].status,
8078            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
8079        ));
8080    }
8081
8082    #[test]
8083    fn verify_context_reports_key_not_found_when_resolver_misses() {
8084        let xml = signature_with_target_reference("AQ==");
8085        let result = VerifyContext::new()
8086            .key_resolver(&MissingKeyResolver)
8087            .verify(&xml)
8088            .expect("resolver miss should report status, not pipeline error");
8089        assert!(matches!(
8090            result.status,
8091            DsigStatus::Invalid(FailureReason::KeyNotFound)
8092        ));
8093        assert_eq!(
8094            result.signed_info_references.len(),
8095            1,
8096            "KeyNotFound path must preserve SignedInfo reference diagnostics",
8097        );
8098        assert!(matches!(
8099            result.signed_info_references[0].status,
8100            DsigStatus::Valid
8101        ));
8102    }
8103
8104    #[test]
8105    fn verification_candidate_budget_covers_preset_and_custom_resolver_paths() {
8106        // Zero is a valid deny-all ceiling. Neither an already-resolved key nor
8107        // a custom resolver may bypass the operation-wide candidate policy.
8108        let xml = signature_with_target_reference("AQ==");
8109        let mut policy = crate::policy::VerificationPolicy::default();
8110        policy.resources.max_key_candidates = 0;
8111
8112        let preset_error = VerifyContext::new()
8113            .key(&RejectingKey)
8114            .policy(policy.clone())
8115            .verify(&xml)
8116            .expect_err("a preset key consumes one candidate");
8117        assert!(matches!(
8118            preset_error,
8119            SignatureVerificationPipelineError::Policy(
8120                crate::policy::PolicyViolation::ResourceLimit {
8121                    resource: crate::policy::resource_name::KEY_CANDIDATES,
8122                    maximum: 0,
8123                    actual: 1,
8124                }
8125            )
8126        ));
8127
8128        let resolver_error = VerifyContext::new()
8129            .key_resolver(&PanicResolver)
8130            .policy(policy)
8131            .verify(&xml)
8132            .expect_err("a custom resolver requires candidate capacity before dispatch");
8133        assert!(matches!(
8134            resolver_error,
8135            SignatureVerificationPipelineError::Policy(
8136                crate::policy::PolicyViolation::ResourceLimit {
8137                    resource: crate::policy::resource_name::KEY_CANDIDATES,
8138                    maximum: 0,
8139                    actual: 1,
8140                }
8141            )
8142        ));
8143    }
8144
8145    #[test]
8146    fn verification_candidate_budget_precedes_embedded_x509_parsing() {
8147        // The first certificate is valid and consumes the sole permitted slot;
8148        // malformed bytes in the second must never reach the X.509 parser.
8149        let first_certificate = base64::engine::general_purpose::STANDARD.encode(include_bytes!(
8150            "../../tests/fixtures/xmldsig/phaos-xmldsig-three/certs/rsa-cert.der"
8151        ));
8152        let xml = signature_with_target_reference("AQ==").replace(
8153            "</ds:SignatureValue>\n  </ds:Signature>",
8154            &format!(
8155                "</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>"
8156            ),
8157        );
8158        let mut policy = crate::policy::VerificationPolicy::default();
8159        policy.resources.max_key_candidates = 1;
8160
8161        let error = VerifyContext::new()
8162            .policy(policy)
8163            .verify(&xml)
8164            .expect_err("candidate policy must run before embedded certificate parsing");
8165
8166        assert!(matches!(
8167            error,
8168            SignatureVerificationPipelineError::Policy(
8169                crate::policy::PolicyViolation::ResourceLimit {
8170                    resource: crate::policy::resource_name::KEY_CANDIDATES,
8171                    maximum: 1,
8172                    actual: 2,
8173                }
8174            )
8175        ));
8176    }
8177
8178    #[test]
8179    fn verify_context_resolver_can_ignore_malformed_keyinfo_by_default() {
8180        let base_xml = signature_with_target_reference("AQ==");
8181        let xml = base_xml
8182            .replace(
8183                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
8184                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
8185            )
8186            .replace(
8187                "</ds:SignatureValue>\n  </ds:Signature>",
8188                "</ds:SignatureValue>\n    <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n  </ds:Signature>",
8189            );
8190
8191        let result = VerifyContext::new()
8192            .key_resolver(&MissingKeyResolver)
8193            .verify(&xml)
8194            .expect("resolver path should not hard-fail on advisory malformed KeyInfo by default");
8195        assert!(matches!(
8196            result.status,
8197            DsigStatus::Invalid(FailureReason::KeyNotFound)
8198        ));
8199    }
8200
8201    #[test]
8202    fn verify_context_resolver_can_opt_in_to_keyinfo_parse_failures() {
8203        let base_xml = signature_with_target_reference("AQ==");
8204        let xml = base_xml
8205            .replace(
8206                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
8207                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
8208            )
8209            .replace(
8210                "</ds:SignatureValue>\n  </ds:Signature>",
8211                "</ds:SignatureValue>\n    <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n  </ds:Signature>",
8212            );
8213
8214        let err = VerifyContext::new()
8215            .key_resolver(&ConsumingKeyInfoResolver)
8216            .verify(&xml)
8217            .expect_err("resolver opted into KeyInfo parsing, malformed KeyInfo must fail");
8218        assert!(matches!(
8219            err,
8220            SignatureVerificationPipelineError::ParseKeyInfo(_)
8221        ));
8222    }
8223
8224    #[test]
8225    fn verify_context_ignores_unsupported_retrieval_before_valid_key_source() {
8226        // An advisory vendor RetrievalMethod cannot prevent the resolver from
8227        // reaching a later supported source in document order.
8228        let xml = signature_with_target_reference("AQ==").replace(
8229            "</ds:SignatureValue>\n  </ds:Signature>",
8230            r##"</ds:SignatureValue>
8231    <ds:KeyInfo>
8232      <ds:RetrievalMethod URI="#vendor" Type="urn:vendor:key">
8233        <ds:Transforms><ds:Transform Algorithm="urn:vendor:transform"/></ds:Transforms>
8234      </ds:RetrievalMethod>
8235      <ds:KeyName>fallback</ds:KeyName>
8236    </ds:KeyInfo>
8237  </ds:Signature>"##,
8238        );
8239
8240        let result = VerifyContext::new()
8241            .key_resolver(&FallbackKeyInfoResolver)
8242            .verify(&xml)
8243            .expect("unsupported advisory retrieval must not abort key resolution");
8244        assert_eq!(result.status, DsigStatus::Valid);
8245    }
8246
8247    #[test]
8248    fn verify_context_does_not_eagerly_fail_unused_retrieval_fallback() {
8249        // KeyInfo sources are alternatives in document order. Once an earlier
8250        // source resolves, a missing later RetrievalMethod is irrelevant.
8251        let xml = signature_with_target_reference("AQ==").replace(
8252            "</ds:SignatureValue>\n  </ds:Signature>",
8253            r#"</ds:SignatureValue>
8254    <ds:KeyInfo>
8255      <ds:KeyName>primary</ds:KeyName>
8256      <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
8257    </ds:KeyInfo>
8258  </ds:Signature>"#,
8259        );
8260
8261        let result = VerifyContext::new()
8262            .key_resolver(&EarlyKeyInfoResolver)
8263            .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
8264            .verify(&xml)
8265            .expect("an unused missing retrieval fallback must not abort verification");
8266
8267        assert_eq!(result.status, DsigStatus::Valid);
8268    }
8269
8270    #[test]
8271    fn verify_context_does_not_eagerly_fail_unused_same_document_x509_retrieval() {
8272        // Same-document X509Data retrieval is an ordered alternative just like
8273        // raw-certificate retrieval; a resolved earlier source makes it unused.
8274        let xml = signature_with_target_reference("AQ==").replace(
8275            "</ds:SignatureValue>\n  </ds:Signature>",
8276            r##"</ds:SignatureValue>
8277    <ds:KeyInfo>
8278      <ds:KeyName>primary</ds:KeyName>
8279      <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
8280    </ds:KeyInfo>
8281  </ds:Signature>"##,
8282        );
8283
8284        let result = VerifyContext::new()
8285            .key_resolver(&EarlyKeyInfoResolver)
8286            .verify(&xml)
8287            .expect("an unused missing X509Data retrieval fallback must not abort verification");
8288
8289        assert_eq!(result.status, DsigStatus::Valid);
8290    }
8291
8292    #[test]
8293    fn verify_context_reports_missing_same_document_x509_retrieval_without_fallback() {
8294        // Deferred failures retain their exact diagnostic when no alternative
8295        // source resolves the verification key.
8296        let xml = signature_with_target_reference("AQ==").replace(
8297            "</ds:SignatureValue>\n  </ds:Signature>",
8298            r##"</ds:SignatureValue>
8299    <ds:KeyInfo>
8300      <ds:RetrievalMethod URI="#missing" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/>
8301    </ds:KeyInfo>
8302  </ds:Signature>"##,
8303        );
8304
8305        let error = VerifyContext::new()
8306            .key_resolver(&ConsumingKeyInfoResolver)
8307            .verify(&xml)
8308            .expect_err("a sole missing X509Data retrieval must remain an explicit error");
8309
8310        assert!(matches!(
8311            error,
8312            SignatureVerificationPipelineError::InvalidStructure {
8313                reason: "X509Data RetrievalMethod target is missing or ambiguous"
8314            }
8315        ));
8316    }
8317
8318    #[test]
8319    fn verify_context_does_not_eagerly_parse_unused_retrieval_fallback() {
8320        // Materialization must preserve ordered fallback semantics even when
8321        // caller-supplied bytes exist but are not a certificate.
8322        let xml = signature_with_target_reference("AQ==").replace(
8323            "</ds:SignatureValue>\n  </ds:Signature>",
8324            r#"</ds:SignatureValue>
8325    <ds:KeyInfo>
8326      <ds:KeyName>primary</ds:KeyName>
8327      <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
8328    </ds:KeyInfo>
8329  </ds:Signature>"#,
8330        );
8331        let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
8332
8333        let result = VerifyContext::new()
8334            .key_resolver(&EarlyKeyInfoResolver)
8335            .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
8336            .external_resources(&resources)
8337            .verify(&xml)
8338            .expect("an unused malformed retrieval fallback must not abort verification");
8339
8340        assert_eq!(result.status, DsigStatus::Valid);
8341    }
8342
8343    #[test]
8344    fn verify_context_reports_missing_retrieval_when_no_key_source_resolves() {
8345        // Deferral changes ordering, not diagnostics: if no alternative source
8346        // resolves, the first missing retrieval remains the pipeline failure.
8347        let xml = signature_with_target_reference("AQ==").replace(
8348            "</ds:SignatureValue>\n  </ds:Signature>",
8349            r#"</ds:SignatureValue>
8350    <ds:KeyInfo>
8351      <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
8352    </ds:KeyInfo>
8353  </ds:Signature>"#,
8354        );
8355
8356        let error = VerifyContext::new()
8357            .key_resolver(&ConsumingKeyInfoResolver)
8358            .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
8359            .verify(&xml)
8360            .expect_err("a missing sole RetrievalMethod must remain an explicit error");
8361
8362        assert!(matches!(
8363            error,
8364            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
8365                crate::xmldsig::TransformError::UnsupportedUri(uri)
8366            )) if uri == "missing.der"
8367        ));
8368    }
8369
8370    #[test]
8371    fn verify_context_reports_malformed_retrieval_when_no_key_source_resolves() {
8372        // Deferral must retain the parse error when the malformed certificate
8373        // is the only candidate rather than degrading it to KeyNotFound.
8374        let xml = signature_with_target_reference("AQ==").replace(
8375            "</ds:SignatureValue>\n  </ds:Signature>",
8376            r#"</ds:SignatureValue>
8377    <ds:KeyInfo>
8378      <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
8379    </ds:KeyInfo>
8380  </ds:Signature>"#,
8381        );
8382        let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
8383
8384        let error = VerifyContext::new()
8385            .key_resolver(&ConsumingKeyInfoResolver)
8386            .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
8387            .external_resources(&resources)
8388            .verify(&xml)
8389            .expect_err("a malformed sole RetrievalMethod must remain a parse error");
8390
8391        assert!(matches!(
8392            error,
8393            SignatureVerificationPipelineError::ParseKeyInfo(_)
8394        ));
8395    }
8396
8397    #[test]
8398    fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
8399        let xml = signature_with_target_reference("@@@");
8400
8401        let err = VerifyContext::new()
8402            .key_resolver(&MissingKeyResolver)
8403            .verify(&xml)
8404            .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
8405        assert!(matches!(
8406            err,
8407            SignatureVerificationPipelineError::SignatureValueBase64(_)
8408        ));
8409    }
8410
8411    #[test]
8412    fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
8413        let xml = signature_with_target_reference("@@@");
8414
8415        let err = VerifyContext::new()
8416            .verify(&xml)
8417            .expect_err("invalid SignatureValue must remain a decode error");
8418        assert!(matches!(
8419            err,
8420            SignatureVerificationPipelineError::SignatureValueBase64(_)
8421        ));
8422    }
8423
8424    #[test]
8425    fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
8426        let references = vec![Reference {
8427            uri: None,
8428            id: None,
8429            ref_type: None,
8430            transforms: vec![],
8431            digest_method: DigestAlgorithm::Sha256,
8432            digest_value: vec![0; 32],
8433        }];
8434        let uri_types = UriTypeSet {
8435            allow_empty: false,
8436            allow_same_document: true,
8437            allow_external: false,
8438        };
8439
8440        let err = enforce_reference_policies(&references, uri_types, None)
8441            .expect_err("missing URI must fail before allow_empty policy is evaluated");
8442        assert!(matches!(
8443            err,
8444            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
8445        ));
8446    }
8447
8448    #[test]
8449    fn enforce_reference_policies_checks_only_terminal_binary_output() {
8450        let c14n = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI).unwrap();
8451        let allowed = HashSet::from([
8452            BASE64_TRANSFORM_URI.to_owned(),
8453            DEFAULT_IMPLICIT_C14N_URI.to_owned(),
8454        ]);
8455        let without_implicit_c14n = HashSet::from([BASE64_TRANSFORM_URI.to_owned()]);
8456
8457        for transforms in [
8458            vec![Transform::Base64Decode, Transform::C14n(c14n)],
8459            vec![Transform::Base64Decode, Transform::Base64Decode],
8460        ] {
8461            let reference = make_reference("", transforms, DigestAlgorithm::Sha256, vec![0; 32]);
8462            enforce_reference_policies(
8463                std::slice::from_ref(&reference),
8464                UriTypeSet::default(),
8465                Some(&allowed),
8466            )
8467            .expect("terminal binary output must not require implicit C14N");
8468        }
8469
8470        let terminal_base64 = make_reference(
8471            "",
8472            vec![Transform::Base64Decode, Transform::Base64Decode],
8473            DigestAlgorithm::Sha256,
8474            vec![0; 32],
8475        );
8476        enforce_reference_policies(
8477            std::slice::from_ref(&terminal_base64),
8478            UriTypeSet::default(),
8479            Some(&without_implicit_c14n),
8480        )
8481        .expect("terminal Base64 output must not require implicit C14N");
8482
8483        let no_transforms = make_reference("", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
8484        let error = enforce_reference_policies(
8485            std::slice::from_ref(&no_transforms),
8486            UriTypeSet::default(),
8487            Some(&without_implicit_c14n),
8488        )
8489        .expect_err("a node-set result must require allowlisted implicit C14N");
8490        assert!(matches!(
8491            error,
8492            SignatureVerificationPipelineError::Policy(
8493                crate::policy::PolicyViolation::Algorithm {
8494                    operation: "verification transform",
8495                    ref algorithm,
8496                }
8497            )
8498                if algorithm == DEFAULT_IMPLICIT_C14N_URI
8499        ));
8500
8501        let detached = make_reference("urn:payload", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
8502        enforce_reference_policies(
8503            std::slice::from_ref(&detached),
8504            UriTypeSet::ALL,
8505            Some(&without_implicit_c14n),
8506        )
8507        .expect("external octets without transforms must not require implicit C14N");
8508
8509        let external_xpath = make_reference(
8510            "urn:payload",
8511            vec![Transform::XPath(
8512                super::super::transforms::XPathExpression::new("true()"),
8513            )],
8514            DigestAlgorithm::Sha256,
8515            vec![0; 32],
8516        );
8517        let error = enforce_reference_policies(
8518            std::slice::from_ref(&external_xpath),
8519            UriTypeSet::ALL,
8520            Some(&HashSet::from([XPATH_TRANSFORM_URI.to_owned()])),
8521        )
8522        .expect_err("external XML converted to a node-set must require implicit C14N");
8523        assert!(matches!(
8524            error,
8525            SignatureVerificationPipelineError::Policy(
8526                crate::policy::PolicyViolation::Algorithm {
8527                    operation: "verification transform",
8528                    ref algorithm,
8529                }
8530            )
8531                if algorithm == DEFAULT_IMPLICIT_C14N_URI
8532        ));
8533    }
8534
8535    #[test]
8536    fn stored_pre_digest_budget_counts_repeated_external_references() {
8537        // The caller map owns one bounded payload, but diagnostic retention is
8538        // charged per Reference because every result owns its pre-digest bytes.
8539        let document =
8540            Document::parse("<ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"/>")
8541                .unwrap();
8542        let payload = vec![b'x'; 7];
8543        let digest = compute_digest(DigestAlgorithm::Sha256, &payload);
8544        let references = (0..5)
8545            .map(|_| {
8546                make_reference(
8547                    "urn:repeated",
8548                    Vec::new(),
8549                    DigestAlgorithm::Sha256,
8550                    digest.clone(),
8551                )
8552            })
8553            .collect::<Vec<_>>();
8554        let resources = HashMap::from([("urn:repeated".to_owned(), payload)]);
8555        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
8556        let transform_budget = TransformExecutionBudget::default();
8557        let canonicalized_data_budget = CanonicalizedDataBudget::with_limit(32);
8558        let execution = ReferenceExecutionContext {
8559            store_pre_digest: true,
8560            transform_options: TransformOptions::default(),
8561            transform_budget: &transform_budget,
8562            canonicalized_data_budget: &canonicalized_data_budget,
8563            provider: crate::provider::default_provider(),
8564        };
8565
8566        let error = process_all_references_with_options(
8567            &references,
8568            &resolver,
8569            document.root_element(),
8570            &execution,
8571        )
8572        .expect_err(
8573            "retained diagnostics must not multiply one external allocation past the aggregate cap",
8574        );
8575        assert!(matches!(
8576            error,
8577            ReferenceProcessingError::Policy(crate::policy::PolicyViolation::ResourceLimit {
8578                resource: "canonicalized bytes",
8579                maximum: 32,
8580                ..
8581            })
8582        ));
8583    }
8584
8585    #[test]
8586    fn operation_memoizes_repeated_external_resource_identity() {
8587        // The external resource map is immutably borrowed for the operation,
8588        // so one content fingerprint safely identifies every repeated URI.
8589        let document = XmlDocument::parse("<root/>").expect("fixture must parse");
8590        let resources = HashMap::from([("urn:repeated".to_owned(), vec![b'x'; 8 * 1_024])]);
8591        let reference = make_reference(
8592            "urn:repeated",
8593            Vec::new(),
8594            DigestAlgorithm::Sha256,
8595            vec![0; 32],
8596        );
8597        let budgets = VerificationOperationBudgets::with_transforms(
8598            &crate::policy::VerificationPolicy::default(),
8599            TransformExecutionBudget::default(),
8600        );
8601
8602        document.with_view(|view| {
8603            let resolver =
8604                UriReferenceResolver::new(view.document()).with_external_resources(&resources);
8605            let first =
8606                budgets.resource_identity_for_reference(&reference, 0, &resolver, view, None);
8607            let second =
8608                budgets.resource_identity_for_reference(&reference, 1, &resolver, view, None);
8609
8610            assert_eq!(first, second);
8611            assert_eq!(budgets.external_resource_identities.borrow().len(), 1);
8612        });
8613    }
8614
8615    #[test]
8616    fn canonical_signed_info_obeys_policy_without_diagnostic_retention() {
8617        // SignedInfo is always materialized for crypto verification, so its
8618        // canonical bytes must consume the configured ceiling even when
8619        // diagnostics do not retain reference output.
8620        let xml = signature_with_target_reference("AQ==");
8621        let marker = "<ds:SignatureMethod";
8622        let padding = " ".repeat(1_025);
8623        let xml = xml.replacen(marker, &format!("{padding}{marker}"), 1);
8624        let policy = crate::policy::VerificationPolicy {
8625            resources: crate::policy::ResourcePolicy {
8626                max_canonicalized_bytes: 1_024,
8627                ..crate::policy::ResourcePolicy::default()
8628            },
8629            ..crate::policy::VerificationPolicy::default()
8630        };
8631
8632        let error = VerifyContext::new()
8633            .key(&AcceptingKey)
8634            .policy(policy)
8635            .verify(&xml)
8636            .expect_err("canonicalized SignedInfo must remain policy-bounded");
8637
8638        assert!(matches!(
8639            error,
8640            SignatureVerificationPipelineError::Policy(
8641                crate::policy::PolicyViolation::ResourceLimit {
8642                    resource: "canonicalized bytes",
8643                    ..
8644                }
8645            )
8646        ));
8647    }
8648
8649    #[test]
8650    fn fragmented_pq_signature_value_decodes_at_exact_wire_limit() {
8651        // Comments split the largest supported signature into tiny text nodes;
8652        // decoding must preserve bytes and reject an oversized aggregate.
8653        let parameter = super::super::pq_algorithm::PqAlgorithm::SlhDsaSha2_256f;
8654        let bytes = vec![0x5a; parameter.signature_len()];
8655        let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes);
8656        let mut xml = String::from("<SignatureValue>");
8657        for chunk in encoded.as_bytes().chunks(4) {
8658            xml.push_str(std::str::from_utf8(chunk).unwrap());
8659            xml.push_str("<!--split-->\n");
8660        }
8661        xml.push_str("</SignatureValue>");
8662        let document = Document::parse(&xml).unwrap();
8663        let algorithm = SignatureAlgorithm::PostQuantum(parameter);
8664        assert_eq!(
8665            decode_signature_value(
8666                document.root_element(),
8667                algorithm,
8668                &crate::policy::ResourcePolicy::default()
8669            )
8670            .unwrap(),
8671            bytes
8672        );
8673        let oversized = xml.replace("</SignatureValue>", "AAAA</SignatureValue>");
8674        let document = Document::parse(&oversized).unwrap();
8675        assert!(matches!(
8676            decode_signature_value(
8677                document.root_element(),
8678                algorithm,
8679                &crate::policy::ResourcePolicy::default()
8680            ),
8681            Err(SignatureVerificationPipelineError::InvalidStructure {
8682                reason: "SignatureValue exceeds maximum allowed length"
8683            })
8684        ));
8685    }
8686
8687    #[test]
8688    fn push_normalized_signature_text_rejects_form_feed() {
8689        let mut normalized = Vec::new();
8690        let mut raw_text_len = 0usize;
8691        let err = push_normalized_signature_text(
8692            "ab\u{000C}cd",
8693            &mut raw_text_len,
8694            &mut normalized,
8695            SignatureAlgorithm::RsaSha256,
8696            &crate::policy::ResourcePolicy::default(),
8697        )
8698        .expect_err("form-feed must not be treated as XML base64 whitespace");
8699        assert!(matches!(
8700            err,
8701            SignatureVerificationPipelineError::SignatureValueBase64(
8702                base64::DecodeError::InvalidByte(_, 0x0C)
8703            )
8704        ));
8705    }
8706
8707    #[test]
8708    fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
8709        let mut normalized = vec![b'A'; MAX_SIGNATURE_VALUE_LEN - 1];
8710        let mut raw_text_len = normalized.len();
8711        let err = push_normalized_signature_text(
8712            "é",
8713            &mut raw_text_len,
8714            &mut normalized,
8715            SignatureAlgorithm::RsaSha256,
8716            &crate::policy::ResourcePolicy::default(),
8717        )
8718        .expect_err("multibyte characters must not bypass byte-size limit");
8719        assert!(matches!(
8720            err,
8721            SignatureVerificationPipelineError::InvalidStructure {
8722                reason: "SignatureValue exceeds maximum allowed length"
8723            }
8724        ));
8725    }
8726
8727    // ── process_reference: happy path ────────────────────────────────
8728
8729    #[test]
8730    fn reference_with_correct_digest_passes() {
8731        // Create a simple document, compute its canonical form digest,
8732        // then verify that process_reference returns Valid status.
8733        let xml = r##"<root>
8734            <data>hello world</data>
8735            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
8736                <ds:SignedInfo/>
8737            </ds:Signature>
8738        </root>"##;
8739        let doc = Document::parse(xml).unwrap();
8740        let resolver = UriReferenceResolver::new(&doc);
8741        let sig_node = doc
8742            .descendants()
8743            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
8744            .unwrap();
8745
8746        // First, compute the expected digest by running the pipeline
8747        let initial_data = resolver.dereference("").unwrap();
8748        let transforms = vec![
8749            Transform::Enveloped,
8750            Transform::C14n(
8751                crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
8752                    .unwrap(),
8753            ),
8754        ];
8755        let pre_digest_bytes =
8756            crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
8757        let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
8758
8759        // Now build a Reference with the correct digest and verify
8760        let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
8761
8762        let result = process_reference(
8763            &reference,
8764            &resolver,
8765            sig_node,
8766            ReferenceSet::SignedInfo,
8767            0,
8768            false,
8769        )
8770        .unwrap();
8771        assert!(
8772            matches!(result.status, DsigStatus::Valid),
8773            "digest should match"
8774        );
8775        assert!(result.pre_digest_data.is_none());
8776    }
8777
8778    #[test]
8779    fn reference_with_wrong_digest_fails() {
8780        let xml = r##"<root>
8781            <data>hello</data>
8782            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8783                <ds:SignedInfo/>
8784            </ds:Signature>
8785        </root>"##;
8786        let doc = Document::parse(xml).unwrap();
8787        let resolver = UriReferenceResolver::new(&doc);
8788        let sig_node = doc
8789            .descendants()
8790            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
8791            .unwrap();
8792
8793        let transforms = vec![Transform::Enveloped];
8794        // Wrong digest value — all zeros
8795        let wrong_digest = vec![0u8; 32];
8796        let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
8797
8798        let result = process_reference(
8799            &reference,
8800            &resolver,
8801            sig_node,
8802            ReferenceSet::SignedInfo,
8803            0,
8804            false,
8805        )
8806        .unwrap();
8807        assert!(matches!(
8808            result.status,
8809            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
8810        ));
8811    }
8812
8813    #[test]
8814    fn reference_with_wrong_digest_preserves_supplied_ref_index() {
8815        let xml = r##"<root>
8816            <data>hello</data>
8817            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8818                <ds:SignedInfo/>
8819            </ds:Signature>
8820        </root>"##;
8821        let doc = Document::parse(xml).unwrap();
8822        let resolver = UriReferenceResolver::new(&doc);
8823        let sig_node = doc
8824            .descendants()
8825            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
8826            .unwrap();
8827
8828        let reference = make_reference(
8829            "",
8830            vec![Transform::Enveloped],
8831            DigestAlgorithm::Sha256,
8832            vec![0u8; 32],
8833        );
8834        let result = process_reference(
8835            &reference,
8836            &resolver,
8837            sig_node,
8838            ReferenceSet::SignedInfo,
8839            7,
8840            false,
8841        )
8842        .unwrap();
8843        assert!(matches!(
8844            result.status,
8845            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
8846        ));
8847    }
8848
8849    #[test]
8850    fn reference_stores_pre_digest_data() {
8851        let xml = "<root><child>text</child></root>";
8852        let doc = Document::parse(xml).unwrap();
8853        let resolver = UriReferenceResolver::new(&doc);
8854
8855        // No transforms, no enveloped — just canonicalize entire document
8856        let initial_data = resolver.dereference("").unwrap();
8857        let pre_digest =
8858            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
8859        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
8860
8861        let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
8862        let result = process_reference(
8863            &reference,
8864            &resolver,
8865            doc.root_element(),
8866            ReferenceSet::SignedInfo,
8867            0,
8868            true,
8869        )
8870        .unwrap();
8871
8872        assert!(matches!(result.status, DsigStatus::Valid));
8873        assert!(result.pre_digest_data.is_some());
8874        assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
8875    }
8876
8877    // ── process_reference: URI dereference ───────────────────────────
8878
8879    #[test]
8880    fn reference_with_id_uri() {
8881        let xml = r##"<root>
8882            <item ID="target">specific content</item>
8883            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
8884                <ds:SignedInfo/>
8885            </ds:Signature>
8886        </root>"##;
8887        let doc = Document::parse(xml).unwrap();
8888        let resolver = UriReferenceResolver::new(&doc);
8889        let sig_node = doc
8890            .descendants()
8891            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
8892            .unwrap();
8893
8894        // Compute expected digest for the #target subtree
8895        let initial_data = resolver.dereference("#target").unwrap();
8896        let transforms = vec![Transform::C14n(
8897            crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
8898                .unwrap(),
8899        )];
8900        let pre_digest =
8901            crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
8902        let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
8903
8904        let reference = make_reference(
8905            "#target",
8906            transforms,
8907            DigestAlgorithm::Sha256,
8908            expected_digest,
8909        );
8910        let result = process_reference(
8911            &reference,
8912            &resolver,
8913            sig_node,
8914            ReferenceSet::SignedInfo,
8915            0,
8916            false,
8917        )
8918        .unwrap();
8919        assert!(matches!(result.status, DsigStatus::Valid));
8920    }
8921
8922    #[test]
8923    fn reference_with_nonexistent_id_fails() {
8924        let xml = "<root><child/></root>";
8925        let doc = Document::parse(xml).unwrap();
8926        let resolver = UriReferenceResolver::new(&doc);
8927
8928        let reference =
8929            make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
8930        let result = process_reference(
8931            &reference,
8932            &resolver,
8933            doc.root_element(),
8934            ReferenceSet::SignedInfo,
8935            0,
8936            false,
8937        );
8938        assert!(result.is_err());
8939    }
8940
8941    #[test]
8942    fn reference_with_absent_uri_fails_closed() {
8943        let xml = "<root><child>text</child></root>";
8944        let doc = Document::parse(xml).unwrap();
8945        let resolver = UriReferenceResolver::new(&doc);
8946
8947        let reference = Reference {
8948            uri: None, // absent URI
8949            id: None,
8950            ref_type: None,
8951            transforms: vec![],
8952            digest_method: DigestAlgorithm::Sha256,
8953            digest_value: vec![0; 32],
8954        };
8955
8956        let result = process_reference(
8957            &reference,
8958            &resolver,
8959            doc.root_element(),
8960            ReferenceSet::SignedInfo,
8961            0,
8962            false,
8963        );
8964        assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
8965    }
8966
8967    // ── process_all_references: fail-fast ────────────────────────────
8968
8969    #[test]
8970    fn all_references_pass() {
8971        let xml = "<root><child>text</child></root>";
8972        let doc = Document::parse(xml).unwrap();
8973        let resolver = UriReferenceResolver::new(&doc);
8974
8975        // Compute correct digest
8976        let initial_data = resolver.dereference("").unwrap();
8977        let pre_digest =
8978            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
8979        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
8980
8981        let refs = vec![
8982            make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
8983            make_reference("", vec![], DigestAlgorithm::Sha256, digest),
8984        ];
8985
8986        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
8987        assert!(result.all_valid());
8988        assert_eq!(result.results.len(), 2);
8989        assert!(result.first_failure.is_none());
8990    }
8991
8992    #[test]
8993    fn reference_processing_shares_xpath_work_across_references() {
8994        // A signature-wide meter must not reset when processing the next
8995        // Reference, even though each transform chain is independently valid.
8996        let document = Document::parse("<root/>").unwrap();
8997        let resolver = UriReferenceResolver::new(&document);
8998        let transform = Transform::XPath(super::super::transforms::XPathExpression::new("true()"));
8999        let initial_data = resolver.dereference("").unwrap();
9000        let pre_digest = crate::xmldsig::execute_transforms(
9001            document.root_element(),
9002            initial_data,
9003            std::slice::from_ref(&transform),
9004        )
9005        .unwrap();
9006        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
9007        let references = vec![
9008            make_reference(
9009                "",
9010                vec![transform.clone()],
9011                DigestAlgorithm::Sha256,
9012                digest.clone(),
9013            ),
9014            make_reference("", vec![transform], DigestAlgorithm::Sha256, digest),
9015        ];
9016        let budget = TransformExecutionBudget::with_xpath_limit(12);
9017        let canonicalized_data_budget = CanonicalizedDataBudget::default();
9018        let execution = ReferenceExecutionContext {
9019            store_pre_digest: false,
9020            transform_options: TransformOptions::default(),
9021            transform_budget: &budget,
9022            canonicalized_data_budget: &canonicalized_data_budget,
9023            provider: crate::provider::default_provider(),
9024        };
9025
9026        let error = process_all_references_with_options(
9027            &references,
9028            &resolver,
9029            document.root_element(),
9030            &execution,
9031        )
9032        .expect_err("the second Reference must consume the first Reference's XPath work");
9033
9034        assert!(matches!(
9035            error,
9036            ReferenceProcessingError::Transform(TransformError::Policy(
9037                crate::policy::PolicyViolation::ResourceLimit {
9038                    resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
9039                    ..
9040                }
9041            ))
9042        ));
9043    }
9044
9045    #[test]
9046    fn reference_processing_shares_node_set_materialization_across_references() {
9047        // Repeated references to the same small subtree must share one owned-
9048        // string budget. Otherwise a large inherited namespace can be cloned
9049        // once per Reference even when canonicalization emits little output.
9050        let document = Document::parse(
9051            r#"<root xmlns:n="urn:0123456789"><target Id="selected">payload</target></root>"#,
9052        )
9053        .unwrap();
9054        let resolver = UriReferenceResolver::new(&document);
9055        let initial_data = resolver.dereference("#selected").unwrap();
9056        let pre_digest =
9057            crate::xmldsig::execute_transforms(document.root_element(), initial_data, &[]).unwrap();
9058        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
9059        let references = vec![
9060            make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest.clone()),
9061            make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest),
9062        ];
9063        let budget = TransformExecutionBudget::with_node_set_materialization_limit(30);
9064        let canonicalized_data_budget = CanonicalizedDataBudget::default();
9065        let execution = ReferenceExecutionContext {
9066            store_pre_digest: false,
9067            transform_options: TransformOptions::default(),
9068            transform_budget: &budget,
9069            canonicalized_data_budget: &canonicalized_data_budget,
9070            provider: crate::provider::default_provider(),
9071        };
9072
9073        let error = process_all_references_with_options(
9074            &references,
9075            &resolver,
9076            document.root_element(),
9077            &execution,
9078        )
9079        .expect_err("the second Reference must consume the first Reference's materialization work");
9080
9081        assert!(matches!(
9082            error,
9083            ReferenceProcessingError::UriDereference(TransformError::Policy(
9084                crate::policy::PolicyViolation::ResourceLimit {
9085                    resource: crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
9086                    ..
9087                }
9088            ))
9089        ));
9090    }
9091
9092    #[test]
9093    fn fail_fast_on_first_mismatch() {
9094        let xml = "<root><child>text</child></root>";
9095        let doc = Document::parse(xml).unwrap();
9096        let resolver = UriReferenceResolver::new(&doc);
9097
9098        let wrong_digest = vec![0u8; 32];
9099        let refs = vec![
9100            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
9101            // Second reference should NOT be processed
9102            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
9103        ];
9104
9105        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
9106        assert!(!result.all_valid());
9107        assert_eq!(result.first_failure, Some(0));
9108        // Only first reference should be in results (fail-fast)
9109        assert_eq!(result.results.len(), 1);
9110        assert!(matches!(
9111            result.results[0].status,
9112            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
9113        ));
9114    }
9115
9116    #[test]
9117    fn fail_fast_second_reference() {
9118        let xml = "<root><child>text</child></root>";
9119        let doc = Document::parse(xml).unwrap();
9120        let resolver = UriReferenceResolver::new(&doc);
9121
9122        // Compute correct digest for first ref
9123        let initial_data = resolver.dereference("").unwrap();
9124        let pre_digest =
9125            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
9126        let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
9127        let wrong_digest = vec![0u8; 32];
9128
9129        let refs = vec![
9130            make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
9131            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
9132        ];
9133
9134        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
9135        assert!(!result.all_valid());
9136        assert_eq!(result.first_failure, Some(1));
9137        // Both references should be in results
9138        assert_eq!(result.results.len(), 2);
9139        assert!(matches!(result.results[0].status, DsigStatus::Valid));
9140        assert!(matches!(
9141            result.results[1].status,
9142            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
9143        ));
9144    }
9145
9146    #[test]
9147    fn empty_references_list() {
9148        let xml = "<root/>";
9149        let doc = Document::parse(xml).unwrap();
9150        let resolver = UriReferenceResolver::new(&doc);
9151
9152        let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
9153        assert!(result.all_valid());
9154        assert!(result.results.is_empty());
9155    }
9156
9157    // ── Digest algorithms ────────────────────────────────────────────
9158
9159    #[test]
9160    fn reference_sha1_digest() {
9161        let xml = "<root>content</root>";
9162        let doc = Document::parse(xml).unwrap();
9163        let resolver = UriReferenceResolver::new(&doc);
9164
9165        let initial_data = resolver.dereference("").unwrap();
9166        let pre_digest =
9167            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
9168        let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
9169
9170        let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
9171        let result = process_reference(
9172            &reference,
9173            &resolver,
9174            doc.root_element(),
9175            ReferenceSet::SignedInfo,
9176            0,
9177            false,
9178        )
9179        .unwrap();
9180        assert!(matches!(result.status, DsigStatus::Valid));
9181        assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
9182    }
9183
9184    #[test]
9185    fn reference_sha512_digest() {
9186        let xml = "<root>content</root>";
9187        let doc = Document::parse(xml).unwrap();
9188        let resolver = UriReferenceResolver::new(&doc);
9189
9190        let initial_data = resolver.dereference("").unwrap();
9191        let pre_digest =
9192            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
9193        let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
9194
9195        let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
9196        let result = process_reference(
9197            &reference,
9198            &resolver,
9199            doc.root_element(),
9200            ReferenceSet::SignedInfo,
9201            0,
9202            false,
9203        )
9204        .unwrap();
9205        assert!(matches!(result.status, DsigStatus::Valid));
9206        assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
9207    }
9208
9209    // ── SAML-like end-to-end ─────────────────────────────────────────
9210
9211    #[test]
9212    fn saml_enveloped_reference_processing() {
9213        // Realistic SAML Response with enveloped signature
9214        let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
9215                                     xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
9216                                     ID="_resp1">
9217            <saml:Assertion ID="_assert1">
9218                <saml:Subject>user@example.com</saml:Subject>
9219            </saml:Assertion>
9220            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
9221                <ds:SignedInfo>
9222                    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
9223                    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
9224                    <ds:Reference URI="">
9225                        <ds:Transforms>
9226                            <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
9227                            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
9228                        </ds:Transforms>
9229                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
9230                        <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
9231                    </ds:Reference>
9232                </ds:SignedInfo>
9233                <ds:SignatureValue>fakesig==</ds:SignatureValue>
9234            </ds:Signature>
9235        </samlp:Response>"##;
9236        let doc = Document::parse(xml).unwrap();
9237        let resolver = UriReferenceResolver::new(&doc);
9238        let sig_node = doc
9239            .descendants()
9240            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
9241            .unwrap();
9242
9243        // Parse SignedInfo to get the Reference
9244        let signed_info_node = sig_node
9245            .children()
9246            .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
9247            .unwrap();
9248        let signed_info = parse_signed_info(signed_info_node).unwrap();
9249        let reference = &signed_info.references[0];
9250
9251        // Compute the correct digest by running the actual pipeline
9252        let initial_data = resolver.dereference("").unwrap();
9253        let pre_digest =
9254            crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
9255                .unwrap();
9256        let correct_digest = compute_digest(reference.digest_method, &pre_digest);
9257
9258        // Build a reference with the correct digest
9259        let corrected_ref = make_reference(
9260            "",
9261            reference.transforms.clone(),
9262            reference.digest_method,
9263            correct_digest,
9264        );
9265
9266        // Verify: should pass
9267        let result = process_reference(
9268            &corrected_ref,
9269            &resolver,
9270            sig_node,
9271            ReferenceSet::SignedInfo,
9272            0,
9273            true,
9274        )
9275        .unwrap();
9276        assert!(
9277            matches!(result.status, DsigStatus::Valid),
9278            "SAML reference should verify"
9279        );
9280        assert!(result.pre_digest_data.is_some());
9281
9282        // Verify the pre-digest data contains the canonicalized document without Signature
9283        let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
9284        assert!(
9285            pre_digest_str.contains("samlp:Response"),
9286            "pre-digest should contain Response"
9287        );
9288        assert!(
9289            !pre_digest_str.contains("SignatureValue"),
9290            "pre-digest should NOT contain Signature"
9291        );
9292    }
9293
9294    #[test]
9295    fn pipeline_missing_signed_info_returns_missing_element() {
9296        let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
9297
9298        let err = verify_signature_with_pem_key(xml, "dummy-key", false)
9299            .expect_err("missing SignedInfo must fail before crypto stage");
9300        assert!(matches!(
9301            err,
9302            SignatureVerificationPipelineError::MissingElement {
9303                element: "SignedInfo"
9304            }
9305        ));
9306    }
9307
9308    #[test]
9309    fn pipeline_multiple_signature_elements_are_rejected() {
9310        let xml = r#"
9311<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
9312  <ds:Signature>
9313    <ds:SignedInfo/>
9314  </ds:Signature>
9315  <ds:Signature/>
9316</root>
9317"#;
9318
9319        let err = verify_signature_with_pem_key(xml, "dummy-key", false)
9320            .expect_err("multiple signatures must fail closed");
9321        assert!(matches!(
9322            err,
9323            SignatureVerificationPipelineError::InvalidStructure {
9324                reason: "Signature must appear exactly once in document",
9325            }
9326        ));
9327    }
9328
9329    #[test]
9330    fn pipeline_start_node_limits_signature_cardinality_to_its_subtree() {
9331        // A start-node selector changes the operation root, not global ID or
9332        // reference resolution; another Signature outside the subtree is irrelevant.
9333        let xml = r#"
9334<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
9335  <scope Id="selected"><ds:Signature/></scope>
9336  <scope Id="other"><ds:Signature/></scope>
9337</root>
9338"#;
9339        let err = VerifyContext::new()
9340            .start_node_id("selected")
9341            .verify(xml)
9342            .expect_err("the selected Signature remains structurally incomplete");
9343        assert!(matches!(
9344            err,
9345            SignatureVerificationPipelineError::MissingElement {
9346                element: "SignedInfo"
9347            }
9348        ));
9349    }
9350
9351    #[test]
9352    fn pipeline_reports_keyinfo_parse_error() {
9353        let xml = r#"
9354<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
9355              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
9356  <ds:SignedInfo>
9357    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
9358    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
9359    <ds:Reference URI="">
9360      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
9361      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
9362    </ds:Reference>
9363  </ds:SignedInfo>
9364  <ds:SignatureValue>AA==</ds:SignatureValue>
9365  <ds:KeyInfo>
9366    <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
9367  </ds:KeyInfo>
9368</ds:Signature>
9369"#;
9370
9371        let err = VerifyContext::new().verify(xml).expect_err(
9372            "invalid KeyInfo must map to ParseKeyInfo when no explicit key is supplied",
9373        );
9374        assert!(matches!(
9375            err,
9376            SignatureVerificationPipelineError::ParseKeyInfo(_)
9377        ));
9378    }
9379
9380    #[test]
9381    fn pipeline_ignores_malformed_keyinfo_when_explicit_key_is_supplied() {
9382        let base_xml = signature_with_target_reference("AQ==");
9383        let xml = base_xml
9384            .replace(
9385                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
9386                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
9387            )
9388            .replace(
9389                "</ds:SignatureValue>\n  </ds:Signature>",
9390                "</ds:SignatureValue>\n    <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n  </ds:Signature>",
9391            );
9392
9393        let result = VerifyContext::new()
9394            .key(&RejectingKey)
9395            .verify(&xml)
9396            .expect("explicit key path should not fail on malformed KeyInfo");
9397        assert!(matches!(
9398            result.status,
9399            DsigStatus::Invalid(FailureReason::SignatureMismatch)
9400        ));
9401    }
9402
9403    #[test]
9404    fn pipeline_rejects_foreign_element_children_under_signature() {
9405        let base_xml = signature_with_target_reference("AQ==");
9406        let xml = base_xml
9407            .replace(
9408                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
9409                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:foo="urn:example:foo">"#,
9410            )
9411            .replace(
9412                "</ds:SignedInfo>\n    <ds:SignatureValue>",
9413                "</ds:SignedInfo>\n    <foo:Bar/>\n    <ds:SignatureValue>",
9414            );
9415
9416        let err = VerifyContext::new()
9417            .key(&RejectingKey)
9418            .verify(&xml)
9419            .expect_err("foreign element children under Signature must fail closed");
9420        assert!(matches!(
9421            err,
9422            SignatureVerificationPipelineError::InvalidStructure {
9423                reason: "Signature must contain only XMLDSIG element children",
9424            }
9425        ));
9426    }
9427
9428    #[test]
9429    fn pipeline_rejects_non_whitespace_mixed_content_under_signature() {
9430        let base_xml = signature_with_target_reference("AQ==");
9431        let xml = base_xml.replace(
9432            "</ds:SignedInfo>\n    <ds:SignatureValue>",
9433            "</ds:SignedInfo>\n    oops\n    <ds:SignatureValue>",
9434        );
9435
9436        let err = VerifyContext::new()
9437            .key(&RejectingKey)
9438            .verify(&xml)
9439            .expect_err("non-whitespace mixed content under Signature must fail closed");
9440        assert!(matches!(
9441            err,
9442            SignatureVerificationPipelineError::InvalidStructure {
9443                reason: "Signature must not contain non-whitespace mixed content",
9444            }
9445        ));
9446    }
9447
9448    #[test]
9449    fn pipeline_rejects_keyinfo_out_of_order() {
9450        let base_xml = signature_with_target_reference("AQ==");
9451        let xml = base_xml.replace(
9452            "</ds:SignatureValue>\n  </ds:Signature>",
9453            "</ds:SignatureValue>\n    <ds:Object/>\n    <ds:KeyInfo><ds:KeyName>late</ds:KeyName></ds:KeyInfo>\n  </ds:Signature>",
9454        );
9455
9456        let err = VerifyContext::new()
9457            .key(&RejectingKey)
9458            .verify(&xml)
9459            .expect_err("KeyInfo after Object must be rejected by Signature child order checks");
9460        assert!(matches!(
9461            err,
9462            SignatureVerificationPipelineError::InvalidStructure {
9463                reason: "KeyInfo must be the third element child of Signature when present"
9464            }
9465        ));
9466    }
9467
9468    #[test]
9469    fn pipeline_accepts_comments_and_processing_instructions_under_signature() {
9470        let xml = r#"
9471<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
9472  <?dbg keep ?>
9473  <!-- signature metadata -->
9474  <ds:SignedInfo>
9475    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
9476    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
9477    <ds:Reference URI="">
9478      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
9479      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
9480    </ds:Reference>
9481  </ds:SignedInfo>
9482  <!-- between required children -->
9483  <ds:SignatureValue>AA==</ds:SignatureValue>
9484</ds:Signature>
9485"#;
9486
9487        let doc = Document::parse(xml).expect("test XML must parse");
9488        let signature_node = doc.root_element();
9489        let parsed = parse_signature_children(signature_node)
9490            .expect("comment/PI nodes under Signature must be ignored");
9491
9492        assert_eq!(parsed.signed_info_node.tag_name().name(), "SignedInfo");
9493        assert_eq!(
9494            parsed.signature_value_node.tag_name().name(),
9495            "SignatureValue"
9496        );
9497        assert!(parsed.key_info_node.is_none());
9498    }
9499}