#![cfg(feature = "xmlenc")]
use std::fs;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use roxmltree::Node;
use rsa::{
RsaPrivateKey, RsaPublicKey,
pkcs8::{DecodePrivateKey, DecodePublicKey},
};
use xml_sec::xmlenc::{
DataEncryptionAlgorithm, DecryptedContent, DocumentEncryptionOptions, EncryptedDataBuilder,
EncryptedDataType, EncryptionRecipient, KekDecryptor, KeyTransportAlgorithm, KeyWrapAlgorithm,
OaepDigestAlgorithm, PrivateKeyDecryptor, RsaOaepParameters, SymmetricKeyDecryptor,
XmlEncError, decrypt, decrypt_document, parse_encrypted_data,
};
const DONOR_DIR: &str = "tests/fixtures/xmlenc/aleksey-xmlenc-01";
const RSA_2048_PRIVATE: &str = "tests/fixtures/keys/rsa/rsa-2048-key.pem";
const RSA_2048_PUBLIC: &str = "tests/fixtures/keys/rsa/rsa-2048-pubkey.pem";
const RSA_4096_PRIVATE: &str = "tests/fixtures/keys/rsa/rsa-4096-key.pem";
const RSA_4096_PUBLIC: &str = "tests/fixtures/keys/rsa/rsa-4096-pubkey.pem";
#[cfg(feature = "legacy-algorithms")]
#[test]
fn direct_aes_key_cannot_be_used_for_tripledes() {
use xml_sec::xmlenc::DecryptionKeyResolver;
assert!(
SymmetricKeyDecryptor::new([0x31; 24])
.resolve_key(
xml_sec::provider::default_provider(),
DataEncryptionAlgorithm::TripleDesCbc,
None
)
.is_err()
);
}
#[cfg(feature = "legacy-algorithms")]
#[test]
fn aes_kek_cannot_be_used_for_tripledes_wrap() {
use xml_sec::policy::EncryptionPolicy;
use xml_sec::xmlenc::DecryptionKeyResolver;
let kek = vec![0x31; 24];
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.add_recipient(EncryptionRecipient::aes_key_wrap(
kek.clone(),
KeyWrapAlgorithm::TripleDes,
))
.policy(EncryptionPolicy {
key_wrap_algorithms: Some([KeyWrapAlgorithm::TripleDes].into()),
..EncryptionPolicy::default()
})
.encrypt_binary(b"KEK family boundary")
.unwrap();
let parsed = parse_encrypted_data(&encrypted.encrypted_data_xml).unwrap();
assert!(matches!(
KekDecryptor::new(kek.clone()).resolve_key(
xml_sec::provider::default_provider(),
DataEncryptionAlgorithm::Aes128Gcm,
parsed.encrypted_keys.first(),
),
Err(XmlEncError::KeyNotFound)
));
assert!(
KekDecryptor::with_kind(kek, xml_sec::key_manager::SymmetricKeyKind::Des)
.resolve_key(
xml_sec::provider::default_provider(),
DataEncryptionAlgorithm::Aes128Gcm,
parsed.encrypted_keys.first(),
)
.is_ok()
);
}
#[cfg(feature = "legacy-algorithms")]
#[test]
fn direct_resolvers_enforce_legacy_method_permissions() {
use xml_sec::policy::{DecryptionPolicy, EncryptionPolicy};
use xml_sec::xmlenc::{DecryptionKeyResolver, KeyCandidateBudget};
for wrap in [KeyWrapAlgorithm::AesKw192, KeyWrapAlgorithm::TripleDes] {
let kek = vec![0x33; wrap.key_len()];
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.add_recipient(EncryptionRecipient::aes_key_wrap(kek.clone(), wrap))
.policy(EncryptionPolicy {
key_wrap_algorithms: Some([wrap].into()),
..EncryptionPolicy::default()
})
.encrypt_binary(b"policy boundary")
.unwrap();
let parsed = parse_encrypted_data(&encrypted.encrypted_data_xml).unwrap();
let resolver = KekDecryptor::with_kind(kek, wrap.key_kind());
assert!(matches!(
resolver.resolve_key_candidates_with_policy(
xml_sec::provider::default_provider(),
DataEncryptionAlgorithm::Aes128Gcm,
parsed.encrypted_keys.first(),
&DecryptionPolicy::default(),
&mut KeyCandidateBudget::for_operation()
),
Err(XmlEncError::Policy(_))
));
}
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.add_recipient(EncryptionRecipient::rsa_pkcs1v15(public_key(
RSA_2048_PUBLIC,
)))
.policy(EncryptionPolicy {
key_transport_algorithms: Some([KeyTransportAlgorithm::RsaPkcs1v15].into()),
..EncryptionPolicy::default()
})
.encrypt_binary(b"policy boundary")
.unwrap();
let parsed = parse_encrypted_data(&encrypted.encrypted_data_xml).unwrap();
assert!(matches!(
PrivateKeyDecryptor::new(private_key(RSA_2048_PRIVATE)).resolve_key_with_policy(
xml_sec::provider::default_provider(),
DataEncryptionAlgorithm::Aes128Gcm,
parsed.encrypted_keys.first(),
&DecryptionPolicy::default()
),
Err(XmlEncError::Policy(_))
));
}
#[cfg(feature = "legacy-algorithms")]
#[test]
fn pkcs1v15_transport_is_parameterless_and_explicitly_permitted() {
use xml_sec::policy::{DecryptionPolicy, EncryptionPolicy};
use xml_sec::xmlenc::DecryptContext;
let public = public_key(RSA_2048_PUBLIC);
let recipient = EncryptionRecipient::rsa_pkcs1v15(public);
assert!(
EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.add_recipient(recipient.clone())
.encrypt_binary(b"legacy transport")
.is_err()
);
let transport = KeyTransportAlgorithm::RsaPkcs1v15;
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.add_recipient(recipient)
.policy(EncryptionPolicy {
key_transport_algorithms: Some([transport].into()),
..EncryptionPolicy::default()
})
.encrypt_binary(b"legacy transport")
.unwrap();
assert!(!encrypted.encrypted_data_xml.contains("OAEPparams"));
assert!(!encrypted.encrypted_data_xml.contains("DigestMethod"));
let resolver = PrivateKeyDecryptor::new(private_key(RSA_2048_PRIVATE));
assert!(decrypt(&encrypted.encrypted_data_xml, &resolver).is_err());
let policy = DecryptionPolicy {
key_transport_algorithms: Some([transport].into()),
..DecryptionPolicy::default()
};
assert_eq!(
DecryptContext::new(&resolver)
.policy(policy.clone())
.decrypt(&encrypted.encrypted_data_xml)
.unwrap(),
DecryptedContent::Bytes(b"legacy transport".to_vec())
);
let wrong_key = PrivateKeyDecryptor::new(private_key(RSA_4096_PRIVATE));
assert!(
DecryptContext::new(&wrong_key)
.policy(policy.clone())
.decrypt(&encrypted.encrypted_data_xml)
.is_err()
);
let document = roxmltree::Document::parse(&encrypted.encrypted_data_xml).unwrap();
let wrapped = document
.descendants()
.find(|node| node.tag_name().name() == "EncryptedKey")
.unwrap()
.descendants()
.find(|node| node.tag_name().name() == "CipherValue")
.unwrap()
.text()
.unwrap();
let mut corrupt = STANDARD.decode(wrapped).unwrap();
corrupt.fill(0);
let damaged = encrypted
.encrypted_data_xml
.replace(wrapped, &STANDARD.encode(&corrupt));
assert!(
DecryptContext::new(&resolver)
.policy(policy.clone())
.decrypt(&damaged)
.is_err()
);
let short = encrypted
.encrypted_data_xml
.replace(wrapped, &STANDARD.encode(&corrupt[..corrupt.len() - 1]));
corrupt.fill(0xff);
let out_of_range = encrypted
.encrypted_data_xml
.replace(wrapped, &STANDARD.encode(&corrupt));
assert!(matches!(
DecryptContext::new(&resolver)
.policy(policy.clone())
.decrypt(&out_of_range),
Err(XmlEncError::AeadAuthenticationFailed)
));
assert!(
DecryptContext::new(&resolver)
.policy(policy.clone())
.decrypt(&short)
.is_err()
);
let malformed = encrypted.encrypted_data_xml.replace(
&format!("Algorithm=\"{}\"/>", transport.uri()),
&format!(
"Algorithm=\"{}\"><OAEPparams>AA==</OAEPparams></EncryptionMethod>",
transport.uri()
),
);
assert_ne!(malformed, encrypted.encrypted_data_xml);
assert!(
DecryptContext::new(&resolver)
.policy(policy)
.decrypt(&malformed)
.is_err()
);
}
#[cfg(feature = "legacy-algorithms")]
#[test]
fn legacy_content_requires_permission_on_both_public_paths() {
use xml_sec::policy::{DecryptionPolicy, EncryptionPolicy};
use xml_sec::xmlenc::DecryptContext;
for algorithm in [
DataEncryptionAlgorithm::TripleDesCbc,
DataEncryptionAlgorithm::Aes192Cbc,
DataEncryptionAlgorithm::Aes192Gcm,
] {
let key = [0x31; 24];
assert!(
EncryptedDataBuilder::new(algorithm)
.direct_key(key)
.encrypt_binary(b"secret")
.is_err()
);
let encrypted = EncryptedDataBuilder::new(algorithm)
.direct_key(key)
.policy(EncryptionPolicy {
data_algorithms: Some([algorithm].into()),
..EncryptionPolicy::default()
})
.encrypt_binary(b"secret")
.unwrap();
let resolver = SymmetricKeyDecryptor::with_kind(key, algorithm.key_kind());
assert!(decrypt(&encrypted.encrypted_data_xml, &resolver).is_err());
assert_eq!(
DecryptContext::new(&resolver)
.policy(DecryptionPolicy {
data_algorithms: Some([algorithm].into()),
..DecryptionPolicy::default()
})
.decrypt(&encrypted.encrypted_data_xml)
.unwrap(),
DecryptedContent::Bytes(b"secret".to_vec())
);
}
}
fn private_key(path: &str) -> RsaPrivateKey {
RsaPrivateKey::from_pkcs8_pem(&fs::read_to_string(path).expect("RSA fixture must load"))
.expect("RSA fixture must contain a PKCS#8 private key")
}
fn public_key(path: &str) -> RsaPublicKey {
RsaPublicKey::from_public_key_pem(
&fs::read_to_string(path).expect("RSA public-key fixture must load"),
)
.expect("RSA fixture must contain an SPKI public key")
}
fn method_algorithm(node: Node<'_, '_>) -> String {
node.attribute("Algorithm")
.expect("donor EncryptionMethod must declare Algorithm")
.to_owned()
}
fn donor_key_name(document: &roxmltree::Document<'_>) -> String {
document
.descendants()
.find(|node| node.tag_name().name() == "KeyName")
.and_then(|node| node.text())
.map(str::trim)
.expect("donor template must contain KeyName")
.to_owned()
}
#[test]
fn donor_direct_templates_match_generated_algorithm_and_key_contracts() {
for (name, algorithm, key) in [
(
"enc-aes128cbc-keyname",
DataEncryptionAlgorithm::Aes128Cbc,
vec![0x11; 16],
),
(
"enc-aes128gcm-keyname",
DataEncryptionAlgorithm::Aes128Gcm,
vec![0x22; 16],
),
(
"enc-aes256cbc-keyname",
DataEncryptionAlgorithm::Aes256Cbc,
vec![0x33; 32],
),
(
"enc-aes256gcm-keyname",
DataEncryptionAlgorithm::Aes256Gcm,
vec![0x44; 32],
),
] {
let template = fs::read_to_string(format!("{DONOR_DIR}/{name}.tmpl"))
.expect("tracked donor template must load");
let template_document =
roxmltree::Document::parse(&template).expect("donor template must be XML");
let template_method = template_document
.descendants()
.find(|node| node.tag_name().name() == "EncryptionMethod")
.expect("donor template must contain EncryptionMethod");
let generated = EncryptedDataBuilder::new(algorithm)
.direct_key(key.clone())
.direct_key_name(if algorithm.key_len() == 16 {
"test-aes128"
} else {
"test-aes256"
})
.encrypt_binary(format!("{name} plaintext").as_bytes())
.expect("donor-shaped direct encryption must succeed");
let generated_data = parse_encrypted_data(&generated.encrypted_data_xml)
.expect("generated EncryptedData must parse");
assert_eq!(
generated_data.encryption_method.algorithm,
method_algorithm(template_method),
"{name}"
);
assert_eq!(
generated_data.key_name.as_deref(),
Some(donor_key_name(&template_document).as_str()),
"{name}"
);
assert_eq!(
decrypt(
&generated.encrypted_data_xml,
&SymmetricKeyDecryptor::new(key)
)
.expect("generated donor-shaped ciphertext must decrypt"),
DecryptedContent::Bytes(format!("{name} plaintext").into_bytes()),
"{name}"
);
}
}
#[test]
fn donor_rsa_templates_match_legacy_and_xmlenc11_oaep_contracts() {
let public = public_key(RSA_4096_PUBLIC);
let private = private_key(RSA_4096_PRIVATE);
let cases = [
(
"enc-aes256-kt-rsa_oaep_sha1-params",
RsaOaepParameters::legacy().label(b"12345678".to_vec()),
),
(
"enc-aes256-kt-rsa_oaep_enc11_sha512_mgf1_sha512",
RsaOaepParameters::xmlenc11(OaepDigestAlgorithm::Sha512, OaepDigestAlgorithm::Sha512),
),
];
for (name, parameters) in cases {
let template = fs::read_to_string(format!("{DONOR_DIR}/{name}.tmpl"))
.expect("tracked donor template must load");
let template_document =
roxmltree::Document::parse(&template).expect("donor template must be XML");
let template_methods = template_document
.descendants()
.filter(|node| node.tag_name().name() == "EncryptionMethod")
.collect::<Vec<_>>();
assert_eq!(template_methods.len(), 2, "{name}");
let generated = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes256Cbc)
.encryption_type(EncryptedDataType::Content)
.id("ED")
.add_recipient(
EncryptionRecipient::rsa_oaep(public.clone())
.oaep_parameters(parameters)
.key_name("TestKeyName-rsa-4096"),
)
.encrypt_xml("<PaymentInfo><Name>John Smith</Name></PaymentInfo>")
.expect("donor-shaped OAEP encryption must succeed");
let generated_data = parse_encrypted_data(&generated.encrypted_data_xml)
.expect("generated EncryptedData must parse");
assert_eq!(
generated_data.encryption_method.algorithm,
method_algorithm(template_methods[0]),
"{name}"
);
assert_eq!(
generated_data.encrypted_keys[0].encryption_method.algorithm,
method_algorithm(template_methods[1]),
"{name}"
);
let donor_digest = template_methods[1]
.children()
.find(|node| node.tag_name().name() == "DigestMethod")
.and_then(|node| node.attribute("Algorithm"))
.map(str::to_owned);
let donor_mgf = template_methods[1]
.children()
.find(|node| node.tag_name().name() == "MGF")
.and_then(|node| node.attribute("Algorithm"))
.map(str::to_owned);
let donor_label = template_methods[1]
.children()
.find(|node| node.tag_name().name() == "OAEPparams")
.and_then(|node| node.text())
.map(str::trim)
.map(|value| {
STANDARD
.decode(value)
.expect("donor OAEP label must be base64")
});
assert_eq!(
generated_data.encrypted_keys[0]
.encryption_method
.oaep_digest,
donor_digest,
"{name}"
);
assert_eq!(
generated_data.encrypted_keys[0]
.encryption_method
.mgf_algorithm,
donor_mgf,
"{name}"
);
assert_eq!(
generated_data.encrypted_keys[0]
.encryption_method
.oaep_params,
donor_label,
"{name}"
);
assert_eq!(
generated_data.encrypted_keys[0].key_name.as_deref(),
Some(donor_key_name(&template_document).as_str()),
"{name}"
);
assert_eq!(
decrypt(
&generated.encrypted_data_xml,
&PrivateKeyDecryptor::new(private.clone())
)
.expect("generated donor-shaped OAEP ciphertext must decrypt"),
DecryptedContent::Xml("<PaymentInfo><Name>John Smith</Name></PaymentInfo>".into()),
"{name}"
);
}
}
#[test]
fn every_supported_oaep_digest_and_mgf_combination_round_trips() {
let private = private_key(RSA_2048_PRIVATE);
let public = RsaPublicKey::from(&private);
let digests = [
OaepDigestAlgorithm::Sha1,
OaepDigestAlgorithm::Sha256,
OaepDigestAlgorithm::Sha384,
OaepDigestAlgorithm::Sha512,
];
for digest in digests {
for mgf_digest in digests {
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.add_recipient(
EncryptionRecipient::rsa_oaep(public.clone())
.oaep_parameters(RsaOaepParameters::xmlenc11(digest, mgf_digest)),
)
.encrypt_binary(b"OAEP matrix")
.expect("supported OAEP combination must encrypt");
assert_eq!(
decrypt(
&encrypted.encrypted_data_xml,
&PrivateKeyDecryptor::new(private.clone())
)
.expect("supported OAEP combination must decrypt"),
DecryptedContent::Bytes(b"OAEP matrix".to_vec()),
"digest={digest:?}, mgf={mgf_digest:?}"
);
}
}
for digest in digests {
let parameters = RsaOaepParameters {
algorithm: KeyTransportAlgorithm::RsaOaepMgf1p,
digest,
mgf_digest: OaepDigestAlgorithm::Sha1,
label: b"legacy label".to_vec(),
};
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Cbc)
.add_recipient(
EncryptionRecipient::rsa_oaep(public.clone()).oaep_parameters(parameters),
)
.encrypt_binary(b"legacy OAEP digest matrix")
.expect("legacy OAEP digest with default MGF1-SHA1 must encrypt");
assert_eq!(
decrypt(
&encrypted.encrypted_data_xml,
&PrivateKeyDecryptor::new(private.clone())
)
.expect("legacy OAEP digest with default MGF1-SHA1 must decrypt"),
DecryptedContent::Bytes(b"legacy OAEP digest matrix".to_vec()),
"legacy digest={digest:?}"
);
}
}
#[test]
fn one_session_key_is_recoverable_by_each_rsa_recipient() {
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes256Gcm)
.add_recipient(
EncryptionRecipient::rsa_oaep(public_key(RSA_2048_PUBLIC))
.recipient("primary")
.key_name("rsa-2048"),
)
.add_recipient(
EncryptionRecipient::rsa_oaep(public_key(RSA_4096_PUBLIC))
.recipient("secondary")
.key_name("rsa-4096"),
)
.encrypt_xml("<secret>shared</secret>")
.expect("multi-recipient encryption must succeed");
let parsed = parse_encrypted_data(&encrypted.encrypted_data_xml)
.expect("multi-recipient output must parse");
assert_eq!(parsed.encrypted_keys.len(), 2);
assert_eq!(
parsed.encrypted_keys[0].recipient.as_deref(),
Some("primary")
);
assert_eq!(
parsed.encrypted_keys[1].recipient.as_deref(),
Some("secondary")
);
for key_path in [RSA_2048_PRIVATE, RSA_4096_PRIVATE] {
assert_eq!(
decrypt(
&encrypted.encrypted_data_xml,
&PrivateKeyDecryptor::new(private_key(key_path))
)
.expect("each recipient must decrypt the shared content"),
DecryptedContent::Xml("<secret>shared</secret>".into()),
"{key_path}"
);
}
}
#[test]
fn both_aes_key_wrap_sizes_cover_both_content_key_sizes() {
for (wrap, kek) in [
(KeyWrapAlgorithm::AesKw128, vec![0x81; 16]),
(KeyWrapAlgorithm::AesKw256, vec![0x82; 32]),
] {
for algorithm in [
DataEncryptionAlgorithm::Aes128Cbc,
DataEncryptionAlgorithm::Aes256Gcm,
] {
let encrypted = EncryptedDataBuilder::new(algorithm)
.recipient_aes_kw(kek.clone(), wrap)
.encrypt_binary(b"wrapped content key")
.expect("supported AES-KW combination must encrypt");
assert_eq!(
decrypt(
&encrypted.encrypted_data_xml,
&KekDecryptor::new(kek.clone())
)
.expect("supported AES-KW combination must decrypt"),
DecryptedContent::Bytes(b"wrapped content key".to_vec())
);
}
}
}
#[test]
fn document_encryption_selects_one_id_and_preserves_surrounding_xml() {
let key = [0x91; 16];
let document = "<root xmlns:p=\"urn:test\"><before/><p:target Id=\"chosen\" role=\"secret\"><p:item/>text</p:target><after/></root>";
for encrypted_type in [EncryptedDataType::Element, EncryptedDataType::Content] {
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.encryption_type(encrypted_type)
.direct_key(key)
.id("encrypted-target")
.encrypt_document(
document,
DocumentEncryptionOptions {
element_id: Some("chosen"),
},
)
.expect("selected document node must encrypt");
assert!(encrypted.contains("<before/>"));
assert!(encrypted.contains("<after/>"));
assert_eq!(
decrypt_document(
&encrypted,
Some("encrypted-target"),
&SymmetricKeyDecryptor::new(key)
)
.expect("selected document node must decrypt"),
document
);
}
}
#[test]
fn content_encryption_preserves_cdata_with_closing_markers() {
let key = [0x92; 16];
let document = "<root><target Id=\"chosen\"><![CDATA[</not-a-tag>]]></target></root>";
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.encryption_type(EncryptedDataType::Content)
.direct_key(key)
.encrypt_document(
document,
DocumentEncryptionOptions {
element_id: Some("chosen"),
},
)
.expect("CDATA content containing a closing marker must encrypt");
assert_eq!(
decrypt_document(&encrypted, None, &SymmetricKeyDecryptor::new(key))
.expect("CDATA content containing a closing marker must decrypt"),
document
);
}
#[test]
fn saml_encrypted_assertion_round_trips_through_rsa_transport() {
let assertion = concat!(
"<saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ",
"ID=\"assertion-1\" Version=\"2.0\" IssueInstant=\"2026-07-21T00:00:00Z\">",
"<saml:Issuer>https://idp.example</saml:Issuer>",
"<saml:Subject><saml:NameID>alice@example.com</saml:NameID></saml:Subject>",
"</saml:Assertion>"
);
let private = private_key(RSA_2048_PRIVATE);
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes256Gcm)
.add_recipient(EncryptionRecipient::rsa_oaep(RsaPublicKey::from(&private)))
.encrypt_xml(assertion)
.expect("SAML Assertion must encrypt");
let response = format!(
"<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\"><saml:EncryptedAssertion>{}</saml:EncryptedAssertion></samlp:Response>",
encrypted.encrypted_data_xml
);
let decrypted = decrypt_document(&response, None, &PrivateKeyDecryptor::new(private))
.expect("SAML EncryptedAssertion child must decrypt");
assert_eq!(
decrypted,
format!(
"<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\"><saml:EncryptedAssertion>{assertion}</saml:EncryptedAssertion></samlp:Response>"
)
);
}
#[test]
fn invalid_targets_configuration_and_keys_fail_closed() {
let key = [0xa1; 16];
let builder = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm).direct_key(key);
assert!(matches!(
builder.encrypt_xml("<root><unclosed></root>"),
Err(XmlEncError::XmlParse(_))
));
assert!(matches!(
builder
.clone()
.encryption_type(EncryptedDataType::Content)
.encrypt_xml("<unclosed>"),
Err(XmlEncError::XmlParse(_))
));
assert!(matches!(
builder.encrypt_document(
"<root><unclosed></root>",
DocumentEncryptionOptions::default()
),
Err(XmlEncError::XmlParse(_))
));
assert!(matches!(
builder.encrypt_document(
"<root><a Id=\"same\"/><b id=\"same\"/></root>",
DocumentEncryptionOptions {
element_id: Some("same"),
}
),
Err(XmlEncError::AmbiguousEncryptionTarget)
));
assert!(matches!(
builder.encrypt_document(
"<root/>",
DocumentEncryptionOptions {
element_id: Some("missing"),
}
),
Err(XmlEncError::EncryptionTargetNotFound)
));
let incompatible = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key(key)
.add_recipient(EncryptionRecipient::aes_key_wrap(
[0xb2; 16],
KeyWrapAlgorithm::AesKw128,
))
.encrypt_binary(b"must not encrypt");
assert!(matches!(
incompatible,
Err(XmlEncError::InvalidEncryptionConfig(_))
));
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.recipient_aes_kw([0xc3; 16], KeyWrapAlgorithm::AesKw128)
.encrypt_binary(b"secret")
.expect("valid KEK must encrypt");
assert!(matches!(
decrypt(
&encrypted.encrypted_data_xml,
&KekDecryptor::new([0xd4; 16])
),
Err(XmlEncError::KeyWrapIntegrity)
));
}
#[test]
fn tampered_generated_gcm_ciphertext_fails_authentication() {
let key = [0xf6; 16];
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key(key)
.encrypt_binary(b"authenticated plaintext")
.expect("GCM encryption must succeed");
let mut tampered = encrypted.encrypted_data_xml;
let value_end = tampered
.find("</xenc:CipherValue>")
.expect("generated output must contain CipherValue");
let index = value_end - 2;
let replacement = if &tampered[index..=index] == "A" {
"B"
} else {
"A"
};
tampered.replace_range(index..=index, replacement);
assert!(matches!(
decrypt(&tampered, &SymmetricKeyDecryptor::new(key)),
Err(XmlEncError::AeadAuthenticationFailed)
));
}
#[test]
fn generated_metadata_is_xml_escaped_and_legacy_mgf_is_explicit() {
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.id("encrypted-id")
.add_recipient(
EncryptionRecipient::aes_key_wrap([0xe5; 16], KeyWrapAlgorithm::AesKw128)
.recipient("recipient<&\"")
.key_name("key<&"),
)
.encrypt_binary(b"metadata")
.expect("metadata must serialize safely");
let parsed =
parse_encrypted_data(&encrypted.encrypted_data_xml).expect("escaped metadata must parse");
assert_eq!(parsed.id.as_deref(), Some("encrypted-id"));
assert_eq!(
parsed.encrypted_keys[0].recipient.as_deref(),
Some("recipient<&\"")
);
assert_eq!(parsed.encrypted_keys[0].key_name.as_deref(), Some("key<&"));
let explicit_legacy = RsaOaepParameters {
algorithm: KeyTransportAlgorithm::RsaOaepMgf1p,
digest: OaepDigestAlgorithm::Sha256,
mgf_digest: OaepDigestAlgorithm::Sha256,
label: Vec::new(),
};
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.add_recipient(
EncryptionRecipient::rsa_oaep(public_key(RSA_2048_PUBLIC))
.oaep_parameters(explicit_legacy),
)
.encrypt_binary(b"explicit legacy MGF")
.expect("legacy OAEP with an explicit MGF must encrypt");
assert!(
encrypted
.encrypted_data_xml
.contains(r#"<xenc11:MGF Algorithm="http://www.w3.org/2009/xmlenc11#mgf1sha256"/>"#)
);
let decrypted = decrypt(
&encrypted.encrypted_data_xml,
&PrivateKeyDecryptor::new(private_key(RSA_2048_PRIVATE)),
)
.expect("serialized legacy OAEP parameters must round trip");
assert_eq!(
decrypted,
DecryptedContent::Bytes(b"explicit legacy MGF".to_vec())
);
}
#[test]
fn default_rsa_oaep_recipient_emits_explicit_secure_parameters() {
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.add_recipient(EncryptionRecipient::rsa_oaep(public_key(RSA_2048_PUBLIC)))
.encrypt_binary(b"explicit OAEP defaults")
.expect("default RSA-OAEP recipient must encrypt");
let parsed = parse_encrypted_data(&encrypted.encrypted_data_xml)
.expect("generated EncryptedData must parse");
let method = &parsed.encrypted_keys[0].encryption_method;
assert_eq!(
method.oaep_digest.as_deref(),
Some("http://www.w3.org/2001/04/xmlenc#sha256")
);
assert_eq!(
method.mgf_algorithm.as_deref(),
Some("http://www.w3.org/2009/xmlenc11#mgf1sha256")
);
}