Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-auth-saml/src/test_support.rs

//! # `test_support` — a fake IdP's signer, for tests only
//!
//! Lets a downstream crate run a SAML IdP in-process: build a Success
//! [`response_xml`], [`sign_response`] it the way real IdPs do (enveloped,
//! exclusive C14N, RSA-SHA256, the `Signature` right after the signed
//! element's `Issuer`), and post [`encode`]d bytes to the SP.
//!
//! The keys below are **throwaway TEST-ONLY keys, published on purpose**. They
//! sign nothing real; nothing may ever trust [`idp_cert_der`] outside a test.
//! The signer canonicalises with this crate's own C14N — the independent proof
//! that it agrees with libxml2 is `tests/sp_interop.rs`, not this module.

use base64::Engine as _;
use roxmltree::{Document, Node};
use sha2::Digest as _;

use crate::sp::{self, c14n, dsig, ASSERTION, DSIG, PROTOCOL};

/// TEST-ONLY RSA-2048 private key (PKCS#8 DER, base64), generated for this
/// crate's test support and published with it on purpose: it signs nothing
/// real and no deployment may trust its certificate.
const TEST_KEY_PKCS8_B64: &[&str] = &[
    "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDIBqWFSFMiQSURRJ5sNk3Vy32uDWGc/ntG+xQt/fxZf8It",
    "/dPN02u5ho7HXkQtV6SqJLl/t5lJUBrDfXKy21+4GQcb0b2U06dGvNVKlI8eTI9+jRalIwidFdawBEkwLTLTH3vtoWIMoFnj",
    "bgaWpM301imlIUt+DyuvBHH3i0yvY/q040ttZ/gqvY9LN6hnRNkt0CF5qO8ePTbJASIQMHUrO2gSLny/FDcGhiZMW9LVqkP6",
    "VtD2x263+c0l7Md994oB3WzXekp2SQ4/kDLGQGv1QTdc2EypB0km8IXDc9v1ce28KpiW4hBYjkKuoTKXKtw3yBTEqxHYa6st",
    "JAUhd9P7AgMBAAECggEAErYAdMPLtwgWAhQDp8a6qDWlV4s75tYtTPn+rMC2At8/GFNGpVnSGWQ7Ze3s/brVle7ihU504dVC",
    "86d/+7gmr49zyUfo7ouP+b9/Sj80Vha+U426IQ9yZlNdJlD4Ua/gsbwfuR5gECNN1Ub9Ccl31J/V/HLtX2jm4rPNP2VB8qQC",
    "uQEM3854xhnbdqV3m+5iTGOBuqGRAaE7Bot+ACQlN7ndkvHGNqpoqKb+SAHXhJsAfrITXXXAWcBRfFsTFVGjgH9NLZiXzfio",
    "G8qc2IT5LYG9CkMdCRbS+qr5e2IzhsPf/6mG1VV5zeLmxlPdDLrL4+OaBu9s43uVw0lvQqbdZQKBgQDrRxU4YHP0KpzqOrsR",
    "RNuZEUeAHLGXHd7W1a9/3I4XjzjSxpuP3oMJqPwP675qXZae1nsSgGkXfOitpSnkvUFPuFQ78IHqIaytg1Lojeyvf2fu//PP",
    "ige8eefZMe22ag8laOFKU145DsSTC8cbtZ+NXydTghu5ZczA3ACiCqT4PQKBgQDZpLoOa+BixKh2G14d71HrNIg1M/0kdp5c",
    "9ActKwv6haUdhOTJPSlKImczdk2gqD6G22I5ddd4DYBbi1gyDnTszH+e+1Dt3jTa/DmWrJcQOoN3l3PKjbn/fbPgmvmCD8j5",
    "qU7BYGt4Gw0z5XjBcH1bDYPsmowDHkUVKpubiv2IlwKBgQDUR7d6vXQBmq0nnUQJqcIlEnUbv0fwnA/icLy/NcWythPEAVR8",
    "c3xxq9l5l++fUr/Coqw6nkez3Iynl/z7mFKkViEST8c385g0meelUeB63A5vktptqBGg2bD5w9XCH2F5FG8pO50pCA0Oy7Kj",
    "arUBvhfifeL73UDnxdiAW/BK1QKBgFjdWJag12qcj4hB3mjsmY8m0OKgSqEDyd98SB3aMLbmsKdf5qaAUYaSi37ItCN/7p/T",
    "ncSGaCb9pEY8nBM8MeOqEAXS6ZVmIEC1sbHi4H9g1OMUtNOkkKhQJAz2WiAP3+iIh76PZvIQYZCqBmc9TgHKf+4woDB5wI1G",
    "62hUFPcbAoGACLzE63OelphHnXnuUgRz7uWqae40KjTUkZs+DwTVl96vb8fldYO10sUvT1smNj4jj/aGTKlsIuluHM9c7/Bl",
    "qOg23L59BQk3MT+ewmubrhWvXPnBMSQ55DgZC+4+bPAGy41A+WxsyjvefE/7DZ0UUuF7ogCt2nCw+1LloDopvN4=",
];

