Skip to content

Commit

Permalink
Merge pull request #213 from onelogin/Umofomia-sign-metadata
Browse files Browse the repository at this point in the history
Add ability to sign metadata. (Improved)
  • Loading branch information
pitbulk committed Mar 26, 2015
2 parents d788072 + e22283f commit 4927043
Show file tree
Hide file tree
Showing 12 changed files with 144 additions and 83 deletions.
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
6 changes: 3 additions & 3 deletions 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) 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 Expand Up @@ -146,13 +146,13 @@ class RequestTest < Minitest::Test

params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings)
assert params['Signature']
assert params['SigAlg'] == XMLSecurity::Document::RSA_SHA1
assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA1

# signature_method only affects the embedeed signature
settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256
params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings)
assert params['Signature']
assert params['SigAlg'] == XMLSecurity::Document::RSA_SHA1
assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA1
end
end

Expand Down
153 changes: 98 additions & 55 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_equal xml_text[0..start.length-1],start

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"
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)
start = "<?xml version='1.0' encoding='UTF-8'?><md:EntityDescriptor"
assert_equal xml_text[0..start.length-1], start

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) 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
6 changes: 3 additions & 3 deletions 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) request" do
it "create a signed request" do
settings = OneLogin::RubySaml::Settings.new
settings.compress_request = false
Expand Down Expand Up @@ -195,13 +195,13 @@ class RequestTest < Minitest::Test

params = OneLogin::RubySaml::Authrequest.new.create_params(settings)
assert params['Signature']
assert params['SigAlg'] == XMLSecurity::Document::RSA_SHA1
assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA1

# signature_method only affects the embedeed signature
settings.security[:signature_method] = XMLSecurity::Document::SHA256
params = OneLogin::RubySaml::Authrequest.new.create_params(settings)
assert params['Signature']
assert params['SigAlg'] == XMLSecurity::Document::RSA_SHA1
assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA1
end
end

Expand Down
Loading

0 comments on commit 4927043

Please sign in to comment.