ruby-saml 0.8.11 → 0.8.16

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of ruby-saml might be problematic. Click here for more details.

Files changed (55) hide show
  1. checksums.yaml +5 -5
  2. data/Gemfile +3 -1
  3. data/Rakefile +0 -14
  4. data/lib/onelogin/ruby-saml/logoutresponse.rb +9 -51
  5. data/lib/onelogin/ruby-saml/response.rb +121 -30
  6. data/lib/onelogin/ruby-saml/settings.rb +27 -10
  7. data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +101 -0
  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 -87
  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/logoutrequest_test.rb +124 -126
  29. data/test/logoutresponse_test.rb +22 -42
  30. data/test/requests/logoutrequest_fixtures.rb +47 -0
  31. data/test/response_test.rb +373 -129
  32. data/test/responses/adfs_response_xmlns.xml +45 -0
  33. data/test/responses/encrypted_new_attack.xml.base64 +1 -0
  34. data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +1 -0
  35. data/test/responses/invalids/invalid_issuer_message.xml.base64 +1 -0
  36. data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
  37. data/test/responses/invalids/no_signature.xml.base64 +1 -0
  38. data/test/responses/invalids/response_with_concealed_signed_assertion.xml +51 -0
  39. data/test/responses/invalids/response_with_doubled_signed_assertion.xml +49 -0
  40. data/test/responses/invalids/signature_wrapping_attack.xml.base64 +1 -0
  41. data/test/responses/logoutresponse_fixtures.rb +4 -4
  42. data/test/responses/response_with_concealed_signed_assertion.xml +51 -0
  43. data/test/responses/response_with_doubled_signed_assertion.xml +49 -0
  44. data/test/responses/response_with_signed_assertion_3.xml +30 -0
  45. data/test/responses/response_with_signed_message_and_assertion.xml +34 -0
  46. data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
  47. data/test/responses/response_wrapped.xml.base64 +150 -0
  48. data/test/responses/valid_response.xml.base64 +1 -0
  49. data/test/responses/valid_response_without_x509certificate.xml.base64 +1 -0
  50. data/test/settings_test.rb +111 -5
  51. data/test/slo_logoutrequest_test.rb +66 -0
  52. data/test/test_helper.rb +110 -41
  53. data/test/utils_test.rb +201 -11
  54. data/test/xml_security_test.rb +359 -68
  55. metadata +77 -7
@@ -0,0 +1,101 @@
1
+ require "xml_security"
2
+ require "time"
3
+
4
+ # Only supports SAML 2.0
5
+ # SAML2 Logout Request (SLO IdP initiated, Parser)
6
+ module OneLogin
7
+ module RubySaml
8
+ class SloLogoutrequest
9
+
10
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
11
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
12
+
13
+ # OneLogin::RubySaml::Settings Toolkit settings
14
+ attr_accessor :settings
15
+
16
+ attr_reader :document
17
+ attr_reader :request
18
+ attr_reader :options
19
+
20
+ def initialize(request, settings = nil, options = {})
21
+ raise ArgumentError.new("Request cannot be nil") if request.nil?
22
+ self.settings = settings
23
+
24
+ @options = options
25
+ @request = OneLogin::RubySaml::Utils.decode_raw_saml(request)
26
+ @document = XMLSecurity::SignedDocument.new(@request)
27
+ end
28
+
29
+ def validate!
30
+ validate(false)
31
+ end
32
+
33
+ def validate(soft = true)
34
+ return false unless validate_structure(soft)
35
+
36
+ valid_issuer?(soft)
37
+ end
38
+
39
+ def name_id
40
+ @name_id ||= begin
41
+ node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
42
+ Utils.element_text(node)
43
+ end
44
+ end
45
+
46
+ alias_method :nameid, :name_id
47
+
48
+ def name_id_format
49
+ @name_id_node ||= REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
50
+ @name_id_format ||=
51
+ if @name_id_node && @name_id_node.attribute("Format")
52
+ @name_id_node.attribute("Format").value
53
+ end
54
+ end
55
+
56
+ alias_method :nameid_format, :name_id_format
57
+
58
+ def id
59
+ @id ||= begin
60
+ node = REXML::XPath.first(document, "/p:LogoutRequest", { "p" => PROTOCOL } )
61
+ node.nil? ? nil : node.attributes['ID']
62
+ end
63
+ end
64
+
65
+ def issuer
66
+ @issuer ||= begin
67
+ node = REXML::XPath.first(document, "/p:LogoutRequest/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
68
+ Utils.element_text(node)
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def validate_structure(soft = true)
75
+ Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
76
+ @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
77
+ @xml = Nokogiri::XML(self.document.to_s)
78
+ end
79
+ if soft
80
+ @schema.validate(@xml).map{ return false }
81
+ else
82
+ @schema.validate(@xml).map{ |error| validation_error("#{error.message}\n\n#{@xml.to_s}") }
83
+ end
84
+ end
85
+
86
+ def valid_issuer?(soft = true)
87
+ return true if settings.nil? || settings.idp_entity_id.nil? || issuer.nil?
88
+
89
+ unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id)
90
+ return soft ? false : validation_error("Doesn't match the issuer, expected: <#{self.settings.idp_entity_id}>, but was: <#{issuer}>")
91
+ end
92
+ true
93
+ end
94
+
95
+ def validation_error(message)
96
+ raise ValidationError.new(message)
97
+ end
98
+
99
+ end
100
+ end
101
+ end
@@ -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.11'
3
+ VERSION = '0.8.16'
4
4
  end
