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
7 changes: 4 additions & 3 deletions lib/samlr/signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ def initialize(original, prefix, options)
@document = original.dup
@prefix = prefix
@options = options
@signature = nil

if @signature = document.at("#{prefix}/ds:Signature", NS_MAP)
@signature.remove # enveloped signatures only
end
id = document.at("#{prefix}")&.attribute('ID')
@signature = document.at("#{prefix}/ds:Signature/ds:SignedInfo/ds:Reference[@URI='##{id}']", NS_MAP)&.parent&.parent if id
@signature.remove if @signature # enveloped signatures only

@fingerprint = if options[:fingerprint]
Fingerprint.from_string(options[:fingerprint])
Expand Down
15 changes: 12 additions & 3 deletions lib/samlr/tools/metadata_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ def self.build(options = {})
name_identity_format = options[:name_identity_format]
consumer_service_url = options[:consumer_service_url]
consumer_service_binding = options[:consumer_service_binding] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
metadata_id = options[:metadata_id] || Samlr::Tools.uuid
sign_metadata = options[:sign_metadata] || false

# Mandatory
entity_id = options.fetch(:entity_id)
entity_id = options.fetch(:entity_id)

builder = Nokogiri::XML::Builder.new do |xml|
xml.EntityDescriptor("xmlns:md" => NS_MAP["md"], "entityID" => entity_id) do
xml.EntityDescriptor("xmlns:md" => NS_MAP["md"], "ID" => metadata_id, "entityID" => entity_id) do
xml.doc.root.namespace = xml.doc.root.namespace_definitions.find { |ns| ns.prefix == "md" }

xml["md"].SPSSODescriptor("protocolSupportEnumeration" => NS_MAP["samlp"]) do
Expand All @@ -33,7 +35,14 @@ def self.build(options = {})
end
end

builder.to_xml(COMPACT)
metadata = builder.doc

if sign_metadata
metadata_options = options.merge(namespaces: [])
metadata = ResponseBuilder.sign(metadata, metadata_id, metadata_options)
end

metadata.to_xml(COMPACT)
end

end
Expand Down
6 changes: 5 additions & 1 deletion lib/samlr/tools/response_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,11 @@ def self.sign(document, element_id, options)
end unless skip_keyinfo
end
# digest.root.last_element_child.after "<SignatureValue>#{signature}</SignatureValue>"
element.at("./saml:Issuer", NS_MAP).add_next_sibling(digest)
if element.at("./saml:Issuer", NS_MAP)
element.at("./saml:Issuer", NS_MAP).add_next_sibling(digest)
else
element.children.first.add_previous_sibling(digest)
end

