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, DsigError>;
45}
46
47pub trait KeyResolver {
52 fn resolve<'a>(&'a self, xml: &str) -> Result<Option<Box<dyn VerifyingKey + 'a>>, DsigError>;
59}
60
61#[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 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 pub const SAME_DOCUMENT: Self = Self {
86 allow_empty: true,
87 allow_same_document: true,
88 allow_external: false,
89 };
90
91 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#[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 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 pub fn key(mut self, key: &'a dyn VerifyingKey) -> Self {
151 self.key = Some(key);
152 self
153 }
154
155 pub fn key_resolver(mut self, resolver: &'a dyn KeyResolver) -> Self {
157 self.key_resolver = Some(resolver);
158 self
159 }
160
161 pub fn process_manifests(mut self, enabled: bool) -> Self {
168 self.process_manifests = enabled;
169 self
170 }
171
172 pub fn allowed_uri_types(mut self, types: UriTypeSet) -> Self {
174 self.allowed_uri_types = types;
175 self
176 }
177
178 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 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 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#[derive(Debug)]
225#[non_exhaustive]
226#[must_use = "inspect status before accepting the reference result"]
227pub struct ReferenceResult {
228 pub uri: String,
230 pub digest_algorithm: DigestAlgorithm,
232 pub status: DsigStatus,
234 pub pre_digest_data: Option<Vec<u8>>,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240#[non_exhaustive]
241pub enum DsigStatus {
242 Valid,
244 Invalid(FailureReason),
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250#[non_exhaustive]
251pub enum FailureReason {
252 ReferenceDigestMismatch {
254 ref_index: usize,
256 },
257 SignatureMismatch,
259 KeyNotFound,
261}
262
263#[derive(Debug)]
265#[non_exhaustive]
266#[must_use = "check first_failure/results before accepting the reference set"]
267pub struct ReferencesResult {
268 pub results: Vec<ReferenceResult>,
271 pub first_failure: Option<usize>,
273}
274
275impl ReferencesResult {
276 #[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
285pub 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 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 let pre_digest_bytes = execute_transforms(signature_node, initial_data, &reference.transforms)
320 .map_err(ReferenceProcessingError::Transform)?;
321
322 let computed_digest = compute_digest(reference.digest_method, &pre_digest_bytes);
324
325 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
344pub 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#[derive(Debug, thiserror::Error)]
386#[non_exhaustive]
387pub enum ReferenceProcessingError {
388 #[error("reference URI is required; omitted URI references are not supported")]
390 MissingUri,
391
392 #[error("URI dereference failed: {0}")]
394 UriDereference(#[source] super::types::TransformError),
395
396 #[error("transform failed: {0}")]
398 Transform(#[source] super::types::TransformError),
399}
400
401#[derive(Debug)]
403#[non_exhaustive]
404#[must_use = "inspect status before accepting the document"]
405pub struct VerifyResult {
406 pub status: DsigStatus,
408 pub signed_info_references: Vec<ReferenceResult>,
412 pub manifest_references: Vec<ReferenceResult>,
414 pub canonicalized_signed_info: Option<Vec<u8>>,
417}
418
419#[derive(Debug, thiserror::Error)]
421#[non_exhaustive]
422pub enum DsigError {
423 #[error("XML parse error: {0}")]
425 XmlParse(#[from] roxmltree::Error),
426
427 #[error("missing required element: <{element}>")]
429 MissingElement {
430 element: &'static str,
432 },
433
434 #[error("invalid Signature structure: {reason}")]
436 InvalidStructure {
437 reason: &'static str,
439 },
440
441 #[error("failed to parse SignedInfo: {0}")]
443 ParseSignedInfo(#[from] super::parse::ParseError),
444
445 #[error("reference processing failed: {0}")]
447 Reference(#[from] ReferenceProcessingError),
448
449 #[error("SignedInfo canonicalization failed: {0}")]
451 Canonicalization(#[from] crate::c14n::C14nError),
452
453 #[error("invalid SignatureValue base64: {0}")]
455 SignatureValueBase64(#[from] base64::DecodeError),
456
457 #[error("signature verification failed: {0}")]
459 Crypto(#[from] SignatureVerificationError),
460
461 #[error("reference URI is not allowed by policy: {uri}")]
463 DisallowedUri {
464 uri: String,
466 },
467
468 #[error("transform is not allowed by policy: {algorithm}")]
470 DisallowedTransform {
471 algorithm: String,
473 },
474
475 #[error("manifest processing is not implemented yet")]
477 ManifestProcessingUnsupported,
478}
479
480type SignatureVerificationPipelineError = DsigError;
481
482pub 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 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 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 #[test]
1327 fn reference_with_correct_digest_passes() {
1328 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 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 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 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 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 #[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 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, 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 #[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 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 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 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 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 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 #[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 #[test]
1641 fn saml_enveloped_reference_processing() {
1642 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 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 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 let corrected_ref = make_reference(
1689 "",
1690 reference.transforms.clone(),
1691 reference.digest_method,
1692 correct_digest,
1693 );
1694
1695 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 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}