5
5
  end
@@ -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'
@@ -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,88 +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 and remove signature node
216
- @sig_element ||= begin
217
- element = REXML::XPath.first(@working_copy, "//ds:Signature", {"ds"=>DSIG})
218
- element.remove
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"))
219
284
  end
220
285
 
221
- # verify signature
222
- 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
+
223
314
  noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
224
315
  noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
225
- canon_algorithm = canon_algorithm REXML::XPath.first(@sig_element, '//ds:CanonicalizationMethod', 'ds' => DSIG)
316
+
226
317
  canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
227
318
  noko_sig_element.remove
228
319
 
320
+ # get inclusive namespaces
321
+ inclusive_namespaces = extract_inclusive_namespaces
322
+
229
323
  # check digests
230
- REXML::XPath.each(@sig_element, "//ds:Reference", {"ds"=>DSIG}) do |ref|
231
- uri = ref.attributes.get_attribute("URI").value
324
+ ref = REXML::XPath.first(sig_element, "//ds:Reference", {"ds"=>DSIG})
232
325
 
233
- hashed_element = document.at_xpath("//*[@ID='#{uri[1..-1]}']")
234
- canon_algorithm = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod', 'ds' => DSIG)
235
- canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
326
+ hashed_element = document.at_xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
236
327
 
237
- 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
+ )
238
333
 
239
- hash = digest_algorithm.digest(canon_hashed_element)
240
- 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)
241
335
 
242
- unless digests_match?(hash, digest_value)
243
- return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Digest mismatch"))
244
- end
245
- end
336
+ canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
246
337
 
247
- base64_signature = OneLogin::RubySaml::Utils.element_text(REXML::XPath.first(@sig_element, "//ds:SignatureValue", {"ds"=>DSIG}))
248
- 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))
249
350
 
250
- # get certificate object
251
- cert_text = Base64.decode64(base64_cert)
252
- 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
253
354
 
254
- # signature method
255
- 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)
256
358
 
359
+ # verify signature
257
360
  unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
258
361
  return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Key validation error"))
259
362
  end
@@ -263,6 +366,33 @@ module XMLSecurity
263
366
 
264
367
  private
265
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
+
266
396
  def digests_match?(hash, digest_value)
267
397
  hash == digest_value
268
398
  end
@@ -281,11 +411,16 @@ module XMLSecurity
281
411
  end
282
412
 
283
413
  def extract_inclusive_namespaces
284
- 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
285
420
  prefix_list = element.attributes.get_attribute("PrefixList").value
286
421
  prefix_list.split(" ")
287
422
  else
288
- []
423
+ nil
289
424
  end
290
425
  end
291
426