/// Its self-signed certificate (DER, base64), valid 2020–2060.
const TEST_CERT_B64: &[&str] = &[
    "MIIC3DCCAcSgAwIBAgICWjEwDQYJKoZIhvcNAQELBQAwMDEuMCwGA1UEAwwlbm9ybmlyLWF1dGgtc2FtbCB0ZXN0IElkUCAo",
    "VEVTVCBPTkxZKTAgFw0yMDAxMDEwMDAwMDBaGA8yMDYwMDEwMTAwMDAwMFowMDEuMCwGA1UEAwwlbm9ybmlyLWF1dGgtc2Ft",
    "bCB0ZXN0IElkUCAoVEVTVCBPTkxZKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgGpYVIUyJBJRFEnmw2TdXL",
    "fa4NYZz+e0b7FC39/Fl/wi39083Ta7mGjsdeRC1XpKokuX+3mUlQGsN9crLbX7gZBxvRvZTTp0a81UqUjx5Mj36NFqUjCJ0V",
    "1rAESTAtMtMfe+2hYgygWeNuBpakzfTWKaUhS34PK68EcfeLTK9j+rTjS21n+Cq9j0s3qGdE2S3QIXmo7x49NskBIhAwdSs7",
    "aBIufL8UNwaGJkxb0tWqQ/pW0PbHbrf5zSXsx333igHdbNd6SnZJDj+QMsZAa/VBN1zYTKkHSSbwhcNz2/Vx7bwqmJbiEFiO",
    "Qq6hMpcq3DfIFMSrEdhrqy0kBSF30/sCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEABDymEhUrUvaTz5xopGrvGsobiO4nTVVq",
    "BrShQjo+V8BAAJs9bmT8pmaNc1+J+L9/ZdeDBTD8G0ltVt8ZOCJvWV0HCTwf4QeLD1vN+C465hSIV8rslUhjXoJz9tgyrGdA",
    "MuJGCI6XVBJOH0xpEhaiMnoNFospRvYovakNjgn/de+K6Omql0ptmiFzqA+7XdV1Oq/Ew+wgvG8kKMk/9Wh2dZ+chIBCvjOG",
    "2tsZdfk8kAviaSxv48q8TrqWz3KWUyKFo3xrX3JTCBhvjo95G1XwDCRCMsZjVBAlZFU5Z/SBjjQuXL/Nz0ghIjVtAFx+Xn98",
    "LrylZvDqqtZvpBWfBEaubg==",
];

