mod common;
use std::io;
use std::process::{Command, Output};
fn xmlsec1_available() -> bool {
Command::new("xmlsec1")
.arg("--version")
.output()
.is_ok_and(|o| o.status.success())
}
fn verify_with_xmlsec1(xml: &str, id_node: &str, tag: &str) -> io::Result<Output> {
let dir = std::env::temp_dir();
let pid = std::process::id();
let doc_path = dir.join(format!("saml-xmlsec-{pid}-{tag}.xml"));
let cert_path = dir.join(format!("saml-xmlsec-{pid}-{tag}-cert.pem"));
std::fs::write(&doc_path, xml)?;
std::fs::write(&cert_path, common::RSA_CERT_PEM)?;
let output = Command::new("xmlsec1")
.arg("--verify")
.arg("--enabled-key-data")
.arg("x509")
.arg("--trusted-pem")
.arg(&cert_path)
.arg("--id-attr:ID")
.arg(id_node)
.arg(&doc_path)
.output();
for path in [&doc_path, &cert_path] {
if std::fs::remove_file(path).is_err() {
}
}
output
}
#[test]
fn xmlsec1_accepts_signed_idp_metadata() {
if !xmlsec1_available() {
eprintln!("skipping: xmlsec1 not on PATH");
return;
}
let idp = common::make_idp(
"https://idp.example.com/saml",
"https://idp.example.com/sso",
)
.expect("idp builds");
let xml = idp.metadata_xml(true).expect("signed idp metadata");
let out = verify_with_xmlsec1(&xml, "EntityDescriptor", "idp").expect("run xmlsec1");
assert!(
out.status.success(),
"xmlsec1 rejected our IdP metadata signature.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}
#[test]
fn xmlsec1_accepts_signed_sp_metadata() {
if !xmlsec1_available() {
eprintln!("skipping: xmlsec1 not on PATH");
return;
}
let sp = common::make_sp(
"https://sp.example.com/saml",
"https://sp.example.com/acs",
true,
)
.expect("sp builds");
let xml = sp.metadata_xml(true).expect("signed sp metadata");
let out = verify_with_xmlsec1(&xml, "EntityDescriptor", "sp").expect("run xmlsec1");
assert!(
out.status.success(),
"xmlsec1 rejected our SP metadata signature.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}
#[test]
fn xmlsec1_rejects_tampered_idp_metadata() {
if !xmlsec1_available() {
eprintln!("skipping: xmlsec1 not on PATH");
return;
}
let idp = common::make_idp(
"https://idp.example.com/saml",
"https://idp.example.com/sso",
)
.expect("idp builds");
let xml = idp.metadata_xml(true).expect("signed idp metadata");
let tampered = xml.replace(
"https://idp.example.com/saml",
"https://evil.example.com/saml",
);
assert_ne!(tampered, xml, "tamper precondition: entityID present");
let out =
verify_with_xmlsec1(&tampered, "EntityDescriptor", "idp-tampered").expect("run xmlsec1");
assert!(
!out.status.success(),
"xmlsec1 accepted a tampered signature — the cross-check is vacuous.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}