Skip to main content

xml_sec/xmldsig/
verify.rs

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