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