diff --git a/tests/fixtures/keys/ec/saml-idp-ecdsa-pubkey.pem b/tests/fixtures/keys/ec/saml-idp-ecdsa-pubkey.pem new file mode 100644 index 0000000..dd22360 --- /dev/null +++ b/tests/fixtures/keys/ec/saml-idp-ecdsa-pubkey.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyjU9gkG4ffc3WwyLF2Q4lmRlMmnw +lzJd31gHv5qBg74j1kKSaQWDZEkTHFt4g7AqIlRRqDt/u9euxVNa5RLqxg== +-----END PUBLIC KEY----- diff --git a/tests/fixtures/saml/response_signed_by_idp_ecdsa.xml b/tests/fixtures/saml/response_signed_by_idp_ecdsa.xml new file mode 100644 index 0000000..e80f1db --- /dev/null +++ b/tests/fixtures/saml/response_signed_by_idp_ecdsa.xml @@ -0,0 +1,68 @@ + + + https://fujifish.github.io/samling/samling.html + + + + + + + + + + + W7iYqYBNLg7dS+ueqLf04nO5V+c= + + + THCZWgdX01bDRNyUHHS+u3U7URTI4c3+1cuXKeWFQDjX/yjrC6V/6wCwXtD4VyjU +aUxevxscW8FBCRTkwDR78A== + + +MIIBhzCCAS0CFGE3kR43hTxJz3hg+bsefDiZjTSiMAoGCCqGSM49BAMCMEUxCzAJ +BgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l +dCBXaWRnaXRzIFB0eSBMdGQwIBcNMjQwNjIzMTc0NTQ5WhgPMzAyMzEwMjUxNzQ1 +NDlaMEUxCzAJBgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK +DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwWTATBgcqhkjOPQIBBggqhkjOPQMB +BwNCAATKNT2CQbh99zdbDIsXZDiWZGUyafCXMl3fWAe/moGDviPWQpJpBYNkSRMc +W3iDsCoiVFGoO3+7167FU1rlEurGMAoGCCqGSM49BAMCA0gAMEUCIQCdW4SacWlI +qj04IXo5QNWgbIrG6MKcXbvWEXDmMkiIewIgHkDlDn8Aq4reI+4BvUN+ZDmvOs1I +UevJyxGd/2RkolE= + + + + + + + + https://fujifish.github.io/samling/samling.html + + + _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 + + + + + + + http://test_accept_signed_with_correct_key.test + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + + + test + + + test@example.com + + + users + examplerole1 + + + + diff --git a/tests/fixtures_smoke.rs b/tests/fixtures_smoke.rs index 0549b45..0819261 100644 --- a/tests/fixtures_smoke.rs +++ b/tests/fixtures_smoke.rs @@ -55,6 +55,7 @@ fn ec_p256_key_files_are_valid_pem() { assert_pem_file(&dir.join("ec-prime256v1-key.pem"), "PRIVATE KEY"); assert_pem_file(&dir.join("ec-prime256v1-cert.pem"), "CERTIFICATE"); assert_pem_file(&dir.join("ec-prime256v1-pubkey.pem"), "PUBLIC KEY"); + assert_pem_file(&dir.join("saml-idp-ecdsa-pubkey.pem"), "PUBLIC KEY"); } /// Verify EC P-384 key triplet exists and contains valid PEM markers. @@ -175,8 +176,8 @@ fn fixture_file_count_matches_expected() { let mut count = 0; count_files_recursive(fixtures_dir(), &mut count); assert_eq!( - count, 77, - "expected 77 fixture files total (22 keys + 41 c14n + 14 donor xmldsig); \ + count, 79, + "expected 79 fixture files total (23 keys + 41 c14n + 14 donor xmldsig + 1 saml); \ if you added/removed files, update this count" ); } diff --git a/tests/saml_idp_integration.rs b/tests/saml_idp_integration.rs new file mode 100644 index 0000000..71bba5c --- /dev/null +++ b/tests/saml_idp_integration.rs @@ -0,0 +1,61 @@ +//! Integration tests for real-world SAML response verification. +//! Covers PR #44. +//! +//! Uses a donor SAML 2.0 IdP response fixture to ensure XMLDSig verification +//! works against non-synthetic assertion payloads. + +use xml_sec::xmldsig::{DsigStatus, FailureReason, verify_signature_with_pem_key}; + +const IDP_RESPONSE_SIGNED_XML: &str = + include_str!("fixtures/saml/response_signed_by_idp_ecdsa.xml"); + +// Fixture intentionally uses legacy SHA-1 DigestMethod for donor-compat coverage. +const IDP_PUBLIC_KEY_PEM: &str = include_str!("fixtures/keys/ec/saml-idp-ecdsa-pubkey.pem"); + +#[test] +fn real_saml_idp_response_signature_is_valid() { + let result = verify_signature_with_pem_key(IDP_RESPONSE_SIGNED_XML, IDP_PUBLIC_KEY_PEM, true) + .expect("real SAML response should verify end-to-end"); + + assert!( + matches!(result.status, DsigStatus::Valid), + "expected Valid status, got {:?}", + result.status + ); + assert_eq!( + result.signed_info_references.len(), + 1, + "expected exactly one SignedInfo reference" + ); + assert!(matches!( + result.signed_info_references[0].status, + DsigStatus::Valid + )); + assert!( + result.signed_info_references[0].pre_digest_data.is_some(), + "store_pre_digest=true must populate pre_digest_data for SignedInfo references" + ); +} + +#[test] +fn real_saml_idp_response_detects_reference_tampering() { + assert!( + IDP_RESPONSE_SIGNED_XML.contains("test@example.com"), + "fixture must contain the signed value being tampered with" + ); + + let tampered = IDP_RESPONSE_SIGNED_XML.replacen("test@example.com", "tampered@example.com", 1); + + assert_ne!( + tampered, IDP_RESPONSE_SIGNED_XML, + "tampering must change the XML so this test exercises reference digest validation" + ); + + let result = verify_signature_with_pem_key(&tampered, IDP_PUBLIC_KEY_PEM, false) + .expect("pipeline should complete with Invalid status on tampering"); + + assert!(matches!( + result.status, + DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { ref_index: 0 }) + )); +}