use subtle::ConstantTimeEq;
use crate::crypto::cert::X509Certificate;
use crate::crypto::verifier::{DefaultVerifier, KeyInfo, SignatureVerifier, VerifyMatch};
use crate::dsig::algorithms::{C14nAlgorithm, SignatureAlgorithm};
use crate::dsig::c14n::canonicalize;
use crate::dsig::reference::{
DS_NS, ancestor_chain, compute_reference_digest, decode_base64_lenient, parse_reference,
};
use crate::error::Error;
use crate::xml::parse::{Document, Element, ElementId};
#[derive(Debug, Clone)]
pub struct VerifiedSignature {
pub signed_element: ElementId,
pub verifying_cert_fingerprint: [u8; 32],
pub signature_algorithm: SignatureAlgorithm,
}
pub(crate) fn verify_signature(
document: &Document,
signature_element: &Element,
candidate_certs: &[X509Certificate],
allowed_algorithms: &[SignatureAlgorithm],
) -> Result<VerifiedSignature, Error> {
verify_signature_with(
document,
signature_element,
candidate_certs,
allowed_algorithms,
&DefaultVerifier,
)
}
pub(crate) fn verify_signature_with(
document: &Document,
signature_element: &Element,
candidate_certs: &[X509Certificate],
allowed_algorithms: &[SignatureAlgorithm],
verifier: &dyn SignatureVerifier,
) -> Result<VerifiedSignature, Error> {
let signed_info = signature_element
.child_element(Some(DS_NS), "SignedInfo")
.ok_or(Error::SignatureVerification {
reason: "missing SignedInfo",
})?;
let c14n_alg = parse_c14n_method(signed_info)?;
let sig_alg = parse_signature_method(signed_info, allowed_algorithms)?;
let reference_elem = single_reference(signed_info)?;
let parsed = parse_reference(document, reference_elem)?;
let computed_digest = compute_reference_digest(document, &parsed, signature_element.id())?;
if !bool::from(computed_digest.ct_eq(&parsed.digest_value)) {
return Err(Error::SignatureVerification {
reason: "digest mismatch",
});
}
let signed_info_id = signed_info.id();
let signed_info_chain =
ancestor_chain(document, signed_info_id).ok_or(Error::SignatureVerification {
reason: "could not compute SignedInfo ancestor chain",
})?;
let signed_info_bytes = canonicalize(document, signed_info, &signed_info_chain, c14n_alg, &[])?;
let signature_value_elem = signature_element
.child_element(Some(DS_NS), "SignatureValue")
.ok_or(Error::SignatureVerification {
reason: "missing SignatureValue",
})?;
let signature_bytes = decode_base64_lenient(&signature_value_elem.text_content())?;
let key_info = extract_key_info(signature_element);
let vmatch = verifier.verify(
sig_alg,
&signed_info_bytes,
&signature_bytes,
candidate_certs,
allowed_algorithms,
&key_info,
)?;
Ok(VerifiedSignature {
signed_element: parsed.target,
verifying_cert_fingerprint: vmatch.cert_fingerprint,
signature_algorithm: vmatch.algorithm,
})
}
pub(crate) fn verify_detached_signature(
signed_query_string: &[u8],
signature_bytes: &[u8],
sig_alg: SignatureAlgorithm,
candidate_certs: &[X509Certificate],
allowed_algorithms: &[SignatureAlgorithm],
) -> Result<VerifyMatch, Error> {
verify_detached_signature_with(
signed_query_string,
signature_bytes,
sig_alg,
candidate_certs,
allowed_algorithms,
&DefaultVerifier,
)
}
pub(crate) fn verify_detached_signature_with(
signed_query_string: &[u8],
signature_bytes: &[u8],
sig_alg: SignatureAlgorithm,
candidate_certs: &[X509Certificate],
allowed_algorithms: &[SignatureAlgorithm],
verifier: &dyn SignatureVerifier,
) -> Result<VerifyMatch, Error> {
if !allowed_algorithms.contains(&sig_alg) {
return Err(Error::DisallowedAlgorithm {
alg: sig_alg.uri().to_owned(),
});
}
verifier.verify(
sig_alg,
signed_query_string,
signature_bytes,
candidate_certs,
allowed_algorithms,
&KeyInfo::default(),
)
}
fn parse_c14n_method(signed_info: &Element) -> Result<C14nAlgorithm, Error> {
let elem = signed_info
.child_element(Some(DS_NS), "CanonicalizationMethod")
.ok_or(Error::SignatureVerification {
reason: "missing CanonicalizationMethod",
})?;
let uri = elem
.attribute(None, "Algorithm")
.ok_or(Error::SignatureVerification {
reason: "CanonicalizationMethod missing Algorithm",
})?;
C14nAlgorithm::from_uri(uri)
}
fn parse_signature_method(
signed_info: &Element,
allowed_algorithms: &[SignatureAlgorithm],
) -> Result<SignatureAlgorithm, Error> {
let elem = signed_info
.child_element(Some(DS_NS), "SignatureMethod")
.ok_or(Error::SignatureVerification {
reason: "missing SignatureMethod",
})?;
let uri = elem
.attribute(None, "Algorithm")
.ok_or(Error::SignatureVerification {
reason: "SignatureMethod missing Algorithm",
})?;
let alg = SignatureAlgorithm::from_uri(uri)?;
if !allowed_algorithms.contains(&alg) {
return Err(Error::DisallowedAlgorithm {
alg: uri.to_owned(),
});
}
Ok(alg)
}
fn single_reference(signed_info: &Element) -> Result<&Element, Error> {
let mut iter = signed_info.all_child_elements(Some(DS_NS), "Reference");
let first = iter.next().ok_or(Error::SignatureVerification {
reason: "SignedInfo has no Reference",
})?;
if iter.next().is_some() {
return Err(Error::SignatureVerification {
reason: "multi-reference signature rejected (XSW vector)",
});
}
Ok(first)
}
fn extract_key_info(signature_element: &Element) -> KeyInfo {
let Some(key_info_elem) = signature_element.child_element(Some(DS_NS), "KeyInfo") else {
return KeyInfo::default();
};
parse_key_info_element(key_info_elem)
}
pub(crate) fn parse_key_info_element(key_info_elem: &Element) -> KeyInfo {
let mut out = KeyInfo::default();
if let Some(name_elem) = key_info_elem.child_element(Some(DS_NS), "KeyName") {
out.key_name = Some(name_elem.text_content());
}
for x509_data in key_info_elem.all_child_elements(Some(DS_NS), "X509Data") {
for cert_elem in x509_data.all_child_elements(Some(DS_NS), "X509Certificate") {
out.x509_certificates_base64.push(cert_elem.text_content());
}
for subject in x509_data.all_child_elements(Some(DS_NS), "X509SubjectName") {
out.x509_subject_names.push(subject.text_content());
}
for serial in x509_data.all_child_elements(Some(DS_NS), "X509IssuerSerial") {
let issuer = serial
.child_element(Some(DS_NS), "X509IssuerName")
.map(Element::text_content)
.unwrap_or_default();
let serial_number = serial
.child_element(Some(DS_NS), "X509SerialNumber")
.map(Element::text_content)
.unwrap_or_default();
out.x509_issuer_serials.push((issuer, serial_number));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use crate::crypto::cert::test_vectors::*;
use crate::crypto::keypair::KeyPair;
use crate::dsig::algorithms::DigestAlgorithm;
use crate::xml::parse::Document;
fn sign_test_root(
target_id: &str,
body_xml: &str,
sig_alg: SignatureAlgorithm,
c14n_alg: C14nAlgorithm,
) -> (String, X509Certificate) {
let kp = KeyPair::from_pkcs8_pem(RSA_KEY_PKCS8_PEM).unwrap();
let cert = X509Certificate::from_pem(RSA_CERT_PEM).unwrap();
let stage_1_xml = format!(
r#"<Root xmlns="urn:p" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ID="{target_id}">{body_xml}</Root>"#
);
let stage_1_doc = Document::parse(stage_1_xml.as_bytes()).unwrap();
let chain_1 = ancestor_chain(&stage_1_doc, stage_1_doc.root().id()).unwrap();
let canonical_root =
canonicalize(&stage_1_doc, stage_1_doc.root(), &chain_1, c14n_alg, &[]).unwrap();
let reference_digest = DigestAlgorithm::Sha256.digest(&canonical_root);
let reference_digest_b64 = BASE64_STANDARD.encode(&reference_digest);
let signed_info_inner = format!(
r##"<ds:CanonicalizationMethod Algorithm="{c14n}"/><ds:SignatureMethod Algorithm="{sig}"/><ds:Reference URI="#{id}"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="{c14n}"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference>"##,
c14n = c14n_alg.uri(),
sig = sig_alg.uri(),
id = target_id,
digest = reference_digest_b64,
);
let signed_info_xml = format!(
r#"<ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">{signed_info_inner}</ds:SignedInfo>"#
);
let signed_info_doc = Document::parse(signed_info_xml.as_bytes()).unwrap();
let signed_info_chain =
ancestor_chain(&signed_info_doc, signed_info_doc.root().id()).unwrap();
let signed_info_canonical = canonicalize(
&signed_info_doc,
signed_info_doc.root(),
&signed_info_chain,
c14n_alg,
&[],
)
.unwrap();
let sig_bytes = kp.sign(sig_alg, &signed_info_canonical).unwrap();
let sig_b64 = BASE64_STANDARD.encode(&sig_bytes);
let cert_b64 = cert.to_base64_x509();
let body = body_xml;
let si_inner = &signed_info_inner;
let sig = &sig_b64;
let cert_text = &cert_b64;
let final_xml = format!(
r#"<Root xmlns="urn:p" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ID="{target_id}">{body}<ds:Signature><ds:SignedInfo>{si_inner}</ds:SignedInfo><ds:SignatureValue>{sig}</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>{cert_text}</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature></Root>"#
);
(final_xml, cert)
}
#[test]
fn happy_path_verifies_rsa_sha256() {
let (xml, cert) = sign_test_root(
"root-1",
"<Inner>payload</Inner>",
SignatureAlgorithm::RsaSha256,
C14nAlgorithm::ExclusiveCanonical,
);
let doc = Document::parse(xml.as_bytes()).unwrap();
let sig_elem = doc
.find_first(Some(DS_NS), "Signature")
.expect("ds:Signature");
let verified = verify_signature(
&doc,
sig_elem,
std::slice::from_ref(&cert),
&[SignatureAlgorithm::RsaSha256],
)
.expect("should verify");
assert_eq!(verified.signed_element, doc.root().id());
assert_eq!(verified.signature_algorithm, SignatureAlgorithm::RsaSha256);
assert_eq!(
verified.verifying_cert_fingerprint,
cert.fingerprint_sha256()
);
}
#[test]
fn rejects_when_signature_algorithm_not_in_allow_list() {
let (xml, cert) = sign_test_root(
"root-1",
"<Inner>payload</Inner>",
SignatureAlgorithm::RsaSha256,
C14nAlgorithm::ExclusiveCanonical,
);
let doc = Document::parse(xml.as_bytes()).unwrap();
let sig_elem = doc.find_first(Some(DS_NS), "Signature").unwrap();
let err = verify_signature(
&doc,
sig_elem,
&[cert],
&[SignatureAlgorithm::RsaSha512],
)
.expect_err("should reject disallowed algorithm");
assert!(matches!(err, Error::DisallowedAlgorithm { .. }));
}
#[test]
fn rejects_when_digest_mismatches_after_tampering() {
let (xml, cert) = sign_test_root(
"root-1",
"<Inner>payload</Inner>",
SignatureAlgorithm::RsaSha256,
C14nAlgorithm::ExclusiveCanonical,
);
let tampered = xml.replace("payload", "TAMPERED");
let doc = Document::parse(tampered.as_bytes()).unwrap();
let sig_elem = doc.find_first(Some(DS_NS), "Signature").unwrap();
let err = verify_signature(&doc, sig_elem, &[cert], &[SignatureAlgorithm::RsaSha256])
.expect_err("should reject tampered digest");
assert!(
matches!(
err,
Error::SignatureVerification {
reason: "digest mismatch"
}
),
"got: {err:?}"
);
}
#[test]
fn rejects_multi_reference_signature() {
let xml = r##"<Root xmlns="urn:p" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ID="root-1">
<Inner ID="a">payload</Inner>
<Inner2 ID="b">other</Inner2>
<ds:Signature>
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#a">
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>AAAA</ds:DigestValue>
</ds:Reference>
<ds:Reference URI="#b">
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>AAAA</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>AAAA</ds:SignatureValue>
</ds:Signature>
</Root>"##;
let doc = Document::parse(xml.as_bytes()).unwrap();
let sig_elem = doc.find_first(Some(DS_NS), "Signature").unwrap();
let err = verify_signature(&doc, sig_elem, &[], &[SignatureAlgorithm::RsaSha256])
.expect_err("multi-reference must be rejected");
assert!(
matches!(
err,
Error::SignatureVerification {
reason: "multi-reference signature rejected (XSW vector)"
}
),
"got: {err:?}"
);
}
#[test]
fn rejects_disallowed_transform_in_reference() {
let xml = r##"<Root xmlns="urn:p" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ID="root-1">
<Inner>payload</Inner>
<ds:Signature>
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#root-1">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xslt-19991116"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>AAAA</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>AAAA</ds:SignatureValue>
</ds:Signature>
</Root>"##;
let doc = Document::parse(xml.as_bytes()).unwrap();
let sig_elem = doc.find_first(Some(DS_NS), "Signature").unwrap();
let err = verify_signature(&doc, sig_elem, &[], &[SignatureAlgorithm::RsaSha256])
.expect_err("XSLT transform must be rejected");
assert!(matches!(err, Error::DisallowedTransform { .. }));
}
#[test]
fn rejects_missing_signed_info() {
let xml = r#"<Root xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:Signature/></Root>"#;
let doc = Document::parse(xml.as_bytes()).unwrap();
let sig_elem = doc.find_first(Some(DS_NS), "Signature").unwrap();
let err = verify_signature(&doc, sig_elem, &[], &[SignatureAlgorithm::RsaSha256])
.expect_err("missing SignedInfo must be rejected");
assert!(matches!(
err,
Error::SignatureVerification {
reason: "missing SignedInfo"
}
));
}
#[test]
fn rejects_unknown_signature_method() {
let xml = r#"<Root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:Signature>
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://example.com/unknown"/>
<ds:Reference><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>AAAA</ds:DigestValue></ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>AAAA</ds:SignatureValue>
</ds:Signature>
</Root>"#;
let doc = Document::parse(xml.as_bytes()).unwrap();
let sig_elem = doc.find_first(Some(DS_NS), "Signature").unwrap();
let err = verify_signature(&doc, sig_elem, &[], &[SignatureAlgorithm::RsaSha256])
.expect_err("unknown SignatureMethod must be rejected");
assert!(matches!(err, Error::DisallowedAlgorithm { .. }));
}
#[test]
fn verify_signature_extracts_inline_keyinfo() {
let (xml, cert) = sign_test_root(
"root-1",
"<Inner>payload</Inner>",
SignatureAlgorithm::RsaSha256,
C14nAlgorithm::ExclusiveCanonical,
);
let doc = Document::parse(xml.as_bytes()).unwrap();
let sig_elem = doc.find_first(Some(DS_NS), "Signature").unwrap();
let key_info = extract_key_info(sig_elem);
assert_eq!(key_info.x509_certificates_base64.len(), 1);
let trusted = key_info.trusted_inline_certs(std::slice::from_ref(&cert));
assert_eq!(trusted.len(), 1);
assert_eq!(trusted[0], cert);
}
#[test]
fn detached_signature_verifies_success() {
let kp = KeyPair::from_pkcs8_pem(RSA_KEY_PKCS8_PEM).unwrap();
let cert = X509Certificate::from_pem(RSA_CERT_PEM).unwrap();
let qs = b"SAMLRequest=abc&RelayState=xyz&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256";
let sig = kp.sign(SignatureAlgorithm::RsaSha256, qs).unwrap();
let m = verify_detached_signature(
qs,
&sig,
SignatureAlgorithm::RsaSha256,
std::slice::from_ref(&cert),
&[SignatureAlgorithm::RsaSha256],
)
.expect("detached signature should verify");
assert_eq!(m.cert_fingerprint, cert.fingerprint_sha256());
assert_eq!(m.algorithm, SignatureAlgorithm::RsaSha256);
}
#[test]
fn detached_signature_rejects_bit_flip_in_signature() {
let kp = KeyPair::from_pkcs8_pem(RSA_KEY_PKCS8_PEM).unwrap();
let cert = X509Certificate::from_pem(RSA_CERT_PEM).unwrap();
let qs = b"SAMLRequest=abc&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256";
let mut sig = kp.sign(SignatureAlgorithm::RsaSha256, qs).unwrap();
sig[0] ^= 0x01;
let err = verify_detached_signature(
qs,
&sig,
SignatureAlgorithm::RsaSha256,
&[cert],
&[SignatureAlgorithm::RsaSha256],
)
.expect_err("tampered signature must be rejected");
assert!(matches!(
err,
Error::SignatureVerification {
reason: "no candidate cert matched"
}
));
}
#[test]
fn detached_signature_rejects_when_alg_not_in_allow_list() {
let kp = KeyPair::from_pkcs8_pem(RSA_KEY_PKCS8_PEM).unwrap();
let cert = X509Certificate::from_pem(RSA_CERT_PEM).unwrap();
let qs = b"SAMLRequest=abc";
let sig = kp.sign(SignatureAlgorithm::RsaSha256, qs).unwrap();
let err = verify_detached_signature(
qs,
&sig,
SignatureAlgorithm::RsaSha256,
&[cert],
&[SignatureAlgorithm::RsaSha512],
)
.expect_err("disallowed algorithm must be rejected");
assert!(matches!(err, Error::DisallowedAlgorithm { .. }));
}
#[test]
fn detached_signature_rejects_query_string_tampering() {
let kp = KeyPair::from_pkcs8_pem(RSA_KEY_PKCS8_PEM).unwrap();
let cert = X509Certificate::from_pem(RSA_CERT_PEM).unwrap();
let qs_signed = b"SAMLRequest=abc&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256";
let qs_tampered = b"SAMLRequest=XYZ&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256";
let sig = kp.sign(SignatureAlgorithm::RsaSha256, qs_signed).unwrap();
let err = verify_detached_signature(
qs_tampered,
&sig,
SignatureAlgorithm::RsaSha256,
&[cert],
&[SignatureAlgorithm::RsaSha256],
)
.expect_err("query string tampering must be rejected");
assert!(matches!(
err,
Error::SignatureVerification {
reason: "no candidate cert matched"
}
));
}
#[test]
fn verified_signed_element_routes_through_element_id() {
let (xml, cert) = sign_test_root(
"root-1",
"<Inner>payload</Inner>",
SignatureAlgorithm::RsaSha256,
C14nAlgorithm::ExclusiveCanonical,
);
let doc = Document::parse(xml.as_bytes()).unwrap();
let sig_elem = doc.find_first(Some(DS_NS), "Signature").unwrap();
let verified = verify_signature(&doc, sig_elem, &[cert], &[SignatureAlgorithm::RsaSha256])
.expect("should verify");
let resolved = doc.element(verified.signed_element).expect("element by id");
assert_eq!(resolved.qname().local(), "Root");
let by_id = doc.element_by_id_attr("root-1").unwrap();
assert_eq!(verified.signed_element, by_id);
}
}