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