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