Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- erikescher
- Guiguiprim
- iliana
- jacques-kigo
- janst97
- jclulow
- jmpesp
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ Current Features:
- `http://www.w3.org/2001/04/xmlenc#rsa-1_5`
- **value info:**
- `http://www.w3.org/2001/04/xmlenc#aes128-cbc`
- `http://www.w3.org/2001/04/xmlenc#aes192-cbc`
- `http://www.w3.org/2001/04/xmlenc#aes256-cbc`
- `http://www.w3.org/2009/xmlenc11#aes128-gcm`
- `http://www.w3.org/2009/xmlenc11#aes192-gcm`
- `http://www.w3.org/2009/xmlenc11#aes256-gcm`
- Verify SAMLRequest (AuthnRequest) message signatures
- Create signed SAMLResponse (Response) messages

Expand Down
48 changes: 48 additions & 0 deletions src/crypto/xmlsec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,26 @@ impl super::CryptoProvider for XmlSec {
&encoded_value[iv_len..],
)?
}
"http://www.w3.org/2001/04/xmlenc#aes192-cbc" => {
let cipher = Cipher::aes_192_cbc();
let iv_len = cipher.iv_len().unwrap();
decrypt(
cipher,
decryption_key,
Some(&encoded_value[0..iv_len]),
&encoded_value[iv_len..],
)?
}
"http://www.w3.org/2001/04/xmlenc#aes256-cbc" => {
let cipher = Cipher::aes_256_cbc();
let iv_len = cipher.iv_len().unwrap();
decrypt(
cipher,
decryption_key,
Some(&encoded_value[0..iv_len]),
&encoded_value[iv_len..],
)?
}
"http://www.w3.org/2009/xmlenc11#aes128-gcm" => {
let cipher = Cipher::aes_128_gcm();
let iv_len = cipher.iv_len().unwrap();
Expand All @@ -295,6 +315,34 @@ impl super::CryptoProvider for XmlSec {
&encoded_value[data_end..],
)?
}
"http://www.w3.org/2009/xmlenc11#aes192-gcm" => {
let cipher = Cipher::aes_192_gcm();
let iv_len = cipher.iv_len().unwrap();
let tag_len = 16usize;
let data_end = encoded_value.len() - tag_len;
decrypt_aead(
cipher,
decryption_key,
Some(&encoded_value[0..iv_len]),
&[],
&encoded_value[iv_len..data_end],
&encoded_value[data_end..],
)?
}
"http://www.w3.org/2009/xmlenc11#aes256-gcm" => {
let cipher = Cipher::aes_256_gcm();
let iv_len = cipher.iv_len().unwrap();
let tag_len = 16usize;
let data_end = encoded_value.len() - tag_len;
decrypt_aead(
cipher,
decryption_key,
Some(&encoded_value[0..iv_len]),
&[],
&encoded_value[iv_len..data_end],
&encoded_value[data_end..],
)?
}
_ => {
return Err(CryptoError::EncryptedAssertionValueMethodUnsupported {
method: method.to_string(),
Expand Down
176 changes: 176 additions & 0 deletions src/service_provider/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,182 @@ mod encrypted_assertion_tests {
assert!(result.is_ok());
}

#[test]
fn test_decrypt_assertion_aes192_cbc() {
let pkey = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/sp_private.pem"
));
let key = PKey::private_key_from_pem(pkey).unwrap();

let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_encrypted_aes192_cbc.xml"
));
let sp = create_sp_with_private_key_for_response(response_xml, key);

let response: crate::schema::Response = response_xml.parse().unwrap();
assert!(response.encrypted_assertion.is_some());

let result = response
.encrypted_assertion
.unwrap()
.decrypt(&sp.key.unwrap());

assert!(result.is_ok());
}

#[test]
fn test_decrypt_assertion_aes256_cbc() {
let pkey = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/sp_private.pem"
));
let key = PKey::private_key_from_pem(pkey).unwrap();

