1use super::keys::load_certificate;
14use crate::constants::transform_algorithm;
15use crate::error::{ReferenceResolutionReason, SamlError, SignatureVerificationReason};
16use crate::util::normalize_cert_string;
17use crate::xml::dom::{self, Node, XmlLimits};
18use bergshamra::core::ns as bergshamra_ns;
19use bergshamra::dsig::verify::verify_all_document_with_source;
20use bergshamra::xml::{uppsala, Document as BergshamraDocument, NodeId as BergshamraNodeId};
21use bergshamra::{verify, DsigContext, KeysManager, VerifiedReference, VerifyResult};
22use std::collections::HashSet;
23
24fn children_named<'a>(node: &'a Node, name: &str) -> Vec<&'a Node> {
25 node.children
26 .iter()
27 .filter(|c| c.local_name == name)
28 .collect()
29}
30
31fn has_child(node: &Node, name: &str) -> bool {
32 node.children.iter().any(|c| c.local_name == name)
33}
34
35fn saml_signature_candidates(root: &Node) -> Vec<&Node> {
36 let mut signatures = children_named(root, "Signature");
37 for assertion in children_named(root, "Assertion") {
38 signatures.extend(children_named(assertion, "Signature"));
39 }
40 signatures
41}
42
43fn has_descendant(node: &Node, names: &[&str]) -> bool {
44 node.children
45 .iter()
46 .any(|c| names.contains(&c.local_name.as_str()) || has_descendant(c, names))
47}
48
49fn wrapping_detected(root: &Node) -> bool {
51 for assertion in children_named(root, "Assertion") {
52 for subject in children_named(assertion, "Subject") {
53 for sc in children_named(subject, "SubjectConfirmation") {
54 for scd in children_named(sc, "SubjectConfirmationData") {
55 if has_descendant(scd, &["Assertion", "Signature"]) {
56 return true;
57 }
58 }
59 }
60 }
61 }
62 false
63}
64
65fn saml_id_attr(name: &str) -> bool {
66 matches!(name, "ID" | "AssertionID")
67}
68
69fn duplicate_saml_id(node: &Node, seen: &mut HashSet<String>) -> Option<String> {
70 for (name, value) in &node.attrs {
71 if saml_id_attr(name) && !value.is_empty() && !seen.insert(value.clone()) {
72 return Some(value.clone());
73 }
74 }
75 node.children
76 .iter()
77 .find_map(|child| duplicate_saml_id(child, seen))
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81enum VerifiedTarget {
82 WholeDocument,
83 Id(String),
84}
85
86fn reference_resolution(reason: ReferenceResolutionReason) -> SamlError {
87 SamlError::ReferenceResolution { reason }
88}
89
90fn verified_target_from_uri(uri: &str) -> Result<VerifiedTarget, SamlError> {
91 if uri.is_empty() || uri == "#xpointer(/)" {
92 return Ok(VerifiedTarget::WholeDocument);
93 }
94
95 let fragment = uri
96 .strip_prefix('#')
97 .ok_or_else(|| reference_resolution(ReferenceResolutionReason::ExternalReference))?;
98 if fragment.is_empty() {
99 return Err(reference_resolution(
100 ReferenceResolutionReason::UnsupportedReferenceUri,
101 ));
102 }
103 if let Some(id) = fragment
104 .strip_prefix("xpointer(id('")
105 .and_then(|rest| rest.strip_suffix("'))"))
106 {
107 if id.is_empty() {
108 return Err(reference_resolution(
109 ReferenceResolutionReason::UnsupportedReferenceUri,
110 ));
111 }
112 return Ok(VerifiedTarget::Id(id.to_string()));
113 }
114 if fragment.starts_with("xpointer(") {
115 return Err(reference_resolution(
116 ReferenceResolutionReason::UnsupportedReferenceUri,
117 ));
118 }
119 Ok(VerifiedTarget::Id(fragment.to_string()))
120}
121
122fn verified_targets(references: &[VerifiedReference]) -> Result<Vec<VerifiedTarget>, SamlError> {
123 if references.is_empty() {
124 return Err(reference_resolution(
125 ReferenceResolutionReason::MissingSignatureReference,
126 ));
127 }
128
129 let mut targets = Vec::with_capacity(references.len());
130 for reference in references {
131 if is_external_reference(&reference.uri) {
132 return Err(reference_resolution(
133 ReferenceResolutionReason::ExternalReference,
134 ));
135 }
136 if !reference.digest_verified {
137 return Err(SamlError::SignatureVerification {
138 reason: SignatureVerificationReason::ReferenceDigest,
139 });
140 }
141 let target = verified_target_from_uri(&reference.uri)?;
142 if matches!(target, VerifiedTarget::Id(_)) && reference.resolved_node.is_none() {
143 return Err(reference_resolution(
144 ReferenceResolutionReason::UnresolvedReference,
145 ));
146 }
147 targets.push(target);
148 }
149 Ok(targets)
150}
151
152fn node_saml_id(node: &Node) -> Option<&str> {
153 node.attr("ID").or_else(|| node.attr("AssertionID"))
154}
155
156fn target_matches_node(targets: &[VerifiedTarget], node: &Node) -> bool {
157 targets.iter().any(|target| match target {
158 VerifiedTarget::WholeDocument => true,
159 VerifiedTarget::Id(id) => node_saml_id(node).is_some_and(|node_id| node_id == id),
160 })
161}
162
163fn id_target_matches_node(targets: &[VerifiedTarget], node: &Node) -> bool {
164 targets.iter().any(|target| match target {
165 VerifiedTarget::WholeDocument => false,
166 VerifiedTarget::Id(id) => node_saml_id(node).is_some_and(|node_id| node_id == id),
167 })
168}
169
170fn response_is_covered(targets: &[VerifiedTarget], root: &Node) -> bool {
171 target_matches_node(targets, root)
172}
173
174fn verified_content_not_covered() -> SamlError {
175 SamlError::SignedReferenceMismatch
176}
177
178const XML_C14N_10: &str = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
179const XML_C14N_10_WITH_COMMENTS: &str =
180 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments";
181const XML_C14N_11: &str = "http://www.w3.org/2006/12/xml-c14n11";
182const XML_C14N_11_WITH_COMMENTS: &str = "http://www.w3.org/2006/12/xml-c14n11#WithComments";
183
184fn metadata_signature_transform_allowed(algorithm: &str) -> bool {
185 matches!(
186 algorithm,
187 transform_algorithm::ENVELOPED_SIGNATURE
188 | transform_algorithm::EXC_C14N
189 | transform_algorithm::EXC_C14N_WITH_COMMENTS
190 | XML_C14N_10
191 | XML_C14N_10_WITH_COMMENTS
192 | XML_C14N_11
193 | XML_C14N_11_WITH_COMMENTS
194 )
195}
196
197fn ensure_metadata_reference_transforms_preserve_descriptor(
198 reference: &Node,
199) -> Result<(), SamlError> {
200 for transforms in children_named(reference, "Transforms") {
201 for transform in children_named(transforms, "Transform") {
202 if transform
203 .attr("Algorithm")
204 .is_some_and(metadata_signature_transform_allowed)
205 {
206 continue;
207 }
208 return Err(verified_content_not_covered());
209 }
210 }
211 Ok(())
212}
213
214fn ensure_metadata_signature_transforms_preserve_descriptor(root: &Node) -> Result<(), SamlError> {
215 if !matches!(
216 root.local_name.as_str(),
217 "EntityDescriptor" | "EntitiesDescriptor"
218 ) {
219 return Ok(());
220 }
221
222 for signature in children_named(root, "Signature") {
223 for signed_info in children_named(signature, "SignedInfo") {
224 for reference in children_named(signed_info, "Reference") {
225 ensure_metadata_reference_transforms_preserve_descriptor(reference)?;
226 }
227 }
228 }
229 Ok(())
230}
231
232fn verified_root_content(
233 root: &Node,
234 xml: &str,
235 targets: &[VerifiedTarget],
236) -> Result<String, SamlError> {
237 if target_matches_node(targets, root) {
238 return Ok(xml[root.start..root.end].to_string());
239 }
240 Err(verified_content_not_covered())
241}
242
243fn assertion_has_bearer_confirmation(assertion: &Node) -> bool {
246 const BEARER: &str = "urn:oasis:names:tc:SAML:2.0:cm:bearer";
247 assertion
248 .children
249 .iter()
250 .filter(|child| child.local_name == "Subject")
251 .flat_map(|subject| subject.children.iter())
252 .filter(|child| child.local_name == "SubjectConfirmation")
253 .any(|confirmation| confirmation.attr("Method") == Some(BEARER))
254}
255
256fn response_assertions_covered(root: &Node, targets: &[VerifiedTarget]) -> bool {
257 let assertions = children_named(root, "Assertion");
258 !assertions.is_empty()
259 && (response_is_covered(targets, root)
260 || assertions
261 .iter()
262 .all(|assertion| id_target_matches_node(targets, assertion)))
263}
264
265fn verified_content(
269 root: &Node,
270 xml: &str,
271 targets: &[VerifiedTarget],
272) -> Result<Option<String>, SamlError> {
273 if root.local_name == "Assertion" {
274 return verified_root_content(root, xml, targets).map(Some);
275 }
276 if root.local_name.contains("Response") {
277 let assertions = children_named(root, "Assertion");
278 if !assertions.is_empty() {
279 if !response_assertions_covered(root, targets) {
280 if assertions.len() > 1 {
281 return Err(SamlError::PotentialWrappingAttack);
282 }
283 return Err(verified_content_not_covered());
284 }
285 let chosen = assertions
286 .iter()
287 .find(|assertion| assertion_has_bearer_confirmation(assertion))
288 .copied()
289 .unwrap_or(assertions[0]);
290 return Ok(Some(xml[chosen.start..chosen.end].to_string()));
291 }
292 if has_child(root, "EncryptedAssertion") {
293 if response_is_covered(targets, root) {
294 return Ok(Some(xml[root.start..root.end].to_string()));
295 }
296 return Err(verified_content_not_covered());
297 }
298 }
299 if matches!(
300 root.local_name.as_str(),
301 "EntityDescriptor" | "EntitiesDescriptor"
302 ) {
303 if target_matches_node(targets, root) {
304 return Ok(Some(xml[root.start..root.end].to_string()));
305 }
306 return Err(verified_content_not_covered());
307 }
308 if matches!(
309 root.local_name.as_str(),
310 "AuthnRequest" | "LogoutRequest" | "LogoutResponse"
311 ) {
312 return verified_root_content(root, xml, targets).map(Some);
313 }
314 Ok(None)
315}
316
317fn verified_targets_cover_accepted_content(root: &Node, targets: &[VerifiedTarget]) -> bool {
323 if root.local_name == "Assertion" {
324 return target_matches_node(targets, root);
325 }
326 if root.local_name.contains("Response") {
327 let assertions = children_named(root, "Assertion");
328 if !assertions.is_empty() {
329 return response_assertions_covered(root, targets);
330 }
331 if has_child(root, "EncryptedAssertion") {
332 return response_is_covered(targets, root);
333 }
334 }
335 if matches!(
336 root.local_name.as_str(),
337 "EntityDescriptor" | "EntitiesDescriptor"
338 ) {
339 return target_matches_node(targets, root);
340 }
341 if matches!(
342 root.local_name.as_str(),
343 "AuthnRequest" | "LogoutRequest" | "LogoutResponse"
344 ) {
345 return target_matches_node(targets, root);
346 }
347 false
348}
349
350fn assertion_is_directly_covered(root: &Node, targets: &[VerifiedTarget]) -> bool {
351 if root.local_name == "Assertion" {
352 return target_matches_node(targets, root);
353 }
354 if root.local_name.contains("Response") {
355 let assertions = children_named(root, "Assertion");
356 return !assertions.is_empty()
357 && assertions
358 .iter()
359 .all(|assertion| id_target_matches_node(targets, assertion));
360 }
361 false
362}
363
364fn references_cover_direct_assertion(
365 document: &BergshamraDocument<'_>,
366 references: &[VerifiedReference],
367) -> bool {
368 let Some(root) = document.document_element() else {
369 return false;
370 };
371 let Some(root_element) = document.element(root) else {
372 return false;
373 };
374 if root_element.matches_name_ns(crate::constants::namespace::ASSERTION, "Assertion") {
375 return reference_covers_node(references, root);
376 }
377 root_element.matches_name_ns(crate::constants::namespace::PROTOCOL, "Response")
378 && document.children_iter(root).any(|child| {
379 document.element(child).is_some_and(|element| {
380 element.matches_name_ns(crate::constants::namespace::ASSERTION, "Assertion")
381 && reference_covers_node(references, child)
382 })
383 })
384}
385
386fn bergshamra_child_element(
387 document: &BergshamraDocument<'_>,
388 parent: BergshamraNodeId,
389 namespace: &str,
390 local_name: &str,
391) -> Option<BergshamraNodeId> {
392 document.children_iter(parent).find(|child| {
393 document
394 .element(*child)
395 .is_some_and(|element| element.matches_name_ns(namespace, local_name))
396 })
397}
398
399fn verified_signature_algorithm(
400 document: &BergshamraDocument<'_>,
401 signature_node: BergshamraNodeId,
402) -> Result<String, SamlError> {
403 let signed_info = bergshamra_child_element(
404 document,
405 signature_node,
406 bergshamra_ns::DSIG,
407 bergshamra_ns::node::SIGNED_INFO,
408 )
409 .ok_or_else(|| SamlError::Crypto("verified signature is missing SignedInfo".into()))?;
410 let signature_method = bergshamra_child_element(
411 document,
412 signed_info,
413 bergshamra_ns::DSIG,
414 bergshamra_ns::node::SIGNATURE_METHOD,
415 )
416 .ok_or_else(|| SamlError::Crypto("verified signature is missing SignatureMethod".into()))?;
417 document
418 .element(signature_method)
419 .and_then(|element| element.get_attribute(bergshamra_ns::attr::ALGORITHM))
420 .filter(|algorithm| !algorithm.is_empty())
421 .map(str::to_string)
422 .ok_or_else(|| {
423 SamlError::Crypto("verified signature is missing SignatureMethod Algorithm".into())
424 })
425}
426
427fn reference_covers_node(references: &[VerifiedReference], node: BergshamraNodeId) -> bool {
428 references
429 .iter()
430 .any(|reference| reference.resolved_node == Some(node))
431}
432
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub(crate) struct VerifiedEmbeddedSignature {
435 algorithm_uri: String,
436 assertion_directly_covered: bool,
437 response_covered: bool,
438}
439
440impl VerifiedEmbeddedSignature {
441 pub(crate) fn algorithm_uri(&self) -> &str {
442 &self.algorithm_uri
443 }
444
445 pub(crate) fn assertion_directly_covered(&self) -> bool {
446 self.assertion_directly_covered
447 }
448
449 pub(crate) fn response_covered(&self) -> bool {
450 self.response_covered
451 }
452}
453
454fn verified_embedded_signature(
455 document: &BergshamraDocument<'_>,
456 signature_node: BergshamraNodeId,
457 references: &[VerifiedReference],
458 targets: &[VerifiedTarget],
459) -> Result<Option<VerifiedEmbeddedSignature>, SamlError> {
460 let Some(root) = document.document_element() else {
461 return Ok(None);
462 };
463 let root_element = document
464 .element(root)
465 .ok_or_else(|| SamlError::Crypto("verified document root is not an element".into()))?;
466 let whole_document_covered = targets
467 .iter()
468 .any(|target| matches!(target, VerifiedTarget::WholeDocument));
469
470 let response_covered = root_element
471 .matches_name_ns(crate::constants::namespace::PROTOCOL, "Response")
472 && (whole_document_covered || reference_covers_node(references, root));
473 let assertion_node =
474 if root_element.matches_name_ns(crate::constants::namespace::ASSERTION, "Assertion") {
475 Some(root)
476 } else if root_element.matches_name_ns(crate::constants::namespace::PROTOCOL, "Response") {
477 bergshamra_child_element(
478 document,
479 root,
480 crate::constants::namespace::ASSERTION,
481 "Assertion",
482 )
483 } else {
484 None
485 };
486 let assertion_directly_covered = assertion_node.is_some_and(|assertion| {
487 reference_covers_node(references, assertion)
488 || (assertion == root && whole_document_covered)
489 });
490
491 if !response_covered && !assertion_directly_covered {
492 return Ok(None);
493 }
494 Ok(Some(VerifiedEmbeddedSignature {
495 algorithm_uri: verified_signature_algorithm(document, signature_node)?,
496 assertion_directly_covered,
497 response_covered,
498 }))
499}
500
501fn is_external_reference(uri: &str) -> bool {
505 !uri.is_empty() && !uri.starts_with('#')
506}
507
508fn has_saml_xml_signature(root: &Node) -> bool {
509 !saml_signature_candidates(root).is_empty()
510}
511
512fn preflight_saml_reference_uris(signatures: &[&Node]) -> Result<(), SamlError> {
513 for signature in signatures {
514 for signed_info in children_named(signature, "SignedInfo") {
515 for reference in children_named(signed_info, "Reference") {
516 verified_target_from_uri(reference.attr("URI").unwrap_or_default())?;
517 }
518 }
519 }
520 Ok(())
521}
522
523fn dsig_children(
524 document: &BergshamraDocument<'_>,
525 parent: BergshamraNodeId,
526 local_name: &str,
527) -> Vec<BergshamraNodeId> {
528 document
529 .children_iter(parent)
530 .filter(|child| {
531 document
532 .element(*child)
533 .is_some_and(|element| element.matches_name_ns(bergshamra_ns::DSIG, local_name))
534 })
535 .collect()
536}
537
538fn exactly_one_dsig_child(
539 document: &BergshamraDocument<'_>,
540 parent: BergshamraNodeId,
541 local_name: &str,
542) -> Option<BergshamraNodeId> {
543 let mut children = dsig_children(document, parent, local_name).into_iter();
544 let only = children.next()?;
545 children.next().is_none().then_some(only)
546}
547
548fn dsig_algorithm<'a>(
549 document: &'a BergshamraDocument<'_>,
550 node: BergshamraNodeId,
551) -> Option<&'a str> {
552 document
553 .element(node)
554 .and_then(|element| element.get_attribute(bergshamra_ns::attr::ALGORITHM))
555}
556
557fn enforce_strict_profile_on_verified_signature(
563 document: &BergshamraDocument<'_>,
564 signature_node: BergshamraNodeId,
565) -> Result<(), SamlError> {
566 let signed_info =
567 exactly_one_dsig_child(document, signature_node, bergshamra_ns::node::SIGNED_INFO)
568 .ok_or(SamlError::SignedReferenceMismatch)?;
569 let canonicalization = exactly_one_dsig_child(
570 document,
571 signed_info,
572 bergshamra_ns::node::CANONICALIZATION_METHOD,
573 )
574 .ok_or(SamlError::AlgorithmUnsupported)?;
575 if dsig_algorithm(document, canonicalization) != Some(transform_algorithm::EXC_C14N) {
576 return Err(SamlError::AlgorithmUnsupported);
577 }
578 let signature_method =
579 exactly_one_dsig_child(document, signed_info, bergshamra_ns::node::SIGNATURE_METHOD)
580 .ok_or(SamlError::AlgorithmUnsupported)?;
581 if !matches!(
582 dsig_algorithm(document, signature_method),
583 Some(
584 crate::constants::signature_algorithm::RSA_SHA256
585 | crate::constants::signature_algorithm::RSA_SHA384
586 | crate::constants::signature_algorithm::RSA_SHA512
587 )
588 ) {
589 return Err(SamlError::AlgorithmUnsupported);
590 }
591 let reference = exactly_one_dsig_child(document, signed_info, bergshamra_ns::node::REFERENCE)
592 .ok_or(SamlError::SignedReferenceMismatch)?;
593 let Some(uri) = document
594 .element(reference)
595 .and_then(|element| element.get_attribute(bergshamra_ns::attr::URI))
596 else {
597 return Err(reference_resolution(
598 ReferenceResolutionReason::UnsupportedReferenceUri,
599 ));
600 };
601 if !uri.starts_with('#') || uri.len() == 1 || uri.starts_with("#xpointer(") {
602 return Err(reference_resolution(
603 ReferenceResolutionReason::UnsupportedReferenceUri,
604 ));
605 }
606 verified_target_from_uri(uri)?;
607
608 let digest_method =
609 exactly_one_dsig_child(document, reference, bergshamra_ns::node::DIGEST_METHOD)
610 .ok_or(SamlError::AlgorithmUnsupported)?;
611 if !matches!(
612 dsig_algorithm(document, digest_method),
613 Some(
614 crate::constants::digest_algorithm::SHA256
615 | crate::constants::digest_algorithm::SHA384
616 | crate::constants::digest_algorithm::SHA512
617 )
618 ) {
619 return Err(SamlError::AlgorithmUnsupported);
620 }
621 for transforms in dsig_children(document, reference, bergshamra_ns::node::TRANSFORMS) {
622 for child in document.children_iter(transforms) {
627 let Some(element) = document.element(child) else {
628 continue;
629 };
630 if !element.matches_name_ns(bergshamra_ns::DSIG, bergshamra_ns::node::TRANSFORM)
631 || !matches!(
632 element.get_attribute(bergshamra_ns::attr::ALGORITHM),
633 Some(transform_algorithm::ENVELOPED_SIGNATURE | transform_algorithm::EXC_C14N)
634 )
635 {
636 return Err(SamlError::AlgorithmUnsupported);
637 }
638 }
639 }
640 Ok(())
641}
642
643pub(crate) fn has_xml_signature_with_limits(
644 xml: &str,
645 limits: XmlLimits,
646) -> Result<bool, SamlError> {
647 let doc = dom::parse_with_limits(xml, limits)?;
648 Ok(has_saml_xml_signature(&doc.root))
649}
650
651fn inline_signature_cert(signatures: &[&Node]) -> Option<String> {
654 fn descendant_cert(node: &Node) -> Option<String> {
655 if node.local_name == "X509Certificate" && !node.text.is_empty() {
656 return Some(node.text.clone());
657 }
658 node.children.iter().find_map(descendant_cert)
659 }
660
661 signatures
662 .iter()
663 .find_map(|signature| descendant_cert(signature))
664}
665
666pub fn verify_signature(
678 xml: &str,
679 metadata_certs: &[String],
680) -> Result<(bool, Option<String>), SamlError> {
681 verify_signature_with_limits(xml, metadata_certs, XmlLimits::default())
682}
683
684pub fn verify_signature_with_limits(
691 xml: &str,
692 metadata_certs: &[String],
693 limits: XmlLimits,
694) -> Result<(bool, Option<String>), SamlError> {
695 let doc = dom::parse_with_limits(xml, limits)?;
696 let root = &doc.root;
697
698 if root.local_name.contains("Response") && wrapping_detected(root) {
699 return Err(SamlError::PotentialWrappingAttack);
700 }
701
702 let mut seen_ids = HashSet::new();
703 if duplicate_saml_id(root, &mut seen_ids).is_some() {
704 return Err(SamlError::PotentialWrappingAttack);
705 }
706
707 let signature_candidates = saml_signature_candidates(root);
709 if signature_candidates.is_empty() {
710 return Ok((false, None));
711 }
712 preflight_saml_reference_uris(&signature_candidates)?;
713
714 if let Some(inline) = inline_signature_cert(&signature_candidates) {
718 let inline = normalize_cert_string(&inline);
719 if !metadata_certs.is_empty()
720 && !metadata_certs
721 .iter()
722 .any(|c| normalize_cert_string(c) == inline)
723 {
724 return Err(SamlError::CertificateMismatch);
725 }
726 }
727
728 super::provider::ensure_crypto_provider_initialized()?;
729
730 let mut have_key = false;
733 let mut key_load_error = None;
734 let mut tried_invalid = false;
735 let mut last_err: Option<SamlError> = None;
736 for cert in metadata_certs {
737 let key = match load_certificate(cert) {
738 Ok(key) => key,
739 Err(error) => {
740 key_load_error.get_or_insert(error);
741 continue;
742 }
743 };
744 have_key = true;
745 let mut manager = KeysManager::new();
746 manager.add_key(key);
747 let ctx = DsigContext::new(manager)
752 .with_trusted_keys_only(true)
753 .with_strict_verification(true)
754 .with_require_reference_digests(true)
755 .with_hmac_min_out_len(160)
756 .with_insecure(true);
757 match verify(&ctx, xml) {
758 Ok(VerifyResult::Valid {
759 signature_node: _,
760 references,
761 ..
762 }) => {
763 let targets = verified_targets(&references)?;
764 return Ok((true, verified_content(root, xml, &targets)?));
765 }
766 Ok(VerifyResult::Invalid { .. }) => tried_invalid = true,
767 Err(e) => last_err = Some(SamlError::Crypto(e.to_string())),
768 }
769 }
770 if !have_key {
771 return Err(key_load_error.unwrap_or(SamlError::NoTrustedCertificate));
772 }
773 match last_err {
777 Some(err) if !tried_invalid => Err(err),
778 _ => Ok((false, None)),
779 }
780}
781
782#[derive(Debug, Clone, PartialEq, Eq)]
783pub(crate) struct SignatureVerification {
784 verified: bool,
785 signed_content: Option<String>,
786 assertion_directly_covered: bool,
787 response_covered: bool,
788 verified_embedded_signatures: Vec<VerifiedEmbeddedSignature>,
789}
790
791impl SignatureVerification {
792 pub(crate) fn verified(&self) -> bool {
793 self.verified
794 }
795
796 pub(crate) fn assertion_directly_covered(&self) -> bool {
797 self.assertion_directly_covered
798 }
799
800 pub(crate) fn response_covered(&self) -> bool {
801 self.response_covered
802 }
803
804 pub(crate) fn verified_embedded_signatures(&self) -> &[VerifiedEmbeddedSignature] {
805 &self.verified_embedded_signatures
806 }
807
808 pub(crate) fn into_signed_content(self) -> Option<String> {
809 self.signed_content
810 }
811}
812
813pub(crate) fn verify_signatures_detailed_with_profile(
814 xml: &str,
815 metadata_certs: &[String],
816 limits: XmlLimits,
817 strict_xml_signature_profile: bool,
818) -> Result<SignatureVerification, SamlError> {
819 let doc = dom::parse_with_limits(xml, limits)?;
820 let root = &doc.root;
821
822 if root.local_name.contains("Response") && wrapping_detected(root) {
823 return Err(SamlError::PotentialWrappingAttack);
824 }
825
826 let mut seen_ids = HashSet::new();
827 if duplicate_saml_id(root, &mut seen_ids).is_some() {
828 return Err(SamlError::PotentialWrappingAttack);
829 }
830
831 let signature_candidates = saml_signature_candidates(root);
832 if signature_candidates.is_empty() {
833 return Ok(SignatureVerification {
834 verified: false,
835 signed_content: None,
836 assertion_directly_covered: false,
837 response_covered: false,
838 verified_embedded_signatures: Vec::new(),
839 });
840 }
841 preflight_saml_reference_uris(&signature_candidates)?;
842
843 if let Some(inline) = inline_signature_cert(&signature_candidates) {
844 let inline = normalize_cert_string(&inline);
845 if !metadata_certs.is_empty()
846 && !metadata_certs
847 .iter()
848 .any(|c| normalize_cert_string(c) == inline)
849 {
850 return Err(SamlError::CertificateMismatch);
851 }
852 }
853
854 super::provider::ensure_crypto_provider_initialized()?;
855
856 let mut have_key = false;
857 let mut key_load_error = None;
858 let mut tried_invalid = false;
859 let mut last_err: Option<SamlError> = None;
860 let mut first_signature_verified = false;
861 let mut targets = Vec::new();
862 let document = uppsala::parse(xml).map_err(|error| SamlError::Crypto(error.to_string()))?;
865 let mut verified_signature_nodes = HashSet::new();
866 let mut verified_embedded_signatures = Vec::new();
867 for cert in metadata_certs {
868 let key = match load_certificate(cert) {
869 Ok(key) => key,
870 Err(error) => {
871 key_load_error.get_or_insert(error);
872 continue;
873 }
874 };
875 have_key = true;
876 let mut manager = KeysManager::new();
877 manager.add_key(key);
878 let ctx = DsigContext::new(manager)
879 .with_trusted_keys_only(true)
880 .with_strict_verification(true)
881 .with_require_reference_digests(true)
882 .with_hmac_min_out_len(160)
883 .with_insecure(true);
884 match verify_all_document_with_source(&ctx, &document, Some(xml)) {
885 Ok(results) => {
886 first_signature_verified |=
887 matches!(results.first(), Some(VerifyResult::Valid { .. }));
888 for result in results {
889 match result {
890 VerifyResult::Valid {
891 signature_node,
892 references,
893 ..
894 } => {
895 let signature_targets = verified_targets(&references)?;
896 if verified_signature_nodes.insert(signature_node) {
897 let signature = verified_embedded_signature(
898 &document,
899 signature_node,
900 &references,
901 &signature_targets,
902 )?;
903 if strict_xml_signature_profile
908 && (signature.is_some()
909 || references_cover_direct_assertion(
910 &document,
911 &references,
912 )
913 || verified_targets_cover_accepted_content(
914 root,
915 &signature_targets,
916 ))
917 {
918 enforce_strict_profile_on_verified_signature(
919 &document,
920 signature_node,
921 )?;
922 }
923 if let Some(signature) = signature {
924 verified_embedded_signatures
925 .push((signature_node.index(), signature));
926 }
927 }
928 targets.extend(signature_targets);
929 }
930 VerifyResult::Invalid { .. } => tried_invalid = true,
931 }
932 }
933 }
934 Err(error) => last_err = Some(SamlError::Crypto(error.to_string())),
935 }
936 }
937 if !have_key {
938 return Err(key_load_error.unwrap_or(SamlError::NoTrustedCertificate));
939 }
940 if first_signature_verified && !targets.is_empty() {
941 verified_embedded_signatures.sort_by_key(|(index, _)| *index);
942 let assertion_directly_covered = assertion_is_directly_covered(root, &targets);
943 let response_covered =
944 root.local_name.contains("Response") && response_is_covered(&targets, root);
945 return Ok(SignatureVerification {
946 verified: true,
947 signed_content: verified_content(root, xml, &targets)?,
948 assertion_directly_covered,
949 response_covered,
950 verified_embedded_signatures: verified_embedded_signatures
951 .into_iter()
952 .map(|(_, signature)| signature)
953 .collect(),
954 });
955 }
956 match last_err {
957 Some(error) if !tried_invalid => Err(error),
958 _ => Ok(SignatureVerification {
959 verified: false,
960 signed_content: None,
961 assertion_directly_covered: false,
962 response_covered: false,
963 verified_embedded_signatures: Vec::new(),
964 }),
965 }
966}
967
968#[derive(Debug, Clone, PartialEq, Eq)]
970pub struct MetadataSignatureVerification {
971 verified: bool,
972 signed_entity_descriptor_xml: Option<String>,
973}
974
975impl MetadataSignatureVerification {
976 pub(crate) fn from_signed_descriptor(signed_entity_descriptor_xml: String) -> Self {
977 Self {
978 verified: true,
979 signed_entity_descriptor_xml: Some(signed_entity_descriptor_xml),
980 }
981 }
982
983 pub(crate) fn unverified() -> Self {
984 Self {
985 verified: false,
986 signed_entity_descriptor_xml: None,
987 }
988 }
989
990 pub fn verified(&self) -> bool {
992 self.verified
993 }
994
995 pub fn signed_entity_descriptor_xml(&self) -> Option<&str> {
997 self.signed_entity_descriptor_xml.as_deref()
998 }
999
1000 pub(crate) fn into_signed_entity_descriptor_xml(self) -> Option<String> {
1001 self.signed_entity_descriptor_xml
1002 }
1003}
1004
1005pub fn verify_metadata_signature(
1014 xml: &str,
1015 trusted_certificates: &[String],
1016) -> Result<bool, SamlError> {
1017 verify_metadata_signature_with_limits(xml, trusted_certificates, XmlLimits::default())
1018}
1019
1020pub fn verify_metadata_signature_with_limits(
1027 xml: &str,
1028 trusted_certificates: &[String],
1029 limits: XmlLimits,
1030) -> Result<bool, SamlError> {
1031 Ok(
1032 verify_metadata_signature_detailed_with_limits(xml, trusted_certificates, limits)?
1033 .verified(),
1034 )
1035}
1036
1037pub fn verify_metadata_signature_detailed(
1046 xml: &str,
1047 trusted_certificates: &[String],
1048) -> Result<MetadataSignatureVerification, SamlError> {
1049 verify_metadata_signature_detailed_with_limits(xml, trusted_certificates, XmlLimits::default())
1050}
1051
1052pub fn verify_metadata_signature_detailed_with_limits(
1060 xml: &str,
1061 trusted_certificates: &[String],
1062 limits: XmlLimits,
1063) -> Result<MetadataSignatureVerification, SamlError> {
1064 let doc = dom::parse_with_limits(xml, limits)?;
1065 ensure_metadata_signature_transforms_preserve_descriptor(&doc.root)?;
1066 verify_metadata_signature_coverage_with_limits(xml, trusted_certificates, limits)
1067}
1068
1069pub(crate) fn verify_metadata_signature_coverage_with_limits(
1078 xml: &str,
1079 trusted_certificates: &[String],
1080 limits: XmlLimits,
1081) -> Result<MetadataSignatureVerification, SamlError> {
1082 let (verified, signed_entity_descriptor_xml) =
1083 verify_signature_with_limits(xml, trusted_certificates, limits)?;
1084 if !verified {
1085 return Ok(MetadataSignatureVerification::unverified());
1086 }
1087 signed_entity_descriptor_xml
1088 .map(MetadataSignatureVerification::from_signed_descriptor)
1089 .ok_or_else(verified_content_not_covered)
1090}
1091
1092#[cfg(test)]
1093mod tests {
1094 use super::*;
1095 use crate::constants::signature_algorithm::{RSA_SHA256, RSA_SHA384, RSA_SHA512};
1096 use crate::constants::{digest_for_signature, namespace, transform_algorithm};
1097 use crate::crypto::construct_saml_signature;
1098 use crate::crypto::keys::load_private_key;
1099 use crate::util::normalize_cert_string;
1100 use crate::xml::{extract, ExtractorField};
1101 use bergshamra::sign;
1102
1103 #[test]
1104 fn external_reference_detection() {
1105 assert!(!is_external_reference("")); assert!(!is_external_reference("#_assertion123")); assert!(is_external_reference("https://evil.example.com/x"));
1108 assert!(is_external_reference("/etc/passwd"));
1109 assert!(is_external_reference("file:///etc/passwd"));
1110 assert!(is_external_reference("cid:attachment"));
1111 }
1112
1113 #[test]
1114 fn metadata_signature_transform_allowlist_preserves_canonicalization_interoperability() {
1115 const XPATH_TRANSFORM: &str = "http://www.w3.org/TR/1999/REC-xpath-19991116";
1116 const XSLT_TRANSFORM: &str = "http://www.w3.org/TR/1999/REC-xslt-19991116";
1117 const UNKNOWN_TRANSFORM: &str = "urn:example:unknown-transform";
1118
1119 for algorithm in [
1120 transform_algorithm::ENVELOPED_SIGNATURE,
1121 transform_algorithm::EXC_C14N,
1122 transform_algorithm::EXC_C14N_WITH_COMMENTS,
1123 XML_C14N_10,
1124 XML_C14N_10_WITH_COMMENTS,
1125 XML_C14N_11,
1126 XML_C14N_11_WITH_COMMENTS,
1127 ] {
1128 assert!(
1129 metadata_signature_transform_allowed(algorithm),
1130 "{algorithm}"
1131 );
1132 }
1133
1134 for algorithm in [XPATH_TRANSFORM, XSLT_TRANSFORM, UNKNOWN_TRANSFORM] {
1135 assert!(
1136 !metadata_signature_transform_allowed(algorithm),
1137 "{algorithm}"
1138 );
1139 }
1140 }
1141
1142 #[test]
1143 fn same_document_reference_target_parsing() -> Result<(), Box<dyn std::error::Error>> {
1144 assert_eq!(verified_target_from_uri("")?, VerifiedTarget::WholeDocument);
1145 assert_eq!(
1146 verified_target_from_uri("#_assertion123")?,
1147 VerifiedTarget::Id("_assertion123".to_string())
1148 );
1149 assert_eq!(
1150 verified_target_from_uri("#xpointer(/)")?,
1151 VerifiedTarget::WholeDocument
1152 );
1153 assert_eq!(
1154 verified_target_from_uri("#xpointer(id('_assertion123'))")?,
1155 VerifiedTarget::Id("_assertion123".to_string())
1156 );
1157 Ok(())
1158 }
1159
1160 #[test]
1161 fn unsupported_reference_target_parsing_fails() {
1162 assert!(matches!(
1163 verified_target_from_uri("#"),
1164 Err(SamlError::ReferenceResolution {
1165 reason: ReferenceResolutionReason::UnsupportedReferenceUri
1166 })
1167 ));
1168 assert!(matches!(
1169 verified_target_from_uri("#xpointer(//saml:Assertion)"),
1170 Err(SamlError::ReferenceResolution {
1171 reason: ReferenceResolutionReason::UnsupportedReferenceUri
1172 })
1173 ));
1174 }
1175
1176 fn preflight_signature(
1177 canonicalization: &str,
1178 signature: &str,
1179 digest: &str,
1180 transform: &str,
1181 references: usize,
1182 uri: &str,
1183 ) -> Result<(), SamlError> {
1184 let reference = format!(
1185 "<ds:Reference URI=\"{uri}\"><ds:Transforms><ds:Transform Algorithm=\"{enveloped}\"/><ds:Transform Algorithm=\"{transform}\"/></ds:Transforms><ds:DigestMethod Algorithm=\"{digest}\"/><ds:DigestValue>AAAA</ds:DigestValue></ds:Reference>",
1186 enveloped = transform_algorithm::ENVELOPED_SIGNATURE,
1187 );
1188 let xml = format!(
1189 "<samlp:Response xmlns:samlp=\"{protocol}\" xmlns:ds=\"{dsig}\" ID=\"_response\"><ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm=\"{canonicalization}\"/><ds:SignatureMethod Algorithm=\"{signature}\"/>{references}</ds:SignedInfo><ds:SignatureValue>AAAA</ds:SignatureValue></ds:Signature></samlp:Response>",
1190 protocol = namespace::PROTOCOL,
1191 dsig = namespace::DSIG,
1192 references = reference.repeat(references),
1193 );
1194 let document =
1195 uppsala::parse(&xml).map_err(|error| SamlError::Crypto(error.to_string()))?;
1196 let root = document.document_element().ok_or_else(|| {
1197 SamlError::Crypto("profile fixture is missing a document element".into())
1198 })?;
1199 let signature = bergshamra_child_element(
1200 &document,
1201 root,
1202 bergshamra_ns::DSIG,
1203 bergshamra_ns::node::SIGNATURE,
1204 )
1205 .ok_or(SamlError::SignedReferenceMismatch)?;
1206 enforce_strict_profile_on_verified_signature(&document, signature)
1207 }
1208
1209 #[test]
1210 fn strict_saml_signature_preflight_accepts_only_the_initial_profile() {
1211 for (signature, digest) in [
1212 (RSA_SHA256, crate::constants::digest_algorithm::SHA256),
1213 (RSA_SHA384, crate::constants::digest_algorithm::SHA384),
1214 (RSA_SHA512, crate::constants::digest_algorithm::SHA512),
1215 ] {
1216 assert!(preflight_signature(
1217 transform_algorithm::EXC_C14N,
1218 signature,
1219 digest,
1220 transform_algorithm::EXC_C14N,
1221 1,
1222 "#_response",
1223 )
1224 .is_ok());
1225 }
1226 }
1227
1228 #[test]
1229 fn strict_saml_signature_preflight_rejects_ambiguous_references_and_algorithms() {
1230 let accepted = (
1231 transform_algorithm::EXC_C14N,
1232 RSA_SHA256,
1233 crate::constants::digest_algorithm::SHA256,
1234 transform_algorithm::EXC_C14N,
1235 );
1236 for (references, uri) in [(0, "#_response"), (2, "#_response"), (1, "")] {
1237 assert!(preflight_signature(
1238 accepted.0, accepted.1, accepted.2, accepted.3, references, uri,
1239 )
1240 .is_err());
1241 }
1242 for rejected in [
1243 preflight_signature(
1244 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
1245 accepted.1,
1246 accepted.2,
1247 accepted.3,
1248 1,
1249 "#_response",
1250 ),
1251 preflight_signature(
1252 accepted.0,
1253 "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
1254 accepted.2,
1255 accepted.3,
1256 1,
1257 "#_response",
1258 ),
1259 preflight_signature(
1260 accepted.0,
1261 accepted.1,
1262 "http://www.w3.org/2000/09/xmldsig#sha1",
1263 accepted.3,
1264 1,
1265 "#_response",
1266 ),
1267 preflight_signature(
1268 accepted.0,
1269 accepted.1,
1270 accepted.2,
1271 "http://www.w3.org/TR/1999/REC-xslt-19991116",
1272 1,
1273 "#_response",
1274 ),
1275 ] {
1276 assert!(matches!(rejected, Err(SamlError::AlgorithmUnsupported)));
1277 }
1278 }
1279
1280 #[test]
1281 fn advice_assertion_is_not_a_wrapping_shape() -> Result<(), Box<dyn std::error::Error>> {
1282 let document = dom::parse(&format!(
1283 "<samlp:Response xmlns:samlp=\"{protocol}\" xmlns:saml=\"{assertion}\"><saml:Assertion ID=\"_outer\"><saml:Advice><saml:Assertion ID=\"_advice\"/></saml:Advice></saml:Assertion></samlp:Response>",
1284 protocol = namespace::PROTOCOL,
1285 assertion = namespace::ASSERTION,
1286 ))?;
1287 assert!(!wrapping_detected(&document.root));
1288 Ok(())
1289 }
1290
1291 #[test]
1292 fn subject_confirmation_data_assertion_is_a_wrapping_shape(
1293 ) -> Result<(), Box<dyn std::error::Error>> {
1294 let document = dom::parse(&format!(
1295 "<samlp:Response xmlns:samlp=\"{protocol}\" xmlns:saml=\"{assertion}\"><saml:Assertion ID=\"_outer\"><saml:Subject><saml:SubjectConfirmation><saml:SubjectConfirmationData><saml:Assertion ID=\"_nested\"/></saml:SubjectConfirmationData></saml:SubjectConfirmation></saml:Subject></saml:Assertion></samlp:Response>",
1296 protocol = namespace::PROTOCOL,
1297 assertion = namespace::ASSERTION,
1298 ))?;
1299 assert!(wrapping_detected(&document.root));
1300 Ok(())
1301 }
1302
1303 #[test]
1304 fn duplicate_saml_id_allows_unique_ids() -> Result<(), Box<dyn std::error::Error>> {
1305 let doc = dom::parse(
1306 r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_response"><saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_assertion"/></samlp:Response>"#,
1307 )?;
1308 let mut seen = HashSet::new();
1309 assert_eq!(duplicate_saml_id(&doc.root, &mut seen), None);
1310 Ok(())
1311 }
1312
1313 #[test]
1314 fn duplicate_saml_id_returns_repeated_value() -> Result<(), Box<dyn std::error::Error>> {
1315 let doc = dom::parse(
1316 r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_same"/><saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_same"/></samlp:Response>"#,
1317 )?;
1318 let mut seen = HashSet::new();
1319 assert_eq!(
1320 duplicate_saml_id(&doc.root, &mut seen),
1321 Some("_same".to_string())
1322 );
1323 Ok(())
1324 }
1325
1326 #[test]
1327 fn duplicate_saml_id_ignores_empty_values() -> Result<(), Box<dyn std::error::Error>> {
1328 let doc = dom::parse(
1329 r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID=""><saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID=""/></samlp:Response>"#,
1330 )?;
1331 let mut seen = HashSet::new();
1332 assert_eq!(duplicate_saml_id(&doc.root, &mut seen), None);
1333 Ok(())
1334 }
1335
1336 const RESPONSE_SIGNED: &str = include_str!("../../tests/fixtures/response_signed.xml");
1337 const SIGNED_REQUEST: &str = include_str!("../../tests/fixtures/signed_request_sha256.xml");
1338 const ATTACK: &str = include_str!("../../tests/fixtures/attack_response_signed.xml");
1339 const FALSE_SIGNED: &str = include_str!("../../tests/fixtures/false_signed_request_sha256.xml");
1340 const RESPONSE: &str = include_str!("../../tests/fixtures/response.xml");
1341 const SP_PRIVKEY: &str = include_str!("../../tests/fixtures/key/sp_privkey.pem");
1342 const IDP_CERT: &str = include_str!("../../tests/fixtures/key/idp_cert.cer");
1344 const SP_CERT: &str = include_str!("../../tests/fixtures/key/sp_cert.cer");
1346 const SP_SIGNING_CERT: &str = include_str!("../../tests/fixtures/key/sp_signing_cert.cer");
1347 const UNTRUSTED_PRIVKEY: &str =
1348 include_str!("../../tests/fixtures/key/idp/provider_matrix_privkey.pkcs8.pem");
1349 const UNTRUSTED_CERT: &str = include_str!("../../tests/fixtures/key/idp/cert.cer");
1350
1351 fn signed_response_with_foreign_extension_certificate(
1352 ) -> Result<String, Box<dyn std::error::Error>> {
1353 let response = RESPONSE.replacen(
1354 "<samlp:Status>",
1355 r#"<samlp:Extensions><x:Signature xmlns:x="urn:example:extension"><x:X509Certificate>attacker</x:X509Certificate></x:Signature></samlp:Extensions><samlp:Status>"#,
1356 1,
1357 );
1358 let key = load_private_key(SP_PRIVKEY, None)?;
1359 Ok(construct_saml_signature(
1360 &response,
1361 false,
1362 &key,
1363 SP_SIGNING_CERT,
1364 RSA_SHA256,
1365 &[],
1366 None,
1367 )?)
1368 }
1369
1370 fn response_with_first_invalid_signature() -> Result<String, Box<dyn std::error::Error>> {
1371 let cert = normalize_cert_string(IDP_CERT);
1372 let digest = digest_for_signature(RSA_SHA256).ok_or("unknown digest")?;
1373 let invalid_signature = format!(
1374 "<ds:Signature xmlns:ds=\"{dsig}\"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm=\"{exc_c14n}\"/><ds:SignatureMethod Algorithm=\"{sig_alg}\"/><ds:Reference URI=\"#_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75\"><ds:Transforms><ds:Transform Algorithm=\"{exc_c14n}\"/></ds:Transforms><ds:DigestMethod Algorithm=\"{digest}\"/><ds:DigestValue>AAAA</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>invalid</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>{cert}</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>",
1375 dsig = namespace::DSIG,
1376 exc_c14n = transform_algorithm::EXC_C14N,
1377 sig_alg = RSA_SHA256,
1378 );
1379 Ok(RESPONSE_SIGNED.replacen(
1380 "<samlp:Status>",
1381 &format!("{invalid_signature}<samlp:Status>"),
1382 1,
1383 ))
1384 }
1385
1386 fn signature_over_top_level_issuer(
1387 key_pem: &str,
1388 certificate: &str,
1389 algorithm: &str,
1390 ) -> Result<String, Box<dyn std::error::Error>> {
1391 let issuer_id = "_signed_response_issuer";
1392 let response = RESPONSE.replacen(
1393 "<saml:Issuer>",
1394 &format!("<saml:Issuer ID=\"{issuer_id}\">"),
1395 1,
1396 );
1397 let digest = digest_for_signature(algorithm).ok_or("unknown digest")?;
1398 let signature = format!(
1399 "<ds:Signature xmlns:ds=\"{dsig}\"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm=\"{exc_c14n}\"/><ds:SignatureMethod Algorithm=\"{algorithm}\"/><ds:Reference URI=\"#{issuer_id}\"><ds:Transforms><ds:Transform Algorithm=\"{enveloped}\"/><ds:Transform Algorithm=\"{exc_c14n}\"/></ds:Transforms><ds:DigestMethod Algorithm=\"{digest}\"/><ds:DigestValue></ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue></ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>{cert}</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>",
1400 dsig = namespace::DSIG,
1401 exc_c14n = transform_algorithm::EXC_C14N,
1402 enveloped = transform_algorithm::ENVELOPED_SIGNATURE,
1403 cert = normalize_cert_string(certificate),
1404 );
1405 let template = response.replacen(
1406 "</saml:Issuer><samlp:Status>",
1407 &format!("</saml:Issuer>{signature}<samlp:Status>"),
1408 1,
1409 );
1410 let key = load_private_key(key_pem, None)?;
1411 let mut manager = KeysManager::new();
1412 manager.add_key(key);
1413 let context = DsigContext::new(manager).with_insecure(true);
1414 Ok(sign(&context, &template)?)
1415 }
1416
1417 fn signature_over_response_and_assertion() -> Result<String, Box<dyn std::error::Error>> {
1418 let document = dom::parse(RESPONSE)?;
1419 let response_id = node_saml_id(&document.root).ok_or("missing Response ID")?;
1420 let assertion = children_named(&document.root, "Assertion")
1421 .into_iter()
1422 .next()
1423 .ok_or("missing Assertion")?;
1424 let assertion_id = node_saml_id(assertion).ok_or("missing Assertion ID")?;
1425 let digest = digest_for_signature(RSA_SHA512).ok_or("unknown digest")?;
1426 let reference = |id: &str| {
1427 format!(
1428 "<ds:Reference URI=\"#{id}\"><ds:Transforms><ds:Transform Algorithm=\"{enveloped}\"/><ds:Transform Algorithm=\"{exc_c14n}\"/></ds:Transforms><ds:DigestMethod Algorithm=\"{digest}\"/><ds:DigestValue></ds:DigestValue></ds:Reference>",
1429 enveloped = transform_algorithm::ENVELOPED_SIGNATURE,
1430 exc_c14n = transform_algorithm::EXC_C14N,
1431 )
1432 };
1433 let signature = format!(
1434 "<ds:Signature xmlns:ds=\"{dsig}\"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm=\"{exc_c14n}\"/><ds:SignatureMethod Algorithm=\"{algorithm}\"/>{response_reference}{assertion_reference}</ds:SignedInfo><ds:SignatureValue></ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>{cert}</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>",
1435 dsig = namespace::DSIG,
1436 exc_c14n = transform_algorithm::EXC_C14N,
1437 algorithm = RSA_SHA512,
1438 response_reference = reference(response_id),
1439 assertion_reference = reference(assertion_id),
1440 cert = normalize_cert_string(SP_SIGNING_CERT),
1441 );
1442 let template = RESPONSE.replacen(
1443 "</saml:Issuer><samlp:Status>",
1444 &format!("</saml:Issuer>{signature}<samlp:Status>"),
1445 1,
1446 );
1447 let key = load_private_key(SP_PRIVKEY, None)?;
1448 let mut manager = KeysManager::new();
1449 manager.add_key(key);
1450 let context = DsigContext::new(manager).with_insecure(true);
1451 Ok(sign(&context, &template)?)
1452 }
1453
1454 fn root_signature(xml: &str) -> Result<String, Box<dyn std::error::Error>> {
1455 let document = dom::parse(xml)?;
1456 let signature = children_named(&document.root, "Signature")
1457 .into_iter()
1458 .next()
1459 .ok_or("missing root Signature")?;
1460 Ok(xml[signature.start..signature.end].to_string())
1461 }
1462
1463 fn remove_signature_key_info(signature: &str) -> Result<String, Box<dyn std::error::Error>> {
1464 let document = dom::parse(signature)?;
1465 let key_info = children_named(&document.root, "KeyInfo")
1466 .into_iter()
1467 .next()
1468 .ok_or("missing KeyInfo")?;
1469 Ok(format!(
1470 "{}{}",
1471 &signature[..key_info.start],
1472 &signature[key_info.end..]
1473 ))
1474 }
1475
1476 fn assertion_signed_response_with_extra_signature(
1477 extra_signature: &str,
1478 ) -> Result<String, Box<dyn std::error::Error>> {
1479 let response = RESPONSE.replacen(
1480 "<saml:Issuer>",
1481 "<saml:Issuer ID=\"_signed_response_issuer\">",
1482 1,
1483 );
1484 let key = load_private_key(SP_PRIVKEY, None)?;
1485 let assertion_signed = construct_saml_signature(
1486 &response,
1487 false,
1488 &key,
1489 SP_SIGNING_CERT,
1490 RSA_SHA256,
1491 &[],
1492 None,
1493 )?;
1494 Ok(assertion_signed.replacen(
1495 "</samlp:Response>",
1496 &format!("{extra_signature}</samlp:Response>"),
1497 1,
1498 ))
1499 }
1500
1501 fn sign_template(template: &str) -> Result<String, Box<dyn std::error::Error>> {
1502 let key = load_private_key(SP_PRIVKEY, None)?;
1503 let mut manager = KeysManager::new();
1504 manager.add_key(key);
1505 let context = DsigContext::new(manager).with_insecure(true);
1506 Ok(sign(&context, template)?)
1507 }
1508
1509 fn response_signed_over_assertion_with_foreign_xpath(
1510 ) -> Result<String, Box<dyn std::error::Error>> {
1511 const XPATH_TRANSFORM: &str = "http://www.w3.org/TR/1999/REC-xpath-19991116";
1512 let document = dom::parse(RESPONSE)?;
1513 let assertion = children_named(&document.root, "Assertion")
1514 .into_iter()
1515 .next()
1516 .ok_or("missing Assertion")?;
1517 let assertion_id = node_saml_id(assertion).ok_or("missing Assertion ID")?;
1518 let digest = digest_for_signature(RSA_SHA256).ok_or("unknown digest")?;
1519 let signature = format!(
1520 "<ds:Signature xmlns:ds=\"{dsig}\"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm=\"{exc}\"/><ds:SignatureMethod Algorithm=\"{algorithm}\"/><ds:Reference URI=\"#{assertion_id}\"><ds:Transforms><ds:Transform Algorithm=\"{enveloped}\"/><Transform xmlns=\"urn:example:other\" Algorithm=\"{xpath}\"><XPath>0</XPath></Transform><ds:Transform Algorithm=\"{exc}\"/></ds:Transforms><ds:DigestMethod Algorithm=\"{digest}\"/><ds:DigestValue></ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue></ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>{cert}</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>",
1521 dsig = namespace::DSIG,
1522 exc = transform_algorithm::EXC_C14N,
1523 algorithm = RSA_SHA256,
1524 enveloped = transform_algorithm::ENVELOPED_SIGNATURE,
1525 xpath = XPATH_TRANSFORM,
1526 digest = digest,
1527 cert = normalize_cert_string(SP_SIGNING_CERT),
1528 );
1529 let template = RESPONSE.replacen(
1530 "</saml:Issuer><samlp:Status>",
1531 &format!("</saml:Issuer>{signature}<samlp:Status>"),
1532 1,
1533 );
1534 sign_template(&template)
1535 }
1536
1537 fn response_with_issuer_only_signature(
1538 canonicalization: &str,
1539 ) -> Result<String, Box<dyn std::error::Error>> {
1540 let response = RESPONSE.replacen(
1541 "<saml:Issuer>",
1542 "<saml:Issuer ID=\"_signed_response_issuer\">",
1543 1,
1544 );
1545 let digest = digest_for_signature(RSA_SHA256).ok_or("unknown digest")?;
1546 let signature = format!(
1547 "<ds:Signature xmlns:ds=\"{dsig}\"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm=\"{canonicalization}\"/><ds:SignatureMethod Algorithm=\"{algorithm}\"/><ds:Reference URI=\"#_signed_response_issuer\"><ds:Transforms><ds:Transform Algorithm=\"{enveloped}\"/><ds:Transform Algorithm=\"{exc}\"/></ds:Transforms><ds:DigestMethod Algorithm=\"{digest}\"/><ds:DigestValue></ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue></ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>{cert}</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>",
1548 dsig = namespace::DSIG,
1549 algorithm = RSA_SHA256,
1550 enveloped = transform_algorithm::ENVELOPED_SIGNATURE,
1551 exc = transform_algorithm::EXC_C14N,
1552 digest = digest,
1553 cert = normalize_cert_string(SP_SIGNING_CERT),
1554 );
1555 let template =
1556 response.replacen("</saml:Issuer>", &format!("</saml:Issuer>{signature}"), 1);
1557 sign_template(&template)
1558 }
1559
1560 fn cid_reference_response() -> Result<String, Box<dyn std::error::Error>> {
1561 let cert = normalize_cert_string(SP_SIGNING_CERT);
1562 let signature = format!(
1563 "<ds:Signature xmlns:ds=\"{dsig}\"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm=\"{exc_c14n}\"/><ds:SignatureMethod Algorithm=\"{sig_alg}\"/><ds:Reference URI=\"cid:attachment-1@example.com\"><ds:DigestMethod Algorithm=\"{digest}\"/><ds:DigestValue>AAAA</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue></ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>{cert}</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>",
1564 dsig = namespace::DSIG,
1565 exc_c14n = transform_algorithm::EXC_C14N,
1566 sig_alg = RSA_SHA256,
1567 digest = digest_for_signature(RSA_SHA256).ok_or("unknown digest")?,
1568 );
1569 let template =
1570 RESPONSE.replacen("<samlp:Status>", &format!("{signature}<samlp:Status>"), 1);
1571 let key = load_private_key(SP_PRIVKEY, None)?;
1572 let mut manager = KeysManager::new();
1573 manager.add_key(key);
1574 let ctx = DsigContext::new(manager).with_insecure(true);
1575 Ok(sign(&ctx, &template)?)
1576 }
1577
1578 fn assert_reference_resolution(
1579 result: Result<(bool, Option<String>), SamlError>,
1580 expected: ReferenceResolutionReason,
1581 ) -> Result<(), Box<dyn std::error::Error>> {
1582 match result {
1583 Err(SamlError::ReferenceResolution { reason }) if reason == expected => Ok(()),
1584 other => Err(format!("expected reference resolution {expected}, got {other:?}").into()),
1585 }
1586 }
1587
1588 #[test]
1589 fn dsig_context_secure_defaults_survive_insecure_builder() {
1590 let ctx = DsigContext::new(KeysManager::new());
1591 assert!(ctx.trusted_keys_only);
1592 assert!(ctx.strict_verification);
1593 assert!(ctx.require_reference_digests);
1594 assert_eq!(ctx.hmac_min_out_len, 160);
1595 assert!(!ctx.insecure);
1596
1597 let insecure = ctx.with_insecure(true);
1598 assert!(insecure.insecure);
1599 assert!(insecure.trusted_keys_only);
1600 assert!(insecure.strict_verification);
1601 assert!(insecure.require_reference_digests);
1602 assert_eq!(insecure.hmac_min_out_len, 160);
1603 }
1604
1605 #[test]
1606 fn verifies_signed_response_with_metadata_cert() -> Result<(), Box<dyn std::error::Error>> {
1607 let (verified, content) = verify_signature(RESPONSE_SIGNED, &[IDP_CERT.to_string()])?;
1608 assert!(
1609 verified,
1610 "response_signed.xml should verify with the IdP cert"
1611 );
1612 assert!(content
1613 .ok_or("expected signed assertion")?
1614 .contains("Assertion"));
1615 Ok(())
1616 }
1617
1618 #[test]
1619 fn verifies_generated_same_document_signature() -> Result<(), Box<dyn std::error::Error>> {
1620 let key = load_private_key(SP_PRIVKEY, None)?;
1621 let signed = construct_saml_signature(
1622 RESPONSE,
1623 false,
1624 &key,
1625 SP_SIGNING_CERT,
1626 RSA_SHA256,
1627 &[],
1628 None,
1629 )?;
1630 let (verified, content) = verify_signature(&signed, &[SP_SIGNING_CERT.to_string()])?;
1631 assert!(verified);
1632 assert!(content
1633 .ok_or("expected signed assertion")?
1634 .contains("Assertion"));
1635 Ok(())
1636 }
1637
1638 #[test]
1639 fn foreign_extension_certificate_is_not_treated_as_signature_key_info(
1640 ) -> Result<(), Box<dyn std::error::Error>> {
1641 let signed = signed_response_with_foreign_extension_certificate()?;
1642 let (verified, content) = verify_signature(&signed, &[SP_SIGNING_CERT.to_string()])?;
1643 assert!(verified);
1644 assert!(content
1645 .ok_or("expected signed assertion")?
1646 .contains("Assertion"));
1647 Ok(())
1648 }
1649
1650 #[test]
1651 fn detailed_verification_ignores_foreign_extension_certificate(
1652 ) -> Result<(), Box<dyn std::error::Error>> {
1653 let signed = signed_response_with_foreign_extension_certificate()?;
1654 let result = verify_signatures_detailed_with_profile(
1655 &signed,
1656 &[SP_SIGNING_CERT.to_string()],
1657 XmlLimits::default(),
1658 false,
1659 )?;
1660 assert!(result.verified() && result.assertion_directly_covered());
1661 Ok(())
1662 }
1663
1664 #[test]
1665 fn inline_certificate_is_not_used_without_metadata_pin(
1666 ) -> Result<(), Box<dyn std::error::Error>> {
1667 match verify_signature(RESPONSE_SIGNED, &[]) {
1668 Err(SamlError::NoTrustedCertificate) => Ok(()),
1669 other => Err(format!("expected missing pinned certificate, got {other:?}").into()),
1670 }
1671 }
1672
1673 #[test]
1674 fn first_invalid_signature_prevents_later_valid_signature_from_authorizing_response(
1675 ) -> Result<(), Box<dyn std::error::Error>> {
1676 let result = verify_signature(
1677 &response_with_first_invalid_signature()?,
1678 &[IDP_CERT.to_string()],
1679 )?;
1680 assert_eq!(result, (false, None));
1681 Ok(())
1682 }
1683
1684 #[test]
1685 fn detailed_verification_rejects_invalid_first_signature_before_later_assertion_coverage(
1686 ) -> Result<(), Box<dyn std::error::Error>> {
1687 let result = verify_signatures_detailed_with_profile(
1688 &response_with_first_invalid_signature()?,
1689 &[IDP_CERT.to_string()],
1690 XmlLimits::default(),
1691 false,
1692 )?;
1693 assert!(!result.verified() && !result.response_covered());
1694 assert!(result.verified_embedded_signatures().is_empty());
1695 Ok(())
1696 }
1697
1698 #[test]
1699 fn detailed_verification_excludes_untrusted_extra_signature_evidence(
1700 ) -> Result<(), Box<dyn std::error::Error>> {
1701 let untrusted = root_signature(&signature_over_top_level_issuer(
1702 UNTRUSTED_PRIVKEY,
1703 UNTRUSTED_CERT,
1704 RSA_SHA512,
1705 )?)?;
1706 let untrusted = remove_signature_key_info(&untrusted)?;
1707 let signed = assertion_signed_response_with_extra_signature(&untrusted)?;
1708 let result = verify_signatures_detailed_with_profile(
1709 &signed,
1710 &[SP_SIGNING_CERT.to_string()],
1711 XmlLimits::default(),
1712 false,
1713 )?;
1714
1715 let [signature] = result.verified_embedded_signatures() else {
1716 return Err("expected only the trusted Assertion signature".into());
1717 };
1718 assert!(result.verified() && result.assertion_directly_covered());
1719 assert_eq!(signature.algorithm_uri(), RSA_SHA256);
1720 assert!(signature.assertion_directly_covered() && !signature.response_covered());
1721 Ok(())
1722 }
1723
1724 #[test]
1725 fn detailed_verification_excludes_valid_unrelated_signature_evidence(
1726 ) -> Result<(), Box<dyn std::error::Error>> {
1727 let unrelated = root_signature(&signature_over_top_level_issuer(
1728 SP_PRIVKEY,
1729 SP_SIGNING_CERT,
1730 RSA_SHA512,
1731 )?)?;
1732 let signed = assertion_signed_response_with_extra_signature(&unrelated)?;
1733 let result = verify_signatures_detailed_with_profile(
1734 &signed,
1735 &[SP_SIGNING_CERT.to_string()],
1736 XmlLimits::default(),
1737 false,
1738 )?;
1739
1740 let [signature] = result.verified_embedded_signatures() else {
1741 return Err("expected only the Assertion-covering signature".into());
1742 };
1743 assert!(result.verified() && result.assertion_directly_covered());
1744 assert_eq!(signature.algorithm_uri(), RSA_SHA256);
1745 assert!(signature.assertion_directly_covered() && !signature.response_covered());
1746 Ok(())
1747 }
1748
1749 #[test]
1750 fn strict_profile_accepts_verified_assertion_beside_nonconforming_extra_signature(
1751 ) -> Result<(), Box<dyn std::error::Error>> {
1752 let extra = format!(
1753 "<ds:Signature xmlns:ds=\"{dsig}\"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"/><ds:SignatureMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#rsa-sha1\"/><ds:Reference URI=\"#_response\"><ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/><ds:DigestValue>AAAA</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>AAAA</ds:SignatureValue></ds:Signature>",
1754 dsig = namespace::DSIG,
1755 );
1756 let signed = assertion_signed_response_with_extra_signature(&extra)?;
1757 let result = verify_signatures_detailed_with_profile(
1758 &signed,
1759 &[SP_SIGNING_CERT.to_string()],
1760 XmlLimits::default(),
1761 true,
1762 )?;
1763
1764 let [signature] = result.verified_embedded_signatures() else {
1765 return Err("expected only the trusted Assertion signature".into());
1766 };
1767 assert!(result.verified() && result.assertion_directly_covered());
1768 assert_eq!(signature.algorithm_uri(), RSA_SHA256);
1769 assert!(signature.assertion_directly_covered() && !signature.response_covered());
1770 Ok(())
1771 }
1772
1773 #[test]
1774 fn strict_profile_rejects_verified_covering_signature_with_two_references(
1775 ) -> Result<(), Box<dyn std::error::Error>> {
1776 let signed = signature_over_response_and_assertion()?;
1777 let result = verify_signatures_detailed_with_profile(
1778 &signed,
1779 &[SP_SIGNING_CERT.to_string()],
1780 XmlLimits::default(),
1781 true,
1782 );
1783
1784 assert!(matches!(result, Err(SamlError::SignedReferenceMismatch)));
1785 Ok(())
1786 }
1787
1788 #[test]
1789 fn strict_profile_rejects_foreign_xpath_transform_on_verified_assertion_signature(
1790 ) -> Result<(), Box<dyn std::error::Error>> {
1791 let signed = response_signed_over_assertion_with_foreign_xpath()?;
1792 let compatible = verify_signatures_detailed_with_profile(
1793 &signed,
1794 &[SP_SIGNING_CERT.to_string()],
1795 XmlLimits::default(),
1796 false,
1797 )?;
1798 let [signature] = compatible.verified_embedded_signatures() else {
1799 return Err("expected the assertion-covering signature".into());
1800 };
1801 assert!(compatible.verified() && compatible.assertion_directly_covered());
1802 assert!(signature.assertion_directly_covered());
1803
1804 let strict = verify_signatures_detailed_with_profile(
1805 &signed,
1806 &[SP_SIGNING_CERT.to_string()],
1807 XmlLimits::default(),
1808 true,
1809 );
1810 assert!(matches!(strict, Err(SamlError::AlgorithmUnsupported)));
1811 Ok(())
1812 }
1813
1814 #[test]
1815 fn strict_profile_ignores_verified_signature_that_does_not_authenticate_the_response(
1816 ) -> Result<(), Box<dyn std::error::Error>> {
1817 let signed = response_with_issuer_only_signature(XML_C14N_10)?;
1818 let result = verify_signatures_detailed_with_profile(
1819 &signed,
1820 &[SP_SIGNING_CERT.to_string()],
1821 XmlLimits::default(),
1822 true,
1823 );
1824 assert!(matches!(result, Err(SamlError::SignedReferenceMismatch)));
1825 Ok(())
1826 }
1827
1828 #[test]
1829 fn detailed_verification_reports_one_signature_covering_response_and_assertion(
1830 ) -> Result<(), Box<dyn std::error::Error>> {
1831 let signed = signature_over_response_and_assertion()?;
1832 let result = verify_signatures_detailed_with_profile(
1833 &signed,
1834 &[SP_SIGNING_CERT.to_string()],
1835 XmlLimits::default(),
1836 false,
1837 )?;
1838
1839 let [signature] = result.verified_embedded_signatures() else {
1840 return Err("expected one verified embedded signature".into());
1841 };
1842 assert!(result.verified() && result.assertion_directly_covered());
1843 assert!(result.response_covered());
1844 assert_eq!(signature.algorithm_uri(), RSA_SHA512);
1845 assert!(signature.assertion_directly_covered() && signature.response_covered());
1846 Ok(())
1847 }
1848
1849 #[test]
1850 fn detailed_verification_aggregates_rolling_cert_coverage_after_first_signature_verifies(
1851 ) -> Result<(), Box<dyn std::error::Error>> {
1852 let response_key = load_private_key(SP_PRIVKEY, None)?;
1853 let signed_response_and_assertion = construct_saml_signature(
1854 RESPONSE_SIGNED,
1855 true,
1856 &response_key,
1857 SP_SIGNING_CERT,
1858 RSA_SHA256,
1859 &[],
1860 None,
1861 )?;
1862 let result = verify_signatures_detailed_with_profile(
1863 &signed_response_and_assertion,
1864 &[SP_SIGNING_CERT.to_string(), IDP_CERT.to_string()],
1865 XmlLimits::default(),
1866 false,
1867 )?;
1868 assert!(
1869 result.verified() && result.assertion_directly_covered() && result.response_covered()
1870 );
1871 assert_eq!(result.verified_embedded_signatures().len(), 2);
1872 Ok(())
1873 }
1874
1875 #[test]
1876 fn signed_cid_reference_is_rejected_before_content_extraction(
1877 ) -> Result<(), Box<dyn std::error::Error>> {
1878 assert_reference_resolution(
1879 verify_signature(&cid_reference_response()?, &[SP_SIGNING_CERT.to_string()]),
1880 ReferenceResolutionReason::ExternalReference,
1881 )
1882 }
1883
1884 #[test]
1885 fn rejects_signed_request_without_root_coverage() -> Result<(), Box<dyn std::error::Error>> {
1886 match verify_signature(SIGNED_REQUEST, &[SP_CERT.to_string()]) {
1887 Err(SamlError::SignedReferenceMismatch) => Ok(()),
1888 other => {
1889 Err(format!("expected uncovered AuthnRequest rejection, got {other:?}").into())
1890 }
1891 }
1892 }
1893
1894 #[test]
1895 fn rejects_wrong_certificate() -> Result<(), Box<dyn std::error::Error>> {
1896 match verify_signature(RESPONSE_SIGNED, &[SP_CERT.to_string()]) {
1899 Err(SamlError::CertificateMismatch) => Ok(()),
1900 other => Err(format!("expected CertificateMismatch, got {other:?}").into()),
1901 }
1902 }
1903
1904 #[test]
1905 fn rejects_tampered_signature() -> Result<(), Box<dyn std::error::Error>> {
1906 let (verified, _) = verify_signature(FALSE_SIGNED, &[SP_CERT.to_string()])?;
1908 assert!(!verified, "tampered message must not verify");
1909 Ok(())
1910 }
1911
1912 #[test]
1913 fn rolling_cert_unloadable_peer_keeps_invalid_verdict() -> Result<(), Box<dyn std::error::Error>>
1914 {
1915 let garbage = "not a certificate".to_string();
1916 let signer = SP_CERT.to_string();
1917 for certs in [vec![garbage.clone(), signer.clone()], vec![signer, garbage]] {
1918 assert_eq!(
1919 verify_signature(FALSE_SIGNED, &certs)?,
1920 (false, None),
1921 "unloadable leftover must not replace Invalid with Crypto"
1922 );
1923 }
1924 Ok(())
1925 }
1926
1927 #[test]
1928 fn detailed_rolling_cert_unloadable_peer_keeps_invalid_verdict(
1929 ) -> Result<(), Box<dyn std::error::Error>> {
1930 let garbage = "not a certificate".to_string();
1931 let signer = SP_CERT.to_string();
1932 for certs in [vec![garbage.clone(), signer.clone()], vec![signer, garbage]] {
1933 let result = verify_signatures_detailed_with_profile(
1934 FALSE_SIGNED,
1935 &certs,
1936 XmlLimits::default(),
1937 false,
1938 )?;
1939 assert!(
1940 !result.verified()
1941 && !result.assertion_directly_covered()
1942 && !result.response_covered(),
1943 "unloadable leftover must not replace Invalid with Crypto"
1944 );
1945 }
1946 Ok(())
1947 }
1948
1949 #[test]
1950 fn rolling_cert_unloadable_peer_does_not_block_valid_signature(
1951 ) -> Result<(), Box<dyn std::error::Error>> {
1952 let key = load_private_key(SP_PRIVKEY, None)?;
1954 let signed = construct_saml_signature(
1955 RESPONSE,
1956 false,
1957 &key,
1958 SP_SIGNING_CERT,
1959 RSA_SHA256,
1960 &[],
1961 None,
1962 )?;
1963 let (verified, content) = verify_signature(
1964 &signed,
1965 &["not a certificate".to_string(), SP_SIGNING_CERT.to_string()],
1966 )?;
1967 assert!(verified);
1968 assert!(content
1969 .ok_or("expected signed assertion")?
1970 .contains("Assertion"));
1971 Ok(())
1972 }
1973
1974 #[test]
1975 fn rejects_multi_root_wrapping_attack_before_signature_verification(
1976 ) -> Result<(), Box<dyn std::error::Error>> {
1977 match verify_signature(ATTACK, &[IDP_CERT.to_string()]) {
1980 Err(SamlError::Xml(message)) if message == "multiple document elements" => Ok(()),
1981 other => Err(format!("expected multi-root XML rejection, got {other:?}").into()),
1982 }
1983 }
1984
1985 #[test]
1986 fn no_signature_returns_false() -> Result<(), Box<dyn std::error::Error>> {
1987 let (verified, content) = verify_signature("<samlp:Response>x</samlp:Response>", &[])?;
1989 assert!(!verified);
1990 assert!(content.is_none());
1991 let _ = extract("<a/>", &[ExtractorField::new("x", &["a"])])?;
1993 Ok(())
1994 }
1995}