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