Skip to main content

xml_sec/xmldsig/
verify.rs

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