Skip to content

Commit 4927043

Browse files
committed
Merge pull request #213 from onelogin/Umofomia-sign-metadata
Add ability to sign metadata. (Improved)
2 parents d788072 + e22283f commit 4927043

File tree

12 files changed

+144
-83
lines changed

12 files changed

+144
-83
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,14 +336,17 @@ In order to be able to sign we need first to define the private key and the publ
336336
The settings related to sign are stored in the `security` attribute of the settings:
337337
338338
```ruby
339-
settings.security[:authn_requests_signed] = true # Enable or not signature on AuthNRequest
340-
settings.security[:logout_requests_signed] = true # Enable or not signature on Logout Request
339+
settings.security[:authn_requests_signed] = true # Enable or not signature on AuthNRequest
340+
settings.security[:logout_requests_signed] = true # Enable or not signature on Logout Request
341341
settings.security[:logout_responses_signed] = true # Enable or not signature on Logout Response
342+
settings.security[:metadata_signed] = true # Enable or not signature on Metadata
342343
343344
settings.security[:digest_method] = XMLSecurity::Document::SHA1
344345
settings.security[:signature_method] = XMLSecurity::Document::SHA1
345346
346-
settings.security[:embed_sign] = false # Embeded signature or HTTP GET parameter Signature
347+
# Embeded signature or HTTP GET parameter signature
348+
# Note that metadata signature is always embedded regardless of this value.
349+
settings.security[:embed_sign] = false
347350
```
348351
349352

lib/onelogin/ruby-saml/authrequest.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def create_authentication_xml_doc(settings)
113113
end
114114
end
115115

