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::collections::HashSet;
16
17use crate::c14n::canonicalize;
18
19use super::digest::{DigestAlgorithm, compute_digest, constant_time_eq};
20use super::parse::{
21    KeyInfo, MAX_REFERENCES_PER_SIGNATURE, ParseError, Reference, SignatureAlgorithm, XMLDSIG_NS,
22};
23use super::parse::{
24    parse_key_info, parse_reference_with_xpath_budget, parse_signed_info_with_xpath_budget,
25};
26use super::signature::{
27    SignatureVerificationError, verify_ecdsa_signature_pem, verify_rsa_signature_pem,
28};
29use super::transforms::{
30    BASE64_TRANSFORM_URI, DEFAULT_IMPLICIT_C14N_URI, Transform, TransformExecutionBudget,
31    TransformOptions, XPATH_TRANSFORM_URI, XPathHereSemantics, XPathSignatureParseBudget,
32    execute_transforms_with_options_and_budget,
33};
34use super::uri::{UriReferenceResolver, parse_xpointer_id_fragment};
35use super::whitespace::{is_xml_whitespace_only, normalize_xml_base64_bytes};
36
37const MAX_SIGNATURE_VALUE_LEN: usize = 8192;
38const MAX_SIGNATURE_VALUE_TEXT_LEN: usize = 65_536;
39/// Cryptographic verifier used by [`VerifyContext`].
40///
41/// This trait intentionally has no `Send + Sync` supertraits so lightweight
42/// single-threaded verifiers can be used without additional bounds.
43pub trait VerifyingKey {
44    /// Verify `signature_value` over `signed_data` with the declared algorithm.
45    fn verify(
46        &self,
47        algorithm: SignatureAlgorithm,
48        signed_data: &[u8],
49        signature_value: &[u8],
50    ) -> Result<bool, DsigError>;
51}
52
53/// Key resolver hook used by [`VerifyContext`] when no pre-set key is provided.
54///
55/// This trait intentionally has no `Send + Sync` supertraits; callers that need
56/// cross-thread sharing can wrap resolvers/keys in their own thread-safe types.
57pub trait KeyResolver {
58    /// Resolve a verification key from parsed `<KeyInfo>` sources.
59    ///
60    /// Return `Ok(None)` when no suitable key could be resolved from available
61    /// key material (for example, missing `<KeyInfo>` candidates). `VerifyContext`
62    /// maps `Ok(None)` to `DsigStatus::Invalid(FailureReason::KeyNotFound)`;
63    /// reserve `Err(...)` for resolver failures.
64    fn resolve<'a>(
65        &'a self,
66        key_info: Option<&KeyInfo>,
67        algorithm: SignatureAlgorithm,
68    ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError>;
69
70    /// Return `true` when this resolver consumes document `<KeyInfo>` material.
71    ///
72    /// The verification pipeline uses this to decide whether malformed
73    /// `<KeyInfo>` should raise `DsigError::ParseKeyInfo` before resolver
74    /// execution. Resolvers that ignore document key material can keep the
75    /// default `false` to avoid fail-closed parsing on advisory `<KeyInfo>`.
76    fn consumes_document_key_info(&self) -> bool {
77        false
78    }
79}
80
81/// Allowed URI classes for `<Reference URI="...">`.
82///
83/// Note: `UriReferenceResolver` currently supports only same-document URIs.
84/// Allowing external URIs via this policy only disables the early policy
85/// rejection; dereference still fails until an external resolver path is added.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87#[must_use = "pass the policy to VerifyContext::allowed_uri_types(), or store it for reuse"]
88pub struct UriTypeSet {
89    allow_empty: bool,
90    allow_same_document: bool,
91    allow_external: bool,
92}
93
94impl UriTypeSet {
95    /// Create a custom URI policy.
96    pub const fn new(allow_empty: bool, allow_same_document: bool, allow_external: bool) -> Self {
97        Self {
98            allow_empty,
99            allow_same_document,
100            allow_external,
101        }
102    }
103
104    /// Allow only same-document references (`""`, `#id`, `#xpointer(...)`).
105    pub const SAME_DOCUMENT: Self = Self {
106        allow_empty: true,
107        allow_same_document: true,
108        allow_external: false,
109    };
110
111    /// Allow all URI classes.
112    ///
113    /// This includes external URI classes at policy level, but external
114    /// dereference is not implemented yet by the default resolver.
115    pub const ALL: Self = Self {
116        allow_empty: true,
117        allow_same_document: true,
118        allow_external: true,
119    };
120
121    fn allows(self, uri: &str) -> bool {
122        if uri.is_empty() {
123            return self.allow_empty;
124        }
125        if uri.starts_with('#') {
126            return self.allow_same_document;
127        }
128        self.allow_external
129    }
130}
131
132impl Default for UriTypeSet {
133    fn default() -> Self {
134        Self::SAME_DOCUMENT
135    }
136}
137
138/// Verification builder/configuration.
139#[must_use = "configure the context and call verify(), or store it for reuse"]
140pub struct VerifyContext<'a> {
141    key: Option<&'a dyn VerifyingKey>,
142    key_resolver: Option<&'a dyn KeyResolver>,
143    process_manifests: bool,
144    allowed_uri_types: UriTypeSet,
145    allowed_transforms: Option<HashSet<String>>,
146    store_pre_digest: bool,
147    transform_options: TransformOptions,
148}
149
150impl<'a> VerifyContext<'a> {
151    /// Create a context with conservative defaults.
152    ///
153    /// Defaults:
154    /// - no pre-set key, no key resolver
155    /// - manifests disabled
156    /// - same-document URIs only
157    /// - all transforms allowed
158    /// - pre-digest buffers not stored
159    pub fn new() -> Self {
160        Self {
161            key: None,
162            key_resolver: None,
163            process_manifests: false,
164            allowed_uri_types: UriTypeSet::default(),
165            allowed_transforms: None,
166            store_pre_digest: false,
167            transform_options: TransformOptions::default(),
168        }
169    }
170
171    /// Set a pre-resolved verification key.
172    pub fn key(mut self, key: &'a dyn VerifyingKey) -> Self {
173        self.key = Some(key);
174        self
175    }
176
177    /// Set a key resolver fallback used when `key()` is not provided.
178    pub fn key_resolver(mut self, resolver: &'a dyn KeyResolver) -> Self {
179        self.key_resolver = Some(resolver);
180        self
181    }
182
183    /// Enable or disable `<Manifest>` processing.
184    ///
185    /// When enabled, references in `<ds:Manifest>` elements that are direct
186    /// element children of `<ds:Object>` are processed only when the direct-child
187    /// `<ds:Object>` or `<ds:Manifest>` itself is referenced from `<SignedInfo>`
188    /// by an ID-based same-document fragment URI such as `#id` or
189    /// `#xpointer(id('id'))`, and that reference uses only canonicalization
190    /// transforms (or implicit canonicalization). Filtering or binary transforms
191    /// do not prove that the complete Manifest structure was authenticated.
192    /// Only those signed Manifest references are returned in
193    /// `VerifyResult::manifest_references`.
194    /// Manifest parsing begins only after every `<SignedInfo>` reference digest
195    /// validates; a failure returns immediately with no Manifest results.
196    /// Nested `<ds:Manifest>` descendants under `<ds:Object>` are not
197    /// processed.
198    /// Direct-child unsigned/unreferenced Manifests are skipped and do not
199    /// appear in `VerifyResult::manifest_references`.
200    /// Whole-document same-document references such as `URI=""` or
201    /// `URI="#xpointer(/)"` do not mark a specific direct-child
202    /// `<ds:Object>`/`<ds:Manifest>` as signed for this option.
203    ///
204    /// Manifests are parsed and processed only after the SignedInfo references
205    /// and SignatureValue both validate. Their digest mismatches, policy
206    /// violations, and processing failures are then reported independently in
207    /// `VerifyResult::manifest_references` and do not alter `VerifyResult::status`.
208    /// Callers that enable `process_manifests(true)` must inspect
209    /// `VerifyResult::manifest_references` in addition to `VerifyResult::status`
210    /// when interpreting `verify()` results.
211    /// Structural/parse errors in Manifest content abort `verify()` and are
212    /// returned as `Err(...)`.
213    pub fn process_manifests(mut self, enabled: bool) -> Self {
214        self.process_manifests = enabled;
215        self
216    }
217
218    /// Restrict allowed reference URI classes.
219    pub fn allowed_uri_types(mut self, types: UriTypeSet) -> Self {
220        self.allowed_uri_types = types;
221        self
222    }
223
224    /// Restrict allowed transform algorithms by URI.
225    ///
226    /// Example values:
227    /// - `http://www.w3.org/2000/09/xmldsig#enveloped-signature`
228    /// - `http://www.w3.org/2001/10/xml-exc-c14n#`
229    ///
230    /// When a `<Reference>` has no explicit canonicalization transform, XMLDSig
231    /// applies implicit default C14N (`http://www.w3.org/TR/2001/REC-xml-c14n-20010315`).
232    /// If an allowlist is configured, include that URI as well unless all
233    /// references use explicit `Transform::C14n(...)`.
234    pub fn allowed_transforms<I, S>(mut self, transforms: I) -> Self
235    where
236        I: IntoIterator<Item = S>,
237        S: Into<String>,
238    {
239        self.allowed_transforms = Some(transforms.into_iter().map(Into::into).collect());
240        self
241    }
242
243    /// Store pre-digest buffers for diagnostics.
244    pub fn store_pre_digest(mut self, enabled: bool) -> Self {
245        self.store_pre_digest = enabled;
246        self
247    }
248
249    /// Select the node returned by XPath's `here()` extension function.
250    ///
251    /// The default follows XMLDSig and returns the `<XPath>` parameter.
252    /// Use [`XPathHereSemantics::XmlSecLegacy`] only for documents known to
253    /// have been generated with libxmlsec1's `<Transform>` interpretation.
254    pub fn xpath_here_semantics(mut self, semantics: XPathHereSemantics) -> Self {
255        self.transform_options = self.transform_options.xpath_here_semantics(semantics);
256        self
257    }
258
259    fn allowed_transform_uris(&self) -> Option<&HashSet<String>> {
260        self.allowed_transforms.as_ref()
261    }
262
263    /// Verify one XMLDSig signature using this context.
264    ///
265    /// Returns `Ok(VerifyResult)` for both valid and invalid signatures; inspect
266    /// `VerifyResult::status` for the core `<SignedInfo>` and signature-value
267    /// outcome. When Manifest processing is enabled, inspect every
268    /// `VerifyResult::manifest_references` entry separately. `Err(...)` is
269    /// reserved for pipeline failures.
270    pub fn verify(&self, xml: &str) -> Result<VerifyResult, DsigError> {
271        verify_signature_with_context(xml, self)
272    }
273}
274
275impl Default for VerifyContext<'_> {
276    fn default() -> Self {
277        Self::new()
278    }
279}
280
281/// Per-reference verification result.
282#[derive(Debug)]
283#[non_exhaustive]
284#[must_use = "inspect status before accepting the reference result"]
285pub struct ReferenceResult {
286    /// Whether this reference came from `<SignedInfo>` or `<Manifest>`.
287    pub reference_set: ReferenceSet,
288    /// Zero-based index within `reference_set`.
289    pub reference_index: usize,
290    /// URI from the `<Reference>` element (for diagnostics).
291    pub uri: String,
292    /// Digest algorithm used.
293    pub digest_algorithm: DigestAlgorithm,
294    /// Reference verification status.
295    pub status: DsigStatus,
296    /// Pre-digest bytes (populated when `store_pre_digest` is enabled).
297    pub pre_digest_data: Option<Vec<u8>>,
298}
299
300/// Origin of a processed `<Reference>`.
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
302#[non_exhaustive]
303pub enum ReferenceSet {
304    /// `<Reference>` under `<SignedInfo>`.
305    SignedInfo,
306    /// `<Reference>` under `<Object>/<Manifest>`.
307    Manifest,
308}
309
310/// Verification status.
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312#[non_exhaustive]
313pub enum DsigStatus {
314    /// Signature/reference is cryptographically valid.
315    Valid,
316    /// Signature/reference is invalid with a concrete reason.
317    Invalid(FailureReason),
318}
319
320/// Why XMLDSig verification failed.
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322#[non_exhaustive]
323pub enum FailureReason {
324    /// `<DigestValue>` mismatch for a `<Reference>` at `ref_index`.
325    ReferenceDigestMismatch {
326        /// Zero-based index of the failing `<Reference>` in its processed set.
327        ///
328        /// On per-reference verification entries, use
329        /// `ReferenceResult::reference_set` to distinguish the `<SignedInfo>`
330        /// and `<Manifest>` reference sets.
331        ///
332        /// When this reason appears in `VerifyResult::status` without an
333        /// accompanying `ReferenceResult`, `ref_index` always refers to the
334        /// `<SignedInfo>` reference set.
335        ref_index: usize,
336    },
337    /// `<Reference>` rejected by URI/transform allowlist policy.
338    ReferencePolicyViolation {
339        /// Zero-based index of the failing `<Reference>` in its processed set.
340        ref_index: usize,
341    },
342    /// `<Reference>` processing failed (dereference, transform, missing URI).
343    ReferenceProcessingFailure {
344        /// Zero-based index of the failing `<Reference>` in its processed set.
345        ref_index: usize,
346    },
347    /// `<SignatureValue>` does not match canonicalized `<SignedInfo>`.
348    SignatureMismatch,
349    /// No verification key was configured or could be resolved.
350    KeyNotFound,
351}
352
353/// Result of processing all `<Reference>` elements in `<SignedInfo>`.
354#[derive(Debug)]
355#[non_exhaustive]
356#[must_use = "check first_failure/results before accepting the reference set"]
357pub struct ReferencesResult {
358    /// Per-reference results (one per `<Reference>` in order).
359    /// On fail-fast, only references up to and including the failed one are present.
360    pub results: Vec<ReferenceResult>,
361    /// Index of the first failed reference, if any.
362    pub first_failure: Option<usize>,
363}
364
365impl ReferencesResult {
366    /// Whether all references passed digest verification.
367    #[must_use]
368    pub fn all_valid(&self) -> bool {
369        self.results
370            .iter()
371            .all(|result| matches!(result.status, DsigStatus::Valid))
372    }
373}
374
375/// Process a single `<Reference>`: dereference URI → apply transforms → compute
376/// digest → compare with stored `<DigestValue>`.
377///
378/// # Arguments
379///
380/// - `reference`: The parsed `<Reference>` element.
381/// - `resolver`: URI resolver for the document.
382/// - `signature_node`: The `<Signature>` element (for enveloped-signature transform).
383/// - `reference_set`: Whether this reference belongs to `<SignedInfo>` or `<Manifest>`.
384/// - `reference_index`: Zero-based index of this reference inside `reference_set`.
385/// - `store_pre_digest`: If true, store the pre-digest bytes in the result.
386///
387/// # Errors
388///
389/// Returns `Err` for processing failures (URI dereference, transform errors).
390/// Digest mismatch is NOT an error — it produces
391/// `Ok(ReferenceResult { status: Invalid(ReferenceDigestMismatch { .. }) })`.
392pub fn process_reference(
393    reference: &Reference,
394    resolver: &UriReferenceResolver<'_>,
395    signature_node: Node<'_, '_>,
396    reference_set: ReferenceSet,
397    reference_index: usize,
398    store_pre_digest: bool,
399) -> Result<ReferenceResult, ReferenceProcessingError> {
400    let execution_budget = TransformExecutionBudget::default();
401    let execution = ReferenceExecutionContext {
402        store_pre_digest,
403        transform_options: TransformOptions::default(),
404        transform_budget: &execution_budget,
405    };
406    process_reference_with_options(
407        reference,
408        resolver,
409        signature_node,
410        reference_set,
411        reference_index,
412        &execution,
413    )
414}
415
416struct ReferenceExecutionContext<'a> {
417    store_pre_digest: bool,
418    transform_options: TransformOptions,
419    transform_budget: &'a TransformExecutionBudget,
420}
421
422fn process_reference_with_options(
423    reference: &Reference,
424    resolver: &UriReferenceResolver<'_>,
425    signature_node: Node<'_, '_>,
426    reference_set: ReferenceSet,
427    reference_index: usize,
428    execution: &ReferenceExecutionContext<'_>,
429) -> Result<ReferenceResult, ReferenceProcessingError> {
430    // 1. Dereference URI. Omitted URI is distinct from URI="" in XMLDSig and
431    // must be rejected until caller-provided external object resolution exists.
432    let uri = reference
433        .uri
434        .as_deref()
435        .ok_or(ReferenceProcessingError::MissingUri)?;
436    let initial_data = resolver
437        .dereference_with_budget(uri, execution.transform_budget.node_set_materialization())
438        .map_err(ReferenceProcessingError::UriDereference)?;
439
440    // 2. Apply transform chain
441    let pre_digest_bytes = execute_transforms_with_options_and_budget(
442        signature_node,
443        initial_data,
444        &reference.transforms,
445        execution.transform_options,
446        execution.transform_budget,
447    )
448    .map_err(ReferenceProcessingError::Transform)?;
449
450    // 3. Compute digest
451    let computed_digest = compute_digest(reference.digest_method, &pre_digest_bytes);
452
453    // 4. Compare with stored DigestValue (constant-time)
454    let status = if constant_time_eq(&computed_digest, &reference.digest_value) {
455        DsigStatus::Valid
456    } else {
457        DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch {
458            ref_index: reference_index,
459        })
460    };
461
462    Ok(ReferenceResult {
463        reference_set,
464        reference_index,
465        uri: uri.to_owned(),
466        digest_algorithm: reference.digest_method,
467        status,
468        pre_digest_data: if execution.store_pre_digest {
469            Some(pre_digest_bytes)
470        } else {
471            None
472        },
473    })
474}
475
476/// Process all `<Reference>` elements in a `<SignedInfo>`, with fail-fast
477/// on the first digest mismatch.
478///
479/// Per XMLDSig spec: if any reference fails, the entire signature is invalid.
480/// Processing stops at the first failure for efficiency.
481///
482/// # Errors
483///
484/// Returns `Err` only for processing failures (malformed XML, unsupported
485/// transform, etc.). Digest mismatches are reported via
486/// `ReferencesResult::first_failure`.
487pub fn process_all_references(
488    references: &[Reference],
489    resolver: &UriReferenceResolver<'_>,
490    signature_node: Node<'_, '_>,
491    store_pre_digest: bool,
492) -> Result<ReferencesResult, ReferenceProcessingError> {
493    let execution_budget = TransformExecutionBudget::default();
494    let execution = ReferenceExecutionContext {
495        store_pre_digest,
496        transform_options: TransformOptions::default(),
497        transform_budget: &execution_budget,
498    };
499    process_all_references_with_options(references, resolver, signature_node, &execution)
500}
501
502fn process_all_references_with_options(
503    references: &[Reference],
504    resolver: &UriReferenceResolver<'_>,
505    signature_node: Node<'_, '_>,
506    execution: &ReferenceExecutionContext<'_>,
507) -> Result<ReferencesResult, ReferenceProcessingError> {
508    let mut results = Vec::with_capacity(references.len());
509
510    for (i, reference) in references.iter().enumerate() {
511        let result = process_reference_with_options(
512            reference,
513            resolver,
514            signature_node,
515            ReferenceSet::SignedInfo,
516            i,
517            execution,
518        )?;
519        let failed = matches!(result.status, DsigStatus::Invalid(_));
520        results.push(result);
521
522        if failed {
523            return Ok(ReferencesResult {
524                results,
525                first_failure: Some(i),
526            });
527        }
528    }
529
530    Ok(ReferencesResult {
531        results,
532        first_failure: None,
533    })
534}
535
536/// Errors during reference processing.
537///
538/// Distinct from digest mismatch (which is a validation result, not a processing error).
539#[derive(Debug, thiserror::Error)]
540#[non_exhaustive]
541pub enum ReferenceProcessingError {
542    /// `<Reference>` omitted the `URI` attribute, which we do not resolve implicitly.
543    #[error("reference URI is required; omitted URI references are not supported")]
544    MissingUri,
545
546    /// URI dereference failed.
547    #[error("URI dereference failed: {0}")]
548    UriDereference(#[source] super::types::TransformError),
549
550    /// Transform execution failed.
551    #[error("transform failed: {0}")]
552    Transform(#[source] super::types::TransformError),
553}
554
555/// End-to-end XMLDSig verification result for one `<Signature>`.
556#[derive(Debug)]
557#[non_exhaustive]
558#[must_use = "inspect status before accepting the document"]
559pub struct VerifyResult {
560    /// Core XMLDSig status for the `<SignedInfo>` references and signature value.
561    ///
562    /// Manifest reference failures do not alter this field; inspect
563    /// [`Self::manifest_references`] before accepting Manifest-backed data.
564    pub status: DsigStatus,
565    /// `<Reference>` verification results from `<SignedInfo>`.
566    /// On fail-fast, this includes references up to and including
567    /// the first digest mismatch only.
568    pub signed_info_references: Vec<ReferenceResult>,
569    /// `<Manifest>` reference results.
570    /// Populated only when `VerifyContext::process_manifests(true)` is enabled
571    /// and core signature validation succeeds.
572    /// Includes only references from signed direct-child `<ds:Object>/<ds:Manifest>`
573    /// blocks that are referenced from `<SignedInfo>`.
574    /// Each entry has an independent status that does not alter [`Self::status`].
575    /// Callers must inspect every entry before accepting Manifest-backed data.
576    /// Unsigned/unreferenced direct-child Manifest blocks are skipped, so an
577    /// empty list does not imply that no Manifest elements existed in `verify()` input.
578    pub manifest_references: Vec<ReferenceResult>,
579    /// Canonicalized `<SignedInfo>` bytes when `store_pre_digest` is enabled
580    /// and verification reaches SignedInfo canonicalization.
581    pub canonicalized_signed_info: Option<Vec<u8>>,
582}
583
584/// Errors while running end-to-end XMLDSig verification.
585#[derive(Debug, thiserror::Error)]
586#[non_exhaustive]
587pub enum DsigError {
588    /// XML parsing failed.
589    #[error("XML parse error: {0}")]
590    XmlParse(#[from] roxmltree::Error),
591
592    /// Required signature element is missing.
593    #[error("missing required element: <{element}>")]
594    MissingElement {
595        /// Name of the missing element.
596        element: &'static str,
597    },
598
599    /// Signature element tree shape violates XMLDSig structure requirements.
600    #[error("invalid Signature structure: {reason}")]
601    InvalidStructure {
602        /// Validation failure reason.
603        reason: &'static str,
604    },
605
606    /// `<SignedInfo>` parsing failed.
607    #[error("failed to parse SignedInfo: {0}")]
608    ParseSignedInfo(#[from] super::parse::ParseError),
609
610    /// `<KeyInfo>` parsing failed.
611    #[error("failed to parse KeyInfo: {0}")]
612    ParseKeyInfo(#[source] super::parse::ParseError),
613
614    /// Configuration-driven key resolution failed.
615    #[error("key resolution failed: {0}")]
616    KeyResolution(#[from] super::keys::KeyResolutionError),
617
618    /// `<Object>/<Manifest>/<Reference>` parsing failed.
619    #[error("failed to parse Manifest reference: {0}")]
620    ParseManifestReference(#[source] ParseError),
621
622    /// Reference processing failed.
623    #[error("reference processing failed: {0}")]
624    Reference(#[from] ReferenceProcessingError),
625
626    /// SignedInfo canonicalization failed.
627    #[error("SignedInfo canonicalization failed: {0}")]
628    Canonicalization(#[from] crate::c14n::C14nError),
629
630    /// SignatureValue base64 decoding failed.
631    #[error("invalid SignatureValue base64: {0}")]
632    SignatureValueBase64(#[from] base64::DecodeError),
633
634    /// Cryptographic verification failed before validity decision.
635    #[error("signature verification failed: {0}")]
636    Crypto(#[from] SignatureVerificationError),
637
638    /// A `<Reference>` URI class is rejected by policy.
639    #[error("reference URI is not allowed by policy: {uri}")]
640    DisallowedUri {
641        /// Offending URI value from `<Reference URI="...">`.
642        uri: String,
643    },
644
645    /// A `<Transform>` algorithm is rejected by policy.
646    #[error("transform is not allowed by policy: {algorithm}")]
647    DisallowedTransform {
648        /// Rejected transform algorithm URI.
649        algorithm: String,
650    },
651}
652
653type SignatureVerificationPipelineError = DsigError;
654
655/// Verify one XMLDSig `<Signature>` end-to-end with a PEM public key.
656///
657/// Pipeline:
658/// 1. Parse `<Signature>` children and enforce structural constraints
659/// 2. Parse `<SignedInfo>`
660/// 3. Validate all `<Reference>` digests (fail-fast)
661/// 4. Canonicalize `<SignedInfo>`
662/// 5. Base64-decode `<SignatureValue>`
663/// 6. Verify signature bytes against canonicalized `<SignedInfo>` using the provided PEM key
664///
665/// If any `<Reference>` digest mismatches, returns `Ok` with
666/// `status == Invalid(ReferenceDigestMismatch { .. })`.
667///
668/// This API uses only the provided PEM key and does not parse embedded
669/// `<KeyInfo>` key material for key selection/validation. Consequently,
670/// malformed optional `<KeyInfo>` does not produce `DsigError::ParseKeyInfo`
671/// on this API path.
672///
673/// Structural constraints enforced by this API:
674/// - The document must contain exactly one XMLDSig `<Signature>` element.
675/// - `<SignedInfo>` must be the first element child of `<Signature>` and appear once.
676/// - `<SignatureValue>` must be the second element child of `<Signature>` and appear once.
677/// - `<KeyInfo>` is optional and, when present, must be the third element child.
678/// - Only XMLDSig namespace element children are allowed under `<Signature>`.
679/// - Non-whitespace mixed text content under `<Signature>` is rejected.
680/// - After `<SignedInfo>`, `<SignatureValue>`, and optional `<KeyInfo>`, only `<Object>` elements are allowed.
681/// - `<SignatureValue>` must not contain nested element children.
682pub fn verify_signature_with_pem_key(
683    xml: &str,
684    public_key_pem: &str,
685    store_pre_digest: bool,
686) -> Result<VerifyResult, DsigError> {
687    struct PemVerifyingKey<'a> {
688        public_key_pem: &'a str,
689    }
690
691    impl VerifyingKey for PemVerifyingKey<'_> {
692        fn verify(
693            &self,
694            algorithm: SignatureAlgorithm,
695            signed_data: &[u8],
696            signature_value: &[u8],
697        ) -> Result<bool, DsigError> {
698            verify_with_algorithm(algorithm, self.public_key_pem, signed_data, signature_value)
699        }
700    }
701
702    let key = PemVerifyingKey { public_key_pem };
703    VerifyContext::new()
704        .key(&key)
705        .store_pre_digest(store_pre_digest)
706        .verify(xml)
707}
708
709fn verify_signature_with_context(
710    xml: &str,
711    ctx: &VerifyContext<'_>,
712) -> Result<VerifyResult, SignatureVerificationPipelineError> {
713    let doc = Document::parse(xml)?;
714    let mut signatures = doc.descendants().filter(|node| {
715        node.is_element()
716            && node.tag_name().name() == "Signature"
717            && node.tag_name().namespace() == Some(XMLDSIG_NS)
718    });
719    let signature_node = match (signatures.next(), signatures.next()) {
720        (None, _) => {
721            return Err(SignatureVerificationPipelineError::MissingElement {
722                element: "Signature",
723            });
724        }
725        (Some(node), None) => node,
726        (Some(_), Some(_)) => {
727            return Err(SignatureVerificationPipelineError::InvalidStructure {
728                reason: "Signature must appear exactly once in document",
729            });
730        }
731    };
732
733    let signature_children = parse_signature_children(signature_node)?;
734    let signed_info_node = signature_children.signed_info_node;
735    let should_parse_key_info = match (ctx.key, ctx.key_resolver) {
736        (Some(_), _) => false,
737        (None, Some(resolver)) => resolver.consumes_document_key_info(),
738        (None, None) => true,
739    };
740    let key_info = if should_parse_key_info {
741        signature_children
742            .key_info_node
743            .map(parse_key_info)
744            .transpose()
745            .map_err(SignatureVerificationPipelineError::ParseKeyInfo)?
746    } else {
747        None
748    };
749
750    let mut xpath_parse_budget = XPathSignatureParseBudget::default();
751    let signed_info =
752        parse_signed_info_with_xpath_budget(signed_info_node, &mut xpath_parse_budget)?;
753    enforce_reference_policies(
754        &signed_info.references,
755        ctx.allowed_uri_types,
756        ctx.allowed_transform_uris(),
757    )?;
758
759    let resolver = UriReferenceResolver::new(&doc);
760    let execution_budget = TransformExecutionBudget::default();
761    let execution = ReferenceExecutionContext {
762        store_pre_digest: ctx.store_pre_digest,
763        transform_options: ctx.transform_options,
764        transform_budget: &execution_budget,
765    };
766    let references = process_all_references_with_options(
767        &signed_info.references,
768        &resolver,
769        signature_node,
770        &execution,
771    )?;
772
773    if let Some(first_failure) = references.first_failure {
774        let status = references.results[first_failure].status;
775        return Ok(VerifyResult {
776            status,
777            signed_info_references: references.results,
778            manifest_references: Vec::new(),
779            canonicalized_signed_info: None,
780        });
781    }
782
783    let signed_info_subtree: HashSet<_> = signed_info_node
784        .descendants()
785        .map(|node: Node<'_, '_>| node.id())
786        .collect();
787    let mut canonical_signed_info = Vec::new();
788    canonicalize(
789        &doc,
790        Some(&|node| signed_info_subtree.contains(&node.id())),
791        &signed_info.c14n_method,
792        &mut canonical_signed_info,
793    )?;
794
795    let signature_value = decode_signature_value(signature_children.signature_value_node)?;
796    let Some(resolved_key) =
797        resolve_verifying_key(ctx, key_info.as_ref(), signed_info.signature_method)?
798    else {
799        return Ok(VerifyResult {
800            status: DsigStatus::Invalid(FailureReason::KeyNotFound),
801            signed_info_references: references.results,
802            manifest_references: Vec::new(),
803            canonicalized_signed_info: if ctx.store_pre_digest {
804                Some(canonical_signed_info)
805            } else {
806                None
807            },
808        });
809    };
810    let verifier = resolved_key.as_ref();
811    let signature_valid = verifier.verify(
812        signed_info.signature_method,
813        &canonical_signed_info,
814        &signature_value,
815    )?;
816
817    if !signature_valid {
818        return Ok(VerifyResult {
819            status: DsigStatus::Invalid(FailureReason::SignatureMismatch),
820            signed_info_references: references.results,
821            manifest_references: Vec::new(),
822            canonicalized_signed_info: if ctx.store_pre_digest {
823                Some(canonical_signed_info)
824            } else {
825                None
826            },
827        });
828    }
829
830    let manifest_references = if ctx.process_manifests {
831        let signed_info_reference_nodes =
832            collect_authenticated_signed_info_reference_nodes(&signed_info.references, &resolver);
833        process_manifest_references(
834            signature_node,
835            &resolver,
836            ctx,
837            &signed_info_reference_nodes,
838            &execution,
839            &mut xpath_parse_budget,
840        )?
841    } else {
842        Vec::new()
843    };
844
845    Ok(VerifyResult {
846        status: DsigStatus::Valid,
847        signed_info_references: references.results,
848        manifest_references,
849        canonicalized_signed_info: if ctx.store_pre_digest {
850            Some(canonical_signed_info)
851        } else {
852            None
853        },
854    })
855}
856
857fn process_manifest_references(
858    signature_node: Node<'_, '_>,
859    resolver: &UriReferenceResolver<'_>,
860    ctx: &VerifyContext<'_>,
861    signed_info_reference_nodes: &HashSet<NodeId>,
862    execution: &ReferenceExecutionContext<'_>,
863    xpath_parse_budget: &mut XPathSignatureParseBudget,
864) -> Result<Vec<ReferenceResult>, SignatureVerificationPipelineError> {
865    let manifest_references = parse_manifest_references(
866        signature_node,
867        signed_info_reference_nodes,
868        xpath_parse_budget,
869    )?;
870    if manifest_references.is_empty() {
871        return Ok(Vec::new());
872    }
873    let mut results = Vec::with_capacity(manifest_references.len());
874    for (index, reference) in manifest_references.iter().enumerate() {
875        match enforce_reference_policies(
876            std::slice::from_ref(reference),
877            ctx.allowed_uri_types,
878            ctx.allowed_transform_uris(),
879        ) {
880            Ok(()) => {}
881            Err(
882                SignatureVerificationPipelineError::DisallowedUri { .. }
883                | SignatureVerificationPipelineError::DisallowedTransform { .. },
884            ) => {
885                results.push(manifest_reference_invalid_result(
886                    reference,
887                    index,
888                    FailureReason::ReferencePolicyViolation { ref_index: index },
889                ));
890                continue;
891            }
892            Err(SignatureVerificationPipelineError::Reference(
893                ReferenceProcessingError::MissingUri,
894            )) => {
895                results.push(manifest_reference_invalid_result(
896                    reference,
897                    index,
898                    FailureReason::ReferenceProcessingFailure { ref_index: index },
899                ));
900                continue;
901            }
902            Err(_) => {
903                // Defensive fallback for future enforce_reference_policies variants:
904                // record as non-fatal per-reference processing failure instead of aborting.
905                results.push(manifest_reference_invalid_result(
906                    reference,
907                    index,
908                    FailureReason::ReferenceProcessingFailure { ref_index: index },
909                ));
910                continue;
911            }
912        }
913
914        match process_reference_with_options(
915            reference,
916            resolver,
917            signature_node,
918            ReferenceSet::Manifest,
919            index,
920            execution,
921        ) {
922            Ok(result) => results.push(result),
923            Err(_) => results.push(manifest_reference_invalid_result(
924                reference,
925                index,
926                FailureReason::ReferenceProcessingFailure { ref_index: index },
927            )),
928        }
929    }
930    Ok(results)
931}
932
933fn manifest_reference_invalid_result(
934    reference: &Reference,
935    index: usize,
936    reason: FailureReason,
937) -> ReferenceResult {
938    ReferenceResult {
939        reference_set: ReferenceSet::Manifest,
940        reference_index: index,
941        uri: reference
942            .uri
943            .clone()
944            .unwrap_or_else(|| "<omitted>".to_owned()),
945        digest_algorithm: reference.digest_method,
946        status: DsigStatus::Invalid(reason),
947        pre_digest_data: None,
948    }
949}
950
951fn parse_manifest_references(
952    signature_node: Node<'_, '_>,
953    signed_info_reference_nodes: &HashSet<NodeId>,
954    xpath_parse_budget: &mut XPathSignatureParseBudget,
955) -> Result<Vec<Reference>, SignatureVerificationPipelineError> {
956    let mut references = Vec::new();
957    for object_node in signature_node.children().filter(|node| {
958        node.is_element()
959            && node.tag_name().namespace() == Some(XMLDSIG_NS)
960            && node.tag_name().name() == "Object"
961    }) {
962        let object_is_signed = signed_info_reference_nodes.contains(&object_node.id());
963        for manifest_node in object_node.children().filter(|node| {
964            node.is_element()
965                && node.tag_name().namespace() == Some(XMLDSIG_NS)
966                && node.tag_name().name() == "Manifest"
967        }) {
968            let manifest_is_signed = signed_info_reference_nodes.contains(&manifest_node.id());
969            if !object_is_signed && !manifest_is_signed {
970                continue;
971            }
972            let mut manifest_children = Vec::new();
973            for child in manifest_node.children() {
974                if child.is_text()
975                    && child.text().is_some_and(|text| {
976                        text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
977                    })
978                {
979                    return Err(SignatureVerificationPipelineError::InvalidStructure {
980                        reason: "Manifest contains non-whitespace mixed content",
981                    });
982                }
983                if child.is_element() {
984                    manifest_children.push(child);
985                }
986            }
987            if manifest_children.is_empty() {
988                return Err(SignatureVerificationPipelineError::InvalidStructure {
989                    reason: "Manifest must contain at least one ds:Reference element child",
990                });
991            }
992            for child in manifest_children {
993                if child.tag_name().namespace() != Some(XMLDSIG_NS)
994                    || child.tag_name().name() != "Reference"
995                {
996                    return Err(SignatureVerificationPipelineError::InvalidStructure {
997                        reason: "Manifest must contain only ds:Reference element children",
998                    });
999                }
1000                if references.len() == MAX_REFERENCES_PER_SIGNATURE {
1001                    return Err(SignatureVerificationPipelineError::InvalidStructure {
1002                        reason: "signed Manifests exceed the per-signature Reference limit",
1003                    });
1004                }
1005                references.push(
1006                    parse_reference_with_xpath_budget(child, xpath_parse_budget)
1007                        .map_err(SignatureVerificationPipelineError::ParseManifestReference)?,
1008                );
1009            }
1010        }
1011    }
1012    Ok(references)
1013}
1014
1015fn collect_authenticated_signed_info_reference_nodes(
1016    references: &[Reference],
1017    resolver: &UriReferenceResolver<'_>,
1018) -> HashSet<NodeId> {
1019    references
1020        .iter()
1021        // URI dereference identifies the transform input, not necessarily the
1022        // bytes authenticated by its digest. Every transform must preserve the
1023        // complete XML structure needed to trust and process a Manifest.
1024        .filter(|reference| {
1025            reference
1026                .transforms
1027                .iter()
1028                .all(transform_preserves_manifest_structure)
1029        })
1030        .filter_map(|reference| reference.uri.as_deref())
1031        .filter_map(signed_info_reference_id_from_uri)
1032        .filter_map(|id| resolver.node_id_for_id(id))
1033        .collect()
1034}
1035
1036fn transform_preserves_manifest_structure(transform: &Transform) -> bool {
1037    match transform {
1038        Transform::C14n(_) => true,
1039        // Both eligible ID targets are descendants of the owning Signature.
1040        // Enveloped subtraction therefore removes their intersection with that
1041        // Signature subtree, even though the Signature node itself is not in the
1042        // dereferenced node set.
1043        Transform::Enveloped
1044        | Transform::XpathExcludeAllSignatures
1045        | Transform::XPath(_)
1046        | Transform::XPathFilter2(_)
1047        | Transform::Base64Decode => false,
1048    }
1049}
1050
1051fn signed_info_reference_id_from_uri(uri: &str) -> Option<&str> {
1052    let fragment = uri.strip_prefix('#')?;
1053    if fragment.is_empty() || fragment == "xpointer(/)" {
1054        return None;
1055    }
1056    if let Some(id) = parse_xpointer_id_fragment(fragment) {
1057        return (!id.is_empty()).then_some(id);
1058    }
1059    (!fragment.starts_with("xpointer(")).then_some(fragment)
1060}
1061
1062enum ResolvedVerifyingKey<'a> {
1063    Borrowed(&'a dyn VerifyingKey),
1064    Owned(Box<dyn VerifyingKey + 'a>),
1065}
1066
1067impl ResolvedVerifyingKey<'_> {
1068    fn as_ref(&self) -> &dyn VerifyingKey {
1069        match self {
1070            Self::Borrowed(key) => *key,
1071            Self::Owned(key) => key.as_ref(),
1072        }
1073    }
1074}
1075
1076fn resolve_verifying_key<'k>(
1077    ctx: &VerifyContext<'k>,
1078    key_info: Option<&KeyInfo>,
1079    algorithm: SignatureAlgorithm,
1080) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
1081    if let Some(key) = ctx.key {
1082        return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
1083    }
1084    if let Some(resolver) = ctx.key_resolver {
1085        let resolved = resolver.resolve(key_info, algorithm)?;
1086        return Ok(resolved.map(ResolvedVerifyingKey::Owned));
1087    }
1088    Ok(None)
1089}
1090
1091fn enforce_reference_policies(
1092    references: &[Reference],
1093    allowed_uri_types: UriTypeSet,
1094    allowed_transforms: Option<&HashSet<String>>,
1095) -> Result<(), SignatureVerificationPipelineError> {
1096    for reference in references {
1097        let uri = reference
1098            .uri
1099            .as_deref()
1100            .ok_or(SignatureVerificationPipelineError::Reference(
1101                ReferenceProcessingError::MissingUri,
1102            ))?;
1103        if !allowed_uri_types.allows(uri) {
1104            return Err(SignatureVerificationPipelineError::DisallowedUri {
1105                uri: uri.to_owned(),
1106            });
1107        }
1108
1109        if let Some(allowed) = allowed_transforms {
1110            for transform in &reference.transforms {
1111                let transform_uri = transform_uri(transform);
1112                if !allowed.contains(transform_uri) {
1113                    return Err(SignatureVerificationPipelineError::DisallowedTransform {
1114                        algorithm: transform_uri.to_owned(),
1115                    });
1116                }
1117            }
1118
1119            let produces_binary = reference.transforms.last().is_some_and(|transform| {
1120                matches!(transform, Transform::C14n(_) | Transform::Base64Decode)
1121            });
1122            if !produces_binary && !allowed.contains(DEFAULT_IMPLICIT_C14N_URI) {
1123                return Err(SignatureVerificationPipelineError::DisallowedTransform {
1124                    algorithm: DEFAULT_IMPLICIT_C14N_URI.to_owned(),
1125                });
1126            }
1127        }
1128    }
1129    Ok(())
1130}
1131
1132fn transform_uri(transform: &Transform) -> &'static str {
1133    match transform {
1134        Transform::Enveloped => super::transforms::ENVELOPED_SIGNATURE_URI,
1135        Transform::XpathExcludeAllSignatures | Transform::XPath(_) => XPATH_TRANSFORM_URI,
1136        Transform::XPathFilter2(_) => super::transforms::XPATH_FILTER2_TRANSFORM_URI,
1137        Transform::C14n(algo) => algo.uri(),
1138        Transform::Base64Decode => BASE64_TRANSFORM_URI,
1139    }
1140}
1141
1142#[derive(Debug, Clone, Copy)]
1143struct SignatureChildNodes<'a, 'input> {
1144    signed_info_node: Node<'a, 'input>,
1145    signature_value_node: Node<'a, 'input>,
1146    key_info_node: Option<Node<'a, 'input>>,
1147}
1148
1149fn parse_signature_children<'a, 'input>(
1150    signature_node: Node<'a, 'input>,
1151) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
1152    let mut signed_info_node: Option<Node<'_, '_>> = None;
1153    let mut signature_value_node: Option<Node<'_, '_>> = None;
1154    let mut key_info_node: Option<Node<'_, '_>> = None;
1155    let mut signed_info_index: Option<usize> = None;
1156    let mut signature_value_index: Option<usize> = None;
1157    let mut key_info_index: Option<usize> = None;
1158    let mut first_unexpected_dsig_index: Option<usize> = None;
1159
1160    let mut element_index = 0usize;
1161    for child in signature_node.children() {
1162        if child.is_text() {
1163            if child
1164                .text()
1165                .is_some_and(|text| !is_xml_whitespace_only(text))
1166            {
1167                return Err(SignatureVerificationPipelineError::InvalidStructure {
1168                    reason: "Signature must not contain non-whitespace mixed content",
1169                });
1170            }
1171            continue;
1172        }
1173        if !child.is_element() {
1174            continue;
1175        }
1176
1177        element_index += 1;
1178        if child.tag_name().namespace() != Some(XMLDSIG_NS) {
1179            return Err(SignatureVerificationPipelineError::InvalidStructure {
1180                reason: "Signature must contain only XMLDSIG element children",
1181            });
1182        }
1183        match child.tag_name().name() {
1184            "SignedInfo" => {
1185                if signed_info_node.is_some() {
1186                    return Err(SignatureVerificationPipelineError::InvalidStructure {
1187                        reason: "SignedInfo must appear exactly once under Signature",
1188                    });
1189                }
1190                signed_info_node = Some(child);
1191                signed_info_index = Some(element_index);
1192            }
1193            "SignatureValue" => {
1194                if signature_value_node.is_some() {
1195                    return Err(SignatureVerificationPipelineError::InvalidStructure {
1196                        reason: "SignatureValue must appear exactly once under Signature",
1197                    });
1198                }
1199                signature_value_node = Some(child);
1200                signature_value_index = Some(element_index);
1201            }
1202            "KeyInfo" => {
1203                if key_info_node.is_some() {
1204                    return Err(SignatureVerificationPipelineError::InvalidStructure {
1205                        reason: "KeyInfo must appear at most once under Signature",
1206                    });
1207                }
1208                key_info_node = Some(child);
1209                key_info_index = Some(element_index);
1210            }
1211            "Object" => {
1212                // Valid Object elements are allowed only after SignedInfo, SignatureValue,
1213                // and optional KeyInfo; this is enforced via first_unexpected_dsig_index.
1214            }
1215            _ => {
1216                if first_unexpected_dsig_index.is_none() {
1217                    first_unexpected_dsig_index = Some(element_index);
1218                }
1219            }
1220        }
1221    }
1222
1223    let signed_info_node =
1224        signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
1225            element: "SignedInfo",
1226        })?;
1227    let signature_value_node =
1228        signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
1229            element: "SignatureValue",
1230        })?;
1231    if signed_info_index != Some(1) {
1232        return Err(SignatureVerificationPipelineError::InvalidStructure {
1233            reason: "SignedInfo must be the first element child of Signature",
1234        });
1235    }
1236    if signature_value_index != Some(2) {
1237        return Err(SignatureVerificationPipelineError::InvalidStructure {
1238            reason: "SignatureValue must be the second element child of Signature",
1239        });
1240    }
1241    if let Some(index) = key_info_index
1242        && index != 3
1243    {
1244        return Err(SignatureVerificationPipelineError::InvalidStructure {
1245            reason: "KeyInfo must be the third element child of Signature when present",
1246        });
1247    }
1248
1249    let allowed_prefix_end = key_info_index.unwrap_or(2);
1250    if let Some(unexpected_index) = first_unexpected_dsig_index {
1251        return Err(SignatureVerificationPipelineError::InvalidStructure {
1252            reason: if unexpected_index > allowed_prefix_end {
1253                "After SignedInfo, SignatureValue, and optional KeyInfo, Signature may contain only Object elements"
1254            } else {
1255                "Signature may contain SignedInfo first, SignatureValue second, optional KeyInfo third, and Object elements thereafter"
1256            },
1257        });
1258    }
1259
1260    Ok(SignatureChildNodes {
1261        signed_info_node,
1262        signature_value_node,
1263        key_info_node,
1264    })
1265}
1266
1267fn decode_signature_value(
1268    signature_value_node: Node<'_, '_>,
1269) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
1270    if signature_value_node
1271        .children()
1272        .any(|child| child.is_element())
1273    {
1274        return Err(SignatureVerificationPipelineError::InvalidStructure {
1275            reason: "SignatureValue must not contain element children",
1276        });
1277    }
1278
1279    let mut normalized = Vec::new();
1280    let mut raw_text_len = 0usize;
1281    for child in signature_value_node
1282        .children()
1283        .filter(|child| child.is_text())
1284    {
1285        if let Some(text) = child.text() {
1286            push_normalized_signature_text(text, &mut raw_text_len, &mut normalized)?;
1287        }
1288    }
1289
1290    Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
1291}
1292
1293fn push_normalized_signature_text(
1294    text: &str,
1295    raw_text_len: &mut usize,
1296    normalized: &mut Vec<u8>,
1297) -> Result<(), SignatureVerificationPipelineError> {
1298    if raw_text_len.saturating_add(text.len()) > MAX_SIGNATURE_VALUE_TEXT_LEN {
1299        return Err(SignatureVerificationPipelineError::InvalidStructure {
1300            reason: "SignatureValue exceeds maximum allowed text length",
1301        });
1302    }
1303    *raw_text_len = raw_text_len.saturating_add(text.len());
1304
1305    normalize_xml_base64_bytes(text.as_bytes(), normalized, |_| true).map_err(|err| {
1306        SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(
1307            err.normalized_offset,
1308            err.invalid_byte,
1309        ))
1310    })?;
1311    if normalized.len() > MAX_SIGNATURE_VALUE_LEN {
1312        return Err(SignatureVerificationPipelineError::InvalidStructure {
1313            reason: "SignatureValue exceeds maximum allowed length",
1314        });
1315    }
1316
1317    Ok(())
1318}
1319
1320fn verify_with_algorithm(
1321    algorithm: SignatureAlgorithm,
1322    public_key_pem: &str,
1323    signed_data: &[u8],
1324    signature_value: &[u8],
1325) -> Result<bool, SignatureVerificationPipelineError> {
1326    match algorithm {
1327        SignatureAlgorithm::RsaSha1
1328        | SignatureAlgorithm::RsaSha256
1329        | SignatureAlgorithm::RsaSha384
1330        | SignatureAlgorithm::RsaSha512 => Ok(verify_rsa_signature_pem(
1331            algorithm,
1332            public_key_pem,
1333            signed_data,
1334            signature_value,
1335        )?),
1336        SignatureAlgorithm::EcdsaP256Sha256 | SignatureAlgorithm::EcdsaP384Sha384 => {
1337            // Malformed ECDSA signature bytes are treated as a verification miss
1338            // (Ok(false)) instead of a pipeline error; only key/algorithm and
1339            // crypto-operation failures propagate as Err.
1340            match verify_ecdsa_signature_pem(
1341                algorithm,
1342                public_key_pem,
1343                signed_data,
1344                signature_value,
1345            ) {
1346                Ok(valid) => Ok(valid),
1347                Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
1348                Err(error) => Err(error.into()),
1349            }
1350        }
1351    }
1352}
1353
1354#[cfg(test)]
1355#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
1356mod tests {
1357    use super::*;
1358    use crate::c14n::C14nAlgorithm;
1359    use crate::xmldsig::digest::DigestAlgorithm;
1360    use crate::xmldsig::parse::{Reference, parse_signed_info};
1361    use crate::xmldsig::transforms::Transform;
1362    use crate::xmldsig::uri::UriReferenceResolver;
1363    use base64::Engine;
1364    use roxmltree::Document;
1365
1366    // ── Helpers ──────────────────────────────────────────────────────
1367
1368    /// Build a Reference with given URI, transforms, digest method, and expected digest.
1369    fn make_reference(
1370        uri: &str,
1371        transforms: Vec<Transform>,
1372        digest_method: DigestAlgorithm,
1373        digest_value: Vec<u8>,
1374    ) -> Reference {
1375        Reference {
1376            uri: Some(uri.to_string()),
1377            id: None,
1378            ref_type: None,
1379            transforms,
1380            digest_method,
1381            digest_value,
1382        }
1383    }
1384
1385    struct RejectingKey;
1386
1387    impl VerifyingKey for RejectingKey {
1388        fn verify(
1389            &self,
1390            _algorithm: SignatureAlgorithm,
1391            _signed_data: &[u8],
1392            _signature_value: &[u8],
1393        ) -> Result<bool, SignatureVerificationPipelineError> {
1394            Ok(false)
1395        }
1396    }
1397
1398    struct AcceptingKey;
1399
1400    impl VerifyingKey for AcceptingKey {
1401        fn verify(
1402            &self,
1403            _algorithm: SignatureAlgorithm,
1404            _signed_data: &[u8],
1405            _signature_value: &[u8],
1406        ) -> Result<bool, SignatureVerificationPipelineError> {
1407            Ok(true)
1408        }
1409    }
1410
1411    struct PanicResolver;
1412
1413    impl KeyResolver for PanicResolver {
1414        fn resolve<'a>(
1415            &'a self,
1416            _key_info: Option<&KeyInfo>,
1417            _algorithm: SignatureAlgorithm,
1418        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
1419        {
1420            panic!("resolver should not be called when references already fail");
1421        }
1422    }
1423
1424    struct MissingKeyResolver;
1425
1426    impl KeyResolver for MissingKeyResolver {
1427        fn resolve<'a>(
1428            &'a self,
1429            _key_info: Option<&KeyInfo>,
1430            _algorithm: SignatureAlgorithm,
1431        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
1432        {
1433            Ok(None)
1434        }
1435    }
1436
1437    struct ConsumingKeyInfoResolver;
1438
1439    impl KeyResolver for ConsumingKeyInfoResolver {
1440        fn resolve<'a>(
1441            &'a self,
1442            _key_info: Option<&KeyInfo>,
1443            _algorithm: SignatureAlgorithm,
1444        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
1445        {
1446            Ok(None)
1447        }
1448
1449        fn consumes_document_key_info(&self) -> bool {
1450            true
1451        }
1452    }
1453
1454    fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
1455        format!(
1456            r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1457  <ds:SignedInfo>
1458    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1459    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1460    <ds:Reference URI="{reference_uri}">
1461      {transforms_xml}
1462      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1463      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
1464    </ds:Reference>
1465  </ds:SignedInfo>
1466  <ds:SignatureValue>AQ==</ds:SignatureValue>
1467</ds:Signature>"#
1468        )
1469    }
1470
1471    fn signature_with_target_reference(signature_value_b64: &str) -> String {
1472        let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1473  <target ID="target">payload</target>
1474  <ds:Signature>
1475    <ds:SignedInfo>
1476      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1477      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1478      <ds:Reference URI="#target">
1479        <ds:Transforms>
1480          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1481        </ds:Transforms>
1482        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1483        <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
1484      </ds:Reference>
1485    </ds:SignedInfo>
1486    <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
1487  </ds:Signature>
1488</root>"##;
1489
1490        let doc = Document::parse(xml_template).unwrap();
1491        let sig_node = doc
1492            .descendants()
1493            .find(|node| node.is_element() && node.tag_name().name() == "Signature")
1494            .unwrap();
1495        let signed_info_node = sig_node
1496            .children()
1497            .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
1498            .unwrap();
1499        let signed_info = parse_signed_info(signed_info_node).unwrap();
1500        let reference = &signed_info.references[0];
1501        let resolver = UriReferenceResolver::new(&doc);
1502        let initial_data = resolver
1503            .dereference(reference.uri.as_deref().unwrap())
1504            .unwrap();
1505        let pre_digest =
1506            crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
1507                .unwrap();
1508        let digest = compute_digest(reference.digest_method, &pre_digest);
1509        let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
1510        xml_template
1511            .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
1512            .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
1513    }
1514
1515    #[test]
1516    fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
1517        let xml = signature_with_target_reference("AQ==");
1518
1519        let result = VerifyContext::new()
1520            .verify(&xml)
1521            .expect("missing key config must be reported as verification status");
1522        assert!(
1523            matches!(
1524                result.status,
1525                DsigStatus::Invalid(FailureReason::KeyNotFound)
1526            ),
1527            "unexpected status: {:?}",
1528            result.status
1529        );
1530    }
1531
1532    #[test]
1533    fn verify_context_rejects_disallowed_uri() {
1534        let xml = minimal_signature_xml("http://example.com/external", "");
1535        let err = VerifyContext::new()
1536            .key(&RejectingKey)
1537            .verify(&xml)
1538            .expect_err("external URI should be rejected by default policy");
1539        assert!(matches!(
1540            err,
1541            SignatureVerificationPipelineError::DisallowedUri { .. }
1542        ));
1543    }
1544
1545    #[test]
1546    fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
1547        let xml = minimal_signature_xml("", "");
1548        let err = VerifyContext::new()
1549            .key(&RejectingKey)
1550            .allowed_uri_types(UriTypeSet::new(false, true, false))
1551            .verify(&xml)
1552            .expect_err("empty URI must be rejected when empty references are disabled");
1553        assert!(matches!(
1554            err,
1555            SignatureVerificationPipelineError::DisallowedUri { ref uri } if uri.is_empty()
1556        ));
1557    }
1558
1559    #[test]
1560    fn verify_context_rejects_disallowed_transform() {
1561        let xml = minimal_signature_xml(
1562            "",
1563            r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
1564        );
1565        let err = VerifyContext::new()
1566            .key(&RejectingKey)
1567            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
1568            .verify(&xml)
1569            .expect_err("enveloped transform should be rejected by allowlist");
1570        assert!(matches!(
1571            err,
1572            SignatureVerificationPipelineError::DisallowedTransform { .. }
1573        ));
1574    }
1575
1576    fn signature_with_manifest_xml(valid_manifest_digest: bool) -> String {
1577        signature_with_manifest_xml_with_manifest_mutation(valid_manifest_digest, |xml| xml)
1578    }
1579
1580    fn signature_with_manifest_xml_with_manifest_mutation<F>(
1581        valid_manifest_digest: bool,
1582        mutate_manifest: F,
1583    ) -> String
1584    where
1585        F: FnOnce(String) -> String,
1586    {
1587        const TMP_SIGNED_INFO_DIGEST: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
1588        const INVALID_MANIFEST_DIGEST: &str = "//////////////////////////8=";
1589        let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1590  <target ID="target">payload</target>
1591  <ds:Signature>
1592    <ds:SignedInfo>
1593      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1594      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1595      <ds:Reference URI="#manifest">
1596        <ds:Transforms>
1597          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1598        </ds:Transforms>
1599        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1600        <ds:DigestValue>SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER</ds:DigestValue>
1601      </ds:Reference>
1602    </ds:SignedInfo>
1603    <ds:SignatureValue>AQ==</ds:SignatureValue>
1604    <ds:Object>
1605      <ds:Manifest ID="manifest">
1606        <ds:Reference URI="#target">
1607          <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1608          <ds:DigestValue>MANIFEST_DIGEST_PLACEHOLDER</ds:DigestValue>
1609        </ds:Reference>
1610      </ds:Manifest>
1611    </ds:Object>
1612  </ds:Signature>
1613</root>"##;
1614        let seed_xml = xml_template.replace(
1615            "SIGNEDINFO_OBJECT_DIGEST_PLACEHOLDER",
1616            TMP_SIGNED_INFO_DIGEST,
1617        );
1618        let doc = Document::parse(&seed_xml).unwrap();
1619        let signature_node = doc
1620            .descendants()
1621            .find(|node| {
1622                node.is_element()
1623                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
1624                    && node.tag_name().name() == "Signature"
1625            })
1626            .unwrap();
1627        let resolver = UriReferenceResolver::new(&doc);
1628        let initial_data = resolver.dereference("#target").unwrap();
1629        let manifest_pre_digest =
1630            crate::xmldsig::execute_transforms(signature_node, initial_data, &[]).unwrap();
1631        let computed_manifest_digest_b64 = base64::engine::general_purpose::STANDARD
1632            .encode(compute_digest(DigestAlgorithm::Sha1, &manifest_pre_digest));
1633        let final_manifest_digest_b64 = if valid_manifest_digest {
1634            computed_manifest_digest_b64.as_str()
1635        } else {
1636            INVALID_MANIFEST_DIGEST
1637        };
1638        let xml_with_manifest_digest = mutate_manifest(
1639            seed_xml.replace("MANIFEST_DIGEST_PLACEHOLDER", final_manifest_digest_b64),
1640        );
1641        let signed_doc = Document::parse(&xml_with_manifest_digest).unwrap();
1642        let signed_signature_node = signed_doc
1643            .descendants()
1644            .find(|node| {
1645                node.is_element()
1646                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
1647                    && node.tag_name().name() == "Signature"
1648            })
1649            .unwrap();
1650        let signed_info_node = signed_signature_node
1651            .children()
1652            .find(|node| {
1653                node.is_element()
1654                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
1655                    && node.tag_name().name() == "SignedInfo"
1656            })
1657            .unwrap();
1658        let signed_info = parse_signed_info(signed_info_node).unwrap();
1659        let object_reference = &signed_info.references[0];
1660        let signed_resolver = UriReferenceResolver::new(&signed_doc);
1661        let signed_initial_data = signed_resolver
1662            .dereference(object_reference.uri.as_deref().unwrap())
1663            .unwrap();
1664        let signed_pre_digest = crate::xmldsig::execute_transforms(
1665            signed_signature_node,
1666            signed_initial_data,
1667            &object_reference.transforms,
1668        )
1669        .unwrap();
1670        let signed_digest_b64 = base64::engine::general_purpose::STANDARD.encode(compute_digest(
1671            object_reference.digest_method,
1672            &signed_pre_digest,
1673        ));
1674
1675        xml_with_manifest_digest.replacen(TMP_SIGNED_INFO_DIGEST, &signed_digest_b64, 1)
1676    }
1677
1678    fn replace_fixture_manifest_digest(xml: &str, replacement: &str) -> String {
1679        let object_marker = "<ds:Object>";
1680        let object_start = xml
1681            .find(object_marker)
1682            .expect("fixture should contain ds:Object")
1683            + object_marker.len();
1684        let open = "<ds:DigestValue>";
1685        let close = "</ds:DigestValue>";
1686        let value_start = xml[object_start..]
1687            .find(open)
1688            .map(|offset| object_start + offset + open.len())
1689            .expect("Manifest should contain DigestValue");
1690        let value_end = xml[value_start..]
1691            .find(close)
1692            .map(|offset| value_start + offset)
1693            .expect("Manifest DigestValue must be closed");
1694
1695        format!("{}{replacement}{}", &xml[..value_start], &xml[value_end..])
1696    }
1697
1698    #[test]
1699    fn verify_context_processes_manifest_references_when_enabled() {
1700        let xml = signature_with_manifest_xml(true);
1701
1702        let result_without_manifests = VerifyContext::new()
1703            .key(&RejectingKey)
1704            .verify(&xml)
1705            .expect("manifest processing disabled should still verify SignedInfo");
1706        assert!(
1707            result_without_manifests.manifest_references.is_empty(),
1708            "manifest results must stay empty when manifest processing is disabled",
1709        );
1710        assert!(matches!(
1711            result_without_manifests.status,
1712            DsigStatus::Invalid(FailureReason::SignatureMismatch)
1713        ));
1714
1715        let malformed_manifest_xml = signature_with_manifest_xml(true).replacen(
1716            "</ds:Object>",
1717            "</ds:Object><ds:Object><ds:Manifest><ds:Foo/></ds:Manifest></ds:Object>",
1718            1,
1719        );
1720        let malformed_with_manifests_disabled = VerifyContext::new()
1721            .key(&RejectingKey)
1722            .verify(&malformed_manifest_xml)
1723            .expect("malformed Manifest must be ignored when manifest processing is disabled");
1724        assert!(
1725            malformed_with_manifests_disabled
1726                .manifest_references
1727                .is_empty(),
1728            "manifest parser must not run when process_manifests is disabled",
1729        );
1730        assert!(matches!(
1731            malformed_with_manifests_disabled.status,
1732            DsigStatus::Invalid(FailureReason::SignatureMismatch)
1733        ));
1734
1735        let result_with_manifests = VerifyContext::new()
1736            .key(&AcceptingKey)
1737            .process_manifests(true)
1738            .verify(&xml)
1739            .expect("manifest references should be processed when enabled");
1740        assert_eq!(result_with_manifests.manifest_references.len(), 1);
1741        assert_eq!(
1742            result_with_manifests.manifest_references[0].reference_set,
1743            ReferenceSet::Manifest
1744        );
1745        assert_eq!(
1746            result_with_manifests.manifest_references[0].reference_index,
1747            0
1748        );
1749        assert!(matches!(
1750            result_with_manifests.manifest_references[0].status,
1751            DsigStatus::Valid
1752        ));
1753        assert!(matches!(result_with_manifests.status, DsigStatus::Valid));
1754    }
1755
1756    #[test]
1757    fn verify_context_skips_manifest_work_when_signature_value_is_invalid() {
1758        // SignedInfo authenticates the Manifest bytes only after SignatureValue
1759        // succeeds. Malformed nested content must not consume parsing work when
1760        // the cryptographic signature itself is invalid.
1761        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1762            replace_fixture_manifest_digest(&xml, "!!!")
1763        });
1764        assert!(
1765            xml.split_once("<ds:Object>")
1766                .is_some_and(|(_, object)| object.contains("<ds:DigestValue>!!!</ds:DigestValue>")),
1767            "fixture mutation must corrupt the nested Manifest DigestValue",
1768        );
1769
1770        let result = VerifyContext::new()
1771            .key(&RejectingKey)
1772            .process_manifests(true)
1773            .verify(&xml)
1774            .expect("invalid SignatureValue must short-circuit Manifest parsing");
1775
1776        assert!(matches!(
1777            result.status,
1778            DsigStatus::Invalid(FailureReason::SignatureMismatch)
1779        ));
1780        assert!(result.manifest_references.is_empty());
1781    }
1782
1783    #[test]
1784    fn verify_context_shares_xpath_parse_budget_with_manifest_references() {
1785        // SignedInfo and every Manifest form one attacker-controlled parse unit:
1786        // splitting expressions across Reference sets must not reset the ceiling.
1787        let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
1788            .repeat(64);
1789        let transform = format!(
1790            r#"<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</ds:Transform>"#
1791        );
1792        let max_transforms = transform.repeat(16);
1793        let max_manifest_reference = format!(
1794            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>"##
1795        );
1796        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1797            xml.replacen(
1798                r##"<ds:Reference URI="#target">"##,
1799                &format!(
1800                    r##"<ds:Reference URI="#target"><ds:Transforms>{}</ds:Transforms>"##,
1801                    max_transforms
1802                ),
1803                1,
1804            )
1805            .replacen(
1806                "</ds:SignedInfo>",
1807                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>"##,
1808                1,
1809            )
1810            .replacen(
1811                "</ds:Manifest>",
1812                &format!("{}</ds:Manifest>", max_manifest_reference.repeat(3)),
1813                1,
1814            )
1815        });
1816
1817        let error = VerifyContext::new()
1818            .key(&AcceptingKey)
1819            .process_manifests(true)
1820            .verify(&xml)
1821            .expect_err("SignedInfo and Manifest References must share one XPath parse budget");
1822
1823        assert!(
1824            matches!(
1825                &error,
1826                SignatureVerificationPipelineError::ParseManifestReference(source)
1827                    if source.to_string().contains("signature-wide XPath expression budget")
1828            ),
1829            "unexpected error: {error:?}"
1830        );
1831    }
1832
1833    #[test]
1834    fn verify_context_processes_manifest_when_signedinfo_references_object() {
1835        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1836            xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
1837                .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
1838                .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
1839        });
1840
1841        let result = VerifyContext::new()
1842            .key(&AcceptingKey)
1843            .process_manifests(true)
1844            .verify(&xml)
1845            .expect("manifest references should be processed when SignedInfo references ds:Object");
1846        assert_eq!(
1847            result.manifest_references.len(),
1848            1,
1849            "signed ds:Object should enable processing of its direct-child ds:Manifest",
1850        );
1851        assert_eq!(
1852            result.manifest_references[0].reference_set,
1853            ReferenceSet::Manifest
1854        );
1855        assert_eq!(result.manifest_references[0].reference_index, 0);
1856        assert!(matches!(
1857            result.manifest_references[0].status,
1858            DsigStatus::Valid
1859        ));
1860    }
1861
1862    #[test]
1863    fn verify_context_skips_manifest_removed_by_enveloped_transform() {
1864        // The owning Signature contains both eligible ID targets. Subtracting
1865        // its subtree therefore removes every target node from the digest input,
1866        // so neither form authenticates the Manifest structure for processing.
1867        for target_object in [false, true] {
1868            let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1869                let xml = xml.replacen(
1870                    r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
1871                    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#"/>"#,
1872                    1,
1873                );
1874                if target_object {
1875                    xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
1876                        .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
1877                        .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
1878                } else {
1879                    xml
1880                }
1881            });
1882
1883            let result = VerifyContext::new()
1884                .key(&AcceptingKey)
1885                .process_manifests(true)
1886                .store_pre_digest(true)
1887                .verify(&xml)
1888                .expect("an emptied reference remains a valid core digest input");
1889
1890            assert!(matches!(result.status, DsigStatus::Valid));
1891            assert_eq!(
1892                result.signed_info_references[0].pre_digest_data.as_deref(),
1893                Some([].as_slice()),
1894                "target_object={target_object} must have empty transformed bytes",
1895            );
1896            assert!(
1897                result.manifest_references.is_empty(),
1898                "target_object={target_object} must not authenticate the Manifest",
1899            );
1900        }
1901    }
1902
1903    #[test]
1904    fn verify_context_ignores_manifest_excluded_from_signed_object() {
1905        // A Reference URI authenticates only its post-transform bytes. Excluding
1906        // the Manifest subtree must not let its independently valid digest chain
1907        // masquerade as data authenticated by SignedInfo.
1908        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
1909            xml.replacen("URI=\"#manifest\"", "URI=\"#object-id\"", 1)
1910                .replacen("<ds:Object>", "<ds:Object ID=\"object-id\">", 1)
1911                .replacen("<ds:Manifest ID=\"manifest\">", "<ds:Manifest>", 1)
1912                .replacen(
1913                    r#"<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>"#,
1914                    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#"/>"#,
1915                    1,
1916                )
1917        });
1918
1919        let result = VerifyContext::new()
1920            .key(&AcceptingKey)
1921            .process_manifests(true)
1922            .verify(&xml)
1923            .expect("excluded Manifest content must be ignored, not parsed");
1924
1925        assert!(matches!(result.status, DsigStatus::Valid));
1926        assert!(
1927            result.manifest_references.is_empty(),
1928            "a transform-excluded Manifest is not authenticated by SignedInfo",
1929        );
1930    }
1931
1932    #[test]
1933    fn verify_context_skips_manifest_digest_work_when_signature_is_invalid() {
1934        let xml = signature_with_manifest_xml(false);
1935        let result = VerifyContext::new()
1936            .key(&RejectingKey)
1937            .process_manifests(true)
1938            .verify(&xml)
1939            .expect("invalid SignatureValue must short-circuit Manifest digest work");
1940        assert!(result.manifest_references.is_empty());
1941        assert!(matches!(
1942            result.status,
1943            DsigStatus::Invalid(FailureReason::SignatureMismatch)
1944        ));
1945    }
1946
1947    #[test]
1948    fn verify_context_manifest_digest_mismatch_is_non_fatal_with_accepting_key() {
1949        let xml = signature_with_manifest_xml(false);
1950        let result = VerifyContext::new()
1951            .key(&AcceptingKey)
1952            .process_manifests(true)
1953            .verify(&xml)
1954            .expect("manifest digest mismatches should be recorded while signature stays valid");
1955        assert_eq!(result.manifest_references.len(), 1);
1956        assert!(matches!(
1957            result.manifest_references[0].status,
1958            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1959        ));
1960        assert!(matches!(result.status, DsigStatus::Valid));
1961    }
1962
1963    #[test]
1964    fn verify_context_skips_manifest_parsing_when_signedinfo_reference_fails() {
1965        // Manifest content is not authenticated after a SignedInfo reference
1966        // failure, so parsing it would spend work on untrusted nested input.
1967        let xml = signature_with_manifest_xml(true);
1968        let (signed_info_prefix, object_suffix) = xml
1969            .split_once("<ds:Object>")
1970            .expect("fixture should contain ds:Object");
1971        let open = "<ds:DigestValue>";
1972        let close = "</ds:DigestValue>";
1973        let digest_start = signed_info_prefix
1974            .find(open)
1975            .expect("SignedInfo should contain DigestValue");
1976        let digest_end = signed_info_prefix[digest_start + open.len()..]
1977            .find(close)
1978            .map(|offset| digest_start + open.len() + offset)
1979            .expect("SignedInfo DigestValue must be closed");
1980        let broken_signed_info_prefix = format!(
1981            "{}{}AAAAAAAAAAAAAAAAAAAAAAAAAAA={}{}",
1982            &signed_info_prefix[..digest_start],
1983            open,
1984            close,
1985            &signed_info_prefix[digest_end + close.len()..],
1986        );
1987        let broken_xml = format!("{broken_signed_info_prefix}<ds:Object>{object_suffix}");
1988        let result = VerifyContext::new()
1989            .key(&RejectingKey)
1990            .process_manifests(true)
1991            .verify(&broken_xml)
1992            .expect("SignedInfo digest failure should return without parsing Manifests");
1993        assert!(matches!(
1994            result.status,
1995            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1996        ));
1997        assert!(
1998            result.manifest_references.is_empty(),
1999            "unauthenticated Manifest content must not be parsed",
2000        );
2001    }
2002
2003    #[test]
2004    fn verify_context_skips_manifest_policy_work_when_signature_is_invalid() {
2005        // A digest-valid SignedInfo reference does not authenticate Manifest
2006        // policy inputs until SignatureValue also succeeds.
2007        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2008            xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
2009        });
2010        let result = VerifyContext::new()
2011            .key(&RejectingKey)
2012            .process_manifests(true)
2013            .verify(&broken_xml)
2014            .expect("invalid SignatureValue must short-circuit Manifest policy work");
2015        assert!(result.manifest_references.is_empty());
2016        assert!(matches!(
2017            result.status,
2018            DsigStatus::Invalid(FailureReason::SignatureMismatch)
2019        ));
2020    }
2021
2022    #[test]
2023    fn verify_context_records_manifest_policy_violations_with_accepting_key() {
2024        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2025            xml.replacen("URI=\"#target\"", "URI=\"http://example.com/external\"", 1)
2026        });
2027        let result = VerifyContext::new()
2028            .key(&AcceptingKey)
2029            .process_manifests(true)
2030            .verify(&broken_xml)
2031            .expect("manifest policy violations should be recorded while signature stays valid");
2032        assert_eq!(result.manifest_references.len(), 1);
2033        assert!(matches!(
2034            result.manifest_references[0].status,
2035            DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { ref_index: 0 })
2036        ));
2037        assert!(matches!(result.status, DsigStatus::Valid));
2038    }
2039
2040    #[test]
2041    fn verify_context_skips_manifest_uri_work_when_signature_is_invalid() {
2042        // Missing Manifest URIs remain unauthenticated until SignatureValue
2043        // succeeds, so they cannot trigger Manifest policy processing here.
2044        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2045            xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
2046        });
2047
2048        let result = VerifyContext::new()
2049            .key(&RejectingKey)
2050            .process_manifests(true)
2051            .verify(&broken_xml)
2052            .expect("invalid SignatureValue must short-circuit Manifest URI processing");
2053        assert!(result.manifest_references.is_empty());
2054        assert!(matches!(
2055            result.status,
2056            DsigStatus::Invalid(FailureReason::SignatureMismatch)
2057        ));
2058    }
2059
2060    #[test]
2061    fn verify_context_records_manifest_missing_uri_with_accepting_key() {
2062        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2063            xml.replacen("<ds:Reference URI=\"#target\">", "<ds:Reference>", 1)
2064        });
2065
2066        let result = VerifyContext::new()
2067            .key(&AcceptingKey)
2068            .process_manifests(true)
2069            .verify(&broken_xml)
2070            .expect("manifest missing URI should be recorded while signature stays valid");
2071        assert_eq!(result.manifest_references.len(), 1);
2072        assert_eq!(result.manifest_references[0].uri, "<omitted>");
2073        assert!(matches!(
2074            result.manifest_references[0].status,
2075            DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { ref_index: 0 })
2076        ));
2077        assert!(matches!(result.status, DsigStatus::Valid));
2078    }
2079
2080    #[test]
2081    fn verify_context_ignores_nested_manifests_in_object() {
2082        // A digest-valid Manifest below a wrapper is outside the strict direct-
2083        // child processing profile and must not appear in diagnostics.
2084        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2085            xml.replacen(
2086                "<ds:Manifest ID=\"manifest\">",
2087                "<wrapper><ds:Manifest ID=\"manifest\">",
2088                1,
2089            )
2090            .replacen("</ds:Manifest>", "</ds:Manifest></wrapper>", 1)
2091        });
2092
2093        let result = VerifyContext::new()
2094            .key(&AcceptingKey)
2095            .process_manifests(true)
2096            .verify(&xml)
2097            .expect("nested Manifest nodes are ignored in strict mode");
2098        assert!(
2099            result.manifest_references.is_empty(),
2100            "only direct ds:Manifest children of ds:Object must be processed"
2101        );
2102        assert!(matches!(result.status, DsigStatus::Valid));
2103    }
2104
2105    #[test]
2106    fn verify_context_reports_manifest_reference_parse_errors_explicitly() {
2107        // Malformed nested DigestValue is parsed only after the enclosing
2108        // Manifest structure has been authenticated by SignedInfo.
2109        let broken_xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2110            replace_fixture_manifest_digest(&xml, "!!!")
2111        });
2112
2113        let err = VerifyContext::new()
2114            .key(&AcceptingKey)
2115            .process_manifests(true)
2116            .verify(&broken_xml)
2117            .expect_err("invalid Manifest DigestValue must map to ParseManifestReference");
2118        assert!(matches!(
2119            err,
2120            SignatureVerificationPipelineError::ParseManifestReference(_)
2121        ));
2122    }
2123
2124    #[test]
2125    fn verify_context_rejects_manifest_non_whitespace_mixed_content() {
2126        // Authenticated mixed content is still structurally invalid under the
2127        // Manifest element-only grammar.
2128        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2129            xml.replacen(
2130                "<ds:Manifest ID=\"manifest\">",
2131                "<ds:Manifest ID=\"manifest\">junk",
2132                1,
2133            )
2134        });
2135
2136        let err = VerifyContext::new()
2137            .key(&AcceptingKey)
2138            .process_manifests(true)
2139            .verify(&xml)
2140            .expect_err("Manifest mixed content must fail verification");
2141        assert!(matches!(
2142            err,
2143            SignatureVerificationPipelineError::InvalidStructure {
2144                reason: "Manifest contains non-whitespace mixed content"
2145            }
2146        ));
2147    }
2148
2149    #[test]
2150    fn verify_context_rejects_empty_manifest_children() {
2151        // An authenticated empty Manifest violates the required Reference+
2152        // content model rather than disappearing as an unsigned block.
2153        let xml = signature_with_manifest_xml_with_manifest_mutation(true, |xml| {
2154            let (prefix, rest) = xml
2155                .split_once("<ds:Manifest ID=\"manifest\">")
2156                .expect("fixture should contain Manifest");
2157            let (_, suffix) = rest
2158                .split_once("</ds:Manifest>")
2159                .expect("fixture should contain closing Manifest");
2160            format!("{prefix}<ds:Manifest ID=\"manifest\"></ds:Manifest>{suffix}")
2161        });
2162
2163        let err = VerifyContext::new()
2164            .key(&AcceptingKey)
2165            .process_manifests(true)
2166            .verify(&xml)
2167            .expect_err("empty Manifest must fail verification");
2168        assert!(matches!(
2169            err,
2170            SignatureVerificationPipelineError::InvalidStructure {
2171                reason: "Manifest must contain at least one ds:Reference element child"
2172            }
2173        ));
2174    }
2175
2176    #[test]
2177    fn verify_context_ignores_unsigned_malformed_manifest_blocks() {
2178        let xml = signature_with_manifest_xml(true).replacen(
2179            "</ds:Object>",
2180            "</ds:Object><ds:Object><ds:Manifest>junk<ds:Foo/></ds:Manifest></ds:Object>",
2181            1,
2182        );
2183        let result = VerifyContext::new()
2184            .key(&AcceptingKey)
2185            .process_manifests(true)
2186            .verify(&xml)
2187            .expect("unsigned malformed Manifest must be ignored");
2188        assert_eq!(
2189            result.manifest_references.len(),
2190            1,
2191            "only signed Manifest references must be reported",
2192        );
2193        assert!(matches!(result.status, DsigStatus::Valid));
2194    }
2195
2196    #[test]
2197    fn verify_context_skips_ambiguous_manifest_id_blocks() {
2198        let xml = signature_with_manifest_xml(true).replacen(
2199            "</ds:Object>",
2200            "</ds:Object><ds:Object><ds:Manifest ID=\"manifest\">junk<ds:Foo/></ds:Manifest></ds:Object>",
2201            1,
2202        );
2203        let err = VerifyContext::new()
2204            .key(&RejectingKey)
2205            .process_manifests(true)
2206            .verify(&xml)
2207            .expect_err("ambiguous manifest IDs should make SignedInfo #manifest dereference fail");
2208        assert!(matches!(
2209            err,
2210            SignatureVerificationPipelineError::Reference(
2211                ReferenceProcessingError::UriDereference(
2212                    crate::xmldsig::types::TransformError::ElementNotFound(id)
2213                )
2214            ) if id == "manifest"
2215        ));
2216    }
2217
2218    #[test]
2219    fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
2220        let xml = minimal_signature_xml("", "");
2221        let err = VerifyContext::new()
2222            .key(&RejectingKey)
2223            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
2224            .verify(&xml)
2225            .expect_err("implicit default C14N must be checked against allowlist");
2226        assert!(matches!(
2227            err,
2228            SignatureVerificationPipelineError::DisallowedTransform { .. }
2229        ));
2230    }
2231
2232    #[test]
2233    fn verify_context_skips_resolver_when_reference_processing_fails() {
2234        let xml = minimal_signature_xml("", "");
2235        let result = VerifyContext::new()
2236            .key_resolver(&PanicResolver)
2237            .verify(&xml)
2238            .expect("reference digest mismatch should short-circuit before resolver");
2239        assert!(matches!(
2240            result.status,
2241            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
2242        ));
2243    }
2244
2245    #[test]
2246    fn verify_context_reports_key_not_found_when_resolver_misses() {
2247        let xml = signature_with_target_reference("AQ==");
2248        let result = VerifyContext::new()
2249            .key_resolver(&MissingKeyResolver)
2250            .verify(&xml)
2251            .expect("resolver miss should report status, not pipeline error");
2252        assert!(matches!(
2253            result.status,
2254            DsigStatus::Invalid(FailureReason::KeyNotFound)
2255        ));
2256        assert_eq!(
2257            result.signed_info_references.len(),
2258            1,
2259            "KeyNotFound path must preserve SignedInfo reference diagnostics",
2260        );
2261        assert!(matches!(
2262            result.signed_info_references[0].status,
2263            DsigStatus::Valid
2264        ));
2265    }
2266
2267    #[test]
2268    fn verify_context_resolver_can_ignore_malformed_keyinfo_by_default() {
2269        let base_xml = signature_with_target_reference("AQ==");
2270        let xml = base_xml
2271            .replace(
2272                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
2273                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
2274            )
2275            .replace(
2276                "</ds:SignatureValue>\n  </ds:Signature>",
2277                "</ds:SignatureValue>\n    <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n  </ds:Signature>",
2278            );
2279
2280        let result = VerifyContext::new()
2281            .key_resolver(&MissingKeyResolver)
2282            .verify(&xml)
2283            .expect("resolver path should not hard-fail on advisory malformed KeyInfo by default");
2284        assert!(matches!(
2285            result.status,
2286            DsigStatus::Invalid(FailureReason::KeyNotFound)
2287        ));
2288    }
2289
2290    #[test]
2291    fn verify_context_resolver_can_opt_in_to_keyinfo_parse_failures() {
2292        let base_xml = signature_with_target_reference("AQ==");
2293        let xml = base_xml
2294            .replace(
2295                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
2296                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
2297            )
2298            .replace(
2299                "</ds:SignatureValue>\n  </ds:Signature>",
2300                "</ds:SignatureValue>\n    <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n  </ds:Signature>",
2301            );
2302
2303        let err = VerifyContext::new()
2304            .key_resolver(&ConsumingKeyInfoResolver)
2305            .verify(&xml)
2306            .expect_err("resolver opted into KeyInfo parsing, malformed KeyInfo must fail");
2307        assert!(matches!(
2308            err,
2309            SignatureVerificationPipelineError::ParseKeyInfo(_)
2310        ));
2311    }
2312
2313    #[test]
2314    fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
2315        let xml = signature_with_target_reference("@@@");
2316
2317        let err = VerifyContext::new()
2318            .key_resolver(&MissingKeyResolver)
2319            .verify(&xml)
2320            .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
2321        assert!(matches!(
2322            err,
2323            SignatureVerificationPipelineError::SignatureValueBase64(_)
2324        ));
2325    }
2326
2327    #[test]
2328    fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
2329        let xml = signature_with_target_reference("@@@");
2330
2331        let err = VerifyContext::new()
2332            .verify(&xml)
2333            .expect_err("invalid SignatureValue must remain a decode error");
2334        assert!(matches!(
2335            err,
2336            SignatureVerificationPipelineError::SignatureValueBase64(_)
2337        ));
2338    }
2339
2340    #[test]
2341    fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
2342        let references = vec![Reference {
2343            uri: None,
2344            id: None,
2345            ref_type: None,
2346            transforms: vec![],
2347            digest_method: DigestAlgorithm::Sha256,
2348            digest_value: vec![0; 32],
2349        }];
2350        let uri_types = UriTypeSet {
2351            allow_empty: false,
2352            allow_same_document: true,
2353            allow_external: false,
2354        };
2355
2356        let err = enforce_reference_policies(&references, uri_types, None)
2357            .expect_err("missing URI must fail before allow_empty policy is evaluated");
2358        assert!(matches!(
2359            err,
2360            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
2361        ));
2362    }
2363
2364    #[test]
2365    fn enforce_reference_policies_checks_only_terminal_binary_output() {
2366        let c14n = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI).unwrap();
2367        let allowed = HashSet::from([
2368            BASE64_TRANSFORM_URI.to_owned(),
2369            DEFAULT_IMPLICIT_C14N_URI.to_owned(),
2370        ]);
2371        let without_implicit_c14n = HashSet::from([BASE64_TRANSFORM_URI.to_owned()]);
2372
2373        for transforms in [
2374            vec![Transform::Base64Decode, Transform::C14n(c14n)],
2375            vec![Transform::Base64Decode, Transform::Base64Decode],
2376        ] {
2377            let reference = make_reference("", transforms, DigestAlgorithm::Sha256, vec![0; 32]);
2378            enforce_reference_policies(
2379                std::slice::from_ref(&reference),
2380                UriTypeSet::default(),
2381                Some(&allowed),
2382            )
2383            .expect("terminal binary output must not require implicit C14N");
2384        }
2385
2386        let terminal_base64 = make_reference(
2387            "",
2388            vec![Transform::Base64Decode, Transform::Base64Decode],
2389            DigestAlgorithm::Sha256,
2390            vec![0; 32],
2391        );
2392        enforce_reference_policies(
2393            std::slice::from_ref(&terminal_base64),
2394            UriTypeSet::default(),
2395            Some(&without_implicit_c14n),
2396        )
2397        .expect("terminal Base64 output must not require implicit C14N");
2398
2399        let no_transforms = make_reference("", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
2400        let error = enforce_reference_policies(
2401            std::slice::from_ref(&no_transforms),
2402            UriTypeSet::default(),
2403            Some(&without_implicit_c14n),
2404        )
2405        .expect_err("a node-set result must require allowlisted implicit C14N");
2406        assert!(matches!(
2407            error,
2408            SignatureVerificationPipelineError::DisallowedTransform { ref algorithm }
2409                if algorithm == DEFAULT_IMPLICIT_C14N_URI
2410        ));
2411    }
2412
2413    #[test]
2414    fn push_normalized_signature_text_rejects_form_feed() {
2415        let mut normalized = Vec::new();
2416        let mut raw_text_len = 0usize;
2417        let err =
2418            push_normalized_signature_text("ab\u{000C}cd", &mut raw_text_len, &mut normalized)
2419                .expect_err("form-feed must not be treated as XML base64 whitespace");
2420        assert!(matches!(
2421            err,
2422            SignatureVerificationPipelineError::SignatureValueBase64(
2423                base64::DecodeError::InvalidByte(_, 0x0C)
2424            )
2425        ));
2426    }
2427
2428    #[test]
2429    fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
2430        let mut normalized = vec![b'A'; MAX_SIGNATURE_VALUE_LEN - 1];
2431        let mut raw_text_len = normalized.len();
2432        let err = push_normalized_signature_text("é", &mut raw_text_len, &mut normalized)
2433            .expect_err("multibyte characters must not bypass byte-size limit");
2434        assert!(matches!(
2435            err,
2436            SignatureVerificationPipelineError::InvalidStructure {
2437                reason: "SignatureValue exceeds maximum allowed length"
2438            }
2439        ));
2440    }
2441
2442    // ── process_reference: happy path ────────────────────────────────
2443
2444    #[test]
2445    fn reference_with_correct_digest_passes() {
2446        // Create a simple document, compute its canonical form digest,
2447        // then verify that process_reference returns Valid status.
2448        let xml = r##"<root>
2449            <data>hello world</data>
2450            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
2451                <ds:SignedInfo/>
2452            </ds:Signature>
2453        </root>"##;
2454        let doc = Document::parse(xml).unwrap();
2455        let resolver = UriReferenceResolver::new(&doc);
2456        let sig_node = doc
2457            .descendants()
2458            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2459            .unwrap();
2460
2461        // First, compute the expected digest by running the pipeline
2462        let initial_data = resolver.dereference("").unwrap();
2463        let transforms = vec![
2464            Transform::Enveloped,
2465            Transform::C14n(
2466                crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
2467                    .unwrap(),
2468            ),
2469        ];
2470        let pre_digest_bytes =
2471            crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
2472        let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
2473
2474        // Now build a Reference with the correct digest and verify
2475        let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
2476
2477        let result = process_reference(
2478            &reference,
2479            &resolver,
2480            sig_node,
2481            ReferenceSet::SignedInfo,
2482            0,
2483            false,
2484        )
2485        .unwrap();
2486        assert!(
2487            matches!(result.status, DsigStatus::Valid),
2488            "digest should match"
2489        );
2490        assert!(result.pre_digest_data.is_none());
2491    }
2492
2493    #[test]
2494    fn reference_with_wrong_digest_fails() {
2495        let xml = r##"<root>
2496            <data>hello</data>
2497            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2498                <ds:SignedInfo/>
2499            </ds:Signature>
2500        </root>"##;
2501        let doc = Document::parse(xml).unwrap();
2502        let resolver = UriReferenceResolver::new(&doc);
2503        let sig_node = doc
2504            .descendants()
2505            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2506            .unwrap();
2507
2508        let transforms = vec![Transform::Enveloped];
2509        // Wrong digest value — all zeros
2510        let wrong_digest = vec![0u8; 32];
2511        let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
2512
2513        let result = process_reference(
2514            &reference,
2515            &resolver,
2516            sig_node,
2517            ReferenceSet::SignedInfo,
2518            0,
2519            false,
2520        )
2521        .unwrap();
2522        assert!(matches!(
2523            result.status,
2524            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
2525        ));
2526    }
2527
2528    #[test]
2529    fn reference_with_wrong_digest_preserves_supplied_ref_index() {
2530        let xml = r##"<root>
2531            <data>hello</data>
2532            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2533                <ds:SignedInfo/>
2534            </ds:Signature>
2535        </root>"##;
2536        let doc = Document::parse(xml).unwrap();
2537        let resolver = UriReferenceResolver::new(&doc);
2538        let sig_node = doc
2539            .descendants()
2540            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2541            .unwrap();
2542
2543        let reference = make_reference(
2544            "",
2545            vec![Transform::Enveloped],
2546            DigestAlgorithm::Sha256,
2547            vec![0u8; 32],
2548        );
2549        let result = process_reference(
2550            &reference,
2551            &resolver,
2552            sig_node,
2553            ReferenceSet::SignedInfo,
2554            7,
2555            false,
2556        )
2557        .unwrap();
2558        assert!(matches!(
2559            result.status,
2560            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
2561        ));
2562    }
2563
2564    #[test]
2565    fn reference_stores_pre_digest_data() {
2566        let xml = "<root><child>text</child></root>";
2567        let doc = Document::parse(xml).unwrap();
2568        let resolver = UriReferenceResolver::new(&doc);
2569
2570        // No transforms, no enveloped — just canonicalize entire document
2571        let initial_data = resolver.dereference("").unwrap();
2572        let pre_digest =
2573            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2574        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2575
2576        let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
2577        let result = process_reference(
2578            &reference,
2579            &resolver,
2580            doc.root_element(),
2581            ReferenceSet::SignedInfo,
2582            0,
2583            true,
2584        )
2585        .unwrap();
2586
2587        assert!(matches!(result.status, DsigStatus::Valid));
2588        assert!(result.pre_digest_data.is_some());
2589        assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
2590    }
2591
2592    // ── process_reference: URI dereference ───────────────────────────
2593
2594    #[test]
2595    fn reference_with_id_uri() {
2596        let xml = r##"<root>
2597            <item ID="target">specific content</item>
2598            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2599                <ds:SignedInfo/>
2600            </ds:Signature>
2601        </root>"##;
2602        let doc = Document::parse(xml).unwrap();
2603        let resolver = UriReferenceResolver::new(&doc);
2604        let sig_node = doc
2605            .descendants()
2606            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2607            .unwrap();
2608
2609        // Compute expected digest for the #target subtree
2610        let initial_data = resolver.dereference("#target").unwrap();
2611        let transforms = vec![Transform::C14n(
2612            crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
2613                .unwrap(),
2614        )];
2615        let pre_digest =
2616            crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
2617        let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2618
2619        let reference = make_reference(
2620            "#target",
2621            transforms,
2622            DigestAlgorithm::Sha256,
2623            expected_digest,
2624        );
2625        let result = process_reference(
2626            &reference,
2627            &resolver,
2628            sig_node,
2629            ReferenceSet::SignedInfo,
2630            0,
2631            false,
2632        )
2633        .unwrap();
2634        assert!(matches!(result.status, DsigStatus::Valid));
2635    }
2636
2637    #[test]
2638    fn reference_with_nonexistent_id_fails() {
2639        let xml = "<root><child/></root>";
2640        let doc = Document::parse(xml).unwrap();
2641        let resolver = UriReferenceResolver::new(&doc);
2642
2643        let reference =
2644            make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
2645        let result = process_reference(
2646            &reference,
2647            &resolver,
2648            doc.root_element(),
2649            ReferenceSet::SignedInfo,
2650            0,
2651            false,
2652        );
2653        assert!(result.is_err());
2654    }
2655
2656    #[test]
2657    fn reference_with_absent_uri_fails_closed() {
2658        let xml = "<root><child>text</child></root>";
2659        let doc = Document::parse(xml).unwrap();
2660        let resolver = UriReferenceResolver::new(&doc);
2661
2662        let reference = Reference {
2663            uri: None, // absent URI
2664            id: None,
2665            ref_type: None,
2666            transforms: vec![],
2667            digest_method: DigestAlgorithm::Sha256,
2668            digest_value: vec![0; 32],
2669        };
2670
2671        let result = process_reference(
2672            &reference,
2673            &resolver,
2674            doc.root_element(),
2675            ReferenceSet::SignedInfo,
2676            0,
2677            false,
2678        );
2679        assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
2680    }
2681
2682    // ── process_all_references: fail-fast ────────────────────────────
2683
2684    #[test]
2685    fn all_references_pass() {
2686        let xml = "<root><child>text</child></root>";
2687        let doc = Document::parse(xml).unwrap();
2688        let resolver = UriReferenceResolver::new(&doc);
2689
2690        // Compute correct digest
2691        let initial_data = resolver.dereference("").unwrap();
2692        let pre_digest =
2693            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2694        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2695
2696        let refs = vec![
2697            make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
2698            make_reference("", vec![], DigestAlgorithm::Sha256, digest),
2699        ];
2700
2701        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
2702        assert!(result.all_valid());
2703        assert_eq!(result.results.len(), 2);
2704        assert!(result.first_failure.is_none());
2705    }
2706
2707    #[test]
2708    fn reference_processing_shares_xpath_work_across_references() {
2709        // A signature-wide meter must not reset when processing the next
2710        // Reference, even though each transform chain is independently valid.
2711        let document = Document::parse("<root/>").unwrap();
2712        let resolver = UriReferenceResolver::new(&document);
2713        let transform = Transform::XPath(super::super::transforms::XPathExpression::new("true()"));
2714        let initial_data = resolver.dereference("").unwrap();
2715        let pre_digest = crate::xmldsig::execute_transforms(
2716            document.root_element(),
2717            initial_data,
2718            std::slice::from_ref(&transform),
2719        )
2720        .unwrap();
2721        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2722        let references = vec![
2723            make_reference(
2724                "",
2725                vec![transform.clone()],
2726                DigestAlgorithm::Sha256,
2727                digest.clone(),
2728            ),
2729            make_reference("", vec![transform], DigestAlgorithm::Sha256, digest),
2730        ];
2731        let budget = TransformExecutionBudget::with_xpath_limit(12);
2732        let execution = ReferenceExecutionContext {
2733            store_pre_digest: false,
2734            transform_options: TransformOptions::default(),
2735            transform_budget: &budget,
2736        };
2737
2738        let error = process_all_references_with_options(
2739            &references,
2740            &resolver,
2741            document.root_element(),
2742            &execution,
2743        )
2744        .expect_err("the second Reference must consume the first Reference's XPath work");
2745
2746        assert!(error.to_string().contains("signature-wide"));
2747    }
2748
2749    #[test]
2750    fn reference_processing_shares_node_set_materialization_across_references() {
2751        // Repeated references to the same small subtree must share one owned-
2752        // string budget. Otherwise a large inherited namespace can be cloned
2753        // once per Reference even when canonicalization emits little output.
2754        let document = Document::parse(
2755            r#"<root xmlns:n="urn:0123456789"><target Id="selected">payload</target></root>"#,
2756        )
2757        .unwrap();
2758        let resolver = UriReferenceResolver::new(&document);
2759        let initial_data = resolver.dereference("#selected").unwrap();
2760        let pre_digest =
2761            crate::xmldsig::execute_transforms(document.root_element(), initial_data, &[]).unwrap();
2762        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2763        let references = vec![
2764            make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest.clone()),
2765            make_reference("#selected", vec![], DigestAlgorithm::Sha256, digest),
2766        ];
2767        let budget = TransformExecutionBudget::with_node_set_materialization_limit(30);
2768        let execution = ReferenceExecutionContext {
2769            store_pre_digest: false,
2770            transform_options: TransformOptions::default(),
2771            transform_budget: &budget,
2772        };
2773
2774        let error = process_all_references_with_options(
2775            &references,
2776            &resolver,
2777            document.root_element(),
2778            &execution,
2779        )
2780        .expect_err("the second Reference must consume the first Reference's materialization work");
2781
2782        assert!(error.to_string().contains("cumulative owned string bytes"));
2783    }
2784
2785    #[test]
2786    fn fail_fast_on_first_mismatch() {
2787        let xml = "<root><child>text</child></root>";
2788        let doc = Document::parse(xml).unwrap();
2789        let resolver = UriReferenceResolver::new(&doc);
2790
2791        let wrong_digest = vec![0u8; 32];
2792        let refs = vec![
2793            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
2794            // Second reference should NOT be processed
2795            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
2796        ];
2797
2798        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
2799        assert!(!result.all_valid());
2800        assert_eq!(result.first_failure, Some(0));
2801        // Only first reference should be in results (fail-fast)
2802        assert_eq!(result.results.len(), 1);
2803        assert!(matches!(
2804            result.results[0].status,
2805            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
2806        ));
2807    }
2808
2809    #[test]
2810    fn fail_fast_second_reference() {
2811        let xml = "<root><child>text</child></root>";
2812        let doc = Document::parse(xml).unwrap();
2813        let resolver = UriReferenceResolver::new(&doc);
2814
2815        // Compute correct digest for first ref
2816        let initial_data = resolver.dereference("").unwrap();
2817        let pre_digest =
2818            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2819        let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
2820        let wrong_digest = vec![0u8; 32];
2821
2822        let refs = vec![
2823            make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
2824            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
2825        ];
2826
2827        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
2828        assert!(!result.all_valid());
2829        assert_eq!(result.first_failure, Some(1));
2830        // Both references should be in results
2831        assert_eq!(result.results.len(), 2);
2832        assert!(matches!(result.results[0].status, DsigStatus::Valid));
2833        assert!(matches!(
2834            result.results[1].status,
2835            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
2836        ));
2837    }
2838
2839    #[test]
2840    fn empty_references_list() {
2841        let xml = "<root/>";
2842        let doc = Document::parse(xml).unwrap();
2843        let resolver = UriReferenceResolver::new(&doc);
2844
2845        let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
2846        assert!(result.all_valid());
2847        assert!(result.results.is_empty());
2848    }
2849
2850    // ── Digest algorithms ────────────────────────────────────────────
2851
2852    #[test]
2853    fn reference_sha1_digest() {
2854        let xml = "<root>content</root>";
2855        let doc = Document::parse(xml).unwrap();
2856        let resolver = UriReferenceResolver::new(&doc);
2857
2858        let initial_data = resolver.dereference("").unwrap();
2859        let pre_digest =
2860            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2861        let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
2862
2863        let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
2864        let result = process_reference(
2865            &reference,
2866            &resolver,
2867            doc.root_element(),
2868            ReferenceSet::SignedInfo,
2869            0,
2870            false,
2871        )
2872        .unwrap();
2873        assert!(matches!(result.status, DsigStatus::Valid));
2874        assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
2875    }
2876
2877    #[test]
2878    fn reference_sha512_digest() {
2879        let xml = "<root>content</root>";
2880        let doc = Document::parse(xml).unwrap();
2881        let resolver = UriReferenceResolver::new(&doc);
2882
2883        let initial_data = resolver.dereference("").unwrap();
2884        let pre_digest =
2885            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
2886        let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
2887
2888        let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
2889        let result = process_reference(
2890            &reference,
2891            &resolver,
2892            doc.root_element(),
2893            ReferenceSet::SignedInfo,
2894            0,
2895            false,
2896        )
2897        .unwrap();
2898        assert!(matches!(result.status, DsigStatus::Valid));
2899        assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
2900    }
2901
2902    // ── SAML-like end-to-end ─────────────────────────────────────────
2903
2904    #[test]
2905    fn saml_enveloped_reference_processing() {
2906        // Realistic SAML Response with enveloped signature
2907        let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
2908                                     xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
2909                                     ID="_resp1">
2910            <saml:Assertion ID="_assert1">
2911                <saml:Subject>user@example.com</saml:Subject>
2912            </saml:Assertion>
2913            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2914                <ds:SignedInfo>
2915                    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2916                    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2917                    <ds:Reference URI="">
2918                        <ds:Transforms>
2919                            <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
2920                            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2921                        </ds:Transforms>
2922                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2923                        <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
2924                    </ds:Reference>
2925                </ds:SignedInfo>
2926                <ds:SignatureValue>fakesig==</ds:SignatureValue>
2927            </ds:Signature>
2928        </samlp:Response>"##;
2929        let doc = Document::parse(xml).unwrap();
2930        let resolver = UriReferenceResolver::new(&doc);
2931        let sig_node = doc
2932            .descendants()
2933            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2934            .unwrap();
2935
2936        // Parse SignedInfo to get the Reference
2937        let signed_info_node = sig_node
2938            .children()
2939            .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
2940            .unwrap();
2941        let signed_info = parse_signed_info(signed_info_node).unwrap();
2942        let reference = &signed_info.references[0];
2943
2944        // Compute the correct digest by running the actual pipeline
2945        let initial_data = resolver.dereference("").unwrap();
2946        let pre_digest =
2947            crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
2948                .unwrap();
2949        let correct_digest = compute_digest(reference.digest_method, &pre_digest);
2950
2951        // Build a reference with the correct digest
2952        let corrected_ref = make_reference(
2953            "",
2954            reference.transforms.clone(),
2955            reference.digest_method,
2956            correct_digest,
2957        );
2958
2959        // Verify: should pass
2960        let result = process_reference(
2961            &corrected_ref,
2962            &resolver,
2963            sig_node,
2964            ReferenceSet::SignedInfo,
2965            0,
2966            true,
2967        )
2968        .unwrap();
2969        assert!(
2970            matches!(result.status, DsigStatus::Valid),
2971            "SAML reference should verify"
2972        );
2973        assert!(result.pre_digest_data.is_some());
2974
2975        // Verify the pre-digest data contains the canonicalized document without Signature
2976        let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
2977        assert!(
2978            pre_digest_str.contains("samlp:Response"),
2979            "pre-digest should contain Response"
2980        );
2981        assert!(
2982            !pre_digest_str.contains("SignatureValue"),
2983            "pre-digest should NOT contain Signature"
2984        );
2985    }
2986
2987    #[test]
2988    fn pipeline_missing_signed_info_returns_missing_element() {
2989        let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
2990
2991        let err = verify_signature_with_pem_key(xml, "dummy-key", false)
2992            .expect_err("missing SignedInfo must fail before crypto stage");
2993        assert!(matches!(
2994            err,
2995            SignatureVerificationPipelineError::MissingElement {
2996                element: "SignedInfo"
2997            }
2998        ));
2999    }
3000
3001    #[test]
3002    fn pipeline_multiple_signature_elements_are_rejected() {
3003        let xml = r#"
3004<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3005  <ds:Signature>
3006    <ds:SignedInfo/>
3007  </ds:Signature>
3008  <ds:Signature/>
3009</root>
3010"#;
3011
3012        let err = verify_signature_with_pem_key(xml, "dummy-key", false)
3013            .expect_err("multiple signatures must fail closed");
3014        assert!(matches!(
3015            err,
3016            SignatureVerificationPipelineError::InvalidStructure {
3017                reason: "Signature must appear exactly once in document",
3018            }
3019        ));
3020    }
3021
3022    #[test]
3023    fn pipeline_reports_keyinfo_parse_error() {
3024        let xml = r#"
3025<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
3026              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
3027  <ds:SignedInfo>
3028    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3029    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3030    <ds:Reference URI="">
3031      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3032      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
3033    </ds:Reference>
3034  </ds:SignedInfo>
3035  <ds:SignatureValue>AA==</ds:SignatureValue>
3036  <ds:KeyInfo>
3037    <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
3038  </ds:KeyInfo>
3039</ds:Signature>
3040"#;
3041
3042        let err = VerifyContext::new().verify(xml).expect_err(
3043            "invalid KeyInfo must map to ParseKeyInfo when no explicit key is supplied",
3044        );
3045        assert!(matches!(
3046            err,
3047            SignatureVerificationPipelineError::ParseKeyInfo(_)
3048        ));
3049    }
3050
3051    #[test]
3052    fn pipeline_ignores_malformed_keyinfo_when_explicit_key_is_supplied() {
3053        let base_xml = signature_with_target_reference("AQ==");
3054        let xml = base_xml
3055            .replace(
3056                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
3057                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">"#,
3058            )
3059            .replace(
3060                "</ds:SignatureValue>\n  </ds:Signature>",
3061                "</ds:SignatureValue>\n    <ds:KeyInfo><dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue></ds:KeyInfo>\n  </ds:Signature>",
3062            );
3063
3064        let result = VerifyContext::new()
3065            .key(&RejectingKey)
3066            .verify(&xml)
3067            .expect("explicit key path should not fail on malformed KeyInfo");
3068        assert!(matches!(
3069            result.status,
3070            DsigStatus::Invalid(FailureReason::SignatureMismatch)
3071        ));
3072    }
3073
3074    #[test]
3075    fn pipeline_rejects_foreign_element_children_under_signature() {
3076        let base_xml = signature_with_target_reference("AQ==");
3077        let xml = base_xml
3078            .replace(
3079                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">"#,
3080                r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:foo="urn:example:foo">"#,
3081            )
3082            .replace(
3083                "</ds:SignedInfo>\n    <ds:SignatureValue>",
3084                "</ds:SignedInfo>\n    <foo:Bar/>\n    <ds:SignatureValue>",
3085            );
3086
3087        let err = VerifyContext::new()
3088            .key(&RejectingKey)
3089            .verify(&xml)
3090            .expect_err("foreign element children under Signature must fail closed");
3091        assert!(matches!(
3092            err,
3093            SignatureVerificationPipelineError::InvalidStructure {
3094                reason: "Signature must contain only XMLDSIG element children",
3095            }
3096        ));
3097    }
3098
3099    #[test]
3100    fn pipeline_rejects_non_whitespace_mixed_content_under_signature() {
3101        let base_xml = signature_with_target_reference("AQ==");
3102        let xml = base_xml.replace(
3103            "</ds:SignedInfo>\n    <ds:SignatureValue>",
3104            "</ds:SignedInfo>\n    oops\n    <ds:SignatureValue>",
3105        );
3106
3107        let err = VerifyContext::new()
3108            .key(&RejectingKey)
3109            .verify(&xml)
3110            .expect_err("non-whitespace mixed content under Signature must fail closed");
3111        assert!(matches!(
3112            err,
3113            SignatureVerificationPipelineError::InvalidStructure {
3114                reason: "Signature must not contain non-whitespace mixed content",
3115            }
3116        ));
3117    }
3118
3119    #[test]
3120    fn pipeline_rejects_keyinfo_out_of_order() {
3121        let base_xml = signature_with_target_reference("AQ==");
3122        let xml = base_xml.replace(
3123            "</ds:SignatureValue>\n  </ds:Signature>",
3124            "</ds:SignatureValue>\n    <ds:Object/>\n    <ds:KeyInfo><ds:KeyName>late</ds:KeyName></ds:KeyInfo>\n  </ds:Signature>",
3125        );
3126
3127        let err = VerifyContext::new()
3128            .key(&RejectingKey)
3129            .verify(&xml)
3130            .expect_err("KeyInfo after Object must be rejected by Signature child order checks");
3131        assert!(matches!(
3132            err,
3133            SignatureVerificationPipelineError::InvalidStructure {
3134                reason: "KeyInfo must be the third element child of Signature when present"
3135            }
3136        ));
3137    }
3138
3139    #[test]
3140    fn pipeline_accepts_comments_and_processing_instructions_under_signature() {
3141        let xml = r#"
3142<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3143  <?dbg keep ?>
3144  <!-- signature metadata -->
3145  <ds:SignedInfo>
3146    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3147    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3148    <ds:Reference URI="">
3149      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3150      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
3151    </ds:Reference>
3152  </ds:SignedInfo>
3153  <!-- between required children -->
3154  <ds:SignatureValue>AA==</ds:SignatureValue>
3155</ds:Signature>
3156"#;
3157
3158        let doc = Document::parse(xml).expect("test XML must parse");
3159        let signature_node = doc.root_element();
3160        let parsed = parse_signature_children(signature_node)
3161            .expect("comment/PI nodes under Signature must be ignored");
3162
3163        assert_eq!(parsed.signed_info_node.tag_name().name(), "SignedInfo");
3164        assert_eq!(
3165            parsed.signature_value_node.tag_name().name(),
3166            "SignatureValue"
3167        );
3168        assert!(parsed.key_info_node.is_none());
3169    }
3170}