document
end
Expand Down
50 changes: 50 additions & 0 deletions test/fixtures/assertion_signature_wrapping.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="samlr-be3960f5-1528-411b-ba60-aa57528d2a9e" InResponseTo="samlr-b09700a2-89ba-4c39-b7ff-6bcb34e6e10d" Version="2.0" IssueInstant="2025-10-30T13:34:17Z" Destination="https://example.org/saml/endpoint">
<saml:Issuer>ResponseBuilder IdP</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion ID="samlr-d51de4a4-e1f2-4b21-a10e-2e309f360160" IssueInstant="2025-10-30T13:34:17Z" Version="2.0">
<saml:Issuer>ResponseBuilder IdP</saml:Issuer>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="#samlr-31d06f27-5176-430b-b087-ac831b9ab9e8">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
<InclusiveNamespaces xmlns="http://www.w3.org/2001/10/xml-exc-c14n#" PrefixList=""/>
</Transform>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>RIrP+dFpc1LqZr0nUVvVNNrqBj0=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>NUShdJOltVGPmJknieVEqwKV13nRRZNrNaOE61YmAU3VvRxWBQiJClEV7VJ2jQlUxj36+5p8CSuhwwghfm3rnQ==</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>MIIBjTCCATegAwIBAgIBATANBgkqhkiG9w0BAQUFADBPMQswCQYDVQQGEwJVUzEUMBIGA1UECgwLZXhhbXBsZS5vcmcxHTAbBgNVBAsMFFphbWwgUmVzcG9uc2VCdWlsZGVyMQswCQYDVQQDDAJDQTAeFw0xMjA4MDgwMjAxMDlaFw0zMjA4MDMwMjAxMTRaME8xCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtleGFtcGxlLm9yZzEdMBsGA1UECwwUWmFtbCBSZXNwb25zZUJ1aWxkZXIxCzAJBgNVBAMMAkNBMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALb9pPmyHrbZJMDLLkVsHzzXvP7DFcPiYdaNU50l5znRr8ZGhwRZFAwKroOxXwhK5e9lz06C+kGqnL1v10h1BEUCAwEAATANBgkqhkiG9w0BAQUFAANBAKU10RznL2p7xRhO9vOh0CY+gWYmT2kbkLTVRYLApghQFAW8EzIHC/NggfEHM554ykzbbPwjSvM7cRBBDHYuWoY=</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">[email protected]</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData InResponseTo="samlr-b09700a2-89ba-4c39-b7ff-6bcb34e6e10d" NotOnOrAfter="2025-10-30T13:35:17Z" Recipient="https://example.org/saml/endpoint">
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="samlr-31d06f27-5176-430b-b087-ac831b9ab9e8" entityID="https://sp.example.com/saml2">
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:NameIDFormat>identity_format</md:NameIDFormat>
<md:AssertionConsumerService index="0" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://support.sp.example.com/"/>
</md:SPSSODescriptor>
</md:EntityDescriptor>
</saml:SubjectConfirmationData>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:AuthnStatement AuthnInstant="2025-10-30T13:34:17Z" SessionIndex="samlr-d51de4a4-e1f2-4b21-a10e-2e309f360160">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
</saml:Assertion>
</samlp:Response>
51 changes: 51 additions & 0 deletions test/fixtures/response_signature_wrapping.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="samlr-32966210-941b-414d-9e74-dd16f7e676ae" InResponseTo="samlr-2bb5bd98-44c8-4676-bb68-7a8673e141c0" Version="2.0" IssueInstant="2025-10-30T13:23:08Z" Destination="https://example.org/saml/endpoint">
<saml:Issuer>ResponseBuilder IdP</saml:Issuer>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="#samlr-222fe0da-90c6-44ed-93a4-3d79a01596d4">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
<InclusiveNamespaces xmlns="http://www.w3.org/2001/10/xml-exc-c14n#" PrefixList=""/>
</Transform>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>qdW6Sx1sUmpN/joEMOUFPO1HOAc=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>i+RCXmzCX5FMlh+0xgxkN6QjTJ5wzku36gv8hmUPB0GAN2AXHaSdYDtFLKCTa4F3MoFZq6oJeqSBfhkg/h1IKw==</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>MIIBjTCCATegAwIBAgIBATANBgkqhkiG9w0BAQUFADBPMQswCQYDVQQGEwJVUzEUMBIGA1UECgwLZXhhbXBsZS5vcmcxHTAbBgNVBAsMFFphbWwgUmVzcG9uc2VCdWlsZGVyMQswCQYDVQQDDAJDQTAeFw0xMjA4MDgwMjAxMDlaFw0zMjA4MDMwMjAxMTRaME8xCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtleGFtcGxlLm9yZzEdMBsGA1UECwwUWmFtbCBSZXNwb25zZUJ1aWxkZXIxCzAJBgNVBAMMAkNBMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALb9pPmyHrbZJMDLLkVsHzzXvP7DFcPiYdaNU50l5znRr8ZGhwRZFAwKroOxXwhK5e9lz06C+kGqnL1v10h1BEUCAwEAATANBgkqhkiG9w0BAQUFAANBAKU10RznL2p7xRhO9vOh0CY+gWYmT2kbkLTVRYLApghQFAW8EzIHC/NggfEHM554ykzbbPwjSvM7cRBBDHYuWoY=</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
<samlp:StatusDetail>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="samlr-222fe0da-90c6-44ed-93a4-3d79a01596d4" entityID="https://sp.example.com/saml2">
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:NameIDFormat>identity_format</md:NameIDFormat>
<md:AssertionConsumerService index="0" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://support.sp.example.com/"/>
</md:SPSSODescriptor>
</md:EntityDescriptor>
</samlp:StatusDetail>
</samlp:Status>
<saml:Assertion ID="samlr-43c490e8-6c91-4a6f-bf28-b52134404be6" IssueInstant="2025-10-30T13:23:08Z" Version="2.0">
<saml:Issuer>ResponseBuilder IdP</saml:Issuer>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">[email protected]</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData InResponseTo="samlr-2bb5bd98-44c8-4676-bb68-7a8673e141c0" NotOnOrAfter="2025-10-30T13:24:08Z" Recipient="https://example.org/saml/endpoint"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:AuthnStatement AuthnInstant="2025-10-30T13:23:08Z" SessionIndex="samlr-43c490e8-6c91-4a6f-bf28-b52134404be6">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
</saml:Assertion>
</samlp:Response>
16 changes: 16 additions & 0 deletions test/unit/test_assertion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@
end
end

describe "#signature" do
it "is associated to the assertion" do
assert subject.signature.present?
end

describe "when assertion envelops a signature referencing other element" do
let(:fingerprint) { Samlr::Certificate.new(TEST_CERTIFICATE.x509).fingerprint.value }
let(:xml_response_doc) { Base64.encode64(File.read(File.join('.', 'test', 'fixtures', 'assertion_signature_wrapping.xml'))) }
subject { Samlr::Response.new(xml_response_doc, fingerprint: fingerprint).assertion }

it "does not associate it with the assertion" do
assert subject.signature.missing?
end
end
end

