Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/crypto/crypto_disabled.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! This module provides the behaviour if no crypto is available.

use crate::crypto::{CertificateDer, CryptoError, CryptoProvider, ReduceMode};
use crate::crypto::{AllowedSignatureAlgorithm, CertificateDer, CryptoError, CryptoProvider, ReduceMode};
use crate::schema::CipherValue;

pub struct NoCrypto;
Expand All @@ -16,10 +16,11 @@ impl CryptoProvider for NoCrypto {
Ok(())
}

fn reduce_xml_to_signed(
fn reduce_xml_to_signed_with_allowed_algorithms(
_xml_str: &str,
_certs: &[CertificateDer],
_reduce_mode: ReduceMode,
_allowed_algorithms: Option<&[AllowedSignatureAlgorithm]>,
) -> Result<String, CryptoError> {
Err(CryptoError::CryptoDisabled)
}
Expand Down
64 changes: 64 additions & 0 deletions src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,48 @@ impl From<Vec<u8>> for CertificateDer {
}
}

/// Allowed signature algorithms for signature verification.
/// By default, all algorithms are allowed (insecure). Use this to restrict
/// to only strong, approved algorithms.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AllowedSignatureAlgorithm {
/// RSA-SHA256 (required by most SAML profiles)
RsaSha256,
/// ECDSA-SHA256 (required by most SAML profiles)
EcdsaSha256,
/// RSA-SHA224
RsaSha224,
/// RSA-SHA384
RsaSha384,
/// RSA-SHA512
RsaSha512,
/// ECDSA-SHA224
EcdsaSha224,
/// ECDSA-SHA384
EcdsaSha384,
/// ECDSA-SHA512
EcdsaSha512,
/// DSA-SHA256
DsaSha256,
}

impl AllowedSignatureAlgorithm {
/// Returns the algorithm URI as defined in XML Signature specifications
pub fn uri(&self) -> &'static str {
match self {
Self::RsaSha256 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
Self::EcdsaSha256 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
Self::RsaSha224 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha224",
Self::RsaSha384 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
Self::RsaSha512 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
Self::EcdsaSha224 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha224",
Self::EcdsaSha384 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
Self::EcdsaSha512 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512",
Self::DsaSha256 => "http://www.w3.org/2009/xmldsig11#dsa-sha256",
}
}
}

/// Defines which algorithm is used to reduce signed XML.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReduceMode {
Expand Down Expand Up @@ -105,6 +147,28 @@ pub trait CryptoProvider {
xml_str: &str,
certs_der: &[CertificateDer],
reduce_mode: ReduceMode,
) -> Result<String, CryptoError> {
Self::reduce_xml_to_signed_with_allowed_algorithms(
xml_str,
certs_der,
reduce_mode,
None,
)
}

/// Takes an XML document, parses it, verifies all XML digital signatures against the given
/// certificates, and returns output according to `reduce_mode`.
///
/// If `allowed_algorithms` is `Some`, only the specified signature algorithms will be accepted.
/// If `None`, all algorithms are allowed (insecure, not recommended for production).
///
/// This provides protection against algorithm substitution attacks by enforcing signature
/// algorithm restrictions at the xmlsec library level before any cryptographic operations.
fn reduce_xml_to_signed_with_allowed_algorithms(
xml_str: &str,
certs_der: &[CertificateDer],
reduce_mode: ReduceMode,
allowed_algorithms: Option<&[AllowedSignatureAlgorithm]>,
) -> Result<String, CryptoError>;

