Skip to main content

xml_sec/xmldsig/
verify.rs

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