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};
15use std::collections::HashSet;
16
17use crate::c14n::canonicalize;
18
19use super::digest::{DigestAlgorithm, compute_digest, constant_time_eq};
20use super::parse::parse_signed_info;
21use super::parse::{Reference, SignatureAlgorithm, XMLDSIG_NS};
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;
29
30const MAX_SIGNATURE_VALUE_LEN: usize = 8192;
31const MAX_SIGNATURE_VALUE_TEXT_LEN: usize = 65_536;
32const MANIFEST_REFERENCE_TYPE_URI: &str = "http://www.w3.org/2000/09/xmldsig#Manifest";
33/// Cryptographic verifier used by [`VerifyContext`].
34///
35/// This trait intentionally has no `Send + Sync` supertraits so lightweight
36/// single-threaded verifiers can be used without additional bounds.
37pub trait VerifyingKey {
38    /// Verify `signature_value` over `signed_data` with the declared algorithm.
39    fn verify(
40        &self,
41        algorithm: SignatureAlgorithm,
42        signed_data: &[u8],
43        signature_value: &[u8],
44    ) -> Result<bool, SignatureVerificationPipelineError>;
45}
46
47/// Key resolver hook used by [`VerifyContext`] when no pre-set key is provided.
48///
49/// This trait intentionally has no `Send + Sync` supertraits; callers that need
50/// cross-thread sharing can wrap resolvers/keys in their own thread-safe types.
51pub trait KeyResolver {
52    /// Resolve a verification key for the provided XML document.
53    ///
54    /// Return `Ok(None)` when no suitable key could be resolved from available
55    /// key material (for example, missing `<KeyInfo>` candidates). `VerifyContext`
56    /// maps `Ok(None)` to `DsigStatus::Invalid(FailureReason::KeyNotFound)`;
57    /// reserve `Err(...)` for resolver failures.
58    fn resolve<'a>(
59        &'a self,
60        xml: &str,
61    ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>;
62}
63
64/// Allowed URI classes for `<Reference URI="...">`.
65///
66/// Note: `UriReferenceResolver` currently supports only same-document URIs.
67/// Allowing external URIs via this policy only disables the early policy
68/// rejection; dereference still fails until an external resolver path is added.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70#[must_use = "pass the policy to VerifyContext::allowed_uri_types(), or store it for reuse"]
71pub struct UriTypeSet {
72    allow_empty: bool,
73    allow_same_document: bool,
74    allow_external: bool,
75}
76
77impl UriTypeSet {
78    /// Create a custom URI policy.
79    pub const fn new(allow_empty: bool, allow_same_document: bool, allow_external: bool) -> Self {
80        Self {
81            allow_empty,
82            allow_same_document,
83            allow_external,
84        }
85    }
86
87    /// Allow only same-document references (`""`, `#id`, `#xpointer(...)`).
88    pub const SAME_DOCUMENT: Self = Self {
89        allow_empty: true,
90        allow_same_document: true,
91        allow_external: false,
92    };
93
94    /// Allow all URI classes.
95    ///
96    /// This includes external URI classes at policy level, but external
97    /// dereference is not implemented yet by the default resolver.
98    pub const ALL: Self = Self {
99        allow_empty: true,
100        allow_same_document: true,
101        allow_external: true,
102    };
103
104    fn allows(self, uri: &str) -> bool {
105        if uri.is_empty() {
106            return self.allow_empty;
107        }
108        if uri.starts_with('#') {
109            return self.allow_same_document;
110        }
111        self.allow_external
112    }
113}
114
115impl Default for UriTypeSet {
116    fn default() -> Self {
117        Self::SAME_DOCUMENT
118    }
119}
120
121/// Verification builder/configuration.
122#[must_use = "configure the context and call verify(), or store it for reuse"]
123pub struct VerifyContext<'a> {
124    key: Option<&'a dyn VerifyingKey>,
125    key_resolver: Option<&'a dyn KeyResolver>,
126    process_manifests: bool,
127    allowed_uri_types: UriTypeSet,
128    allowed_transforms: Option<HashSet<String>>,
129    store_pre_digest: bool,
130}
131
132impl<'a> VerifyContext<'a> {
133    /// Create a context with conservative defaults.
134    ///
135    /// Defaults:
136    /// - no pre-set key, no key resolver
137    /// - manifests disabled
138    /// - same-document URIs only
139    /// - all transforms allowed
140    /// - pre-digest buffers not stored
141    pub fn new() -> Self {
142        Self {
143            key: None,
144            key_resolver: None,
145            process_manifests: false,
146            allowed_uri_types: UriTypeSet::default(),
147            allowed_transforms: None,
148            store_pre_digest: false,
149        }
150    }
151
152    /// Set a pre-resolved verification key.
153    pub fn key(mut self, key: &'a dyn VerifyingKey) -> Self {
154        self.key = Some(key);
155        self
156    }
157
158    /// Set a key resolver fallback used when `key()` is not provided.
159    pub fn key_resolver(mut self, resolver: &'a dyn KeyResolver) -> Self {
160        self.key_resolver = Some(resolver);
161        self
162    }
163
164    /// Enable or disable `<Manifest>` processing.
165    ///
166    /// Note: manifest verification is not implemented yet. When enabled, the
167    /// verifier fails closed with `ManifestProcessingUnsupported` if a
168    /// `<ds:Manifest>` is present under `<ds:Object>` or if a
169    /// `<Reference Type="http://www.w3.org/2000/09/xmldsig#Manifest">` is present.
170    pub fn process_manifests(mut self, enabled: bool) -> Self {
171        self.process_manifests = enabled;
172        self
173    }
174
175    /// Restrict allowed reference URI classes.
176    pub fn allowed_uri_types(mut self, types: UriTypeSet) -> Self {
177        self.allowed_uri_types = types;
178        self
179    }
180
181    /// Restrict allowed transform algorithms by URI.
182    ///
183    /// Example values:
184    /// - `http://www.w3.org/2000/09/xmldsig#enveloped-signature`
185    /// - `http://www.w3.org/2001/10/xml-exc-c14n#`
186    ///
187    /// When a `<Reference>` has no explicit canonicalization transform, XMLDSig
188    /// applies implicit default C14N (`http://www.w3.org/TR/2001/REC-xml-c14n-20010315`).
189    /// If an allowlist is configured, include that URI as well unless all
190    /// references use explicit `Transform::C14n(...)`.
191    pub fn allowed_transforms<I, S>(mut self, transforms: I) -> Self
192    where
193        I: IntoIterator<Item = S>,
194        S: Into<String>,
195    {
196        self.allowed_transforms = Some(transforms.into_iter().map(Into::into).collect());
197        self
198    }
199
200    /// Store pre-digest buffers for diagnostics.
201    pub fn store_pre_digest(mut self, enabled: bool) -> Self {
202        self.store_pre_digest = enabled;
203        self
204    }
205
206    fn allowed_transform_uris(&self) -> Option<&HashSet<String>> {
207        self.allowed_transforms.as_ref()
208    }
209
210    /// Verify one XMLDSig signature using this context.
211    ///
212    /// Returns `Ok(VerifyResult)` for both valid and invalid signatures; inspect
213    /// `VerifyResult::status` for the verification outcome. `Err(...)` is
214    /// reserved for pipeline failures.
215    pub fn verify(&self, xml: &str) -> Result<VerifyResult, SignatureVerificationPipelineError> {
216        verify_signature_with_context(xml, self)
217    }
218}
219
220impl Default for VerifyContext<'_> {
221    fn default() -> Self {
222        Self::new()
223    }
224}
225
226/// Per-reference verification result.
227#[derive(Debug)]
228#[non_exhaustive]
229#[must_use = "inspect status before accepting the reference result"]
230pub struct ReferenceResult {
231    /// URI from the `<Reference>` element (for diagnostics).
232    pub uri: String,
233    /// Digest algorithm used.
234    pub digest_algorithm: DigestAlgorithm,
235    /// Reference verification status.
236    pub status: DsigStatus,
237    /// Pre-digest bytes (populated when `store_pre_digest` is enabled).
238    pub pre_digest_data: Option<Vec<u8>>,
239}
240
241/// Verification status.
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243#[non_exhaustive]
244pub enum DsigStatus {
245    /// Signature/reference is cryptographically valid.
246    Valid,
247    /// Signature/reference is invalid with a concrete reason.
248    Invalid(FailureReason),
249}
250
251/// Why XMLDSig verification failed.
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253#[non_exhaustive]
254pub enum FailureReason {
255    /// `<DigestValue>` mismatch for a `<Reference>` at `ref_index`.
256    ReferenceDigestMismatch {
257        /// Zero-based index of the failing `<Reference>` in `<SignedInfo>`.
258        ref_index: usize,
259    },
260    /// `<SignatureValue>` does not match canonicalized `<SignedInfo>`.
261    SignatureMismatch,
262    /// No verification key was configured or could be resolved.
263    KeyNotFound,
264}
265
266/// Result of processing all `<Reference>` elements in `<SignedInfo>`.
267#[derive(Debug)]
268#[non_exhaustive]
269#[must_use = "check first_failure/results before accepting the reference set"]
270pub struct ReferencesResult {
271    /// Per-reference results (one per `<Reference>` in order).
272    /// On fail-fast, only references up to and including the failed one are present.
273    pub results: Vec<ReferenceResult>,
274    /// Index of the first failed reference, if any.
275    pub first_failure: Option<usize>,
276}
277
278impl ReferencesResult {
279    /// Whether all references passed digest verification.
280    #[must_use]
281    pub fn all_valid(&self) -> bool {
282        self.results
283            .iter()
284            .all(|result| matches!(result.status, DsigStatus::Valid))
285    }
286}
287
288/// Process a single `<Reference>`: dereference URI → apply transforms → compute
289/// digest → compare with stored `<DigestValue>`.
290///
291/// # Arguments
292///
293/// - `reference`: The parsed `<Reference>` element.
294/// - `resolver`: URI resolver for the document.
295/// - `signature_node`: The `<Signature>` element (for enveloped-signature transform).
296/// - `ref_index`: Zero-based index of this reference in `<SignedInfo>`.
297/// - `store_pre_digest`: If true, store the pre-digest bytes in the result.
298///
299/// # Errors
300///
301/// Returns `Err` for processing failures (URI dereference, transform errors).
302/// Digest mismatch is NOT an error — it produces
303/// `Ok(ReferenceResult { status: Invalid(ReferenceDigestMismatch { .. }) })`.
304pub fn process_reference(
305    reference: &Reference,
306    resolver: &UriReferenceResolver<'_>,
307    signature_node: Node<'_, '_>,
308    ref_index: usize,
309    store_pre_digest: bool,
310) -> Result<ReferenceResult, ReferenceProcessingError> {
311    // 1. Dereference URI. Omitted URI is distinct from URI="" in XMLDSig and
312    // must be rejected until caller-provided external object resolution exists.
313    let uri = reference
314        .uri
315        .as_deref()
316        .ok_or(ReferenceProcessingError::MissingUri)?;
317    let initial_data = resolver
318        .dereference(uri)
319        .map_err(ReferenceProcessingError::UriDereference)?;
320
321    // 2. Apply transform chain
322    let pre_digest_bytes = execute_transforms(signature_node, initial_data, &reference.transforms)
323        .map_err(ReferenceProcessingError::Transform)?;
324
325    // 3. Compute digest
326    let computed_digest = compute_digest(reference.digest_method, &pre_digest_bytes);
327
328    // 4. Compare with stored DigestValue (constant-time)
329    let status = if constant_time_eq(&computed_digest, &reference.digest_value) {
330        DsigStatus::Valid
331    } else {
332        DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index })
333    };
334
335    Ok(ReferenceResult {
336        uri: uri.to_owned(),
337        digest_algorithm: reference.digest_method,
338        status,
339        pre_digest_data: if store_pre_digest {
340            Some(pre_digest_bytes)
341        } else {
342            None
343        },
344    })
345}
346
347/// Process all `<Reference>` elements in a `<SignedInfo>`, with fail-fast
348/// on the first digest mismatch.
349///
350/// Per XMLDSig spec: if any reference fails, the entire signature is invalid.
351/// Processing stops at the first failure for efficiency.
352///
353/// # Errors
354///
355/// Returns `Err` only for processing failures (malformed XML, unsupported
356/// transform, etc.). Digest mismatches are reported via
357/// `ReferencesResult::first_failure`.
358pub fn process_all_references(
359    references: &[Reference],
360    resolver: &UriReferenceResolver<'_>,
361    signature_node: Node<'_, '_>,
362    store_pre_digest: bool,
363) -> Result<ReferencesResult, ReferenceProcessingError> {
364    let mut results = Vec::with_capacity(references.len());
365
366    for (i, reference) in references.iter().enumerate() {
367        let result = process_reference(reference, resolver, signature_node, i, store_pre_digest)?;
368        let failed = matches!(result.status, DsigStatus::Invalid(_));
369        results.push(result);
370
371        if failed {
372            return Ok(ReferencesResult {
373                results,
374                first_failure: Some(i),
375            });
376        }
377    }
378
379    Ok(ReferencesResult {
380        results,
381        first_failure: None,
382    })
383}
384
385/// Errors during reference processing.
386///
387/// Distinct from digest mismatch (which is a validation result, not a processing error).
388#[derive(Debug, thiserror::Error)]
389#[non_exhaustive]
390pub enum ReferenceProcessingError {
391    /// `<Reference>` omitted the `URI` attribute, which we do not resolve implicitly.
392    #[error("reference URI is required; omitted URI references are not supported")]
393    MissingUri,
394
395    /// URI dereference failed.
396    #[error("URI dereference failed: {0}")]
397    UriDereference(super::types::TransformError),
398
399    /// Transform execution failed.
400    #[error("transform failed: {0}")]
401    Transform(super::types::TransformError),
402}
403
404/// End-to-end XMLDSig verification result for one `<Signature>`.
405#[derive(Debug)]
406#[non_exhaustive]
407#[must_use = "inspect status before accepting the document"]
408pub struct VerifyResult {
409    /// Final XMLDSig status for this signature.
410    pub status: DsigStatus,
411    /// `<Reference>` verification results from `<SignedInfo>`.
412    /// On fail-fast, this includes references up to and including
413    /// the first digest mismatch only.
414    pub signed_info_references: Vec<ReferenceResult>,
415    /// `<Manifest>` reference results. Empty until manifest processing is implemented.
416    pub manifest_references: Vec<ReferenceResult>,
417    /// Canonicalized `<SignedInfo>` bytes when `store_pre_digest` is enabled
418    /// and verification reaches SignedInfo canonicalization.
419    pub canonicalized_signed_info: Option<Vec<u8>>,
420}
421
422/// Errors while running end-to-end XMLDSig verification.
423#[derive(Debug, thiserror::Error)]
424#[non_exhaustive]
425pub enum SignatureVerificationPipelineError {
426    /// XML parsing failed.
427    #[error("XML parse error: {0}")]
428    XmlParse(#[from] roxmltree::Error),
429
430    /// Required signature element is missing.
431    #[error("missing required element: <{element}>")]
432    MissingElement {
433        /// Name of the missing element.
434        element: &'static str,
435    },
436
437    /// Signature element tree shape violates XMLDSig structure requirements.
438    #[error("invalid Signature structure: {reason}")]
439    InvalidStructure {
440        /// Validation failure reason.
441        reason: &'static str,
442    },
443
444    /// `<SignedInfo>` parsing failed.
445    #[error("failed to parse SignedInfo: {0}")]
446    ParseSignedInfo(#[from] super::parse::ParseError),
447
448    /// Reference processing failed.
449    #[error("reference processing failed: {0}")]
450    Reference(#[from] ReferenceProcessingError),
451
452    /// SignedInfo canonicalization failed.
453    #[error("SignedInfo canonicalization failed: {0}")]
454    Canonicalization(#[from] crate::c14n::C14nError),
455
456    /// SignatureValue base64 decoding failed.
457    #[error("invalid SignatureValue base64: {0}")]
458    SignatureValueBase64(#[from] base64::DecodeError),
459
460    /// Cryptographic verification failed before validity decision.
461    #[error("signature verification failed: {0}")]
462    Crypto(#[from] SignatureVerificationError),
463
464    /// A `<Reference>` URI class is rejected by policy.
465    #[error("reference URI is not allowed by policy: {uri}")]
466    DisallowedUri {
467        /// Offending URI value from `<Reference URI="...">`.
468        uri: String,
469    },
470
471    /// A `<Transform>` algorithm is rejected by policy.
472    #[error("transform is not allowed by policy: {algorithm}")]
473    DisallowedTransform {
474        /// Rejected transform algorithm URI.
475        algorithm: String,
476    },
477
478    /// Manifest processing was requested but is not implemented in this phase.
479    #[error("manifest processing is not implemented yet")]
480    ManifestProcessingUnsupported,
481}
482
483/// Verify one XMLDSig `<Signature>` end-to-end with a PEM public key.
484///
485/// Pipeline:
486/// 1. Parse `<SignedInfo>`
487/// 2. Validate all `<Reference>` digests (fail-fast)
488/// 3. Canonicalize `<SignedInfo>`
489/// 4. Base64-decode `<SignatureValue>`
490/// 5. Verify signature bytes against canonicalized `<SignedInfo>`
491///
492/// If any `<Reference>` digest mismatches, returns `Ok` with
493/// `status == Invalid(ReferenceDigestMismatch { .. })`.
494///
495/// Structural constraints enforced by this API:
496/// - The document must contain exactly one XMLDSig `<Signature>` element.
497/// - `<SignedInfo>` must be the first element child of `<Signature>` and appear once.
498/// - `<SignatureValue>` must be the second element child of `<Signature>` and appear once.
499/// - `<SignatureValue>` must not contain nested element children.
500pub fn verify_signature_with_pem_key(
501    xml: &str,
502    public_key_pem: &str,
503    store_pre_digest: bool,
504) -> Result<VerifyResult, SignatureVerificationPipelineError> {
505    struct PemVerifyingKey<'a> {
506        public_key_pem: &'a str,
507    }
508
509    impl VerifyingKey for PemVerifyingKey<'_> {
510        fn verify(
511            &self,
512            algorithm: SignatureAlgorithm,
513            signed_data: &[u8],
514            signature_value: &[u8],
515        ) -> Result<bool, SignatureVerificationPipelineError> {
516            verify_with_algorithm(algorithm, self.public_key_pem, signed_data, signature_value)
517        }
518    }
519
520    let key = PemVerifyingKey { public_key_pem };
521    VerifyContext::new()
522        .key(&key)
523        .store_pre_digest(store_pre_digest)
524        .verify(xml)
525}
526
527fn verify_signature_with_context(
528    xml: &str,
529    ctx: &VerifyContext<'_>,
530) -> Result<VerifyResult, SignatureVerificationPipelineError> {
531    let doc = Document::parse(xml)?;
532    let mut signatures = doc.descendants().filter(|node| {
533        node.is_element()
534            && node.tag_name().name() == "Signature"
535            && node.tag_name().namespace() == Some(XMLDSIG_NS)
536    });
537    let signature_node = match (signatures.next(), signatures.next()) {
538        (None, _) => {
539            return Err(SignatureVerificationPipelineError::MissingElement {
540                element: "Signature",
541            });
542        }
543        (Some(node), None) => node,
544        (Some(_), Some(_)) => {
545            return Err(SignatureVerificationPipelineError::InvalidStructure {
546                reason: "Signature must appear exactly once in document",
547            });
548        }
549    };
550
551    let signature_children = parse_signature_children(signature_node)?;
552    let signed_info_node = signature_children.signed_info_node;
553
554    if ctx.process_manifests && has_manifest_children(signature_node) {
555        return Err(SignatureVerificationPipelineError::ManifestProcessingUnsupported);
556    }
557
558    let signed_info = parse_signed_info(signed_info_node)?;
559    if ctx.process_manifests && has_manifest_type_references(&signed_info.references) {
560        return Err(SignatureVerificationPipelineError::ManifestProcessingUnsupported);
561    }
562    enforce_reference_policies(
563        &signed_info.references,
564        ctx.allowed_uri_types,
565        ctx.allowed_transform_uris(),
566    )?;
567
568    let resolver = UriReferenceResolver::new(&doc);
569    let references = process_all_references(
570        &signed_info.references,
571        &resolver,
572        signature_node,
573        ctx.store_pre_digest,
574    )?;
575
576    if let Some(first_failure) = references.first_failure {
577        let status = references.results[first_failure].status;
578        return Ok(VerifyResult {
579            status,
580            signed_info_references: references.results,
581            manifest_references: Vec::new(),
582            canonicalized_signed_info: None,
583        });
584    }
585
586    let signed_info_subtree: HashSet<_> = signed_info_node
587        .descendants()
588        .map(|node: Node<'_, '_>| node.id())
589        .collect();
590    let mut canonical_signed_info = Vec::new();
591    canonicalize(
592        &doc,
593        Some(&|node| signed_info_subtree.contains(&node.id())),
594        &signed_info.c14n_method,
595        &mut canonical_signed_info,
596    )?;
597
598    let signature_value = decode_signature_value(signature_children.signature_value_node)?;
599    let Some(resolved_key) = resolve_verifying_key(ctx, xml)? else {
600        return Ok(VerifyResult {
601            status: DsigStatus::Invalid(FailureReason::KeyNotFound),
602            signed_info_references: references.results,
603            manifest_references: Vec::new(),
604            canonicalized_signed_info: if ctx.store_pre_digest {
605                Some(canonical_signed_info)
606            } else {
607                None
608            },
609        });
610    };
611    let verifier = resolved_key.as_ref();
612    let signature_valid = verifier.verify(
613        signed_info.signature_method,
614        &canonical_signed_info,
615        &signature_value,
616    )?;
617
618    Ok(VerifyResult {
619        status: if signature_valid {
620            DsigStatus::Valid
621        } else {
622            DsigStatus::Invalid(FailureReason::SignatureMismatch)
623        },
624        signed_info_references: references.results,
625        manifest_references: Vec::new(),
626        canonicalized_signed_info: if ctx.store_pre_digest {
627            Some(canonical_signed_info)
628        } else {
629            None
630        },
631    })
632}
633
634fn has_manifest_children(signature_node: Node<'_, '_>) -> bool {
635    signature_node.children().any(|child| {
636        child.is_element()
637            && child.tag_name().namespace() == Some(XMLDSIG_NS)
638            && child.tag_name().name() == "Object"
639            && child.descendants().any(|inner| {
640                inner.is_element()
641                    && inner.tag_name().namespace() == Some(XMLDSIG_NS)
642                    && inner.tag_name().name() == "Manifest"
643            })
644    })
645}
646
647fn has_manifest_type_references(references: &[Reference]) -> bool {
648    references
649        .iter()
650        .any(|reference| reference.ref_type.as_deref() == Some(MANIFEST_REFERENCE_TYPE_URI))
651}
652
653enum ResolvedVerifyingKey<'a> {
654    Borrowed(&'a dyn VerifyingKey),
655    Owned(Box<dyn VerifyingKey + 'a>),
656}
657
658impl ResolvedVerifyingKey<'_> {
659    fn as_ref(&self) -> &dyn VerifyingKey {
660        match self {
661            Self::Borrowed(key) => *key,
662            Self::Owned(key) => key.as_ref(),
663        }
664    }
665}
666
667fn resolve_verifying_key<'k>(
668    ctx: &VerifyContext<'k>,
669    xml: &str,
670) -> Result<Option<ResolvedVerifyingKey<'k>>, SignatureVerificationPipelineError> {
671    if let Some(key) = ctx.key {
672        return Ok(Some(ResolvedVerifyingKey::Borrowed(key)));
673    }
674    if let Some(resolver) = ctx.key_resolver {
675        let resolved = resolver.resolve(xml)?;
676        return Ok(resolved.map(ResolvedVerifyingKey::Owned));
677    }
678    Ok(None)
679}
680
681fn enforce_reference_policies(
682    references: &[Reference],
683    allowed_uri_types: UriTypeSet,
684    allowed_transforms: Option<&HashSet<String>>,
685) -> Result<(), SignatureVerificationPipelineError> {
686    for reference in references {
687        let uri = reference
688            .uri
689            .as_deref()
690            .ok_or(SignatureVerificationPipelineError::Reference(
691                ReferenceProcessingError::MissingUri,
692            ))?;
693        if !allowed_uri_types.allows(uri) {
694            return Err(SignatureVerificationPipelineError::DisallowedUri {
695                uri: uri.to_owned(),
696            });
697        }
698
699        if let Some(allowed) = allowed_transforms {
700            for transform in &reference.transforms {
701                let transform_uri = transform_uri(transform);
702                if !allowed.contains(transform_uri) {
703                    return Err(SignatureVerificationPipelineError::DisallowedTransform {
704                        algorithm: transform_uri.to_owned(),
705                    });
706                }
707            }
708
709            let has_explicit_c14n = reference
710                .transforms
711                .iter()
712                .any(|transform| matches!(transform, Transform::C14n(_)));
713            if !has_explicit_c14n && !allowed.contains(DEFAULT_IMPLICIT_C14N_URI) {
714                return Err(SignatureVerificationPipelineError::DisallowedTransform {
715                    algorithm: DEFAULT_IMPLICIT_C14N_URI.to_owned(),
716                });
717            }
718        }
719    }
720    Ok(())
721}
722
723fn transform_uri(transform: &Transform) -> &'static str {
724    match transform {
725        Transform::Enveloped => super::transforms::ENVELOPED_SIGNATURE_URI,
726        Transform::XpathExcludeAllSignatures => XPATH_TRANSFORM_URI,
727        Transform::C14n(algo) => algo.uri(),
728    }
729}
730
731#[derive(Debug, Clone, Copy)]
732struct SignatureChildNodes<'a, 'input> {
733    signed_info_node: Node<'a, 'input>,
734    signature_value_node: Node<'a, 'input>,
735}
736
737fn parse_signature_children<'a, 'input>(
738    signature_node: Node<'a, 'input>,
739) -> Result<SignatureChildNodes<'a, 'input>, SignatureVerificationPipelineError> {
740    let mut signed_info_node: Option<Node<'_, '_>> = None;
741    let mut signature_value_node: Option<Node<'_, '_>> = None;
742    let mut signed_info_index: Option<usize> = None;
743    let mut signature_value_index: Option<usize> = None;
744    for (zero_based_index, child) in signature_node
745        .children()
746        .filter(|node| node.is_element())
747        .enumerate()
748    {
749        let element_index = zero_based_index + 1;
750        if child.tag_name().namespace() != Some(XMLDSIG_NS) {
751            continue;
752        }
753        match child.tag_name().name() {
754            "SignedInfo" => {
755                if signed_info_node.is_some() {
756                    return Err(SignatureVerificationPipelineError::InvalidStructure {
757                        reason: "SignedInfo must appear exactly once under Signature",
758                    });
759                }
760                signed_info_node = Some(child);
761                signed_info_index = Some(element_index);
762            }
763            "SignatureValue" => {
764                if signature_value_node.is_some() {
765                    return Err(SignatureVerificationPipelineError::InvalidStructure {
766                        reason: "SignatureValue must appear exactly once under Signature",
767                    });
768                }
769                signature_value_node = Some(child);
770                signature_value_index = Some(element_index);
771            }
772            _ => {}
773        }
774    }
775
776    let signed_info_node =
777        signed_info_node.ok_or(SignatureVerificationPipelineError::MissingElement {
778            element: "SignedInfo",
779        })?;
780    let signature_value_node =
781        signature_value_node.ok_or(SignatureVerificationPipelineError::MissingElement {
782            element: "SignatureValue",
783        })?;
784    if signed_info_index != Some(1) {
785        return Err(SignatureVerificationPipelineError::InvalidStructure {
786            reason: "SignedInfo must be the first element child of Signature",
787        });
788    }
789    if signature_value_index != Some(2) {
790        return Err(SignatureVerificationPipelineError::InvalidStructure {
791            reason: "SignatureValue must be the second element child of Signature",
792        });
793    }
794    Ok(SignatureChildNodes {
795        signed_info_node,
796        signature_value_node,
797    })
798}
799
800fn decode_signature_value(
801    signature_value_node: Node<'_, '_>,
802) -> Result<Vec<u8>, SignatureVerificationPipelineError> {
803    if signature_value_node
804        .children()
805        .any(|child| child.is_element())
806    {
807        return Err(SignatureVerificationPipelineError::InvalidStructure {
808            reason: "SignatureValue must not contain element children",
809        });
810    }
811
812    let mut normalized = String::new();
813    let mut raw_text_len = 0usize;
814    for child in signature_value_node
815        .children()
816        .filter(|child| child.is_text())
817    {
818        if let Some(text) = child.text() {
819            push_normalized_signature_text(text, &mut raw_text_len, &mut normalized)?;
820        }
821    }
822
823    Ok(base64::engine::general_purpose::STANDARD.decode(normalized)?)
824}
825
826fn push_normalized_signature_text(
827    text: &str,
828    raw_text_len: &mut usize,
829    normalized: &mut String,
830) -> Result<(), SignatureVerificationPipelineError> {
831    for ch in text.chars() {
832        if raw_text_len.saturating_add(ch.len_utf8()) > MAX_SIGNATURE_VALUE_TEXT_LEN {
833            return Err(SignatureVerificationPipelineError::InvalidStructure {
834                reason: "SignatureValue exceeds maximum allowed text length",
835            });
836        }
837        *raw_text_len = raw_text_len.saturating_add(ch.len_utf8());
838        if matches!(ch, ' ' | '\t' | '\r' | '\n') {
839            continue;
840        }
841        if ch.is_ascii_whitespace() {
842            let invalid_byte =
843                u8::try_from(u32::from(ch)).expect("ASCII whitespace always fits into u8");
844            return Err(SignatureVerificationPipelineError::SignatureValueBase64(
845                base64::DecodeError::InvalidByte(normalized.len(), invalid_byte),
846            ));
847        }
848        if normalized.len().saturating_add(ch.len_utf8()) > MAX_SIGNATURE_VALUE_LEN {
849            return Err(SignatureVerificationPipelineError::InvalidStructure {
850                reason: "SignatureValue exceeds maximum allowed length",
851            });
852        }
853        normalized.push(ch);
854    }
855    Ok(())
856}
857
858fn verify_with_algorithm(
859    algorithm: SignatureAlgorithm,
860    public_key_pem: &str,
861    signed_data: &[u8],
862    signature_value: &[u8],
863) -> Result<bool, SignatureVerificationPipelineError> {
864    match algorithm {
865        SignatureAlgorithm::RsaSha1
866        | SignatureAlgorithm::RsaSha256
867        | SignatureAlgorithm::RsaSha384
868        | SignatureAlgorithm::RsaSha512 => Ok(verify_rsa_signature_pem(
869            algorithm,
870            public_key_pem,
871            signed_data,
872            signature_value,
873        )?),
874        SignatureAlgorithm::EcdsaP256Sha256 | SignatureAlgorithm::EcdsaP384Sha384 => {
875            // Malformed ECDSA signature bytes are treated as a verification miss
876            // (Ok(false)) instead of a pipeline error; only key/algorithm and
877            // crypto-operation failures propagate as Err.
878            match verify_ecdsa_signature_pem(
879                algorithm,
880                public_key_pem,
881                signed_data,
882                signature_value,
883            ) {
884                Ok(valid) => Ok(valid),
885                Err(SignatureVerificationError::InvalidSignatureFormat) => Ok(false),
886                Err(error) => Err(error.into()),
887            }
888        }
889    }
890}
891
892#[cfg(test)]
893#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
894mod tests {
895    use super::*;
896    use crate::xmldsig::digest::DigestAlgorithm;
897    use crate::xmldsig::parse::{Reference, parse_signed_info};
898    use crate::xmldsig::transforms::Transform;
899    use crate::xmldsig::uri::UriReferenceResolver;
900    use base64::Engine;
901    use roxmltree::Document;
902
903    // ── Helpers ──────────────────────────────────────────────────────
904
905    /// Build a Reference with given URI, transforms, digest method, and expected digest.
906    fn make_reference(
907        uri: &str,
908        transforms: Vec<Transform>,
909        digest_method: DigestAlgorithm,
910        digest_value: Vec<u8>,
911    ) -> Reference {
912        Reference {
913            uri: Some(uri.to_string()),
914            id: None,
915            ref_type: None,
916            transforms,
917            digest_method,
918            digest_value,
919        }
920    }
921
922    struct RejectingKey;
923
924    impl VerifyingKey for RejectingKey {
925        fn verify(
926            &self,
927            _algorithm: SignatureAlgorithm,
928            _signed_data: &[u8],
929            _signature_value: &[u8],
930        ) -> Result<bool, SignatureVerificationPipelineError> {
931            Ok(false)
932        }
933    }
934
935    struct PanicResolver;
936
937    impl KeyResolver for PanicResolver {
938        fn resolve<'a>(
939            &'a self,
940            _xml: &str,
941        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
942        {
943            panic!("resolver should not be called when references already fail");
944        }
945    }
946
947    struct MissingKeyResolver;
948
949    impl KeyResolver for MissingKeyResolver {
950        fn resolve<'a>(
951            &'a self,
952            _xml: &str,
953        ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>
954        {
955            Ok(None)
956        }
957    }
958
959    fn minimal_signature_xml(reference_uri: &str, transforms_xml: &str) -> String {
960        format!(
961            r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
962  <ds:SignedInfo>
963    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
964    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
965    <ds:Reference URI="{reference_uri}">
966      {transforms_xml}
967      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
968      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
969    </ds:Reference>
970  </ds:SignedInfo>
971  <ds:SignatureValue>AQ==</ds:SignatureValue>
972</ds:Signature>"#
973        )
974    }
975
976    fn signature_with_target_reference(signature_value_b64: &str) -> String {
977        let xml_template = r##"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
978  <target ID="target">payload</target>
979  <ds:Signature>
980    <ds:SignedInfo>
981      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
982      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
983      <ds:Reference URI="#target">
984        <ds:Transforms>
985          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
986        </ds:Transforms>
987        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
988        <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
989      </ds:Reference>
990    </ds:SignedInfo>
991    <ds:SignatureValue>SIGNATURE_VALUE_PLACEHOLDER</ds:SignatureValue>
992  </ds:Signature>
993</root>"##;
994
995        let doc = Document::parse(xml_template).unwrap();
996        let sig_node = doc
997            .descendants()
998            .find(|node| node.is_element() && node.tag_name().name() == "Signature")
999            .unwrap();
1000        let signed_info_node = sig_node
1001            .children()
1002            .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo")
1003            .unwrap();
1004        let signed_info = parse_signed_info(signed_info_node).unwrap();
1005        let reference = &signed_info.references[0];
1006        let resolver = UriReferenceResolver::new(&doc);
1007        let initial_data = resolver
1008            .dereference(reference.uri.as_deref().unwrap())
1009            .unwrap();
1010        let pre_digest =
1011            crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
1012                .unwrap();
1013        let digest = compute_digest(reference.digest_method, &pre_digest);
1014        let digest_b64 = base64::engine::general_purpose::STANDARD.encode(digest);
1015        xml_template
1016            .replace("AAAAAAAAAAAAAAAAAAAAAAAAAAA=", &digest_b64)
1017            .replace("SIGNATURE_VALUE_PLACEHOLDER", signature_value_b64)
1018    }
1019
1020    #[test]
1021    fn verify_context_reports_key_not_found_status_without_key_or_resolver() {
1022        let xml = signature_with_target_reference("AQ==");
1023
1024        let result = VerifyContext::new()
1025            .verify(&xml)
1026            .expect("missing key config must be reported as verification status");
1027        assert!(
1028            matches!(
1029                result.status,
1030                DsigStatus::Invalid(FailureReason::KeyNotFound)
1031            ),
1032            "unexpected status: {:?}",
1033            result.status
1034        );
1035    }
1036
1037    #[test]
1038    fn verify_context_rejects_disallowed_uri() {
1039        let xml = minimal_signature_xml("http://example.com/external", "");
1040        let err = VerifyContext::new()
1041            .key(&RejectingKey)
1042            .verify(&xml)
1043            .expect_err("external URI should be rejected by default policy");
1044        assert!(matches!(
1045            err,
1046            SignatureVerificationPipelineError::DisallowedUri { .. }
1047        ));
1048    }
1049
1050    #[test]
1051    fn verify_context_rejects_empty_uri_when_policy_disallows_empty() {
1052        let xml = minimal_signature_xml("", "");
1053        let err = VerifyContext::new()
1054            .key(&RejectingKey)
1055            .allowed_uri_types(UriTypeSet::new(false, true, false))
1056            .verify(&xml)
1057            .expect_err("empty URI must be rejected when empty references are disabled");
1058        assert!(matches!(
1059            err,
1060            SignatureVerificationPipelineError::DisallowedUri { ref uri } if uri.is_empty()
1061        ));
1062    }
1063
1064    #[test]
1065    fn verify_context_rejects_disallowed_transform() {
1066        let xml = minimal_signature_xml(
1067            "",
1068            r#"<ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></ds:Transforms>"#,
1069        );
1070        let err = VerifyContext::new()
1071            .key(&RejectingKey)
1072            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
1073            .verify(&xml)
1074            .expect_err("enveloped transform should be rejected by allowlist");
1075        assert!(matches!(
1076            err,
1077            SignatureVerificationPipelineError::DisallowedTransform { .. }
1078        ));
1079    }
1080
1081    fn signature_with_manifest_xml() -> String {
1082        r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1083  <ds:SignedInfo>
1084    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1085    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1086    <ds:Reference URI="">
1087      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1088      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
1089    </ds:Reference>
1090  </ds:SignedInfo>
1091  <ds:SignatureValue>AQ==</ds:SignatureValue>
1092  <ds:Object>
1093    <ds:Manifest>
1094      <ds:Reference URI="">
1095        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1096        <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
1097      </ds:Reference>
1098    </ds:Manifest>
1099  </ds:Object>
1100</ds:Signature>"#
1101            .to_owned()
1102    }
1103
1104    fn signature_with_nested_manifest_xml() -> String {
1105        r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1106  <ds:SignedInfo>
1107    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1108    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1109    <ds:Reference URI="">
1110      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1111      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
1112    </ds:Reference>
1113  </ds:SignedInfo>
1114  <ds:SignatureValue>AQ==</ds:SignatureValue>
1115  <ds:Object>
1116    <wrapper>
1117      <ds:Manifest>
1118        <ds:Reference URI="">
1119          <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1120          <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
1121        </ds:Reference>
1122      </ds:Manifest>
1123    </wrapper>
1124  </ds:Object>
1125</ds:Signature>"#
1126            .to_owned()
1127    }
1128
1129    fn signature_with_manifest_type_reference_xml() -> String {
1130        r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1131  <ds:SignedInfo>
1132    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1133    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1134    <ds:Reference URI="" Type="http://www.w3.org/2000/09/xmldsig#Manifest">
1135      <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1136      <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
1137    </ds:Reference>
1138  </ds:SignedInfo>
1139  <ds:SignatureValue>AQ==</ds:SignatureValue>
1140</ds:Signature>"#
1141            .to_owned()
1142    }
1143
1144    #[test]
1145    fn verify_context_manifest_policy_toggle_is_enforced() {
1146        let xml = signature_with_manifest_xml();
1147        let err = VerifyContext::new()
1148            .key(&RejectingKey)
1149            .process_manifests(true)
1150            .verify(&xml)
1151            .expect_err("manifest processing must fail closed while unsupported");
1152        assert!(matches!(
1153            err,
1154            SignatureVerificationPipelineError::ManifestProcessingUnsupported
1155        ));
1156
1157        let result = VerifyContext::new()
1158            .key(&RejectingKey)
1159            .process_manifests(false)
1160            .verify(&xml)
1161            .expect("manifest processing disabled should preserve prior behavior");
1162        assert!(matches!(
1163            result.status,
1164            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1165        ));
1166    }
1167
1168    #[test]
1169    fn verify_context_rejects_nested_manifest_when_processing_enabled() {
1170        let xml = signature_with_nested_manifest_xml();
1171        let err = VerifyContext::new()
1172            .key(&RejectingKey)
1173            .process_manifests(true)
1174            .verify(&xml)
1175            .expect_err("nested manifests under <Object> must also be rejected");
1176        assert!(matches!(
1177            err,
1178            SignatureVerificationPipelineError::ManifestProcessingUnsupported
1179        ));
1180    }
1181
1182    #[test]
1183    fn verify_context_rejects_manifest_type_reference_when_processing_enabled() {
1184        let xml = signature_with_manifest_type_reference_xml();
1185        let err = VerifyContext::new()
1186            .key(&RejectingKey)
1187            .process_manifests(true)
1188            .verify(&xml)
1189            .expect_err("manifest-typed references must fail closed while unsupported");
1190        assert!(matches!(
1191            err,
1192            SignatureVerificationPipelineError::ManifestProcessingUnsupported
1193        ));
1194    }
1195
1196    #[test]
1197    fn verify_context_rejects_implicit_default_c14n_when_not_allowlisted() {
1198        let xml = minimal_signature_xml("", "");
1199        let err = VerifyContext::new()
1200            .key(&RejectingKey)
1201            .allowed_transforms(["http://www.w3.org/2001/10/xml-exc-c14n#"])
1202            .verify(&xml)
1203            .expect_err("implicit default C14N must be checked against allowlist");
1204        assert!(matches!(
1205            err,
1206            SignatureVerificationPipelineError::DisallowedTransform { .. }
1207        ));
1208    }
1209
1210    #[test]
1211    fn verify_context_skips_resolver_when_reference_processing_fails() {
1212        let xml = minimal_signature_xml("", "");
1213        let result = VerifyContext::new()
1214            .key_resolver(&PanicResolver)
1215            .verify(&xml)
1216            .expect("reference digest mismatch should short-circuit before resolver");
1217        assert!(matches!(
1218            result.status,
1219            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1220        ));
1221    }
1222
1223    #[test]
1224    fn verify_context_reports_key_not_found_when_resolver_misses() {
1225        let xml = signature_with_target_reference("AQ==");
1226        let result = VerifyContext::new()
1227            .key_resolver(&MissingKeyResolver)
1228            .verify(&xml)
1229            .expect("resolver miss should report status, not pipeline error");
1230        assert!(matches!(
1231            result.status,
1232            DsigStatus::Invalid(FailureReason::KeyNotFound)
1233        ));
1234        assert_eq!(
1235            result.signed_info_references.len(),
1236            1,
1237            "KeyNotFound path must preserve SignedInfo reference diagnostics",
1238        );
1239        assert!(matches!(
1240            result.signed_info_references[0].status,
1241            DsigStatus::Valid
1242        ));
1243    }
1244
1245    #[test]
1246    fn verify_context_preserves_signaturevalue_decode_errors_when_resolver_misses() {
1247        let xml = signature_with_target_reference("@@@");
1248
1249        let err = VerifyContext::new()
1250            .key_resolver(&MissingKeyResolver)
1251            .verify(&xml)
1252            .expect_err("invalid SignatureValue must remain a decode error on resolver miss");
1253        assert!(matches!(
1254            err,
1255            SignatureVerificationPipelineError::SignatureValueBase64(_)
1256        ));
1257    }
1258
1259    #[test]
1260    fn verify_context_preserves_signaturevalue_decode_errors_without_key() {
1261        let xml = signature_with_target_reference("@@@");
1262
1263        let err = VerifyContext::new()
1264            .verify(&xml)
1265            .expect_err("invalid SignatureValue must remain a decode error");
1266        assert!(matches!(
1267            err,
1268            SignatureVerificationPipelineError::SignatureValueBase64(_)
1269        ));
1270    }
1271
1272    #[test]
1273    fn enforce_reference_policies_rejects_missing_uri_before_uri_type_checks() {
1274        let references = vec![Reference {
1275            uri: None,
1276            id: None,
1277            ref_type: None,
1278            transforms: vec![],
1279            digest_method: DigestAlgorithm::Sha256,
1280            digest_value: vec![0; 32],
1281        }];
1282        let uri_types = UriTypeSet {
1283            allow_empty: false,
1284            allow_same_document: true,
1285            allow_external: false,
1286        };
1287
1288        let err = enforce_reference_policies(&references, uri_types, None)
1289            .expect_err("missing URI must fail before allow_empty policy is evaluated");
1290        assert!(matches!(
1291            err,
1292            SignatureVerificationPipelineError::Reference(ReferenceProcessingError::MissingUri)
1293        ));
1294    }
1295
1296    #[test]
1297    fn push_normalized_signature_text_rejects_form_feed() {
1298        let mut normalized = String::new();
1299        let mut raw_text_len = 0usize;
1300        let err =
1301            push_normalized_signature_text("ab\u{000C}cd", &mut raw_text_len, &mut normalized)
1302                .expect_err("form-feed must not be treated as XML base64 whitespace");
1303        assert!(matches!(
1304            err,
1305            SignatureVerificationPipelineError::SignatureValueBase64(
1306                base64::DecodeError::InvalidByte(_, 0x0C)
1307            )
1308        ));
1309    }
1310
1311    #[test]
1312    fn push_normalized_signature_text_enforces_byte_limit_for_multibyte_chars() {
1313        let mut normalized = "A".repeat(MAX_SIGNATURE_VALUE_LEN - 1);
1314        let mut raw_text_len = normalized.len();
1315        let err = push_normalized_signature_text("é", &mut raw_text_len, &mut normalized)
1316            .expect_err("multibyte characters must not bypass byte-size limit");
1317        assert!(matches!(
1318            err,
1319            SignatureVerificationPipelineError::InvalidStructure {
1320                reason: "SignatureValue exceeds maximum allowed length"
1321            }
1322        ));
1323    }
1324
1325    // ── process_reference: happy path ────────────────────────────────
1326
1327    #[test]
1328    fn reference_with_correct_digest_passes() {
1329        // Create a simple document, compute its canonical form digest,
1330        // then verify that process_reference returns Valid status.
1331        let xml = r##"<root>
1332            <data>hello world</data>
1333            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
1334                <ds:SignedInfo/>
1335            </ds:Signature>
1336        </root>"##;
1337        let doc = Document::parse(xml).unwrap();
1338        let resolver = UriReferenceResolver::new(&doc);
1339        let sig_node = doc
1340            .descendants()
1341            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
1342            .unwrap();
1343
1344        // First, compute the expected digest by running the pipeline
1345        let initial_data = resolver.dereference("").unwrap();
1346        let transforms = vec![
1347            Transform::Enveloped,
1348            Transform::C14n(
1349                crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
1350                    .unwrap(),
1351            ),
1352        ];
1353        let pre_digest_bytes =
1354            crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
1355        let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
1356
1357        // Now build a Reference with the correct digest and verify
1358        let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
1359
1360        let result = process_reference(&reference, &resolver, sig_node, 0, false).unwrap();
1361        assert!(
1362            matches!(result.status, DsigStatus::Valid),
1363            "digest should match"
1364        );
1365        assert!(result.pre_digest_data.is_none());
1366    }
1367
1368    #[test]
1369    fn reference_with_wrong_digest_fails() {
1370        let xml = r##"<root>
1371            <data>hello</data>
1372            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1373                <ds:SignedInfo/>
1374            </ds:Signature>
1375        </root>"##;
1376        let doc = Document::parse(xml).unwrap();
1377        let resolver = UriReferenceResolver::new(&doc);
1378        let sig_node = doc
1379            .descendants()
1380            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
1381            .unwrap();
1382
1383        let transforms = vec![Transform::Enveloped];
1384        // Wrong digest value — all zeros
1385        let wrong_digest = vec![0u8; 32];
1386        let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
1387
1388        let result = process_reference(&reference, &resolver, sig_node, 0, false).unwrap();
1389        assert!(matches!(
1390            result.status,
1391            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1392        ));
1393    }
1394
1395    #[test]
1396    fn reference_with_wrong_digest_preserves_supplied_ref_index() {
1397        let xml = r##"<root>
1398            <data>hello</data>
1399            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1400                <ds:SignedInfo/>
1401            </ds:Signature>
1402        </root>"##;
1403        let doc = Document::parse(xml).unwrap();
1404        let resolver = UriReferenceResolver::new(&doc);
1405        let sig_node = doc
1406            .descendants()
1407            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
1408            .unwrap();
1409
1410        let reference = make_reference(
1411            "",
1412            vec![Transform::Enveloped],
1413            DigestAlgorithm::Sha256,
1414            vec![0u8; 32],
1415        );
1416        let result = process_reference(&reference, &resolver, sig_node, 7, false).unwrap();
1417        assert!(matches!(
1418            result.status,
1419            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 7 })
1420        ));
1421    }
1422
1423    #[test]
1424    fn reference_stores_pre_digest_data() {
1425        let xml = "<root><child>text</child></root>";
1426        let doc = Document::parse(xml).unwrap();
1427        let resolver = UriReferenceResolver::new(&doc);
1428
1429        // No transforms, no enveloped — just canonicalize entire document
1430        let initial_data = resolver.dereference("").unwrap();
1431        let pre_digest =
1432            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
1433        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
1434
1435        let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
1436        let result = process_reference(&reference, &resolver, doc.root_element(), 0, true).unwrap();
1437
1438        assert!(matches!(result.status, DsigStatus::Valid));
1439        assert!(result.pre_digest_data.is_some());
1440        assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
1441    }
1442
1443    // ── process_reference: URI dereference ───────────────────────────
1444
1445    #[test]
1446    fn reference_with_id_uri() {
1447        let xml = r##"<root>
1448            <item ID="target">specific content</item>
1449            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1450                <ds:SignedInfo/>
1451            </ds:Signature>
1452        </root>"##;
1453        let doc = Document::parse(xml).unwrap();
1454        let resolver = UriReferenceResolver::new(&doc);
1455        let sig_node = doc
1456            .descendants()
1457            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
1458            .unwrap();
1459
1460        // Compute expected digest for the #target subtree
1461        let initial_data = resolver.dereference("#target").unwrap();
1462        let transforms = vec![Transform::C14n(
1463            crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
1464                .unwrap(),
1465        )];
1466        let pre_digest =
1467            crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
1468        let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
1469
1470        let reference = make_reference(
1471            "#target",
1472            transforms,
1473            DigestAlgorithm::Sha256,
1474            expected_digest,
1475        );
1476        let result = process_reference(&reference, &resolver, sig_node, 0, false).unwrap();
1477        assert!(matches!(result.status, DsigStatus::Valid));
1478    }
1479
1480    #[test]
1481    fn reference_with_nonexistent_id_fails() {
1482        let xml = "<root><child/></root>";
1483        let doc = Document::parse(xml).unwrap();
1484        let resolver = UriReferenceResolver::new(&doc);
1485
1486        let reference =
1487            make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
1488        let result = process_reference(&reference, &resolver, doc.root_element(), 0, false);
1489        assert!(result.is_err());
1490    }
1491
1492    #[test]
1493    fn reference_with_absent_uri_fails_closed() {
1494        let xml = "<root><child>text</child></root>";
1495        let doc = Document::parse(xml).unwrap();
1496        let resolver = UriReferenceResolver::new(&doc);
1497
1498        let reference = Reference {
1499            uri: None, // absent URI
1500            id: None,
1501            ref_type: None,
1502            transforms: vec![],
1503            digest_method: DigestAlgorithm::Sha256,
1504            digest_value: vec![0; 32],
1505        };
1506
1507        let result = process_reference(&reference, &resolver, doc.root_element(), 0, false);
1508        assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
1509    }
1510
1511    // ── process_all_references: fail-fast ────────────────────────────
1512
1513    #[test]
1514    fn all_references_pass() {
1515        let xml = "<root><child>text</child></root>";
1516        let doc = Document::parse(xml).unwrap();
1517        let resolver = UriReferenceResolver::new(&doc);
1518
1519        // Compute correct digest
1520        let initial_data = resolver.dereference("").unwrap();
1521        let pre_digest =
1522            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
1523        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
1524
1525        let refs = vec![
1526            make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
1527            make_reference("", vec![], DigestAlgorithm::Sha256, digest),
1528        ];
1529
1530        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
1531        assert!(result.all_valid());
1532        assert_eq!(result.results.len(), 2);
1533        assert!(result.first_failure.is_none());
1534    }
1535
1536    #[test]
1537    fn fail_fast_on_first_mismatch() {
1538        let xml = "<root><child>text</child></root>";
1539        let doc = Document::parse(xml).unwrap();
1540        let resolver = UriReferenceResolver::new(&doc);
1541
1542        let wrong_digest = vec![0u8; 32];
1543        let refs = vec![
1544            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
1545            // Second reference should NOT be processed
1546            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
1547        ];
1548
1549        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
1550        assert!(!result.all_valid());
1551        assert_eq!(result.first_failure, Some(0));
1552        // Only first reference should be in results (fail-fast)
1553        assert_eq!(result.results.len(), 1);
1554        assert!(matches!(
1555            result.results[0].status,
1556            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
1557        ));
1558    }
1559
1560    #[test]
1561    fn fail_fast_second_reference() {
1562        let xml = "<root><child>text</child></root>";
1563        let doc = Document::parse(xml).unwrap();
1564        let resolver = UriReferenceResolver::new(&doc);
1565
1566        // Compute correct digest for first ref
1567        let initial_data = resolver.dereference("").unwrap();
1568        let pre_digest =
1569            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
1570        let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
1571        let wrong_digest = vec![0u8; 32];
1572
1573        let refs = vec![
1574            make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
1575            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
1576        ];
1577
1578        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
1579        assert!(!result.all_valid());
1580        assert_eq!(result.first_failure, Some(1));
1581        // Both references should be in results
1582        assert_eq!(result.results.len(), 2);
1583        assert!(matches!(result.results[0].status, DsigStatus::Valid));
1584        assert!(matches!(
1585            result.results[1].status,
1586            DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 1 })
1587        ));
1588    }
1589
1590    #[test]
1591    fn empty_references_list() {
1592        let xml = "<root/>";
1593        let doc = Document::parse(xml).unwrap();
1594        let resolver = UriReferenceResolver::new(&doc);
1595
1596        let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
1597        assert!(result.all_valid());
1598        assert!(result.results.is_empty());
1599    }
1600
1601    // ── Digest algorithms ────────────────────────────────────────────
1602
1603    #[test]
1604    fn reference_sha1_digest() {
1605        let xml = "<root>content</root>";
1606        let doc = Document::parse(xml).unwrap();
1607        let resolver = UriReferenceResolver::new(&doc);
1608
1609        let initial_data = resolver.dereference("").unwrap();
1610        let pre_digest =
1611            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
1612        let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
1613
1614        let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
1615        let result =
1616            process_reference(&reference, &resolver, doc.root_element(), 0, false).unwrap();
1617        assert!(matches!(result.status, DsigStatus::Valid));
1618        assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
1619    }
1620
1621    #[test]
1622    fn reference_sha512_digest() {
1623        let xml = "<root>content</root>";
1624        let doc = Document::parse(xml).unwrap();
1625        let resolver = UriReferenceResolver::new(&doc);
1626
1627        let initial_data = resolver.dereference("").unwrap();
1628        let pre_digest =
1629            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
1630        let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
1631
1632        let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
1633        let result =
1634            process_reference(&reference, &resolver, doc.root_element(), 0, false).unwrap();
1635        assert!(matches!(result.status, DsigStatus::Valid));
1636        assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
1637    }
1638
1639    // ── SAML-like end-to-end ─────────────────────────────────────────
1640
1641    #[test]
1642    fn saml_enveloped_reference_processing() {
1643        // Realistic SAML Response with enveloped signature
1644        let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
1645                                     xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
1646                                     ID="_resp1">
1647            <saml:Assertion ID="_assert1">
1648                <saml:Subject>user@example.com</saml:Subject>
1649            </saml:Assertion>
1650            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1651                <ds:SignedInfo>
1652                    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1653                    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1654                    <ds:Reference URI="">
1655                        <ds:Transforms>
1656                            <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
1657                            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1658                        </ds:Transforms>
1659                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1660                        <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
1661                    </ds:Reference>
1662                </ds:SignedInfo>
1663                <ds:SignatureValue>fakesig==</ds:SignatureValue>
1664            </ds:Signature>
1665        </samlp:Response>"##;
1666        let doc = Document::parse(xml).unwrap();
1667        let resolver = UriReferenceResolver::new(&doc);
1668        let sig_node = doc
1669            .descendants()
1670            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
1671            .unwrap();
1672
1673        // Parse SignedInfo to get the Reference
1674        let signed_info_node = sig_node
1675            .children()
1676            .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
1677            .unwrap();
1678        let signed_info = parse_signed_info(signed_info_node).unwrap();
1679        let reference = &signed_info.references[0];
1680
1681        // Compute the correct digest by running the actual pipeline
1682        let initial_data = resolver.dereference("").unwrap();
1683        let pre_digest =
1684            crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
1685                .unwrap();
1686        let correct_digest = compute_digest(reference.digest_method, &pre_digest);
1687
1688        // Build a reference with the correct digest
1689        let corrected_ref = make_reference(
1690            "",
1691            reference.transforms.clone(),
1692            reference.digest_method,
1693            correct_digest,
1694        );
1695
1696        // Verify: should pass
1697        let result = process_reference(&corrected_ref, &resolver, sig_node, 0, true).unwrap();
1698        assert!(
1699            matches!(result.status, DsigStatus::Valid),
1700            "SAML reference should verify"
1701        );
1702        assert!(result.pre_digest_data.is_some());
1703
1704        // Verify the pre-digest data contains the canonicalized document without Signature
1705        let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
1706        assert!(
1707            pre_digest_str.contains("samlp:Response"),
1708            "pre-digest should contain Response"
1709        );
1710        assert!(
1711            !pre_digest_str.contains("SignatureValue"),
1712            "pre-digest should NOT contain Signature"
1713        );
1714    }
1715
1716    #[test]
1717    fn pipeline_missing_signed_info_returns_missing_element() {
1718        let xml = r#"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"></ds:Signature>"#;
1719
1720        let err = verify_signature_with_pem_key(xml, "dummy-key", false)
1721            .expect_err("missing SignedInfo must fail before crypto stage");
1722        assert!(matches!(
1723            err,
1724            SignatureVerificationPipelineError::MissingElement {
1725                element: "SignedInfo"
1726            }
1727        ));
1728    }
1729
1730    #[test]
1731    fn pipeline_multiple_signature_elements_are_rejected() {
1732        let xml = r#"
1733<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1734  <ds:Signature>
1735    <ds:SignedInfo/>
1736  </ds:Signature>
1737  <ds:Signature/>
1738</root>
1739"#;
1740
1741        let err = verify_signature_with_pem_key(xml, "dummy-key", false)
1742            .expect_err("multiple signatures must fail closed");
1743        assert!(matches!(
1744            err,
1745            SignatureVerificationPipelineError::InvalidStructure {
1746                reason: "Signature must appear exactly once in document",
1747            }
1748        ));
1749    }
1750}