ruby-saml 0.8.11 → 0.8.16

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 (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