/// A SECOND throwaway key — the attacker's: its signatures must never verify
/// against [`idp_cert_der`]. TEST-ONLY, published on purpose.
const ATTACKER_KEY_PKCS8_B64: &[&str] = &[
    "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQClwHo4q1AvVJguaO7oGdgIX+vVSVH0mMm9SjRA+hPteAIl",
    "TO0KyA6ockN5Npka0tE3lotYI2sQQgVXVyEQUFIcd2EaPdiuPOw0dCn2aQEduRc56cl3Cb3mC/0cyy7L0xnBIq4UXW2Bp/NC",
    "CE6s9pp2pNQa+l2oK8mIp/Chp2Yck2Zs++vnk3FcyxE1RGwZGXFFX/q9R0KlyVN8m3SPQ0qMDi/qDGcP/ZZuOESlBJYk2vVt",
    "2d+rXch6CzvZQlI6hQ9dB7TgjY57I5XAWq9MdV05Z26I/KEOur8nFrRvCU7lyTH58MYkFGLWsvyBc6BtLWHGmg8YOWx00fRV",
    "St1bdrNbAgMBAAECggEAGgNuyyUL1RnMNoBuekvQDWRsgVFcBOzTwJr8P3RgG0VZ5NvBWg9SyZoBP+1TMxQCFNzSKreITolm",
    "CDKa9FH+yQvtV+576sCR+5t9mHgbXjxVyleIMF/9hzS7eUDkK8YKRTb5piWRkac3tNXT7y5WyKcOyNshRsr0ereMuvooOvSg",
    "Qwq9Dl1t7LkgAL26tManCu5tuhBoQbCWyi8c51DUkVa7d5q760YsyAObKSbYYOAak1m0euQvQn9XNW5LOmZIIb6ypi8uzyxw",
    "xmuxW/ql0Wa2WqFrEgSSduZv4/Zx6Hv2WziLsNtT8FdnrO8x/8ofaRJj7fdCrco2aJ+CZ7UyWQKBgQDjwDp+X9EowTBGMaIe",
    "fC26KQEVSfVvLavSDAORRULv6juJi0GwRVLVhXpxgrZIUfm/XsoBIAjWpykFiFxf7hFjUnP/sEbAe73pyg+TQT5Nyy/eiLUh",
    "xie5vLSUeq7hF4ugG62DVuD0Lv9RbT62gva4yX1XcjDEHaelO3JzUk36wwKBgQC6T5gyNC4/JWQkiEOM7nZERN3Q3frXj6O8",
    "Fbyo5nmq+72l5sUmkd32Tf6R95VDUJ8X+ysaQGXticscE2islB7ir3LbPCcrn2rZmasTfXHhBeRwHyXXew8VNWVtiYtgjcSJ",
    "XSnwK9FJNGsA07JSFwLsKQ8z0B34xPSEngXJcaRriQKBgH0sxfl3SEoSBCpQVT5UGf3bU5ET2SWGAI2ttLg/TQ73uklalHwB",
    "XgmlFysUIIOXKqEXxAbiSHlR3qX/0GWZP01LoB3EEN7yI+s+91iD3256YpD4qjUp5+OZ59ZDp6JngoI5NCCfu3v56LIkTPea",
    "+2eMtqs9Q7hGZEHYbosbjWhpAoGAVPAwnMK5SsSLMTSGR1v3wwv6bFynHVs49GiciAv67/6w6U+aFgluPfo00usiu3un7FDv",
    "x5oQXgjx9izLDdLBeesu29BCs1qMZgPuwIXyQvpiuNMad3B84dhkE1oflrdLIKy+j+YKFeu11LyukUo4ZN4bNi3tXBq+ty8r",
    "yDjtErECgYAkyJFjJ0b1/9WdyrG4JgMcjsPWHUp4zr3/0vcBOfavyHTodXycFvyPtAG+tVlA6mdOHfVH4yTgLUokG538qcJQ",
    "ReFXGj0mgDR/tSEif4xwrnv8fQuL9r1TZFAQkSpqLW6XnFMnvo/LzNapzM0xoOnR7ieZWVcM1ur77rkQqsNE+w==",
];

const ATTACKER_CERT_B64: &[&str] = &[
    "MIIC3DCCAcSgAwIBAgICWjEwDQYJKoZIhvcNAQELBQAwMDEuMCwGA1UEAwwlbm9ybmlyLWF1dGgtc2FtbCBBVFRBQ0tFUiAo",
    "VEVTVCBPTkxZKTAgFw0yMDAxMDEwMDAwMDBaGA8yMDYwMDEwMTAwMDAwMFowMDEuMCwGA1UEAwwlbm9ybmlyLWF1dGgtc2Ft",
    "bCBBVFRBQ0tFUiAoVEVTVCBPTkxZKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKXAejirUC9UmC5o7ugZ2Ahf",
    "69VJUfSYyb1KNED6E+14AiVM7QrIDqhyQ3k2mRrS0TeWi1gjaxBCBVdXIRBQUhx3YRo92K487DR0KfZpAR25FznpyXcJveYL",
    "/RzLLsvTGcEirhRdbYGn80IITqz2mnak1Br6XagryYin8KGnZhyTZmz76+eTcVzLETVEbBkZcUVf+r1HQqXJU3ybdI9DSowO",
    "L+oMZw/9lm44RKUEliTa9W3Z36tdyHoLO9lCUjqFD10HtOCNjnsjlcBar0x1XTlnboj8oQ66vycWtG8JTuXJMfnwxiQUYtay",
    "/IFzoG0tYcaaDxg5bHTR9FVK3Vt2s1sCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEApBfs9tzQrfm+f90xnotia6LZ8r2t8nZ3",
    "kwrLU2Fic9F0ohLsoM4r0UBbk7TYQ4Eb2njTA6Hnmv5enG6UYNF6DHlENhwW96OC/d9w1RQ9uwYhXR0u1upbSliybLQB9J3c",
    "nVJ2/g/AFlA1ZMLgs3dk9SZGZJKysoGipoI9yrsvPdfy5vENwVsWYXa2aglh9TSvXyrAhTZ+AXSOgXupHkxBKC++VfnDVN6r",
    "To8bM1ipu5HA4I9F4xAQBW4+jEdy9BdOVkcGlI/46m2jRNWBT8l9UJ6v1p3j5VXbKz8z60IV3fpiZoQzqw4rXwsvzWzUEU/B",
    "FaeZaEiX8w5HwUOuw2loeQ==",
];

