Skip to content

Commit dad900e

Browse files
authored
Merge pull request #44 from structured-world/test/p1-026-saml-idp-integration
test(xmldsig): add real SAML IdP integration coverage
2 parents 6f9156e + c97fcb9 commit dad900e

4 files changed

Lines changed: 136 additions & 2 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyjU9gkG4ffc3WwyLF2Q4lmRlMmnw
3+
lzJd31gHv5qBg74j1kKSaQWDZEkTHFt4g7AqIlRRqDt/u9euxVNa5RLqxg==
4+
-----END PUBLIC KEY-----
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?xml version="1.0"?>
2+
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="pfxf63324d7-7ba2-b371-90d6-171637d97253" Version="2.0" IssueInstant="2014-07-17T01:01:48Z" Destination="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685">
3+
<saml:Issuer>https://fujifish.github.io/samling/samling.html</saml:Issuer>
4+
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5+
<ds:SignedInfo>
6+
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7+
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
8+
<ds:Reference URI="#pfxf63324d7-7ba2-b371-90d6-171637d97253">
9+
<ds:Transforms>
10+
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
11+
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
12+
</ds:Transforms>
13+
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
14+
<ds:DigestValue>W7iYqYBNLg7dS+ueqLf04nO5V+c=</ds:DigestValue>
15+
</ds:Reference>
16+
</ds:SignedInfo>
17+
<ds:SignatureValue>THCZWgdX01bDRNyUHHS+u3U7URTI4c3+1cuXKeWFQDjX/yjrC6V/6wCwXtD4VyjU
18+
aUxevxscW8FBCRTkwDR78A==</ds:SignatureValue>
19+
<ds:KeyInfo>
20+
<ds:X509Data>
21+
<ds:X509Certificate>MIIBhzCCAS0CFGE3kR43hTxJz3hg+bsefDiZjTSiMAoGCCqGSM49BAMCMEUxCzAJ
22+
BgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l
23+
dCBXaWRnaXRzIFB0eSBMdGQwIBcNMjQwNjIzMTc0NTQ5WhgPMzAyMzEwMjUxNzQ1
24+
NDlaMEUxCzAJBgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK
25+
DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwWTATBgcqhkjOPQIBBggqhkjOPQMB
26+
BwNCAATKNT2CQbh99zdbDIsXZDiWZGUyafCXMl3fWAe/moGDviPWQpJpBYNkSRMc
27+
W3iDsCoiVFGoO3+7167FU1rlEurGMAoGCCqGSM49BAMCA0gAMEUCIQCdW4SacWlI
28+
qj04IXo5QNWgbIrG6MKcXbvWEXDmMkiIewIgHkDlDn8Aq4reI+4BvUN+ZDmvOs1I
29+
UevJyxGd/2RkolE=</ds:X509Certificate>
30+
</ds:X509Data>
31+
</ds:KeyInfo>
32+
</ds:Signature>
33+
<samlp:Status>
34+
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
35+
</samlp:Status>
36+
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75" Version="2.0" IssueInstant="2014-07-17T01:01:48Z">
37+
<saml:Issuer>https://fujifish.github.io/samling/samling.html</saml:Issuer>
38+
<saml:Subject>
39+
<saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">
40+
_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
41+
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
42+
<saml:SubjectConfirmationData NotOnOrAfter="2030-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
43+
</saml:SubjectConfirmation>
44+
</saml:Subject>
45+
<saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2030-01-18T06:21:48Z">
46+
<saml:AudienceRestriction>
47+
<saml:Audience>http://test_accept_signed_with_correct_key.test</saml:Audience>
48+
</saml:AudienceRestriction>
49+
</saml:Conditions>
50+
<saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2030-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
51+
<saml:AuthnContext>
52+
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
53+
</saml:AuthnContext>
54+
</saml:AuthnStatement>
55+
<saml:AttributeStatement>
56+
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
57+
<saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
58+
</saml:Attribute>
59+
<saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
60+
<saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
61+
</saml:Attribute>
62+
<saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
63+
<saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
64+
<saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
65+
</saml:Attribute>
66+
</saml:AttributeStatement>
67+
</saml:Assertion>
68+
</samlp:Response>

