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;
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/// Decoded HTTP request inputs for a binding.
31#[derive(Debug, Default, Clone)]
32pub struct HttpRequest {
33    /// URL-decoded query parameters (HTTP-Redirect).
34    pub query: Vec<(String, String)>,
35    /// Form body parameters (HTTP-POST / SimpleSign).
36    pub body: Vec<(String, String)>,
37    /// Signed octet string for detached-signature verification.
38    pub octet_string: Option<String>,
39}
40
41impl HttpRequest {
42    /// HTTP-Redirect request from query pairs.
43    pub fn redirect(query: Vec<(String, String)>) -> Self {
44        Self {
45            query,
46            ..Default::default()
47        }
48    }
49
50    /// HTTP-POST/SimpleSign request from body pairs.
51    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/// Inputs controlling a flow run.
91#[non_exhaustive]
92#[derive(Debug, Clone)]
93pub struct FlowOptions<'a> {
94    /// Protocol binding.
95    pub binding: Option<Binding>,
96    /// Message parser type.
97    pub parser_type: Option<ParserType>,
98    /// Maximum decoded compressed and inflated raw-DEFLATE bytes accepted for
99    /// HTTP-Redirect input.
100    pub redirect_inflate_max_bytes: usize,
101    /// XML parser resource limits for decoded messages and DOM reparses.
102    pub xml_limits: XmlLimits,
103    /// Whether to require and verify a signature.
104    pub check_signature: bool,
105    /// Verify a signature when one is present, without requiring one.
106    ///
107    /// Ignored when `check_signature` is set.
108    pub verify_signature_if_present: bool,
109    /// Reject a bearer assertion that omits `<AudienceRestriction>`.
110    ///
111    /// Applies only when `expected_audience` is set. When false, a present
112    /// restriction is still evaluated.
113    pub require_audience_restriction: bool,
114    /// Whether embedded SAML signatures must satisfy the strict RSA-SHA2 profile.
115    pub strict_xml_signature_profile: bool,
116    /// Expected issuer (peer `entityID`).
117    pub from_issuer: Option<&'a str>,
118    /// Peer signing certificate(s) for verification.
119    pub signing_certs: &'a [String],
120    /// Our decryption private key PEM (when assertions are encrypted).
121    pub decrypt_key: Option<&'a str>,
122    /// Passphrase for `decrypt_key`.
123    pub decrypt_key_pass: Option<&'a str>,
124    /// Allow XML-Enc RSA key-transport decryption with the bundled RustCrypto
125    /// software RSA backend.
126    ///
127    /// Enforced only for `crypto-rustcrypto`, where that path reaches
128    /// `RUSTSEC-2023-0071`-affected `rsa` code when an attacker can observe
129    /// timing. AWS-LC and FIPS ignore this flag.
130    pub allow_insecure_software_rsa_key_transport_decryption: bool,
131    /// Clock drift tolerance `(not_before_ms, not_on_or_after_ms)`.
132    pub clock_drifts: (i64, i64),
133    /// Validation instant. `None` keeps raw compatibility behavior by reading
134    /// the process clock during validation.
135    pub now: Option<SystemTime>,
136    /// Expected `<Audience>` (this SP's entity ID); `None` skips the check.
137    pub expected_audience: Option<&'a str>,
138    /// Expected `InResponseTo` (originating request ID); `None` skips the check.
139    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/// Result of a successful low-level flow.
232///
233/// This records only validation supported by the supplied flow context. The
234/// public raw flow has no actual receiving endpoint, so success does not imply
235/// that a message `Destination` was compared with that endpoint. Typed
236/// receivers supply the additional context before exposing this result.
237#[derive(Debug, Clone)]
238pub struct FlowResult {
239    /// The decoded (and, when verified, authenticated) SAML XML.
240    pub saml_content: String,
241    /// Extracted fields.
242    pub extract: Value,
243    /// Verified signature algorithm, if a signature was checked.
244    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        // Older saml-rs outbound SimpleSign signed an empty RelayState field
448        // even when the form body omitted RelayState; keep accepting it for
449        // compatibility.
450        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/// Verify and optionally decrypt the message, returning the authenticated
614/// `(saml_content, assertion)`. Requires a crypto provider.
615#[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            // signed-then-encrypted: the verified content is a Response carrying
658            // an EncryptedAssertion.
659            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        // encrypted-then-signed: decrypt first, then verify the result.
690        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/// Verify a detached (redirect/SimpleSign) message signature, returning the
769/// verified `SigAlg`. Requires a crypto provider.
770#[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    // A leftover unloadable cert must not poison a rolling-cert verdict.
818    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    // SAML Bindings 2.0 §§3.4.5.2 and 3.5.5.2, and SimpleSign §2.4,
858    // require Destination on a signed message received through these bindings.
859    if message_authenticated && destination.is_none() {
860        return Err(SamlError::destination_mismatch(expected, None));
861    }
862    // SAML Core 2.0 §§3.2.1 and 3.2.2 require the actual recipient to discard
863    // any protocol message whose present Destination does not match itself.
864    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
946/// Same placement as embedded-signature verification: a `Signature` child of
947/// the protocol root, or of an `Assertion` child of that root.
948fn 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
1073/// Run the inbound flow described by `opts` against `request`.
1074pub(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}