#![cfg(feature = "xmlsec")]
#[cfg(test)]
mod encrypted_assertion_tests {
use crate::crypto::ReduceMode;
use crate::metadata::EntityDescriptor;
use crate::schema::{Assertion, Response};
use crate::service_provider::{Error, ServiceProvider, ServiceProviderBuilder};
use crate::traits::ToXml;
use chrono::{DateTime, Duration, Utc};
use openssl::pkey::PKey;
fn encrypted_response_max_issue_delay(response_xml: &str) -> Duration {
let response: Response = response_xml.parse().unwrap();
Utc::now() - response.issue_instant + Duration::seconds(60)
}
fn required_clock_skew_for_not_before(not_before: DateTime<Utc>) -> Duration {
std::cmp::max(
not_before - Utc::now() + Duration::seconds(60),
Duration::zero(),
)
}
fn required_clock_skew_for_not_on_or_after(not_on_or_after: DateTime<Utc>) -> Duration {
std::cmp::max(
Utc::now() - not_on_or_after + Duration::seconds(60),
Duration::zero(),
)
}
fn required_validation_clock_skew(assertion: &Assertion) -> Duration {
let mut required = Duration::zero();
if let Some(conditions) = assertion.conditions.as_ref() {
if let Some(not_before) = conditions.not_before {
required = std::cmp::max(required, required_clock_skew_for_not_before(not_before));
}
if let Some(not_on_or_after) = conditions.not_on_or_after {
required = std::cmp::max(
required,
required_clock_skew_for_not_on_or_after(not_on_or_after),
);
}
}
if let Some(confirmations) = assertion
.subject
.as_ref()
.and_then(|subject| subject.subject_confirmations.as_ref())
{
for confirmation in confirmations {
if let Some(data) = confirmation.subject_confirmation_data.as_ref() {
if let Some(not_before) = data.not_before {
required =
std::cmp::max(required, required_clock_skew_for_not_before(not_before));
}
if let Some(not_on_or_after) = data.not_on_or_after {
required = std::cmp::max(
required,
required_clock_skew_for_not_on_or_after(not_on_or_after),
);
}
}
}
}
required
}
fn decrypted_encrypted_response_assertion(
response_xml: &str,
key: &PKey<openssl::pkey::Private>,
) -> Assertion {
let response: Response = response_xml.parse().unwrap();
response
.encrypted_assertion
.as_ref()
.unwrap()
.decrypt(key)
.unwrap()
}
fn encrypted_response_validation_clock_skew(
response_xml: &str,
key: &PKey<openssl::pkey::Private>,
) -> Duration {
let assertion = decrypted_encrypted_response_assertion(response_xml, key);
required_validation_clock_skew(&assertion)
}
fn encrypted_response_entity_id(
response_xml: &str,
key: &PKey<openssl::pkey::Private>,
) -> String {
decrypted_encrypted_response_assertion(response_xml, key)
.conditions
.as_ref()
.and_then(|conditions| conditions.audience_restrictions.as_ref())
.and_then(|restrictions| restrictions.iter().flat_map(|r| r.audience.iter()).next())
.cloned()
.unwrap_or_else(|| "example".to_string())
}
// Helper function to create a service provider with a private key
fn create_sp_with_private_key_for_response(
response_xml: &str,
key: PKey<openssl::pkey::Private>,
) -> ServiceProvider {
let max_clock_skew = encrypted_response_validation_clock_skew(response_xml, &key);
let entity_id = encrypted_response_entity_id(response_xml, &key);
// Create a service provider with the private key
ServiceProviderBuilder::default()
.idp_metadata(create_mock_idp())
.allow_idp_initiated(true)
.key(key)
.entity_id(Some(entity_id))
.max_clock_skew(max_clock_skew)
.max_issue_delay(encrypted_response_max_issue_delay(response_xml))
.build()
.unwrap()
}
fn create_sp_with_private_key(key: PKey<openssl::pkey::Private>) -> ServiceProvider {
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_encrypted.xml"
));
create_sp_with_private_key_for_response(response_xml, key)
}
fn create_mock_idp() -> EntityDescriptor {
EntityDescriptor {
entity_id: Some("saml-mock".to_string()),
valid_until: Some(Utc::now() + Duration::days(365)),
..Default::default()
}
}
#[test]
fn test_missing_private_key() {
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_encrypted.xml"
));
// Create a service provider without a private key
let sp = ServiceProviderBuilder::default()
.idp_metadata(create_mock_idp())
.max_clock_skew(Duration::days(365))
.max_issue_delay(encrypted_response_max_issue_delay(response_xml))
.build()
.unwrap();
// Sample response with an encrypted assertion
// Attempt to parse the response
let result = sp.parse_xml_response(response_xml, Some(&["example"]));
// Verify that the correct error is returned
assert_eq!(
result.err().unwrap().to_string(),
Error::MissingPrivateKeySP.to_string()
);
}
#[test]
fn test_decrypt_assertion() {
// In this test, we're primarily testing the decryption functionality,
// so we won't perform full validation
let pkey = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/sp_private.pem"
));
let key = PKey::private_key_from_pem(pkey).unwrap();
// Create a service provider with the private key but don't validate assertion
let sp = create_sp_with_private_key(key);
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_encrypted.xml"
));
// Extract encrypted assertion directly to test decryption functionality
// without validation
let response: crate::schema::Response = response_xml.parse().unwrap();
// Verify that an encrypted assertion is present
assert!(response.encrypted_assertion.is_some());
// Directly decrypt the assertion without validation
let result = response
.encrypted_assertion
.unwrap()
.decrypt(&sp.key.unwrap());
assert!(result.is_ok());
}
#[test]
fn test_decrypt_and_validate_assertion() {
let pkey = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/sp_private.pem"
));
let key = PKey::private_key_from_pem(pkey).unwrap();
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_encrypted_valid.xml"
));
let sp = create_sp_with_private_key_for_response(response_xml, key);
let result = sp.parse_xml_response(response_xml, Some(&["example"]));
assert!(result.is_ok());
}
fn extract_first_certificate(xml: &str) -> String {
let start = xml
.find("<ds:X509Certificate>")
.expect("response should contain a signing certificate");
let start = start + "<ds:X509Certificate>".len();
let end = xml[start..]
.find("</ds:X509Certificate>")
.expect("response should contain a closing certificate tag");
xml[start..start + end].to_string()
}
fn create_predigest_assertion_sp(acs_url: &str) -> ServiceProvider {
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_signed_assertion.xml"
));
let cert = extract_first_certificate(response_xml);
let metadata_xml = format!(
r#"<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://idp.example.com/metadata.php">
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>{cert}</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
</md:IDPSSODescriptor>
</md:EntityDescriptor>"#
);
create_predigest_assertion_sp_with_metadata(acs_url, metadata_xml.parse().unwrap())
}
fn create_predigest_assertion_sp_without_signing_certs(acs_url: &str) -> ServiceProvider {
create_predigest_assertion_sp_with_metadata(
acs_url,
EntityDescriptor {
entity_id: Some("http://idp.example.com/metadata.php".to_string()),
..Default::default()
},
)
}
fn create_predigest_assertion_sp_with_metadata(
acs_url: &str,
idp_metadata: EntityDescriptor,
) -> ServiceProvider {
let response_instant = "2014-07-17T01:01:48Z".parse::<DateTime<Utc>>().unwrap();
let max_issue_delay = Utc::now() - response_instant + Duration::seconds(60);
ServiceProviderBuilder::default()
.idp_metadata(idp_metadata)
.entity_id(Some("http://sp.example.com/demo1/metadata.php".to_string()))
.acs_url(Some(acs_url.to_string()))
.max_clock_skew(Duration::days(5000))
.max_issue_delay(max_issue_delay)
.build()
.unwrap()
}
fn create_multi_signed_response_sp() -> ServiceProvider {
let idp_metadata = r#"<?xml version="1.0"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://some.idp.test/blah/">
<md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIB+jCCAWOgAwIBAgIUdcXUPTE+mOSWxRCJh8ldmDMPzREwDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwEVGVzdDAeFw0yNjA0MDIxMjQzMTNaFw0yNzA0MDIxMjQzMTNaMA8xDTALBgNVBAMMBFRlc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAJEBNDJKH5nXr0hZKcSNIY1l4HeYLPBEKJLXyAnoFTdgGrvi40YyIx9lHh0LbDVWCgxJp21BmKll0CkgmeKidvGlr3FUwtETro44L+SgmjiJNbftvFxhNkgA26O2GDQuBoQwgSiagVadWXwJKkodH8tx4ojBPYK1pBO8fHf3wOnxAgMBAAGjUzBRMB0GA1UdDgQWBBSLoT4AEwcK1+0IMwgo6JYfA4e8ZTAfBgNVHSMEGDAWgBSLoT4AEwcK1+0IMwgo6JYfA4e8ZTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4GBAAtV1hclbZBD17LMbBwyrTj7szmmeUVISPeFEPaAKqiTXrHwRZ+akajboB2JjT3YYMXX2/eDaSvq9f20vJQUvkEAaYu8eNNDKWgm4btJFAeJT8uGxizmTspdJ0cxFSwxqaosV3qIqJgpwLbzUXEcu6mKfyqDM6AeFZdZevkxmKlE</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://some.idp.test/blah/"/>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://some.idp.test/blah/"/>
</md:IDPSSODescriptor>
</md:EntityDescriptor>
"#
.parse()
.unwrap();
let mut sp = create_predigest_assertion_sp_with_metadata(
"https://api.dev.zoo.dev/auth/saml/00000000-00000000-00000000-00000000/login",
idp_metadata,
);
sp.allow_idp_initiated = true;
sp
}
fn refresh_assertion_validation_windows(assertion: &mut Assertion) {
if let Some(conditions) = assertion.conditions.as_mut() {
conditions.not_before = Some(Utc::now() - Duration::minutes(1));
conditions.not_on_or_after = Some(Utc::now() + Duration::days(1));
}
if let Some(confirmations) = assertion
.subject
.as_mut()
.and_then(|subject| subject.subject_confirmations.as_mut())
{
for confirmation in confirmations {
if let Some(data) = confirmation.subject_confirmation_data.as_mut() {
data.not_before = Some(Utc::now() - Duration::minutes(1));
data.not_on_or_after = Some(Utc::now() + Duration::days(1));
}
}
}
}
#[test]
fn test_predigest_accepts_signed_assertion_response() {
let sp = create_predigest_assertion_sp("http://sp.example.com/demo1/index.php?acs");
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_signed_assertion.xml"
));
let assertion = sp
.parse_xml_response_with_mode(
response_xml,
Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]),
ReduceMode::PreDigest,
)
.expect("signed assertion should parse in pre-digest mode");
assert_eq!(
assertion.issuer.value.as_deref(),
Some("http://idp.example.com/metadata.php")
);
}
#[test]
fn test_predigest_rejects_mismatched_request_id() {
let sp = create_predigest_assertion_sp("http://sp.example.com/demo1/index.php?acs");
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_signed_assertion.xml"
));
let error = sp
.parse_xml_response_with_mode(
response_xml,
Some(&["WRONG_REQUEST_ID"]),
ReduceMode::PreDigest,
)
.expect_err("mismatched request id should fail in pre-digest mode");
assert!(matches!(error, Error::AssertionInResponseToInvalid { .. }));
}
#[test]
fn test_predigest_rejects_mismatched_recipient() {
let sp = create_predigest_assertion_sp("http://sp.example.com/demo1/index.php?wrong");
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_signed_assertion.xml"
));
let error = sp
.parse_xml_response_with_mode(
response_xml,
Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]),
ReduceMode::PreDigest,
)
.expect_err("mismatched recipient should fail in pre-digest mode");
assert!(matches!(error, Error::AssertionRecipientMismatch { .. }));
}
#[test]
fn test_predigest_response_api_rejects_unsigned_bare_assertion_without_signing_certs() {
let sp = create_predigest_assertion_sp_without_signing_certs(
"http://sp.example.com/demo1/index.php?acs",
);
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_signed_assertion.xml"
));
let response: Response = response_xml.parse().unwrap();
let assertion_xml = response.assertion.as_ref().unwrap().to_string().unwrap();
let error = sp
.parse_xml_response_with_mode(
&assertion_xml,
Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]),
ReduceMode::PreDigest,
)
.expect_err("response parser should not accept a bare unsigned assertion");
assert!(matches!(error, Error::FailedToParseSamlResponse(_)));
}
#[test]
fn test_default_response_api_ignores_unsigned_response_wrapper_for_signed_assertions() {
let sp = create_predigest_assertion_sp("http://sp.example.com/demo1/index.php?acs");
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_signed_assertion.xml"
));
let mutated_response_xml = response_xml
.replace(
r#"Destination="http://sp.example.com/demo1/index.php?acs""#,
r#"Destination="http://sp.example.com/demo1/index.php?wrong""#,
)
.replacen(
r#"InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685""#,
r#"InResponseTo="WRONG_REQUEST_ID""#,
1,
);
let assertion = sp
.parse_xml_response(
&mutated_response_xml,
Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]),
)
.expect("default response parsing should trust the signed assertion, not the unsigned response wrapper");
assert_eq!(
assertion.issuer.value.as_deref(),
Some("http://idp.example.com/metadata.php")
);
}
// TODO: this should work, but it does not with ValidateAndMark
#[test]
fn test_validate_and_mark_only_assertion_signed() {
let sp = create_predigest_assertion_sp("http://sp.example.com/demo1/index.php?acs");
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_signed_assertion.xml"
));
let assertion = sp
.parse_xml_response_with_mode(
&response_xml,
Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]),
ReduceMode::ValidateAndMark,
)
.unwrap();
assert_eq!(
assertion.issuer.value.as_deref(),
Some("http://idp.example.com/metadata.php")
);
}
#[test]
fn test_response_validation_requires_assertion_recipient_binding() {
let sp = create_predigest_assertion_sp("http://sp.example.com/demo1/index.php?acs");
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_signed_assertion.xml"
));
let mut response: Response = response_xml.parse().unwrap();
response.destination = None;
response
.assertion
.as_mut()
.unwrap()
.subject
.as_mut()
.unwrap()
.subject_confirmations
.as_mut()
.unwrap()[0]
.subject_confirmation_data
.as_mut()
.unwrap()
.recipient = None;
let error = sp
.validate_parsed_response(
response,
Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]),
)
.expect_err("missing bearer recipient should fail response validation");
assert!(matches!(error, Error::AssertionRecipientMismatch { .. }));
}
#[test]
fn test_validate_assertion_requires_subject_confirmation_expiry() {
let sp = create_predigest_assertion_sp("http://sp.example.com/demo1/index.php?acs");
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_signed_assertion.xml"
));
let response: Response = response_xml.parse().unwrap();
let mut assertion = response.assertion.unwrap();
refresh_assertion_validation_windows(&mut assertion);
assertion
.subject
.as_mut()
.unwrap()
.subject_confirmations
.as_mut()
.unwrap()[0]
.subject_confirmation_data
.as_mut()
.unwrap()
.not_on_or_after = None;
let error = sp
.validate_assertion(
&assertion,
Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]),
)
.expect_err("missing subject confirmation expiry should be rejected");
assert!(matches!(error, Error::AssertionSubjectConfirmationMissing));
}
#[test]
fn test_validate_assertion_requires_bearer_subject_confirmation() {
let sp = create_predigest_assertion_sp("http://sp.example.com/demo1/index.php?acs");
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_signed_assertion.xml"
));
let response: Response = response_xml.parse().unwrap();
let mut assertion = response.assertion.unwrap();
refresh_assertion_validation_windows(&mut assertion);
assertion
.subject
.as_mut()
.unwrap()
.subject_confirmations
.as_mut()
.unwrap()[0]
.method = Some("urn:oasis:names:tc:SAML:2.0:cm:holder-of-key".to_string());
let error = sp
.validate_assertion(
&assertion,
Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]),
)
.expect_err("non-bearer confirmations should be rejected");
assert!(matches!(
error,
Error::AssertionBearerSubjectConfirmationMissing
));
}
#[test]
fn test_validate_assertion_rejects_expired_subject_confirmation() {
let mut sp = create_predigest_assertion_sp("http://sp.example.com/demo1/index.php?acs");
sp.max_clock_skew = Duration::zero();
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_signed_assertion.xml"
));
let response: Response = response_xml.parse().unwrap();
let mut assertion = response.assertion.unwrap();
refresh_assertion_validation_windows(&mut assertion);
assertion
.subject
.as_mut()
.unwrap()
.subject_confirmations
.as_mut()
.unwrap()[0]
.subject_confirmation_data
.as_mut()
.unwrap()
.not_on_or_after = Some(Utc::now() - Duration::minutes(1));
let error = sp
.validate_assertion(
&assertion,
Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]),
)
.expect_err("expired subject confirmation should be rejected");
assert!(matches!(
error,
Error::AssertionSubjectConfirmationExpired { .. }
));
}
#[test]
fn test_validate_assertion_rejects_not_yet_valid_subject_confirmation() {
let mut sp = create_predigest_assertion_sp("http://sp.example.com/demo1/index.php?acs");
sp.max_clock_skew = Duration::zero();
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_signed_assertion.xml"
));
let response: Response = response_xml.parse().unwrap();
let mut assertion = response.assertion.unwrap();
refresh_assertion_validation_windows(&mut assertion);
assertion
.subject
.as_mut()
.unwrap()
.subject_confirmations
.as_mut()
.unwrap()[0]
.subject_confirmation_data
.as_mut()
.unwrap()
.not_before = Some(Utc::now() + Duration::minutes(1));
let error = sp
.validate_assertion(
&assertion,
Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]),
)
.expect_err("future subject confirmation should be rejected");
assert!(matches!(
error,
Error::AssertionSubjectConfirmationExpiredBefore { .. }
));
}
#[test]
fn test_parse_xml_response_with_empty_saml_response() {
let sp = create_multi_signed_response_sp();
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/multi_saml_response_signed_2.xml"
));
sp.parse_xml_response_with_mode(response_xml, None, ReduceMode::PreDigest)
.expect("signed assertion should parse in pre-digest mode");
}
#[test]
fn test_allowed_signature_algorithms_accepts_listed_algorithm() {
use crate::crypto::AllowedSignatureAlgorithm;
let mut sp = create_multi_signed_response_sp();
sp.allowed_signature_algorithms = Some(vec![AllowedSignatureAlgorithm::RsaSha256]);
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/multi_saml_response_signed_2.xml"
));
sp.parse_xml_response_with_mode(response_xml, None, ReduceMode::PreDigest)
.expect("RSA-SHA256 should verify when RSA-SHA256 is allowed");
}
#[test]
fn test_allowed_signature_algorithms_rejects_weak_algorithms() {
use crate::crypto::AllowedSignatureAlgorithm;
let idp_metadata: EntityDescriptor = include_str!("../../test_vectors/idp_metadata.xml")
.parse()
.unwrap();
// Create a ServiceProvider that only allows strong algorithms (SHA256 and above)
let sp = ServiceProviderBuilder::default()
.idp_metadata(idp_metadata)
.acs_url(Some("http://sp.example.com/acs".to_string()))
.allowed_signature_algorithms(Some(vec![
AllowedSignatureAlgorithm::RsaSha256,
AllowedSignatureAlgorithm::EcdsaSha256,
AllowedSignatureAlgorithm::RsaSha384,
AllowedSignatureAlgorithm::RsaSha512,
]))
.build()
.unwrap();
// Verify the configuration is set
assert!(sp.allowed_signature_algorithms.is_some());
assert_eq!(sp.allowed_signature_algorithms.as_ref().unwrap().len(), 4);
// Test that a response signed with weak RSA-SHA1 is REJECTED
// This test vector is signed with RSA-SHA1, which is NOT in our allowed list
let response_xml = include_str!("../../test_vectors/response_signed_by_idp_2.xml");
let result =
sp.parse_xml_response(response_xml, Some(&["id-sRfZ4Fe8w3bPPOtxPcLEYW6aWpZm"]));
// This should FAIL because RSA-SHA1 is not in the allowed list
assert!(
result.is_err(),
"RSA-SHA1 should be rejected when not in allowed list"
);
assert!(matches!(result, Err(Error::FailedToValidateSignature)));
}
#[test]
fn test_allowed_signature_algorithms_rejects_weak_digest_methods() {
use crate::crypto::AllowedSignatureAlgorithm;
let idp_metadata_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/idp_ecdsa_metadata.xml"
));
let response_instant = "2014-07-17T01:01:48Z".parse::<DateTime<Utc>>().unwrap();
let sp = ServiceProvider {
metadata_url: Some("http://test_accept_signed_with_correct_key.test".into()),
acs_url: Some("http://sp.example.com/demo1/index.php?acs".into()),
idp_metadata: idp_metadata_xml.parse().unwrap(),
max_issue_delay: Utc::now() - response_instant + Duration::seconds(60),
allowed_signature_algorithms: Some(vec![AllowedSignatureAlgorithm::EcdsaSha256]),
..Default::default()
};
let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_signed_by_idp_ecdsa.xml"
));
let result = sp.parse_xml_response(
response_xml,
Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]),
);
assert!(matches!(result, Err(Error::FailedToValidateSignature)));
}
#[test]
fn test_default_has_no_algorithm_restrictions() {
let idp_metadata: EntityDescriptor = include_str!("../../test_vectors/idp_metadata.xml")
.parse()
.unwrap();
// ServiceProvider without explicit algorithm restrictions
let sp = ServiceProviderBuilder::default()
.idp_metadata(idp_metadata)
.acs_url(Some("http://sp.example.com/acs".to_string()))
.build()
.unwrap();
// Verify no restrictions are set by default
assert!(sp.allowed_signature_algorithms.is_none());
// Note: All other existing tests in the suite pass without restrictions,
// demonstrating that the default behavior (no restrictions) works correctly.
// This test simply verifies that the field defaults to None.
}
}