1use crate::binding::{
5 base64_decode_with_limit, deflate_raw_decode_with_limit, MAX_DEFLATE_RAW_DECODE_BYTES,
6};
7use crate::constants::{Binding, ParserType};
8use crate::context::is_valid_xml_with_limits;
9use crate::error::SamlError;
10#[cfg(any(
11 feature = "crypto-rustcrypto",
12 feature = "crypto-aws-lc",
13 feature = "crypto-fips"
14))]
15use crate::error::SignatureVerificationReason;
16#[cfg(any(
17 feature = "crypto-rustcrypto",
18 feature = "crypto-aws-lc",
19 feature = "crypto-fips"
20))]
21use crate::model::RelayStateParam;
22use crate::util::Value;
23use crate::validator::{check_status_with_limits, logout_request_not_on_or_after_deadline};
24use crate::xml::{
25 extract_with_limits, fields, validate_protocol_profile, ExtractorField, XmlLimits,
26};
27use std::time::SystemTime;
28use time::OffsetDateTime;
29
30#[derive(Debug, Default, Clone)]
32pub struct HttpRequest {
33 pub query: Vec<(String, String)>,
35 pub body: Vec<(String, String)>,
37 pub octet_string: Option<String>,
39}
40
41impl HttpRequest {
42 pub fn redirect(query: Vec<(String, String)>) -> Self {
44 Self {
45 query,
46 ..Default::default()
47 }
48 }
49
50 pub fn post(body: Vec<(String, String)>) -> Self {
52 Self {
53 body,
54 ..Default::default()
55 }
56 }
57
58 fn query_get(&self, key: &str) -> Result<Option<&str>, SamlError> {
59 single_param(&self.query, key)
60 }
61
62 fn body_get(&self, key: &str) -> Result<Option<&str>, SamlError> {
63 single_param(&self.body, key)
64 }
65}
66
67fn single_param<'a>(
68 params: &'a [(String, String)],
69 key: &str,
70) -> Result<Option<&'a str>, SamlError> {
71 let mut values = params
72 .iter()
73 .filter(|(candidate, _)| candidate == key)
74 .map(|(_, value)| value.as_str());
75 let first = values.next();
76 if values.next().is_some() {
77 return Err(SamlError::Invalid("ERR_AMBIGUOUS_FLOW_INPUT".into()));
78 }
79 Ok(first)
80}
81
82fn missing_binding_parameter(name: &'static str) -> SamlError {
83 SamlError::MissingBindingParameter { name }
84}
85
86fn unsupported_binding(binding: Binding) -> SamlError {
87 SamlError::UnsupportedBinding { binding }
88}
89
90#[non_exhaustive]
92#[derive(Debug, Clone)]
93pub struct FlowOptions<'a> {
94 pub binding: Option<Binding>,
96 pub parser_type: Option<ParserType>,
98 pub redirect_inflate_max_bytes: usize,
101 pub xml_limits: XmlLimits,
103 pub check_signature: bool,
105 pub verify_signature_if_present: bool,
109 pub require_audience_restriction: bool,
114 pub strict_xml_signature_profile: bool,
116 pub from_issuer: Option<&'a str>,
118 pub signing_certs: &'a [String],
120 pub decrypt_key: Option<&'a str>,
122 pub decrypt_key_pass: Option<&'a str>,
124 pub allow_insecure_software_rsa_key_transport_decryption: bool,
131 pub clock_drifts: (i64, i64),
133 pub now: Option<SystemTime>,
136 pub expected_audience: Option<&'a str>,
138 pub expected_in_response_to: Option<&'a str>,
140}
141
142impl<'a> Default for FlowOptions<'a> {
143 fn default() -> Self {
144 Self {
145 binding: None,
146 parser_type: None,
147 redirect_inflate_max_bytes: MAX_DEFLATE_RAW_DECODE_BYTES,
148 xml_limits: XmlLimits::default(),
149 check_signature: false,
150 verify_signature_if_present: false,
151 require_audience_restriction: true,
152 strict_xml_signature_profile: false,
153 from_issuer: None,
154 signing_certs: &[],
155 decrypt_key: None,
156 decrypt_key_pass: None,
157 allow_insecure_software_rsa_key_transport_decryption: false,
158 clock_drifts: (0, 0),
159 now: None,
160 expected_audience: None,
161 expected_in_response_to: None,
162 }
163 }
164}
165
166impl FlowOptions<'_> {
167 pub(crate) fn validation_now(&self) -> Result<OffsetDateTime, SamlError> {
168 self.now.map_or_else(
169 || Ok(OffsetDateTime::now_utc()),
170 crate::validator::offset_datetime_from_system_time,
171 )
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub(crate) enum AssertionSignatureRequirement {
177 Compatible,
178 Direct,
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub(crate) enum ResponseSignatureRequirement {
183 Optional,
184 RequiredForEncryptedCbc,
185 Required,
186}
187
188#[derive(Debug)]
189struct PreparedMessage {
190 saml_content: String,
191 assertion: Option<String>,
192 message_authenticated: bool,
193 verified_xml_signatures: Vec<VerifiedXmlSignatureEvidence>,
194}
195
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub(crate) struct VerifiedXmlSignatureEvidence {
198 algorithm_uri: String,
199 assertion_directly_covered: bool,
200 response_covered: bool,
201}
202
203impl VerifiedXmlSignatureEvidence {
204 pub(crate) fn algorithm_uri(&self) -> &str {
205 &self.algorithm_uri
206 }
207
208 pub(crate) fn assertion_directly_covered(&self) -> bool {
209 self.assertion_directly_covered
210 }
211
212 pub(crate) fn response_covered(&self) -> bool {
213 self.response_covered
214 }
215}
216
217#[cfg(any(
218 feature = "crypto-rustcrypto",
219 feature = "crypto-aws-lc",
220 feature = "crypto-fips"
221))]
222#[derive(Debug)]
223struct EmbeddedSignatureEvidence {
224 verified: bool,
225 verified_node: Option<String>,
226 assertion_directly_covered: bool,
227 response_covered: bool,
228 verified_xml_signatures: Vec<VerifiedXmlSignatureEvidence>,
229}
230
231#[derive(Debug, Clone)]
238pub struct FlowResult {
239 pub saml_content: String,
241 pub extract: Value,
243 pub sig_alg: Option<String>,
245}
246
247#[derive(Debug)]
248pub(crate) struct FlowResultWithSignatureEvidence {
249 flow_result: FlowResult,
250 verified_xml_signatures: Vec<VerifiedXmlSignatureEvidence>,
251 message_authenticated: bool,
252}
253
254impl FlowResultWithSignatureEvidence {
255 pub(crate) fn into_parts(self) -> (FlowResult, Vec<VerifiedXmlSignatureEvidence>) {
256 (self.flow_result, self.verified_xml_signatures)
257 }
258
259 pub(crate) fn message_authenticated(&self) -> bool {
260 self.message_authenticated
261 }
262
263 fn into_flow_result(self) -> FlowResult {
264 self.flow_result
265 }
266
267 pub(crate) fn extract(&self) -> &Value {
268 &self.flow_result.extract
269 }
270
271 pub(crate) fn saml_content(&self) -> &str {
272 &self.flow_result.saml_content
273 }
274}
275
276fn default_fields(
277 parser_type: ParserType,
278 assertion: Option<&str>,
279) -> Result<Vec<ExtractorField>, SamlError> {
280 Ok(match parser_type {
281 ParserType::SamlRequest => fields::login_request_fields(),
282 ParserType::SamlResponse => {
283 let assertion =
284 assertion.ok_or_else(|| SamlError::Xml("ERR_EMPTY_ASSERTION".into()))?;
285 fields::login_response_fields(assertion)
286 }
287 ParserType::LogoutRequest => fields::logout_request_fields(),
288 ParserType::LogoutResponse => fields::logout_response_fields(),
289 })
290}
291
292fn decode_message(
293 binding: Binding,
294 parser_type: ParserType,
295 request: &HttpRequest,
296 redirect_inflate_max_bytes: usize,
297 xml_limits: XmlLimits,
298) -> Result<String, SamlError> {
299 let direction = parser_type.query_param();
300 let bytes = match binding {
301 Binding::Redirect => {
302 let content = request
303 .query_get(direction)?
304 .ok_or_else(|| missing_binding_parameter(direction))?;
305 let redirect_max_bytes = redirect_inflate_max_bytes.min(xml_limits.max_bytes);
306 let compressed = base64_decode_with_limit(content, redirect_max_bytes)?;
307 deflate_raw_decode_with_limit(&compressed, redirect_max_bytes)?
308 }
309 Binding::Post | Binding::SimpleSign => {
310 let content = request
311 .body_get(direction)?
312 .ok_or_else(|| missing_binding_parameter(direction))?;
313 base64_decode_with_limit(content, xml_limits.max_bytes)?
314 }
315 Binding::Artifact => return Err(unsupported_binding(binding)),
316 };
317 xml_limits.check_input_bytes(bytes.len())?;
318 String::from_utf8(bytes).map_err(|e| SamlError::Xml(e.to_string()))
319}
320
321fn assertion_shortcut(xml: &str, limits: XmlLimits) -> Result<Option<String>, SamlError> {
322 crate::assertion_acceptance::first_login_assertion_xml(xml, limits)
323}
324
325#[cfg(any(
326 feature = "crypto-rustcrypto",
327 feature = "crypto-aws-lc",
328 feature = "crypto-fips"
329))]
330fn verified_content_not_covered() -> SamlError {
331 SamlError::SignedReferenceMismatch
332}
333
334#[cfg(any(
335 feature = "crypto-rustcrypto",
336 feature = "crypto-aws-lc",
337 feature = "crypto-fips"
338))]
339fn decoded_octet_params(octet: &str) -> Vec<(String, String)> {
340 url::form_urlencoded::parse(octet.as_bytes())
341 .map(|(key, value)| (key.into_owned(), value.into_owned()))
342 .collect()
343}
344
345#[cfg(any(
346 feature = "crypto-rustcrypto",
347 feature = "crypto-aws-lc",
348 feature = "crypto-fips"
349))]
350fn detached_signature_verification() -> SamlError {
351 SamlError::SignatureVerification {
352 reason: SignatureVerificationReason::DetachedMessageSignature,
353 }
354}
355
356#[cfg(any(
357 feature = "crypto-rustcrypto",
358 feature = "crypto-aws-lc",
359 feature = "crypto-fips"
360))]
361fn relay_state_param(value: Option<&str>) -> Option<RelayStateParam> {
362 RelayStateParam::try_from_option(value.map(str::to_string)).ok()
363}
364
365#[cfg(any(
366 feature = "crypto-rustcrypto",
367 feature = "crypto-aws-lc",
368 feature = "crypto-fips"
369))]
370fn detached_relay_state_mismatch(expected: Option<&str>, actual: Option<&str>) -> SamlError {
371 match (relay_state_param(expected), relay_state_param(actual)) {
372 (Some(expected), Some(actual)) => SamlError::RelayStateMismatch { expected, actual },
373 _ => SamlError::SignatureVerification {
374 reason: SignatureVerificationReason::RelayStateCorrelation,
375 },
376 }
377}
378
379#[cfg(any(
380 feature = "crypto-rustcrypto",
381 feature = "crypto-aws-lc",
382 feature = "crypto-fips"
383))]
384fn ensure_redirect_octet_matches_consumed_fields(
385 parser_type: ParserType,
386 request: &HttpRequest,
387 sig_alg: &str,
388 octet: &str,
389) -> Result<(), SamlError> {
390 let direction = parser_type.query_param();
391 let signed = decoded_octet_params(octet);
392 if single_param(&signed, "Signature")?.is_some() {
393 return Err(detached_signature_verification());
394 }
395
396 let signed_message =
397 single_param(&signed, direction)?.ok_or_else(|| missing_binding_parameter(direction))?;
398 let consumed_message = request
399 .query_get(direction)?
400 .ok_or_else(|| missing_binding_parameter(direction))?;
401 if signed_message != consumed_message {
402 return Err(detached_signature_verification());
403 }
404
405 let signed_sig_alg =
406 single_param(&signed, "SigAlg")?.ok_or_else(|| missing_binding_parameter("SigAlg"))?;
407 if signed_sig_alg != sig_alg {
408 return Err(detached_signature_verification());
409 }
410
411 let signed_relay_state = single_param(&signed, "RelayState")?;
412 let consumed_relay_state = request.query_get("RelayState")?;
413 if signed_relay_state != consumed_relay_state {
414 return Err(detached_relay_state_mismatch(
415 signed_relay_state,
416 consumed_relay_state,
417 ));
418 }
419
420 Ok(())
421}
422
423#[cfg(any(
424 feature = "crypto-rustcrypto",
425 feature = "crypto-aws-lc",
426 feature = "crypto-fips"
427))]
428fn ensure_simplesign_octet_matches_consumed_fields(
429 parser_type: ParserType,
430 request: &HttpRequest,
431 xml: &str,
432 sig_alg: &str,
433 octet: &str,
434) -> Result<(), SamlError> {
435 let direction = parser_type.query_param();
436 request
437 .body_get(direction)?
438 .ok_or_else(|| missing_binding_parameter(direction))?;
439
440 let message_and_sig_alg = format!("{direction}={xml}&SigAlg={sig_alg}");
441 let message_empty_relay_and_sig_alg = format!("{direction}={xml}&RelayState=&SigAlg={sig_alg}");
442 let matches = match request.body_get("RelayState")? {
443 Some(relay_state) => {
444 let expected = format!("{direction}={xml}&RelayState={relay_state}&SigAlg={sig_alg}");
445 octet == expected
446 }
447 None => octet == message_and_sig_alg || octet == message_empty_relay_and_sig_alg,
451 };
452
453 if matches {
454 Ok(())
455 } else {
456 Err(detached_signature_verification())
457 }
458}
459
460#[cfg(any(
461 feature = "crypto-rustcrypto",
462 feature = "crypto-aws-lc",
463 feature = "crypto-fips"
464))]
465fn ensure_detached_octet_matches_consumed_fields(
466 binding: Binding,
467 parser_type: ParserType,
468 request: &HttpRequest,
469 xml: &str,
470 sig_alg: &str,
471 octet: &str,
472) -> Result<(), SamlError> {
473 match binding {
474 Binding::Redirect => {
475 ensure_redirect_octet_matches_consumed_fields(parser_type, request, sig_alg, octet)
476 }
477 Binding::SimpleSign => ensure_simplesign_octet_matches_consumed_fields(
478 parser_type,
479 request,
480 xml,
481 sig_alg,
482 octet,
483 ),
484 Binding::Post | Binding::Artifact => Ok(()),
485 }
486}
487
488#[cfg(any(
489 feature = "crypto-rustcrypto",
490 feature = "crypto-aws-lc",
491 feature = "crypto-fips"
492))]
493fn required_xml_signature_failed(signature_present: bool) -> SamlError {
494 if signature_present {
495 SamlError::SignatureVerification {
496 reason: SignatureVerificationReason::XmlSignature,
497 }
498 } else {
499 SamlError::SignatureMissing
500 }
501}
502
503#[cfg(any(
504 feature = "crypto-rustcrypto",
505 feature = "crypto-aws-lc",
506 feature = "crypto-fips"
507))]
508fn verify_embedded_signature(
509 xml: &str,
510 opts: &FlowOptions<'_>,
511) -> Result<EmbeddedSignatureEvidence, SamlError> {
512 let verification = crate::crypto::verify::verify_signatures_detailed_with_profile(
513 xml,
514 opts.signing_certs,
515 opts.xml_limits,
516 opts.strict_xml_signature_profile,
517 )?;
518 let verified_xml_signatures = verification
519 .verified_embedded_signatures()
520 .iter()
521 .map(|signature| VerifiedXmlSignatureEvidence {
522 algorithm_uri: signature.algorithm_uri().to_string(),
523 assertion_directly_covered: signature.assertion_directly_covered(),
524 response_covered: signature.response_covered(),
525 })
526 .collect();
527 Ok(EmbeddedSignatureEvidence {
528 verified: verification.verified(),
529 assertion_directly_covered: verification.assertion_directly_covered(),
530 response_covered: verification.response_covered(),
531 verified_node: verification.into_signed_content(),
532 verified_xml_signatures,
533 })
534}
535
536#[cfg(any(
537 feature = "crypto-rustcrypto",
538 feature = "crypto-aws-lc",
539 feature = "crypto-fips"
540))]
541fn require_direct_assertion_coverage(
542 assertion_signature: AssertionSignatureRequirement,
543 assertion_directly_covered: bool,
544) -> Result<(), SamlError> {
545 if assertion_signature == AssertionSignatureRequirement::Direct && !assertion_directly_covered {
546 return Err(SamlError::AssertionSignatureRequired);
547 }
548 Ok(())
549}
550
551#[cfg(any(
552 feature = "crypto-rustcrypto",
553 feature = "crypto-aws-lc",
554 feature = "crypto-fips"
555))]
556fn require_response_coverage(
557 response_signature_required: bool,
558 response_covered: bool,
559) -> Result<(), SamlError> {
560 if response_signature_required && !response_covered {
561 return Err(SamlError::SignedReferenceMismatch);
562 }
563 Ok(())
564}
565
566#[cfg(any(
567 feature = "crypto-rustcrypto",
568 feature = "crypto-aws-lc",
569 feature = "crypto-fips"
570))]
571fn response_uses_cbc_encrypted_assertion(xml: &str, limits: XmlLimits) -> Result<bool, SamlError> {
572 let document = crate::xml::dom::parse_with_limits(xml, limits)?;
573 Ok(document
574 .root
575 .children
576 .iter()
577 .filter(|child| child.local_name == "EncryptedAssertion")
578 .filter_map(|encrypted_assertion| {
579 encrypted_assertion
580 .children
581 .iter()
582 .find(|child| child.local_name == "EncryptedData")
583 })
584 .filter_map(|encrypted_data| {
585 encrypted_data
586 .children
587 .iter()
588 .find(|child| child.local_name == "EncryptionMethod")
589 })
590 .filter_map(|encryption_method| encryption_method.attr("Algorithm"))
591 .any(crate::constants::is_xml_encryption_cbc_algorithm))
592}
593
594#[cfg(any(
595 feature = "crypto-rustcrypto",
596 feature = "crypto-aws-lc",
597 feature = "crypto-fips"
598))]
599fn response_signature_is_required(
600 requirement: ResponseSignatureRequirement,
601 xml: &str,
602 limits: XmlLimits,
603) -> Result<bool, SamlError> {
604 match requirement {
605 ResponseSignatureRequirement::Optional => Ok(false),
606 ResponseSignatureRequirement::RequiredForEncryptedCbc => {
607 response_uses_cbc_encrypted_assertion(xml, limits)
608 }
609 ResponseSignatureRequirement::Required => Ok(true),
610 }
611}
612
613#[cfg(any(
616 feature = "crypto-rustcrypto",
617 feature = "crypto-aws-lc",
618 feature = "crypto-fips"
619))]
620fn verify_and_prepare(
621 xml: &str,
622 parser_type: ParserType,
623 opts: &FlowOptions<'_>,
624 assertion_signature: AssertionSignatureRequirement,
625 response_signature: ResponseSignatureRequirement,
626) -> Result<PreparedMessage, SamlError> {
627 use crate::crypto::{
628 decrypt_assertion_with_limits,
629 enc::{require_software_rsa_opt_in, AssertionDecryptionOptions},
630 keys::load_private_key,
631 verify::has_xml_signature_with_limits,
632 };
633
634 let signature_present = has_xml_signature_with_limits(xml, opts.xml_limits)?;
635 let evidence = verify_embedded_signature(xml, opts)?;
636 let response_signature_required =
637 response_signature_is_required(response_signature, xml, opts.xml_limits)?;
638 if response_signature_required {
639 if !evidence.verified {
640 return Err(required_xml_signature_failed(signature_present));
641 }
642 require_response_coverage(response_signature_required, evidence.response_covered)?;
643 }
644 let decrypt_required = opts.decrypt_key.is_some();
645 let decrypt_options = AssertionDecryptionOptions {
646 allow_insecure_software_rsa_key_transport_decryption: opts
647 .allow_insecure_software_rsa_key_transport_decryption,
648 };
649 if decrypt_required {
650 require_software_rsa_opt_in(decrypt_options)?;
651 }
652 let load_key = || load_private_key(opts.decrypt_key.unwrap_or_default(), opts.decrypt_key_pass);
653
654 if decrypt_required && evidence.verified && parser_type == ParserType::SamlResponse {
655 if let Some(node) = evidence.verified_node.as_deref() {
656 let mut verified_xml_signatures = evidence.verified_xml_signatures.clone();
657 let (content, assertion) = decrypt_assertion_with_limits(
660 node,
661 &load_key()?,
662 decrypt_options,
663 opts.xml_limits,
664 )?;
665 is_valid_xml_with_limits(&content, opts.xml_limits)?;
666 validate_protocol_profile(&content, parser_type, opts.xml_limits)?;
667 if assertion_signature == AssertionSignatureRequirement::Direct {
668 let decrypted_signature_present =
669 has_xml_signature_with_limits(&assertion, opts.xml_limits)?;
670 let decrypted_evidence = verify_embedded_signature(&assertion, opts)?;
671 if !decrypted_evidence.verified {
672 return Err(required_xml_signature_failed(decrypted_signature_present));
673 }
674 require_direct_assertion_coverage(
675 assertion_signature,
676 decrypted_evidence.assertion_directly_covered,
677 )?;
678 verified_xml_signatures.extend(decrypted_evidence.verified_xml_signatures);
679 }
680 return Ok(PreparedMessage {
681 saml_content: content,
682 assertion: Some(assertion),
683 message_authenticated: evidence.response_covered,
684 verified_xml_signatures,
685 });
686 }
687 }
688 if decrypt_required && !evidence.verified {
689 let (content, assertion) =
691 decrypt_assertion_with_limits(xml, &load_key()?, decrypt_options, opts.xml_limits)?;
692 is_valid_xml_with_limits(&content, opts.xml_limits)?;
693 validate_protocol_profile(&content, parser_type, opts.xml_limits)?;
694 let verification_xml = if assertion_signature == AssertionSignatureRequirement::Direct {
695 assertion.as_str()
696 } else {
697 content.as_str()
698 };
699 let signature_present = has_xml_signature_with_limits(verification_xml, opts.xml_limits)?;
700 let re_evidence = verify_embedded_signature(verification_xml, opts)?;
701 return if re_evidence.verified {
702 require_direct_assertion_coverage(
703 assertion_signature,
704 re_evidence.assertion_directly_covered,
705 )?;
706 let verified_assertion = if assertion_signature == AssertionSignatureRequirement::Direct
707 {
708 Some(assertion)
709 } else {
710 re_evidence.verified_node
711 };
712 Ok(PreparedMessage {
713 saml_content: content,
714 assertion: verified_assertion,
715 message_authenticated: false,
716 verified_xml_signatures: re_evidence.verified_xml_signatures,
717 })
718 } else {
719 Err(required_xml_signature_failed(signature_present))
720 };
721 }
722 if evidence.verified {
723 require_direct_assertion_coverage(
724 assertion_signature,
725 evidence.assertion_directly_covered,
726 )?;
727 if matches!(
728 parser_type,
729 ParserType::SamlRequest | ParserType::LogoutRequest | ParserType::LogoutResponse
730 ) {
731 let content = evidence
732 .verified_node
733 .ok_or_else(verified_content_not_covered)?;
734 return Ok(PreparedMessage {
735 saml_content: content,
736 assertion: None,
737 message_authenticated: true,
738 verified_xml_signatures: evidence.verified_xml_signatures,
739 });
740 }
741 return Ok(PreparedMessage {
742 saml_content: xml.to_string(),
743 assertion: evidence.verified_node,
744 message_authenticated: evidence.response_covered,
745 verified_xml_signatures: evidence.verified_xml_signatures,
746 });
747 }
748 Err(required_xml_signature_failed(signature_present))
749}
750
751#[cfg(not(any(
752 feature = "crypto-rustcrypto",
753 feature = "crypto-aws-lc",
754 feature = "crypto-fips"
755)))]
756fn verify_and_prepare(
757 _xml: &str,
758 _parser_type: ParserType,
759 _opts: &FlowOptions<'_>,
760 _assertion_signature: AssertionSignatureRequirement,
761 _response_signature: ResponseSignatureRequirement,
762) -> Result<PreparedMessage, SamlError> {
763 Err(SamlError::Unsupported(
764 "signature verification requires a crypto provider feature".into(),
765 ))
766}
767
768#[cfg(any(
771 feature = "crypto-rustcrypto",
772 feature = "crypto-aws-lc",
773 feature = "crypto-fips"
774))]
775fn verify_detached(
776 binding: Binding,
777 parser_type: ParserType,
778 request: &HttpRequest,
779 opts: &FlowOptions<'_>,
780 xml: &str,
781) -> Result<String, SamlError> {
782 let get = |k: &str| -> Result<Option<&str>, SamlError> {
783 match binding {
784 Binding::Redirect => request.query_get(k),
785 _ => request.body_get(k),
786 }
787 };
788 let signature = get("Signature")?.ok_or(SamlError::SignatureMissing)?;
789 let sig_alg = get("SigAlg")?.ok_or_else(|| missing_binding_parameter("SigAlg"))?;
790 let octet = request
791 .octet_string
792 .as_deref()
793 .ok_or_else(|| missing_binding_parameter("octet_string"))?;
794 ensure_detached_octet_matches_consumed_fields(
795 binding,
796 parser_type,
797 request,
798 xml,
799 sig_alg,
800 octet,
801 )?;
802 crate::crypto::initialize_crypto_provider()?;
803
804 let mut provider_error = None;
805 let mut tried_invalid = false;
806 for cert in opts.signing_certs {
807 match crate::crypto::verify_message_signature(octet, signature, cert, sig_alg) {
808 Ok(true) => return Ok(sig_alg.to_string()),
809 Ok(false) => tried_invalid = true,
810 Err(error @ SamlError::Crypto(_)) => {
811 provider_error.get_or_insert(error);
812 }
813 Err(_) => {}
814 }
815 }
816
817 match provider_error {
819 Some(error) if !tried_invalid => Err(error),
820 _ => Err(detached_signature_verification()),
821 }
822}
823
824#[cfg(not(any(
825 feature = "crypto-rustcrypto",
826 feature = "crypto-aws-lc",
827 feature = "crypto-fips"
828)))]
829fn verify_detached(
830 _binding: Binding,
831 _parser_type: ParserType,
832 _request: &HttpRequest,
833 _opts: &FlowOptions<'_>,
834 _xml: &str,
835) -> Result<String, SamlError> {
836 Err(SamlError::Unsupported(
837 "signature verification requires a crypto provider feature".into(),
838 ))
839}
840
841fn validate_message_destination(
842 parser_type: ParserType,
843 extracted: &Value,
844 expected_recipient: Option<&str>,
845 message_authenticated: bool,
846) -> Result<(), SamlError> {
847 let Some(expected) = expected_recipient else {
848 return Ok(());
849 };
850 let destination = match parser_type {
851 ParserType::SamlResponse | ParserType::LogoutResponse => {
852 extracted.get_str("response.destination")
853 }
854 ParserType::LogoutRequest => extracted.get_str("request.destination"),
855 ParserType::SamlRequest => return Ok(()),
856 };
857 if message_authenticated && destination.is_none() {
860 return Err(SamlError::destination_mismatch(expected, None));
861 }
862 if destination.is_some_and(|destination| destination != expected) {
865 return Err(SamlError::destination_mismatch(expected, destination));
866 }
867 Ok(())
868}
869
870fn validate_context(
871 parser_type: ParserType,
872 saml_content: &str,
873 extracted: &Value,
874 opts: &FlowOptions<'_>,
875 expected_recipient: Option<&str>,
876 message_authenticated: bool,
877) -> Result<(), SamlError> {
878 let should_validate_issuer = matches!(
879 parser_type,
880 ParserType::SamlRequest
881 | ParserType::SamlResponse
882 | ParserType::LogoutRequest
883 | ParserType::LogoutResponse
884 );
885 if should_validate_issuer {
886 if let Some(expected) = opts.from_issuer {
887 let actual = extracted.get_str("issuer");
888 if actual != Some(expected) {
889 return Err(SamlError::issuer_mismatch(expected, actual));
890 }
891 }
892 }
893 let is_response = matches!(
894 parser_type,
895 ParserType::SamlResponse | ParserType::LogoutResponse
896 );
897 if is_response {
898 if let Some(expected) = opts.expected_in_response_to {
899 let actual = extracted.get_str("response.inResponseTo");
900 if actual != Some(expected) {
901 return Err(SamlError::in_response_to_mismatch(Some(expected), actual));
902 }
903 }
904 }
905 validate_message_destination(
906 parser_type,
907 extracted,
908 expected_recipient,
909 message_authenticated,
910 )?;
911 if parser_type == ParserType::SamlResponse {
912 crate::assertion_acceptance::accept_response_assertions(
913 saml_content,
914 opts,
915 expected_recipient,
916 )?;
917 }
918 if parser_type == ParserType::LogoutRequest {
919 logout_request_not_on_or_after_deadline(
920 extracted,
921 opts.validation_now()?,
922 opts.clock_drifts.1,
923 )?;
924 }
925 Ok(())
926}
927
928fn signature_is_present(
929 binding: Binding,
930 request: &HttpRequest,
931 xml: &str,
932 limits: XmlLimits,
933) -> Result<bool, SamlError> {
934 let detached = match binding {
935 Binding::Redirect => request.query.iter().any(|(name, _)| name == "Signature"),
936 Binding::SimpleSign => request.body.iter().any(|(name, _)| name == "Signature"),
937 Binding::Post | Binding::Artifact => false,
938 };
939 if detached {
940 return Ok(true);
941 }
942 let document = crate::xml::dom::parse_with_limits(xml, limits)?;
943 Ok(protocol_signature_candidate(&document.root))
944}
945
946fn protocol_signature_candidate(root: &crate::xml::dom::Node) -> bool {
949 root.children
950 .iter()
951 .any(|child| child.local_name == "Signature")
952 || root.children.iter().any(|child| {
953 child.local_name == "Assertion"
954 && child
955 .children
956 .iter()
957 .any(|nested| nested.local_name == "Signature")
958 })
959}
960
961fn flow_inner(
962 opts: &FlowOptions<'_>,
963 request: &HttpRequest,
964 expected_recipient: Option<&str>,
965 assertion_signature: AssertionSignatureRequirement,
966 response_signature: ResponseSignatureRequirement,
967) -> Result<FlowResultWithSignatureEvidence, SamlError> {
968 let binding = opts
969 .binding
970 .ok_or_else(|| missing_binding_parameter("binding"))?;
971 let parser_type = opts
972 .parser_type
973 .ok_or_else(|| SamlError::Invalid("ERR_UNDEFINED_PARSERTYPE".into()))?;
974
975 let xml = decode_message(
976 binding,
977 parser_type,
978 request,
979 opts.redirect_inflate_max_bytes,
980 opts.xml_limits,
981 )?;
982 is_valid_xml_with_limits(&xml, opts.xml_limits)?;
983 validate_protocol_profile(&xml, parser_type, opts.xml_limits)?;
984 check_status_with_limits(&xml, parser_type, opts.xml_limits)?;
985
986 let verify_signature = opts.check_signature
987 || (opts.verify_signature_if_present
988 && signature_is_present(binding, request, &xml, opts.xml_limits)?);
989 let (saml_content, assertion, sig_alg, message_authenticated, verified_xml_signatures) =
990 if verify_signature {
991 match binding {
992 Binding::Redirect | Binding::SimpleSign => {
993 let sig_alg = verify_detached(binding, parser_type, request, opts, &xml)?;
994 let prepared = if parser_type == ParserType::SamlResponse
995 && assertion_signature == AssertionSignatureRequirement::Direct
996 {
997 verify_and_prepare(
998 &xml,
999 parser_type,
1000 opts,
1001 assertion_signature,
1002 ResponseSignatureRequirement::Optional,
1003 )?
1004 } else {
1005 let assertion = if parser_type == ParserType::SamlResponse {
1006 assertion_shortcut(&xml, opts.xml_limits)?
1007 } else {
1008 None
1009 };
1010 PreparedMessage {
1011 saml_content: xml,
1012 assertion,
1013 message_authenticated: false,
1014 verified_xml_signatures: Vec::new(),
1015 }
1016 };
1017 (
1018 prepared.saml_content,
1019 prepared.assertion,
1020 Some(sig_alg),
1021 true,
1022 prepared.verified_xml_signatures,
1023 )
1024 }
1025 _ => {
1026 let prepared = verify_and_prepare(
1027 &xml,
1028 parser_type,
1029 opts,
1030 assertion_signature,
1031 response_signature,
1032 )?;
1033 (
1034 prepared.saml_content,
1035 prepared.assertion,
1036 None,
1037 prepared.message_authenticated,
1038 prepared.verified_xml_signatures,
1039 )
1040 }
1041 }
1042 } else {
1043 let assertion = if parser_type == ParserType::SamlResponse {
1044 assertion_shortcut(&xml, opts.xml_limits)?
1045 } else {
1046 None
1047 };
1048 (xml, assertion, None, false, Vec::new())
1049 };
1050
1051 let fields = default_fields(parser_type, assertion.as_deref())?;
1052 let extracted = extract_with_limits(&saml_content, &fields, opts.xml_limits)?;
1053 validate_context(
1054 parser_type,
1055 &saml_content,
1056 &extracted,
1057 opts,
1058 expected_recipient,
1059 message_authenticated,
1060 )?;
1061
1062 Ok(FlowResultWithSignatureEvidence {
1063 flow_result: FlowResult {
1064 saml_content,
1065 extract: extracted,
1066 sig_alg,
1067 },
1068 verified_xml_signatures,
1069 message_authenticated,
1070 })
1071}
1072
1073pub(crate) fn flow_with_authentication(
1075 opts: &FlowOptions<'_>,
1076 request: &HttpRequest,
1077) -> Result<(FlowResult, bool), SamlError> {
1078 let result = flow_inner(
1079 opts,
1080 request,
1081 None,
1082 AssertionSignatureRequirement::Compatible,
1083 ResponseSignatureRequirement::Optional,
1084 )?;
1085 let message_authenticated = result.message_authenticated();
1086 Ok((result.into_flow_result(), message_authenticated))
1087}
1088
1089pub fn flow(opts: &FlowOptions<'_>, request: &HttpRequest) -> Result<FlowResult, SamlError> {
1090 Ok(flow_inner(
1091 opts,
1092 request,
1093 None,
1094 AssertionSignatureRequirement::Compatible,
1095 ResponseSignatureRequirement::Optional,
1096 )?
1097 .into_flow_result())
1098}
1099
1100pub(crate) fn flow_with_expected_recipient_and_signature_evidence(
1101 opts: &FlowOptions<'_>,
1102 request: &HttpRequest,
1103 expected_recipient: &str,
1104 assertion_signature: AssertionSignatureRequirement,
1105 response_signature: ResponseSignatureRequirement,
1106) -> Result<FlowResultWithSignatureEvidence, SamlError> {
1107 flow_inner(
1108 opts,
1109 request,
1110 Some(expected_recipient),
1111 assertion_signature,
1112 response_signature,
1113 )
1114}
1115
1116#[cfg(all(
1117 test,
1118 any(
1119 feature = "crypto-rustcrypto",
1120 feature = "crypto-aws-lc",
1121 feature = "crypto-fips"
1122 )
1123))]
1124mod tests {
1125 use super::*;
1126 use crate::constants::signature_algorithm::RSA_SHA256;
1127
1128 #[test]
1129 fn detached_verification_preserves_provider_error() {
1130 let certificates = vec!["not a certificate".to_string()];
1131 let options = FlowOptions {
1132 signing_certs: &certificates,
1133 ..Default::default()
1134 };
1135 let octet = url::form_urlencoded::Serializer::new(String::new())
1136 .append_pair("SAMLRequest", "payload")
1137 .append_pair("SigAlg", RSA_SHA256)
1138 .finish();
1139 let request = HttpRequest {
1140 query: vec![
1141 ("SAMLRequest".into(), "payload".into()),
1142 ("SigAlg".into(), RSA_SHA256.into()),
1143 ("Signature".into(), "AA==".into()),
1144 ],
1145 octet_string: Some(octet),
1146 ..Default::default()
1147 };
1148
1149 assert!(matches!(
1150 verify_detached(
1151 Binding::Redirect,
1152 ParserType::SamlRequest,
1153 &request,
1154 &options,
1155 "<samlp:AuthnRequest/>",
1156 ),
1157 Err(SamlError::Crypto(_))
1158 ));
1159 }
1160
1161 #[test]
1162 fn detached_rolling_cert_unloadable_peer_keeps_invalid_verdict(
1163 ) -> Result<(), Box<dyn std::error::Error>> {
1164 const SP_PRIVKEY: &str = include_str!("../tests/fixtures/key/sp_privkey.pem");
1165 const SP_CERT: &str = include_str!("../tests/fixtures/key/sp_signing_cert.cer");
1166
1167 let key = crate::crypto::keys::load_private_key(SP_PRIVKEY, None)?;
1168 let signature =
1169 crate::crypto::construct_message_signature("SAMLRequest=other", &key, RSA_SHA256)?;
1170 let octet = url::form_urlencoded::Serializer::new(String::new())
1171 .append_pair("SAMLRequest", "payload")
1172 .append_pair("SigAlg", RSA_SHA256)
1173 .finish();
1174 let request = HttpRequest {
1175 query: vec![
1176 ("SAMLRequest".into(), "payload".into()),
1177 ("SigAlg".into(), RSA_SHA256.into()),
1178 ("Signature".into(), signature),
1179 ],
1180 octet_string: Some(octet),
1181 ..Default::default()
1182 };
1183 let garbage = "not a certificate".to_string();
1184 let signer = SP_CERT.to_string();
1185
1186 for certificates in [vec![garbage.clone(), signer.clone()], vec![signer, garbage]] {
1187 let options = FlowOptions {
1188 signing_certs: &certificates,
1189 ..Default::default()
1190 };
1191 assert!(
1192 matches!(
1193 verify_detached(
1194 Binding::Redirect,
1195 ParserType::SamlRequest,
1196 &request,
1197 &options,
1198 "<samlp:AuthnRequest/>",
1199 ),
1200 Err(SamlError::SignatureVerification {
1201 reason: SignatureVerificationReason::DetachedMessageSignature,
1202 })
1203 ),
1204 "unloadable leftover must not replace detached-invalid with Crypto"
1205 );
1206 }
1207 Ok(())
1208 }
1209}