fn joined(parts: &[&str]) -> Vec<u8> {
    base64::engine::general_purpose::STANDARD
        .decode(parts.concat())
        .expect("the embedded test key is base64")
}

/// The test IdP's certificate (DER) — what an SP under test is configured with.
pub fn idp_cert_der() -> Vec<u8> {
    joined(TEST_CERT_B64)
}

/// The test IdP's certificate as one base64 line (an `X509Certificate`).
pub fn idp_cert_b64() -> String {
    TEST_CERT_B64.concat()
}

/// The attacker's key and certificate: `(pkcs8_der, cert_der)`.
pub fn attacker_key() -> (Vec<u8>, Vec<u8>) {
    (joined(ATTACKER_KEY_PKCS8_B64), joined(ATTACKER_CERT_B64))
}

/// The test IdP's own `(pkcs8_der, cert_der)`.
pub fn idp_key() -> (Vec<u8>, Vec<u8>) {
    (joined(TEST_KEY_PKCS8_B64), joined(TEST_CERT_B64))
}

/// IdP metadata for the test IdP: `entity_id`, an HTTP-Redirect SSO endpoint
/// at `sso_url`, and [`idp_cert_der`] as its signing certificate.
pub fn idp_metadata_xml(entity_id: &str, sso_url: &str) -> String {
    format!(
        "<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"{}\">\
         <md:IDPSSODescriptor protocolSupportEnumeration=\"{PROTOCOL}\">\
         <md:KeyDescriptor use=\"signing\"><ds:KeyInfo xmlns:ds=\"{DSIG}\"><ds:X509Data>\
         <ds:X509Certificate>{}</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor>\
         <md:SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"{}\"/>\
         </md:IDPSSODescriptor></md:EntityDescriptor>",
        sp::escape(entity_id),
        idp_cert_b64(),
        sp::escape(sso_url),
    )
}

/// Which element(s) [`sign_response`] signs.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Sign {
    /// The Assertion (Okta's, Keycloak's, ADFS's default).
    Assertion,
    /// The Response only (an Entra ID option).
    Response,
    /// The Assertion, then the Response over it.
    Both,
}

/// What [`response_xml`] writes. [`ResponseParams::new`] fills a valid one.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResponseParams {
    pub response_id: String,
    pub assertion_id: String,
    pub issuer: String,
    pub name_id: String,
    pub name_id_format: Option<String>,
    /// The SP's entity id.
    pub audience: String,
    /// The Response's Destination; `None` writes none.
    pub destination: Option<String>,
    /// The bearer confirmation's Recipient.
    pub recipient: String,
    pub in_response_to: Option<String>,
    pub issue_instant: i64,
    pub not_before: i64,
    pub not_on_or_after: i64,
    pub session_index: Option<String>,
    pub attributes: Vec<(String, Vec<String>)>,
}