116-
# embebed sign
116+
# embed signature
117117
if settings.security[:authn_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
118118
private_key = settings.get_sp_key()
119119
cert = settings.get_sp_cert()

lib/onelogin/ruby-saml/logoutrequest.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def create_logout_request_xml_doc(settings)
8989
sessionindex.text = settings.sessionindex
9090
end
9191

92-
# embebed sign
92+
# embed signature
9393
if settings.security[:logout_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
9494
private_key = settings.get_sp_key()
9595
cert = settings.get_sp_cert()

lib/onelogin/ruby-saml/metadata.rb

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
require "rexml/document"
2-
require "rexml/xpath"
31
require "uri"
2+
require "uuid"
43

54
require "onelogin/ruby-saml/logging"
65

@@ -10,10 +9,9 @@
109
# will be updated automatically
1110
module OneLogin
1211
module RubySaml
13-
include REXML
1412
class Metadata
15-
def generate(settings)
16-
meta_doc = REXML::Document.new
13+
def generate(settings, pretty_print=true)
14+
meta_doc = XMLSecurity::Document.new
1715
root = meta_doc.add_element "md:EntityDescriptor", {
1816
"xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata"
1917
}
@@ -23,6 +21,7 @@ def generate(settings)
2321
# However we would like assertions signed if idp_cert_fingerprint or idp_cert is set
2422
"WantAssertionsSigned" => !!(settings.idp_cert_fingerprint || settings.idp_cert)
2523
}
24+
root.attributes["ID"] = "_" + UUID.new.generate
2625
if settings.issuer
2726
root.attributes["entityID"] = settings.issuer
2827
end
@@ -85,9 +84,20 @@ def generate(settings)
8584
# <md:XACMLAuthzDecisionQueryDescriptor WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
8685

8786
meta_doc << REXML::XMLDecl.new("1.0", "UTF-8")
87+
88+
# embed signature
89+
if settings.security[:metadata_signed] && settings.private_key && settings.certificate
90+
private_key = settings.get_sp_key()
91+
meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
92+
end
93+
8894
ret = ""
8995
# pretty print the XML so IdP administrators can easily see what the SP supports
90-
meta_doc.write(ret, 1)
96+
if pretty_print
97+
meta_doc.write(ret, 1)
98+
else
99+
ret = meta_doc.to_s
100+
end
91101

92102
return ret
93103
end

lib/onelogin/ruby-saml/settings.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ def get_sp_key
111111
:security => {
112112
:authn_requests_signed => false,
113113
:logout_requests_signed => false,
114-
:logout_responses_signed => false,
114+
:logout_responses_signed => false,
115+
:metadata_signed => false,
115116
:embed_sign => false,
116117
:digest_method => XMLSecurity::Document::SHA1,
117118
:signature_method => XMLSecurity::Document::RSA_SHA1

lib/onelogin/ruby-saml/slo_logoutresponse.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def create_logout_response_xml_doc(settings, request_id = nil, logout_message =
8787
issuer.text = settings.issuer
8888
end
8989

90-
# embebed sign
90+
# embed signature
9191
if settings.security[:logout_responses_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
9292
private_key = settings.get_sp_key()
9393
cert = settings.get_sp_cert()

lib/xml_security.rb

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ def algorithm(element)
5656
algorithm = element
5757
if algorithm.is_a?(REXML::Element)
5858
algorithm = element.attribute("Algorithm").value
59-
algorithm = algorithm && algorithm =~ /sha(.*?)$/i && $1.to_i
6059
end
6160

61+
algorithm = algorithm && algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
62+
6263
case algorithm
6364
when 256 then OpenSSL::Digest::SHA256
6465
when 384 then OpenSSL::Digest::SHA384
@@ -80,7 +81,7 @@ class Document < BaseDocument
8081
SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"
8182
SHA512 = "http://www.w3.org/2001/04/xmldsig-more#sha512"
8283
ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
83-
INC_PREFIX_LIST = "#default samlp saml ds xs xsi"
84+
INC_PREFIX_LIST = "#default samlp saml ds xs xsi md"
8485

8586
attr_accessor :uuid
8687

@@ -119,8 +120,6 @@ def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_
119120
# Add Transforms
120121
transforms_element = reference_element.add_element("ds:Transforms")
121122
transforms_element.add_element("ds:Transform", {"Algorithm" => ENVELOPED_SIG})
122-
#transforms_element.add_element("ds:Transform", {"Algorithm" => C14N})
123-
#transforms_element.add_element("ds:InclusiveNamespaces", {"xmlns" => C14N, "PrefixList" => INC_PREFIX_LIST})
124123
c14element = transforms_element.add_element("ds:Transform", {"Algorithm" => C14N})
125124
c14element.add_element("ec:InclusiveNamespaces", {"xmlns:ec" => C14N, "PrefixList" => INC_PREFIX_LIST})
126125

@@ -133,6 +132,7 @@ def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_
133132
noko_sig_element = Nokogiri.parse(signature_element.to_s)
134133
noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG)
135134
canon_string = noko_signed_info_element.canonicalize(canon_algorithm(C14N))
135+
136136
signature = compute_signature(private_key, algorithm(signature_method).new, canon_string)
137137
signature_element.add_element("ds:SignatureValue").text = signature
138138

@@ -150,7 +150,11 @@ def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_
150150
if issuer_element
151151
self.root.insert_after issuer_element, signature_element
152152
else
153-
self.root.add_element(signature_element)
153+
if sp_sso_descriptor = self.elements["/md:EntityDescriptor"]
154+
self.root.insert_before sp_sso_descriptor, signature_element
155+
else
156+
self.root.add_element(signature_element)
157+
end
154158
end
155159
end
156160

test/logoutrequest_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class RequestTest < Minitest::Test
9090
end
9191
end
9292

93-
describe "when the settings indicate to sign (embebed) the logout request" do
93+
describe "when the settings indicate to sign (embedded) logout request" do
9494
it "created a signed logout request" do
9595
settings = OneLogin::RubySaml::Settings.new
9696
settings.idp_slo_target_url = "http://example.com?field=value"
@@ -146,13 +146,13 @@ class RequestTest < Minitest::Test
146146

147147
params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings)
148148
assert params['Signature']
149-
assert params['SigAlg'] == XMLSecurity::Document::RSA_SHA1
149+
assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA1
150150

151151
# signature_method only affects the embedeed signature
152152
settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256
153153
params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings)
154154
assert params['Signature']
155-
assert params['SigAlg'] == XMLSecurity::Document::RSA_SHA1
155+
assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA1
156156
end
157157
end
158158

test/metadata_test.rb

Lines changed: 98 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,86 +5,129 @@
55
class MetadataTest < Minitest::Test
66

77
describe 'Metadata' do
8-
def setup
9-
@settings = OneLogin::RubySaml::Settings.new
10-
@settings.issuer = "https://example.com"
11-
@settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
12-
@settings.assertion_consumer_service_url = "https://foo.example/saml/consume"
13-
@settings.security[:authn_requests_signed] = false
8+
let(:settings) { OneLogin::RubySaml::Settings.new }
9+
let(:xml_text) { OneLogin::RubySaml::Metadata.new.generate(settings, false) }
10+
let(:xml_doc) { REXML::Document.new(xml_text) }
11+
let(:spsso_descriptor) { REXML::XPath.first(xml_doc, "//md:SPSSODescriptor") }
12+
let(:acs) { REXML::XPath.first(xml_doc, "//md:AssertionConsumerService") }
13+
14+
before do
15+
settings.issuer = "https://example.com"
16+
settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
17+
settings.assertion_consumer_service_url = "https://foo.example/saml/consume"
1418
end
1519

16-
it "generates Service Provider Metadata with X509Certificate" do
17-
@settings.security[:authn_requests_signed] = true
18-
@settings.certificate = ruby_saml_cert_text
20+
it "generates Pretty Print Service Provider Metadata" do
21+
xml_text = OneLogin::RubySaml::Metadata.new.generate(settings, true)
22+
# assert correct xml declaration
23+
start = "<?xml version='1.0' encoding='UTF-8'?>\n<md:EntityDescriptor"
24+
assert_equal xml_text[0..start.length-1],start
1925

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

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

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

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

3738
it "generates Service Provider Metadata" do
38-
settings = OneLogin::RubySaml::Settings.new
39-
settings.issuer = "https://example.com"
40-
settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
41-
settings.assertion_consumer_service_url = "https://foo.example/saml/consume"
42-
settings.security[:authn_requests_signed] = false
43-
44-
xml_text = OneLogin::RubySaml::Metadata.new.generate(settings)
45-
4639
# assert correct xml declaration
47-
start = "<?xml version='1.0' encoding='UTF-8'?>\n<md:EntityDescriptor"
48-
assert xml_text[0..start.length-1] == start
49-
50-
# assert xml_text can be parsed into an xml doc
51-
xml_doc = REXML::Document.new(xml_text)
40+
start = "<?xml version='1.0' encoding='UTF-8'?><md:EntityDescriptor"
41+
assert_equal xml_text[0..start.length-1], start
5242

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

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

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

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

67-
it "generates attribute service if configured" do
68-
settings = OneLogin::RubySaml::Settings.new
69-
settings.issuer = "https://example.com"
70-
settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
71-
settings.assertion_consumer_service_url = "https://foo.example/saml/consume"
72-
settings.attribute_consuming_service.configure do
73-
service_name "Test Service"
74-
add_attribute(:name => "Name", :name_format => "Name Format", :friendly_name => "Friendly Name", :attribute_value => "Attribute Value")
55+
describe "when auth requests are signed" do
56+
let(:cert_node) do
57+
REXML::XPath.first(
58+
xml_doc,
59+
"//md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
60+
"md" => "urn:oasis:names:tc:SAML:2.0:metadata",
61+
"ds" => "http://www.w3.org/2000/09/xmldsig#"
62+
)
63+
end
64+
let(:cert) { OpenSSL::X509::Certificate.new(Base64.decode64(cert_node.text)) }
65+
66+
before do
67+
settings.security[:authn_requests_signed] = true
68+
settings.certificate = ruby_saml_cert_text
7569
end
7670

77-
xml_text = OneLogin::RubySaml::Metadata.new.generate(settings)
78-
xml_doc = REXML::Document.new(xml_text)
79-
acs = REXML::XPath.first(xml_doc, "//md:AttributeConsumingService")
80-
assert_equal "true", acs.attribute("isDefault").value
81-
assert_equal "1", acs.attribute("index").value
82-
assert_equal REXML::XPath.first(xml_doc, "//md:ServiceName").text.strip, "Test Service"
83-
req_attr = REXML::XPath.first(xml_doc, "//md:RequestedAttribute")
84-
assert_equal "Name", req_attr.attribute("Name").value
85-
assert_equal "Name Format", req_attr.attribute("NameFormat").value
86-
assert_equal "Friendly Name", req_attr.attribute("FriendlyName").value
87-
assert_equal "Attribute Value", REXML::XPath.first(xml_doc, "//md:AttributeValue").text.strip
71+
it "generates Service Provider Metadata with X509Certificate" do
72+
assert_equal "true", spsso_descriptor.attribute("AuthnRequestsSigned").value
73+
assert_equal ruby_saml_cert.to_der, cert.to_der
74+
end
75+
end
76+
77+
describe "when attribute service is configured" do
78+
let(:attr_svc) { REXML::XPath.first(xml_doc, "//md:AttributeConsumingService") }
79+
let(:req_attr) { REXML::XPath.first(xml_doc, "//md:RequestedAttribute") }
80+
81+
before do
82+
settings.attribute_consuming_service.configure do
83+
service_name "Test Service"
84+
add_attribute(:name => "Name", :name_format => "Name Format", :friendly_name => "Friendly Name", :attribute_value => "Attribute Value")
85+
end
86+
end
87+
88+
it "generates attribute service" do
89+
assert_equal "true", attr_svc.attribute("isDefault").value
90+
assert_equal "1", attr_svc.attribute("index").value
91+
assert_equal REXML::XPath.first(xml_doc, "//md:ServiceName").text.strip, "Test Service"
92+
93+
assert_equal "Name", req_attr.attribute("Name").value
94+
assert_equal "Name Format", req_attr.attribute("NameFormat").value
95+
assert_equal "Friendly Name", req_attr.attribute("FriendlyName").value
96+
assert_equal "Attribute Value", REXML::XPath.first(xml_doc, "//md:AttributeValue").text.strip
97+
end
98+
end
99+
100+
describe "when the settings indicate to sign (embedded) metadata" do
101+
before do
102+
settings.security[:metadata_signed] = true
103+
settings.certificate = ruby_saml_cert_text
104+
settings.private_key = ruby_saml_key_text
105+
end
106+
107+
it "creates a signed metadata" do
108+
assert_match %r[<ds:SignatureValue>([a-zA-Z0-9/+=]+)</ds:SignatureValue>]m, xml_text
109+
assert_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1'/>], xml_text
110+
assert_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2000/09/xmldsig#sha1'/>], xml_text
111+
signed_metadata = XMLSecurity::SignedDocument.new(xml_text)
112+
assert signed_metadata.validate_document(ruby_saml_cert_fingerprint, false)
113+
end
114+
115+
describe "when digest and signature methods are specified" do
116+
before do
117+
settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256
118+
settings.security[:digest_method] = XMLSecurity::Document::SHA512
119+
end
120+
121+
it "creates a signed metadata with specified digest and signature methods" do
122+
assert_match %r[<ds:SignatureValue>([a-zA-Z0-9/+=]+)</ds:SignatureValue>]m, xml_text
123+
assert_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'/>], xml_text
124+
assert_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2001/04/xmldsig-more#sha512'/>], xml_text
125+
126+
signed_metadata_2 = XMLSecurity::SignedDocument.new(xml_text)
127+
128+
assert signed_metadata_2.validate_document(ruby_saml_cert_fingerprint, false)
129+
end
130+
end
88131
end
89132
end
90133
end

test/request_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ class RequestTest < Minitest::Test
145145
end
146146
end
147147

148-
describe "when the settings indicate to sign (embebed) the request" do
148+
describe "when the settings indicate to sign (embedded) request" do
149149
it "create a signed request" do
150150
settings = OneLogin::RubySaml::Settings.new
151151
settings.compress_request = false
@@ -195,13 +195,13 @@ class RequestTest < Minitest::Test
195195

196196
params = OneLogin::RubySaml::Authrequest.new.create_params(settings)
197197
assert params['Signature']
198-
assert params['SigAlg'] == XMLSecurity::Document::RSA_SHA1
198+
assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA1
199199

200200
# signature_method only affects the embedeed signature
201201
settings.security[:signature_method] = XMLSecurity::Document::SHA256
202202
params = OneLogin::RubySaml::Authrequest.new.create_params(settings)
203203
assert params['Signature']
204-
assert params['SigAlg'] == XMLSecurity::Document::RSA_SHA1
204+
assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA1
205205
end
206206
end
207207

0 commit comments

Comments
 (0)