describe "#verify!" do
let(:condition) do
Class.new do
Expand Down
8 changes: 4 additions & 4 deletions test/unit/test_condition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def verify!
verify!
flunk "Expected exception"
rescue Samlr::ConditionsError => e
assert_match /Audience/, e.message
assert_match %r{Audience}, e.message
end
end
end
Expand Down Expand Up @@ -132,7 +132,7 @@ def verify!
verify!
flunk "Expected exception"
rescue Samlr::ConditionsError => e
assert_match /Audience/, e.message
assert_match %r{Audience}, e.message
end
end
end
Expand All @@ -151,7 +151,7 @@ def verify!
subject.verify!
flunk "Expected exception"
rescue Samlr::ConditionsError => e
assert_match /Not before/, e.message
assert_match %r{Not before}, e.message
end
end
end
Expand All @@ -168,7 +168,7 @@ def verify!
subject.verify!
flunk "Expected exception"
rescue Samlr::ConditionsError => e
assert_match /Not on or after/, e.message
assert_match %r{Not on or after}, e.message
end
end
end
Expand Down
10 changes: 5 additions & 5 deletions test/unit/test_logout_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,29 +57,29 @@ def capture_stderr
request.body
end

body.must_include '<saml:NameID Format="some format">'
stderr.must_equal "[DEPRECATION] options[:name_id_format] is deprecated. Please use options[:name_id_options][:format] instead\n"
_(body).must_include '<saml:NameID Format="some format">'
_(stderr).must_equal "[DEPRECATION] options[:name_id_format] is deprecated. Please use options[:name_id_options][:format] instead\n"
end

it "understands [:name_id_options][:format]" do
options.merge!(:name_id_options => {:format => "some format"})
request = Samlr::LogoutRequest.new(nil, options)

assert_match /<saml:NameID Format="some format">/, request.body
assert_match %r{<saml:NameID Format="some format">}, request.body
end

it "understands NameQualifier" do
options.merge!(:name_id_options => {:name_qualifier => "Some name qualifier"})
request = Samlr::LogoutRequest.new(nil, options)

assert_match /NameQualifier="Some name qualifier"/, request.body
assert_match %r{NameQualifier="Some name qualifier"}, request.body
end

it "understands SPNameQualifier" do
options.merge!(:name_id_options => {:spname_qualifier => "Some SPName qualifier"})
request = Samlr::LogoutRequest.new(nil, options)

assert_match /SPNameQualifier="Some SPName qualifier"/, request.body
assert_match %r{SPNameQualifier="Some SPName qualifier"}, request.body
end
end

Expand Down
43 changes: 41 additions & 2 deletions test/unit/test_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,45 @@
end
end

describe "#signature" do
let(:metadata_doc) do
Nokogiri::XML(
Samlr::Tools::MetadataBuilder.build(
:entity_id => "https://sp.example.com/saml2",
:name_identity_format => "identity_format",
:consumer_service_url => "https://support.sp.example.com/",
:sign_metadata => true,
:certificate => TEST_CERTIFICATE
)
)
end

it "is associated to the response" do
assert subject.signature.present?
end

describe "when response envelops a signature" do
let(:fingerprint) { Samlr::Certificate.new(TEST_CERTIFICATE.x509).fingerprint.value }
let(:saml_response) { Samlr::Response.new(xml_response_doc, fingerprint: fingerprint) }

describe "referencing other response" do
let(:xml_response_doc) { Base64.encode64(File.read(File.join('.', 'test', 'fixtures', 'multiple_responses.xml'))) }

it "does not associate it with the response" do
assert saml_response.signature.missing?
end
end

describe "referencing other element" do
let(:xml_response_doc) { Base64.encode64(File.read(File.join('.', 'test', 'fixtures', 'response_signature_wrapping.xml'))) }

it "does not associate it with the response" do
assert saml_response.signature.missing?
end
end
end
end

describe "XSW attack" do
it "should not validate if SAML response is hacked" do
document = saml_response_document(:certificate => TEST_CERTIFICATE)
Expand Down Expand Up @@ -85,12 +124,12 @@
let(:saml_resp) { Samlr::Response.new(saml_response_doc, fingerprint: Samlr::FingerprintSHA256.x509(TEST_CERTIFICATE.x509)) }

it "validates the saml response" do
assert_match /[email protected]<!---->.evil.com/, saml_response_doc
assert_match %r{[email protected]<!---->.evil.com}, saml_response_doc
assert saml_resp.verify!
end

it "ignores the comment and parses the name_id XML node correctly" do
assert_match /[email protected]<!---->.evil.com/, saml_response_doc
assert_match %r{[email protected]<!---->.evil.com}, saml_response_doc
assert_equal "[email protected]", saml_resp.name_id
end
end
Expand Down
9 changes: 0 additions & 9 deletions test/unit/test_response_scenarios.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,6 @@
end
end

describe "invalid multiple saml responses" do
let(:xml_response_doc) { Base64.encode64(File.read(File.join('.', 'test', 'fixtures', 'multiple_responses.xml'))) }
let(:saml_response) { Samlr::Response.new(xml_response_doc, fingerprint: '6F:B9:D2:55:52:E8:81:0C:F2:91:97:3D:CE:60:08:82:09:96:27:77:3C:FF:33:A2:0E:04:A6:01:D1:B8:CA:1D') }

it "fails" do
assert_raises(Samlr::FormatError) { saml_response.verify! }
end
end

describe "an unsatisfied before condition" do
subject { saml_response(:certificate => TEST_CERTIFICATE, :not_before => Samlr::Tools::Timestamp.stamp(Time.now + 60)) }

Expand Down
Loading