impl ResponseParams {
    /// A response the SP at `acs_url` (entity `sp_entity_id`) accepts at
    /// `now_unix`: valid from a minute ago for five minutes, no
    /// `InResponseTo` (set it for an SP-initiated sign-in).
    pub fn new(
        idp_entity_id: &str,
        name_id: &str,
        sp_entity_id: &str,
        acs_url: &str,
        now_unix: i64,
    ) -> Self {
        ResponseParams {
            response_id: format!("_r{now_unix:x}"),
            assertion_id: format!("_a{now_unix:x}"),
            issuer: idp_entity_id.to_owned(),
            name_id: name_id.to_owned(),
            name_id_format: Some(
                "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress".to_owned(),
            ),
            audience: sp_entity_id.to_owned(),
            destination: Some(acs_url.to_owned()),
            recipient: acs_url.to_owned(),
            in_response_to: None,
            issue_instant: now_unix,
            not_before: now_unix - 60,
            not_on_or_after: now_unix + 300,
            session_index: Some(format!("_s{now_unix:x}")),
            attributes: Vec::new(),
        }
    }
}

/// An UNSIGNED Success Response with one Assertion.
pub fn response_xml(p: &ResponseParams) -> String {
    let e = sp::escape;
    let instant = sp::format_instant;
    let in_response = p
        .in_response_to
        .as_ref()
        .map(|r| format!(" InResponseTo=\"{}\"", e(r)))
        .unwrap_or_default();
    let destination = p
        .destination
        .as_ref()
        .map(|d| format!(" Destination=\"{}\"", e(d)))
        .unwrap_or_default();
    let format = p
        .name_id_format
        .as_ref()
        .map(|f| format!(" Format=\"{}\"", e(f)))
        .unwrap_or_default();
    let session = p
        .session_index
        .as_ref()
        .map(|s| format!(" SessionIndex=\"{}\"", e(s)))
        .unwrap_or_default();
    let mut attributes = String::new();
    if !p.attributes.is_empty() {
        attributes.push_str("<saml:AttributeStatement>");
        for (name, values) in &p.attributes {
            attributes.push_str(&format!("<saml:Attribute Name=\"{}\">", e(name)));
            for v in values {
                attributes.push_str(&format!(
                    "<saml:AttributeValue>{}</saml:AttributeValue>",
                    e(v)
                ));
            }
            attributes.push_str("</saml:Attribute>");
        }
        attributes.push_str("</saml:AttributeStatement>");
    }
    format!(
        "<samlp:Response xmlns:samlp=\"{PROTOCOL}\" xmlns:saml=\"{ASSERTION}\" ID=\"{rid}\" Version=\"2.0\" \
         IssueInstant=\"{ii}\"{destination}{in_response}>\
         <saml:Issuer>{issuer}</saml:Issuer>\
         <samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/></samlp:Status>\
         <saml:Assertion ID=\"{aid}\" Version=\"2.0\" IssueInstant=\"{ii}\">\
         <saml:Issuer>{issuer}</saml:Issuer>\
         <saml:Subject><saml:NameID{format}>{name_id}</saml:NameID>\
         <saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\">\
         <saml:SubjectConfirmationData NotOnOrAfter=\"{nooa}\" Recipient=\"{recipient}\"{in_response}/>\
         </saml:SubjectConfirmation></saml:Subject>\
         <saml:Conditions NotBefore=\"{nb}\" NotOnOrAfter=\"{nooa}\">\
         <saml:AudienceRestriction><saml:Audience>{audience}</saml:Audience></saml:AudienceRestriction>\
         </saml:Conditions>\
         <saml:AuthnStatement AuthnInstant=\"{ii}\"{session}><saml:AuthnContext><saml:AuthnContextClassRef>\
         urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>\
         </saml:AuthnContext></saml:AuthnStatement>{attributes}\
         </saml:Assertion></samlp:Response>",
        rid = e(&p.response_id),
        aid = e(&p.assertion_id),
        ii = instant(p.issue_instant),
        nb = instant(p.not_before),
        nooa = instant(p.not_on_or_after),
        issuer = e(&p.issuer),
        name_id = e(&p.name_id),
        recipient = e(&p.recipient),
        audience = e(&p.audience),
    )
}

/// `SAMLResponse`: the document base64-encoded, as the IdP's form posts it.
pub fn encode(xml: &str) -> String {
    base64::engine::general_purpose::STANDARD.encode(xml.as_bytes())
}

/// Sign `xml` with the test IdP's key. Panics on a document that is not a
/// Response with an Issuer in each element to sign (a test bug, not a case).
pub fn sign_response(xml: &str, sign: Sign) -> String {
    let (key, cert) = idp_key();
    sign_response_with_key(xml, sign, &key, &cert)
}

