Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to sign metadata. (Improved) #213

Merged
merged 9 commits into from
Mar 26, 2015
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,14 +336,17 @@ In order to be able to sign we need first to define the private key and the publ
The settings related to sign are stored in the `security` attribute of the settings:

```ruby
settings.security[:authn_requests_signed] = true # Enable or not signature on AuthNRequest
settings.security[:logout_requests_signed] = true # Enable or not signature on Logout Request
settings.security[:authn_requests_signed] = true # Enable or not signature on AuthNRequest
settings.security[:logout_requests_signed] = true # Enable or not signature on Logout Request
settings.security[:logout_responses_signed] = true # Enable or not signature on Logout Response
settings.security[:metadata_signed] = true # Enable or not signature on Metadata

settings.security[:digest_method] = XMLSecurity::Document::SHA1
settings.security[:signature_method] = XMLSecurity::Document::SHA1

settings.security[:embed_sign] = false # Embeded signature or HTTP GET parameter Signature
# Embeded signature or HTTP GET parameter signature
# Note that metadata signature is always embedded regardless of this value.
settings.security[:embed_sign] = false
```


Expand Down
2 changes: 1 addition & 1 deletion lib/onelogin/ruby-saml/authrequest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def create_authentication_xml_doc(settings)
end
end

# embebed sign
# embed signature
if settings.security[:authn_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
private_key = settings.get_sp_key()
cert = settings.get_sp_cert()
Expand Down
2 changes: 1 addition & 1 deletion lib/onelogin/ruby-saml/logoutrequest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def create_logout_request_xml_doc(settings)
sessionindex.text = settings.sessionindex
end

# embebed sign
# embed signature
if settings.security[:logout_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
private_key = settings.get_sp_key()
cert = settings.get_sp_cert()
Expand Down
22 changes: 16 additions & 6 deletions lib/onelogin/ruby-saml/metadata.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
require "rexml/document"
require "rexml/xpath"
require "uri"
require "uuid"

require "onelogin/ruby-saml/logging"

Expand All @@ -10,10 +9,9 @@
# will be updated automatically
module OneLogin
module RubySaml
include REXML
class Metadata
def generate(settings)
meta_doc = REXML::Document.new
def generate(settings, pretty_print=true)
meta_doc = XMLSecurity::Document.new
root = meta_doc.add_element "md:EntityDescriptor", {
"xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata"
}
Expand All @@ -23,6 +21,7 @@ def generate(settings)
# However we would like assertions signed if idp_cert_fingerprint or idp_cert is set
"WantAssertionsSigned" => !!(settings.idp_cert_fingerprint || settings.idp_cert)
}
root.attributes["ID"] = "_" + UUID.new.generate
if settings.issuer
root.attributes["entityID"] = settings.issuer
end
Expand Down Expand Up @@ -85,9 +84,20 @@ def generate(settings)
# <md:XACMLAuthzDecisionQueryDescriptor WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>

meta_doc << REXML::XMLDecl.new("1.0", "UTF-8")

# embed signature
if settings.security[:metadata_signed] && settings.private_key && settings.certificate
private_key = settings.get_sp_key()
meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
end

ret = ""
# pretty print the XML so IdP administrators can easily see what the SP supports
meta_doc.write(ret, 1)
if pretty_print
meta_doc.write(ret, 1)
else
ret = meta_doc.to_s
end

return ret
end
Expand Down
3 changes: 2 additions & 1 deletion lib/onelogin/ruby-saml/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ def get_sp_key
:security => {
:authn_requests_signed => false,
:logout_requests_signed => false,
:logout_responses_signed => false,
:logout_responses_signed => false,
:metadata_signed => false,
:embed_sign => false,
:digest_method => XMLSecurity::Document::SHA1,
:signature_method => XMLSecurity::Document::RSA_SHA1
Expand Down
2 changes: 1 addition & 1 deletion lib/onelogin/ruby-saml/slo_logoutresponse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def create_logout_response_xml_doc(settings, request_id = nil, logout_message =
issuer.text = settings.issuer
end

# embebed sign
# embed signature
if settings.security[:logout_responses_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
private_key = settings.get_sp_key()
cert = settings.get_sp_cert()
Expand Down
14 changes: 9 additions & 5 deletions lib/xml_security.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ def algorithm(element)
algorithm = element
if algorithm.is_a?(REXML::Element)
algorithm = element.attribute("Algorithm").value
algorithm = algorithm && algorithm =~ /sha(.*?)$/i && $1.to_i
end

algorithm = algorithm && algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i

case algorithm
when 256 then OpenSSL::Digest::SHA256
when 384 then OpenSSL::Digest::SHA384
Expand All @@ -80,7 +81,7 @@ class Document < BaseDocument
SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"
SHA512 = "http://www.w3.org/2001/04/xmldsig-more#sha512"
ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
INC_PREFIX_LIST = "#default samlp saml ds xs xsi"
INC_PREFIX_LIST = "#default samlp saml ds xs xsi md"

attr_accessor :uuid

Expand Down Expand Up @@ -119,8 +120,6 @@ def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_
# Add Transforms
transforms_element = reference_element.add_element("ds:Transforms")
transforms_element.add_element("ds:Transform", {"Algorithm" => ENVELOPED_SIG})
#transforms_element.add_element("ds:Transform", {"Algorithm" => C14N})
#transforms_element.add_element("ds:InclusiveNamespaces", {"xmlns" => C14N, "PrefixList" => INC_PREFIX_LIST})
c14element = transforms_element.add_element("ds:Transform", {"Algorithm" => C14N})
c14element.add_element("ec:InclusiveNamespaces", {"xmlns:ec" => C14N, "PrefixList" => INC_PREFIX_LIST})

Expand All @@ -133,6 +132,7 @@ def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_
noko_sig_element = Nokogiri.parse(signature_element.to_s)
noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG)
canon_string = noko_signed_info_element.canonicalize(canon_algorithm(C14N))

signature = compute_signature(private_key, algorithm(signature_method).new, canon_string)
signature_element.add_element("ds:SignatureValue").text = signature

Expand All @@ -150,7 +150,11 @@ def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_
if issuer_element
self.root.insert_after issuer_element, signature_element
else
self.root.add_element(signature_element)
if sp_sso_descriptor = self.elements["/md:EntityDescriptor"]
self.root.insert_before sp_sso_descriptor, signature_element
else
self.root.add_element(signature_element)
end
end
end

Expand Down
2 changes: 1 addition & 1 deletion test/logoutrequest_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class RequestTest < Minitest::Test
end
end

describe "when the settings indicate to sign (embebed) the logout request" do
describe "when the settings indicate to sign (embedded) the logout request" do
it "created a signed logout request" do
settings = OneLogin::RubySaml::Settings.new
settings.idp_slo_target_url = "http://example.com?field=value"
Expand Down
151 changes: 97 additions & 54 deletions test/metadata_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,86 +5,129 @@
class MetadataTest < Minitest::Test

describe 'Metadata' do
def setup
@settings = OneLogin::RubySaml::Settings.new
@settings.issuer = "https://example.com"
@settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
@settings.assertion_consumer_service_url = "https://foo.example/saml/consume"
@settings.security[:authn_requests_signed] = false
let(:settings) { OneLogin::RubySaml::Settings.new }
let(:xml_text) { OneLogin::RubySaml::Metadata.new.generate(settings, false) }
let(:xml_doc) { REXML::Document.new(xml_text) }
let(:spsso_descriptor) { REXML::XPath.first(xml_doc, "//md:SPSSODescriptor") }
let(:acs) { REXML::XPath.first(xml_doc, "//md:AssertionConsumerService") }

before do
settings.issuer = "https://example.com"
settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
settings.assertion_consumer_service_url = "https://foo.example/saml/consume"
end

it "generates Service Provider Metadata with X509Certificate" do
@settings.security[:authn_requests_signed] = true
@settings.certificate = ruby_saml_cert_text
it "generates Pretty Print Service Provider Metadata" do
xml_text = OneLogin::RubySaml::Metadata.new.generate(settings, true)
# assert correct xml declaration
start = "<?xml version='1.0' encoding='UTF-8'?>\n<md:EntityDescriptor"
assert xml_text[0..start.length-1] == start
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably good to use assert_equal here to be consistent


xml_text = OneLogin::RubySaml::Metadata.new.generate(@settings)
assert_equal "https://example.com", REXML::XPath.first(xml_doc, "//md:EntityDescriptor").attribute("entityID").value

# assert xml_text can be parsed into an xml doc
xml_doc = REXML::Document.new(xml_text)
assert_equal "urn:oasis:names:tc:SAML:2.0:protocol", spsso_descriptor.attribute("protocolSupportEnumeration").value
assert_equal "false", spsso_descriptor.attribute("AuthnRequestsSigned").value
assert_equal "false", spsso_descriptor.attribute("WantAssertionsSigned").value

spsso_descriptor = REXML::XPath.first(xml_doc, "//md:SPSSODescriptor")
assert_equal "true", spsso_descriptor.attribute("AuthnRequestsSigned").value
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", REXML::XPath.first(xml_doc, "//md:NameIDFormat").text.strip

cert_node = REXML::XPath.first(xml_doc, "//md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate", {
"md" => "urn:oasis:names:tc:SAML:2.0:metadata",
"ds" => "http://www.w3.org/2000/09/xmldsig#"
})
cert_text = cert_node.text
cert = OpenSSL::X509::Certificate.new(Base64.decode64(cert_text))
assert_equal ruby_saml_cert.to_der, cert.to_der
assert_equal "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", acs.attribute("Binding").value
assert_equal "https://foo.example/saml/consume", acs.attribute("Location").value
end

it "generates Service Provider Metadata" do
settings = OneLogin::RubySaml::Settings.new
settings.issuer = "https://example.com"
settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
settings.assertion_consumer_service_url = "https://foo.example/saml/consume"
settings.security[:authn_requests_signed] = false

xml_text = OneLogin::RubySaml::Metadata.new.generate(settings)

# assert correct xml declaration
start = "<?xml version='1.0' encoding='UTF-8'?>\n<md:EntityDescriptor"
start = "<?xml version='1.0' encoding='UTF-8'?><md:EntityDescriptor"
assert xml_text[0..start.length-1] == start

# assert xml_text can be parsed into an xml doc
xml_doc = REXML::Document.new(xml_text)

assert_equal "https://example.com", REXML::XPath.first(xml_doc, "//md:EntityDescriptor").attribute("entityID").value

spsso_descriptor = REXML::XPath.first(xml_doc, "//md:SPSSODescriptor")
assert_equal "urn:oasis:names:tc:SAML:2.0:protocol", spsso_descriptor.attribute("protocolSupportEnumeration").value
assert_equal "false", spsso_descriptor.attribute("AuthnRequestsSigned").value
assert_equal "false", spsso_descriptor.attribute("WantAssertionsSigned").value

assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", REXML::XPath.first(xml_doc, "//md:NameIDFormat").text.strip

acs = REXML::XPath.first(xml_doc, "//md:AssertionConsumerService")
assert_equal "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", acs.attribute("Binding").value
assert_equal "https://foo.example/saml/consume", acs.attribute("Location").value
end

it "generates attribute service if configured" do
settings = OneLogin::RubySaml::Settings.new
settings.issuer = "https://example.com"
settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
settings.assertion_consumer_service_url = "https://foo.example/saml/consume"
settings.attribute_consuming_service.configure do
service_name "Test Service"
add_attribute(:name => "Name", :name_format => "Name Format", :friendly_name => "Friendly Name", :attribute_value => "Attribute Value")
describe "when auth requests are signed" do
let(:cert_node) do
REXML::XPath.first(
xml_doc,
"//md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
"md" => "urn:oasis:names:tc:SAML:2.0:metadata",
"ds" => "http://www.w3.org/2000/09/xmldsig#"
)
end
let(:cert) { OpenSSL::X509::Certificate.new(Base64.decode64(cert_node.text)) }

before do
settings.security[:authn_requests_signed] = true
settings.certificate = ruby_saml_cert_text
end

xml_text = OneLogin::RubySaml::Metadata.new.generate(settings)
xml_doc = REXML::Document.new(xml_text)
acs = REXML::XPath.first(xml_doc, "//md:AttributeConsumingService")
assert_equal "true", acs.attribute("isDefault").value
assert_equal "1", acs.attribute("index").value
assert_equal REXML::XPath.first(xml_doc, "//md:ServiceName").text.strip, "Test Service"
req_attr = REXML::XPath.first(xml_doc, "//md:RequestedAttribute")
assert_equal "Name", req_attr.attribute("Name").value
assert_equal "Name Format", req_attr.attribute("NameFormat").value
assert_equal "Friendly Name", req_attr.attribute("FriendlyName").value
assert_equal "Attribute Value", REXML::XPath.first(xml_doc, "//md:AttributeValue").text.strip
it "generates Service Provider Metadata with X509Certificate" do
assert_equal "true", spsso_descriptor.attribute("AuthnRequestsSigned").value
assert_equal ruby_saml_cert.to_der, cert.to_der
end
end

describe "when attribute service is configured" do
let(:attr_svc) { REXML::XPath.first(xml_doc, "//md:AttributeConsumingService") }
let(:req_attr) { REXML::XPath.first(xml_doc, "//md:RequestedAttribute") }

before do
settings.attribute_consuming_service.configure do
service_name "Test Service"
add_attribute(:name => "Name", :name_format => "Name Format", :friendly_name => "Friendly Name", :attribute_value => "Attribute Value")
end
end

it "generates attribute service" do
assert_equal "true", attr_svc.attribute("isDefault").value
assert_equal "1", attr_svc.attribute("index").value
assert_equal REXML::XPath.first(xml_doc, "//md:ServiceName").text.strip, "Test Service"

assert_equal "Name", req_attr.attribute("Name").value
assert_equal "Name Format", req_attr.attribute("NameFormat").value
assert_equal "Friendly Name", req_attr.attribute("FriendlyName").value
assert_equal "Attribute Value", REXML::XPath.first(xml_doc, "//md:AttributeValue").text.strip
end
end

describe "when the settings indicate to sign (embedded) the metadata" do
before do
settings.security[:metadata_signed] = true
settings.certificate = ruby_saml_cert_text
settings.private_key = ruby_saml_key_text
end

it "creates a signed metadata" do
assert_match %r[<ds:SignatureValue>([a-zA-Z0-9/+=]+)</ds:SignatureValue>]m, xml_text
assert_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1'/>], xml_text
assert_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2000/09/xmldsig#sha1'/>], xml_text
signed_metadata = XMLSecurity::SignedDocument.new(xml_text)
assert signed_metadata.validate_document(ruby_saml_cert_fingerprint, false)
end

describe "when digest and signature methods are specified" do
before do
settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256
settings.security[:digest_method] = XMLSecurity::Document::SHA512
end

it "creates a signed metadata with specified digest and signature methods" do
assert_match %r[<ds:SignatureValue>([a-zA-Z0-9/+=]+)</ds:SignatureValue>]m, xml_text
assert_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'/>], xml_text
assert_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2001/04/xmldsig-more#sha512'/>], xml_text

signed_metadata_2 = XMLSecurity::SignedDocument.new(xml_text)

assert signed_metadata_2.validate_document(ruby_saml_cert_fingerprint, false)
end
end
end
end
end
2 changes: 1 addition & 1 deletion test/request_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ class RequestTest < Minitest::Test
end
end

describe "when the settings indicate to sign (embebed) the request" do
describe "when the settings indicate to sign (embedded) the request" do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

describe 'when the settings indicate to sign (embedded) request' do

it "create a signed request" do
settings = OneLogin::RubySaml::Settings.new
settings.compress_request = false
Expand Down
2 changes: 1 addition & 1 deletion test/slo_logoutresponse_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class SloLogoutresponseTest < Minitest::Test
assert_match /<samlp:StatusMessage>Custom Logout Message<\/samlp:StatusMessage>/, inflated
end

describe "when the settings indicate to sign (embebed) the logout response" do
describe "when the settings indicate to sign (embedded) the logout response" do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

describe 'when the settings indicate to sign (embedded) logout response' do

it "create a signed logout response" do
settings = OneLogin::RubySaml::Settings.new
settings.compress_response = false
Expand Down