fn decrypt_assertion_key_info(
Expand Down
78 changes: 77 additions & 1 deletion src/crypto/xmlsec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,72 @@ struct ParsedSelection {
sequence_nodes: Vec<libxml::tree::Node>,
}

/// Helper function to enable only allowed signature algorithms in the signature context.
/// Once called, xmlsec switches to whitelist mode and rejects all other algorithms.
///
/// This also enables the necessary reference transforms (C14N, enveloped signature, digests)
/// that are commonly used in SAML signatures.
fn enable_allowed_signature_algorithms(
sig_ctx: &mut XmlSecSignatureContext,
allowed: &[super::AllowedSignatureAlgorithm],
) -> Result<(), XmlSecProviderError> {
use super::AllowedSignatureAlgorithm;

// Enable the allowed signature algorithms
for algo in allowed {
let transform_id = unsafe {
match algo {
AllowedSignatureAlgorithm::RsaSha224 => {
wrapper::xmlSecOpenSSLTransformRsaSha224GetKlass()
}
AllowedSignatureAlgorithm::RsaSha256 => {
wrapper::xmlSecOpenSSLTransformRsaSha256GetKlass()
}
AllowedSignatureAlgorithm::RsaSha384 => {
wrapper::xmlSecOpenSSLTransformRsaSha384GetKlass()
}
AllowedSignatureAlgorithm::RsaSha512 => {
wrapper::xmlSecOpenSSLTransformRsaSha512GetKlass()
}
AllowedSignatureAlgorithm::EcdsaSha224 => {
wrapper::xmlSecOpenSSLTransformEcdsaSha224GetKlass()
}
AllowedSignatureAlgorithm::EcdsaSha256 => {
wrapper::xmlSecOpenSSLTransformEcdsaSha256GetKlass()
}
AllowedSignatureAlgorithm::EcdsaSha384 => {
wrapper::xmlSecOpenSSLTransformEcdsaSha384GetKlass()
}
AllowedSignatureAlgorithm::EcdsaSha512 => {
wrapper::xmlSecOpenSSLTransformEcdsaSha512GetKlass()
}
AllowedSignatureAlgorithm::DsaSha256 => {
wrapper::xmlSecOpenSSLTransformDsaSha256GetKlass()
}
}
};

sig_ctx.enable_signature_transform(transform_id)?;
}

// Enable common transforms used in SAML signatures
unsafe {
// Exclusive C14N (most common in SAML) - needed for both signature and references
let c14n_transform = wrapper::xmlSecTransformExclC14NGetKlass();
sig_ctx.enable_signature_transform(c14n_transform)?;
sig_ctx.enable_reference_transform(c14n_transform)?;

// Enveloped signature transform (required for embedded signatures)
sig_ctx.enable_reference_transform(wrapper::xmlSecTransformEnvelopedGetKlass())?;

// Digest algorithms - SHA1 and SHA256 are most common in SAML
sig_ctx.enable_reference_transform(wrapper::xmlSecOpenSSLTransformSha1GetKlass())?;
sig_ctx.enable_reference_transform(wrapper::xmlSecOpenSSLTransformSha256GetKlass())?;
}

Ok(())
}

pub struct XmlSec;

impl super::CryptoProvider for XmlSec {
Expand Down Expand Up @@ -149,10 +215,14 @@ impl super::CryptoProvider for XmlSec {
/// Takes an XML document, parses it, verifies all XML digital signatures against the given
/// certificates, and returns a derived version of the document where all elements that are not
/// covered by a digital signature have been removed.
fn reduce_xml_to_signed(
///
/// If `allowed_algorithms` is provided, only those signature algorithms will be accepted.
/// This provides protection against algorithm substitution attacks.
fn reduce_xml_to_signed_with_allowed_algorithms(
xml_str: &str,
certs_der: &[CertificateDer],
reduce_mode: ReduceMode,
allowed_algorithms: Option<&[super::AllowedSignatureAlgorithm]>,
) -> Result<String, CryptoError> {
let mut xml = XmlParser::default().parse_string(xml_str)?;

Expand Down Expand Up @@ -180,6 +250,12 @@ impl super::CryptoProvider for XmlSec {
let mut sig_ctx = XmlSecSignatureContext::new_with_flags(
wrapper::XMLSEC_DSIG_FLAGS_STORE_SIGNEDINFO_REFERENCES,
)?;

// Enable only allowed algorithms if specified
if let Some(allowed) = allowed_algorithms {
enable_allowed_signature_algorithms(&mut sig_ctx, allowed)?;
}

let key = XmlSecKey::from_memory(key_data.der_data(), XmlSecKeyFormat::CertDer)?;
sig_ctx.insert_key(key);
verified = sig_ctx.verify_node(sig_node)?;
Expand Down
12 changes: 12 additions & 0 deletions src/crypto/xmlsec/wrapper/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,15 @@ pub use self::keys::XmlSecKey;
pub use self::keys::XmlSecKeyFormat;
pub use self::xmldsig::VerifiedReference;
pub use self::xmldsig::XmlSecSignatureContext;

// Export transform ID getters for signature algorithm restrictions
#[doc(hidden)]
pub use self::bindings::{
xmlSecOpenSSLTransformDsaSha256GetKlass, xmlSecOpenSSLTransformEcdsaSha224GetKlass,
xmlSecOpenSSLTransformEcdsaSha256GetKlass, xmlSecOpenSSLTransformEcdsaSha384GetKlass,
xmlSecOpenSSLTransformEcdsaSha512GetKlass, xmlSecOpenSSLTransformRsaSha224GetKlass,
xmlSecOpenSSLTransformRsaSha256GetKlass, xmlSecOpenSSLTransformRsaSha384GetKlass,
xmlSecOpenSSLTransformRsaSha512GetKlass, xmlSecOpenSSLTransformSha1GetKlass,
xmlSecOpenSSLTransformSha256GetKlass, xmlSecTransformEnvelopedGetKlass,
xmlSecTransformExclC14NGetKlass,
};
34 changes: 34 additions & 0 deletions src/crypto/xmlsec/wrapper/xmldsig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,40 @@ impl XmlSecSignatureContext {
Ok(ctx)
}

/// Enables a specific signature algorithm transform. Once called, only explicitly
/// enabled transforms will be allowed (whitelist mode).
pub fn enable_signature_transform(
&mut self,
transform_id: bindings::xmlSecTransformId,
) -> XmlSecResult<()> {
let rc = unsafe {
bindings::xmlSecDSigCtxEnableSignatureTransform(self.ctx.as_ptr(), transform_id)
};

if rc < 0 {
Err(XmlSecError::ContextInitError)
} else {
Ok(())
}
}

/// Enables a specific reference transform. Once called, only explicitly
/// enabled transforms will be allowed for references (whitelist mode).
pub fn enable_reference_transform(
&mut self,
transform_id: bindings::xmlSecTransformId,
) -> XmlSecResult<()> {
let rc = unsafe {
bindings::xmlSecDSigCtxEnableReferenceTransform(self.ctx.as_ptr(), transform_id)
};

if rc < 0 {
Err(XmlSecError::ContextInitError)
} else {
Ok(())
}
}

/// Retrieves the verified references from `<ds:SignedInfo>`.
pub fn get_verified_references(&self) -> XmlSecResult<Vec<VerifiedReference>> {
let mut result = Vec::new();
Expand Down
22 changes: 19 additions & 3 deletions src/service_provider/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ pub struct ServiceProvider {
pub contact_person: Option<ContactPerson>,
pub max_issue_delay: Duration,
pub max_clock_skew: Duration,
/// Optional list of allowed signature algorithms for signature verification.
/// If None, all algorithms are allowed (insecure, not recommended).
/// If Some, only the specified algorithms will be accepted, providing protection
/// against algorithm substitution attacks.
pub allowed_signature_algorithms: Option<Vec<crypto::AllowedSignatureAlgorithm>>,
}

impl Default for ServiceProvider {
Expand All @@ -194,6 +199,7 @@ impl Default for ServiceProvider {
contact_person: None,
max_issue_delay: Duration::seconds(90),
max_clock_skew: Duration::seconds(180),
allowed_signature_algorithms: None,
}
}
}
Expand Down Expand Up @@ -391,9 +397,19 @@ impl ServiceProvider {
) -> Result<Assertion, Error> {
let (reduced_xml, reduced_from_verified_signature) =
if let Some(sign_certs) = self.idp_signing_certs()? {
let allowed_algorithms = self
.allowed_signature_algorithms
.as_ref()
.map(|v| v.as_slice());

(
Crypto::reduce_xml_to_signed(response_xml, &sign_certs, reduce_mode)
.map_err(|_e| Error::FailedToValidateSignature)?,
Crypto::reduce_xml_to_signed_with_allowed_algorithms(
response_xml,
&sign_certs,
reduce_mode,
allowed_algorithms,
)
.map_err(|_e| Error::FailedToValidateSignature)?,
true,
)
} else {
Expand Down Expand Up @@ -701,7 +717,7 @@ impl ServiceProvider {
}
}

fn root_element_local_name(xml: &str) -> Option<String> {
pub(crate) fn root_element_local_name(xml: &str) -> Option<String> {
let mut reader = Reader::from_str(xml);

loop {
Expand Down
61 changes: 61 additions & 0 deletions src/service_provider/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,4 +583,65 @@ mod encrypted_assertion_tests {
Error::AssertionSubjectConfirmationExpiredBefore { .. }
));
}

#[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("http://sp.example.com/acs".to_string())
.allowed_signature_algorithms(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_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("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.
}
}