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 base64::Engine;
14use roxmltree::{Document, Node, NodeId};
15use std::cell::Cell;
16use std::collections::{HashMap, HashSet};
17
18use crate::c14n::canonicalize_bounded_with_xml_base_budget;
19use crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING;
20
21#[cfg(test)]
22use super::digest::compute_digest;
23use super::digest::{DigestAlgorithm, constant_time_eq};
24#[cfg(test)]
25use super::parse::MAX_REFERENCES_PER_SIGNATURE;
26#[cfg(test)]
27use super::parse::parse_key_info;
28use super::parse::{
29    KeyInfo, MAX_X509_DATA_TOTAL_BINARY_LEN, MAX_X509_DECODED_BINARY_LEN, ParseError, Reference,
30    RetrievalMethodTransforms, SignatureAlgorithm, XMLDSIG_NS,
31};
32use super::parse::{
33    parse_key_info_with_policy_budgets, parse_reference_with_xpath_budget,
34    parse_signed_info_with_xpath_budget, parse_x509_certificate,
35    parse_x509_data_dispatch_with_budget_and_provider, reference_digest_method,
36};
37use super::signature::{
38    SignatureVerificationError, verify_dsa_signature_spki, verify_ecdsa_signature_pem,
39    verify_rsa_signature_pem,
40};
41#[cfg(test)]
42use super::transforms::BASE64_TRANSFORM_URI;
43use super::transforms::{
44    DEFAULT_IMPLICIT_C14N_URI, Transform, TransformExecutionBudget, TransformOptions,
45    XPATH_TRANSFORM_URI, XPathHereSemantics, XPathSignatureParseBudget,
46    execute_transforms_with_options_and_budget, map_c14n_resource_policy_violation,
47    transform_chain_produces_binary,
48};
49use super::types::{NodeSet, TransformError};
50use super::uri::{UriReferenceResolver, same_document_reference_id};
51use super::whitespace::{is_xml_whitespace_only, normalize_xml_base64_bytes};
52
53const MAX_SIGNATURE_VALUE_LEN: usize = 8192;
54const MAX_SIGNATURE_VALUE_TEXT_LEN: usize = 65_536;
55const MAX_RETRIEVAL_METHOD_COUNT: usize = 64;
56/// Cryptographic verifier used by [`VerifyContext`].
57///
58/// This trait intentionally has no `Send + Sync` supertraits so lightweight
59/// single-threaded verifiers can be used without additional bounds.
60pub trait VerifyingKey {
61    /// Validate this key against the operation's immutable trust policy.
62    ///
63    /// Built-in keys override this hook so pre-resolved and resolver-produced
64    /// keys enforce identical strength constraints. Custom opaque keys may
65    /// retain their own policy enforcement by accepting the default no-op.
66    fn validate_policy(
67        &self,
68        _policy: &crate::policy::VerificationPolicy,
69    ) -> Result<(), DsigError> {
70        Ok(())
71    }
72
73    /// Check that `signature_value` has the wire framing required by the
74    /// declared algorithm and this key before provider dispatch.
75    ///
76    /// Key implementations with key-size-dependent framing should override
77    /// this method. The default enforces the algorithm-wide XMLDSig envelope.
78    fn validate_signature_value(
79        &self,
80        algorithm: SignatureAlgorithm,
81        signature_value: &[u8],
82    ) -> Result<bool, DsigError> {
83        Ok(super::signature::signature_value_matches_algorithm(
84            algorithm,
85            signature_value,
86        ))
87    }
88
89    /// Verify `signature_value` over `signed_data` with the declared algorithm.
90    fn verify(
91        &self,
92        algorithm: SignatureAlgorithm,
93        signed_data: &[u8],
94        signature_value: &[u8],
95    ) -> Result<bool, DsigError>;
96}
97
98/// Key resolver hook used by [`VerifyContext`] when no pre-set key is provided.
99///
100/// This trait intentionally has no `Send + Sync` supertraits; callers that need
101/// cross-thread sharing can wrap resolvers/keys in their own thread-safe types.
102pub trait KeyResolver {
103    /// Resolve a verification key from parsed `<KeyInfo>` sources.
104    ///
105    /// Return `Ok(None)` when no suitable key could be resolved from available
106    /// key material (for example, missing `<KeyInfo>` candidates). `VerifyContext`
107    /// maps `Ok(None)` to `DsigStatus::Invalid(FailureReason::KeyNotFound)`;
108    /// reserve `Err(...)` for resolver failures.
109    fn resolve<'a>(
110        &'a self,
111        key_info: Option<&KeyInfo>,
112        algorithm: SignatureAlgorithm,
113    ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError>;
114
115    /// Resolve under the operation's immutable policy snapshot.
116    ///
117    /// Implementations that make trust or key-source decisions must override
118    /// this method. Resolver implementations that inspect multiple candidates
119    /// must enforce [`crate::policy::ResourcePolicy::max_key_candidates`] across
120    /// that internal search. The verification pipeline separately requires
121    /// capacity for the single candidate returned by any resolver. The default
122    /// preserves source-only custom resolvers whose behavior is independent of
123    /// other policy fields.
124    fn resolve_with_policy<'a>(
125        &'a self,
126        key_info: Option<&KeyInfo>,
127        algorithm: SignatureAlgorithm,
128        _policy: &crate::policy::VerificationPolicy,
129    ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
130        self.resolve(key_info, algorithm)
131    }
132
133    /// Resolve under both the operation policy and cryptographic provider.
134    ///
135    /// Resolvers that evaluate cryptographic key metadata, such as
136    /// `X509Digest`, must override this hook. The default keeps existing
137    /// policy-aware custom resolvers source-compatible.
138    fn resolve_with_policy_and_provider<'a>(
139        &'a self,
140        key_info: Option<&KeyInfo>,
141        algorithm: SignatureAlgorithm,
142        policy: &crate::policy::VerificationPolicy,
143        _provider: &dyn crate::provider::CryptoProvider,
144    ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError> {
145        self.resolve_with_policy(key_info, algorithm, policy)
146    }
147
148    /// Return `true` when this resolver consumes document `<KeyInfo>` material.
149    ///
150    /// The verification pipeline uses this to decide whether malformed
151    /// `<KeyInfo>` should raise `DsigError::ParseKeyInfo` before resolver
152    /// execution. Resolvers that ignore document key material can keep the
153    /// default `false` to avoid fail-closed parsing on advisory `<KeyInfo>`.
154    fn consumes_document_key_info(&self) -> bool {
155        false
156    }
157}
158
159/// Allowed URI classes for `<Reference URI="...">`.
160///
161/// External URIs resolve only from bytes supplied through
162/// [`VerifyContext::external_resources`]; allowing them never enables I/O.
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164#[must_use = "pass the policy to VerifyContext::allowed_uri_types(), or store it for reuse"]
165pub struct UriTypeSet {
166    allow_empty: bool,
167    allow_same_document: bool,
168    allow_external: bool,
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172enum UriClass {
173    Empty,
174    SameDocument,
175    External,
176}
177
178fn classify_uri(uri: &str) -> UriClass {
179    if uri.is_empty() {
180        UriClass::Empty
181    } else if uri.starts_with('#') {
182        UriClass::SameDocument
183    } else {
184        UriClass::External
185    }
186}
187
188impl UriTypeSet {
189    /// Create a custom URI policy.
190    pub const fn new(allow_empty: bool, allow_same_document: bool, allow_external: bool) -> Self {
191        Self {
192            allow_empty,
193            allow_same_document,
194            allow_external,
195        }
196    }
197
198    /// Allow only same-document references (`""`, `#id`, `#xpointer(...)`).
199    pub const SAME_DOCUMENT: Self = Self {
200        allow_empty: true,
201        allow_same_document: true,
202        allow_external: false,
203    };
204
205    /// Allow all URI classes.
206    ///
207    /// External URIs still require an explicit caller-owned resource map.
208    pub const ALL: Self = Self {
209        allow_empty: true,
210        allow_same_document: true,
211        allow_external: true,
212    };
213
214    pub(crate) fn allows(self, uri: &str) -> bool {
215        match classify_uri(uri) {
216            UriClass::Empty => self.allow_empty,
217            UriClass::SameDocument => self.allow_same_document,
218            UriClass::External => self.allow_external,
219        }
220    }
221}
222
223impl Default for UriTypeSet {
224    fn default() -> Self {
225        Self::SAME_DOCUMENT
226    }
227}
228
229/// Request-scoped selection of the XMLDSig operation node.
230#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
231pub enum SignatureSelection<'a> {
232    /// Require exactly one `Signature` in the complete document.
233    #[default]
234    UniqueDocumentSignature,
235    /// Select the first descendant `Signature` from the document root.
236    FirstDocumentSignature,
237    /// Select the first descendant `Signature` below the element with this ID.
238    FirstSignatureUnderId(&'a str),
239}
240
241/// Verification builder/configuration.
242#[must_use = "configure the context and call verify(), or store it for reuse"]
243pub struct VerifyContext<'a> {
244    key: Option<&'a dyn VerifyingKey>,
245    key_resolver: Option<&'a dyn KeyResolver>,
246    policy: crate::policy::VerificationPolicy,
247    provider: &'a dyn crate::provider::CryptoProvider,
248    store_pre_digest: bool,
249    external_resources: Option<&'a HashMap<String, Vec<u8>>>,
250    signature_selection: SignatureSelection<'a>,
251    id_attributes: &'a [crate::IdAttributeRegistration],
252}
253
254impl<'a> VerifyContext<'a> {
255    /// Create a context with conservative defaults.
256    ///
257    /// Defaults:
258    /// - no pre-set key, no key resolver
259    /// - manifests disabled
260    /// - same-document URIs only
261    /// - all transforms allowed
262    /// - pre-digest buffers not stored
263    pub fn new() -> Self {
264        Self {
265            key: None,
266            key_resolver: None,
267            policy: crate::policy::VerificationPolicy::default(),
268            provider: crate::provider::default_provider(),
269            store_pre_digest: false,
270            external_resources: None,
271            signature_selection: SignatureSelection::UniqueDocumentSignature,
272            id_attributes: &[],
273        }
274    }
275
276    /// Set a pre-resolved verification key.
277    ///
278    /// Built-in [`super::VerificationKey`] values are validated against the
279    /// same operation key-strength policy as resolver-produced keys. Custom
280    /// opaque [`VerifyingKey`] implementations retain responsibility for any
281    /// key metadata that the core cannot inspect.
282    pub fn key(mut self, key: &'a dyn VerifyingKey) -> Self {
283        self.key = Some(key);
284        self
285    }
286
287    /// Set a key resolver fallback used when `key()` is not provided.
288    pub fn key_resolver(mut self, resolver: &'a dyn KeyResolver) -> Self {
289        self.key_resolver = Some(resolver);
290        self
291    }
292
293    /// Replace the complete immutable verification policy snapshot.
294    pub fn policy(mut self, policy: crate::policy::VerificationPolicy) -> Self {
295        self.policy = policy;
296        self
297    }
298
299    /// Select the cryptographic provider for this verification operation.
300    pub fn provider(mut self, provider: &'a dyn crate::provider::CryptoProvider) -> Self {
301        self.provider = provider;
302        self
303    }
304
305    /// Enable or disable `<Manifest>` processing.
306    ///
307    /// When enabled, references in `<ds:Manifest>` elements that are direct
308    /// element children of `<ds:Object>` are processed only when the direct-child
309    /// `<ds:Object>` or `<ds:Manifest>` itself is referenced from `<SignedInfo>`
310    /// by an ID-based same-document fragment URI such as `#id` or
311    /// `#xpointer(id('id'))`, and that reference uses only canonicalization
312    /// transforms (or implicit canonicalization). Filtering or binary transforms
313    /// do not prove that the complete Manifest structure was authenticated.
314    /// Only those signed Manifest references are returned in
315    /// `VerifyResult::manifest_references`.
316    /// Manifest parsing begins only after every `<SignedInfo>` reference digest
317    /// validates; a failure returns immediately with no Manifest results.
318    /// Nested `<ds:Manifest>` descendants under `<ds:Object>` are not
319    /// processed.
320    /// Direct-child unsigned/unreferenced Manifests are skipped and do not
321    /// appear in `VerifyResult::manifest_references`.
322    /// Whole-document same-document references such as `URI=""` or
323    /// `URI="#xpointer(/)"` do not mark a specific direct-child
324    /// `<ds:Object>`/`<ds:Manifest>` as signed for this option.
325    ///
326    /// Manifests are parsed and processed only after the SignedInfo references
327    /// and SignatureValue both validate. Their digest mismatches, policy
328    /// violations, and processing failures are then reported independently in
329    /// `VerifyResult::manifest_references` and do not alter `VerifyResult::status`.
330    /// Callers that enable `process_manifests(true)` must inspect
331    /// `VerifyResult::manifest_references` in addition to `VerifyResult::status`
332    /// when interpreting `verify()` results.
333    /// Structural/parse errors in Manifest content abort `verify()` and are
334    /// returned as `Err(...)`.
335    pub fn process_manifests(mut self, enabled: bool) -> Self {
336        self.policy.manifest_processing = if enabled {
337            crate::policy::ManifestProcessing::Process
338        } else {
339            crate::policy::ManifestProcessing::Ignore
340        };
341        self
342    }
343
344    /// Restrict allowed reference URI classes.
345    pub fn allowed_uri_types(mut self, types: UriTypeSet) -> Self {
346        self.policy.uris.references = types;
347        self
348    }
349
350    /// Restrict URI classes used to retrieve key material from `<KeyInfo>`.
351    ///
352    /// This policy is independent from [`Self::allowed_uri_types`]: allowing an
353    /// external signed payload does not implicitly allow external key retrieval.
354    /// Same-document retrieval is enabled by default; external retrieval requires
355    /// an explicit opt-in and still uses only caller-supplied resources.
356    pub fn allowed_retrieval_method_uri_types(mut self, types: UriTypeSet) -> Self {
357        self.policy.uris.retrieval_methods = types;
358        self
359    }
360
361    /// Provide external URI payloads explicitly.
362    ///
363    /// The map is the complete external I/O boundary: verification never
364    /// performs network or filesystem access. External URIs must also be
365    /// enabled through [`UriTypeSet`]. Map keys are RFC 3986 resolved URI
366    /// identities: use normalized paths with dot segments removed and retain
367    /// query or fragment suffixes.
368    pub fn external_resources(mut self, resources: &'a HashMap<String, Vec<u8>>) -> Self {
369        self.external_resources = Some(resources);
370        self
371    }
372
373    /// Select the operation start node by its XML ID value.
374    ///
375    /// Verification selects the first descendant `<Signature>` in document
376    /// order. This is request context, not a policy decision, and mirrors
377    /// libxmlsec1's depth-first `xmlSecFindNode` start-node contract.
378    pub fn start_node_id(mut self, id: &'a str) -> Self {
379        self.signature_selection = SignatureSelection::FirstSignatureUnderId(id);
380        self
381    }
382
383    /// Select the first descendant `<Signature>` from the document root.
384    ///
385    /// This is the libxmlsec1 command-line operation-root contract. The library
386    /// default remains fail-closed and requires a unique document signature.
387    pub fn first_document_signature(mut self) -> Self {
388        self.signature_selection = SignatureSelection::FirstDocumentSignature;
389        self
390    }
391
392    /// Add caller-declared ID attributes for start-node and Reference lookup.
393    pub fn id_attributes(mut self, registrations: &'a [crate::IdAttributeRegistration]) -> Self {
394        self.id_attributes = registrations;
395        self
396    }
397
398    /// Allow bounded internal DTD declarations while keeping external entity
399    /// resolution disabled. This is off by default.
400    pub fn allow_internal_dtd(mut self, enabled: bool) -> Self {
401        self.policy.xml.allow_internal_dtd = enabled;
402        self
403    }
404
405    /// Restrict allowed transform and canonicalization algorithms by URI.
406    ///
407    /// Example values:
408    /// - `http://www.w3.org/2000/09/xmldsig#enveloped-signature`
409    /// - `http://www.w3.org/2001/10/xml-exc-c14n#`
410    ///
411    /// The allowlist covers explicit Reference and RetrievalMethod transforms,
412    /// the declared SignedInfo canonicalization method, and implicit default
413    /// C14N (`http://www.w3.org/TR/2001/REC-xml-c14n-20010315`) when a Reference
414    /// transform chain ends as a node set.
415    pub fn allowed_transforms<I, S>(mut self, transforms: I) -> Self
416    where
417        I: IntoIterator<Item = S>,
418        S: Into<String>,
419    {
420        self.policy.transforms.allowed_algorithms =
421            Some(transforms.into_iter().map(Into::into).collect());
422        self
423    }
424
425    /// Store pre-digest buffers for diagnostics.
426    ///
427    /// Retained reference buffers and canonicalized `<SignedInfo>` share a
428    /// non-configurable 32 MiB safety ceiling. Canonicalized `<SignedInfo>` is
429    /// charged even when diagnostic retention is disabled because signature
430    /// verification always materializes it. Overflow remains a typed policy
431    /// violation at both low-level and end-to-end entry points.
432    pub fn store_pre_digest(mut self, enabled: bool) -> Self {
433        self.store_pre_digest = enabled;
434        self
435    }
436
437    /// Select the node returned by XPath's `here()` extension function.
438    ///
439    /// The default follows XMLDSig and returns the `<XPath>` parameter.
440    /// Use [`XPathHereSemantics::XmlSecLegacy`] only for documents known to
441    /// have been generated with libxmlsec1's `<Transform>` interpretation.
442    pub fn xpath_here_semantics(mut self, semantics: XPathHereSemantics) -> Self {
443        self.policy.transforms.xpath_here_semantics = semantics;
444        self
445    }
446
447    fn allowed_transform_uris(&self) -> Option<&HashSet<String>> {
448        self.policy.transforms.allowed_algorithms.as_ref()
449    }
450
451    fn transform_options(&self) -> TransformOptions {
452        TransformOptions::default()
453            .allow_internal_dtd(self.policy.xml.allow_internal_dtd)
454            .xpath_here_semantics(self.policy.transforms.xpath_here_semantics)
455    }
456
457    /// Verify one XMLDSig signature using this context.
458    ///
459    /// Returns `Ok(VerifyResult)` for both valid and invalid signatures; inspect
460    /// `VerifyResult::status` for the core `<SignedInfo>` and signature-value
461    /// outcome. When Manifest processing is enabled, inspect every
462    /// `VerifyResult::manifest_references` entry separately. `Err(...)` is
463    /// reserved for pipeline failures.
464    pub fn verify(&self, xml: &str) -> Result<VerifyResult, DsigError> {
465        verify_signature_with_context(xml, self)
466    }
467}
468
469impl Default for VerifyContext<'_> {
470    fn default() -> Self {
471        Self::new()
472    }
473}
474
475/// Per-reference verification result.
476#[derive(Debug)]
477#[non_exhaustive]
478#[must_use = "inspect status before accepting the reference result"]
479pub struct ReferenceResult {
480    /// Whether this reference came from `<SignedInfo>` or `<Manifest>`.
481    pub reference_set: ReferenceSet,
482    /// Zero-based index within `reference_set`.
483    pub reference_index: usize,
484    /// URI from the `<Reference>` element (for diagnostics).
485    pub uri: String,
486    /// Digest algorithm used.
487    pub digest_algorithm: DigestAlgorithm,
488    /// Reference verification status.
489    pub status: DsigStatus,
490    /// Pre-digest bytes (populated when `store_pre_digest` is enabled).
491    pub pre_digest_data: Option<Vec<u8>>,
492}
493
494/// Origin of a processed `<Reference>`.
495#[derive(Debug, Clone, Copy, PartialEq, Eq)]
496#[non_exhaustive]
497pub enum ReferenceSet {
498    /// `<Reference>` under `<SignedInfo>`.
499    SignedInfo,
500    /// `<Reference>` under `<Object>/<Manifest>`.
501    Manifest,
502}
503
504/// Verification status.
505#[derive(Debug, Clone, Copy, PartialEq, Eq)]
506#[non_exhaustive]
507pub enum DsigStatus {
508    /// Signature/reference is cryptographically valid.
509    Valid,
510    /// Signature/reference is invalid with a concrete reason.
511    Invalid(FailureReason),
512}
513
514/// Why XMLDSig verification failed.
515#[derive(Debug, Clone, Copy, PartialEq, Eq)]
516#[non_exhaustive]
517pub enum FailureReason {
518    /// `<DigestValue>` mismatch for a `<Reference>` at `ref_index`.
519    ReferenceDigestMismatch {
520        /// Zero-based index of the failing `<Reference>` in its processed set.
521        ///
522        /// On per-reference verification entries, use
523        /// `ReferenceResult::reference_set` to distinguish the `<SignedInfo>`
524        /// and `<Manifest>` reference sets.
525        ///
526        /// When this reason appears in `VerifyResult::status` without an
527        /// accompanying `ReferenceResult`, `ref_index` always refers to the
528        /// `<SignedInfo>` reference set.
529        ref_index: usize,
530    },
531    /// `<Reference>` rejected by URI/transform allowlist policy.
532    ReferencePolicyViolation {
533        /// Zero-based index of the failing `<Reference>` in its processed set.
534        ref_index: usize,
535    },
536    /// `<Reference>` processing failed (dereference, transform, missing URI).
537    ReferenceProcessingFailure {
538        /// Zero-based index of the failing `<Reference>` in its processed set.
539        ref_index: usize,
540    },
541    /// `<SignatureValue>` does not match canonicalized `<SignedInfo>`.
542    SignatureMismatch,
543    /// No verification key was configured or could be resolved.
544    KeyNotFound,
545}
546
547/// Result of processing all `<Reference>` elements in `<SignedInfo>`.
548#[derive(Debug)]
549#[non_exhaustive]
550#[must_use = "check first_failure/results before accepting the reference set"]
551pub struct ReferencesResult {
552    /// Per-reference results (one per `<Reference>` in order).
553    /// On fail-fast, only references up to and including the failed one are present.
554    pub results: Vec<ReferenceResult>,
555    /// Index of the first failed reference, if any.
556    pub first_failure: Option<usize>,
557}
558
559impl ReferencesResult {
560    /// Whether all references passed digest verification.
561    #[must_use]
562    pub fn all_valid(&self) -> bool {
563        self.results
564            .iter()
565            .all(|result| matches!(result.status, DsigStatus::Valid))
566    }
567}
568
569/// Process a single `<Reference>`: dereference URI → apply transforms → compute
570/// digest → compare with stored `<DigestValue>`.
571///
572/// # Arguments
573///
574/// - `reference`: The parsed `<Reference>` element.
575/// - `resolver`: URI resolver for the document.
576/// - `signature_node`: The `<Signature>` element (for enveloped-signature transform).
577/// - `reference_set`: Whether this reference belongs to `<SignedInfo>` or `<Manifest>`.
578/// - `reference_index`: Zero-based index of this reference inside `reference_set`.
579/// - `store_pre_digest`: If true, store the pre-digest bytes in the result,
580///   subject to the signature-wide diagnostic retention ceiling.
581///
582/// # Errors
583///
584/// Returns `Err` for processing failures (URI dereference, transform errors).
585/// Digest mismatch is NOT an error — it produces
586/// `Ok(ReferenceResult { status: Invalid(ReferenceDigestMismatch { .. }) })`.
587pub fn process_reference(
588    reference: &Reference,
589    resolver: &UriReferenceResolver<'_>,
590    signature_node: Node<'_, '_>,
591    reference_set: ReferenceSet,
592    reference_index: usize,
593    store_pre_digest: bool,
594) -> Result<ReferenceResult, ReferenceProcessingError> {
595    let execution_budget = TransformExecutionBudget::default();
596    let canonicalized_data_budget = CanonicalizedDataBudget::default();
597    let execution = ReferenceExecutionContext {
598        store_pre_digest,
599        transform_options: TransformOptions::default(),
600        transform_budget: &execution_budget,
601        canonicalized_data_budget: &canonicalized_data_budget,
602        provider: crate::provider::default_provider(),
603    };
604    process_reference_with_options(
605        reference,
606        resolver,
607        signature_node,
608        reference_set,
609        reference_index,
610        reference_origin_node(signature_node, reference_set, reference_index),
611        &execution,
612    )
613}
614
615fn reference_origin_node<'a, 'input>(
616    signature_node: Node<'a, 'input>,
617    reference_set: ReferenceSet,
618    reference_index: usize,
619) -> Option<Node<'a, 'input>> {
620    let is_reference = |node: &Node<'_, '_>| {
621        node.is_element()
622            && node.tag_name().namespace() == Some(XMLDSIG_NS)
623            && node.tag_name().name() == "Reference"
624    };
625    match reference_set {
626        ReferenceSet::SignedInfo => signature_node
627            .children()
628            .find(|node| {
629                node.is_element()
630                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
631                    && node.tag_name().name() == "SignedInfo"
632            })?
633            .children()
634            .filter(is_reference)
635            .nth(reference_index),
636        ReferenceSet::Manifest => signature_node
637            .children()
638            .filter(|node| {
639                node.is_element()
640                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
641                    && node.tag_name().name() == "Object"
642            })
643            .flat_map(|object| {
644                object.children().filter(|node| {
645                    node.is_element()
646                        && node.tag_name().namespace() == Some(XMLDSIG_NS)
647                        && node.tag_name().name() == "Manifest"
648                })
649            })
650            .flat_map(|manifest| manifest.children().filter(is_reference))
651            .nth(reference_index),
652    }
653}
654
655struct ReferenceExecutionContext<'a> {
656    store_pre_digest: bool,
657    transform_options: TransformOptions,
658    transform_budget: &'a TransformExecutionBudget,
659    canonicalized_data_budget: &'a CanonicalizedDataBudget,
660    provider: &'a dyn crate::provider::CryptoProvider,
661}
662
663struct CanonicalizedDataBudget {
664    remaining: Cell<usize>,
665    max_bytes: usize,
666}
667
668impl Default for CanonicalizedDataBudget {
669    fn default() -> Self {
670        Self {
671            remaining: Cell::new(CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING),
672            max_bytes: CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
673        }
674    }
675}
676
677impl CanonicalizedDataBudget {
678    fn remaining(&self) -> usize {
679        self.remaining.get()
680    }
681
682    fn charge(&self, bytes: usize) -> Result<(), ReferenceProcessingError> {
683        let available = self.remaining.get();
684        let Some(remaining) = available.checked_sub(bytes) else {
685            self.remaining.set(0);
686            return Err(crate::policy::PolicyViolation::ResourceLimit {
687                resource: crate::policy::resource_name::CANONICALIZED_BYTES,
688                maximum: self.max_bytes,
689                actual: self
690                    .max_bytes
691                    .saturating_add(bytes.saturating_sub(available)),
692            }
693            .into());
694        };
695        self.remaining.set(remaining);
696        Ok(())
697    }
698
699    fn with_limit(max_bytes: usize) -> Self {
700        Self {
701            remaining: Cell::new(max_bytes),
702            max_bytes,
703        }
704    }
705}
706
707fn process_reference_with_options(
708    reference: &Reference,
709    resolver: &UriReferenceResolver<'_>,
710    signature_node: Node<'_, '_>,
711    reference_set: ReferenceSet,
712    reference_index: usize,
713    reference_node: Option<Node<'_, '_>>,
714    execution: &ReferenceExecutionContext<'_>,
715) -> Result<ReferenceResult, ReferenceProcessingError> {
716    // 1. Dereference URI. Omitted URI is distinct from URI="" in XMLDSig and
717    // must be rejected until caller-provided external object resolution exists.
718    let uri = reference
719        .uri
720        .as_deref()
721        .ok_or(ReferenceProcessingError::MissingUri)?;
722    let initial_data = reference_node
723        .map_or_else(
724            || {
725                resolver.dereference_with_budget(
726                    uri,
727                    execution.transform_budget.node_set_materialization(),
728                )
729            },
730            |node| {
731                resolver.dereference_from_with_budget(
732                    uri,
733                    node,
734                    execution.transform_budget.node_set_materialization(),
735                    execution.transform_budget.xml_base_resolution(),
736                )
737            },
738        )
739        .map_err(ReferenceProcessingError::UriDereference)?;
740
741    // 2. Apply transform chain
742    let pre_digest_bytes = execute_transforms_with_options_and_budget(
743        signature_node,
744        initial_data,
745        &reference.transforms,
746        execution.transform_options,
747        execution.transform_budget,
748    )
749    .map_err(ReferenceProcessingError::Transform)?;
750
751    // 3. Compute digest
752    let computed_digest = super::compute_digest_with_provider(
753        execution.provider,
754        reference.digest_method,
755        &pre_digest_bytes,
756    )?;
757
758    // 4. Compare with stored DigestValue (constant-time)
759    let status = if constant_time_eq(&computed_digest, &reference.digest_value) {
760        DsigStatus::Valid
761    } else {
762        DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch {
763            ref_index: reference_index,
764        })
765    };
766
767    let pre_digest_data = if execution.store_pre_digest {
768        execution
769            .canonicalized_data_budget
770            .charge(pre_digest_bytes.len())?;
771        Some(pre_digest_bytes)
772    } else {
773        None
774    };
775
776    Ok(ReferenceResult {
777        reference_set,
778        reference_index,
779        uri: uri.to_owned(),
780        digest_algorithm: reference.digest_method,
781        status,
782        pre_digest_data,
783    })
784}
785
786/// Process all `<Reference>` elements in a `<SignedInfo>`, with fail-fast
787/// on the first digest mismatch.
788///
789/// Per XMLDSig spec: if any reference fails, the entire signature is invalid.
790/// Processing stops at the first failure for efficiency.
791///
792/// # Errors
793///
794/// Returns `Err` only for processing failures (malformed XML, unsupported
795/// transform, etc.). Digest mismatches are reported via
796/// `ReferencesResult::first_failure`.
797pub fn process_all_references(
798    references: &[Reference],
799    resolver: &UriReferenceResolver<'_>,
800    signature_node: Node<'_, '_>,
801    store_pre_digest: bool,
802) -> Result<ReferencesResult, ReferenceProcessingError> {
803    let execution_budget = TransformExecutionBudget::default();
804    let canonicalized_data_budget = CanonicalizedDataBudget::default();
805    let execution = ReferenceExecutionContext {
806        store_pre_digest,
807        transform_options: TransformOptions::default(),
808        transform_budget: &execution_budget,
809        canonicalized_data_budget: &canonicalized_data_budget,
810        provider: crate::provider::default_provider(),
811    };
812    process_all_references_with_options(references, resolver, signature_node, &execution)
813}
814
815fn process_all_references_with_options(
816    references: &[Reference],
817    resolver: &UriReferenceResolver<'_>,
818    signature_node: Node<'_, '_>,
819    execution: &ReferenceExecutionContext<'_>,
820) -> Result<ReferencesResult, ReferenceProcessingError> {
821    let mut results = Vec::with_capacity(references.len());
822
823    for (i, reference) in references.iter().enumerate() {
824        let result = process_reference_with_options(
825            reference,
826            resolver,
827            signature_node,
828            ReferenceSet::SignedInfo,
829            i,
830            reference_origin_node(signature_node, ReferenceSet::SignedInfo, i),
831            execution,
832        )?;
833        let failed = matches!(result.status, DsigStatus::Invalid(_));
834        results.push(result);
835
836        if failed {
837            return Ok(ReferencesResult {
838                results,
839                first_failure: Some(i),
840            });
841        }
842    }
843
844    Ok(ReferencesResult {
845        results,
846        first_failure: None,
847    })
848}
849
850/// Errors during reference processing.
851///
852/// Distinct from digest mismatch (which is a validation result, not a processing error).
853#[derive(Debug, thiserror::Error)]
854#[non_exhaustive]
855pub enum ReferenceProcessingError {
856    /// The immutable verification policy rejected reference processing.
857    #[error("verification policy violation: {0}")]
858    Policy(#[from] crate::policy::PolicyViolation),
859
860    /// The selected provider could not compute the declared digest.
861    #[error("cryptographic provider error: {0}")]
862    Provider(#[from] crate::provider::ProviderError),
863
864    /// `<Reference>` omitted the `URI` attribute, which we do not resolve implicitly.
865    #[error("reference URI is required; omitted URI references are not supported")]
866    MissingUri,
867
868    /// URI dereference failed.
869    #[error("URI dereference failed: {0}")]
870    UriDereference(#[source] super::types::TransformError),
871
872    /// Transform execution failed.
873    #[error("transform failed: {0}")]
874    Transform(#[source] super::types::TransformError),
875}
876
877impl ReferenceProcessingError {
878    fn into_policy_violation(self) -> Result<crate::policy::PolicyViolation, Self> {
879        match self {
880            Self::Policy(error)
881            | Self::UriDereference(TransformError::Policy(error))
882            | Self::Transform(TransformError::Policy(error)) => Ok(error),
883            error => Err(error),
884        }
885    }
886}
887
888/// End-to-end XMLDSig verification result for one `<Signature>`.
889#[derive(Debug)]
890#[non_exhaustive]
891#[must_use = "inspect status before accepting the document"]
892pub struct VerifyResult {
893    /// Core XMLDSig status for the `<SignedInfo>` references and signature value.
894    ///
895    /// Manifest reference failures do not alter this field; inspect
896    /// [`Self::manifest_references`] before accepting Manifest-backed data.
897    pub status: DsigStatus,
898    /// `<Reference>` verification results from `<SignedInfo>`.
899    /// On fail-fast, this includes references up to and including
900    /// the first digest mismatch only.
901    pub signed_info_references: Vec<ReferenceResult>,
902    /// `<Manifest>` reference results.
903    /// Populated only when `VerifyContext::process_manifests(true)` is enabled
904    /// and core signature validation succeeds.
905    /// Includes only references from signed direct-child `<ds:Object>/<ds:Manifest>`
906    /// blocks that are referenced from `<SignedInfo>`.
907    /// Each entry has an independent status that does not alter [`Self::status`].
908    /// Callers must inspect every entry before accepting Manifest-backed data.
909    /// Unsigned/unreferenced direct-child Manifest blocks are skipped, so an
910    /// empty list does not imply that no Manifest elements existed in `verify()` input.
911    pub manifest_references: Vec<ReferenceResult>,
912    /// Canonicalized `<SignedInfo>` bytes when `store_pre_digest` is enabled
913    /// and verification reaches SignedInfo canonicalization.
914    pub canonicalized_signed_info: Option<Vec<u8>>,
915}
916
917/// Errors while running end-to-end XMLDSig verification.
918#[derive(Debug, thiserror::Error)]
919#[non_exhaustive]
920pub enum DsigError {
921    /// The compiled verification policy rejected an operation input.
922    #[error("verification policy violation: {0}")]
923    Policy(#[from] crate::policy::PolicyViolation),
924
925    /// The selected provider cannot execute the requested operation.
926    #[error("cryptographic provider error: {0}")]
927    Provider(#[from] crate::provider::ProviderError),
928
929    /// XML parsing failed.
930    #[error("XML parse error: {0}")]
931    XmlParse(#[from] roxmltree::Error),
932
933    /// Required signature element is missing.
934    #[error("missing required element: <{element}>")]
935    MissingElement {
936        /// Name of the missing element.
937        element: &'static str,
938    },
939
940    /// Signature element tree shape violates XMLDSig structure requirements.
941    #[error("invalid Signature structure: {reason}")]
942    InvalidStructure {
943        /// Validation failure reason.
944        reason: &'static str,
945    },
946
947    /// The requested operation start node is absent or has a duplicate ID.
948    #[error("selected node ID is missing or ambiguous: {id}")]
949    SelectedNodeUnavailable {
950        /// Caller-provided XML ID value.
951        id: String,
952    },
953
954    /// `<SignedInfo>` parsing failed.
955    #[error("failed to parse SignedInfo: {0}")]
956    ParseSignedInfo(super::parse::ParseError),
957
958    /// `<KeyInfo>` parsing failed.
959    #[error("failed to parse KeyInfo: {0}")]
960    ParseKeyInfo(#[source] super::parse::ParseError),
961
962    /// Configuration-driven key resolution failed.
963    #[error("key resolution failed: {0}")]
964    KeyResolution(#[from] super::keys::KeyResolutionError),
965
966    /// `<Object>/<Manifest>/<Reference>` parsing failed.
967    #[error("failed to parse Manifest reference: {0}")]
968    ParseManifestReference(#[source] ParseError),
969
970    /// Reference processing failed.
971    #[error("reference processing failed: {0}")]
972    Reference(ReferenceProcessingError),
973
974    /// SignedInfo canonicalization failed.
975    #[error("SignedInfo canonicalization failed: {0}")]
976    Canonicalization(#[from] crate::c14n::C14nError),
977
978    /// SignatureValue base64 decoding failed.
979    #[error("invalid SignatureValue base64: {0}")]
980    SignatureValueBase64(#[from] base64::DecodeError),
981
982    /// Cryptographic verification failed before validity decision.
983    #[error("signature verification failed: {0}")]
984    Crypto(#[from] SignatureVerificationError),
985}
986
987impl From<super::parse::ParseError> for DsigError {
988    fn from(error: super::parse::ParseError) -> Self {
989        match error {
990            super::parse::ParseError::Policy(error) => Self::Policy(error),
991            super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
992                Self::Policy(error)
993            }
994            error => Self::ParseSignedInfo(error),
995        }
996    }
997}
998
999fn map_key_info_parse_error(error: super::parse::ParseError) -> DsigError {
1000    match error {
1001        super::parse::ParseError::Policy(error)
1002        | super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1003            DsigError::Policy(error)
1004        }
1005        error => DsigError::ParseKeyInfo(error),
1006    }
1007}
1008
1009fn map_manifest_parse_error(error: super::parse::ParseError) -> DsigError {
1010    match error {
1011        super::parse::ParseError::Policy(error)
1012        | super::parse::ParseError::Transform(super::TransformError::Policy(error)) => {
1013            DsigError::Policy(error)
1014        }
1015        error => DsigError::ParseManifestReference(error),
1016    }
1017}
1018
1019impl From<ReferenceProcessingError> for DsigError {
1020    fn from(error: ReferenceProcessingError) -> Self {
1021        match error.into_policy_violation() {
1022            Ok(error) => Self::Policy(error),
1023            Err(error) => Self::Reference(error),
1024        }
1025    }
1026}
1027
1028type SignatureVerificationPipelineError = DsigError;
1029
1030/// Verify one XMLDSig `<Signature>` end-to-end with a PEM public key.
1031///
1032/// Pipeline:
1033/// 1. Parse `<Signature>` children and enforce structural constraints
1034/// 2. Parse `<SignedInfo>`
1035/// 3. Validate all `<Reference>` digests (fail-fast)
1036/// 4. Canonicalize `<SignedInfo>`
1037/// 5. Base64-decode `<SignatureValue>`
1038/// 6. Verify signature bytes against canonicalized `<SignedInfo>` using the provided PEM key
1039///
1040/// If any `<Reference>` digest mismatches, returns `Ok` with
1041/// `status == Invalid(ReferenceDigestMismatch { .. })`.
1042///
1043/// This API uses only the provided PEM key and does not parse embedded
1044/// `<KeyInfo>` key material for key selection/validation. Consequently,
1045/// malformed optional `<KeyInfo>` does not produce `DsigError::ParseKeyInfo`
1046/// on this API path.
1047///
1048/// Structural constraints enforced by this API:
1049/// - The document must contain exactly one XMLDSig `<Signature>` element.
1050/// - `<SignedInfo>` must be the first element child of `<Signature>` and appear once.
1051/// - `<SignatureValue>` must be the second element child of `<Signature>` and appear once.
1052/// - `<KeyInfo>` is optional and, when present, must be the third element child.
1053/// - Only XMLDSig namespace element children are allowed under `<Signature>`.
1054/// - Non-whitespace mixed text content under `<Signature>` is rejected.
1055/// - After `<SignedInfo>`, `<SignatureValue>`, and optional `<KeyInfo>`, only `<Object>` elements are allowed.
1056/// - `<SignatureValue>` must not contain nested element children.
1057pub fn verify_signature_with_pem_key(
1058    xml: &str,
1059    public_key_pem: &str,
1060    store_pre_digest: bool,
1061) -> Result<VerifyResult, DsigError> {
1062    struct PemVerifyingKey<'a> {
1063        public_key_pem: &'a str,
1064    }
1065
1066    impl VerifyingKey for PemVerifyingKey<'_> {
1067        fn verify(
1068            &self,
1069            algorithm: SignatureAlgorithm,
1070            signed_data: &[u8],
1071            signature_value: &[u8],
1072        ) -> Result<bool, DsigError> {
1073            verify_with_algorithm(algorithm, self.public_key_pem, signed_data, signature_value)
1074        }
1075    }
1076
1077    let key = PemVerifyingKey { public_key_pem };
1078    VerifyContext::new()
1079        .key(&key)
1080        .store_pre_digest(store_pre_digest)
1081        .verify(xml)
1082}
1083
1084fn verify_signature_with_context(
1085    xml: &str,
1086    ctx: &VerifyContext<'_>,
1087) -> Result<VerifyResult, SignatureVerificationPipelineError> {
1088    ctx.policy.validate()?;
1089    ctx.policy.resources.validate_xml_document_len(xml.len())?;
1090    let doc = Document::parse_with_options(
1091        xml,
1092        roxmltree::ParsingOptions {
1093            allow_dtd: ctx.policy.xml.allow_internal_dtd,
1094            nodes_limit: ctx.policy.resources.effective_xml_nodes(),
1095            entity_resolver: None,
1096        },
1097    )?;
1098    let resolver = UriReferenceResolver::with_id_registrations(&doc, ctx.id_attributes)
1099        .with_external_resource_limits(
1100            ctx.policy.resources.max_external_resource_bytes,
1101            ctx.policy.resources.max_external_resource_total_bytes,
1102        );
1103    let resolver = match ctx.external_resources {
1104        Some(resources) => resolver.with_external_resources(resources),
1105        None => resolver,
1106    };
1107    let execution_budget = TransformExecutionBudget::from_resources(&ctx.policy.resources);
1108    let start_node = match ctx.signature_selection {
1109        SignatureSelection::FirstSignatureUnderId(id) => {
1110            resolver.node_for_id(id).ok_or_else(|| {
1111                SignatureVerificationPipelineError::SelectedNodeUnavailable { id: id.to_owned() }
1112            })?
1113        }
1114        SignatureSelection::UniqueDocumentSignature
1115        | SignatureSelection::FirstDocumentSignature => doc.root(),
1116    };
1117    let mut signatures = start_node.descendants().filter(|node| {
1118        node.is_element()
1119            && node.tag_name().name() == "Signature"
1120            && node.tag_name().namespace() == Some(XMLDSIG_NS)
1121    });
1122    let signature_node = match (signatures.next(), ctx.signature_selection) {
1123        (None, _) => {
1124            return Err(SignatureVerificationPipelineError::MissingElement {
1125                element: "Signature",
1126            });
1127        }
1128        // libxmlsec1 treats --node-id as an operation start node and performs
1129        // a depth-first xmlSecFindNode lookup from there. Without a selector,
1130        // the library API retains its fail-closed document-wide cardinality.
1131        (
1132            Some(node),
1133            SignatureSelection::FirstDocumentSignature
1134            | SignatureSelection::FirstSignatureUnderId(_),
1135        ) => node,
1136        (Some(node), SignatureSelection::UniqueDocumentSignature)
1137            if signatures.next().is_none() =>
1138        {
1139            node
1140        }
1141        (Some(_), SignatureSelection::UniqueDocumentSignature) => {
1142            return Err(SignatureVerificationPipelineError::InvalidStructure {
1143                reason: "Signature must appear exactly once in document",
1144            });
1145        }
1146    };
1147
1148    let signature_children = parse_signature_children(signature_node)?;
1149    let signed_info_node = signature_children.signed_info_node;
1150    let should_parse_key_info = match (ctx.key, ctx.key_resolver) {
1151        (Some(_), _) => false,
1152        (None, Some(resolver)) => resolver.consumes_document_key_info(),
1153        (None, None) => true,
1154    };
1155    let mut key_info = if should_parse_key_info {
1156        signature_children
1157            .key_info_node
1158            .map(|node| {
1159                parse_key_info_with_policy_budgets(
1160                    node,
1161                    ctx.provider,
1162                    execution_budget.xml_base_resolution(),
1163                    &ctx.policy.resources,
1164                )
1165            })
1166            .transpose()
1167            .map_err(map_key_info_parse_error)?
1168    } else {
1169        None
1170    };
1171
1172    let mut xpath_parse_budget = XPathSignatureParseBudget::from_resources(&ctx.policy.resources);
1173    let signed_info =
1174        parse_signed_info_with_xpath_budget(signed_info_node, &mut xpath_parse_budget)?;
1175    if signed_info.references.len() > ctx.policy.resources.max_references {
1176        return Err(crate::policy::PolicyViolation::ResourceLimit {
1177            resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
1178            maximum: ctx.policy.resources.max_references,
1179            actual: signed_info.references.len(),
1180        }
1181        .into());
1182    }
1183    for reference in &signed_info.references {
1184        if reference.transforms.len() > ctx.policy.resources.max_transforms_per_reference {
1185            return Err(crate::policy::PolicyViolation::ResourceLimit {
1186                resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
1187                maximum: ctx.policy.resources.max_transforms_per_reference,
1188                actual: reference.transforms.len(),
1189            }
1190            .into());
1191        }
1192    }
1193    ctx.policy
1194        .check_signature_algorithm(signed_info.signature_method)?;
1195    for reference in &signed_info.references {
1196        if ctx
1197            .policy
1198            .digest_algorithms
1199            .as_ref()
1200            .is_some_and(|allowed| !allowed.contains(&reference.digest_method))
1201        {
1202            return Err(crate::policy::PolicyViolation::Algorithm {
1203                operation: "verification",
1204                algorithm: reference.digest_method.uri().to_string(),
1205            }
1206            .into());
1207        }
1208    }
1209    enforce_reference_policies(
1210        &signed_info.references,
1211        ctx.policy.uris.references,
1212        ctx.allowed_transform_uris(),
1213    )?;
1214    enforce_transform_allowed(ctx.allowed_transform_uris(), signed_info.c14n_method.uri())?;
1215
1216    if let Some(resources) = ctx.external_resources {
1217        let mut total = 0usize;
1218        for bytes in resources.values() {
1219            if bytes.len() > ctx.policy.resources.max_external_resource_bytes {
1220                return Err(crate::policy::PolicyViolation::ResourceLimit {
1221                    resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
1222                    maximum: ctx.policy.resources.max_external_resource_bytes,
1223                    actual: bytes.len(),
1224                }
1225                .into());
1226            }
1227            total = total.checked_add(bytes.len()).ok_or(
1228                SignatureVerificationPipelineError::InvalidStructure {
1229                    reason: "external resource total length overflow",
1230                },
1231            )?;
1232        }
1233        if total > ctx.policy.resources.max_external_resource_total_bytes {
1234            return Err(crate::policy::PolicyViolation::ResourceLimit {
1235                resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
1236                maximum: ctx.policy.resources.max_external_resource_total_bytes,
1237                actual: total,
1238            }
1239            .into());
1240        }
1241    }
1242    let retrieval_materialization = if let Some(info) = key_info.as_mut() {
1243        let mut retrieval_budgets = RetrievalMaterializationBudgets {
1244            xpath_parse: &mut xpath_parse_budget,
1245            execution: &execution_budget,
1246            resources: &ctx.policy.resources,
1247        };
1248        materialize_retrieval_methods_with_budgets(
1249            info,
1250            &resolver,
1251            ctx.policy.uris.retrieval_methods,
1252            ctx.allowed_transform_uris(),
1253            ctx.provider,
1254            &mut retrieval_budgets,
1255        )?
1256    } else {
1257        RetrievalMaterialization::default()
1258    };
1259    let canonicalized_data_budget =
1260        CanonicalizedDataBudget::with_limit(ctx.policy.resources.effective_canonicalized_bytes());
1261    let execution = ReferenceExecutionContext {
1262        store_pre_digest: ctx.store_pre_digest,
1263        transform_options: ctx.transform_options(),
1264        transform_budget: &execution_budget,
1265        canonicalized_data_budget: &canonicalized_data_budget,
1266        provider: ctx.provider,
1267    };
1268    let references = process_all_references_with_options(
1269        &signed_info.references,
1270        &resolver,
1271        signature_node,
1272        &execution,
1273    )?;
1274
1275    if let Some(first_failure) = references.first_failure {
1276        let status = references.results[first_failure].status;
1277        return Ok(VerifyResult {
1278            status,
1279            signed_info_references: references.results,
1280            manifest_references: Vec::new(),
1281            canonicalized_signed_info: None,
1282        });
1283    }
1284
1285    let signed_info_subtree: HashSet<_> = signed_info_node
1286        .descendants()
1287        .map(|node: Node<'_, '_>| node.id())
1288        .collect();
1289    let mut canonical_signed_info = Vec::new();
1290    let signed_info_limit = canonicalized_data_budget
1291        .remaining()
1292        .min(execution_budget.remaining_c14n_output());
1293    canonicalize_bounded_with_xml_base_budget(
1294        &doc,
1295        Some(&|node| signed_info_subtree.contains(&node.id())),
1296        &signed_info.c14n_method,
1297        signed_info_limit,
1298        execution_budget.xml_base_resolution(),
1299        &mut canonical_signed_info,
1300    )
1301    .map_err(|error| {
1302        if let Some(violation) = map_c14n_resource_policy_violation(
1303            &error,
1304            crate::policy::resource_name::CANONICALIZED_BYTES,
1305            canonicalized_data_budget.max_bytes,
1306        ) {
1307            SignatureVerificationPipelineError::Policy(violation)
1308        } else {
1309            SignatureVerificationPipelineError::Canonicalization(error)
1310        }
1311    })?;
1312    execution_budget
1313        .charge_c14n_output(canonical_signed_info.len())
1314        .map_err(ReferenceProcessingError::Transform)?;
1315    canonicalized_data_budget.charge(canonical_signed_info.len())?;
1316
1317    let signature_value = decode_signature_value(signature_children.signature_value_node)?;
1318    if signed_info.signature_method == SignatureAlgorithm::HmacSha1 {
1319        let expected_bits = signed_info.hmac_output_length_bits.unwrap_or(160);
1320        if signature_value.len() != expected_bits / 8 {
1321            return Err(SignatureVerificationPipelineError::InvalidStructure {
1322                reason: "SignatureValue length does not match HMACOutputLength",
1323            });
1324        }
1325    }
1326    let Some(resolved_key) =
1327        resolve_verifying_key(ctx, key_info.as_ref(), signed_info.signature_method)?
1328    else {
1329        if let Some(error) = retrieval_materialization.deferred_error {
1330            return Err(error);
1331        }
1332        return Ok(VerifyResult {
1333            status: DsigStatus::Invalid(FailureReason::KeyNotFound),
1334            signed_info_references: references.results,
1335            manifest_references: Vec::new(),
1336            canonicalized_signed_info: if ctx.store_pre_digest {
1337                Some(canonical_signed_info)
1338            } else {
1339                None
1340            },
1341        });
1342    };
1343    let verifier = resolved_key.as_ref();
1344    verifier.validate_policy(&ctx.policy)?;
1345    if !verifier.validate_signature_value(signed_info.signature_method, &signature_value)? {
1346        return Ok(VerifyResult {
1347            status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
1348            signed_info_references: references.results,
1349            manifest_references: Vec::new(),
1350            canonicalized_signed_info: if ctx.store_pre_digest {
1351                Some(canonical_signed_info)
1352            } else {
1353                None
1354            },
1355        });
1356    }
1357    ctx.provider
1358        .require_capability(crate::provider::ProviderCapability::Verify(
1359            signed_info.signature_method,
1360        ))?;
1361    let signature_valid = ctx.provider.verify(
1362        verifier,
1363        signed_info.signature_method,
1364        &canonical_signed_info,
1365        &signature_value,
1366    )?;
1367
1368    if !signature_valid {
1369        return Ok(VerifyResult {
1370            status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
1371            signed_info_references: references.results,
1372            manifest_references: Vec::new(),
1373            canonicalized_signed_info: if ctx.store_pre_digest {
1374                Some(canonical_signed_info)
1375            } else {
1376                None
1377            },
1378        });
1379    }
1380
1381    let manifest_references = if ctx.policy.manifest_processing
1382        == crate::policy::ManifestProcessing::Process
1383    {
1384        let signed_info_reference_nodes =
1385            collect_authenticated_signed_info_reference_nodes(&signed_info.references, &resolver);
1386        let remaining_reference_capacity = ctx
1387            .policy
1388            .resources
1389            .max_references
1390            .checked_sub(signed_info.references.len())
1391            .ok_or(SignatureVerificationPipelineError::InvalidStructure {
1392                reason: "SignedInfo exceeds the per-signature Reference limit",
1393            })?;
1394        process_manifest_references(
1395            signature_node,
1396            &resolver,
1397            ctx,
1398            &signed_info_reference_nodes,
1399            remaining_reference_capacity,
1400            &execution,
1401            &mut xpath_parse_budget,
1402        )?
1403    } else {
1404        Vec::new()
1405    };
1406
1407    Ok(VerifyResult {
1408        status: DsigStatus::Valid,
1409        signed_info_references: references.results,
1410        manifest_references,
1411        canonicalized_signed_info: if ctx.store_pre_digest {
1412            Some(canonical_signed_info)
1413        } else {
1414            None
1415        },
1416    })
1417}
1418
1419#[derive(Debug, Default)]
1420struct RetrievalMaterialization {
1421    deferred_error: Option<SignatureVerificationPipelineError>,
1422}
1423
1424struct RetrievalMaterializationBudgets<'a> {
1425    xpath_parse: &'a mut XPathSignatureParseBudget,
1426    execution: &'a TransformExecutionBudget,
1427    resources: &'a crate::policy::ResourcePolicy,
1428}
1429
1430fn materialize_retrieval_methods_with_budgets(
1431    key_info: &mut KeyInfo,
1432    resolver: &UriReferenceResolver<'_>,
1433    allowed_uri_types: UriTypeSet,
1434    allowed_transforms: Option<&HashSet<String>>,
1435    provider: &dyn crate::provider::CryptoProvider,
1436    budgets: &mut RetrievalMaterializationBudgets<'_>,
1437) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
1438    let retrieval_count = key_info
1439        .sources
1440        .iter()
1441        .filter(|source| matches!(source, super::parse::KeyInfoSource::RetrievalMethod { .. }))
1442        .count();
1443    if retrieval_count > MAX_RETRIEVAL_METHOD_COUNT {
1444        return Err(SignatureVerificationPipelineError::InvalidStructure {
1445            reason: "KeyInfo contains too many RetrievalMethod elements",
1446        });
1447    }
1448
1449    let mut total_binary_len = existing_x509_binary_len(key_info)?;
1450    let mut materialized_candidate_preflight_count = key_info.embedded_candidate_count();
1451    let mut seen = HashSet::new();
1452    let mut materialized = Vec::with_capacity(key_info.sources.len());
1453    let mut outcome = RetrievalMaterialization::default();
1454    for source in std::mem::take(&mut key_info.sources) {
1455        let super::parse::KeyInfoSource::RetrievalMethod {
1456            uri: resolved_uri,
1457            resource_type,
1458            transforms,
1459        } = source
1460        else {
1461            materialized.push(source);
1462            continue;
1463        };
1464
1465        let identity = (
1466            resolved_uri.clone(),
1467            resource_type.clone(),
1468            transforms.clone(),
1469        );
1470        if !seen.insert(identity) {
1471            continue;
1472        }
1473
1474        if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#rawX509Certificate")
1475        {
1476            if transforms != RetrievalMethodTransforms::None
1477                || classify_uri(&resolved_uri) != UriClass::External
1478            {
1479                return Err(SignatureVerificationPipelineError::InvalidStructure {
1480                    reason: "raw X509 RetrievalMethod requires an untransformed external URI",
1481                });
1482            }
1483            if !allowed_uri_types.allows(&resolved_uri) {
1484                return Err(crate::policy::PolicyViolation::Uri {
1485                    operation: "verification",
1486                    reason: "retrieval method URI class is not permitted",
1487                }
1488                .into());
1489            }
1490            let certificate = resolver.external_resource(&resolved_uri).map_err(|error| {
1491                SignatureVerificationPipelineError::from(ReferenceProcessingError::Transform(error))
1492            })?;
1493            let Some(certificate) = certificate else {
1494                outcome.deferred_error.get_or_insert_with(|| {
1495                    SignatureVerificationPipelineError::Reference(
1496                        ReferenceProcessingError::Transform(super::TransformError::UnsupportedUri(
1497                            resolved_uri.clone(),
1498                        )),
1499                    )
1500                });
1501                materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
1502                    uri: resolved_uri,
1503                    resource_type,
1504                    transforms,
1505                });
1506                continue;
1507            };
1508            materialized_candidate_preflight_count =
1509                materialized_candidate_preflight_count.saturating_add(1);
1510            budgets
1511                .resources
1512                .validate_key_candidates(materialized_candidate_preflight_count)?;
1513            if certificate.len() > MAX_X509_DECODED_BINARY_LEN {
1514                return Err(SignatureVerificationPipelineError::InvalidStructure {
1515                    reason: "raw X509 RetrievalMethod certificate exceeds maximum allowed length",
1516                });
1517            }
1518            add_retrieval_binary_usage(&mut total_binary_len, certificate.len())?;
1519            let parsed = match parse_x509_certificate(certificate) {
1520                Ok(parsed) => parsed,
1521                Err(error) => {
1522                    let error = map_key_info_parse_error(error);
1523                    if matches!(error, SignatureVerificationPipelineError::Policy(_)) {
1524                        return Err(error);
1525                    }
1526                    outcome.deferred_error.get_or_insert(error);
1527                    materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
1528                        uri: resolved_uri,
1529                        resource_type,
1530                        transforms,
1531                    });
1532                    continue;
1533                }
1534            };
1535            materialized.push(super::parse::KeyInfoSource::X509Data(
1536                super::parse::X509DataInfo {
1537                    certificates: vec![certificate.to_vec()],
1538                    parsed_certificates: vec![parsed],
1539                    certificate_chain: vec![0],
1540                    ..super::parse::X509DataInfo::default()
1541                },
1542            ));
1543        } else if resource_type.as_deref() == Some("http://www.w3.org/2000/09/xmldsig#X509Data") {
1544            if !allowed_uri_types.allows(&resolved_uri) {
1545                return Err(crate::policy::PolicyViolation::Uri {
1546                    operation: "verification",
1547                    reason: "retrieval method URI class is not permitted",
1548                }
1549                .into());
1550            }
1551            let id = same_document_reference_id(&resolved_uri).ok_or(
1552                SignatureVerificationPipelineError::InvalidStructure {
1553                    reason: "X509Data RetrievalMethod requires a same-document URI",
1554                },
1555            )?;
1556            let target = resolver.node_for_id(id).ok_or(
1557                SignatureVerificationPipelineError::InvalidStructure {
1558                    reason: "X509Data RetrievalMethod target is missing or ambiguous",
1559                },
1560            )?;
1561            let node = match transforms {
1562                RetrievalMethodTransforms::None
1563                    if target.has_tag_name((XMLDSIG_NS, "X509Data")) =>
1564                {
1565                    target
1566                }
1567                RetrievalMethodTransforms::None => {
1568                    return Err(SignatureVerificationPipelineError::InvalidStructure {
1569                        reason: "untransformed X509Data RetrievalMethod must target X509Data directly",
1570                    });
1571                }
1572                RetrievalMethodTransforms::X509DataNodeSetFilter {
1573                    expression,
1574                    namespaces,
1575                } => {
1576                    enforce_transform_allowed(allowed_transforms, XPATH_TRANSFORM_URI)?;
1577                    budgets
1578                        .xpath_parse
1579                        .validate_expression(&expression)
1580                        .map_err(ReferenceProcessingError::Transform)?;
1581                    budgets
1582                        .xpath_parse
1583                        .validate_namespaces(&namespaces)
1584                        .map_err(ReferenceProcessingError::Transform)?;
1585                    select_retrieved_x509_data_root(target, budgets.execution)?
1586                }
1587                RetrievalMethodTransforms::Unsupported => {
1588                    return Err(SignatureVerificationPipelineError::InvalidStructure {
1589                        reason: "X509Data RetrievalMethod contains unsupported transforms",
1590                    });
1591                }
1592            };
1593            let data = parse_x509_data_dispatch_with_budget_and_provider(
1594                node,
1595                &mut total_binary_len,
1596                &mut materialized_candidate_preflight_count,
1597                provider,
1598                budgets.resources,
1599            )
1600            .map_err(map_key_info_parse_error)?;
1601            materialized.push(super::parse::KeyInfoSource::X509Data(data));
1602        } else {
1603            materialized.push(super::parse::KeyInfoSource::RetrievalMethod {
1604                uri: resolved_uri,
1605                resource_type,
1606                transforms,
1607            });
1608        }
1609    }
1610    key_info.sources = materialized;
1611    Ok(outcome)
1612}
1613
1614fn select_retrieved_x509_data_root<'a, 'input>(
1615    target: Node<'a, 'input>,
1616    execution_budget: &TransformExecutionBudget,
1617) -> Result<Node<'a, 'input>, SignatureVerificationPipelineError> {
1618    // XMLDSig XPath filtering evaluates the predicate for every node in the
1619    // dereferenced node-set, including attribute and namespace nodes. The
1620    // element-only scan below selects X509Data but must not undercount that
1621    // XPath context cardinality.
1622    let context_nodes = NodeSet::ensure_subtree_materialization_fits_with_budget(
1623        target,
1624        false,
1625        execution_budget.node_set_materialization(),
1626    )
1627    .map_err(ReferenceProcessingError::Transform)?;
1628    execution_budget
1629        .validate_xpath_context_evaluations(context_nodes)
1630        .map_err(ReferenceProcessingError::Transform)?;
1631    execution_budget
1632        .charge_xpath_work(context_nodes)
1633        .map_err(ReferenceProcessingError::Transform)?;
1634    execution_budget
1635        .charge_node_filter_work(context_nodes)
1636        .map_err(ReferenceProcessingError::Transform)?;
1637    let mut root = None;
1638    for candidate in target.descendants() {
1639        if !candidate.is_element()
1640            || candidate.tag_name().namespace() != Some(XMLDSIG_NS)
1641            || candidate.tag_name().name() != "X509Data"
1642        {
1643            continue;
1644        }
1645        if root.replace(candidate).is_some() {
1646            return Err(SignatureVerificationPipelineError::InvalidStructure {
1647                reason: "X509Data RetrievalMethod selected multiple X509Data elements",
1648            });
1649        }
1650    }
1651    root.ok_or(SignatureVerificationPipelineError::InvalidStructure {
1652        reason: "X509Data RetrievalMethod selected no X509Data element",
1653    })
1654}
1655
1656#[cfg(test)]
1657fn materialize_retrieval_methods(
1658    key_info: &mut KeyInfo,
1659    resolver: &UriReferenceResolver<'_>,
1660    allowed_uri_types: UriTypeSet,
1661    allowed_transforms: Option<&HashSet<String>>,
1662    provider: &dyn crate::provider::CryptoProvider,
1663) -> Result<RetrievalMaterialization, SignatureVerificationPipelineError> {
1664    let mut xpath_parse_budget = XPathSignatureParseBudget::default();
1665    let execution_budget = TransformExecutionBudget::default();
1666    let resources = crate::policy::ResourcePolicy::default();
1667    let mut budgets = RetrievalMaterializationBudgets {
1668        xpath_parse: &mut xpath_parse_budget,
1669        execution: &execution_budget,
1670        resources: &resources,
1671    };
1672    materialize_retrieval_methods_with_budgets(
1673        key_info,
1674        resolver,
1675        allowed_uri_types,
1676        allowed_transforms,
1677        provider,
1678        &mut budgets,
1679    )
1680}
1681
1682fn existing_x509_binary_len(
1683    key_info: &KeyInfo,
1684) -> Result<usize, SignatureVerificationPipelineError> {
1685    let mut total = 0usize;
1686    for source in &key_info.sources {
1687        if let super::parse::KeyInfoSource::X509Data(info) = source {
1688            for len in info
1689                .certificates
1690                .iter()
1691                .chain(&info.skis)
1692                .chain(&info.crls)
1693                .map(Vec::len)
1694                .chain(info.digests.iter().map(|(_, digest)| digest.len()))
1695            {
1696                add_retrieval_binary_usage(&mut total, len)?;
1697            }
1698        }
1699    }
1700    Ok(total)
1701}
1702
1703fn add_retrieval_binary_usage(
1704    total: &mut usize,
1705    delta: usize,
1706) -> Result<(), SignatureVerificationPipelineError> {
1707    *total =
1708        total
1709            .checked_add(delta)
1710            .ok_or(SignatureVerificationPipelineError::InvalidStructure {
1711                reason: "RetrievalMethod X509Data binary length overflow",
1712            })?;
1713    if *total > MAX_X509_DATA_TOTAL_BINARY_LEN {
1714        return Err(SignatureVerificationPipelineError::InvalidStructure {
1715            reason: "RetrievalMethod X509Data exceeds maximum aggregate binary length",
1716        });
1717    }
1718    Ok(())
1719}
1720
1721fn manifest_reference_failure_reason(
1722    error: ReferenceProcessingError,
1723    ref_index: usize,
1724) -> FailureReason {
1725    match error.into_policy_violation() {
1726        Ok(_) => FailureReason::ReferencePolicyViolation { ref_index },
1727        Err(_) => FailureReason::ReferenceProcessingFailure { ref_index },
1728    }
1729}
1730
1731fn process_manifest_references(
1732    signature_node: Node<'_, '_>,
1733    resolver: &UriReferenceResolver<'_>,
1734    ctx: &VerifyContext<'_>,
1735    signed_info_reference_nodes: &HashSet<NodeId>,
1736    remaining_reference_capacity: usize,
1737    execution: &ReferenceExecutionContext<'_>,
1738    xpath_parse_budget: &mut XPathSignatureParseBudget,
1739) -> Result<Vec<ReferenceResult>, SignatureVerificationPipelineError> {
1740    let mut authenticated_nodes = signed_info_reference_nodes.clone();
1741    let mut processed_manifests = HashSet::new();
1742    let mut remaining_reference_capacity = remaining_reference_capacity;
1743    let mut next_reference_index = 0usize;
1744    let mut results = Vec::new();
1745    loop {
1746        let parsed = parse_manifest_references(
1747            signature_node,
1748            &authenticated_nodes,
1749            &mut processed_manifests,
1750            &mut remaining_reference_capacity,
1751            &mut next_reference_index,
1752            xpath_parse_budget,
1753            ctx.allowed_transform_uris(),
1754        )?;
1755        let manifest_references = parsed.references;
1756        results.extend(parsed.invalid_results);
1757        if manifest_references.is_empty() {
1758            break;
1759        }
1760        results.reserve(manifest_references.len());
1761        for (index, reference, reference_node_id) in &manifest_references {
1762            let result = if execution.transform_budget.remaining_c14n_output() == 0
1763                || reference.transforms.len() > ctx.policy.resources.max_transforms_per_reference
1764                || ctx
1765                    .policy
1766                    .digest_algorithms
1767                    .as_ref()
1768                    .is_some_and(|allowed| !allowed.contains(&reference.digest_method))
1769            {
1770                manifest_reference_invalid_result(
1771                    reference,
1772                    *index,
1773                    FailureReason::ReferencePolicyViolation { ref_index: *index },
1774                )
1775            } else {
1776                match enforce_reference_policies(
1777                    std::slice::from_ref(reference),
1778                    ctx.policy.uris.references,
1779                    ctx.allowed_transform_uris(),
1780                ) {
1781                    Ok(()) => process_reference_with_options(
1782                        reference,
1783                        resolver,
1784                        signature_node,
1785                        ReferenceSet::Manifest,
1786                        *index,
1787                        resolver.node_for_node_id(*reference_node_id),
1788                        execution,
1789                    )
1790                    .unwrap_or_else(|error| {
1791                        manifest_reference_invalid_result(
1792                            reference,
1793                            *index,
1794                            manifest_reference_failure_reason(error, *index),
1795                        )
1796                    }),
1797                    Err(SignatureVerificationPipelineError::Policy(_)) => {
1798                        manifest_reference_invalid_result(
1799                            reference,
1800                            *index,
1801                            FailureReason::ReferencePolicyViolation { ref_index: *index },
1802                        )
1803                    }
1804                    Err(_) => manifest_reference_invalid_result(
1805                        reference,
1806                        *index,
1807                        FailureReason::ReferenceProcessingFailure { ref_index: *index },
1808                    ),
1809                }
1810            };
1811            if result.status == DsigStatus::Valid
1812                && reference
1813                    .transforms
1814                    .iter()
1815                    .all(transform_preserves_manifest_structure)
1816                && let Some(target_id) = reference
1817                    .uri
1818                    .as_deref()
1819                    .and_then(same_document_reference_id)
1820                    .and_then(|id| resolver.node_id_for_id(id))
1821            {
1822                // A valid Manifest digest extends trust only to the exact
1823                // same-document structure preserved by its transform chain.
1824                authenticated_nodes.insert(target_id);
1825            }
1826            results.push(result);
1827        }
1828    }
1829    results.sort_by_key(|result| result.reference_index);
1830    Ok(results)
1831}
1832
1833fn manifest_reference_invalid_result(
1834    reference: &Reference,
1835    index: usize,
1836    reason: FailureReason,
1837) -> ReferenceResult {
1838    ReferenceResult {
1839        reference_set: ReferenceSet::Manifest,
1840        reference_index: index,
1841        uri: reference
1842            .uri
1843            .clone()
1844            .unwrap_or_else(|| "<omitted>".to_owned()),
1845        digest_algorithm: reference.digest_method,
1846        status: DsigStatus::Invalid(reason),
1847        pre_digest_data: None,
1848    }
1849}
1850
1851fn parse_manifest_references(
1852    signature_node: Node<'_, '_>,
1853    authenticated_nodes: &HashSet<NodeId>,
1854    processed_manifests: &mut HashSet<NodeId>,
1855    remaining_reference_capacity: &mut usize,
1856    next_reference_index: &mut usize,
1857    xpath_parse_budget: &mut XPathSignatureParseBudget,
1858    allowed_transforms: Option<&HashSet<String>>,
1859) -> Result<ParsedManifestReferences, SignatureVerificationPipelineError> {
1860    let mut references = Vec::new();
1861    let mut invalid = Vec::new();
1862    for object_node in signature_node.children().filter(|node| {
1863        node.is_element()
1864            && node.tag_name().namespace() == Some(XMLDSIG_NS)
1865            && node.tag_name().name() == "Object"
1866    }) {
1867        let object_is_signed = authenticated_nodes.contains(&object_node.id());
1868        for manifest_node in object_node.children().filter(|node| {
1869            node.is_element()
1870                && node.tag_name().namespace() == Some(XMLDSIG_NS)
1871                && node.tag_name().name() == "Manifest"
1872        }) {
1873            let manifest_is_signed = authenticated_nodes.contains(&manifest_node.id());
1874            // Leave unauthenticated Manifests unmarked so a verified outer
1875            // Manifest can make them eligible on the next discovery pass.
1876            if !object_is_signed && !manifest_is_signed {
1877                continue;
1878            }
1879            if !processed_manifests.insert(manifest_node.id()) {
1880                continue;
1881            }
1882            let mut manifest_children = Vec::new();
1883            for child in manifest_node.children() {
1884                if child.is_text()
1885                    && child.text().is_some_and(|text| {
1886                        text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
1887                    })
1888                {
1889                    return Err(SignatureVerificationPipelineError::InvalidStructure {
1890                        reason: "Manifest contains non-whitespace mixed content",
1891                    });
1892                }
1893                if child.is_element() {
1894                    manifest_children.push(child);
1895                }
1896            }
1897            if manifest_children.is_empty() {
1898                return Err(SignatureVerificationPipelineError::InvalidStructure {
1899                    reason: "Manifest must contain at least one ds:Reference element child",
1900                });
1901            }
1902            for child in manifest_children {
1903                if child.tag_name().namespace() != Some(XMLDSIG_NS)
1904                    || child.tag_name().name() != "Reference"
1905                {
1906                    return Err(SignatureVerificationPipelineError::InvalidStructure {
1907                        reason: "Manifest must contain only ds:Reference element children",
1908                    });
1909                }
1910                if *remaining_reference_capacity == 0 {
1911                    return Err(SignatureVerificationPipelineError::InvalidStructure {
1912                        reason: "signed Manifests exceed the per-signature Reference limit",
1913                    });
1914                }
1915                *remaining_reference_capacity -= 1;
1916                let reference_index = *next_reference_index;
1917                *next_reference_index += 1;
1918                match parse_reference_with_xpath_budget(child, xpath_parse_budget) {
1919                    Ok(reference) => references.push((reference_index, reference, child.id())),
1920                    Err(ParseError::Transform(super::TransformError::UnsupportedTransform(
1921                        uri,
1922                    ))) => {
1923                        let digest_algorithm =
1924                            reference_digest_method(child).map_err(map_manifest_parse_error)?;
1925                        let reason =
1926                            if allowed_transforms.is_some_and(|allowed| !allowed.contains(&uri)) {
1927                                FailureReason::ReferencePolicyViolation {
1928                                    ref_index: reference_index,
1929                                }
1930                            } else {
1931                                FailureReason::ReferenceProcessingFailure {
1932                                    ref_index: reference_index,
1933                                }
1934                            };
1935                        invalid.push(ReferenceResult {
1936                            reference_set: ReferenceSet::Manifest,
1937                            reference_index,
1938                            uri: child.attribute("URI").unwrap_or("<omitted>").to_owned(),
1939                            digest_algorithm,
1940                            status: DsigStatus::Invalid(reason),
1941                            pre_digest_data: None,
1942                        });
1943                    }
1944                    Err(error) => return Err(map_manifest_parse_error(error)),
1945                }
1946            }
1947        }
1948    }
1949    Ok(ParsedManifestReferences {
1950        references,
1951        invalid_results: invalid,
1952    })
1953}
1954
1955struct ParsedManifestReferences {
1956    references: Vec<(usize, Reference, NodeId)>,
1957    invalid_results: Vec<ReferenceResult>,
1958}
1959
1960fn collect_authenticated_signed_info_reference_nodes(
1961    references: &[Reference],
1962    resolver: &UriReferenceResolver<'_>,
1963) -> HashSet<NodeId> {
1964    references
1965        .iter()
1966        // URI dereference identifies the transform input, not necessarily the
1967        // bytes authenticated by its digest. Every transform must preserve the
1968        // complete XML structure needed to trust and process a Manifest.
1969        .filter(|reference| {
1970            reference
1971                .transforms
1972                .iter()
1973                .all(transform_preserves_manifest_structure)
1974        })
1975        .filter_map(|reference| reference.uri.as_deref())
1976        .filter_map(same_document_reference_id)
1977        .filter_map(|id| resolver.node_id_for_id(id))
1978        .collect()
1979}
1980
1981fn transform_preserves_manifest_structure(transform: &Transform) -> bool {
1982    match transform {
1983        Transform::C14n(_) => true,
1984        // Both eligible ID targets are descendants of the owning Signature.
1985        // Enveloped subtraction therefore removes their intersection with that
1986        // Signature subtree, even though the Signature node itself is not in the
1987        // dereferenced node set.
1988        Transform::Enveloped
1989        | Transform::XpathExcludeAllSignatures
1990        | Transform::XPath(_)
1991        | Transform::XPathFilter2(_)
1992        | Transform::Base64Decode => false,
1993    }
1994}
1995
1996enum ResolvedVerifyingKey<'a> {
1997    Borrowed(&'a dyn VerifyingKey),
1998    Owned(Box<dyn VerifyingKey + 'a>),
1999}
2000
2001impl ResolvedVerifyingKey<'_> {
2002    fn as_ref(&self) -> &dyn VerifyingKey {
2003        match self {
2004            Self::Borrowed(key) => *key,
2005            Self::Owned(key) => key.as_ref(),
2006        }
2007    }
2008}
2009
2010fn resolve_verifying_key<'k>(
2011    ctx: &VerifyContext<'k>,
2012    key_info: Option<&KeyInfo>,
2013    algorithm: SignatureAlgorithm,
2014) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
2015    if let Some(key) = ctx.key {
2016        if !ctx.policy.key_sources.preset_key {
2017            return Err(crate::policy::PolicyViolation::KeyTrust {
2018                reason: "pre-resolved verification keys are disabled",
2019            }
2020            .into());
2021        }
2022        require_verifying_key_candidate_capacity(&ctx.policy)?;
2023        return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
2024    }
2025    if let Some(resolver) = ctx.key_resolver {
2026        require_verifying_key_candidate_capacity(&ctx.policy)?;
2027        let resolved = resolver.resolve_with_policy_and_provider(
2028            key_info,
2029            algorithm,
2030            &ctx.policy,
2031            ctx.provider,
2032        )?;
2033        return Ok(resolved.map(ResolvedVerifyingKey::Owned));
2034    }
2035    Ok(None)
2036}
2037
2038fn require_verifying_key_candidate_capacity(
2039    policy: &crate::policy::VerificationPolicy,
2040) -> Result<(), SignatureVerificationPipelineError> {
2041    policy
2042        .resources
2043        .validate_key_candidates(1)
2044        .map_err(Into::into)
2045}
2046
2047fn enforce_reference_policies(
2048    references: &[Reference],
2049    allowed_uri_types: UriTypeSet,
2050    allowed_transforms: Option<&HashSet<String>>,
2051) -> Result<(), SignatureVerificationPipelineError> {
2052    for reference in references {
2053        let uri = reference
2054            .uri
2055            .as_deref()
2056            .ok_or(SignatureVerificationPipelineError::Reference(
2057                ReferenceProcessingError::MissingUri,
2058            ))?;
2059        if !allowed_uri_types.allows(uri) {
2060            return Err(crate::policy::PolicyViolation::Uri {
2061                operation: "verification",
2062                reason: "reference URI class is not permitted",
2063            }
2064            .into());
2065        }
2066
2067        if let Some(allowed) = allowed_transforms {
2068            for transform in &reference.transforms {
2069                let transform_uri = transform.algorithm_uri();
2070                enforce_transform_allowed(Some(allowed), transform_uri)?;
2071            }
2072
2073            // External dereference has an octet-stream data type independent of
2074            // whether the caller supplied the resource. Every transform then
2075            // determines the next type, including implicit binary-to-node-set
2076            // adapters before XML-level transforms.
2077            let produces_binary = transform_chain_produces_binary(
2078                classify_uri(uri) == UriClass::External,
2079                &reference.transforms,
2080            );
2081            if !produces_binary {
2082                enforce_transform_allowed(Some(allowed), DEFAULT_IMPLICIT_C14N_URI)?;
2083            }
2084        }
2085    }
2086    Ok(())
2087}
2088
2089fn enforce_transform_allowed(
2090    allowed_transforms: Option<&HashSet<String>>,
2091    algorithm: &str,
2092) -> Result<(), SignatureVerificationPipelineError> {
2093    if allowed_transforms.is_some_and(|allowed| !allowed.contains(algorithm)) {
2094        return Err(crate::policy::PolicyViolation::Algorithm {
2095            operation: "verification transform",
2096            algorithm: algorithm.to_owned(),
2097        }
2098        .into());
2099    }
2100    Ok(())
2101}
2102
2103#[derive(Debug, Clone, Copy)]
2104pub(super) struct SignatureChildNodes<'a, 'input> {
2105    signed_info_node: Node<'a, 'input>,
2106    signature_value_node: Node<'a, 'input>,
2107    key_info_node: Option<Node<'a, 'input>>,
2108}
2109
2110pub(super) fn parse_signature_children<'a, 'input>(
2111    signature_node: Node<'a, 'input>,
2112) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
2113    let mut signed_info_node: Option<Node<'_, '_>> = None;
2114    let mut signature_value_node: Option<Node<'_, '_>> = None;
2115    let mut key_info_node: Option<Node<'_, '_>> = None;
2116    let mut signed_info_index: Option<usize> = None;
2117    let mut signature_value_index: Option<usize> = None;
2118    let mut key_info_index: Option<usize> = None;
2119    let mut first_unexpected_dsig_index: Option<usize> = None;
2120
2121    let mut element_index = 0usize;
2122    for child in signature_node.children() {
2123        if child.is_text() {
2124            if child
2125                .text()
2126                .is_some_and(|text| !is_xml_whitespace_only(text))
2127            {
2128                return Err(SignatureVerificationPipelineError::InvalidStructure {
2129                    reason: "Signature must not contain non-whitespace mixed content",
2130                });
2131            }
2132            continue;
2133        }
2134        if !child.is_element() {
2135            continue;
2136        }
2137
2138        element_index += 1;
2139        if child.tag_name().namespace() != Some(XMLDSIG_NS) {
2140            return Err(SignatureVerificationPipelineError::InvalidStructure {
2141                reason: "Signature must contain only XMLDSIG element children",
2142            });
2143        }
2144        match child.tag_name().name() {
2145            "SignedInfo" => {
2146                if signed_info_node.is_some() {
2147                    return Err(SignatureVerificationPipelineError::InvalidStructure {
2148                        reason: "SignedInfo must appear exactly once under Signature",
2149                    });
2150                }
2151                signed_info_node = Some(child);
2152                signed_info_index = Some(element_index);
2153            }
2154            "SignatureValue" => {
2155                if signature_value_node.is_some() {
2156                    return Err(SignatureVerificationPipelineError::InvalidStructure {
2157                        reason: "SignatureValue must appear exactly once under Signature",
2158                    });
2159                }
2160                signature_value_node = Some(child);
2161                signature_value_index = Some(element_index);
2162            }
2163            "KeyInfo" => {
2164                if key_info_node.is_some() {
2165                    return Err(SignatureVerificationPipelineError::InvalidStructure {
2166                        reason: "KeyInfo must appear at most once under Signature",
2167                    });
2168                }
2169                key_info_node = Some(child);
2170                key_info_index = Some(element_index);
2171            }
2172            "Object" => {
2173                // Valid Object elements are allowed only after SignedInfo, SignatureValue,
2174                // and optional KeyInfo; this is enforced via first_unexpected_dsig_index.
2175            }
2176            _ => {
2177                if first_unexpected_dsig_index.is_none() {
2178                    first_unexpected_dsig_index = Some(element_index);
2179                }
2180            }
2181        }
2182    }
2183
2184    let signed_info_node =
2185        signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
2186            element: "SignedInfo",
2187        })?;
2188    let signature_value_node =
2189        signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
2190            element: "SignatureValue",
2191        })?;
2192    if signed_info_index != Some(1) {
2193        return Err(SignatureVerificationPipelineError::InvalidStructure {
2194            reason: "SignedInfo must be the first element child of Signature",
2195        });
2196    }
2197    if signature_value_index != Some(2) {
2198        return Err(SignatureVerificationPipelineError::InvalidStructure {
2199            reason: "SignatureValue must be the second element child of Signature",
2200        });
2201    }
2202    if let Some(index) = key_info_index
2203        && index != 3
2204    {
2205        return Err(SignatureVerificationPipelineError::InvalidStructure {
2206            reason: "KeyInfo must be the third element child of Signature when present",
2207        });
2208    }
2209
2210    let allowed_prefix_end = key_info_index.unwrap_or(2);
2211    if let Some(unexpected_index) = first_unexpected_dsig_index {
2212        return Err(SignatureVerificationPipelineError::InvalidStructure {
2213            reason: if unexpected_index > allowed_prefix_end {
2214                "After SignedInfo, SignatureValue, and optional KeyInfo, Signature may contain only Object elements"
2215            } else {
2216                "Signature may contain SignedInfo first, SignatureValue second, optional KeyInfo third, and Object elements thereafter"
2217            },
2218        });
2219    }
2220
2221    Ok(SignatureChildNodes {
2222        signed_info_node,
2223        signature_value_node,
2224        key_info_node,
2225    })
2226}
2227
2228fn decode_signature_value(
2229    signature_value_node: Node<'_, '_>,
2230) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
2231    if signature_value_node
2232        .children()
2233        .any(|child| child.is_element())
2234    {
2235        return Err(SignatureVerificationPipelineError::InvalidStructure {
2236            reason: "SignatureValue must not contain element children",
2237        });
2238    }
2239
2240    let mut normalized = Vec::new();
2241    let mut raw_text_len = 0usize;
2242    for child in signature_value_node
2243        .children()
2244        .filter(|child| child.is_text())
2245    {
2246        if let Some(text) = child.text() {
2247            push_normalized_signature_text(text, &mut raw_text_len, &mut normalized)?;
2248        }
2249    }
2250
2251    Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
2252}
2253
2254fn push_normalized_signature_text(
2255    text: &str,
2256    raw_text_len: &mut usize,
2257    normalized: &mut Vec<u8>,
2258) -> Result<(), SignatureVerificationPipelineError> {
2259    if raw_text_len.saturating_add(text.len()) > MAX_SIGNATURE_VALUE_TEXT_LEN {
2260        return Err(SignatureVerificationPipelineError::InvalidStructure {
2261            reason: "SignatureValue exceeds maximum allowed text length",
2262        });
2263    }
2264    *raw_text_len = raw_text_len.saturating_add(text.len());
2265
2266    normalize_xml_base64_bytes(text.as_bytes(), normalized, |_| true).map_err(|err| {
2267        SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(
2268            err.normalized_offset,
2269            err.invalid_byte,
2270        ))
2271    })?;
2272    if normalized.len() > MAX_SIGNATURE_VALUE_LEN {
2273        return Err(SignatureVerificationPipelineError::InvalidStructure {
2274            reason: "SignatureValue exceeds maximum allowed length",
2275        });
2276    }
2277
2278    Ok(())
2279}
2280
2281fn verify_with_algorithm(
2282    algorithm: SignatureAlgorithm,
2283    public_key_pem: &str,
2284    signed_data: &[u8],
2285    signature_value: &[u8],
2286) -> Result<bool, SignatureVerificationPipelineError> {
2287    match algorithm {
2288        SignatureAlgorithm::DsaSha1 => {
2289            let (rest, pem) = x509_parser::pem::parse_x509_pem(public_key_pem.as_bytes())
2290                .map_err(|_| SignatureVerificationError::InvalidKeyPem)?;
2291            if !rest.iter().all(|byte| byte.is_ascii_whitespace()) || pem.label != "PUBLIC KEY" {
2292                return Err(SignatureVerificationError::InvalidKeyPem.into());
2293            }
2294            Ok(verify_dsa_signature_spki(
2295                algorithm,
2296                &pem.contents,
2297                signed_data,
2298                signature_value,
2299            )?)
2300        }
2301        SignatureAlgorithm::HmacSha1 => Err(SignatureVerificationError::UnsupportedAlgorithm {
2302            uri: algorithm.uri().to_string(),
2303        }
2304        .into()),
2305        SignatureAlgorithm::RsaSha1
2306        | SignatureAlgorithm::RsaSha256
2307        | SignatureAlgorithm::RsaSha384
2308        | SignatureAlgorithm::RsaSha512 => Ok(verify_rsa_signature_pem(
2309            algorithm,
2310            public_key_pem,
2311            signed_data,
2312            signature_value,
2313        )?),
2314        SignatureAlgorithm::EcdsaSha256 | SignatureAlgorithm::EcdsaSha384 => {
2315            // Malformed ECDSA signature bytes are treated as a verification miss
2316            // (Ok(false)) instead of a pipeline error; only key/algorithm and
2317            // crypto-operation failures propagate as Err.
2318            match verify_ecdsa_signature_pem(
2319                algorithm,
2320                public_key_pem,
2321                signed_data,
2322                signature_value,
2323            ) {
2324                Ok(valid) => Ok(valid),
2325                Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
2326                Err(error) => Err(error.into()),
2327            }
2328        }
2329    }
2330}
2331
2332#[cfg(test)]
2333#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
2334mod tests {
2335    use super::*;
2336    use crate::c14n::C14nAlgorithm;
2337    use crate::xmldsig::TransformError;
2338    use crate::xmldsig::digest::DigestAlgorithm;
2339    use crate::xmldsig::parse::{Reference, parse_signed_info};
2340    use crate::xmldsig::transforms::Transform;
2341    use crate::xmldsig::uri::UriReferenceResolver;
2342    use base64::Engine;
2343    use roxmltree::Document;
2344
2345    // ── Helpers ──────────────────────────────────────────────────────
2346
2347    /// Build a Reference with given URI, transforms, digest method, and expected digest.
2348    fn make_reference(
2349        uri: &str,
2350        transforms: Vec<Transform>,
2351        digest_method: DigestAlgorithm,
2352        digest_value: Vec<u8>,
2353    ) -> Reference {
2354        Reference {
2355            uri: Some(uri.to_string()),
2356            id: None,
2357            ref_type: None,
2358            transforms,
2359            digest_method,
2360            digest_value,
2361        }
2362    }
2363
2364    #[test]
2365    fn reference_resolution_uses_each_elements_effective_xml_base() {
2366        // Equal lexical URIs under different xml:base values identify distinct
2367        // caller-owned resources and must not collide in the resolver.
2368        let first = b"first payload";
2369        let second = b"second payload";
2370        let first_digest = base64::engine::general_purpose::STANDARD
2371            .encode(compute_digest(DigestAlgorithm::Sha256, first));
2372        let second_digest = base64::engine::general_purpose::STANDARD
2373            .encode(compute_digest(DigestAlgorithm::Sha256, second));
2374        let xml = format!(
2375            r#"<root xml:base="https://example.test/base/" xmlns:ds="{XMLDSIG_NS}">
2376                <ds:Signature><ds:SignedInfo>
2377                    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2378                    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2379                    <ds:Reference xml:base="one/" URI="payload.bin">
2380                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2381                        <ds:DigestValue>{first_digest}</ds:DigestValue>
2382                    </ds:Reference>
2383                    <ds:Reference xml:base="../two/" URI="payload.bin">
2384                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2385                        <ds:DigestValue>{second_digest}</ds:DigestValue>
2386                    </ds:Reference>
2387                </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>
2388            </root>"#
2389        );
2390        let document = Document::parse(&xml).unwrap();
2391        let signature = document
2392            .descendants()
2393            .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
2394            .unwrap();
2395        let signed_info_node = signature
2396            .children()
2397            .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
2398            .unwrap();
2399        let signed_info = parse_signed_info(signed_info_node).unwrap();
2400        let resources = HashMap::from([
2401            (
2402                "https://example.test/base/one/payload.bin".into(),
2403                first.to_vec(),
2404            ),
2405            (
2406                "https://example.test/two/payload.bin".into(),
2407                second.to_vec(),
2408            ),
2409        ]);
2410        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
2411
2412        let result = process_all_references(&signed_info.references, &resolver, signature, false)
2413            .expect("each Reference should resolve against its own effective base");
2414
2415        assert!(result.all_valid());
2416    }
2417
2418    #[test]
2419    fn internal_dtd_opt_in_applies_to_detached_xml_transforms() {
2420        // The parse policy covers every XML document in one verification
2421        // pipeline, including caller-owned octets converted to a node-set.
2422        let detached = b"<!DOCTYPE payload [<!ELEMENT payload (#PCDATA)>]><payload>ok</payload>";
2423        let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
2424            DigestAlgorithm::Sha256,
2425            b"<payload>ok</payload>",
2426        ));
2427        let xml = format!(
2428            r#"<root xmlns:ds="{XMLDSIG_NS}">
2429  <ds:Signature>
2430    <ds:SignedInfo>
2431      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2432      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2433      <ds:Reference URI="urn:detached-dtd">
2434        <ds:Transforms>
2435          <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
2436        </ds:Transforms>
2437        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2438        <ds:DigestValue>{digest}</ds:DigestValue>
2439      </ds:Reference>
2440    </ds:SignedInfo>
2441    <ds:SignatureValue>AQ==</ds:SignatureValue>
2442  </ds:Signature>
2443</root>"#
2444        );
2445        let resources = HashMap::from([("urn:detached-dtd".to_owned(), detached.to_vec())]);
2446        let key = AcceptingKey;
2447
2448        let default_error = VerifyContext::new()
2449            .key(&key)
2450            .allowed_uri_types(UriTypeSet::ALL)
2451            .external_resources(&resources)
2452            .verify(&xml)
2453            .expect_err("internal DTD parsing must remain disabled by default");
2454        assert!(matches!(
2455            default_error,
2456            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
2457                crate::xmldsig::TransformError::XmlParse(_)
2458            ))
2459        ));
2460
2461        let result = VerifyContext::new()
2462            .key(&key)
2463            .allowed_uri_types(UriTypeSet::ALL)
2464            .external_resources(&resources)
2465            .allow_internal_dtd(true)
2466            .verify(&xml)
2467            .expect("the explicit DTD opt-in must cover detached XML transforms");
2468
2469        assert_eq!(result.status, DsigStatus::Valid);
2470
2471        let external_entity = br#"<!DOCTYPE payload [
2472            <!ENTITY ext SYSTEM "file:///etc/passwd">
2473        ]><payload>&ext;</payload>"#;
2474        let external_entity_resources =
2475            HashMap::from([("urn:detached-dtd".to_owned(), external_entity.to_vec())]);
2476        let external_entity_error = VerifyContext::new()
2477            .key(&key)
2478            .allowed_uri_types(UriTypeSet::ALL)
2479            .external_resources(&external_entity_resources)
2480            .allow_internal_dtd(true)
2481            .verify(&xml)
2482            .expect_err("the internal-DTD opt-in must not resolve external entities");
2483        assert!(matches!(
2484            external_entity_error,
2485            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
2486                crate::xmldsig::TransformError::XmlParse(_)
2487            ))
2488        ));
2489    }
2490
2491    #[test]
2492    fn verification_policy_bounds_reference_canonicalization() {
2493        // Reference transforms and SignedInfo canonicalization are one operation;
2494        // references must not fall back to the transform hard-limit budget.
2495        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
2496        let xml = format!(
2497            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>"#,
2498            "payload".repeat(16)
2499        );
2500        let policy = crate::policy::VerificationPolicy {
2501            resources: crate::policy::ResourcePolicy {
2502                max_canonicalized_bytes: 64,
2503                ..crate::policy::ResourcePolicy::default()
2504            },
2505            ..crate::policy::VerificationPolicy::default()
2506        };
2507
2508        let error = VerifyContext::new()
2509            .key(&AcceptingKey)
2510            .policy(policy)
2511            .verify(&xml)
2512            .expect_err("reference canonicalization must consume the policy budget");
2513
2514        assert!(
2515            matches!(
2516                error,
2517                SignatureVerificationPipelineError::Policy(
2518                    crate::policy::PolicyViolation::ResourceLimit {
2519                        resource: crate::policy::resource_name::CANONICALIZED_BYTES,
2520                        maximum: 64,
2521                        ..
2522                    }
2523                )
2524            ),
2525            "unexpected error: {error:?}"
2526        );
2527    }
2528
2529    #[test]
2530    fn verification_policy_bounds_document_bytes_before_parsing() {
2531        // A small node count does not bound parser work when one text node is
2532        // large, so the byte ceiling must reject before structural inspection.
2533        let xml = format!("<root>{}</root>", "x".repeat(1_024));
2534        let policy = crate::policy::VerificationPolicy {
2535            resources: crate::policy::ResourcePolicy {
2536                max_xml_document_bytes: xml.len() - 1,
2537                ..crate::policy::ResourcePolicy::default()
2538            },
2539            ..crate::policy::VerificationPolicy::default()
2540        };
2541
2542        assert!(matches!(
2543            VerifyContext::new().policy(policy).verify(&xml),
2544            Err(SignatureVerificationPipelineError::Policy(
2545                crate::policy::PolicyViolation::ResourceLimit {
2546                    resource: crate::policy::resource_name::XML_DOCUMENT,
2547                    maximum,
2548                    actual,
2549                }
2550            )) if maximum == xml.len() - 1 && actual == xml.len()
2551        ));
2552    }
2553
2554    #[test]
2555    fn verification_policy_bounds_base64_transform_input() {
2556        // The operation snapshot must reach the transform executor rather than
2557        // silently falling back to its implementation-wide default budget.
2558        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
2559        let xml = format!(
2560            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>"##
2561        );
2562        let policy = crate::policy::VerificationPolicy {
2563            resources: crate::policy::ResourcePolicy {
2564                max_base64_transform_input_bytes: 4,
2565                ..crate::policy::ResourcePolicy::default()
2566            },
2567            ..crate::policy::VerificationPolicy::default()
2568        };
2569
2570        let error = VerifyContext::new()
2571            .key(&AcceptingKey)
2572            .policy(policy)
2573            .verify(&xml)
2574            .expect_err("Base64 input must use the operation policy ceiling");
2575
2576        assert!(matches!(
2577            error,
2578            SignatureVerificationPipelineError::Policy(
2579                crate::policy::PolicyViolation::ResourceLimit {
2580                    resource: crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
2581                    maximum: 4,
2582                    ..
2583                }
2584            )
2585        ));
2586    }
2587
2588    #[test]
2589    fn verification_policy_bounds_cumulative_base64_transform_output() {
2590        // References share one operation budget. Validating each decoded value
2591        // against the full ceiling would let a signature multiply output work.
2592        let first_digest = base64::engine::general_purpose::STANDARD
2593            .encode(compute_digest(DigestAlgorithm::Sha256, b"a"));
2594        let second_digest = base64::engine::general_purpose::STANDARD
2595            .encode(compute_digest(DigestAlgorithm::Sha256, b"b"));
2596        let xml = format!(
2597            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>"##
2598        );
2599        let policy = crate::policy::VerificationPolicy {
2600            resources: crate::policy::ResourcePolicy {
2601                max_base64_transform_input_bytes: 8,
2602                max_base64_transform_output_bytes: 1,
2603                ..crate::policy::ResourcePolicy::default()
2604            },
2605            ..crate::policy::VerificationPolicy::default()
2606        };
2607
2608        let error = VerifyContext::new()
2609            .key(&AcceptingKey)
2610            .policy(policy)
2611            .verify(&xml)
2612            .expect_err("references must share the Base64 output allowance");
2613
2614        assert!(matches!(
2615            error,
2616            SignatureVerificationPipelineError::Policy(
2617                crate::policy::PolicyViolation::ResourceLimit {
2618                    resource: crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
2619                    maximum: 1,
2620                    actual: 2,
2621                }
2622            )
2623        ));
2624    }
2625
2626    #[test]
2627    fn verification_policy_bounds_xpath_source_before_compilation() {
2628        // XPath parser limits are part of the same operation snapshot as the
2629        // evaluator limits; parsing cannot use a separate hard-coded budget.
2630        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
2631        let xml = format!(
2632            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>"#
2633        );
2634        let policy = crate::policy::VerificationPolicy {
2635            resources: crate::policy::ResourcePolicy {
2636                max_xpath_expression_bytes: 4,
2637                ..crate::policy::ResourcePolicy::default()
2638            },
2639            ..crate::policy::VerificationPolicy::default()
2640        };
2641
2642        let error = VerifyContext::new()
2643            .key(&AcceptingKey)
2644            .policy(policy)
2645            .verify(&xml)
2646            .expect_err("XPath source must use the operation policy ceiling");
2647
2648        assert!(matches!(
2649            error,
2650            SignatureVerificationPipelineError::Policy(
2651                crate::policy::PolicyViolation::ResourceLimit {
2652                    resource: crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
2653                    maximum: 4,
2654                    ..
2655                }
2656            )
2657        ));
2658    }
2659
2660    #[test]
2661    fn verification_policy_shares_canonicalization_budget_with_signed_info() {
2662        // Reference transforms and SignedInfo canonicalization are one operation.
2663        // Each output fits independently, but their aggregate must not receive
2664        // two separate copies of the configured canonicalization allowance.
2665        let payload_text = "x".repeat(700);
2666        let canonical_payload = format!("<payload ID=\"payload\">{payload_text}</payload>");
2667        let digest = base64::engine::general_purpose::STANDARD.encode(compute_digest(
2668            DigestAlgorithm::Sha256,
2669            canonical_payload.as_bytes(),
2670        ));
2671        let xml = format!(
2672            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>"##
2673        );
2674        let policy = crate::policy::VerificationPolicy {
2675            resources: crate::policy::ResourcePolicy {
2676                max_canonicalized_bytes: 1_024,
2677                ..crate::policy::ResourcePolicy::default()
2678            },
2679            ..crate::policy::VerificationPolicy::default()
2680        };
2681
2682        let error = VerifyContext::new()
2683            .key(&AcceptingKey)
2684            .policy(policy)
2685            .verify(&xml)
2686            .expect_err("SignedInfo must consume the remaining operation C14N budget");
2687
2688        assert!(
2689            matches!(
2690                &error,
2691                SignatureVerificationPipelineError::Policy(
2692                    crate::policy::PolicyViolation::ResourceLimit {
2693                        resource: "canonicalized bytes",
2694                        maximum: 1_024,
2695                        ..
2696                    }
2697                )
2698            ),
2699            "unexpected error: {error:?}"
2700        );
2701    }
2702
2703    #[test]
2704    fn manifest_processing_stops_after_c14n_budget_exhaustion() {
2705        // A failed bounded render consumes the remaining operation allowance.
2706        // Later Manifest references, including cheap binary ones, must not run.
2707        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
2708        let xml = format!(
2709            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>"##
2710        );
2711        let document = Document::parse(&xml).expect("test signature must parse");
2712        let signature = document
2713            .descendants()
2714            .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
2715            .expect("test signature must contain Signature");
2716        let object = signature
2717            .children()
2718            .find(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
2719            .expect("test signature must contain Object");
2720        let resources = HashMap::from([("urn:small".to_owned(), b"small".to_vec())]);
2721        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
2722        let transform_budget = TransformExecutionBudget::with_c14n_limit(8);
2723        let canonicalized_data_budget = CanonicalizedDataBudget::default();
2724        let execution = ReferenceExecutionContext {
2725            store_pre_digest: false,
2726            transform_options: TransformOptions::default(),
2727            transform_budget: &transform_budget,
2728            canonicalized_data_budget: &canonicalized_data_budget,
2729            provider: crate::provider::default_provider(),
2730        };
2731        let ctx = VerifyContext::new()
2732            .allowed_uri_types(UriTypeSet::ALL)
2733            .external_resources(&resources);
2734        let authenticated = HashSet::from([object.id()]);
2735        let mut xpath_budget = XPathSignatureParseBudget::default();
2736
2737        let results = process_manifest_references(
2738            signature,
2739            &resolver,
2740            &ctx,
2741            &authenticated,
2742            2,
2743            &execution,
2744            &mut xpath_budget,
2745        )
2746        .expect("resource exhaustion is reported per Manifest reference");
2747
2748        assert_eq!(results.len(), 2);
2749        assert!(results.iter().all(|result| {
2750            matches!(
2751                result.status,
2752                DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { .. })
2753            )
2754        }));
2755    }
2756
2757    #[test]
2758    fn verification_policy_bounds_detached_xml_nodes() {
2759        // Caller-owned detached octets become a second XML document during a
2760        // node-set transform and must inherit the same operation node ceiling.
2761        let detached = format!("<payload>{}</payload>", "<n/>".repeat(32));
2762        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
2763        let xml = format!(
2764            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>"#
2765        );
2766        let resources = HashMap::from([("urn:detached-nodes".to_owned(), detached.into_bytes())]);
2767        let policy = crate::policy::VerificationPolicy {
2768            uris: crate::policy::UriPolicy {
2769                references: UriTypeSet::ALL,
2770                ..crate::policy::UriPolicy::default()
2771            },
2772            resources: crate::policy::ResourcePolicy {
2773                max_xml_nodes: 24,
2774                ..crate::policy::ResourcePolicy::default()
2775            },
2776            ..crate::policy::VerificationPolicy::default()
2777        };
2778
2779        let error = VerifyContext::new()
2780            .key(&AcceptingKey)
2781            .policy(policy)
2782            .external_resources(&resources)
2783            .verify(&xml)
2784            .expect_err("detached XML must inherit the policy node ceiling");
2785
2786        assert!(
2787            matches!(
2788                error,
2789                SignatureVerificationPipelineError::Policy(
2790                    crate::policy::PolicyViolation::ResourceLimit {
2791                        resource: crate::policy::resource_name::XML_NODES,
2792                        maximum: 24,
2793                        ..
2794                    }
2795                )
2796            ),
2797            "unexpected error: {error:?}"
2798        );
2799    }
2800
2801    #[test]
2802    fn query_only_reference_resolves_against_relative_xml_base() {
2803        // A query-only URI replaces the inherited base query without changing
2804        // its relative path; no absolute document base is required by XML Base.
2805        let payload = b"query-selected payload";
2806        let digest = base64::engine::general_purpose::STANDARD
2807            .encode(compute_digest(DigestAlgorithm::Sha256, payload));
2808        let xml = format!(
2809            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}"><ds:SignedInfo>
2810                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2811                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2812                <ds:Reference xml:base="a/b?old" URI="?new">
2813                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2814                    <ds:DigestValue>{digest}</ds:DigestValue>
2815                </ds:Reference>
2816            </ds:SignedInfo><ds:SignatureValue>AA==</ds:SignatureValue></ds:Signature>"#
2817        );
2818        let document = Document::parse(&xml).unwrap();
2819        let signature = document.root_element();
2820        let signed_info_node = signature
2821            .children()
2822            .find(|node| node.has_tag_name((XMLDSIG_NS, "SignedInfo")))
2823            .unwrap();
2824        let signed_info = parse_signed_info(signed_info_node).unwrap();
2825        let resources = HashMap::from([("a/b?new".to_string(), payload.to_vec())]);
2826        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
2827
2828        let result = process_all_references(&signed_info.references, &resolver, signature, false)
2829            .expect("query-only URI must resolve against the complete relative base path");
2830
2831        assert!(result.all_valid());
2832    }
2833
2834    #[test]
2835    fn manifest_reference_resolution_uses_its_effective_xml_base() {
2836        // Manifest references carry their own XML Base context and must not
2837        // accidentally reuse the SignedInfo or Signature element context.
2838        let payload = b"manifest payload";
2839        let digest = base64::engine::general_purpose::STANDARD
2840            .encode(compute_digest(DigestAlgorithm::Sha256, payload));
2841        let xml = format!(
2842            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
2843                <ds:Object><ds:Manifest xml:base="manifests/">
2844                    <ds:Reference URI="payload.bin">
2845                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2846                        <ds:DigestValue>{digest}</ds:DigestValue>
2847                    </ds:Reference>
2848                </ds:Manifest></ds:Object>
2849            </ds:Signature>"#
2850        );
2851        let document = Document::parse(&xml).unwrap();
2852        let signature = document.root_element();
2853        let reference_node = signature
2854            .descendants()
2855            .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
2856            .unwrap();
2857        let reference = super::super::parse::parse_reference(reference_node).unwrap();
2858        let resources = HashMap::from([(
2859            "https://example.test/manifests/payload.bin".to_string(),
2860            payload.to_vec(),
2861        )]);
2862        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
2863
2864        let result = process_reference(
2865            &reference,
2866            &resolver,
2867            signature,
2868            ReferenceSet::Manifest,
2869            0,
2870            false,
2871        )
2872        .expect("Manifest Reference should inherit its own XML Base context");
2873
2874        assert_eq!(result.status, DsigStatus::Valid);
2875    }
2876
2877    #[test]
2878    fn manifest_reference_index_ignores_nested_manifest_descendants() {
2879        // The public Manifest index follows Signature/Object/Manifest structure;
2880        // wrapper descendants must not steal an index and supply another base URI.
2881        let payload = b"direct manifest payload";
2882        let digest = base64::engine::general_purpose::STANDARD
2883            .encode(compute_digest(DigestAlgorithm::Sha256, payload));
2884        let xml = format!(
2885            r#"<ds:Signature xmlns:ds="{XMLDSIG_NS}" xml:base="https://example.test/">
2886                <ds:Object><wrapper><ds:Manifest xml:base="nested/">
2887                    <ds:Reference URI="payload.bin">
2888                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2889                        <ds:DigestValue>{digest}</ds:DigestValue>
2890                    </ds:Reference>
2891                </ds:Manifest></wrapper></ds:Object>
2892                <ds:Object><ds:Manifest xml:base="direct/">
2893                    <ds:Reference URI="payload.bin">
2894                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2895                        <ds:DigestValue>{digest}</ds:DigestValue>
2896                    </ds:Reference>
2897                </ds:Manifest></ds:Object>
2898            </ds:Signature>"#
2899        );
2900        let document = Document::parse(&xml).unwrap();
2901        let signature = document.root_element();
2902        let direct_reference_node = signature
2903            .children()
2904            .filter(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
2905            .nth(1)
2906            .unwrap()
2907            .children()
2908            .find(|node| node.has_tag_name((XMLDSIG_NS, "Manifest")))
2909            .unwrap()
2910            .children()
2911            .find(|node| node.has_tag_name((XMLDSIG_NS, "Reference")))
2912            .unwrap();
2913        let reference = super::super::parse::parse_reference(direct_reference_node).unwrap();
2914        let resources = HashMap::from([(
2915            "https://example.test/direct/payload.bin".to_string(),
2916            payload.to_vec(),
2917        )]);
2918        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
2919
2920        let result = process_reference(
2921            &reference,
2922            &resolver,
2923            signature,
2924            ReferenceSet::Manifest,
2925            0,
2926            false,
2927        )
2928        .expect("Manifest index must select the direct Object/Manifest reference");
2929
2930        assert_eq!(result.status, DsigStatus::Valid);
2931    }
2932
2933    struct RejectingKey;
2934
2935    impl VerifyingKey for RejectingKey {
2936        fn verify(
2937            &self,
2938            _algorithm: SignatureAlgorithm,
2939            _signed_data: &[u8],
2940            _signature_value: &[u8],
2941        ) -> Result<bool, SignatureVerificationPipelineError> {
2942            Ok(false)
2943        }
2944    }
2945
2946    struct AcceptingKey;
2947
2948    impl VerifyingKey for AcceptingKey {
2949        fn verify(
2950            &self,
2951            _algorithm: SignatureAlgorithm,
2952            _signed_data: &[u8],
2953            _signature_value: &[u8],
2954        ) -> Result<bool, SignatureVerificationPipelineError> {
2955            Ok(true)
2956        }
2957    }
2958
2959    struct PanicResolver;
2960
2961    impl KeyResolver for PanicResolver {
2962        fn resolve<'a>(
2963            &'a self,
2964            _key_info: Option<&KeyInfo>,
2965            _algorithm: SignatureAlgorithm,
2966        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
2967        {
2968            panic!("resolver should not be called when references already fail");
2969        }
2970    }
2971
2972    struct MissingKeyResolver;
2973
2974    impl KeyResolver for MissingKeyResolver {
2975        fn resolve<'a>(
2976            &'a self,
2977            _key_info: Option<&KeyInfo>,
2978            _algorithm: SignatureAlgorithm,
2979        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
2980        {
2981            Ok(None)
2982        }
2983    }
2984
2985    struct ConsumingKeyInfoResolver;
2986
2987    impl KeyResolver for ConsumingKeyInfoResolver {
2988        fn resolve<'a>(
2989            &'a self,
2990            _key_info: Option<&KeyInfo>,
2991            _algorithm: SignatureAlgorithm,
2992        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
2993        {
2994            Ok(None)
2995        }
2996
2997        fn consumes_document_key_info(&self) -> bool {
2998            true
2999        }
3000    }
3001
3002    struct FallbackKeyInfoResolver;
3003
3004    impl KeyResolver for FallbackKeyInfoResolver {
3005        fn resolve<'a>(
3006            &'a self,
3007            key_info: Option<&KeyInfo>,
3008            _algorithm: SignatureAlgorithm,
3009        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
3010        {
3011            let sources = &key_info.expect("KeyInfo must be parsed").sources;
3012            assert!(matches!(
3013                sources.as_slice(),
3014                [
3015                    super::super::parse::KeyInfoSource::RetrievalMethod { .. },
3016                    super::super::parse::KeyInfoSource::KeyName(name),
3017                ] if name == "fallback"
3018            ));
3019            Ok(Some(Box::new(AcceptingKey)))
3020        }
3021
3022        fn consumes_document_key_info(&self) -> bool {
3023            true
3024        }
3025    }
3026
3027    struct EarlyKeyInfoResolver;
3028
3029    impl KeyResolver for EarlyKeyInfoResolver {
3030        fn resolve<'a>(
3031            &'a self,
3032            key_info: Option<&KeyInfo>,
3033            _algorithm: SignatureAlgorithm,
3034        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
3035        {
3036            let sources = &key_info.expect("KeyInfo must be parsed").sources;
3037            assert!(matches!(
3038                sources.as_slice(),
3039                [
3040                    super::super::parse::KeyInfoSource::KeyName(name),
3041                    super::super::parse::KeyInfoSource::RetrievalMethod { .. },
3042                ] if name == "primary"
3043            ));
3044            Ok(Some(Box::new(AcceptingKey)))
3045        }
3046
3047        fn consumes_document_key_info(&self) -> bool {
3048            true
3049        }
3050    }
3051
3052    fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
3053        format!(
3054            r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3055  <ds:SignedInfo>
3056    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3057    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3058    <ds:Reference URI="{reference_uri}">
3059      {transforms_xml}
3060      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3061      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
3062    </ds:Reference>
3063  </ds:SignedInfo>
3064  <ds:SignatureValue>AQ==</ds:SignatureValue>
3065</ds:Signature>"#
3066        )
3067    }
3068
3069    fn signature_with_target_reference(signature_value_b64: &str) -> String {
3070        let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3071  <target ID="target">payload</target>
3072  <ds:Signature>
3073    <ds:SignedInfo>
3074      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3075      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3076      <ds:Reference URI="#target">
3077        <ds:Transforms>
3078          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3079        </ds:Transforms>
3080        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3081        <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
3082      </ds:Reference>
3083    </ds:SignedInfo>
3084    <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
3085  </ds:Signature>
3086</root>"##;
3087
3088        let doc = Document::parse(xml_template).unwrap();
3089        let sig_node = doc
3090            .descendants()
3091            .find(|node| node.is_element() && node.tag_name().name() == "Signature")
3092            .unwrap();
3093        let signed_info_node = sig_node
3094            .children()
3095            .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
3096            .unwrap();
3097        let signed_info = parse_signed_info(signed_info_node).unwrap();
3098        let reference = &signed_info.references[0];
3099        let resolver = UriReferenceResolver::new(&doc);
3100        let initial_data = resolver
3101            .dereference(reference.uri.as_deref().unwrap())
3102            .unwrap();
3103        let pre_digest =
3104            crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
3105                .unwrap();
3106        let digest = compute_digest(reference.digest_method, &pre_digest);
3107        let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
3108        xml_template
3109            .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
3110            .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
3111    }
3112
3113    #[test]
3114    fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
3115        let xml = signature_with_target_reference("AQ==");
3116
3117        let result = VerifyContext::new()
3118            .verify(&xml)
3119            .expect("missing key config must be reported as verification status");
3120        assert!(
3121            matches!(
3122                result.status,
3123                DsigStatus::Invalid(FailureReason::KeyNotFound)
3124            ),
3125            "unexpected status: {:?}",
3126            result.status
3127        );
3128    }
3129
3130    #[test]
3131    fn verify_context_rejects_disallowed_uri() {
3132        let xml = minimal_signature_xml("http://example.com/external", "");
3133        let err = VerifyContext::new()
3134            .key(&RejectingKey)
3135            .verify(&xml)
3136            .expect_err("external URI should be rejected by default policy");
3137        assert!(matches!(
3138            err,
3139            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
3140                operation: "verification",
3141                ..
3142            })
3143        ));
3144    }
3145
3146    #[test]
3147    fn verify_context_bounds_effective_xml_base_components() {
3148        // External URI resolution must stop before repeatedly copying an
3149        // attacker-controlled chain of effective XML Base values.
3150        let mut xml = minimal_signature_xml("payload", "");
3151        for _ in 0..65 {
3152            xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
3153        }
3154        let resources = HashMap::new();
3155        let error = VerifyContext::new()
3156            .key(&AcceptingKey)
3157            .allowed_uri_types(UriTypeSet::ALL)
3158            .external_resources(&resources)
3159            .verify(&xml)
3160            .expect_err("XML Base component work must be bounded before lookup");
3161
3162        assert!(matches!(
3163            error,
3164            SignatureVerificationPipelineError::Policy(
3165                crate::policy::PolicyViolation::ResourceLimit {
3166                    resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
3167                    maximum: 64,
3168                    actual: 65,
3169                }
3170            )
3171        ));
3172    }
3173
3174    #[test]
3175    fn verify_context_bounds_cumulative_xml_base_resolution_bytes() {
3176        // The operation-wide byte budget charges intermediate URI copies, not
3177        // merely the small final external resource returned by the caller map.
3178        let mut xml = minimal_signature_xml("payload", "");
3179        for _ in 0..2 {
3180            xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
3181        }
3182        let resources = HashMap::new();
3183        let mut policy = crate::policy::VerificationPolicy::default();
3184        policy.resources.max_xml_base_resolution_bytes = 32;
3185        let error = VerifyContext::new()
3186            .policy(policy)
3187            .key(&AcceptingKey)
3188            .allowed_uri_types(UriTypeSet::ALL)
3189            .external_resources(&resources)
3190            .verify(&xml)
3191            .expect_err("cumulative XML Base copies must obey the operation budget");
3192
3193        assert!(matches!(
3194            error,
3195            SignatureVerificationPipelineError::Policy(
3196                crate::policy::PolicyViolation::ResourceLimit {
3197                    resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
3198                    maximum: 32,
3199                    ..
3200                }
3201            )
3202        ));
3203    }
3204
3205    #[test]
3206    fn verify_context_applies_xml_base_policy_to_signed_info_c14n() {
3207        // The SignedInfo node-set excludes its ancestors, so C14N 1.1 must
3208        // resolve their inherited xml:base values through the same operation
3209        // budget already used by Reference processing.
3210        let xml = signature_with_target_reference("AQ==")
3211            .replacen(
3212                "http://www.w3.org/2001/10/xml-exc-c14n#",
3213                "http://www.w3.org/2006/12/xml-c14n11",
3214                1,
3215            )
3216            .replace(
3217                "  <ds:Signature>",
3218                "  <outer xml:base=\"one/\"><inner xml:base=\"two/\"><ds:Signature>",
3219            )
3220            .replace("  </ds:Signature>", "  </ds:Signature></inner></outer>");
3221        let policy = crate::policy::VerificationPolicy {
3222            resources: crate::policy::ResourcePolicy {
3223                max_xml_base_components: 1,
3224                ..crate::policy::ResourcePolicy::default()
3225            },
3226            ..crate::policy::VerificationPolicy::default()
3227        };
3228
3229        let error = VerifyContext::new()
3230            .key(&AcceptingKey)
3231            .policy(policy)
3232            .verify(&xml)
3233            .expect_err("SignedInfo C14N must use the operation XML Base budget");
3234
3235        assert!(matches!(
3236            error,
3237            SignatureVerificationPipelineError::Policy(
3238                crate::policy::PolicyViolation::ResourceLimit {
3239                    resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
3240                    maximum: 1,
3241                    actual: 2,
3242                }
3243            )
3244        ));
3245    }
3246
3247    #[test]
3248    fn verify_context_classifies_signed_info_xml_base_byte_limit_as_policy() {
3249        // SignedInfo C14N 1.1 XML Base work is policy enforcement, not a
3250        // malformed canonicalization request, and must retain typed diagnostics.
3251        let xml = signature_with_target_reference("AQ==")
3252            .replacen(
3253                "http://www.w3.org/2001/10/xml-exc-c14n#",
3254                "http://www.w3.org/2006/12/xml-c14n11",
3255                1,
3256            )
3257            .replace(
3258                "  <ds:Signature>",
3259                "  <outer xml:base=\"segment/\"><ds:Signature>",
3260            )
3261            .replace("  </ds:Signature>", "  </ds:Signature></outer>");
3262        let policy = crate::policy::VerificationPolicy {
3263            resources: crate::policy::ResourcePolicy {
3264                max_xml_base_resolution_bytes: 1,
3265                ..crate::policy::ResourcePolicy::default()
3266            },
3267            ..crate::policy::VerificationPolicy::default()
3268        };
3269
3270        let error = VerifyContext::new()
3271            .key(&AcceptingKey)
3272            .policy(policy)
3273            .verify(&xml)
3274            .expect_err("SignedInfo XML Base byte exhaustion must be a policy error");
3275
3276        assert!(matches!(
3277            error,
3278            SignatureVerificationPipelineError::Policy(
3279                crate::policy::PolicyViolation::ResourceLimit {
3280                    resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
3281                    maximum: 1,
3282                    actual,
3283                }
3284            ) if actual > 1
3285        ));
3286    }
3287
3288    #[test]
3289    fn verify_context_meters_repeated_external_dereferences() {
3290        // One caller-owned entry can be referenced repeatedly. The aggregate
3291        // ceiling bounds bytes cloned and processed, not just unique map data.
3292        let payload = b"payload";
3293        let digest = base64::engine::general_purpose::STANDARD.encode(
3294            crate::xmldsig::compute_digest(DigestAlgorithm::Sha1, payload),
3295        );
3296        let reference = format!(
3297            r#"<ds:Reference URI="urn:payload"><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference>"#
3298        );
3299        let xml = format!(
3300            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>"#
3301        );
3302        let resources = HashMap::from([("urn:payload".to_owned(), payload.to_vec())]);
3303        let policy = crate::policy::VerificationPolicy {
3304            uris: crate::policy::UriPolicy {
3305                references: UriTypeSet::ALL,
3306                ..crate::policy::UriPolicy::default()
3307            },
3308            resources: crate::policy::ResourcePolicy {
3309                max_external_resource_bytes: payload.len(),
3310                max_external_resource_total_bytes: payload.len(),
3311                ..crate::policy::ResourcePolicy::default()
3312            },
3313            ..crate::policy::VerificationPolicy::default()
3314        };
3315
3316        let error = VerifyContext::new()
3317            .key(&AcceptingKey)
3318            .policy(policy)
3319            .external_resources(&resources)
3320            .verify(&xml)
3321            .expect_err("the second dereference must exhaust the aggregate byte ceiling");
3322
3323        assert!(
3324            error
3325                .to_string()
3326                .contains("aggregate external resource bytes")
3327        );
3328    }
3329
3330    #[test]
3331    fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
3332        let xml = minimal_signature_xml("", "");
3333        let err = VerifyContext::new()
3334            .key(&RejectingKey)
3335            .allowed_uri_types(UriTypeSet::new(false, true, false))
3336            .verify(&xml)
3337            .expect_err("empty URI must be rejected when empty references are disabled");
3338        assert!(matches!(
3339            err,
3340            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Uri {
3341                operation: "verification",
3342                ..
3343            })
3344        ));
3345    }
3346
3347    #[test]
3348    fn verify_context_rejects_disallowed_transform() {
3349        let xml = minimal_signature_xml(
3350            "",
3351            r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
3352        );
3353        let err = VerifyContext::new()
3354            .key(&RejectingKey)
3355            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
3356            .verify(&xml)
3357            .expect_err("enveloped transform should be rejected by allowlist");
3358        assert!(matches!(
3359            err,
3360            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
3361                operation: "verification transform",
3362                ..
3363            })
3364        ));
3365    }
3366
3367    #[test]
3368    fn verify_context_applies_transform_allowlist_to_signed_info_c14n() {
3369        // Reference C14N remains allowlisted; only the distinct SignedInfo
3370        // canonicalization method should trigger this policy rejection.
3371        let xml = signature_with_target_reference("AQ==").replacen(
3372            "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
3373            "<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"/>",
3374            1,
3375        );
3376        let error = VerifyContext::new()
3377            .key(&AcceptingKey)
3378            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
3379            .verify(&xml)
3380            .expect_err("SignedInfo C14N must obey the operation transform allowlist");
3381
3382        assert!(matches!(
3383            error,
3384            SignatureVerificationPipelineError::Policy(
3385                crate::policy::PolicyViolation::Algorithm {
3386                    operation: "verification transform",
3387                    ref algorithm,
3388                }
3389            )
3390                if algorithm == "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
3391        ));
3392    }
3393
3394    #[test]
3395    fn verify_context_applies_transform_allowlist_to_key_retrieval() {
3396        // The reference and SignedInfo both use exclusive C14N. The only XPath
3397        // operation is document-selected key retrieval and must be rejected.
3398        let xml = signature_with_target_reference("AQ==")
3399            .replacen(
3400                "</ds:Signature>",
3401                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>"##,
3402                1,
3403            )
3404            .replacen(
3405                "</root>",
3406                r#"<holder ID="keys"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder></root>"#,
3407                1,
3408            );
3409        let error = VerifyContext::new()
3410            .key_resolver(&ConsumingKeyInfoResolver)
3411            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
3412            .verify(&xml)
3413            .expect_err("RetrievalMethod XPath must obey the operation transform allowlist");
3414
3415        assert!(matches!(
3416            error,
3417            SignatureVerificationPipelineError::Policy(
3418                crate::policy::PolicyViolation::Algorithm {
3419                    operation: "verification transform",
3420                    ref algorithm,
3421                }
3422            )
3423                if algorithm == XPATH_TRANSFORM_URI
3424        ));
3425    }
3426
3427    fn signature_with_manifest_xml(valid_manifest_digest: bool) -> String {
3428        signature_with_manifest_xml_with_manifest_mutation(valid_manifest_digest, |xml| xml)
3429    }
3430
3431    fn signature_with_manifest_xml_with_manifest_mutation<F>(
3432        valid_manifest_digest: bool,
3433        mutate_manifest: F,
3434    ) -> String
3435    where
3436        F: FnOnce(String) -> String,
3437    {
3438        const TMP_SIGNED_INFO_DIGEST: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
3439        const INVALID_MANIFEST_DIGEST: &str = "//////////////////////////8=";
3440        let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3441  <target ID="target">payload</target>
3442  <ds:Signature>
3443    <ds:SignedInfo>
3444      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3445      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3446      <ds:Reference URI="#manifest">
3447        <ds:Transforms>
3448          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3449        </ds:Transforms>
3450        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3451        <ds:DigestValue>SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER</ds:DigestValue>
3452      </ds:Reference>
3453    </ds:SignedInfo>
3454    <ds:SignatureValue>AQ==</ds:SignatureValue>
3455    <ds:Object>
3456      <ds:Manifest ID="manifest">
3457        <ds:Reference URI="#target">
3458          <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3459          <ds:DigestValue>MANIFEST_DIGEST_PLACEHOLDER</ds:DigestValue>
3460        </ds:Reference>
3461      </ds:Manifest>
3462    </ds:Object>
3463  </ds:Signature>
3464</root>"##;
3465        let seed_xml = xml_template.replace(
3466            "SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER",
3467            TMP_SIGNED_INFO_DIGEST,
3468        );
3469        let doc = Document::parse(&seed_xml).unwrap();
3470        let signature_node = doc
3471            .descendants()
3472            .find(|node| {
3473                node.is_element()
3474                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
3475                    && node.tag_name().name() == "Signature"
3476            })
3477            .unwrap();
3478        let resolver = UriReferenceResolver::new(&doc);
3479        let initial_data = resolver.dereference("#target").unwrap();
3480        let manifest_pre_digest =
3481            crate::xmldsig::execute_transforms(signature_node, initial_data, &[]).unwrap();
3482        let computed_manifest_digest_b64 = base64::engine::general_purpose::STANDARD
3483            .encode(compute_digest(DigestAlgorithm::Sha1, &manifest_pre_digest));
3484        let final_manifest_digest_b64 = if valid_manifest_digest {
3485            computed_manifest_digest_b64.as_str()
3486        } else {
3487            INVALID_MANIFEST_DIGEST
3488        };
3489        let xml_with_manifest_digest = mutate_manifest(
3490            seed_xml.replace("MANIFEST_DIGEST_PLACEHOLDER", final_manifest_digest_b64),
3491        );
3492        let signed_doc = Document::parse(&xml_with_manifest_digest).unwrap();
3493        let signed_signature_node = signed_doc
3494            .descendants()
3495            .find(|node| {
3496                node.is_element()
3497                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
3498                    && node.tag_name().name() == "Signature"
3499            })
3500            .unwrap();
3501        let signed_info_node = signed_signature_node
3502            .children()
3503            .find(|node| {
3504                node.is_element()
3505                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
3506                    && node.tag_name().name() == "SignedInfo"
3507            })
3508            .unwrap();
3509        let signed_info = parse_signed_info(signed_info_node).unwrap();
3510        let object_reference = &signed_info.references[0];
3511        let signed_resolver = UriReferenceResolver::new(&signed_doc);
3512        let signed_initial_data = signed_resolver
3513            .dereference(object_reference.uri.as_deref().unwrap())
3514            .unwrap();
3515        let signed_pre_digest = crate::xmldsig::execute_transforms(
3516            signed_signature_node,
3517            signed_initial_data,
3518            &object_reference.transforms,
3519        )
3520        .unwrap();
3521        let signed_digest_b64 = base64::engine::general_purpose::STANDARD.encode(compute_digest(
3522            object_reference.digest_method,
3523            &signed_pre_digest,
3524        ));
3525
3526        xml_with_manifest_digest.replacen(TMP_SIGNED_INFO_DIGEST, &signed_digest_b64, 1)
3527    }
3528
3529    fn replace_fixture_manifest_digest(xml: &str, replacement: &str) -> String {
3530        let object_marker = "<ds:Object>";
3531        let object_start = xml
3532            .find(object_marker)
3533            .expect("fixture should contain ds:Object")
3534            + object_marker.len();
3535        let open = "<ds:DigestValue>";
3536        let close = "</ds:DigestValue>";
3537        let value_start = xml[object_start..]
3538            .find(open)
3539            .map(|offset| object_start + offset + open.len())
3540            .expect("Manifest should contain DigestValue");
3541        let value_end = xml[value_start..]
3542            .find(close)
3543            .map(|offset| value_start + offset)
3544            .expect("Manifest DigestValue must be closed");
3545
3546        format!("{}{replacement}{}", &xml[..value_start], &xml[value_end..])
3547    }
3548
3549    #[test]
3550    fn verify_context_processes_manifest_references_when_enabled() {
3551        let xml = signature_with_manifest_xml(true);
3552
3553        let result_without_manifests = VerifyContext::new()
3554            .key(&RejectingKey)
3555            .verify(&xml)
3556            .expect("manifest processing disabled should still verify SignedInfo");
3557        assert!(
3558            result_without_manifests.manifest_references.is_empty(),
3559            "manifest results must stay empty when manifest processing is disabled",
3560        );
3561        assert!(matches!(
3562            result_without_manifests.status,
3563            DsigStatus::Invalid(FailureReason::SignatureMismatch)
3564        ));
3565
3566        let malformed_manifest_xml = signature_with_manifest_xml(true).replacen(
3567            "</ds:Object>",
3568            "</ds:Object><ds:Object><ds:Manifest><ds:Foo/></ds:Manifest></ds:Object>",
3569            1,
3570        );
3571        let malformed_with_manifests_disabled = VerifyContext::new()
3572            .key(&RejectingKey)
3573            .verify(&malformed_manifest_xml)
3574            .expect("malformed Manifest must be ignored when manifest processing is disabled");
3575        assert!(
3576            malformed_with_manifests_disabled
3577                .manifest_references
3578                .is_empty(),
3579            "manifest parser must not run when process_manifests is disabled",
3580        );
3581        assert!(matches!(
3582            malformed_with_manifests_disabled.status,
3583            DsigStatus::Invalid(FailureReason::SignatureMismatch)
3584        ));
3585
3586        let result_with_manifests = VerifyContext::new()
3587            .key(&AcceptingKey)
3588            .process_manifests(true)
3589            .verify(&xml)
3590            .expect("manifest references should be processed when enabled");
3591        assert_eq!(result_with_manifests.manifest_references.len(), 1);
3592        assert_eq!(
3593            result_with_manifests.manifest_references[0].reference_set,
3594            ReferenceSet::Manifest
3595        );
3596        assert_eq!(
3597            result_with_manifests.manifest_references[0].reference_index,
3598            0
3599        );
3600        assert!(matches!(
3601            result_with_manifests.manifest_references[0].status,
3602            DsigStatus::Valid
3603        ));
3604        assert!(matches!(result_with_manifests.status, DsigStatus::Valid));
3605    }
3606
3607    #[test]
3608    fn verify_context_skips_manifest_work_when_signature_value_is_invalid() {
3609        // SignedInfo authenticates the Manifest bytes only after SignatureValue
3610        // succeeds. Malformed nested content must not consume parsing work when
3611        // the cryptographic signature itself is invalid.
3612        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3613            replace_fixture_manifest_digest(&xml, "!!!")
3614        });
3615        assert!(
3616            xml.split_once("<ds:Object>")
3617                .is_some_and(|(_, object)| object.contains("<ds:DigestValue>!!!</ds:DigestValue>")),
3618            "fixture mutation must corrupt the nested Manifest DigestValue",
3619        );
3620
3621        let result = VerifyContext::new()
3622            .key(&RejectingKey)
3623            .process_manifests(true)
3624            .verify(&xml)
3625            .expect("invalid SignatureValue must short-circuit Manifest parsing");
3626
3627        assert!(matches!(
3628            result.status,
3629            DsigStatus::Invalid(FailureReason::SignatureMismatch)
3630        ));
3631        assert!(result.manifest_references.is_empty());
3632    }
3633
3634    #[test]
3635    fn verify_context_shares_xpath_parse_budget_with_manifest_references() {
3636        // SignedInfo and every Manifest form one attacker-controlled parse unit:
3637        // splitting expressions across Reference sets must not reset the ceiling.
3638        let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
3639            .repeat(64);
3640        let transform = format!(
3641            r#"<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</ds:Transform>"#
3642        );
3643        let max_transforms = transform.repeat(16);
3644        let max_manifest_reference = format!(
3645            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>"##
3646        );
3647        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3648            xml.replacen(
3649                r##"<ds:Reference URI="#target">"##,
3650                &format!(
3651                    r##"<ds:Reference URI="#target"><ds:Transforms>{}</ds:Transforms>"##,
3652                    max_transforms
3653                ),
3654                1,
3655            )
3656            .replacen(
3657                "</ds:SignedInfo>",
3658                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>"##,
3659                1,
3660            )
3661            .replacen(
3662                "</ds:Manifest>",
3663                &format!("{}</ds:Manifest>", max_manifest_reference.repeat(3)),
3664                1,
3665            )
3666        });
3667
3668        let error = VerifyContext::new()
3669            .key(&AcceptingKey)
3670            .process_manifests(true)
3671            .verify(&xml)
3672            .expect_err("SignedInfo and Manifest References must share one XPath parse budget");
3673
3674        assert!(
3675            matches!(
3676                &error,
3677                SignatureVerificationPipelineError::Policy(
3678                    crate::policy::PolicyViolation::ResourceLimit {
3679                        resource: "XPath expressions",
3680                        ..
3681                    }
3682                )
3683            ),
3684            "unexpected error: {error:?}"
3685        );
3686    }
3687
3688    #[test]
3689    fn verify_context_processes_manifest_when_signedinfo_references_object() {
3690        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3691            xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
3692                .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
3693                .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
3694        });
3695
3696        let result = VerifyContext::new()
3697            .key(&AcceptingKey)
3698            .process_manifests(true)
3699            .verify(&xml)
3700            .expect("manifest references should be processed when SignedInfo references ds:Object");
3701        assert_eq!(
3702            result.manifest_references.len(),
3703            1,
3704            "signed ds:Object should enable processing of its direct-child ds:Manifest",
3705        );
3706        assert_eq!(
3707            result.manifest_references[0].reference_set,
3708            ReferenceSet::Manifest
3709        );
3710        assert_eq!(result.manifest_references[0].reference_index, 0);
3711        assert!(matches!(
3712            result.manifest_references[0].status,
3713            DsigStatus::Valid
3714        ));
3715    }
3716
3717    #[test]
3718    fn verify_context_skips_manifest_removed_by_enveloped_transform() {
3719        // The owning Signature contains both eligible ID targets. Subtracting
3720        // its subtree therefore removes every target node from the digest input,
3721        // so neither form authenticates the Manifest structure for processing.
3722        for target_object in [false, true] {
3723            let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3724                let xml = xml.replacen(
3725                    r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
3726                    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#"/>"#,
3727                    1,
3728                );
3729                if target_object {
3730                    xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
3731                        .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
3732                        .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
3733                } else {
3734                    xml
3735                }
3736            });
3737
3738            let result = VerifyContext::new()
3739                .key(&AcceptingKey)
3740                .process_manifests(true)
3741                .store_pre_digest(true)
3742                .verify(&xml)
3743                .expect("an emptied reference remains a valid core digest input");
3744
3745            assert!(matches!(result.status, DsigStatus::Valid));
3746            assert_eq!(
3747                result.signed_info_references[0].pre_digest_data.as_deref(),
3748                Some([].as_slice()),
3749                "target_object={target_object} must have empty transformed bytes",
3750            );
3751            assert!(
3752                result.manifest_references.is_empty(),
3753                "target_object={target_object} must not authenticate the Manifest",
3754            );
3755        }
3756    }
3757
3758    #[test]
3759    fn verify_context_ignores_manifest_excluded_from_signed_object() {
3760        // A Reference URI authenticates only its post-transform bytes. Excluding
3761        // the Manifest subtree must not let its independently valid digest chain
3762        // masquerade as data authenticated by SignedInfo.
3763        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3764            xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
3765                .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
3766                .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
3767                .replacen(
3768                    r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
3769                    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#"/>"#,
3770                    1,
3771                )
3772        });
3773
3774        let result = VerifyContext::new()
3775            .key(&AcceptingKey)
3776            .process_manifests(true)
3777            .verify(&xml)
3778            .expect("excluded Manifest content must be ignored, not parsed");
3779
3780        assert!(matches!(result.status, DsigStatus::Valid));
3781        assert!(
3782            result.manifest_references.is_empty(),
3783            "a transform-excluded Manifest is not authenticated by SignedInfo",
3784        );
3785    }
3786
3787    #[test]
3788    fn verify_context_skips_manifest_digest_work_when_signature_is_invalid() {
3789        let xml = signature_with_manifest_xml(false);
3790        let result = VerifyContext::new()
3791            .key(&RejectingKey)
3792            .process_manifests(true)
3793            .verify(&xml)
3794            .expect("invalid SignatureValue must short-circuit Manifest digest work");
3795        assert!(result.manifest_references.is_empty());
3796        assert!(matches!(
3797            result.status,
3798            DsigStatus::Invalid(FailureReason::SignatureMismatch)
3799        ));
3800    }
3801
3802    #[test]
3803    fn verify_context_manifest_digest_mismatch_is_non_fatal_with_accepting_key() {
3804        let xml = signature_with_manifest_xml(false);
3805        let result = VerifyContext::new()
3806            .key(&AcceptingKey)
3807            .process_manifests(true)
3808            .verify(&xml)
3809            .expect("manifest digest mismatches should be recorded while signature stays valid");
3810        assert_eq!(result.manifest_references.len(), 1);
3811        assert!(matches!(
3812            result.manifest_references[0].status,
3813            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
3814        ));
3815        assert!(matches!(result.status, DsigStatus::Valid));
3816    }
3817
3818    #[test]
3819    fn verify_context_skips_manifest_parsing_when_signedinfo_reference_fails() {
3820        // Manifest content is not authenticated after a SignedInfo reference
3821        // failure, so parsing it would spend work on untrusted nested input.
3822        let xml = signature_with_manifest_xml(true);
3823        let (signed_info_prefix, object_suffix) = xml
3824            .split_once("<ds:Object>")
3825            .expect("fixture should contain ds:Object");
3826        let open = "<ds:DigestValue>";
3827        let close = "</ds:DigestValue>";
3828        let digest_start = signed_info_prefix
3829            .find(open)
3830            .expect("SignedInfo should contain DigestValue");
3831        let digest_end = signed_info_prefix[digest_start + open.len()..]
3832            .find(close)
3833            .map(|offset| digest_start + open.len() + offset)
3834            .expect("SignedInfo DigestValue must be closed");
3835        let broken_signed_info_prefix = format!(
3836            "{}{}AAAAAAAAAAAAAAAAAAAAAAAAAAA={}{}",
3837            &signed_info_prefix[..digest_start],
3838            open,
3839            close,
3840            &signed_info_prefix[digest_end + close.len()..],
3841        );
3842        let broken_xml = format!("{broken_signed_info_prefix}<ds:Object>{object_suffix}");
3843        let result = VerifyContext::new()
3844            .key(&RejectingKey)
3845            .process_manifests(true)
3846            .verify(&broken_xml)
3847            .expect("SignedInfo digest failure should return without parsing Manifests");
3848        assert!(matches!(
3849            result.status,
3850            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
3851        ));
3852        assert!(
3853            result.manifest_references.is_empty(),
3854            "unauthenticated Manifest content must not be parsed",
3855        );
3856    }
3857
3858    #[test]
3859    fn verify_context_skips_manifest_policy_work_when_signature_is_invalid() {
3860        // A digest-valid SignedInfo reference does not authenticate Manifest
3861        // policy inputs until SignatureValue also succeeds.
3862        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3863            xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
3864        });
3865        let result = VerifyContext::new()
3866            .key(&RejectingKey)
3867            .process_manifests(true)
3868            .verify(&broken_xml)
3869            .expect("invalid SignatureValue must short-circuit Manifest policy work");
3870        assert!(result.manifest_references.is_empty());
3871        assert!(matches!(
3872            result.status,
3873            DsigStatus::Invalid(FailureReason::SignatureMismatch)
3874        ));
3875    }
3876
3877    #[test]
3878    fn verify_context_records_manifest_policy_violations_with_accepting_key() {
3879        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3880            xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
3881        });
3882        let result = VerifyContext::new()
3883            .key(&AcceptingKey)
3884            .process_manifests(true)
3885            .verify(&broken_xml)
3886            .expect("manifest policy violations should be recorded while signature stays valid");
3887        assert_eq!(result.manifest_references.len(), 1);
3888        assert!(matches!(
3889            result.manifest_references[0].status,
3890            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
3891        ));
3892        assert!(matches!(result.status, DsigStatus::Valid));
3893    }
3894
3895    #[test]
3896    fn verify_context_applies_digest_policy_to_manifest_references() {
3897        // Manifest results are authenticated extension data and must obey the
3898        // same digest allowlist as SignedInfo references.
3899        let policy = crate::policy::VerificationPolicy {
3900            manifest_processing: crate::policy::ManifestProcessing::Process,
3901            digest_algorithms: Some(HashSet::from([DigestAlgorithm::Sha1])),
3902            ..crate::policy::VerificationPolicy::default()
3903        };
3904        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
3905            let legacy = "http://www.w3.org/2000/09/xmldsig#sha1";
3906            let offset = xml
3907                .rfind(legacy)
3908                .expect("Manifest DigestMethod must be present");
3909            xml.replace_range(offset..offset + legacy.len(), DigestAlgorithm::Sha256.uri());
3910            let value_start = xml[offset..]
3911                .find("<ds:DigestValue>")
3912                .map(|relative| offset + relative + "<ds:DigestValue>".len())
3913                .expect("Manifest DigestValue must be present");
3914            let value_end = xml[value_start..]
3915                .find("</ds:DigestValue>")
3916                .map(|relative| value_start + relative)
3917                .expect("Manifest DigestValue must be closed");
3918            xml.replace_range(
3919                value_start..value_end,
3920                &base64::engine::general_purpose::STANDARD.encode([0_u8; 32]),
3921            );
3922            xml
3923        });
3924        let result = VerifyContext::new()
3925            .key(&AcceptingKey)
3926            .policy(policy)
3927            .verify(&xml)
3928            .expect("a disallowed Manifest digest is a per-reference result");
3929
3930        assert!(matches!(result.status, DsigStatus::Valid));
3931        assert!(matches!(
3932            result.manifest_references[0].status,
3933            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
3934        ));
3935    }
3936
3937    #[test]
3938    fn verify_context_applies_transform_count_policy_to_manifest_references() {
3939        // Authenticated Manifest references share the caller's per-reference
3940        // transform ceiling and fail before transform execution when exceeded.
3941        let policy = crate::policy::VerificationPolicy {
3942            manifest_processing: crate::policy::ManifestProcessing::Process,
3943            resources: crate::policy::ResourcePolicy {
3944                max_transforms_per_reference: 1,
3945                ..crate::policy::ResourcePolicy::default()
3946            },
3947            ..crate::policy::VerificationPolicy::default()
3948        };
3949        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |mut xml| {
3950            let manifest_start = xml
3951                .find("<ds:Manifest")
3952                .expect("fixture must contain a Manifest");
3953            let manifest = xml[manifest_start..].replacen(
3954                "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
3955                concat!(
3956                    "<ds:Transforms>",
3957                    "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
3958                    "<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>",
3959                    "</ds:Transforms>",
3960                    "<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>"
3961                ),
3962                1,
3963            );
3964            xml.replace_range(manifest_start.., &manifest);
3965            xml
3966        });
3967        let result = VerifyContext::new()
3968            .key(&AcceptingKey)
3969            .policy(policy)
3970            .verify(&xml)
3971            .expect("Manifest transform policy is a per-reference result");
3972
3973        assert!(matches!(result.status, DsigStatus::Valid));
3974        assert!(matches!(
3975            result.manifest_references[0].status,
3976            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
3977        ));
3978    }
3979
3980    #[test]
3981    fn verify_context_skips_manifest_uri_work_when_signature_is_invalid() {
3982        // Missing Manifest URIs remain unauthenticated until SignatureValue
3983        // succeeds, so they cannot trigger Manifest policy processing here.
3984        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
3985            xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
3986        });
3987
3988        let result = VerifyContext::new()
3989            .key(&RejectingKey)
3990            .process_manifests(true)
3991            .verify(&broken_xml)
3992            .expect("invalid SignatureValue must short-circuit Manifest URI processing");
3993        assert!(result.manifest_references.is_empty());
3994        assert!(matches!(
3995            result.status,
3996            DsigStatus::Invalid(FailureReason::SignatureMismatch)
3997        ));
3998    }
3999
4000    #[test]
4001    fn verify_context_records_manifest_missing_uri_with_accepting_key() {
4002        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4003            xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
4004        });
4005
4006        let result = VerifyContext::new()
4007            .key(&AcceptingKey)
4008            .process_manifests(true)
4009            .verify(&broken_xml)
4010            .expect("manifest missing URI should be recorded while signature stays valid");
4011        assert_eq!(result.manifest_references.len(), 1);
4012        assert_eq!(result.manifest_references[0].uri, "<omitted>");
4013        assert!(matches!(
4014            result.manifest_references[0].status,
4015            DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
4016        ));
4017        assert!(matches!(result.status, DsigStatus::Valid));
4018    }
4019
4020    #[test]
4021    fn verify_context_ignores_nested_manifests_in_object() {
4022        // A digest-valid Manifest below a wrapper is outside the strict direct-
4023        // child processing profile and must not appear in diagnostics.
4024        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4025            xml.replacen(
4026                "<ds:Manifest ID=\"manifest\">",
4027                "<wrapper><ds:Manifest ID=\"manifest\">",
4028                1,
4029            )
4030            .replacen("</ds:Manifest>", "</ds:Manifest></wrapper>", 1)
4031        });
4032
4033        let result = VerifyContext::new()
4034            .key(&AcceptingKey)
4035            .process_manifests(true)
4036            .verify(&xml)
4037            .expect("nested Manifest nodes are ignored in strict mode");
4038        assert!(
4039            result.manifest_references.is_empty(),
4040            "only direct ds:Manifest children of ds:Object must be processed"
4041        );
4042        assert!(matches!(result.status, DsigStatus::Valid));
4043    }
4044
4045    #[test]
4046    fn verify_context_reports_manifest_reference_parse_errors_explicitly() {
4047        // Malformed nested DigestValue is parsed only after the enclosing
4048        // Manifest structure has been authenticated by SignedInfo.
4049        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4050            replace_fixture_manifest_digest(&xml, "!!!")
4051        });
4052
4053        let err = VerifyContext::new()
4054            .key(&AcceptingKey)
4055            .process_manifests(true)
4056            .verify(&broken_xml)
4057            .expect_err("invalid Manifest DigestValue must map to ParseManifestReference");
4058        assert!(matches!(
4059            err,
4060            SignatureVerificationPipelineError::ParseManifestReference(_)
4061        ));
4062    }
4063
4064    #[test]
4065    fn verify_context_reports_unsupported_manifest_transform_with_declared_digest() {
4066        // Unsupported optional Manifest transforms do not invalidate core
4067        // SignedInfo, but their result must preserve the declared digest method.
4068        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4069            let xml = xml.replacen(
4070                "<ds:Reference URI=\"#target\">",
4071                "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
4072                1,
4073            );
4074            let xml = xml.replacen(
4075                "</ds:Transforms>\n          <ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>",
4076                "</ds:Transforms>\n          <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>",
4077                1,
4078            );
4079            replace_fixture_manifest_digest(&xml, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
4080        });
4081        assert!(xml.contains("urn:unsupported"));
4082        assert!(xml.contains("http://www.w3.org/2001/04/xmlenc#sha256"));
4083
4084        let result = VerifyContext::new()
4085            .key(&AcceptingKey)
4086            .process_manifests(true)
4087            .verify(&xml)
4088            .expect("unsupported Manifest transform is a per-reference result");
4089        assert_eq!(result.status, DsigStatus::Valid);
4090        assert_eq!(result.manifest_references.len(), 1);
4091        assert_eq!(
4092            result.manifest_references[0].digest_algorithm,
4093            DigestAlgorithm::Sha256
4094        );
4095        assert!(matches!(
4096            result.manifest_references[0].status,
4097            DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
4098        ));
4099
4100        let restricted = VerifyContext::new()
4101            .key(&AcceptingKey)
4102            .process_manifests(true)
4103            .allowed_transforms([
4104                DEFAULT_IMPLICIT_C14N_URI,
4105                "http://www.w3.org/2001/10/xml-exc-c14n#",
4106            ])
4107            .verify(&xml)
4108            .expect("a disallowed Manifest transform is a per-reference policy result");
4109        assert!(matches!(
4110            restricted.manifest_references[0].status,
4111            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
4112        ));
4113    }
4114
4115    #[test]
4116    fn manifest_reference_limit_counts_unsupported_entries() {
4117        let references = (0..=MAX_REFERENCES_PER_SIGNATURE)
4118            .map(|index| {
4119                format!(
4120                    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>"##
4121                )
4122            })
4123            .collect::<String>();
4124        let xml = format!(
4125            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>"#
4126        );
4127        let document = Document::parse(&xml).unwrap();
4128        let signature = document.root_element();
4129        let object = signature.children().find(|node| node.is_element()).unwrap();
4130        let authenticated = HashSet::from([object.id()]);
4131        let mut processed = HashSet::new();
4132        let mut remaining = MAX_REFERENCES_PER_SIGNATURE;
4133        let mut next_index = 0;
4134
4135        let error = match parse_manifest_references(
4136            signature,
4137            &authenticated,
4138            &mut processed,
4139            &mut remaining,
4140            &mut next_index,
4141            &mut XPathSignatureParseBudget::default(),
4142            None,
4143        ) {
4144            Ok(_) => panic!("unsupported references must consume the same aggregate limit"),
4145            Err(error) => error,
4146        };
4147        assert!(matches!(
4148            error,
4149            SignatureVerificationPipelineError::InvalidStructure {
4150                reason: "signed Manifests exceed the per-signature Reference limit"
4151            }
4152        ));
4153    }
4154
4155    #[test]
4156    fn unsigned_manifest_remains_eligible_after_trust_expands() {
4157        // The second Object is not authenticated during the first discovery
4158        // pass. It must remain unprocessed so a valid reference from the first
4159        // Manifest can make its sibling Manifest eligible on the next pass.
4160        let digest = base64::engine::general_purpose::STANDARD.encode([0_u8; 32]);
4161        let xml = format!(
4162            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>"##
4163        );
4164        let document = Document::parse(&xml).expect("nested Manifest fixture must parse");
4165        let signature = document.root_element();
4166        let mut objects = signature.children().filter(|node| node.is_element());
4167        let outer = objects.next().expect("outer Object");
4168        let inner = objects.next().expect("inner Object");
4169        let mut authenticated = HashSet::from([outer.id()]);
4170        let mut processed = HashSet::new();
4171        let mut remaining = 2;
4172        let mut next_index = 0;
4173        let mut xpath_budget = XPathSignatureParseBudget::default();
4174
4175        let first = parse_manifest_references(
4176            signature,
4177            &authenticated,
4178            &mut processed,
4179            &mut remaining,
4180            &mut next_index,
4181            &mut xpath_budget,
4182            None,
4183        )
4184        .expect("outer Manifest must be discovered");
4185        assert_eq!(first.references.len(), 1);
4186        assert_eq!(first.references[0].1.uri.as_deref(), Some("#inner"));
4187
4188        authenticated.insert(inner.id());
4189        let second = parse_manifest_references(
4190            signature,
4191            &authenticated,
4192            &mut processed,
4193            &mut remaining,
4194            &mut next_index,
4195            &mut xpath_budget,
4196            None,
4197        )
4198        .expect("newly authenticated sibling Manifest must remain eligible");
4199        assert_eq!(second.references.len(), 1);
4200        assert_eq!(second.references[0].1.uri.as_deref(), Some("#payload"));
4201    }
4202
4203    #[test]
4204    fn manifest_reference_limit_includes_signed_info_references() {
4205        // The per-signature ceiling is shared by core and authenticated
4206        // Manifest references; enabling Manifest processing must not reset it.
4207        let xml = signature_with_manifest_xml(true);
4208        let reference_start = xml
4209            .find(r##"<ds:Reference URI="#manifest">"##)
4210            .expect("fixture SignedInfo must reference the Manifest");
4211        let reference_end = xml[reference_start..]
4212            .find("</ds:Reference>")
4213            .map(|offset| reference_start + offset + "</ds:Reference>".len())
4214            .expect("fixture SignedInfo Reference must be closed");
4215        let repeated = xml[reference_start..reference_end].repeat(MAX_REFERENCES_PER_SIGNATURE);
4216        let xml = format!(
4217            "{}{repeated}{}",
4218            &xml[..reference_start],
4219            &xml[reference_end..]
4220        );
4221
4222        let error = VerifyContext::new()
4223            .key(&AcceptingKey)
4224            .process_manifests(true)
4225            .verify(&xml)
4226            .expect_err("one Manifest Reference must exceed the exhausted signature-wide limit");
4227
4228        assert!(matches!(
4229            error,
4230            SignatureVerificationPipelineError::InvalidStructure {
4231                reason: "signed Manifests exceed the per-signature Reference limit"
4232            }
4233        ));
4234    }
4235
4236    #[test]
4237    fn configured_reference_limit_is_shared_with_manifests() {
4238        // Lowering the operation policy must lower the aggregate SignedInfo and
4239        // Manifest capacity rather than falling back to the crate hard limit.
4240        let policy = crate::policy::VerificationPolicy {
4241            manifest_processing: crate::policy::ManifestProcessing::Process,
4242            resources: crate::policy::ResourcePolicy {
4243                max_references: 1,
4244                ..crate::policy::ResourcePolicy::default()
4245            },
4246            ..crate::policy::VerificationPolicy::default()
4247        };
4248
4249        let error = VerifyContext::new()
4250            .key(&AcceptingKey)
4251            .policy(policy)
4252            .verify(&signature_with_manifest_xml(true))
4253            .expect_err("Manifest must exceed the caller-selected aggregate limit");
4254        assert!(matches!(
4255            error,
4256            SignatureVerificationPipelineError::InvalidStructure {
4257                reason: "signed Manifests exceed the per-signature Reference limit"
4258            }
4259        ));
4260    }
4261
4262    #[test]
4263    fn retrieval_method_materializes_single_x509_data_subtree() {
4264        for uri in [
4265            "#target",
4266            "#xpointer(id('target'))",
4267            "#xpointer(id(&quot;target&quot;))",
4268        ] {
4269            for target_xml in [
4270                r#"<ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>"#,
4271                r#"<holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>"#,
4272            ] {
4273                let xml = format!(
4274                    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>"#
4275                );
4276                let document = Document::parse(&xml).unwrap();
4277                let key_info_node = document
4278                    .descendants()
4279                    .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
4280                    .unwrap();
4281                let mut key_info = parse_key_info(key_info_node).unwrap();
4282                let resolver = UriReferenceResolver::new(&document);
4283
4284                materialize_retrieval_methods(
4285                    &mut key_info,
4286                    &resolver,
4287                    UriTypeSet::SAME_DOCUMENT,
4288                    None,
4289                    crate::provider::default_provider(),
4290                )
4291                .expect("XPath filter must produce one X509Data-rooted node-set");
4292                assert!(matches!(
4293                    key_info.sources.as_slice(),
4294                    [super::super::parse::KeyInfoSource::X509Data(info)]
4295                        if info.subject_names == ["CN=leaf"]
4296                ));
4297            }
4298        }
4299    }
4300
4301    fn retrieval_method_xpath_signature() -> String {
4302        format!(
4303            r##"<root xmlns:ds="{XMLDSIG_NS}">
4304              <payload Id="payload">ok</payload>
4305              <ds:Signature>
4306                <ds:SignedInfo>
4307                  <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4308                  <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4309                  <ds:Reference URI="#payload">
4310                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4311                    <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
4312                  </ds:Reference>
4313                </ds:SignedInfo>
4314                <ds:SignatureValue>AQ==</ds:SignatureValue>
4315                <ds:KeyInfo>
4316                  <ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data">
4317                    <ds:Transforms><ds:Transform Algorithm="{XPATH_TRANSFORM_URI}">
4318                      <ds:XPath>ancestor-or-self::ds:X509Data</ds:XPath>
4319                    </ds:Transform></ds:Transforms>
4320                  </ds:RetrievalMethod>
4321                </ds:KeyInfo>
4322              </ds:Signature>
4323              <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
4324            </root>"##
4325        )
4326    }
4327
4328    #[test]
4329    fn retrieval_method_xpath_uses_signature_expression_budget() {
4330        // RetrievalMethod XPath belongs to the same untrusted Signature as
4331        // Reference XPath and must not receive a separate parse allowance.
4332        let mut policy = crate::policy::VerificationPolicy::default();
4333        policy.resources.max_xpath_expressions = 0;
4334
4335        let error = VerifyContext::new()
4336            .policy(policy)
4337            .verify(&retrieval_method_xpath_signature())
4338            .expect_err("RetrievalMethod XPath must consume the signature parse budget");
4339
4340        assert!(
4341            matches!(
4342                &error,
4343                SignatureVerificationPipelineError::Policy(
4344                    crate::policy::PolicyViolation::ResourceLimit {
4345                        resource: "XPath expressions",
4346                        maximum: 0,
4347                        actual: 1,
4348                    }
4349                )
4350            ),
4351            "unexpected error: {error:?}"
4352        );
4353    }
4354
4355    #[test]
4356    fn retrieval_method_xpath_obeys_expression_byte_limit() {
4357        // Edge whitespace is semantically harmless for this restricted shape,
4358        // but its raw untrusted bytes still belong to the operation budget.
4359        let expression = "ancestor-or-self::ds:X509Data";
4360        let padded_expression = format!("  {expression}  ");
4361        let xml = retrieval_method_xpath_signature().replace(expression, &padded_expression);
4362        let mut policy = crate::policy::VerificationPolicy::default();
4363        policy.resources.max_xpath_expression_bytes = expression.len();
4364
4365        let error = VerifyContext::new()
4366            .policy(policy)
4367            .verify(&xml)
4368            .expect_err("RetrievalMethod XPath must obey the expression byte limit");
4369
4370        assert!(
4371            matches!(
4372                &error,
4373                SignatureVerificationPipelineError::Policy(
4374                    crate::policy::PolicyViolation::ResourceLimit {
4375                        resource: "XPath expression bytes",
4376                        maximum,
4377                        actual,
4378                    }
4379                ) if *maximum == expression.len() && *actual == padded_expression.len()
4380            ),
4381            "unexpected error: {error:?}"
4382        );
4383    }
4384
4385    #[test]
4386    fn retrieval_method_xpath_obeys_expression_complexity_limit() {
4387        // Recognizing a fixed safe predicate must not bypass the common XPath
4388        // complexity policy applied to every expression in the signature.
4389        let mut policy = crate::policy::VerificationPolicy::default();
4390        policy.resources.max_xpath_expression_complexity = 0;
4391
4392        let error = VerifyContext::new()
4393            .policy(policy)
4394            .verify(&retrieval_method_xpath_signature())
4395            .expect_err("RetrievalMethod XPath must obey the complexity limit");
4396
4397        assert!(
4398            matches!(
4399                &error,
4400                SignatureVerificationPipelineError::Policy(
4401                    crate::policy::PolicyViolation::ResourceLimit {
4402                        resource: "XPath expression complexity",
4403                        maximum: 0,
4404                        actual,
4405                    }
4406                ) if *actual > 0
4407            ),
4408            "unexpected error: {error:?}"
4409        );
4410    }
4411
4412    #[test]
4413    fn retrieval_method_xpath_uses_node_filter_work_budget() {
4414        // Attribute and namespace XPath nodes participate in filtering even
4415        // though the optimized X509Data locator scans only tree descendants.
4416        let mut policy = crate::policy::VerificationPolicy::default();
4417        policy.resources.max_node_set_filter_work = 4;
4418        let xml = retrieval_method_xpath_signature().replace(
4419            "<holder Id=\"target\">",
4420            "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
4421        );
4422
4423        let error = VerifyContext::new()
4424            .policy(policy)
4425            .verify(&xml)
4426            .expect_err("RetrievalMethod XPath must consume node-filter work");
4427
4428        assert!(
4429            matches!(
4430                &error,
4431                SignatureVerificationPipelineError::Policy(
4432                    crate::policy::PolicyViolation::ResourceLimit {
4433                        resource: crate::policy::resource_name::NODE_SET_FILTER_WORK,
4434                        maximum: 4,
4435                        actual,
4436                    }
4437                ) if *actual > 4
4438            ),
4439            "unexpected error: {error:?}"
4440        );
4441    }
4442
4443    #[test]
4444    fn retrieval_method_xpath_charges_every_context_to_evaluation_work() {
4445        // The optimized predicate avoids a generic XPath engine, but every
4446        // attribute and namespace context still consumes evaluation work.
4447        let mut policy = crate::policy::VerificationPolicy::default();
4448        policy.resources.max_xpath_evaluation_work = 4;
4449        let xml = retrieval_method_xpath_signature().replace(
4450            "<holder Id=\"target\">",
4451            "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
4452        );
4453
4454        let error = VerifyContext::new()
4455            .policy(policy)
4456            .verify(&xml)
4457            .expect_err("RetrievalMethod XPath must charge every evaluation context");
4458
4459        assert!(matches!(
4460            error,
4461            SignatureVerificationPipelineError::Policy(
4462                crate::policy::PolicyViolation::ResourceLimit {
4463                    resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
4464                    maximum: 4,
4465                    actual,
4466                }
4467            ) if actual > 4
4468        ));
4469    }
4470
4471    #[test]
4472    fn retrieval_method_xpath_obeys_namespace_binding_limit() {
4473        // The specialized RetrievalMethod path must retain the XPath element's
4474        // in-scope namespaces and enforce the same limit as ordinary XPath.
4475        let mut policy = crate::policy::VerificationPolicy::default();
4476        policy.resources.max_xpath_namespace_bindings = 0;
4477
4478        let error = VerifyContext::new()
4479            .policy(policy)
4480            .verify(&retrieval_method_xpath_signature())
4481            .expect_err("RetrievalMethod XPath namespaces must obey the binding limit");
4482
4483        assert!(matches!(
4484            error,
4485            SignatureVerificationPipelineError::Policy(
4486                crate::policy::PolicyViolation::ResourceLimit {
4487                    resource: crate::policy::resource_name::XPATH_NAMESPACE_BINDINGS,
4488                    maximum: 0,
4489                    actual,
4490                }
4491            ) if actual > 0
4492        ));
4493    }
4494
4495    #[test]
4496    fn retrieval_method_xpath_obeys_namespace_byte_limit() {
4497        // Prefix and URI bytes retained from the XPath namespace axis consume
4498        // the same per-expression byte budget as an ordinary XPath transform.
4499        let mut policy = crate::policy::VerificationPolicy::default();
4500        policy.resources.max_xpath_namespace_bytes = 0;
4501
4502        let error = VerifyContext::new()
4503            .policy(policy)
4504            .verify(&retrieval_method_xpath_signature())
4505            .expect_err("RetrievalMethod XPath namespaces must obey the byte limit");
4506
4507        assert!(matches!(
4508            error,
4509            SignatureVerificationPipelineError::Policy(
4510                crate::policy::PolicyViolation::ResourceLimit {
4511                    resource: crate::policy::resource_name::XPATH_NAMESPACE_BYTES,
4512                    maximum: 0,
4513                    actual,
4514                }
4515            ) if actual > 0
4516        ));
4517    }
4518
4519    #[test]
4520    fn retrieval_method_xpath_obeys_context_evaluation_limit() {
4521        // A bare fragment includes attribute and namespace XPath nodes in
4522        // addition to the four tree nodes below. Tree-only accounting would
4523        // incorrectly admit this input at the configured ceiling.
4524        let mut policy = crate::policy::VerificationPolicy::default();
4525        policy.resources.max_xpath_context_evaluations = 4;
4526        let xml = retrieval_method_xpath_signature().replace(
4527            "<holder Id=\"target\">",
4528            "<holder Id=\"target\" role=\"signing\" xmlns:metadata=\"urn:metadata\">",
4529        );
4530
4531        let error = VerifyContext::new()
4532            .policy(policy)
4533            .verify(&xml)
4534            .expect_err("RetrievalMethod XPath contexts must obey the evaluation limit");
4535
4536        assert!(matches!(
4537            error,
4538            SignatureVerificationPipelineError::Policy(
4539                crate::policy::PolicyViolation::ResourceLimit {
4540                    resource: crate::policy::resource_name::XPATH_CONTEXT_EVALUATIONS,
4541                    maximum: 4,
4542                    actual,
4543                }
4544            ) if actual > 4
4545        ));
4546    }
4547
4548    #[test]
4549    fn retrieval_method_materializes_direct_untransformed_x509_data() {
4550        // A typed RetrievalMethod may point directly at the XML structure it
4551        // identifies; no transform is needed when X509Data is the URI root.
4552        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4553          <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
4554          <ds:X509Data Id="target"><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data>
4555        </root>"##;
4556        let document = Document::parse(xml).unwrap();
4557        let key_info_node = document
4558            .descendants()
4559            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
4560            .unwrap();
4561        let mut key_info = parse_key_info(key_info_node).unwrap();
4562
4563        materialize_retrieval_methods(
4564            &mut key_info,
4565            &UriReferenceResolver::new(&document),
4566            UriTypeSet::SAME_DOCUMENT,
4567            None,
4568            crate::provider::default_provider(),
4569        )
4570        .expect("a direct X509Data target needs no transform");
4571        assert!(matches!(
4572            key_info.sources.as_slice(),
4573            [super::super::parse::KeyInfoSource::X509Data(info)]
4574                if info.subject_names == ["CN=leaf"]
4575        ));
4576    }
4577
4578    #[test]
4579    fn raw_x509_retrieval_method_uses_inherited_xml_base() {
4580        // RetrievalMethod URI is an attribute URI reference, so XML Base uses
4581        // the effective base of the element bearing that attribute.
4582        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
4583        let xml = format!(
4584            r#"<root xml:base="https://example.test/keys/nested/" xmlns:ds="{XMLDSIG_NS}">
4585                <ds:KeyInfo><ds:RetrievalMethod URI="../signer.der" Type="{RAW_X509_TYPE}"/></ds:KeyInfo>
4586            </root>"#
4587        );
4588        let document = Document::parse(&xml).unwrap();
4589        let key_info_node = document
4590            .descendants()
4591            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
4592            .unwrap();
4593        let mut key_info = parse_key_info(key_info_node).unwrap();
4594        let certificate = include_bytes!(
4595            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
4596        )
4597        .to_vec();
4598        let resources = HashMap::from([(
4599            "https://example.test/keys/signer.der".to_string(),
4600            certificate,
4601        )]);
4602        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4603
4604        materialize_retrieval_methods(
4605            &mut key_info,
4606            &resolver,
4607            UriTypeSet::ALL,
4608            None,
4609            crate::provider::default_provider(),
4610        )
4611        .expect("RetrievalMethod should resolve against inherited xml:base");
4612
4613        assert!(matches!(
4614            key_info.sources.as_slice(),
4615            [super::super::parse::KeyInfoSource::X509Data(info)]
4616                if info.certificates.len() == 1
4617        ));
4618    }
4619
4620    #[test]
4621    fn retrieval_method_requires_xpath_for_x509_data_below_uri_root() {
4622        // Without a transform the dereferenced holder, not its descendant,
4623        // is the result and therefore cannot masquerade as typed X509Data.
4624        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4625          <ds:KeyInfo><ds:RetrievalMethod URI="#target" Type="http://www.w3.org/2000/09/xmldsig#X509Data"/></ds:KeyInfo>
4626          <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
4627        </root>"##;
4628        let document = Document::parse(xml).unwrap();
4629        let key_info_node = document
4630            .descendants()
4631            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
4632            .unwrap();
4633        let mut key_info = parse_key_info(key_info_node).unwrap();
4634
4635        let error = materialize_retrieval_methods(
4636            &mut key_info,
4637            &UriReferenceResolver::new(&document),
4638            UriTypeSet::SAME_DOCUMENT,
4639            None,
4640            crate::provider::default_provider(),
4641        )
4642        .expect_err("a wrapper target requires an explicit selection transform");
4643        assert!(matches!(
4644            error,
4645            SignatureVerificationPipelineError::InvalidStructure {
4646                reason: "untransformed X509Data RetrievalMethod must target X509Data directly"
4647            }
4648        ));
4649    }
4650
4651    #[test]
4652    fn retrieval_method_rejects_target_inside_external_x509_data_ancestor() {
4653        // XPath filtering cannot add an ancestor that was outside the URI's
4654        // dereferenced node-set, so this result is not rooted at X509Data.
4655        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4656          <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>
4657          <ds:X509Data><ds:X509SubjectName Id="target">CN=leaf</ds:X509SubjectName></ds:X509Data>
4658        </root>"##;
4659        let document = Document::parse(xml).unwrap();
4660        let key_info_node = document
4661            .descendants()
4662            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
4663            .unwrap();
4664        let mut key_info = parse_key_info(key_info_node).unwrap();
4665
4666        let error = materialize_retrieval_methods(
4667            &mut key_info,
4668            &UriReferenceResolver::new(&document),
4669            UriTypeSet::SAME_DOCUMENT,
4670            None,
4671            crate::provider::default_provider(),
4672        )
4673        .expect_err("filter output without an X509Data root must be rejected");
4674        assert!(matches!(
4675            error,
4676            SignatureVerificationPipelineError::InvalidStructure {
4677                reason: "X509Data RetrievalMethod selected no X509Data element"
4678            }
4679        ));
4680    }
4681
4682    #[test]
4683    fn retrieval_method_rejects_ambiguous_x509_data_relation() {
4684        // A transformed result with multiple X509Data roots is not one KeyInfo child.
4685        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4686          <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>
4687          <holder Id="target"><ds:X509Data/><ds:X509Data/></holder>
4688        </root>"##;
4689        let document = Document::parse(xml).unwrap();
4690        let key_info_node = document
4691            .descendants()
4692            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
4693            .unwrap();
4694        let mut key_info = parse_key_info(key_info_node).unwrap();
4695
4696        let error = materialize_retrieval_methods(
4697            &mut key_info,
4698            &UriReferenceResolver::new(&document),
4699            UriTypeSet::SAME_DOCUMENT,
4700            None,
4701            crate::provider::default_provider(),
4702        )
4703        .expect_err("multiple transformed X509Data roots must be rejected");
4704        assert!(matches!(
4705            error,
4706            SignatureVerificationPipelineError::InvalidStructure {
4707                reason: "X509Data RetrievalMethod selected multiple X509Data elements"
4708            }
4709        ));
4710    }
4711
4712    #[test]
4713    fn retrieval_method_materialization_preserves_key_info_order() {
4714        // Replacing the source in place keeps a later fallback behind the
4715        // retrieved key material for first-match resolvers.
4716        let xml = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4717          <ds:KeyInfo>
4718            <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>
4719            <ds:KeyName>fallback</ds:KeyName>
4720          </ds:KeyInfo>
4721          <holder Id="target"><ds:X509Data><ds:X509SubjectName>CN=leaf</ds:X509SubjectName></ds:X509Data></holder>
4722        </root>"##;
4723        let document = Document::parse(xml).unwrap();
4724        let key_info_node = document
4725            .descendants()
4726            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
4727            .unwrap();
4728        let mut key_info = parse_key_info(key_info_node).unwrap();
4729
4730        materialize_retrieval_methods(
4731            &mut key_info,
4732            &UriReferenceResolver::new(&document),
4733            UriTypeSet::SAME_DOCUMENT,
4734            None,
4735            crate::provider::default_provider(),
4736        )
4737        .unwrap();
4738        assert!(matches!(
4739            key_info.sources.as_slice(),
4740            [
4741                super::super::parse::KeyInfoSource::X509Data(_),
4742                super::super::parse::KeyInfoSource::KeyName(name)
4743            ] if name == "fallback"
4744        ));
4745    }
4746
4747    #[test]
4748    fn retrieval_method_materialization_bounds_repeated_sources() {
4749        // Repeating one allowed certificate must not multiply parsing and clones
4750        // before SignatureValue validation.
4751        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
4752        let certificate = include_bytes!(
4753            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
4754        )
4755        .to_vec();
4756        let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
4757        let mut key_info = KeyInfo {
4758            sources: (0..=64)
4759                .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
4760                    uri: "urn:certificate".into(),
4761                    resource_type: Some(RAW_X509_TYPE.into()),
4762                    transforms: RetrievalMethodTransforms::None,
4763                })
4764                .collect(),
4765        };
4766        let document = Document::parse("<root/>").unwrap();
4767        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4768
4769        let error = materialize_retrieval_methods(
4770            &mut key_info,
4771            &resolver,
4772            UriTypeSet::ALL,
4773            None,
4774            crate::provider::default_provider(),
4775        )
4776        .expect_err("retrieval count must be bounded before materialization");
4777        assert!(matches!(
4778            error,
4779            SignatureVerificationPipelineError::InvalidStructure {
4780                reason: "KeyInfo contains too many RetrievalMethod elements"
4781            }
4782        ));
4783    }
4784
4785    #[test]
4786    fn retrieval_method_materialization_deduplicates_within_count_limit() {
4787        // Repeated references to the same raw certificate produce one parsed
4788        // key source rather than one certificate clone per XML element.
4789        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
4790        let certificate = include_bytes!(
4791            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
4792        )
4793        .to_vec();
4794        let resources = HashMap::from([("urn:certificate".to_string(), certificate)]);
4795        let mut key_info = KeyInfo {
4796            sources: (0..MAX_RETRIEVAL_METHOD_COUNT)
4797                .map(|_| super::super::parse::KeyInfoSource::RetrievalMethod {
4798                    uri: "urn:certificate".into(),
4799                    resource_type: Some(RAW_X509_TYPE.into()),
4800                    transforms: RetrievalMethodTransforms::None,
4801                })
4802                .collect(),
4803        };
4804        let document = Document::parse("<root/>").unwrap();
4805        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4806
4807        materialize_retrieval_methods(
4808            &mut key_info,
4809            &resolver,
4810            UriTypeSet::ALL,
4811            None,
4812            crate::provider::default_provider(),
4813        )
4814        .unwrap();
4815        assert!(matches!(
4816            key_info.sources.as_slice(),
4817            [super::super::parse::KeyInfoSource::X509Data(info)]
4818                if info.certificates.len() == 1
4819        ));
4820    }
4821
4822    #[test]
4823    fn retrieval_method_candidate_budget_includes_embedded_key_values() {
4824        // A previously parsed KeyValue consumes the sole candidate slot, so
4825        // malformed retrieved DER must be rejected by policy before X.509 parsing.
4826        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
4827        let resources = HashMap::from([("urn:certificate".to_string(), vec![1, 2, 3])]);
4828        let mut key_info = KeyInfo {
4829            sources: vec![
4830                super::super::parse::KeyInfoSource::KeyValue(
4831                    super::super::parse::KeyValueInfo::Unsupported {
4832                        namespace: Some(XMLDSIG_NS.into()),
4833                        local_name: "FutureKeyValue".into(),
4834                    },
4835                ),
4836                super::super::parse::KeyInfoSource::RetrievalMethod {
4837                    uri: "urn:certificate".into(),
4838                    resource_type: Some(RAW_X509_TYPE.into()),
4839                    transforms: RetrievalMethodTransforms::None,
4840                },
4841            ],
4842        };
4843        let document = Document::parse("<root/>").unwrap();
4844        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4845        let mut xpath_parse_budget = XPathSignatureParseBudget::default();
4846        let execution_budget = TransformExecutionBudget::default();
4847        let resource_policy = crate::policy::ResourcePolicy {
4848            max_key_candidates: 1,
4849            ..crate::policy::ResourcePolicy::default()
4850        };
4851        let mut budgets = RetrievalMaterializationBudgets {
4852            xpath_parse: &mut xpath_parse_budget,
4853            execution: &execution_budget,
4854            resources: &resource_policy,
4855        };
4856
4857        let error = materialize_retrieval_methods_with_budgets(
4858            &mut key_info,
4859            &resolver,
4860            UriTypeSet::ALL,
4861            None,
4862            crate::provider::default_provider(),
4863            &mut budgets,
4864        )
4865        .expect_err("the retrieved certificate must exceed the aggregate candidate limit");
4866
4867        assert!(matches!(
4868            error,
4869            SignatureVerificationPipelineError::Policy(
4870                crate::policy::PolicyViolation::ResourceLimit {
4871                    resource: crate::policy::resource_name::KEY_CANDIDATES,
4872                    maximum: 1,
4873                    actual: 2,
4874                }
4875            )
4876        ));
4877    }
4878
4879    #[test]
4880    fn raw_x509_retrieval_rejects_empty_same_document_uri() {
4881        // rawX509Certificate consumes external DER octets; an empty URI denotes
4882        // the XML document and must never become a key into the external map.
4883        const RAW_X509_TYPE: &str = "http://www.w3.org/2000/09/xmldsig#rawX509Certificate";
4884        let certificate = include_bytes!(
4885            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
4886        )
4887        .to_vec();
4888        let resources = HashMap::from([(String::new(), certificate)]);
4889        let mut key_info = KeyInfo {
4890            sources: vec![super::super::parse::KeyInfoSource::RetrievalMethod {
4891                uri: String::new(),
4892                resource_type: Some(RAW_X509_TYPE.into()),
4893                transforms: RetrievalMethodTransforms::None,
4894            }],
4895        };
4896        let document = Document::parse("<root/>").unwrap();
4897        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
4898
4899        let error = materialize_retrieval_methods(
4900            &mut key_info,
4901            &resolver,
4902            UriTypeSet::ALL,
4903            None,
4904            crate::provider::default_provider(),
4905        )
4906        .expect_err("empty URI must retain same-document semantics");
4907        assert!(matches!(
4908            error,
4909            SignatureVerificationPipelineError::InvalidStructure {
4910                reason: "raw X509 RetrievalMethod requires an untransformed external URI"
4911            }
4912        ));
4913    }
4914
4915    #[test]
4916    fn verify_context_does_not_hide_malformed_digest_behind_unsupported_transform() {
4917        // A bad DigestValue remains a parse error even when its transform URI is unsupported.
4918        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4919            let xml = xml.replacen(
4920                "<ds:Reference URI=\"#target\">",
4921                "<ds:Reference URI=\"#target\"><ds:Transforms><ds:Transform Algorithm=\"urn:unsupported\"/></ds:Transforms>",
4922                1,
4923            );
4924            replace_fixture_manifest_digest(&xml, "!!!")
4925        });
4926
4927        let error = VerifyContext::new()
4928            .key(&AcceptingKey)
4929            .process_manifests(true)
4930            .verify(&broken_xml)
4931            .expect_err("malformed Manifest digest must not become a validity result");
4932        assert!(matches!(
4933            error,
4934            SignatureVerificationPipelineError::ParseManifestReference(_)
4935        ));
4936    }
4937
4938    #[test]
4939    fn verify_context_rejects_manifest_non_whitespace_mixed_content() {
4940        // Authenticated mixed content is still structurally invalid under the
4941        // Manifest element-only grammar.
4942        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4943            xml.replacen(
4944                "<ds:Manifest ID=\"manifest\">",
4945                "<ds:Manifest ID=\"manifest\">junk",
4946                1,
4947            )
4948        });
4949
4950        let err = VerifyContext::new()
4951            .key(&AcceptingKey)
4952            .process_manifests(true)
4953            .verify(&xml)
4954            .expect_err("Manifest mixed content must fail verification");
4955        assert!(matches!(
4956            err,
4957            SignatureVerificationPipelineError::InvalidStructure {
4958                reason: "Manifest contains non-whitespace mixed content"
4959            }
4960        ));
4961    }
4962
4963    #[test]
4964    fn verify_context_rejects_empty_manifest_children() {
4965        // An authenticated empty Manifest violates the required Reference+
4966        // content model rather than disappearing as an unsigned block.
4967        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
4968            let (prefix, rest) = xml
4969                .split_once("<ds:Manifest ID=\"manifest\">")
4970                .expect("fixture should contain Manifest");
4971            let (_, suffix) = rest
4972                .split_once("</ds:Manifest>")
4973                .expect("fixture should contain closing Manifest");
4974            format!("{prefix}<ds:Manifest ID=\"manifest\"></ds:Manifest>{suffix}")
4975        });
4976
4977        let err = VerifyContext::new()
4978            .key(&AcceptingKey)
4979            .process_manifests(true)
4980            .verify(&xml)
4981            .expect_err("empty Manifest must fail verification");
4982        assert!(matches!(
4983            err,
4984            SignatureVerificationPipelineError::InvalidStructure {
4985                reason: "Manifest must contain at least one ds:Reference element child"
4986            }
4987        ));
4988    }
4989
4990    #[test]
4991    fn verify_context_ignores_unsigned_malformed_manifest_blocks() {
4992        let xml = signature_with_manifest_xml(true).replacen(
4993            "</ds:Object>",
4994            "</ds:Object><ds:Object><ds:Manifest>junk<ds:Foo/></ds:Manifest></ds:Object>",
4995            1,
4996        );
4997        let result = VerifyContext::new()
4998            .key(&AcceptingKey)
4999            .process_manifests(true)
5000            .verify(&xml)
5001            .expect("unsigned malformed Manifest must be ignored");
5002        assert_eq!(
5003            result.manifest_references.len(),
5004            1,
5005            "only signed Manifest references must be reported",
5006        );
5007        assert!(matches!(result.status, DsigStatus::Valid));
5008    }
5009
5010    #[test]
5011    fn verify_context_skips_ambiguous_manifest_id_blocks() {
5012        let xml = signature_with_manifest_xml(true).replacen(
5013            "</ds:Object>",
5014            "</ds:Object><ds:Object><ds:Manifest ID=\"manifest\">junk<ds:Foo/></ds:Manifest></ds:Object>",
5015            1,
5016        );
5017        let err = VerifyContext::new()
5018            .key(&RejectingKey)
5019            .process_manifests(true)
5020            .verify(&xml)
5021            .expect_err("ambiguous manifest IDs should make SignedInfo #manifest dereference fail");
5022        assert!(matches!(
5023            err,
5024            SignatureVerificationPipelineError::Reference(
5025                ReferenceProcessingError::UriDereference(
5026                    crate::xmldsig::types::TransformError::ElementNotFound(id)
5027                )
5028            ) if id == "manifest"
5029        ));
5030    }
5031
5032    #[test]
5033    fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
5034        let xml = minimal_signature_xml("", "");
5035        let err = VerifyContext::new()
5036            .key(&RejectingKey)
5037            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
5038            .verify(&xml)
5039            .expect_err("implicit default C14N must be checked against allowlist");
5040        assert!(matches!(
5041            err,
5042            SignatureVerificationPipelineError::Policy(crate::policy::PolicyViolation::Algorithm {
5043                operation: "verification transform",
5044                ..
5045            })
5046        ));
5047    }
5048
5049    #[test]
5050    fn verify_context_skips_resolver_when_reference_processing_fails() {
5051        let xml = minimal_signature_xml("", "");
5052        let result = VerifyContext::new()
5053            .key_resolver(&PanicResolver)
5054            .verify(&xml)
5055            .expect("reference digest mismatch should short-circuit before resolver");
5056        assert!(matches!(
5057            result.status,
5058            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
5059        ));
5060    }
5061
5062    #[test]
5063    fn verify_context_reports_key_not_found_when_resolver_misses() {
5064        let xml = signature_with_target_reference("AQ==");
5065        let result = VerifyContext::new()
5066            .key_resolver(&MissingKeyResolver)
5067            .verify(&xml)
5068            .expect("resolver miss should report status, not pipeline error");
5069        assert!(matches!(
5070            result.status,
5071            DsigStatus::Invalid(FailureReason::KeyNotFound)
5072        ));
5073        assert_eq!(
5074            result.signed_info_references.len(),
5075            1,
5076            "KeyNotFound path must preserve SignedInfo reference diagnostics",
5077        );
5078        assert!(matches!(
5079            result.signed_info_references[0].status,
5080            DsigStatus::Valid
5081        ));
5082    }
5083
5084    #[test]
5085    fn verification_candidate_budget_covers_preset_and_custom_resolver_paths() {
5086        // Zero is a valid deny-all ceiling. Neither an already-resolved key nor
5087        // a custom resolver may bypass the operation-wide candidate policy.
5088        let xml = signature_with_target_reference("AQ==");
5089        let mut policy = crate::policy::VerificationPolicy::default();
5090        policy.resources.max_key_candidates = 0;
5091
5092        let preset_error = VerifyContext::new()
5093            .key(&RejectingKey)
5094            .policy(policy.clone())
5095            .verify(&xml)
5096            .expect_err("a preset key consumes one candidate");
5097        assert!(matches!(
5098            preset_error,
5099            SignatureVerificationPipelineError::Policy(
5100                crate::policy::PolicyViolation::ResourceLimit {
5101                    resource: crate::policy::resource_name::KEY_CANDIDATES,
5102                    maximum: 0,
5103                    actual: 1,
5104                }
5105            )
5106        ));
5107
5108        let resolver_error = VerifyContext::new()
5109            .key_resolver(&PanicResolver)
5110            .policy(policy)
5111            .verify(&xml)
5112            .expect_err("a custom resolver requires candidate capacity before dispatch");
5113        assert!(matches!(
5114            resolver_error,
5115            SignatureVerificationPipelineError::Policy(
5116                crate::policy::PolicyViolation::ResourceLimit {
5117                    resource: crate::policy::resource_name::KEY_CANDIDATES,
5118                    maximum: 0,
5119                    actual: 1,
5120                }
5121            )
5122        ));
5123    }
5124
5125    #[test]
5126    fn verification_candidate_budget_precedes_embedded_x509_parsing() {
5127        // The first certificate is valid and consumes the sole permitted slot;
5128        // malformed bytes in the second must never reach the X.509 parser.
5129        let first_certificate = base64::engine::general_purpose::STANDARD.encode(include_bytes!(
5130            "../../tests/fixtures/xmldsig/phaos-xmldsig-three/certs/rsa-cert.der"
5131        ));
5132        let xml = signature_with_target_reference("AQ==").replace(
5133            "</ds:SignatureValue>\n  </ds:Signature>",
5134            &format!(
5135                "</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>"
5136            ),
5137        );
5138        let mut policy = crate::policy::VerificationPolicy::default();
5139        policy.resources.max_key_candidates = 1;
5140
5141        let error = VerifyContext::new()
5142            .policy(policy)
5143            .verify(&xml)
5144            .expect_err("candidate policy must run before embedded certificate parsing");
5145
5146        assert!(matches!(
5147            error,
5148            SignatureVerificationPipelineError::Policy(
5149                crate::policy::PolicyViolation::ResourceLimit {
5150                    resource: crate::policy::resource_name::KEY_CANDIDATES,
5151                    maximum: 1,
5152                    actual: 2,
5153                }
5154            )
5155        ));
5156    }
5157
5158    #[test]
5159    fn verify_context_resolver_can_ignore_malformed_keyinfo_by_default() {
5160        let base_xml = signature_with_target_reference("AQ==");
5161        let xml = base_xml
5162            .replace(
5163                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
5164                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
5165            )
5166            .replace(
5167                "</ds:SignatureValue>\n  </ds:Signature>",
5168                "</ds:SignatureValue>\n    <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n  </ds:Signature>",
5169            );
5170
5171        let result = VerifyContext::new()
5172            .key_resolver(&MissingKeyResolver)
5173            .verify(&xml)
5174            .expect("resolver path should not hard-fail on advisory malformed KeyInfo by default");
5175        assert!(matches!(
5176            result.status,
5177            DsigStatus::Invalid(FailureReason::KeyNotFound)
5178        ));
5179    }
5180
5181    #[test]
5182    fn verify_context_resolver_can_opt_in_to_keyinfo_parse_failures() {
5183        let base_xml = signature_with_target_reference("AQ==");
5184        let xml = base_xml
5185            .replace(
5186                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
5187                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
5188            )
5189            .replace(
5190                "</ds:SignatureValue>\n  </ds:Signature>",
5191                "</ds:SignatureValue>\n    <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n  </ds:Signature>",
5192            );
5193
5194        let err = VerifyContext::new()
5195            .key_resolver(&ConsumingKeyInfoResolver)
5196            .verify(&xml)
5197            .expect_err("resolver opted into KeyInfo parsing, malformed KeyInfo must fail");
5198        assert!(matches!(
5199            err,
5200            SignatureVerificationPipelineError::ParseKeyInfo(_)
5201        ));
5202    }
5203
5204    #[test]
5205    fn verify_context_ignores_unsupported_retrieval_before_valid_key_source() {
5206        // An advisory vendor RetrievalMethod cannot prevent the resolver from
5207        // reaching a later supported source in document order.
5208        let xml = signature_with_target_reference("AQ==").replace(
5209            "</ds:SignatureValue>\n  </ds:Signature>",
5210            r##"</ds:SignatureValue>
5211    <ds:KeyInfo>
5212      <ds:RetrievalMethod URI="#vendor" Type="urn:vendor:key">
5213        <ds:Transforms><ds:Transform Algorithm="urn:vendor:transform"/></ds:Transforms>
5214      </ds:RetrievalMethod>
5215      <ds:KeyName>fallback</ds:KeyName>
5216    </ds:KeyInfo>
5217  </ds:Signature>"##,
5218        );
5219
5220        let result = VerifyContext::new()
5221            .key_resolver(&FallbackKeyInfoResolver)
5222            .verify(&xml)
5223            .expect("unsupported advisory retrieval must not abort key resolution");
5224        assert_eq!(result.status, DsigStatus::Valid);
5225    }
5226
5227    #[test]
5228    fn verify_context_does_not_eagerly_fail_unused_retrieval_fallback() {
5229        // KeyInfo sources are alternatives in document order. Once an earlier
5230        // source resolves, a missing later RetrievalMethod is irrelevant.
5231        let xml = signature_with_target_reference("AQ==").replace(
5232            "</ds:SignatureValue>\n  </ds:Signature>",
5233            r#"</ds:SignatureValue>
5234    <ds:KeyInfo>
5235      <ds:KeyName>primary</ds:KeyName>
5236      <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
5237    </ds:KeyInfo>
5238  </ds:Signature>"#,
5239        );
5240
5241        let result = VerifyContext::new()
5242            .key_resolver(&EarlyKeyInfoResolver)
5243            .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
5244            .verify(&xml)
5245            .expect("an unused missing retrieval fallback must not abort verification");
5246
5247        assert_eq!(result.status, DsigStatus::Valid);
5248    }
5249
5250    #[test]
5251    fn verify_context_does_not_eagerly_parse_unused_retrieval_fallback() {
5252        // Materialization must preserve ordered fallback semantics even when
5253        // caller-supplied bytes exist but are not a certificate.
5254        let xml = signature_with_target_reference("AQ==").replace(
5255            "</ds:SignatureValue>\n  </ds:Signature>",
5256            r#"</ds:SignatureValue>
5257    <ds:KeyInfo>
5258      <ds:KeyName>primary</ds:KeyName>
5259      <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
5260    </ds:KeyInfo>
5261  </ds:Signature>"#,
5262        );
5263        let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
5264
5265        let result = VerifyContext::new()
5266            .key_resolver(&EarlyKeyInfoResolver)
5267            .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
5268            .external_resources(&resources)
5269            .verify(&xml)
5270            .expect("an unused malformed retrieval fallback must not abort verification");
5271
5272        assert_eq!(result.status, DsigStatus::Valid);
5273    }
5274
5275    #[test]
5276    fn verify_context_reports_missing_retrieval_when_no_key_source_resolves() {
5277        // Deferral changes ordering, not diagnostics: if no alternative source
5278        // resolves, the first missing retrieval remains the pipeline failure.
5279        let xml = signature_with_target_reference("AQ==").replace(
5280            "</ds:SignatureValue>\n  </ds:Signature>",
5281            r#"</ds:SignatureValue>
5282    <ds:KeyInfo>
5283      <ds:RetrievalMethod URI="missing.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
5284    </ds:KeyInfo>
5285  </ds:Signature>"#,
5286        );
5287
5288        let error = VerifyContext::new()
5289            .key_resolver(&ConsumingKeyInfoResolver)
5290            .allowed_retrieval_method_uri_types(UriTypeSet::new(true, true, true))
5291            .verify(&xml)
5292            .expect_err("a missing sole RetrievalMethod must remain an explicit error");
5293
5294        assert!(matches!(
5295            error,
5296            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::Transform(
5297                crate::xmldsig::TransformError::UnsupportedUri(uri)
5298            )) if uri == "missing.der"
5299        ));
5300    }
5301
5302    #[test]
5303    fn verify_context_reports_malformed_retrieval_when_no_key_source_resolves() {
5304        // Deferral must retain the parse error when the malformed certificate
5305        // is the only candidate rather than degrading it to KeyNotFound.
5306        let xml = signature_with_target_reference("AQ==").replace(
5307            "</ds:SignatureValue>\n  </ds:Signature>",
5308            r#"</ds:SignatureValue>
5309    <ds:KeyInfo>
5310      <ds:RetrievalMethod URI="malformed.der" Type="http://www.w3.org/2000/09/xmldsig#rawX509Certificate"/>
5311    </ds:KeyInfo>
5312  </ds:Signature>"#,
5313        );
5314        let resources = HashMap::from([("malformed.der".to_string(), b"not DER".to_vec())]);
5315
5316        let error = VerifyContext::new()
5317            .key_resolver(&ConsumingKeyInfoResolver)
5318            .allowed_retrieval_method_uri_types(UriTypeSet::ALL)
5319            .external_resources(&resources)
5320            .verify(&xml)
5321            .expect_err("a malformed sole RetrievalMethod must remain a parse error");
5322
5323        assert!(matches!(
5324            error,
5325            SignatureVerificationPipelineError::ParseKeyInfo(_)
5326        ));
5327    }
5328
5329    #[test]
5330    fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
5331        let xml = signature_with_target_reference("@@@");
5332
5333        let err = VerifyContext::new()
5334            .key_resolver(&MissingKeyResolver)
5335            .verify(&xml)
5336            .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
5337        assert!(matches!(
5338            err,
5339            SignatureVerificationPipelineError::SignatureValueBase64(_)
5340        ));
5341    }
5342
5343    #[test]
5344    fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
5345        let xml = signature_with_target_reference("@@@");
5346
5347        let err = VerifyContext::new()
5348            .verify(&xml)
5349            .expect_err("invalid SignatureValue must remain a decode error");
5350        assert!(matches!(
5351            err,
5352            SignatureVerificationPipelineError::SignatureValueBase64(_)
5353        ));
5354    }
5355
5356    #[test]
5357    fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
5358        let references = vec![Reference {
5359            uri: None,
5360            id: None,
5361            ref_type: None,
5362            transforms: vec![],
5363            digest_method: DigestAlgorithm::Sha256,
5364            digest_value: vec![0; 32],
5365        }];
5366        let uri_types = UriTypeSet {
5367            allow_empty: false,
5368            allow_same_document: true,
5369            allow_external: false,
5370        };
5371
5372        let err = enforce_reference_policies(&references, uri_types, None)
5373            .expect_err("missing URI must fail before allow_empty policy is evaluated");
5374        assert!(matches!(
5375            err,
5376            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
5377        ));
5378    }
5379
5380    #[test]
5381    fn enforce_reference_policies_checks_only_terminal_binary_output() {
5382        let c14n = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI).unwrap();
5383        let allowed = HashSet::from([
5384            BASE64_TRANSFORM_URI.to_owned(),
5385            DEFAULT_IMPLICIT_C14N_URI.to_owned(),
5386        ]);
5387        let without_implicit_c14n = HashSet::from([BASE64_TRANSFORM_URI.to_owned()]);
5388
5389        for transforms in [
5390            vec![Transform::Base64Decode, Transform::C14n(c14n)],
5391            vec![Transform::Base64Decode, Transform::Base64Decode],
5392        ] {
5393            let reference = make_reference("", transforms, DigestAlgorithm::Sha256, vec![0; 32]);
5394            enforce_reference_policies(
5395                std::slice::from_ref(&reference),
5396                UriTypeSet::default(),
5397                Some(&allowed),
5398            )
5399            .expect("terminal binary output must not require implicit C14N");
5400        }
5401
5402        let terminal_base64 = make_reference(
5403            "",
5404            vec![Transform::Base64Decode, Transform::Base64Decode],
5405            DigestAlgorithm::Sha256,
5406            vec![0; 32],
5407        );
5408        enforce_reference_policies(
5409            std::slice::from_ref(&terminal_base64),
5410            UriTypeSet::default(),
5411            Some(&without_implicit_c14n),
5412        )
5413        .expect("terminal Base64 output must not require implicit C14N");
5414
5415        let no_transforms = make_reference("", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
5416        let error = enforce_reference_policies(
5417            std::slice::from_ref(&no_transforms),
5418            UriTypeSet::default(),
5419            Some(&without_implicit_c14n),
5420        )
5421        .expect_err("a node-set result must require allowlisted implicit C14N");
5422        assert!(matches!(
5423            error,
5424            SignatureVerificationPipelineError::Policy(
5425                crate::policy::PolicyViolation::Algorithm {
5426                    operation: "verification transform",
5427                    ref algorithm,
5428                }
5429            )
5430                if algorithm == DEFAULT_IMPLICIT_C14N_URI
5431        ));
5432
5433        let detached = make_reference("urn:payload", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
5434        enforce_reference_policies(
5435            std::slice::from_ref(&detached),
5436            UriTypeSet::ALL,
5437            Some(&without_implicit_c14n),
5438        )
5439        .expect("external octets without transforms must not require implicit C14N");
5440
5441        let external_xpath = make_reference(
5442            "urn:payload",
5443            vec![Transform::XPath(
5444                super::super::transforms::XPathExpression::new("true()"),
5445            )],
5446            DigestAlgorithm::Sha256,
5447            vec![0; 32],
5448        );
5449        let error = enforce_reference_policies(
5450            std::slice::from_ref(&external_xpath),
5451            UriTypeSet::ALL,
5452            Some(&HashSet::from([XPATH_TRANSFORM_URI.to_owned()])),
5453        )
5454        .expect_err("external XML converted to a node-set must require implicit C14N");
5455        assert!(matches!(
5456            error,
5457            SignatureVerificationPipelineError::Policy(
5458                crate::policy::PolicyViolation::Algorithm {
5459                    operation: "verification transform",
5460                    ref algorithm,
5461                }
5462            )
5463                if algorithm == DEFAULT_IMPLICIT_C14N_URI
5464        ));
5465    }
5466
5467    #[test]
5468    fn stored_pre_digest_budget_counts_repeated_external_references() {
5469        // The caller map owns one bounded payload, but diagnostic retention is
5470        // charged per Reference because every result owns its pre-digest bytes.
5471        let document =
5472            Document::parse("<ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"/>")
5473                .unwrap();
5474        let payload = vec![b'x'; 7];
5475        let digest = compute_digest(DigestAlgorithm::Sha256, &payload);
5476        let references = (0..5)
5477            .map(|_| {
5478                make_reference(
5479                    "urn:repeated",
5480                    Vec::new(),
5481                    DigestAlgorithm::Sha256,
5482                    digest.clone(),
5483                )
5484            })
5485            .collect::<Vec<_>>();
5486        let resources = HashMap::from([("urn:repeated".to_owned(), payload)]);
5487        let resolver = UriReferenceResolver::new(&document).with_external_resources(&resources);
5488        let transform_budget = TransformExecutionBudget::default();
5489        let canonicalized_data_budget = CanonicalizedDataBudget::with_limit(32);
5490        let execution = ReferenceExecutionContext {
5491            store_pre_digest: true,
5492            transform_options: TransformOptions::default(),
5493            transform_budget: &transform_budget,
5494            canonicalized_data_budget: &canonicalized_data_budget,
5495            provider: crate::provider::default_provider(),
5496        };
5497
5498        let error = process_all_references_with_options(
5499            &references,
5500            &resolver,
5501            document.root_element(),
5502            &execution,
5503        )
5504        .expect_err(
5505            "retained diagnostics must not multiply one external allocation past the aggregate cap",
5506        );
5507        assert!(matches!(
5508            error,
5509            ReferenceProcessingError::Policy(crate::policy::PolicyViolation::ResourceLimit {
5510                resource: "canonicalized bytes",
5511                maximum: 32,
5512                ..
5513            })
5514        ));
5515    }
5516
5517    #[test]
5518    fn canonical_signed_info_obeys_policy_without_diagnostic_retention() {
5519        // SignedInfo is always materialized for crypto verification, so its
5520        // canonical bytes must consume the configured ceiling even when
5521        // diagnostics do not retain reference output.
5522        let xml = signature_with_target_reference("AQ==");
5523        let marker = "<ds:SignatureMethod";
5524        let padding = " ".repeat(1_025);
5525        let xml = xml.replacen(marker, &format!("{padding}{marker}"), 1);
5526        let policy = crate::policy::VerificationPolicy {
5527            resources: crate::policy::ResourcePolicy {
5528                max_canonicalized_bytes: 1_024,
5529                ..crate::policy::ResourcePolicy::default()
5530            },
5531            ..crate::policy::VerificationPolicy::default()
5532        };
5533
5534        let error = VerifyContext::new()
5535            .key(&AcceptingKey)
5536            .policy(policy)
5537            .verify(&xml)
5538            .expect_err("canonicalized SignedInfo must remain policy-bounded");
5539
5540        assert!(matches!(
5541            error,
5542            SignatureVerificationPipelineError::Policy(
5543                crate::policy::PolicyViolation::ResourceLimit {
5544                    resource: "canonicalized bytes",
5545                    ..
5546                }
5547            )
5548        ));
5549    }
5550
5551    #[test]
5552    fn push_normalized_signature_text_rejects_form_feed() {
5553        let mut normalized = Vec::new();
5554        let mut raw_text_len = 0usize;
5555        let err =
5556            push_normalized_signature_text("ab\u{000C}cd", &mut raw_text_len, &mut normalized)
5557                .expect_err("form-feed must not be treated as XML base64 whitespace");
5558        assert!(matches!(
5559            err,
5560            SignatureVerificationPipelineError::SignatureValueBase64(
5561                base64::DecodeError::InvalidByte(_, 0x0C)
5562            )
5563        ));
5564    }
5565
5566    #[test]
5567    fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
5568        let mut normalized = vec![b'A'; MAX_SIGNATURE_VALUE_LEN - 1];
5569        let mut raw_text_len = normalized.len();
5570        let err = push_normalized_signature_text("é", &mut raw_text_len, &mut normalized)
5571            .expect_err("multibyte characters must not bypass byte-size limit");
5572        assert!(matches!(
5573            err,
5574            SignatureVerificationPipelineError::InvalidStructure {
5575                reason: "SignatureValue exceeds maximum allowed length"
5576            }
5577        ));
5578    }
5579
5580    // ── process_reference: happy path ────────────────────────────────
5581
5582    #[test]
5583    fn reference_with_correct_digest_passes() {
5584        // Create a simple document, compute its canonical form digest,
5585        // then verify that process_reference returns Valid status.
5586        let xml = r##"<root>
5587            <data>hello world</data>
5588            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
5589                <ds:SignedInfo/>
5590            </ds:Signature>
5591        </root>"##;
5592        let doc = Document::parse(xml).unwrap();
5593        let resolver = UriReferenceResolver::new(&doc);
5594        let sig_node = doc
5595            .descendants()
5596            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
5597            .unwrap();
5598
5599        // First, compute the expected digest by running the pipeline
5600        let initial_data = resolver.dereference("").unwrap();
5601        let transforms = vec![
5602            Transform::Enveloped,
5603            Transform::C14n(
5604                crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
5605                    .unwrap(),
5606            ),
5607        ];
5608        let pre_digest_bytes =
5609            crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
5610        let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
5611
5612        // Now build a Reference with the correct digest and verify
5613        let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
5614
5615        let result = process_reference(
5616            &reference,
5617            &resolver,
5618            sig_node,
5619            ReferenceSet::SignedInfo,
5620            0,
5621            false,
5622        )
5623        .unwrap();
5624        assert!(
5625            matches!(result.status, DsigStatus::Valid),
5626            "digest should match"
5627        );
5628        assert!(result.pre_digest_data.is_none());
5629    }
5630
5631    #[test]
5632    fn reference_with_wrong_digest_fails() {
5633        let xml = r##"<root>
5634            <data>hello</data>
5635            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5636                <ds:SignedInfo/>
5637            </ds:Signature>
5638        </root>"##;
5639        let doc = Document::parse(xml).unwrap();
5640        let resolver = UriReferenceResolver::new(&doc);
5641        let sig_node = doc
5642            .descendants()
5643            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
5644            .unwrap();
5645
5646        let transforms = vec![Transform::Enveloped];
5647        // Wrong digest value — all zeros
5648        let wrong_digest = vec![0u8; 32];
5649        let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
5650
5651        let result = process_reference(
5652            &reference,
5653            &resolver,
5654            sig_node,
5655            ReferenceSet::SignedInfo,
5656            0,
5657            false,
5658        )
5659        .unwrap();
5660        assert!(matches!(
5661            result.status,
5662            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
5663        ));
5664    }
5665
5666    #[test]
5667    fn reference_with_wrong_digest_preserves_supplied_ref_index() {
5668        let xml = r##"<root>
5669            <data>hello</data>
5670            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5671                <ds:SignedInfo/>
5672            </ds:Signature>
5673        </root>"##;
5674        let doc = Document::parse(xml).unwrap();
5675        let resolver = UriReferenceResolver::new(&doc);
5676        let sig_node = doc
5677            .descendants()
5678            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
5679            .unwrap();
5680
5681        let reference = make_reference(
5682            "",
5683            vec![Transform::Enveloped],
5684            DigestAlgorithm::Sha256,
5685            vec![0u8; 32],
5686        );
5687        let result = process_reference(
5688            &reference,
5689            &resolver,
5690            sig_node,
5691            ReferenceSet::SignedInfo,
5692            7,
5693            false,
5694        )
5695        .unwrap();
5696        assert!(matches!(
5697            result.status,
5698            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
5699        ));
5700    }
5701
5702    #[test]
5703    fn reference_stores_pre_digest_data() {
5704        let xml = "<root><child>text</child></root>";
5705        let doc = Document::parse(xml).unwrap();
5706        let resolver = UriReferenceResolver::new(&doc);
5707
5708        // No transforms, no enveloped — just canonicalize entire document
5709        let initial_data = resolver.dereference("").unwrap();
5710        let pre_digest =
5711            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
5712        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
5713
5714        let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
5715        let result = process_reference(
5716            &reference,
5717            &resolver,
5718            doc.root_element(),
5719            ReferenceSet::SignedInfo,
5720            0,
5721            true,
5722        )
5723        .unwrap();
5724
5725        assert!(matches!(result.status, DsigStatus::Valid));
5726        assert!(result.pre_digest_data.is_some());
5727        assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
5728    }
5729
5730    // ── process_reference: URI dereference ───────────────────────────
5731
5732    #[test]
5733    fn reference_with_id_uri() {
5734        let xml = r##"<root>
5735            <item ID="target">specific content</item>
5736            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5737                <ds:SignedInfo/>
5738            </ds:Signature>
5739        </root>"##;
5740        let doc = Document::parse(xml).unwrap();
5741        let resolver = UriReferenceResolver::new(&doc);
5742        let sig_node = doc
5743            .descendants()
5744            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
5745            .unwrap();
5746
5747        // Compute expected digest for the #target subtree
5748        let initial_data = resolver.dereference("#target").unwrap();
5749        let transforms = vec![Transform::C14n(
5750            crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
5751                .unwrap(),
5752        )];
5753        let pre_digest =
5754            crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
5755        let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
5756
5757        let reference = make_reference(
5758            "#target",
5759            transforms,
5760            DigestAlgorithm::Sha256,
5761            expected_digest,
5762        );
5763        let result = process_reference(
5764            &reference,
5765            &resolver,
5766            sig_node,
5767            ReferenceSet::SignedInfo,
5768            0,
5769            false,
5770        )
5771        .unwrap();
5772        assert!(matches!(result.status, DsigStatus::Valid));
5773    }
5774
5775    #[test]
5776    fn reference_with_nonexistent_id_fails() {
5777        let xml = "<root><child/></root>";
5778        let doc = Document::parse(xml).unwrap();
5779        let resolver = UriReferenceResolver::new(&doc);
5780
5781        let reference =
5782            make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
5783        let result = process_reference(
5784            &reference,
5785            &resolver,
5786            doc.root_element(),
5787            ReferenceSet::SignedInfo,
5788            0,
5789            false,
5790        );
5791        assert!(result.is_err());
5792    }
5793
5794    #[test]
5795    fn reference_with_absent_uri_fails_closed() {
5796        let xml = "<root><child>text</child></root>";
5797        let doc = Document::parse(xml).unwrap();
5798        let resolver = UriReferenceResolver::new(&doc);
5799
5800        let reference = Reference {
5801            uri: None, // absent URI
5802            id: None,
5803            ref_type: None,
5804            transforms: vec![],
5805            digest_method: DigestAlgorithm::Sha256,
5806            digest_value: vec![0; 32],
5807        };
5808
5809        let result = process_reference(
5810            &reference,
5811            &resolver,
5812            doc.root_element(),
5813            ReferenceSet::SignedInfo,
5814            0,
5815            false,
5816        );
5817        assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
5818    }
5819
5820    // ── process_all_references: fail-fast ────────────────────────────
5821
5822    #[test]
5823    fn all_references_pass() {
5824        let xml = "<root><child>text</child></root>";
5825        let doc = Document::parse(xml).unwrap();
5826        let resolver = UriReferenceResolver::new(&doc);
5827
5828        // Compute correct digest
5829        let initial_data = resolver.dereference("").unwrap();
5830        let pre_digest =
5831            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
5832        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
5833
5834        let refs = vec![
5835            make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
5836            make_reference("", vec![], DigestAlgorithm::Sha256, digest),
5837        ];
5838
5839        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
5840        assert!(result.all_valid());
5841        assert_eq!(result.results.len(), 2);
5842        assert!(result.first_failure.is_none());
5843    }
5844
5845    #[test]
5846    fn reference_processing_shares_xpath_work_across_references() {
5847        // A signature-wide meter must not reset when processing the next
5848        // Reference, even though each transform chain is independently valid.
5849        let document = Document::parse("<root/>").unwrap();
5850        let resolver = UriReferenceResolver::new(&document);
5851        let transform = Transform::XPath(super::super::transforms::XPathExpression::new("true()"));
5852        let initial_data = resolver.dereference("").unwrap();
5853        let pre_digest = crate::xmldsig::execute_transforms(
5854            document.root_element(),
5855            initial_data,
5856            std::slice::from_ref(&transform),
5857        )
5858        .unwrap();
5859        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
5860        let references = vec![
5861            make_reference(
5862                "",
5863                vec![transform.clone()],
5864                DigestAlgorithm::Sha256,
5865                digest.clone(),
5866            ),
5867            make_reference("", vec![transform], DigestAlgorithm::Sha256, digest),
5868        ];
5869        let budget = TransformExecutionBudget::with_xpath_limit(12);
5870        let canonicalized_data_budget = CanonicalizedDataBudget::default();
5871        let execution = ReferenceExecutionContext {
5872            store_pre_digest: false,
5873            transform_options: TransformOptions::default(),
5874            transform_budget: &budget,
5875            canonicalized_data_budget: &canonicalized_data_budget,
5876            provider: crate::provider::default_provider(),
5877        };
5878
5879        let error = process_all_references_with_options(
5880            &references,
5881            &resolver,
5882            document.root_element(),
5883            &execution,
5884        )
5885        .expect_err("the second Reference must consume the first Reference's XPath work");
5886
5887        assert!(matches!(
5888            error,
5889            ReferenceProcessingError::Transform(TransformError::Policy(
5890                crate::policy::PolicyViolation::ResourceLimit {
5891                    resource: crate::policy::resource_name::XPATH_EVALUATION_WORK,
5892                    ..
5893                }
5894            ))
5895        ));
5896    }
5897
5898    #[test]
5899    fn reference_processing_shares_node_set_materialization_across_references() {
5900        // Repeated references to the same small subtree must share one owned-
5901        // string budget. Otherwise a large inherited namespace can be cloned
5902        // once per Reference even when canonicalization emits little output.
5903        let document = Document::parse(
5904            r#"<root xmlns:n="urn:0123456789"><target Id="selected">payload</target></root>"#,
5905        )
5906        .unwrap();
5907        let resolver = UriReferenceResolver::new(&document);
5908        let initial_data = resolver.dereference("#selected").unwrap();
5909        let pre_digest =
5910            crate::xmldsig::execute_transforms(document.root_element(), initial_data, &[]).unwrap();
5911        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
5912        let references = vec![
5913            make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest.clone()),
5914            make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest),
5915        ];
5916        let budget = TransformExecutionBudget::with_node_set_materialization_limit(30);
5917        let canonicalized_data_budget = CanonicalizedDataBudget::default();
5918        let execution = ReferenceExecutionContext {
5919            store_pre_digest: false,
5920            transform_options: TransformOptions::default(),
5921            transform_budget: &budget,
5922            canonicalized_data_budget: &canonicalized_data_budget,
5923            provider: crate::provider::default_provider(),
5924        };
5925
5926        let error = process_all_references_with_options(
5927            &references,
5928            &resolver,
5929            document.root_element(),
5930            &execution,
5931        )
5932        .expect_err("the second Reference must consume the first Reference's materialization work");
5933
5934        assert!(matches!(
5935            error,
5936            ReferenceProcessingError::UriDereference(TransformError::Policy(
5937                crate::policy::PolicyViolation::ResourceLimit {
5938                    resource: crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
5939                    ..
5940                }
5941            ))
5942        ));
5943    }
5944
5945    #[test]
5946    fn fail_fast_on_first_mismatch() {
5947        let xml = "<root><child>text</child></root>";
5948        let doc = Document::parse(xml).unwrap();
5949        let resolver = UriReferenceResolver::new(&doc);
5950
5951        let wrong_digest = vec![0u8; 32];
5952        let refs = vec![
5953            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
5954            // Second reference should NOT be processed
5955            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
5956        ];
5957
5958        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
5959        assert!(!result.all_valid());
5960        assert_eq!(result.first_failure, Some(0));
5961        // Only first reference should be in results (fail-fast)
5962        assert_eq!(result.results.len(), 1);
5963        assert!(matches!(
5964            result.results[0].status,
5965            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
5966        ));
5967    }
5968
5969    #[test]
5970    fn fail_fast_second_reference() {
5971        let xml = "<root><child>text</child></root>";
5972        let doc = Document::parse(xml).unwrap();
5973        let resolver = UriReferenceResolver::new(&doc);
5974
5975        // Compute correct digest for first ref
5976        let initial_data = resolver.dereference("").unwrap();
5977        let pre_digest =
5978            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
5979        let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
5980        let wrong_digest = vec![0u8; 32];
5981
5982        let refs = vec![
5983            make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
5984            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
5985        ];
5986
5987        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
5988        assert!(!result.all_valid());
5989        assert_eq!(result.first_failure, Some(1));
5990        // Both references should be in results
5991        assert_eq!(result.results.len(), 2);
5992        assert!(matches!(result.results[0].status, DsigStatus::Valid));
5993        assert!(matches!(
5994            result.results[1].status,
5995            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
5996        ));
5997    }
5998
5999    #[test]
6000    fn empty_references_list() {
6001        let xml = "<root/>";
6002        let doc = Document::parse(xml).unwrap();
6003        let resolver = UriReferenceResolver::new(&doc);
6004
6005        let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
6006        assert!(result.all_valid());
6007        assert!(result.results.is_empty());
6008    }
6009
6010    // ── Digest algorithms ────────────────────────────────────────────
6011
6012    #[test]
6013    fn reference_sha1_digest() {
6014        let xml = "<root>content</root>";
6015        let doc = Document::parse(xml).unwrap();
6016        let resolver = UriReferenceResolver::new(&doc);
6017
6018        let initial_data = resolver.dereference("").unwrap();
6019        let pre_digest =
6020            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
6021        let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
6022
6023        let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
6024        let result = process_reference(
6025            &reference,
6026            &resolver,
6027            doc.root_element(),
6028            ReferenceSet::SignedInfo,
6029            0,
6030            false,
6031        )
6032        .unwrap();
6033        assert!(matches!(result.status, DsigStatus::Valid));
6034        assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
6035    }
6036
6037    #[test]
6038    fn reference_sha512_digest() {
6039        let xml = "<root>content</root>";
6040        let doc = Document::parse(xml).unwrap();
6041        let resolver = UriReferenceResolver::new(&doc);
6042
6043        let initial_data = resolver.dereference("").unwrap();
6044        let pre_digest =
6045            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
6046        let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
6047
6048        let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
6049        let result = process_reference(
6050            &reference,
6051            &resolver,
6052            doc.root_element(),
6053            ReferenceSet::SignedInfo,
6054            0,
6055            false,
6056        )
6057        .unwrap();
6058        assert!(matches!(result.status, DsigStatus::Valid));
6059        assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
6060    }
6061
6062    // ── SAML-like end-to-end ─────────────────────────────────────────
6063
6064    #[test]
6065    fn saml_enveloped_reference_processing() {
6066        // Realistic SAML Response with enveloped signature
6067        let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
6068                                     xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
6069                                     ID="_resp1">
6070            <saml:Assertion ID="_assert1">
6071                <saml:Subject>user@example.com</saml:Subject>
6072            </saml:Assertion>
6073            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6074                <ds:SignedInfo>
6075                    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6076                    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6077                    <ds:Reference URI="">
6078                        <ds:Transforms>
6079                            <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
6080                            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6081                        </ds:Transforms>
6082                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6083                        <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6084                    </ds:Reference>
6085                </ds:SignedInfo>
6086                <ds:SignatureValue>fakesig==</ds:SignatureValue>
6087            </ds:Signature>
6088        </samlp:Response>"##;
6089        let doc = Document::parse(xml).unwrap();
6090        let resolver = UriReferenceResolver::new(&doc);
6091        let sig_node = doc
6092            .descendants()
6093            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
6094            .unwrap();
6095
6096        // Parse SignedInfo to get the Reference
6097        let signed_info_node = sig_node
6098            .children()
6099            .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
6100            .unwrap();
6101        let signed_info = parse_signed_info(signed_info_node).unwrap();
6102        let reference = &signed_info.references[0];
6103
6104        // Compute the correct digest by running the actual pipeline
6105        let initial_data = resolver.dereference("").unwrap();
6106        let pre_digest =
6107            crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
6108                .unwrap();
6109        let correct_digest = compute_digest(reference.digest_method, &pre_digest);
6110
6111        // Build a reference with the correct digest
6112        let corrected_ref = make_reference(
6113            "",
6114            reference.transforms.clone(),
6115            reference.digest_method,
6116            correct_digest,
6117        );
6118
6119        // Verify: should pass
6120        let result = process_reference(
6121            &corrected_ref,
6122            &resolver,
6123            sig_node,
6124            ReferenceSet::SignedInfo,
6125            0,
6126            true,
6127        )
6128        .unwrap();
6129        assert!(
6130            matches!(result.status, DsigStatus::Valid),
6131            "SAML reference should verify"
6132        );
6133        assert!(result.pre_digest_data.is_some());
6134
6135        // Verify the pre-digest data contains the canonicalized document without Signature
6136        let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
6137        assert!(
6138            pre_digest_str.contains("samlp:Response"),
6139            "pre-digest should contain Response"
6140        );
6141        assert!(
6142            !pre_digest_str.contains("SignatureValue"),
6143            "pre-digest should NOT contain Signature"
6144        );
6145    }
6146
6147    #[test]
6148    fn pipeline_missing_signed_info_returns_missing_element() {
6149        let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
6150
6151        let err = verify_signature_with_pem_key(xml, "dummy-key", false)
6152            .expect_err("missing SignedInfo must fail before crypto stage");
6153        assert!(matches!(
6154            err,
6155            SignatureVerificationPipelineError::MissingElement {
6156                element: "SignedInfo"
6157            }
6158        ));
6159    }
6160
6161    #[test]
6162    fn pipeline_multiple_signature_elements_are_rejected() {
6163        let xml = r#"
6164<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6165  <ds:Signature>
6166    <ds:SignedInfo/>
6167  </ds:Signature>
6168  <ds:Signature/>
6169</root>
6170"#;
6171
6172        let err = verify_signature_with_pem_key(xml, "dummy-key", false)
6173            .expect_err("multiple signatures must fail closed");
6174        assert!(matches!(
6175            err,
6176            SignatureVerificationPipelineError::InvalidStructure {
6177                reason: "Signature must appear exactly once in document",
6178            }
6179        ));
6180    }
6181
6182    #[test]
6183    fn pipeline_start_node_limits_signature_cardinality_to_its_subtree() {
6184        // A start-node selector changes the operation root, not global ID or
6185        // reference resolution; another Signature outside the subtree is irrelevant.
6186        let xml = r#"
6187<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6188  <scope Id="selected"><ds:Signature/></scope>
6189  <scope Id="other"><ds:Signature/></scope>
6190</root>
6191"#;
6192        let err = VerifyContext::new()
6193            .start_node_id("selected")
6194            .verify(xml)
6195            .expect_err("the selected Signature remains structurally incomplete");
6196        assert!(matches!(
6197            err,
6198            SignatureVerificationPipelineError::MissingElement {
6199                element: "SignedInfo"
6200            }
6201        ));
6202    }
6203
6204    #[test]
6205    fn pipeline_reports_keyinfo_parse_error() {
6206        let xml = r#"
6207<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
6208              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
6209  <ds:SignedInfo>
6210    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6211    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6212    <ds:Reference URI="">
6213      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
6214      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6215    </ds:Reference>
6216  </ds:SignedInfo>
6217  <ds:SignatureValue>AA==</ds:SignatureValue>
6218  <ds:KeyInfo>
6219    <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
6220  </ds:KeyInfo>
6221</ds:Signature>
6222"#;
6223
6224        let err = VerifyContext::new().verify(xml).expect_err(
6225            "invalid KeyInfo must map to ParseKeyInfo when no explicit key is supplied",
6226        );
6227        assert!(matches!(
6228            err,
6229            SignatureVerificationPipelineError::ParseKeyInfo(_)
6230        ));
6231    }
6232
6233    #[test]
6234    fn pipeline_ignores_malformed_keyinfo_when_explicit_key_is_supplied() {
6235        let base_xml = signature_with_target_reference("AQ==");
6236        let xml = base_xml
6237            .replace(
6238                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
6239                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
6240            )
6241            .replace(
6242                "</ds:SignatureValue>\n  </ds:Signature>",
6243                "</ds:SignatureValue>\n    <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n  </ds:Signature>",
6244            );
6245
6246        let result = VerifyContext::new()
6247            .key(&RejectingKey)
6248            .verify(&xml)
6249            .expect("explicit key path should not fail on malformed KeyInfo");
6250        assert!(matches!(
6251            result.status,
6252            DsigStatus::Invalid(FailureReason::SignatureMismatch)
6253        ));
6254    }
6255
6256    #[test]
6257    fn pipeline_rejects_foreign_element_children_under_signature() {
6258        let base_xml = signature_with_target_reference("AQ==");
6259        let xml = base_xml
6260            .replace(
6261                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
6262                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:foo="urn:example:foo">"#,
6263            )
6264            .replace(
6265                "</ds:SignedInfo>\n    <ds:SignatureValue>",
6266                "</ds:SignedInfo>\n    <foo:Bar/>\n    <ds:SignatureValue>",
6267            );
6268
6269        let err = VerifyContext::new()
6270            .key(&RejectingKey)
6271            .verify(&xml)
6272            .expect_err("foreign element children under Signature must fail closed");
6273        assert!(matches!(
6274            err,
6275            SignatureVerificationPipelineError::InvalidStructure {
6276                reason: "Signature must contain only XMLDSIG element children",
6277            }
6278        ));
6279    }
6280
6281    #[test]
6282    fn pipeline_rejects_non_whitespace_mixed_content_under_signature() {
6283        let base_xml = signature_with_target_reference("AQ==");
6284        let xml = base_xml.replace(
6285            "</ds:SignedInfo>\n    <ds:SignatureValue>",
6286            "</ds:SignedInfo>\n    oops\n    <ds:SignatureValue>",
6287        );
6288
6289        let err = VerifyContext::new()
6290            .key(&RejectingKey)
6291            .verify(&xml)
6292            .expect_err("non-whitespace mixed content under Signature must fail closed");
6293        assert!(matches!(
6294            err,
6295            SignatureVerificationPipelineError::InvalidStructure {
6296                reason: "Signature must not contain non-whitespace mixed content",
6297            }
6298        ));
6299    }
6300
6301    #[test]
6302    fn pipeline_rejects_keyinfo_out_of_order() {
6303        let base_xml = signature_with_target_reference("AQ==");
6304        let xml = base_xml.replace(
6305            "</ds:SignatureValue>\n  </ds:Signature>",
6306            "</ds:SignatureValue>\n    <ds:Object/>\n    <ds:KeyInfo><ds:KeyName>late</ds:KeyName></ds:KeyInfo>\n  </ds:Signature>",
6307        );
6308
6309        let err = VerifyContext::new()
6310            .key(&RejectingKey)
6311            .verify(&xml)
6312            .expect_err("KeyInfo after Object must be rejected by Signature child order checks");
6313        assert!(matches!(
6314            err,
6315            SignatureVerificationPipelineError::InvalidStructure {
6316                reason: "KeyInfo must be the third element child of Signature when present"
6317            }
6318        ));
6319    }
6320
6321    #[test]
6322    fn pipeline_accepts_comments_and_processing_instructions_under_signature() {
6323        let xml = r#"
6324<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6325  <?dbg keep ?>
6326  <!-- signature metadata -->
6327  <ds:SignedInfo>
6328    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6329    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6330    <ds:Reference URI="">
6331      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
6332      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6333    </ds:Reference>
6334  </ds:SignedInfo>
6335  <!-- between required children -->
6336  <ds:SignatureValue>AA==</ds:SignatureValue>
6337</ds:Signature>
6338"#;
6339
6340        let doc = Document::parse(xml).expect("test XML must parse");
6341        let signature_node = doc.root_element();
6342        let parsed = parse_signature_children(signature_node)
6343            .expect("comment/PI nodes under Signature must be ignored");
6344
6345        assert_eq!(parsed.signed_info_node.tag_name().name(), "SignedInfo");
6346        assert_eq!(
6347            parsed.signature_value_node.tag_name().name(),
6348            "SignatureValue"
6349        );
6350        assert!(parsed.key_info_node.is_none());
6351    }
6352}