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.

Files changed (50) hide show
  1. checksums.yaml +7 -7
  2. data/lib/onelogin/ruby-saml/logoutrequest.rb +2 -1
  3. data/lib/onelogin/ruby-saml/logoutresponse.rb +9 -51
  4. data/lib/onelogin/ruby-saml/response.rb +133 -21
  5. data/lib/onelogin/ruby-saml/settings.rb +28 -10
  6. data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +101 -0
  7. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +12 -9
  8. data/lib/onelogin/ruby-saml/utils.rb +92 -0
  9. data/lib/onelogin/ruby-saml/version.rb +1 -1
  10. data/lib/ruby-saml.rb +1 -0
  11. data/lib/xml_security.rb +222 -86
  12. data/test/certificates/certificate.der +0 -0
  13. data/test/certificates/formatted_certificate +14 -0
  14. data/test/certificates/formatted_chained_certificate +42 -0
  15. data/test/certificates/formatted_private_key +12 -0
  16. data/test/certificates/formatted_rsa_private_key +12 -0
  17. data/test/certificates/invalid_certificate1 +1 -0
  18. data/test/certificates/invalid_certificate2 +1 -0
  19. data/test/certificates/invalid_certificate3 +12 -0
  20. data/test/certificates/invalid_chained_certificate1 +1 -0
  21. data/test/certificates/invalid_private_key1 +1 -0
  22. data/test/certificates/invalid_private_key2 +1 -0
  23. data/test/certificates/invalid_private_key3 +10 -0
  24. data/test/certificates/invalid_rsa_private_key1 +1 -0
  25. data/test/certificates/invalid_rsa_private_key2 +1 -0
  26. data/test/certificates/invalid_rsa_private_key3 +10 -0
  27. data/test/certificates/ruby-saml-2.crt +15 -0
  28. data/test/logoutresponse_test.rb +2 -16
  29. data/test/requests/logoutrequest_fixtures.rb +47 -0
  30. data/test/response_test.rb +227 -15
  31. data/test/responses/adfs_response_xmlns.xml +45 -0
  32. data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +1 -0
  33. data/test/responses/invalids/invalid_issuer_message.xml.base64 +1 -0
  34. data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
  35. data/test/responses/invalids/no_signature.xml.base64 +1 -0
  36. data/test/responses/invalids/response_with_concealed_signed_assertion.xml +51 -0
  37. data/test/responses/invalids/response_with_doubled_signed_assertion.xml +49 -0
  38. data/test/responses/invalids/signature_wrapping_attack.xml.base64 +1 -0
  39. data/test/responses/logoutresponse_fixtures.rb +4 -4
  40. data/test/responses/response_with_signed_assertion_3.xml +30 -0
  41. data/test/responses/response_with_signed_message_and_assertion.xml +34 -0
  42. data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
  43. data/test/responses/valid_response_without_x509certificate.xml.base64 +1 -0
  44. data/test/settings_test.rb +106 -0
  45. data/test/slo_logoutrequest_test.rb +66 -0
  46. data/test/slo_logoutresponse_test.rb +8 -0
  47. data/test/test_helper.rb +62 -30
  48. data/test/utils_test.rb +191 -1
  49. data/test/xml_security_test.rb +329 -36
  50. 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 = status.add_element 'samlp:StatusCode'
135
- status_code.attributes['Value'] = 'urn:oasis:names:tc:SAML:2.0:status:Success'
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
@@ -1,5 +1,5 @@
1
1
  module OneLogin
2
2
  module RubySaml
3
- VERSION = '0.8.12'
3
+ VERSION = '0.8.17'
4
4
  end
5
5
  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
- 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
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(self, "//ds:X509Certificate", { "ds"=>DSIG })
187
- raise OneLogin::RubySaml::ValidationError.new("Certificate element missing in response (ds:X509Certificate)") unless cert_element
188
- base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element)
189
- cert_text = Base64.decode64(base64_cert)
190
- cert = OpenSSL::X509::Certificate.new(cert_text)
197
+ cert_element = REXML::XPath.first(
198
+ self,
199
+ "//ds:X509Certificate",
200
+ { "ds"=>DSIG }
201
+ )
191
202
 
192
- # check cert matches registered idp cert
193
- fingerprint = Digest::SHA1.hexdigest(cert.to_der)
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
- if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
196
- return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Fingerprint mismatch"))
197
- end
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 validate_signature(base64_cert, soft = true)
203
- # validate references
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
- # check for inclusive namespaces
206
- inclusive_namespaces = extract_inclusive_namespaces
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 working copy so we don't modify the original
272
+ # create a rexml document
213
273
  @working_copy ||= REXML::Document.new(self.to_s).root
214
274
 
215
- # store signature node
216
- @sig_element ||= begin
217
- element = REXML::XPath.first(@working_copy, "//ds:Signature", {"ds"=>DSIG})
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
- # verify signature
221
- signed_info_element = REXML::XPath.first(@sig_element, "//ds:SignedInfo", {"ds"=>DSIG})
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
- canon_algorithm = canon_algorithm REXML::XPath.first(@sig_element, '//ds:CanonicalizationMethod', 'ds' => DSIG)
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.each(@sig_element, "//ds:Reference", {"ds"=>DSIG}) do |ref|
230
- uri = ref.attributes.get_attribute("URI").value
324
+ ref = REXML::XPath.first(sig_element, "//ds:Reference", {"ds"=>DSIG})
231
325
 
232
- hashed_element = document.at_xpath("//*[@ID='#{uri[1..-1]}']")
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
- digest_algorithm = algorithm(REXML::XPath.first(ref, "//ds:DigestMethod", 'ds' => DSIG))
328
+ canon_algorithm = canon_algorithm REXML::XPath.first(
329
+ ref,
330
+ '//ds:CanonicalizationMethod',
331
+ { "ds" => DSIG }
332
+ )
237
333
 
238
- hash = digest_algorithm.digest(canon_hashed_element)
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
- unless digests_match?(hash, digest_value)
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
- base64_signature = OneLogin::RubySaml::Utils.element_text(REXML::XPath.first(@sig_element, "//ds:SignatureValue", {"ds"=>DSIG}))
247
- signature = Base64.decode64(base64_signature)
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
- # get certificate object
250
- cert_text = Base64.decode64(base64_cert)
251
- cert = OpenSSL::X509::Certificate.new(cert_text)
351
+ unless digests_match?(hash, digest_value)
352
+ return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Digest mismatch"))
353
+ end
252
354
 
253
- # signature method
254
- signature_algorithm = algorithm(REXML::XPath.first(signed_info_element, "//ds:SignatureMethod", {"ds"=>DSIG}))
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
- if element = REXML::XPath.first(self, "//ec:InclusiveNamespaces", { "ec" => C14N })
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