/// [`sign_response`] with another key (PKCS#8 DER) and certificate (DER) —
/// the attacker's, in a test that must see it refused.
pub fn sign_response_with_key(xml: &str, sign: Sign, pkcs8_der: &[u8], cert_der: &[u8]) -> String {
    match sign {
        Sign::Assertion => sign_element(xml, ASSERTION, "Assertion", pkcs8_der, cert_der),
        Sign::Response => sign_element(xml, PROTOCOL, "Response", pkcs8_der, cert_der),
        Sign::Both => {
            let once = sign_element(xml, ASSERTION, "Assertion", pkcs8_der, cert_der);
            sign_element(&once, PROTOCOL, "Response", pkcs8_der, cert_der)
        }
    }
}

const PLACEHOLDER: &str = "@@SIGNATURE-VALUE@@";

fn find<'a, 'i>(doc: &'a Document<'i>, ns: &str, local: &str) -> Node<'a, 'i> {
    doc.descendants()
        .find(|n| {
            n.is_element() && n.tag_name().namespace() == Some(ns) && n.tag_name().name() == local
        })
        .unwrap_or_else(|| panic!("the test document has no <{local}>"))
}

fn sign_element(xml: &str, ns: &str, local: &str, pkcs8_der: &[u8], cert_der: &[u8]) -> String {
    use rsa::pkcs8::DecodePrivateKey as _;
    use rsa::signature::{SignatureEncoding as _, Signer as _};
    let b64 = |b: &[u8]| base64::engine::general_purpose::STANDARD.encode(b);

    let doc = Document::parse(xml).expect("the test document parses");
    let element = find(&doc, ns, local);
    let id = element
        .attribute("ID")
        .expect("the element to sign has an ID")
        .to_owned();
    let digest = b64(&sha2::Sha256::digest(
        c14n::exclusive(&doc, element, &[], None).as_bytes(),
    ));
    let issuer = element
        .children()
        .find(|n| {
            n.is_element()
                && n.tag_name().namespace() == Some(ASSERTION)
                && n.tag_name().name() == "Issuer"
        })
        .expect("the element to sign has an Issuer (the Signature follows it)");
    let at = issuer.range().end;
    let signature = format!(
        "<ds:Signature xmlns:ds=\"{DSIG}\"><ds:SignedInfo>\
         <ds:CanonicalizationMethod Algorithm=\"{exc}\"/>\
         <ds:SignatureMethod Algorithm=\"{rsa}\"/>\
         <ds:Reference URI=\"#{id}\"><ds:Transforms>\
         <ds:Transform Algorithm=\"{env}\"/><ds:Transform Algorithm=\"{exc}\"/>\
         </ds:Transforms><ds:DigestMethod Algorithm=\"{sha}\"/>\
         <ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo>\
         <ds:SignatureValue>{PLACEHOLDER}</ds:SignatureValue>\
         <ds:KeyInfo><ds:X509Data><ds:X509Certificate>{cert}</ds:X509Certificate></ds:X509Data></ds:KeyInfo>\
         </ds:Signature>",
        exc = dsig::EXC_C14N,
        rsa = dsig::RSA_SHA256,
        env = dsig::ENVELOPED,
        sha = dsig::SHA256,
        id = sp::escape(&id),
        cert = b64(cert_der),
    );
    let with = format!("{}{}{}", &xml[..at], signature, &xml[at..]);

    let doc = Document::parse(&with).expect("the signed test document parses");
    let signed = doc
        .descendants()
        .find(|n| n.is_element() && n.attribute("ID") == Some(id.as_str()))
        .expect("the signed element is still there");
    let signed_info = signed
        .children()
        .find(|n| {
            n.is_element()
                && n.tag_name().namespace() == Some(DSIG)
                && n.tag_name().name() == "Signature"
        })
        .and_then(|s| {
            s.children()
                .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
        })
        .expect("the Signature just inserted");
    let canonical = c14n::exclusive(&doc, signed_info, &[], None);
    let key = rsa::RsaPrivateKey::from_pkcs8_der(pkcs8_der).expect("a PKCS#8 RSA test key");
    let value = rsa::pkcs1v15::SigningKey::<sha2::Sha256>::new(key)
        .sign(canonical.as_bytes())
        .to_vec();
    with.replacen(PLACEHOLDER, &b64(&value), 1)
}