Skip to main content

saml_rs/
flow.rs

1//! Inbound message flow: decode, validate XML/status, verify signatures,
2//! optionally decrypt, extract fields, and validate issuer/time constraints.
3
4use 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/// Decoded HTTP request inputs for a binding.
37#[derive(Debug, Default, Clone)]
38pub struct HttpRequest {
39    /// URL-decoded query parameters (HTTP-Redirect).
40    pub query: Vec<(String, String)>,
41    /// Form body parameters (HTTP-POST / SimpleSign).
42    pub body: Vec<(String, String)>,
43    /// Signed octet string for detached-signature verification.
44    pub octet_string: Option<String>,
45}
46
47impl HttpRequest {
48    /// HTTP-Redirect request from query pairs.
49    pub fn redirect(query: Vec<(String, String)>) -> Self {
50        Self {
51            query,
52            ..Default::default()
53        }
54    }
55
56    /// HTTP-POST/SimpleSign request from body pairs.
57    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/// Inputs controlling a flow run.
97#[non_exhaustive]
98#[derive(Debug, Clone)]
99pub struct FlowOptions<'a> {
100    /// Protocol binding.
101    pub binding: Option<Binding>,
102    /// Message parser type.
103    pub parser_type: Option<ParserType>,
104    /// Maximum decoded compressed and inflated raw-DEFLATE bytes accepted for
105    /// HTTP-Redirect input.
106    pub redirect_inflate_max_bytes: usize,
107    /// XML parser resource limits for decoded messages and DOM reparses.
108    pub xml_limits: XmlLimits,
109    /// Whether to require and verify a signature.
110    pub check_signature: bool,
111    /// Expected issuer (peer `entityID`).
112    pub from_issuer: Option<&'a str>,
113    /// Peer signing certificate(s) for verification.
114    pub signing_certs: &'a [String],
115    /// Our decryption private key PEM (when assertions are encrypted).
116    pub decrypt_key: Option<&'a str>,
117    /// Passphrase for `decrypt_key`.
118    pub decrypt_key_pass: Option<&'a str>,
119    /// Allow XML-Enc RSA key-transport decryption with the bundled RustCrypto
120    /// software RSA backend.
121    ///
122    /// Enforced only for `crypto-rustcrypto`, where that path reaches
123    /// `RUSTSEC-2023-0071`-affected `rsa` code when an attacker can observe
124    /// timing. AWS-LC and FIPS ignore this flag.
125    pub allow_insecure_software_rsa_key_transport_decryption: bool,
126    /// Clock drift tolerance `(not_before_ms, not_on_or_after_ms)`.
127    pub clock_drifts: (i64, i64),
128    /// Validation instant. `None` keeps raw compatibility behavior by reading
129    /// the process clock during validation.
130    pub now: Option<SystemTime>,
131    /// Expected `<Audience>` (this SP's entity ID); `None` skips the check.
132    pub expected_audience: Option<&'a str>,
133    /// Expected `InResponseTo` (originating request ID); `None` skips the check.
134    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/// Result of a successful flow.
201#[derive(Debug, Clone)]
202pub struct FlowResult {
203    /// The decoded (and, when verified, authenticated) SAML XML.
204    pub saml_content: String,
205    /// Extracted fields.
206    pub extract: Value,
207    /// Verified signature algorithm, if a signature was checked.
208    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        // Older saml-rs outbound SimpleSign signed an empty RelayState field
388        // even when the form body omitted RelayState; keep accepting it for
389        // compatibility.
390        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/// Verify and optionally decrypt the message, returning the authenticated
543/// `(saml_content, assertion)`. Requires a crypto provider.
544#[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            // signed-then-encrypted: the verified content is a Response carrying
586            // an EncryptedAssertion.
587            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        // encrypted-then-signed: decrypt first, then verify the result.
616        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/// Verify a detached (redirect/SimpleSign) message signature, returning the
692/// verified `SigAlg`. Requires a crypto provider.
693#[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    // A leftover unloadable cert must not poison a rolling-cert verdict.
741    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
1103/// Run the inbound flow described by `opts` against `request`.
1104pub 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}