pub mod generate;
pub mod idp;
pub mod sp;
mod write;
#[cfg(feature = "crypto-bergshamra")]
pub use crate::crypto::MetadataSignatureVerification;
pub use generate::{
generate_idp_metadata, generate_sp_metadata, try_generate_idp_metadata, Endpoint,
IdpMetadataConfig, SpMetadataConfig,
};
pub use idp::IdpMetadata;
pub use sp::SpMetadata;
use crate::constants::{Binding, CertUse};
use crate::error::SamlError;
use crate::util::Value;
use crate::xml::{dom, extract_with_limits, ExtractorField, XmlLimits};
fn base_fields() -> Vec<ExtractorField> {
vec![
ExtractorField::new("entityID", &["EntityDescriptor"]).attrs(&["entityID"]),
ExtractorField::new(
"sharedCertificate",
&[
"EntityDescriptor",
"~SSODescriptor",
"KeyDescriptor",
"KeyInfo",
"X509Data",
"X509Certificate",
],
),
ExtractorField::new(
"certificate",
&["EntityDescriptor", "~SSODescriptor", "KeyDescriptor"],
)
.aggregate(&["use"], &["KeyInfo", "X509Data", "X509Certificate"]),
ExtractorField::new(
"singleLogoutService",
&["EntityDescriptor", "~SSODescriptor", "SingleLogoutService"],
)
.attrs(&["Binding", "Location"]),
ExtractorField::new(
"nameIDFormat",
&["EntityDescriptor", "~SSODescriptor", "NameIDFormat"],
),
]
}
pub(crate) fn as_object_list(value: &Value) -> Vec<&Value> {
match value {
Value::Array(items) => items.iter().collect(),
Value::Object(_) => vec![value],
_ => Vec::new(),
}
}
fn location_for_binding(value: Option<&Value>, binding: Binding) -> Option<String> {
let value = value?;
for obj in as_object_list(value) {
if obj.get_str("binding") == Some(binding.urn()) {
return obj.get_str("location").map(str::to_string);
}
}
None
}
#[derive(Debug, Clone)]
pub struct Metadata {
xml: String,
pub(crate) meta: Value,
}
impl Metadata {
pub fn parse(xml: &str, extra: Vec<ExtractorField>) -> Result<Self, SamlError> {
Self::parse_with_limits(xml, extra, XmlLimits::default())
}
pub fn parse_with_limits(
xml: &str,
extra: Vec<ExtractorField>,
limits: XmlLimits,
) -> Result<Self, SamlError> {
let roots = dom::parse_roots_with_limits(xml, limits)?;
if roots
.iter()
.filter(|n| n.local_name == "EntityDescriptor")
.count()
> 1
{
return Err(SamlError::Xml(
"ERR_MULTIPLE_METADATA_ENTITYDESCRIPTOR".into(),
));
}
let mut fields = base_fields();
fields.extend(extra);
let mut meta = extract_with_limits(xml, &fields, limits)?;
if let Some(shared) = meta.get_str("sharedCertificate") {
let shared = shared.to_string();
meta.insert(
"certificate",
Value::Object(vec![
("signing".into(), Value::Str(shared.clone())),
("encryption".into(), Value::Str(shared)),
]),
);
}
Ok(Self {
xml: xml.to_string(),
meta,
})
}
pub fn get_metadata(&self) -> &str {
&self.xml
}
pub fn get_entity_id(&self) -> Option<&str> {
self.meta.get_str("entityID")
}
pub fn get_name_id_format(&self) -> Vec<String> {
match self.meta.get("nameIDFormat") {
Some(Value::Array(items)) => items
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect(),
Some(Value::Str(s)) => vec![s.clone()],
_ => Vec::new(),
}
}
pub fn x509_certificates(&self, use_: CertUse) -> Vec<String> {
match self
.meta
.get("certificate")
.and_then(|c| c.get_key(use_.as_str()))
{
Some(Value::Str(s)) => vec![s.clone()],
Some(Value::Array(items)) => items
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect(),
_ => Vec::new(),
}
}
pub fn get_x509_certificate(&self, use_: CertUse) -> Option<String> {
self.x509_certificates(use_).into_iter().next()
}
pub fn get_single_logout_service(&self, binding: Binding) -> Option<String> {
location_for_binding(self.meta.get("singleLogoutService"), binding)
}
pub fn export_metadata(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
std::fs::write(path, &self.xml)
}
pub fn get_support_bindings(&self) -> Vec<Binding> {
[Binding::Redirect, Binding::Post, Binding::SimpleSign]
.into_iter()
.filter(|b| self.get_single_logout_service(*b).is_some())
.collect()
}
#[cfg(feature = "crypto-bergshamra")]
pub fn verify_signature(&self, trusted_certificates: &[String]) -> Result<bool, SamlError> {
self.verify_signature_with_limits(trusted_certificates, XmlLimits::default())
}
#[cfg(feature = "crypto-bergshamra")]
pub fn verify_signature_with_limits(
&self,
trusted_certificates: &[String],
limits: XmlLimits,
) -> Result<bool, SamlError> {
crate::crypto::verify_metadata_signature_with_limits(
&self.xml,
trusted_certificates,
limits,
)
}
#[cfg(feature = "crypto-bergshamra")]
pub fn verify_signature_detailed(
&self,
trusted_certificates: &[String],
) -> Result<crate::crypto::MetadataSignatureVerification, SamlError> {
crate::crypto::verify_metadata_signature_detailed(&self.xml, trusted_certificates)
}
#[cfg(feature = "crypto-bergshamra")]
pub fn verify_signature_detailed_with_limits(
&self,
trusted_certificates: &[String],
limits: XmlLimits,
) -> Result<MetadataSignatureVerification, SamlError> {
crate::crypto::verify_metadata_signature_detailed_with_limits(
&self.xml,
trusted_certificates,
limits,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
const IDPMETA: &str = include_str!("../../tests/fixtures/idpmeta.xml");
const SPMETA: &str = include_str!("../../tests/fixtures/spmeta.xml");
const MULTIPLE: &str = include_str!("../../tests/fixtures/multiple_entitydescriptor.xml");
#[test]
fn rejects_multiple_entity_descriptors() {
assert!(Metadata::parse(MULTIPLE, Vec::new()).is_err());
}
#[test]
fn parses_idp_metadata() -> Result<(), Box<dyn std::error::Error>> {
let idp = IdpMetadata::from_xml(IDPMETA)?;
assert_eq!(
idp.get_entity_id(),
Some("https://idp.example.com/metadata")
);
assert!(idp.is_want_authn_requests_signed());
assert_eq!(
idp.get_single_sign_on_service(Binding::Redirect).as_deref(),
Some("https://idp.example.org/sso/SingleSignOnService")
);
assert!(idp.get_x509_certificate(CertUse::Signing).is_some());
assert!(idp
.get_name_id_format()
.iter()
.any(|f| f.contains("persistent")));
Ok(())
}
#[test]
fn parses_sp_metadata() -> Result<(), Box<dyn std::error::Error>> {
let sp = SpMetadata::from_xml(SPMETA)?;
assert_eq!(sp.get_entity_id(), Some("https://sp.example.org/metadata"));
assert!(sp.is_want_assertions_signed());
assert!(sp.is_authn_request_signed());
assert_eq!(
sp.get_assertion_consumer_service(Binding::Post).as_deref(),
Some("https://sp.example.org/sp/sso")
);
assert_eq!(
sp.get_single_logout_service(Binding::Redirect).as_deref(),
Some("https://sp.example.org/sp/slo")
);
assert!(sp.get_x509_certificate(CertUse::Encryption).is_some());
Ok(())
}
#[test]
fn support_bindings_and_export() -> Result<(), Box<dyn std::error::Error>> {
let sp = SpMetadata::from_xml(SPMETA)?;
assert!(sp.get_support_bindings().contains(&Binding::Redirect));
let mut path = std::env::temp_dir();
path.push(format!("saml_rs_md_{}.xml", std::process::id()));
sp.export_metadata(&path)?;
assert_eq!(std::fs::read_to_string(&path)?, sp.get_metadata());
std::fs::remove_file(&path)?;
Ok(())
}
}