let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_encrypted_aes256_cbc.xml"
));
let sp = create_sp_with_private_key_for_response(response_xml, key);

let response: crate::schema::Response = response_xml.parse().unwrap();
assert!(response.encrypted_assertion.is_some());

let result = response
.encrypted_assertion
.unwrap()
.decrypt(&sp.key.unwrap());

assert!(result.is_ok());
}

#[test]
fn test_decrypt_and_validate_assertion_aes192_cbc() {
let pkey = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/sp_private.pem"
));
let key = PKey::private_key_from_pem(pkey).unwrap();

let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_encrypted_valid_aes192_cbc.xml"
));
let sp = create_sp_with_private_key_for_response(response_xml, key);

let result = sp.parse_xml_response(response_xml, Some(&["example"]));

assert!(result.is_ok());
}

#[test]
fn test_decrypt_and_validate_assertion_aes256_cbc() {
let pkey = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/sp_private.pem"
));
let key = PKey::private_key_from_pem(pkey).unwrap();

let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_encrypted_valid_aes256_cbc.xml"
));
let sp = create_sp_with_private_key_for_response(response_xml, key);

let result = sp.parse_xml_response(response_xml, Some(&["example"]));

assert!(result.is_ok());
}

#[test]
fn test_decrypt_assertion_aes192_gcm() {
let pkey = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/sp_private.pem"
));
let key = PKey::private_key_from_pem(pkey).unwrap();

let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_encrypted_aes192_gcm.xml"
));
let sp = create_sp_with_private_key_for_response(response_xml, key);

let response: crate::schema::Response = response_xml.parse().unwrap();
assert!(response.encrypted_assertion.is_some());

let result = response
.encrypted_assertion
.unwrap()
.decrypt(&sp.key.unwrap());

assert!(result.is_ok());
}

#[test]
fn test_decrypt_assertion_aes256_gcm() {
let pkey = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/sp_private.pem"
));
let key = PKey::private_key_from_pem(pkey).unwrap();

let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_encrypted_aes256_gcm.xml"
));
let sp = create_sp_with_private_key_for_response(response_xml, key);

let response: crate::schema::Response = response_xml.parse().unwrap();
assert!(response.encrypted_assertion.is_some());

let result = response
.encrypted_assertion
.unwrap()
.decrypt(&sp.key.unwrap());

assert!(result.is_ok());
}

#[test]
fn test_decrypt_and_validate_assertion_aes192_gcm() {
let pkey = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/sp_private.pem"
));
let key = PKey::private_key_from_pem(pkey).unwrap();

let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_encrypted_valid_aes192_gcm.xml"
));
let sp = create_sp_with_private_key_for_response(response_xml, key);

let result = sp.parse_xml_response(response_xml, Some(&["example"]));

assert!(result.is_ok());
}

#[test]
fn test_decrypt_and_validate_assertion_aes256_gcm() {
let pkey = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/sp_private.pem"
));
let key = PKey::private_key_from_pem(pkey).unwrap();

let response_xml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test_vectors/response_encrypted_valid_aes256_gcm.xml"
));
let sp = create_sp_with_private_key_for_response(response_xml, key);

let result = sp.parse_xml_response(response_xml, Some(&["example"]));

assert!(result.is_ok());
}

