ruby-saml 0.8.12 → 0.8.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ruby-saml might be problematic. Click here for more details.
- checksums.yaml +7 -7
- data/lib/onelogin/ruby-saml/logoutrequest.rb +2 -1
- data/lib/onelogin/ruby-saml/logoutresponse.rb +9 -51
- data/lib/onelogin/ruby-saml/response.rb +133 -21
- data/lib/onelogin/ruby-saml/settings.rb +28 -10
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +101 -0
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +12 -9
- data/lib/onelogin/ruby-saml/utils.rb +92 -0
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/ruby-saml.rb +1 -0
- data/lib/xml_security.rb +222 -86
- data/test/certificates/certificate.der +0 -0
- data/test/certificates/formatted_certificate +14 -0
- data/test/certificates/formatted_chained_certificate +42 -0
- data/test/certificates/formatted_private_key +12 -0
- data/test/certificates/formatted_rsa_private_key +12 -0
- data/test/certificates/invalid_certificate1 +1 -0
- data/test/certificates/invalid_certificate2 +1 -0
- data/test/certificates/invalid_certificate3 +12 -0
- data/test/certificates/invalid_chained_certificate1 +1 -0
- data/test/certificates/invalid_private_key1 +1 -0
- data/test/certificates/invalid_private_key2 +1 -0
- data/test/certificates/invalid_private_key3 +10 -0
- data/test/certificates/invalid_rsa_private_key1 +1 -0
- data/test/certificates/invalid_rsa_private_key2 +1 -0
- data/test/certificates/invalid_rsa_private_key3 +10 -0
- data/test/certificates/ruby-saml-2.crt +15 -0
- data/test/logoutresponse_test.rb +2 -16
- data/test/requests/logoutrequest_fixtures.rb +47 -0
- data/test/response_test.rb +227 -15
- data/test/responses/adfs_response_xmlns.xml +45 -0
- data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +1 -0
- data/test/responses/invalids/invalid_issuer_message.xml.base64 +1 -0
- data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
- data/test/responses/invalids/no_signature.xml.base64 +1 -0
- data/test/responses/invalids/response_with_concealed_signed_assertion.xml +51 -0
- data/test/responses/invalids/response_with_doubled_signed_assertion.xml +49 -0
- data/test/responses/invalids/signature_wrapping_attack.xml.base64 +1 -0
- data/test/responses/logoutresponse_fixtures.rb +4 -4
- data/test/responses/response_with_signed_assertion_3.xml +30 -0
- data/test/responses/response_with_signed_message_and_assertion.xml +34 -0
- data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
- data/test/responses/valid_response_without_x509certificate.xml.base64 +1 -0
- data/test/settings_test.rb +106 -0
- data/test/slo_logoutrequest_test.rb +66 -0
- data/test/slo_logoutresponse_test.rb +8 -0
- data/test/test_helper.rb +62 -30
- data/test/utils_test.rb +191 -1
- data/test/xml_security_test.rb +329 -36
- metadata +109 -45
@@ -26,10 +26,11 @@ module OneLogin
|
|
26
26
|
# @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
|
27
27
|
# @param logout_message [String] The Message to be placed as StatusMessage in the logout response
|
28
28
|
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
29
|
+
# @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response
|
29
30
|
# @return [String] Logout Request string that includes the SAMLRequest
|
30
31
|
#
|
31
|
-
def create(settings, request_id = nil, logout_message = nil, params = {})
|
32
|
-
params = create_params(settings, request_id, logout_message, params)
|
32
|
+
def create(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil)
|
33
|
+
params = create_params(settings, request_id, logout_message, params, logout_status_code)
|
33
34
|
params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
|
34
35
|
saml_response = CGI.escape(params.delete("SAMLResponse"))
|
35
36
|
response_params = "#{params_prefix}SAMLResponse=#{saml_response}"
|
@@ -45,9 +46,10 @@ module OneLogin
|
|
45
46
|
# @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
|
46
47
|
# @param logout_message [String] The Message to be placed as StatusMessage in the logout response
|
47
48
|
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
49
|
+
# @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response
|
48
50
|
# @return [Hash] Parameters
|
49
51
|
#
|
50
|
-
def create_params(settings, request_id = nil, logout_message = nil, params = {})
|
52
|
+
def create_params(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil)
|
51
53
|
# The method expects :RelayState but sometimes we get 'RelayState' instead.
|
52
54
|
# Based on the HashWithIndifferentAccess value in Rails we could experience
|
53
55
|
# conflicts so this line will solve them.
|
@@ -58,7 +60,7 @@ module OneLogin
|
|
58
60
|
params.delete('RelayState')
|
59
61
|
end
|
60
62
|
|
61
|
-
response_doc = create_logout_response_xml_doc(settings, request_id, logout_message)
|
63
|
+
response_doc = create_logout_response_xml_doc(settings, request_id, logout_message, logout_status_code)
|
62
64
|
response_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
|
63
65
|
|
64
66
|
response = ""
|
@@ -104,12 +106,12 @@ module OneLogin
|
|
104
106
|
# @param logout_message [String] The Message to be placed as StatusMessage in the logout response
|
105
107
|
# @return [String] The SAMLResponse String.
|
106
108
|
#
|
107
|
-
def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil)
|
108
|
-
document = create_xml_document(settings, request_id, logout_message)
|
109
|
+
def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil, logout_status_code = nil)
|
110
|
+
document = create_xml_document(settings, request_id, logout_message, logout_status_code)
|
109
111
|
sign_document(document, settings)
|
110
112
|
end
|
111
113
|
|
112
|
-
def create_xml_document(settings, request_id = nil, logout_message = nil)
|
114
|
+
def create_xml_document(settings, request_id = nil, logout_message = nil, status_code = nil)
|
113
115
|
time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
|
114
116
|
|
115
117
|
response_doc = XMLSecurity::Document.new
|
@@ -131,8 +133,9 @@ module OneLogin
|
|
131
133
|
status = root.add_element 'samlp:Status'
|
132
134
|
|
133
135
|
# success status code
|
134
|
-
status_code
|
135
|
-
|
136
|
+
status_code ||= 'urn:oasis:names:tc:SAML:2.0:status:Success'
|
137
|
+
status_code_elem = status.add_element 'samlp:StatusCode'
|
138
|
+
status_code_elem.attributes['Value'] = status_code
|
136
139
|
|
137
140
|
# success status message
|
138
141
|
logout_message ||= 'Successfully Signed Out'
|
@@ -4,6 +4,9 @@ else
|
|
4
4
|
require 'securerandom'
|
5
5
|
end
|
6
6
|
|
7
|
+
require "base64"
|
8
|
+
require "zlib"
|
9
|
+
|
7
10
|
module OneLogin
|
8
11
|
module RubySaml
|
9
12
|
|
@@ -12,6 +15,8 @@ module OneLogin
|
|
12
15
|
class Utils
|
13
16
|
@@uuid_generator = UUID.new if RUBY_VERSION < '1.9'
|
14
17
|
|
18
|
+
BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z)
|
19
|
+
|
15
20
|
# Given a REXML::Element instance, return the concatenation of all child text nodes. Assumes
|
16
21
|
# that there all children other than text nodes can be ignored (e.g. comments). If nil is
|
17
22
|
# passed, nil will be returned.
|
@@ -114,6 +119,93 @@ module OneLogin
|
|
114
119
|
|
115
120
|
error_msg
|
116
121
|
end
|
122
|
+
|
123
|
+
# Base64 decode and try also to inflate a SAML Message
|
124
|
+
# @param saml [String] The deflated and encoded SAML Message
|
125
|
+
# @return [String] The plain SAML Message
|
126
|
+
#
|
127
|
+
def self.decode_raw_saml(saml)
|
128
|
+
return saml unless base64_encoded?(saml)
|
129
|
+
|
130
|
+
decoded = decode(saml)
|
131
|
+
begin
|
132
|
+
inflate(decoded)
|
133
|
+
rescue
|
134
|
+
decoded
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Base 64 decode method
|
139
|
+
# @param string [String] The string message
|
140
|
+
# @return [String] The decoded string
|
141
|
+
#
|
142
|
+
def self.decode(string)
|
143
|
+
Base64.decode64(string)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Base 64 encode method
|
147
|
+
# @param string [String] The string
|
148
|
+
# @return [String] The encoded string
|
149
|
+
#
|
150
|
+
def self.encode(string)
|
151
|
+
if Base64.respond_to?('strict_encode64')
|
152
|
+
Base64.strict_encode64(string)
|
153
|
+
else
|
154
|
+
Base64.encode64(string).gsub(/\n/, "")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Check if a string is base64 encoded
|
159
|
+
# @param string [String] string to check the encoding of
|
160
|
+
# @return [true, false] whether or not the string is base64 encoded
|
161
|
+
#
|
162
|
+
def self.base64_encoded?(string)
|
163
|
+
!!string.gsub(/[\r\n]|\\r|\\n|\s/, "").match(BASE64_FORMAT)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Inflate method
|
167
|
+
# @param deflated [String] The string
|
168
|
+
# @return [String] The inflated string
|
169
|
+
#
|
170
|
+
def self.inflate(deflated)
|
171
|
+
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(deflated)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Deflate method
|
175
|
+
# @param inflated [String] The string
|
176
|
+
# @return [String] The deflated string
|
177
|
+
#
|
178
|
+
def self.deflate(inflated)
|
179
|
+
Zlib::Deflate.deflate(inflated, 9)[2..-5]
|
180
|
+
end
|
181
|
+
|
182
|
+
# Given two strings, attempt to match them as URIs using Rails' parse method. If they can be parsed,
|
183
|
+
# then the fully-qualified domain name and the host should performa a case-insensitive match, per the
|
184
|
+
# RFC for URIs. If Rails can not parse the string in to URL pieces, return a boolean match of the
|
185
|
+
# two strings. This maintains the previous functionality.
|
186
|
+
# @return [Boolean]
|
187
|
+
def self.uri_match?(destination_url, settings_url)
|
188
|
+
dest_uri = URI.parse(destination_url)
|
189
|
+
acs_uri = URI.parse(settings_url)
|
190
|
+
|
191
|
+
if dest_uri.scheme.nil? || acs_uri.scheme.nil? || dest_uri.host.nil? || acs_uri.host.nil?
|
192
|
+
raise URI::InvalidURIError
|
193
|
+
else
|
194
|
+
dest_uri.scheme.downcase == acs_uri.scheme.downcase &&
|
195
|
+
dest_uri.host.downcase == acs_uri.host.downcase &&
|
196
|
+
dest_uri.path == acs_uri.path &&
|
197
|
+
dest_uri.query == acs_uri.query
|
198
|
+
end
|
199
|
+
rescue URI::InvalidURIError
|
200
|
+
original_uri_match?(destination_url, settings_url)
|
201
|
+
end
|
202
|
+
|
203
|
+
# If Rails' URI.parse can't match to valid URL, default back to the original matching service.
|
204
|
+
# @return [Boolean]
|
205
|
+
def self.original_uri_match?(destination_url, settings_url)
|
206
|
+
destination_url == settings_url
|
207
|
+
end
|
208
|
+
|
117
209
|
end
|
118
210
|
end
|
119
211
|
end
|
data/lib/ruby-saml.rb
CHANGED
@@ -2,6 +2,7 @@ require 'onelogin/ruby-saml/logging'
|
|
2
2
|
require 'onelogin/ruby-saml/authrequest'
|
3
3
|
require 'onelogin/ruby-saml/logoutrequest'
|
4
4
|
require 'onelogin/ruby-saml/logoutresponse'
|
5
|
+
require 'onelogin/ruby-saml/slo_logoutrequest'
|
5
6
|
require 'onelogin/ruby-saml/slo_logoutresponse'
|
6
7
|
require 'onelogin/ruby-saml/response'
|
7
8
|
require 'onelogin/ruby-saml/settings'
|
data/lib/xml_security.rb
CHANGED
@@ -42,40 +42,41 @@ module XMLSecurity
|
|
42
42
|
NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT |
|
43
43
|
Nokogiri::XML::ParseOptions::NONET
|
44
44
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
45
|
+
def canon_algorithm(element)
|
46
|
+
algorithm = element
|
47
|
+
if algorithm.is_a?(REXML::Element)
|
48
|
+
algorithm = element.attribute('Algorithm').value
|
49
|
+
end
|
50
|
+
|
51
|
+
case algorithm
|
52
|
+
when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
|
53
|
+
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
|
54
|
+
Nokogiri::XML::XML_C14N_1_0
|
55
|
+
when "http://www.w3.org/2006/12/xml-c14n11",
|
56
|
+
"http://www.w3.org/2006/12/xml-c14n11#WithComments"
|
57
|
+
Nokogiri::XML::XML_C14N_1_1
|
58
|
+
else
|
59
|
+
Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def algorithm(element)
|
64
|
+
algorithm = element
|
65
|
+
if algorithm.is_a?(REXML::Element)
|
66
|
+
algorithm = element.attribute("Algorithm").value
|
67
|
+
end
|
68
|
+
|
69
|
+
algorithm = algorithm && algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
|
70
|
+
|
71
|
+
case algorithm
|
72
|
+
when 256 then OpenSSL::Digest::SHA256
|
73
|
+
when 384 then OpenSSL::Digest::SHA384
|
74
|
+
when 512 then OpenSSL::Digest::SHA512
|
75
|
+
else
|
76
|
+
OpenSSL::Digest::SHA1
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
79
80
|
end
|
80
81
|
|
81
82
|
class Document < BaseDocument
|
@@ -98,15 +99,30 @@ module XMLSecurity
|
|
98
99
|
end
|
99
100
|
end
|
100
101
|
|
102
|
+
#<Signature>
|
103
|
+
#<SignedInfo>
|
104
|
+
#<CanonicalizationMethod />
|
105
|
+
#<SignatureMethod />
|
106
|
+
#<Reference>
|
107
|
+
#<Transforms>
|
108
|
+
#<DigestMethod>
|
109
|
+
#<DigestValue>
|
110
|
+
#</Reference>
|
111
|
+
#<Reference /> etc.
|
112
|
+
#</SignedInfo>
|
113
|
+
#<SignatureValue />
|
114
|
+
#<KeyInfo />
|
115
|
+
#<Object />
|
116
|
+
#</Signature>
|
101
117
|
def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_method = SHA1)
|
102
118
|
noko = Nokogiri::XML(self.to_s) do |config|
|
103
|
-
config.options = NOKOGIRI_OPTIONS
|
119
|
+
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
|
104
120
|
end
|
105
121
|
|
106
122
|
signature_element = REXML::Element.new("ds:Signature").add_namespace('ds', DSIG)
|
107
123
|
signed_info_element = signature_element.add_element("ds:SignedInfo")
|
108
124
|
signed_info_element.add_element("ds:CanonicalizationMethod", {"Algorithm" => C14N})
|
109
|
-
signed_info_element.add_element("ds:SignatureMethod", {"Algorithm"=>signature_method})
|
125
|
+
signed_info_element.add_element("ds:SignatureMethod", {"Algorithm" => signature_method})
|
110
126
|
|
111
127
|
# Add Reference
|
112
128
|
reference_element = signed_info_element.add_element("ds:Reference", {"URI" => "##{uuid}"})
|
@@ -124,7 +140,7 @@ module XMLSecurity
|
|
124
140
|
|
125
141
|
# add SignatureValue
|
126
142
|
noko_sig_element = Nokogiri::XML(signature_element.to_s) do |config|
|
127
|
-
config.options = NOKOGIRI_OPTIONS
|
143
|
+
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
|
128
144
|
end
|
129
145
|
|
130
146
|
noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG)
|
@@ -172,87 +188,175 @@ module XMLSecurity
|
|
172
188
|
|
173
189
|
attr_writer :signed_element_id
|
174
190
|
|
175
|
-
def initialize(response)
|
176
|
-
super(response)
|
177
|
-
extract_signed_element_id
|
178
|
-
end
|
179
|
-
|
180
191
|
def signed_element_id
|
181
192
|
@signed_element_id ||= extract_signed_element_id
|
182
193
|
end
|
183
194
|
|
184
|
-
def validate_document(idp_cert_fingerprint, soft = true)
|
195
|
+
def validate_document(idp_cert_fingerprint, soft = true, options = {})
|
185
196
|
# get cert from response
|
186
|
-
cert_element = REXML::XPath.first(
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
197
|
+
cert_element = REXML::XPath.first(
|
198
|
+
self,
|
199
|
+
"//ds:X509Certificate",
|
200
|
+
{ "ds"=>DSIG }
|
201
|
+
)
|
191
202
|
|
192
|
-
|
193
|
-
|
203
|
+
if cert_element
|
204
|
+
base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element)
|
205
|
+
cert_text = Base64.decode64(base64_cert)
|
206
|
+
begin
|
207
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
208
|
+
rescue OpenSSL::X509::CertificateError => _e
|
209
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Certificate Error"))
|
210
|
+
end
|
194
211
|
|
195
|
-
|
196
|
-
|
197
|
-
|
212
|
+
if options[:fingerprint_alg]
|
213
|
+
fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(options[:fingerprint_alg]).new
|
214
|
+
else
|
215
|
+
fingerprint_alg = OpenSSL::Digest::SHA1.new
|
216
|
+
end
|
217
|
+
fingerprint = fingerprint_alg.hexdigest(cert.to_der)
|
198
218
|
|
219
|
+
# check cert matches registered idp cert
|
220
|
+
if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
|
221
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Fingerprint mismatch"))
|
222
|
+
end
|
223
|
+
else
|
224
|
+
if options[:cert]
|
225
|
+
cert = options[:cert]
|
226
|
+
if cert.is_a? String
|
227
|
+
cert = OpenSSL::X509::Certificate.new(cert)
|
228
|
+
end
|
229
|
+
base64_cert = Base64.encode64(cert.to_pem)
|
230
|
+
else
|
231
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Certificate element missing in response (ds:X509Certificate) and not cert provided at settings"))
|
232
|
+
end
|
233
|
+
end
|
199
234
|
validate_signature(base64_cert, soft)
|
200
235
|
end
|
201
236
|
|
202
|
-
def
|
203
|
-
#
|
237
|
+
def validate_document_with_cert(idp_cert, soft = true)
|
238
|
+
# get cert from response
|
239
|
+
cert_element = REXML::XPath.first(
|
240
|
+
self,
|
241
|
+
"//ds:X509Certificate",
|
242
|
+
{ "ds"=>DSIG }
|
243
|
+
)
|
204
244
|
|
205
|
-
|
206
|
-
|
245
|
+
if cert_element
|
246
|
+
base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element)
|
247
|
+
cert_text = Base64.decode64(base64_cert)
|
248
|
+
begin
|
249
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
250
|
+
rescue OpenSSL::X509::CertificateError => _e
|
251
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Certificate Error"))
|
252
|
+
end
|
253
|
+
|
254
|
+
# check saml response cert matches provided idp cert
|
255
|
+
if idp_cert.to_pem != cert.to_pem
|
256
|
+
return false
|
257
|
+
end
|
258
|
+
elsif not idp_cert
|
259
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Certificate element missing in response (ds:X509Certificate) and not cert provided at settings"))
|
260
|
+
else
|
261
|
+
base64_cert = Base64.encode64(idp_cert.to_pem)
|
262
|
+
end
|
263
|
+
validate_signature(base64_cert, true)
|
264
|
+
end
|
265
|
+
|
266
|
+
def validate_signature(base64_cert, soft = true)
|
207
267
|
|
208
268
|
document = Nokogiri::XML(self.to_s) do |config|
|
209
|
-
config.options = NOKOGIRI_OPTIONS
|
269
|
+
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
|
210
270
|
end
|
211
271
|
|
212
|
-
# create a
|
272
|
+
# create a rexml document
|
213
273
|
@working_copy ||= REXML::Document.new(self.to_s).root
|
214
274
|
|
215
|
-
#
|
216
|
-
|
217
|
-
|
275
|
+
# get signature node
|
276
|
+
sig_element = REXML::XPath.first(
|
277
|
+
@working_copy,
|
278
|
+
"//ds:Signature",
|
279
|
+
{"ds"=>DSIG}
|
280
|
+
)
|
281
|
+
|
282
|
+
if sig_element.nil?
|
283
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("No Signature Node"))
|
218
284
|
end
|
219
285
|
|
220
|
-
#
|
221
|
-
|
286
|
+
# signature method
|
287
|
+
sig_alg_value = REXML::XPath.first(
|
288
|
+
sig_element,
|
289
|
+
"./ds:SignedInfo/ds:SignatureMethod",
|
290
|
+
{"ds"=>DSIG}
|
291
|
+
)
|
292
|
+
signature_algorithm = algorithm(sig_alg_value)
|
293
|
+
|
294
|
+
# get signature
|
295
|
+
base64_signature = REXML::XPath.first(
|
296
|
+
sig_element,
|
297
|
+
"./ds:SignatureValue",
|
298
|
+
{"ds" => DSIG}
|
299
|
+
)
|
300
|
+
|
301
|
+
if base64_signature.nil?
|
302
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("SignatureValue not found"))
|
303
|
+
end
|
304
|
+
|
305
|
+
signature = Base64.decode64(OneLogin::RubySaml::Utils.element_text(base64_signature))
|
306
|
+
|
307
|
+
# canonicalization method
|
308
|
+
canon_algorithm = canon_algorithm REXML::XPath.first(
|
309
|
+
sig_element,
|
310
|
+
'./ds:SignedInfo/ds:CanonicalizationMethod',
|
311
|
+
'ds' => DSIG
|
312
|
+
)
|
313
|
+
|
222
314
|
noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
|
223
315
|
noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
|
224
|
-
|
316
|
+
|
225
317
|
canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
|
226
318
|
noko_sig_element.remove
|
227
319
|
|
320
|
+
# get inclusive namespaces
|
321
|
+
inclusive_namespaces = extract_inclusive_namespaces
|
322
|
+
|
228
323
|
# check digests
|
229
|
-
REXML::XPath.
|
230
|
-
uri = ref.attributes.get_attribute("URI").value
|
324
|
+
ref = REXML::XPath.first(sig_element, "//ds:Reference", {"ds"=>DSIG})
|
231
325
|
|
232
|
-
|
233
|
-
canon_algorithm = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod', 'ds' => DSIG)
|
234
|
-
canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
|
326
|
+
hashed_element = document.at_xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
|
235
327
|
|
236
|
-
|
328
|
+
canon_algorithm = canon_algorithm REXML::XPath.first(
|
329
|
+
ref,
|
330
|
+
'//ds:CanonicalizationMethod',
|
331
|
+
{ "ds" => DSIG }
|
332
|
+
)
|
237
333
|
|
238
|
-
|
239
|
-
digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>DSIG})))
|
334
|
+
canon_algorithm = process_transforms(ref, canon_algorithm)
|
240
335
|
|
241
|
-
|
242
|
-
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Digest mismatch"))
|
243
|
-
end
|
244
|
-
end
|
336
|
+
canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
|
245
337
|
|
246
|
-
|
247
|
-
|
338
|
+
digest_algorithm = algorithm(REXML::XPath.first(
|
339
|
+
ref,
|
340
|
+
"//ds:DigestMethod",
|
341
|
+
{ "ds" => DSIG }
|
342
|
+
))
|
343
|
+
hash = digest_algorithm.digest(canon_hashed_element)
|
344
|
+
encoded_digest_value = REXML::XPath.first(
|
345
|
+
ref,
|
346
|
+
"//ds:DigestValue",
|
347
|
+
{ "ds" => DSIG }
|
348
|
+
)
|
349
|
+
digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value))
|
248
350
|
|
249
|
-
|
250
|
-
|
251
|
-
|
351
|
+
unless digests_match?(hash, digest_value)
|
352
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Digest mismatch"))
|
353
|
+
end
|
252
354
|
|
253
|
-
#
|
254
|
-
|
355
|
+
# get certificate object
|
356
|
+
cert_text = Base64.decode64(base64_cert)
|
357
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
255
358
|
|
359
|
+
# verify signature
|
256
360
|
unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
|
257
361
|
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Key validation error"))
|
258
362
|
end
|
@@ -262,6 +366,33 @@ module XMLSecurity
|
|
262
366
|
|
263
367
|
private
|
264
368
|
|
369
|
+
def process_transforms(ref, canon_algorithm)
|
370
|
+
transforms = REXML::XPath.match(
|
371
|
+
ref,
|
372
|
+
"//ds:Transforms/ds:Transform",
|
373
|
+
{ "ds" => DSIG }
|
374
|
+
)
|
375
|
+
|
376
|
+
transforms.each do |transform_element|
|
377
|
+
if transform_element.attributes && transform_element.attributes["Algorithm"]
|
378
|
+
algorithm = transform_element.attributes["Algorithm"]
|
379
|
+
case algorithm
|
380
|
+
when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
|
381
|
+
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
|
382
|
+
canon_algorithm = Nokogiri::XML::XML_C14N_1_0
|
383
|
+
when "http://www.w3.org/2006/12/xml-c14n11",
|
384
|
+
"http://www.w3.org/2006/12/xml-c14n11#WithComments"
|
385
|
+
canon_algorithm = Nokogiri::XML::XML_C14N_1_1
|
386
|
+
when "http://www.w3.org/2001/10/xml-exc-c14n#",
|
387
|
+
"http://www.w3.org/2001/10/xml-exc-c14n#WithComments"
|
388
|
+
canon_algorithm = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
canon_algorithm
|
394
|
+
end
|
395
|
+
|
265
396
|
def digests_match?(hash, digest_value)
|
266
397
|
hash == digest_value
|
267
398
|
end
|
@@ -280,11 +411,16 @@ module XMLSecurity
|
|
280
411
|
end
|
281
412
|
|
282
413
|
def extract_inclusive_namespaces
|
283
|
-
|
414
|
+
element = REXML::XPath.first(
|
415
|
+
self,
|
416
|
+
"//ec:InclusiveNamespaces",
|
417
|
+
{ "ec" => C14N }
|
418
|
+
)
|
419
|
+
if element
|
284
420
|
prefix_list = element.attributes.get_attribute("PrefixList").value
|
285
421
|
prefix_list.split(" ")
|
286
422
|
else
|
287
|
-
|
423
|
+
nil
|
288
424
|
end
|
289
425
|
end
|
290
426
|
|