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)
}