1use 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";
33pub trait VerifyingKey {
38 fn verify(
40 &self,
41 algorithm: SignatureAlgorithm,
42 signed_data: &[u8],
43 signature_value: &[u8],
44 ) -> Result<bool, SignatureVerificationPipelineError>;
45}
46
47pub trait KeyResolver {
52 fn resolve<'a>(
59 &'a self,
60 xml: &str,
61 ) -> Result<Option<Box<dyn VerifyingKey + 'a>>, SignatureVerificationPipelineError>;
62}
63
64#[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 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 pub const SAME_DOCUMENT: Self = Self {
89 allow_empty: true,
90 allow_same_document: true,
91 allow_external: false,
92 };
93
94 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#[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 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 pub fn key(mut self, key: &'a dyn VerifyingKey) -> Self {
154 self.key = Some(key);
155 self
156 }
157
158 pub fn key_resolver(mut self, resolver: &'a dyn KeyResolver) -> Self {
160 self.key_resolver = Some(resolver);
161 self
162 }
163
164 pub fn process_manifests(mut self, enabled: bool) -> Self {
171 self.process_manifests = enabled;
172 self
173 }
174
175 pub fn allowed_uri_types(mut self, types: UriTypeSet) -> Self {
177 self.allowed_uri_types = types;
178 self
179 }
180
181 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 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 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#[derive(Debug)]
228#[non_exhaustive]
229#[must_use = "inspect status before accepting the reference result"]
230pub struct ReferenceResult {
231 pub uri: String,
233 pub digest_algorithm: DigestAlgorithm,
235 pub status: DsigStatus,
237 pub pre_digest_data: Option<Vec<u8>>,
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243#[non_exhaustive]
244pub enum DsigStatus {
245 Valid,
247 Invalid(FailureReason),
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253#[non_exhaustive]
254pub enum FailureReason {
255 ReferenceDigestMismatch {
257 ref_index: usize,
259 },
260 SignatureMismatch,
262 KeyNotFound,
264}
265
266#[derive(Debug)]
268#[non_exhaustive]
269#[must_use = "check first_failure/results before accepting the reference set"]
270pub struct ReferencesResult {
271 pub results: Vec<ReferenceResult>,
274 pub first_failure: Option<usize>,
276}
277
278impl ReferencesResult {
279 #[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
288pub 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 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 let pre_digest_bytes = execute_transforms(signature_node, initial_data, &reference.transforms)
323 .map_err(ReferenceProcessingError::Transform)?;
324
325 let computed_digest = compute_digest(reference.digest_method, &pre_digest_bytes);
327
328 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
347pub 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#[derive(Debug, thiserror::Error)]
389#[non_exhaustive]
390pub enum ReferenceProcessingError {
391 #[error("reference URI is required; omitted URI references are not supported")]
393 MissingUri,
394
395 #[error("URI dereference failed: {0}")]
397 UriDereference(super::types::TransformError),
398
399 #[error("transform failed: {0}")]
401 Transform(super::types::TransformError),
402}
403
404#[derive(Debug)]
406#[non_exhaustive]
407#[must_use = "inspect status before accepting the document"]
408pub struct VerifyResult {
409 pub status: DsigStatus,
411 pub signed_info_references: Vec<ReferenceResult>,
415 pub manifest_references: Vec<ReferenceResult>,
417 pub canonicalized_signed_info: Option<Vec<u8>>,
420}
421
422#[derive(Debug, thiserror::Error)]
424#[non_exhaustive]
425pub enum SignatureVerificationPipelineError {
426 #[error("XML parse error: {0}")]
428 XmlParse(#[from] roxmltree::Error),
429
430 #[error("missing required element: <{element}>")]
432 MissingElement {
433 element: &'static str,
435 },
436
437 #[error("invalid Signature structure: {reason}")]
439 InvalidStructure {
440 reason: &'static str,
442 },
443
444 #[error("failed to parse SignedInfo: {0}")]
446 ParseSignedInfo(#[from] super::parse::ParseError),
447
448 #[error("reference processing failed: {0}")]
450 Reference(#[from] ReferenceProcessingError),
451
452 #[error("SignedInfo canonicalization failed: {0}")]
454 Canonicalization(#[from] crate::c14n::C14nError),
455
456 #[error("invalid SignatureValue base64: {0}")]
458 SignatureValueBase64(#[from] base64::DecodeError),
459
460 #[error("signature verification failed: {0}")]
462 Crypto(#[from] SignatureVerificationError),
463
464 #[error("reference URI is not allowed by policy: {uri}")]
466 DisallowedUri {
467 uri: String,
469 },
470
471 #[error("transform is not allowed by policy: {algorithm}")]
473 DisallowedTransform {
474 algorithm: String,
476 },
477
478 #[error("manifest processing is not implemented yet")]
480 ManifestProcessingUnsupported,
481}
482
483pub 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 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 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 #[test]
1328 fn reference_with_correct_digest_passes() {
1329 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 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 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 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 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 #[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 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, 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 #[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 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 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 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 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 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 #[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 #[test]
1642 fn saml_enveloped_reference_processing() {
1643 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 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 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 let corrected_ref = make_reference(
1690 "",
1691 reference.transforms.clone(),
1692 reference.digest_method,
1693 correct_digest,
1694 );
1695
1696 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 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}