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