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