fn extract_first_certificate(xml: &str) -> String {
let start = xml
.find("<ds:X509Certificate>")
Expand Down
51 changes: 51 additions & 0 deletions test_vectors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,54 @@ openssl x509 -in ec_cert.pem -outform DER -out ec_cert.der
# Step 5: Use the Private Key and Certificate with xmlsec1
xmlsec1 --sign --privkey-der ec_private.der,ec_cert.der --output response_signed_by_idp_ecdsa.xml --id-attr:ID Response response_signed_template.xml
```

# Generating encrypted responses for tests

The `response_encrypted_aes{192,256}_{cbc,gcm}.xml` and `response_encrypted_valid_aes{192,256}_{cbc,gcm}.xml` fixtures are generated with `xmlsec1 --encrypt`, using the existing `sp_cert.pem` / `sp_private.pem` keypair (rsa-oaep-mgf1p key transport).

Step 1 — extract the plaintext assertion from the existing AES-128-CBC fixture (the `<xenc:EncryptedData>` element is self-contained namespace-wise, so it can be passed directly to `xmlsec1 --decrypt`):

```bash
# Extract the inner <xenc:EncryptedData> element to a standalone file.
python3 -c "
import re
with open('response_encrypted_valid.xml') as f:
print(re.search(r'<xenc:EncryptedData.*?</xenc:EncryptedData>', f.read(), re.DOTALL).group(0))
" > /tmp/encrypted_data.xml

# Decrypt it to obtain the plaintext <saml:Assertion>.
xmlsec1 --decrypt --lax-key-search --privkey-pem sp_private.pem,sp_cert.pem /tmp/encrypted_data.xml \
| tail -n +2 > /tmp/plaintext_assertion.xml
```

Step 2 — create an encryption template per algorithm. Example for AES-256-CBC (substitute `aes192-cbc` for the AES-192-CBC variant, or use the `xmlenc11` namespace and `aes{192,256}-gcm` for GCM):

```xml
<xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" Type="http://www.w3.org/2001/04/xmlenc#Element">
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc"/>
<dsig:KeyInfo>
<xenc:EncryptedKey>
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/>
<xenc:CipherData>
<xenc:CipherValue/>
</xenc:CipherData>
</xenc:EncryptedKey>
</dsig:KeyInfo>
<xenc:CipherData>
<xenc:CipherValue/>
</xenc:CipherData>
</xenc:EncryptedData>
```

Step 3 — encrypt the plaintext with `xmlsec1 --encrypt`, generating a fresh AES session key wrapped with rsa-oaep-mgf1p:

```bash
xmlsec1 --encrypt --lax-key-search \
--pubkey-cert-pem sp_cert.pem \
--session-key aes-256 \
--xml-data /tmp/plaintext_assertion.xml \
--output /tmp/encrypted_data_aes256_cbc.xml \
template_aes256_cbc.xml
```

Step 4 — wrap the resulting `<xenc:EncryptedData>` in a `<samlp:Response>` envelope (mirroring `response_encrypted.xml` / `response_encrypted_valid.xml`). When inlining the encrypted block, restore `xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"` on the `<dsig:KeyInfo>` element — `xmlsec1` strips the redundant namespace declaration since the prefix is already in scope from the parent, but samael's serde-based parser requires the explicit attribute on the element itself (see `EncryptedKeyInfo.ds` in `src/key_info.rs`).
46 changes: 46 additions & 0 deletions test_vectors/response_encrypted_aes192_cbc.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_aes192_cbc_encrypted_response" InResponseTo="example" Version="2.0" IssueInstant="2025-03-04T21:09:01.566Z" Destination="http://localhost:8080/saml/acs">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">saml-mock</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>

<saml:EncryptedAssertion><xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" Type="http://www.w3.org/2001/04/xmlenc#Element">
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes192-cbc"/>
<dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
<xenc:EncryptedKey>
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/>
<xenc:CipherData>
<xenc:CipherValue>lrc3O8uSApl9LMbrkEzSptFy3NSmnhuRp4FEpLgkqUAqDcC+IBs9H0Eo880joXJp
SJ1yspvzGxEJHZENSxKqC4uJnKORKrbEtwGG3uGQRwoyCcgQcxW6QK4DK4YBF+1e
H6GNKuGhc21K2cQIcT+JP1byidkpk7fIDRIZXxr97bU=</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedKey>
</dsig:KeyInfo>
<xenc:CipherData>
<xenc:CipherValue>u4VhWKJ2qeI1EidISwhS5qh0Y3nt4utLmFpzbYjto5m4dlmt45jfAH9uGdOeJb6k
EB6kzEzle7zKvpitnYwqp9NAWDEWG9uEtx+jOX4WA55Yd1G6DWwr+HUar/zMpFX1
tV8aK9rN+0iVFS7Io3LDrlskxPyuH2amynAXm6ZOwVkOWvs0u3bluw9cPPYIp49Y
Ep11g6bVdEa+HvHDTy3lcBiDdzILMQTVP5CY7Afoh8VRXhcz5uEj20VPKPI/79KJ
thhtfTXOweYSTqYKFypT7x3J14d0IcN9R03a0Kkcpn/zCega6VaFk0eMESqHfF/y
mLSskFtkS0bqxoy8F4Kho+blSvsTVwpcRQ9h9V+Ypwk1aCak3DFdA1ESlKNew+W2
1hg3LFfEhJpqize893V9u07/CP9Zc+RrinIqBICjbKpu9sdHCwECOzWf8KtdpLS/
F/ZHC8OFwOPOZWrvQ/EXbYQqAC8wSeKSaAEVB5rz/juRBcBvdxatjbqxth5YU/IA
qw7gl5y2hTusnK6OEs6wBky3VdmfNxiA06KHR8NMvPmoRf06csnyVMes6wKIS3ie
Xn4gNp1/VSi2ViJrW8eRPB9c91LNgX18kUaN1g6L2cN53tfSbClEYb9CFL2199qC
d+zwFNtA5GNO7tu1faKoC45Q3GpL3uI/CZ2YWJytq4vVojDyFm2lqvYDZAXKw8e4
ylAKhL+ZoZ8DpE5qvHAUkZ7PLJ5CSBt2NEj68Zmx5HFuimpcVgQO1j2Kh66AQnYx
DS5Cs3tcl+u3wsYQ2vZ9QOcb2cp/7Hneva6Znj8nSIlMIhNGGCEU0FEn1pi6ylG0
0mLrk9TubU9NeuVdMRa1fpDvCWRjalkl93DbJfexWuNtTcYS5ghXiwH5NigsDAYO
1C3nKzPTXPaI5AzDUaAQFZz+r2jQv8wz1icoepISqVTNPQeirGBlfEZFCwBRmR/k
r4vjj+bIIZ6ZpH5luERpE7cudK9dIq2b1OkpDhhNMqHUWD6Ivszfl2upTCl2zcaX
9Lyr+9olKglI6YwJGNfhTBmnRKyqs0tbcJ1SH69o7p0eAlul5o5PlOPEyOROxIa8
OpbsGdiVcuImJOuhVySX0dQdjUdZnrdEJwCWbPY/DtiwBRh6xcQDQdqQlRvwMv1s
ut/jUTnCUKu7gOpr4yC7WlBTnNlNGUGzdRrAWqu8qPXlThQbEciIx8/danF2YE/D
gjcNxTMxgfiWkGRkNFZBt9wVPV7uhxs9T4zhaEXq1n/cy52lLOfw13DKf0lfh4p6
21nu3s2YI3sF8CCKHENwhbIKcMKyTsL8thZbxLKUzmbK/EXATR42UjjNUZBcgFMc
F8OobSYXivrOmFXZe1wa9xssVMxdrrWHVO2gTVxY1bJkJj38CdjB1Dip/z30vftP
ftHiCb7xrEljFnNYbKr6EQ/lyRe9bI7r3jwgsE3E+dGrtVgvVkmLnTM0FKU1sAuM
NIcvkz0k9aXF0nZa3IE+pEQ7P4EyUbFH3NNP9O0nGzu0CcjhkO8JeiK9L+10fvs/
55nOt6wICN/+wcTMeDR1mjcJwMZXf0FmMixYkm45M+I2ytCqtyhUcRFLDPa91ay3</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedData></saml:EncryptedAssertion></samlp:Response>
Loading