Skip to main content

xml_sec/xmldsig/
verify.rs

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