tests/fixtures_smoke.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ fn ec_p256_key_files_are_valid_pem() {
5555
assert_pem_file(&dir.join("ec-prime256v1-key.pem"), "PRIVATE KEY");
5656
assert_pem_file(&dir.join("ec-prime256v1-cert.pem"), "CERTIFICATE");
5757
assert_pem_file(&dir.join("ec-prime256v1-pubkey.pem"), "PUBLIC KEY");
58+
assert_pem_file(&dir.join("saml-idp-ecdsa-pubkey.pem"), "PUBLIC KEY");
5859
}
5960

6061
/// Verify EC P-384 key triplet exists and contains valid PEM markers.
@@ -175,8 +176,8 @@ fn fixture_file_count_matches_expected() {
175176
let mut count = 0;
176177
count_files_recursive(fixtures_dir(), &mut count);
177178
assert_eq!(
178-
count, 77,
179-
"expected 77 fixture files total (22 keys + 41 c14n + 14 donor xmldsig); \
179+
count, 79,
180+
"expected 79 fixture files total (23 keys + 41 c14n + 14 donor xmldsig + 1 saml); \
180181
if you added/removed files, update this count"
181182
);
182183
}

tests/saml_idp_integration.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//! Integration tests for real-world SAML response verification.
2+
//! Covers PR #44.
3+
//!
4+
//! Uses a donor SAML 2.0 IdP response fixture to ensure XMLDSig verification
5+
//! works against non-synthetic assertion payloads.
6+
7+
use xml_sec::xmldsig::{DsigStatus, FailureReason, verify_signature_with_pem_key};
8+
9+
const IDP_RESPONSE_SIGNED_XML: &str =
10+
include_str!("fixtures/saml/response_signed_by_idp_ecdsa.xml");
11+
12+
// Fixture intentionally uses legacy SHA-1 DigestMethod for donor-compat coverage.
13+
const IDP_PUBLIC_KEY_PEM: &str = include_str!("fixtures/keys/ec/saml-idp-ecdsa-pubkey.pem");
14+
15+
#[test]
16+
fn real_saml_idp_response_signature_is_valid() {
17+
let result = verify_signature_with_pem_key(IDP_RESPONSE_SIGNED_XML, IDP_PUBLIC_KEY_PEM, true)
18+
.expect("real SAML response should verify end-to-end");
19+
20+
assert!(
21+
matches!(result.status, DsigStatus::Valid),
22+
"expected Valid status, got {:?}",
23+
result.status
24+
);
25+
assert_eq!(
26+
result.signed_info_references.len(),
27+
1,
28+
"expected exactly one SignedInfo reference"
29+
);
30+
assert!(matches!(
31+
result.signed_info_references[0].status,
32+
DsigStatus::Valid
33+
));
34+
assert!(
35+
result.signed_info_references[0].pre_digest_data.is_some(),
36+
"store_pre_digest=true must populate pre_digest_data for SignedInfo references"
37+
);
38+
}
39+
40+
#[test]
41+
fn real_saml_idp_response_detects_reference_tampering() {
42+
assert!(
43+
IDP_RESPONSE_SIGNED_XML.contains("test@example.com"),
44+
"fixture must contain the signed value being tampered with"
45+
);
46+
47+
let tampered = IDP_RESPONSE_SIGNED_XML.replacen("test@example.com", "tampered@example.com", 1);
48+
49+
assert_ne!(
50+
tampered, IDP_RESPONSE_SIGNED_XML,
51+
"tampering must change the XML so this test exercises reference digest validation"
52+
);
53+
54+
let result = verify_signature_with_pem_key(&tampered, IDP_PUBLIC_KEY_PEM, false)
55+
.expect("pipeline should complete with Invalid status on tampering");
56+
57+
assert!(matches!(
58+
result.status,
59+
DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 })
60+
));
61+
}

0 commit comments

Comments
 (0)