use crate::binding::{
base64_decode_with_limit, deflate_raw_decode_with_limit, MAX_DEFLATE_RAW_DECODE_BYTES,
};
use crate::constants::{Binding, ParserType};
use crate::context::is_valid_xml_with_limits;
#[cfg(feature = "crypto-bergshamra")]
use crate::error::SignatureVerificationReason;
use crate::error::{SamlError, SubjectConfirmationReason, TimeWindowField};
#[cfg(feature = "crypto-bergshamra")]
use crate::model::RelayStateParam;
use crate::util::Value;
use crate::validator::{check_status_with_limits, verify_time_at};
use crate::xml::{extract_with_limits, fields, ExtractorField, XmlLimits};
use time::OffsetDateTime;
const BEARER_SUBJECT_CONFIRMATION_METHOD: &str = "urn:oasis:names:tc:SAML:2.0:cm:bearer";
#[derive(Debug, Default, Clone)]
pub struct HttpRequest {
pub query: Vec<(String, String)>,
pub body: Vec<(String, String)>,
pub octet_string: Option<String>,
}
impl HttpRequest {
pub fn redirect(query: Vec<(String, String)>) -> Self {
Self {
query,
..Default::default()
}
}
pub fn post(body: Vec<(String, String)>) -> Self {
Self {
body,
..Default::default()
}
}
fn query_get(&self, key: &str) -> Result<Option<&str>, SamlError> {
single_param(&self.query, key)
}
fn body_get(&self, key: &str) -> Result<Option<&str>, SamlError> {
single_param(&self.body, key)
}
}
fn single_param<'a>(
params: &'a [(String, String)],
key: &str,
) -> Result<Option<&'a str>, SamlError> {
let mut values = params
.iter()
.filter(|(candidate, _)| candidate == key)
.map(|(_, value)| value.as_str());
let first = values.next();
if values.next().is_some() {
return Err(SamlError::Invalid("ERR_AMBIGUOUS_FLOW_INPUT".into()));
}
Ok(first)
}
fn missing_binding_parameter(name: &'static str) -> SamlError {
SamlError::MissingBindingParameter { name }
}
fn unsupported_binding(binding: Binding) -> SamlError {
SamlError::UnsupportedBinding { binding }
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct FlowOptions<'a> {
pub binding: Option<Binding>,
pub parser_type: Option<ParserType>,
pub redirect_inflate_max_bytes: usize,
pub xml_limits: XmlLimits,
pub check_signature: bool,
pub from_issuer: Option<&'a str>,
pub signing_certs: &'a [String],
pub decrypt_key: Option<&'a str>,
pub decrypt_key_pass: Option<&'a str>,
pub allow_insecure_software_rsa_key_transport_decryption: bool,
pub clock_drifts: (i64, i64),
pub now: Option<OffsetDateTime>,
pub expected_audience: Option<&'a str>,
pub expected_in_response_to: Option<&'a str>,
}
impl<'a> Default for FlowOptions<'a> {
fn default() -> Self {
Self {
binding: None,
parser_type: None,
redirect_inflate_max_bytes: MAX_DEFLATE_RAW_DECODE_BYTES,
xml_limits: XmlLimits::default(),
check_signature: false,
from_issuer: None,
signing_certs: &[],
decrypt_key: None,
decrypt_key_pass: None,
allow_insecure_software_rsa_key_transport_decryption: false,
clock_drifts: (0, 0),
now: None,
expected_audience: None,
expected_in_response_to: None,
}
}
}
impl FlowOptions<'_> {
fn validation_now(&self) -> OffsetDateTime {
self.now.unwrap_or_else(OffsetDateTime::now_utc)
}
}
#[derive(Debug, Clone)]
pub struct FlowResult {
pub saml_content: String,
pub extract: Value,
pub sig_alg: Option<String>,
}
fn default_fields(
parser_type: ParserType,
assertion: Option<&str>,
) -> Result<Vec<ExtractorField>, SamlError> {
Ok(match parser_type {
ParserType::SamlRequest => fields::login_request_fields(),
ParserType::SamlResponse => {
let assertion =
assertion.ok_or_else(|| SamlError::Xml("ERR_EMPTY_ASSERTION".into()))?;
fields::login_response_fields(assertion)
}
ParserType::LogoutRequest => fields::logout_request_fields(),
ParserType::LogoutResponse => fields::logout_response_fields(),
})
}
fn decode_message(
binding: Binding,
parser_type: ParserType,
request: &HttpRequest,
redirect_inflate_max_bytes: usize,
xml_limits: XmlLimits,
) -> Result<String, SamlError> {
let direction = parser_type.query_param();
let bytes = match binding {
Binding::Redirect => {
let content = request
.query_get(direction)?
.ok_or_else(|| missing_binding_parameter(direction))?;
let redirect_max_bytes = redirect_inflate_max_bytes.min(xml_limits.max_bytes);
let compressed = base64_decode_with_limit(content, redirect_max_bytes)?;
deflate_raw_decode_with_limit(&compressed, redirect_max_bytes)?
}
Binding::Post | Binding::SimpleSign => {
let content = request
.body_get(direction)?
.ok_or_else(|| missing_binding_parameter(direction))?;
base64_decode_with_limit(content, xml_limits.max_bytes)?
}
Binding::Artifact => return Err(unsupported_binding(binding)),
};
xml_limits.check_input_bytes(bytes.len())?;
String::from_utf8(bytes).map_err(|e| SamlError::Xml(e.to_string()))
}
fn assertion_shortcut(xml: &str, limits: XmlLimits) -> Result<Option<String>, SamlError> {
let field = ExtractorField::new("assertion", &["Response", "Assertion"]).with_context();
Ok(
extract_with_limits(xml, std::slice::from_ref(&field), limits)?
.get_str("assertion")
.map(str::to_string),
)
}
#[cfg(feature = "crypto-bergshamra")]
fn verified_content_not_covered() -> SamlError {
SamlError::SignedReferenceMismatch
}
#[cfg(feature = "crypto-bergshamra")]
fn decoded_octet_params(octet: &str) -> Vec<(String, String)> {
url::form_urlencoded::parse(octet.as_bytes())
.map(|(key, value)| (key.into_owned(), value.into_owned()))
.collect()
}
#[cfg(feature = "crypto-bergshamra")]
fn detached_signature_verification() -> SamlError {
SamlError::SignatureVerification {
reason: SignatureVerificationReason::DetachedMessageSignature,
}
}
#[cfg(feature = "crypto-bergshamra")]
fn relay_state_param(value: Option<&str>) -> Option<RelayStateParam> {
RelayStateParam::try_from_option(value.map(str::to_string)).ok()
}
#[cfg(feature = "crypto-bergshamra")]
fn detached_relay_state_mismatch(expected: Option<&str>, actual: Option<&str>) -> SamlError {
match (relay_state_param(expected), relay_state_param(actual)) {
(Some(expected), Some(actual)) => SamlError::RelayStateMismatch { expected, actual },
_ => SamlError::SignatureVerification {
reason: SignatureVerificationReason::RelayStateCorrelation,
},
}
}
#[cfg(feature = "crypto-bergshamra")]
fn ensure_redirect_octet_matches_consumed_fields(
parser_type: ParserType,
request: &HttpRequest,
sig_alg: &str,
octet: &str,
) -> Result<(), SamlError> {
let direction = parser_type.query_param();
let signed = decoded_octet_params(octet);
if single_param(&signed, "Signature")?.is_some() {
return Err(detached_signature_verification());
}
let signed_message =
single_param(&signed, direction)?.ok_or_else(|| missing_binding_parameter(direction))?;
let consumed_message = request
.query_get(direction)?
.ok_or_else(|| missing_binding_parameter(direction))?;
if signed_message != consumed_message {
return Err(detached_signature_verification());
}
let signed_sig_alg =
single_param(&signed, "SigAlg")?.ok_or_else(|| missing_binding_parameter("SigAlg"))?;
if signed_sig_alg != sig_alg {
return Err(detached_signature_verification());
}
let signed_relay_state = single_param(&signed, "RelayState")?;
let consumed_relay_state = request.query_get("RelayState")?;
if signed_relay_state != consumed_relay_state {
return Err(detached_relay_state_mismatch(
signed_relay_state,
consumed_relay_state,
));
}
Ok(())
}
#[cfg(feature = "crypto-bergshamra")]
fn ensure_simplesign_octet_matches_consumed_fields(
parser_type: ParserType,
request: &HttpRequest,
xml: &str,
sig_alg: &str,
octet: &str,
) -> Result<(), SamlError> {
let direction = parser_type.query_param();
request
.body_get(direction)?
.ok_or_else(|| missing_binding_parameter(direction))?;
let message_and_sig_alg = format!("{direction}={xml}&SigAlg={sig_alg}");
let message_empty_relay_and_sig_alg = format!("{direction}={xml}&RelayState=&SigAlg={sig_alg}");
let matches = match request.body_get("RelayState")? {
Some(relay_state) => {
let expected = format!("{direction}={xml}&RelayState={relay_state}&SigAlg={sig_alg}");
octet == expected
}
None => octet == message_and_sig_alg || octet == message_empty_relay_and_sig_alg,
};
if matches {
Ok(())
} else {
Err(detached_signature_verification())
}
}
#[cfg(feature = "crypto-bergshamra")]
fn ensure_detached_octet_matches_consumed_fields(
binding: Binding,
parser_type: ParserType,
request: &HttpRequest,
xml: &str,
sig_alg: &str,
octet: &str,
) -> Result<(), SamlError> {
match binding {
Binding::Redirect => {
ensure_redirect_octet_matches_consumed_fields(parser_type, request, sig_alg, octet)
}
Binding::SimpleSign => ensure_simplesign_octet_matches_consumed_fields(
parser_type,
request,
xml,
sig_alg,
octet,
),
Binding::Post | Binding::Artifact => Ok(()),
}
}
#[cfg(feature = "crypto-bergshamra")]
fn required_xml_signature_failed(signature_present: bool) -> SamlError {
if signature_present {
SamlError::SignatureVerification {
reason: SignatureVerificationReason::XmlSignature,
}
} else {
SamlError::SignatureMissing
}
}
#[cfg(feature = "crypto-bergshamra")]
fn verify_and_prepare(
xml: &str,
parser_type: ParserType,
opts: &FlowOptions<'_>,
) -> Result<(String, Option<String>), SamlError> {
use crate::crypto::{
decrypt_assertion_with_limits,
enc::{software_rsa_decryption_disabled, AssertionDecryptionOptions},
keys::load_private_key,
verify::has_xml_signature_with_limits,
verify_signature_with_limits,
};
let signature_present = has_xml_signature_with_limits(xml, opts.xml_limits)?;
let (verified, verified_node) =
verify_signature_with_limits(xml, opts.signing_certs, opts.xml_limits)?;
let decrypt_required = opts.decrypt_key.is_some();
if decrypt_required && !opts.allow_insecure_software_rsa_key_transport_decryption {
return Err(software_rsa_decryption_disabled());
}
let decrypt_options = AssertionDecryptionOptions {
allow_insecure_software_rsa_key_transport_decryption: opts
.allow_insecure_software_rsa_key_transport_decryption,
};
let load_key = || load_private_key(opts.decrypt_key.unwrap_or_default(), opts.decrypt_key_pass);
if decrypt_required && verified && parser_type == ParserType::SamlResponse {
if let Some(node) = verified_node {
let (content, assertion) = decrypt_assertion_with_limits(
&node,
&load_key()?,
decrypt_options,
opts.xml_limits,
)?;
return Ok((content, Some(assertion)));
}
}
if decrypt_required && !verified {
let (content, _) =
decrypt_assertion_with_limits(xml, &load_key()?, decrypt_options, opts.xml_limits)?;
let signature_present = has_xml_signature_with_limits(&content, opts.xml_limits)?;
let (re_verified, re_node) =
verify_signature_with_limits(&content, opts.signing_certs, opts.xml_limits)?;
return if re_verified {
Ok((content, re_node))
} else {
Err(required_xml_signature_failed(signature_present))
};
}
if verified {
if matches!(
parser_type,
ParserType::SamlRequest | ParserType::LogoutRequest | ParserType::LogoutResponse
) {
let content = verified_node.ok_or_else(verified_content_not_covered)?;
return Ok((content, None));
}
return Ok((xml.to_string(), verified_node));
}
Err(required_xml_signature_failed(signature_present))
}
#[cfg(not(feature = "crypto-bergshamra"))]
fn verify_and_prepare(
_xml: &str,
_parser_type: ParserType,
_opts: &FlowOptions<'_>,
) -> Result<(String, Option<String>), SamlError> {
Err(SamlError::Unsupported(
"signature verification requires feature crypto-bergshamra".into(),
))
}
#[cfg(feature = "crypto-bergshamra")]
fn verify_detached(
binding: Binding,
parser_type: ParserType,
request: &HttpRequest,
opts: &FlowOptions<'_>,
xml: &str,
) -> Result<String, SamlError> {
let get = |k: &str| -> Result<Option<&str>, SamlError> {
match binding {
Binding::Redirect => request.query_get(k),
_ => request.body_get(k),
}
};
let signature = get("Signature")?.ok_or(SamlError::SignatureMissing)?;
let sig_alg = get("SigAlg")?.ok_or_else(|| missing_binding_parameter("SigAlg"))?;
let octet = request
.octet_string
.as_deref()
.ok_or_else(|| missing_binding_parameter("octet_string"))?;
ensure_detached_octet_matches_consumed_fields(
binding,
parser_type,
request,
xml,
sig_alg,
octet,
)?;
let verified = opts.signing_certs.iter().any(|cert| {
crate::crypto::verify_message_signature(octet, signature, cert, sig_alg).unwrap_or(false)
});
if verified {
Ok(sig_alg.to_string())
} else {
Err(detached_signature_verification())
}
}
#[cfg(not(feature = "crypto-bergshamra"))]
fn verify_detached(
_binding: Binding,
_parser_type: ParserType,
_request: &HttpRequest,
_opts: &FlowOptions<'_>,
_xml: &str,
) -> Result<String, SamlError> {
Err(SamlError::Unsupported(
"signature verification requires feature crypto-bergshamra".into(),
))
}
fn audience_contains(extracted: &Value, expected: &str) -> bool {
match extracted.get("audience") {
Some(Value::Str(s)) => s == expected,
Some(Value::Array(items)) => items.iter().any(|v| v.as_str() == Some(expected)),
_ => false,
}
}
fn subject_confirmation_xmls(extracted: &Value) -> Vec<&str> {
match extracted.get("subjectConfirmation") {
Some(Value::Str(xml)) => vec![xml.as_str()],
Some(Value::Array(items)) => items.iter().filter_map(Value::as_str).collect(),
_ => Vec::new(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SubjectConfirmationCheck {
Valid,
Invalid(SubjectConfirmationReason),
}
fn check_bearer_subject_confirmation(
xml: &str,
opts: &FlowOptions<'_>,
expected_recipient: Option<&str>,
) -> Result<SubjectConfirmationCheck, SamlError> {
let fields = [
ExtractorField::new("subjectConfirmation", &["SubjectConfirmation"]).attrs(&["Method"]),
ExtractorField::new(
"subjectConfirmationData",
&["SubjectConfirmation", "SubjectConfirmationData"],
)
.attrs(&["NotOnOrAfter", "Recipient", "InResponseTo"]),
];
let extracted = extract_with_limits(xml, &fields, opts.xml_limits)?;
if extracted.get_str("subjectConfirmation") != Some(BEARER_SUBJECT_CONFIRMATION_METHOD) {
return Ok(SubjectConfirmationCheck::Invalid(
SubjectConfirmationReason::InvalidMethod,
));
}
let Some(not_on_or_after) = extracted.get_str("subjectConfirmationData.notOnOrAfter") else {
return Ok(SubjectConfirmationCheck::Invalid(
SubjectConfirmationReason::MissingNotOnOrAfter,
));
};
if !verify_time_at(
None,
Some(not_on_or_after),
opts.clock_drifts,
opts.validation_now(),
) {
return Ok(SubjectConfirmationCheck::Invalid(
SubjectConfirmationReason::TimeWindowInvalid,
));
}
if let Some(expected) = expected_recipient {
if extracted.get_str("subjectConfirmationData.recipient") != Some(expected) {
return Ok(SubjectConfirmationCheck::Invalid(
SubjectConfirmationReason::RecipientMismatch,
));
}
}
if let Some(expected) = opts.expected_in_response_to {
if extracted.get_str("subjectConfirmationData.inResponseTo") != Some(expected) {
return Ok(SubjectConfirmationCheck::Invalid(
SubjectConfirmationReason::InResponseToMismatch,
));
}
}
Ok(SubjectConfirmationCheck::Valid)
}
fn validate_subject_confirmation(
extracted: &Value,
opts: &FlowOptions<'_>,
expected_recipient: Option<&str>,
) -> Result<(), SamlError> {
let mut reason = None;
for xml in subject_confirmation_xmls(extracted) {
match check_bearer_subject_confirmation(xml, opts, expected_recipient)? {
SubjectConfirmationCheck::Valid => return Ok(()),
SubjectConfirmationCheck::Invalid(current) => reason = Some(current),
}
}
Err(SamlError::SubjectConfirmationInvalid {
reason: reason.unwrap_or(SubjectConfirmationReason::MissingBearerConfirmation),
})
}
fn validate_response_destination(
extracted: &Value,
expected_recipient: Option<&str>,
) -> Result<(), SamlError> {
let Some(expected) = expected_recipient else {
return Ok(());
};
if let Some(destination) = extracted.get_str("response.destination") {
if destination != expected {
return Err(SamlError::destination_mismatch(expected, Some(destination)));
}
}
Ok(())
}
fn validate_context(
parser_type: ParserType,
extracted: &Value,
opts: &FlowOptions<'_>,
expected_recipient: Option<&str>,
) -> Result<(), SamlError> {
let should_validate_issuer = matches!(
parser_type,
ParserType::SamlRequest
| ParserType::SamlResponse
| ParserType::LogoutRequest
| ParserType::LogoutResponse
);
if should_validate_issuer {
if let Some(expected) = opts.from_issuer {
let actual = extracted.get_str("issuer");
if actual != Some(expected) {
return Err(SamlError::issuer_mismatch(expected, actual));
}
}
}
let is_response = matches!(
parser_type,
ParserType::SamlResponse | ParserType::LogoutResponse
);
if is_response {
if let Some(expected) = opts.expected_in_response_to {
let actual = extracted.get_str("response.inResponseTo");
if actual != Some(expected) {
return Err(SamlError::in_response_to_mismatch(Some(expected), actual));
}
}
}
if parser_type == ParserType::SamlResponse {
validate_response_destination(extracted, expected_recipient)?;
validate_subject_confirmation(extracted, opts, expected_recipient)?;
if let Some(expected) = opts.expected_audience {
if !audience_contains(extracted, expected) {
return Err(SamlError::AudienceMismatch {
expected: expected.to_string(),
});
}
}
if let Some(session_not_on_or_after) = extracted.get_str("sessionIndex.sessionNotOnOrAfter")
{
if !verify_time_at(
None,
Some(session_not_on_or_after),
opts.clock_drifts,
opts.validation_now(),
) {
return Err(SamlError::TimeWindowInvalid {
field: TimeWindowField::SessionNotOnOrAfter,
});
}
}
if let Some(conditions) = extracted.get("conditions") {
let not_before = conditions.get_str("notBefore");
let not_on_or_after = conditions.get_str("notOnOrAfter");
if !verify_time_at(
not_before,
not_on_or_after,
opts.clock_drifts,
opts.validation_now(),
) {
return Err(SamlError::TimeWindowInvalid {
field: TimeWindowField::Conditions,
});
}
}
}
Ok(())
}
fn flow_inner(
opts: &FlowOptions<'_>,
request: &HttpRequest,
expected_recipient: Option<&str>,
) -> Result<FlowResult, SamlError> {
let binding = opts
.binding
.ok_or_else(|| missing_binding_parameter("binding"))?;
let parser_type = opts
.parser_type
.ok_or_else(|| SamlError::Invalid("ERR_UNDEFINED_PARSERTYPE".into()))?;
let xml = decode_message(
binding,
parser_type,
request,
opts.redirect_inflate_max_bytes,
opts.xml_limits,
)?;
is_valid_xml_with_limits(&xml, opts.xml_limits)?;
check_status_with_limits(&xml, parser_type, opts.xml_limits)?;
let (saml_content, assertion, sig_alg) = if opts.check_signature {
match binding {
Binding::Redirect | Binding::SimpleSign => {
let sig_alg = verify_detached(binding, parser_type, request, opts, &xml)?;
let assertion = if parser_type == ParserType::SamlResponse {
assertion_shortcut(&xml, opts.xml_limits)?
} else {
None
};
(xml, assertion, Some(sig_alg))
}
_ => {
let (content, assertion) = verify_and_prepare(&xml, parser_type, opts)?;
(content, assertion, None)
}
}
} else {
let assertion = if parser_type == ParserType::SamlResponse {
assertion_shortcut(&xml, opts.xml_limits)?
} else {
None
};
(xml, assertion, None)
};
let fields = default_fields(parser_type, assertion.as_deref())?;
let extracted = extract_with_limits(&saml_content, &fields, opts.xml_limits)?;
validate_context(parser_type, &extracted, opts, expected_recipient)?;
Ok(FlowResult {
saml_content,
extract: extracted,
sig_alg,
})
}
pub fn flow(opts: &FlowOptions<'_>, request: &HttpRequest) -> Result<FlowResult, SamlError> {
flow_inner(opts, request, None)
}
pub(crate) fn flow_with_expected_recipient(
opts: &FlowOptions<'_>,
request: &HttpRequest,
expected_recipient: &str,
) -> Result<FlowResult, SamlError> {
flow_inner(opts, request, Some(expected_recipient))
}