use crate::binding::Endpoint;
use crate::crypto::cert::X509Certificate;
use crate::crypto::keypair::KeyPair;
use crate::dsig::algorithms::{C14nAlgorithm, DigestAlgorithm, SignatureAlgorithm};
use crate::error::Error;
use crate::nameid::NameIdFormat;
use crate::xml::emit::emit_document;
use crate::xml::parse::{Document, Element, Node, QName};
#[cfg(feature = "xmlenc")]
use crate::xmlenc::algorithms::DataEncryptionAlgorithm;
use super::MetadataExtras;
#[cfg(feature = "xmlenc")]
use super::emit_sp::build_encryption_key_descriptor;
use super::emit_sp::{
DS_NS, MD_NS, SAML2_PROTOCOL, append_extras, bool_str, build_signing_key_descriptor,
format_cache_duration, md_qname,
};
pub struct IdpMetadataInputs<'a> {
pub entity_id: &'a str,
pub sso: &'a [Endpoint],
pub slo: &'a [Endpoint],
pub artifact_resolution: &'a [Endpoint],
pub name_id_formats: &'a [NameIdFormat],
pub signing_cert: &'a X509Certificate,
#[cfg(feature = "xmlenc")]
pub encryption_cert: Option<&'a X509Certificate>,
#[cfg(feature = "xmlenc")]
pub encryption_algorithms: &'a [DataEncryptionAlgorithm],
pub want_authn_requests_signed: bool,
pub valid_until: Option<std::time::SystemTime>,
pub cache_duration: Option<std::time::Duration>,
pub extras: Option<&'a MetadataExtras>,
}
pub fn emit_idp_metadata(
inputs: &IdpMetadataInputs<'_>,
signer: Option<(&KeyPair, SignatureAlgorithm, DigestAlgorithm, C14nAlgorithm)>,
) -> Result<String, Error> {
let entity_descriptor_id = crate::binding::random_xml_id()?;
let root = build_idp_entity_descriptor(inputs, &entity_descriptor_id)?;
let final_root = if let Some((key, sig_alg, digest, c14n)) = signer {
let unsigned_doc = Document::new(root.clone())?;
crate::dsig::sign::sign_element(
root,
&unsigned_doc,
crate::dsig::sign::SignOptions {
signing_key: key,
sig_alg,
digest_alg: digest,
c14n_alg: c14n,
inclusive_namespaces: &[],
include_x509_cert: true,
},
)?
} else {
root
};
let doc = Document::new(final_root)?;
emit_document(&doc)
}
pub(super) fn build_idp_entity_descriptor(
inputs: &IdpMetadataInputs<'_>,
entity_descriptor_id: &str,
) -> Result<Element, Error> {
let mut idp_descriptor = Element::build(md_qname("IDPSSODescriptor"))
.with_attribute(
QName::new(None, "protocolSupportEnumeration"),
SAML2_PROTOCOL,
)
.with_attribute(
QName::new(None, "WantAuthnRequestsSigned"),
bool_str(inputs.want_authn_requests_signed),
);
idp_descriptor = idp_descriptor.with_child(Node::Element(build_signing_key_descriptor(
inputs.signing_cert,
)));
#[cfg(feature = "xmlenc")]
if let Some(cert) = inputs.encryption_cert {
idp_descriptor = idp_descriptor.with_child(Node::Element(build_encryption_key_descriptor(
cert,
inputs.encryption_algorithms,
)));
}
for fmt in inputs.name_id_formats {
idp_descriptor = idp_descriptor.with_child(Node::Element(
Element::build(md_qname("NameIDFormat"))
.with_text(fmt.as_uri().to_owned())
.finish(),
));
}
for endpoint in inputs.sso {
idp_descriptor = idp_descriptor.with_child(Node::Element(build_sso_endpoint(endpoint)));
}
for endpoint in inputs.slo {
idp_descriptor = idp_descriptor.with_child(Node::Element(build_slo_endpoint(endpoint)));
}
for endpoint in inputs.artifact_resolution {
idp_descriptor =
idp_descriptor.with_child(Node::Element(build_artifact_resolution_endpoint(endpoint)));
}
let idp_descriptor = idp_descriptor.finish();
let mut entity_descriptor = Element::build(md_qname("EntityDescriptor"))
.with_namespace(Some("md".to_owned()), MD_NS)
.with_namespace(Some("ds".to_owned()), DS_NS)
.with_attribute(QName::new(None, "entityID"), inputs.entity_id.to_owned())
.with_attribute(QName::new(None, "ID"), entity_descriptor_id.to_owned());
if let Some(valid_until) = inputs.valid_until {
entity_descriptor = entity_descriptor.with_attribute(
QName::new(None, "validUntil"),
crate::time::format_xs_datetime(valid_until)?,
);
}
if let Some(cache_duration) = inputs.cache_duration {
entity_descriptor = entity_descriptor.with_attribute(
QName::new(None, "cacheDuration"),
format_cache_duration(cache_duration),
);
}
entity_descriptor = entity_descriptor.with_child(Node::Element(idp_descriptor));
if let Some(extras) = inputs.extras {
entity_descriptor = append_extras(entity_descriptor, extras);
}
Ok(entity_descriptor.finish())
}
fn build_sso_endpoint(endpoint: &Endpoint) -> Element {
Element::build(md_qname("SingleSignOnService"))
.with_attribute(QName::new(None, "Binding"), endpoint.binding.uri())
.with_attribute(QName::new(None, "Location"), endpoint.url.clone())
.finish()
}
fn build_slo_endpoint(endpoint: &Endpoint) -> Element {
Element::build(md_qname("SingleLogoutService"))
.with_attribute(QName::new(None, "Binding"), endpoint.binding.uri())
.with_attribute(QName::new(None, "Location"), endpoint.url.clone())
.finish()
}
fn build_artifact_resolution_endpoint(endpoint: &Endpoint) -> Element {
let mut builder = Element::build(md_qname("ArtifactResolutionService"))
.with_attribute(QName::new(None, "Binding"), endpoint.binding.uri())
.with_attribute(QName::new(None, "Location"), endpoint.url.clone());
let index = endpoint.index.unwrap_or(0);
builder = builder.with_attribute(QName::new(None, "index"), index.to_string());
if endpoint.is_default {
builder = builder.with_attribute(QName::new(None, "isDefault"), "true");
}
builder.finish()
}
#[cfg(test)]
#[cfg(feature = "xmlenc")]
mod tests {
use super::*;
use crate::binding::{Binding, Endpoint};
use crate::crypto::cert::X509Certificate;
use crate::crypto::cert::test_vectors::{RSA_CERT_PEM, RSA_KEY_PKCS8_PEM};
use crate::dsig::algorithms::{C14nAlgorithm, DigestAlgorithm, SignatureAlgorithm};
use crate::metadata::{
MetadataContact, MetadataContactType, MetadataExtras, MetadataOrganization,
};
use crate::xml::parse::Document;
use std::time::{Duration, SystemTime};
const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
fn rsa_cert() -> X509Certificate {
X509Certificate::from_pem(RSA_CERT_PEM).unwrap()
}
fn signing_keypair() -> KeyPair {
KeyPair::from_pkcs8_pem(RSA_KEY_PKCS8_PEM)
.unwrap()
.with_certificate(rsa_cert())
}
fn baseline_inputs<'a>(
cert: &'a X509Certificate,
sso: &'a [Endpoint],
slo: &'a [Endpoint],
ars: &'a [Endpoint],
formats: &'a [NameIdFormat],
algos: &'a [DataEncryptionAlgorithm],
) -> IdpMetadataInputs<'a> {
IdpMetadataInputs {
entity_id: "https://idp.example.com/saml",
sso,
slo,
artifact_resolution: ars,
name_id_formats: formats,
signing_cert: cert,
encryption_cert: Some(cert),
encryption_algorithms: algos,
want_authn_requests_signed: true,
valid_until: None,
cache_duration: None,
extras: None,
}
}
#[test]
fn emits_well_formed_idp_descriptor_with_expected_shape() {
let cert = rsa_cert();
let sso = [
Endpoint::post("https://idp.example.com/sso/post", 0, true),
Endpoint::redirect("https://idp.example.com/sso/redir", 1, false),
];
let slo = [Endpoint::redirect("https://idp.example.com/slo", 0, false)];
let ars = [Endpoint::soap("https://idp.example.com/ars", Some(0), true)];
let formats = [NameIdFormat::Persistent, NameIdFormat::Transient];
let algos = [DataEncryptionAlgorithm::Aes256Gcm];
let inputs = baseline_inputs(&cert, &sso, &slo, &ars, &formats, &algos);
let xml = emit_idp_metadata(&inputs, None).expect("emit");
let doc = Document::parse(xml.as_bytes()).expect("re-parse");
let root = doc.root();
assert_eq!(root.qname().namespace(), Some(MD_NS));
assert_eq!(root.qname().local(), "EntityDescriptor");
assert_eq!(
root.attribute(None, "entityID"),
Some("https://idp.example.com/saml")
);
let idp_desc = root
.child_element(Some(MD_NS), "IDPSSODescriptor")
.expect("IDPSSODescriptor present");
assert_eq!(
idp_desc.attribute(None, "protocolSupportEnumeration"),
Some(SAML2_PROTOCOL)
);
assert_eq!(
idp_desc.attribute(None, "WantAuthnRequestsSigned"),
Some("true")
);
let key_descriptors: Vec<_> = idp_desc
.all_child_elements(Some(MD_NS), "KeyDescriptor")
.collect();
assert_eq!(key_descriptors.len(), 2);
assert_eq!(key_descriptors[0].attribute(None, "use"), Some("signing"));
assert_eq!(
key_descriptors[1].attribute(None, "use"),
Some("encryption")
);
let enc_methods: Vec<_> = key_descriptors[1]
.all_child_elements(Some(MD_NS), "EncryptionMethod")
.collect();
assert_eq!(enc_methods.len(), 1);
assert_eq!(
enc_methods[0].attribute(None, "Algorithm"),
Some(DataEncryptionAlgorithm::Aes256Gcm.uri())
);
let formats: Vec<_> = idp_desc
.all_child_elements(Some(MD_NS), "NameIDFormat")
.map(Element::text_content)
.collect();
assert_eq!(
formats,
vec![
NameIdFormat::Persistent.as_uri().to_owned(),
NameIdFormat::Transient.as_uri().to_owned(),
]
);
let sso_elements: Vec<_> = idp_desc
.all_child_elements(Some(MD_NS), "SingleSignOnService")
.collect();
assert_eq!(sso_elements.len(), 2);
assert_eq!(
sso_elements[0].attribute(None, "Binding"),
Some(Binding::HttpPost.uri())
);
assert_eq!(
sso_elements[0].attribute(None, "Location"),
Some("https://idp.example.com/sso/post")
);
assert_eq!(sso_elements[0].attribute(None, "index"), None);
assert_eq!(sso_elements[0].attribute(None, "isDefault"), None);
assert_eq!(
sso_elements[1].attribute(None, "Binding"),
Some(Binding::HttpRedirect.uri())
);
let slo_elements: Vec<_> = idp_desc
.all_child_elements(Some(MD_NS), "SingleLogoutService")
.collect();
assert_eq!(slo_elements.len(), 1);
assert_eq!(
slo_elements[0].attribute(None, "Binding"),
Some(Binding::HttpRedirect.uri())
);
let ars_elements: Vec<_> = idp_desc
.all_child_elements(Some(MD_NS), "ArtifactResolutionService")
.collect();
assert_eq!(ars_elements.len(), 1);
assert_eq!(
ars_elements[0].attribute(None, "Binding"),
Some(Binding::Soap.uri())
);
assert_eq!(ars_elements[0].attribute(None, "index"), Some("0"));
assert_eq!(ars_elements[0].attribute(None, "isDefault"), Some("true"));
}
#[test]
fn valid_until_and_cache_duration_emit_attributes() {
let cert = rsa_cert();
let sso = [Endpoint::post("https://idp.example.com/sso", 0, true)];
let formats = [NameIdFormat::Persistent];
let algos: [DataEncryptionAlgorithm; 0] = [];
let mut inputs = baseline_inputs(&cert, &sso, &[], &[], &formats, &algos);
let valid_until = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
inputs.valid_until = Some(valid_until);
inputs.cache_duration = Some(Duration::from_hours(2));
let xml = emit_idp_metadata(&inputs, None).unwrap();
let doc = Document::parse(xml.as_bytes()).unwrap();
let root = doc.root();
let vu = root.attribute(None, "validUntil").expect("validUntil");
let parsed = crate::time::parse_xs_datetime(vu).expect("parse");
assert_eq!(parsed, valid_until);
assert_eq!(root.attribute(None, "cacheDuration"), Some("PT7200S"));
}
#[test]
fn extras_emit_organization_and_contact_person() {
let cert = rsa_cert();
let sso = [Endpoint::post("https://idp.example.com/sso", 0, true)];
let formats = [NameIdFormat::Persistent];
let algos: [DataEncryptionAlgorithm; 0] = [];
let extras = MetadataExtras {
organization: Some(MetadataOrganization {
name: "Example IdP Inc".into(),
display_name: "Example Identity Provider".into(),
url: "https://idp.example.com".into(),
language: "en-US".into(),
}),
contacts: vec![
MetadataContact {
contact_type: MetadataContactType::Technical,
given_name: Some("Sam".into()),
surname: None,
email_addresses: vec!["ops@idp.example.com".into()],
telephone_numbers: vec![],
company: None,
},
MetadataContact {
contact_type: MetadataContactType::Support,
given_name: None,
surname: None,
email_addresses: vec!["help@idp.example.com".into()],
telephone_numbers: vec!["+15550000".into(), "+15550001".into()],
company: Some("Example IdP Inc".into()),
},
],
#[cfg(feature = "idp-disco")]
discovery_response_endpoints: vec![],
};
let mut inputs = baseline_inputs(&cert, &sso, &[], &[], &formats, &algos);
inputs.extras = Some(&extras);
let xml = emit_idp_metadata(&inputs, None).unwrap();
let doc = Document::parse(xml.as_bytes()).unwrap();
let root = doc.root();
let org = root
.child_element(Some(MD_NS), "Organization")
.expect("Organization");
assert_eq!(
org.child_element(Some(MD_NS), "OrganizationName")
.map(Element::text_content),
Some("Example IdP Inc".to_owned())
);
assert_eq!(
org.child_element(Some(MD_NS), "OrganizationName")
.and_then(|e| e.attribute(Some(XML_NS), "lang").map(str::to_owned)),
Some("en-US".to_owned())
);
let contacts: Vec<_> = root
.all_child_elements(Some(MD_NS), "ContactPerson")
.collect();
assert_eq!(contacts.len(), 2);
assert_eq!(
contacts[0].attribute(None, "contactType"),
Some("technical")
);
assert_eq!(contacts[1].attribute(None, "contactType"), Some("support"));
let phones: Vec<_> = contacts[1]
.all_child_elements(Some(MD_NS), "TelephoneNumber")
.map(Element::text_content)
.collect();
assert_eq!(phones, vec!["+15550000".to_owned(), "+15550001".to_owned()]);
}
#[test]
fn signed_metadata_carries_ds_signature_referencing_entity_descriptor_id() {
let cert = rsa_cert();
let sso = [Endpoint::post("https://idp.example.com/sso", 0, true)];
let formats = [NameIdFormat::Persistent];
let algos: [DataEncryptionAlgorithm; 0] = [];
let inputs = baseline_inputs(&cert, &sso, &[], &[], &formats, &algos);
let kp = signing_keypair();
let xml = emit_idp_metadata(
&inputs,
Some((
&kp,
SignatureAlgorithm::RsaSha256,
DigestAlgorithm::Sha256,
C14nAlgorithm::ExclusiveCanonical,
)),
)
.expect("sign + emit");
let doc = Document::parse(xml.as_bytes()).unwrap();
let root = doc.root();
let id_attr = root.attribute(None, "ID").expect("ID");
let children: Vec<_> = root.child_elements().collect();
assert!(!children.is_empty());
assert_eq!(children[0].qname().namespace(), Some(DS_NS));
assert_eq!(children[0].qname().local(), "Signature");
let signed_info = children[0]
.child_element(Some(DS_NS), "SignedInfo")
.unwrap();
let reference = signed_info.child_element(Some(DS_NS), "Reference").unwrap();
assert_eq!(
reference.attribute(None, "URI"),
Some(format!("#{id_attr}").as_str())
);
let c14n_method = signed_info
.child_element(Some(DS_NS), "CanonicalizationMethod")
.unwrap();
assert_eq!(
c14n_method.attribute(None, "Algorithm"),
Some(C14nAlgorithm::ExclusiveCanonical.uri())
);
assert!(
root.child_element(Some(MD_NS), "IDPSSODescriptor")
.is_some()
);
}
#[test]
fn omits_encryption_key_descriptor_when_cert_absent() {
let cert = rsa_cert();
let sso = [Endpoint::post("https://idp.example.com/sso", 0, true)];
let formats = [NameIdFormat::Persistent];
let algos: [DataEncryptionAlgorithm; 0] = [];
let mut inputs = baseline_inputs(&cert, &sso, &[], &[], &formats, &algos);
inputs.encryption_cert = None;
let xml = emit_idp_metadata(&inputs, None).unwrap();
let doc = Document::parse(xml.as_bytes()).unwrap();
let idp_desc = doc
.root()
.child_element(Some(MD_NS), "IDPSSODescriptor")
.unwrap();
let kds: Vec<_> = idp_desc
.all_child_elements(Some(MD_NS), "KeyDescriptor")
.collect();
assert_eq!(kds.len(), 1);
assert_eq!(kds[0].attribute(None, "use"), Some("signing"));
}
#[test]
fn want_authn_requests_signed_false_emits_false() {
let cert = rsa_cert();
let sso = [Endpoint::post("https://idp.example.com/sso", 0, true)];
let formats = [NameIdFormat::Persistent];
let algos: [DataEncryptionAlgorithm; 0] = [];
let mut inputs = baseline_inputs(&cert, &sso, &[], &[], &formats, &algos);
inputs.want_authn_requests_signed = false;
let xml = emit_idp_metadata(&inputs, None).unwrap();
let doc = Document::parse(xml.as_bytes()).unwrap();
let idp_desc = doc
.root()
.child_element(Some(MD_NS), "IDPSSODescriptor")
.unwrap();
assert_eq!(
idp_desc.attribute(None, "WantAuthnRequestsSigned"),
Some("false")
);
}
}