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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 10ec0e5a4a9fc6a7f65613599597cef7ae8b8293
4
- data.tar.gz: 0b63798d5d6b78e3073ccb899e355e6aa0134648
2
+ SHA256:
3
+ metadata.gz: 68cb87ca6e3a580cea96b7784b71f60afe0f0982fc9b3c7b2de4fdda0ea1af31
4
+ data.tar.gz: 216f43c0d0a179f3a9c506f198c31e2a01a08a8661bfaafbe3cb50811b1acf88
5
5
  SHA512:
6
- metadata.gz: 1883986a5fd1b3925e6745bf7d259a907c656d36958efe50dfb760192c8273a3723b76344ee542a3a07b84da677808fad850b8bd272d949c5ce7fcd8c171a881
7
- data.tar.gz: 8538b7c75961e99186b320ff1425fb82fa0c759e3e7267eaad76659adf9f0ba98249207122092dbc383f464c1c600b03a37f010d5a3e8bfa7fbb6fba0aaa8915
6
+ metadata.gz: 35ba610649dbff55acae0612782ab7e81947907212b9c454494f9baa5c3926a126430eed919356049261a9cb40767d7079874f56f1b4cb1bd7efb637f4f6ba4f
7
+ data.tar.gz: a3e9ce681547c0e648f477198a134749c9febb1f86e042d2bb3266e01a740767672a667d0c85a162d0f11489e87ee4c1a53cbd15d45e6aeefeb31fc30a2fe99f
data/Gemfile CHANGED
@@ -7,10 +7,13 @@ gemspec
7
7
 
8
8
  if RUBY_VERSION < '1.9'
9
9
  gem 'nokogiri', '~> 1.5.0'
10
+ gem 'minitest', '~> 5.5', '<= 5.11.3'
10
11
  elsif RUBY_VERSION < '2.1'
11
12
  gem 'nokogiri', '>= 1.5.0', '<= 1.6.8.1'
13
+ gem 'minitest', '~> 5.5'
12
14
  else
13
15
  gem 'nokogiri', '>= 1.5.0'
16
+ gem 'minitest', '~> 5.5'
14
17
  end
15
18
 
16
19
  group :test do
@@ -30,6 +33,5 @@ group :test do
30
33
  gem 'shoulda', '~> 2.11'
31
34
  gem 'systemu', '~> 2'
32
35
  gem 'test-unit', '~> 3.0.9'
33
- gem 'minitest', '~> 5.5'
34
36
  gem 'timecop', '<= 0.6.0'
35
37
  end
data/Rakefile CHANGED
@@ -25,17 +25,3 @@ end
25
25
  task :test
26
26
 
27
27
  task :default => :test
28
-
29
- # require 'rake/rdoctask'
30
- # Rake::RDocTask.new do |rdoc|
31
- # if File.exist?('VERSION')
32
- # version = File.read('VERSION')
33
- # else
34
- # version = ""
35
- # end
36
-
37
- # rdoc.rdoc_dir = 'rdoc'
38
- # rdoc.title = "ruby-saml #{version}"
39
- # rdoc.rdoc_files.include('README*')
40
- # rdoc.rdoc_files.include('lib/**/*.rb')
41
- #end
@@ -1,7 +1,5 @@
1
1
  require "xml_security"
2
2
  require "time"
3
- require "base64"
4
- require "zlib"
5
3
 
6
4
  module OneLogin
7
5
  module RubySaml
@@ -30,8 +28,8 @@ module OneLogin
30
28
  self.settings = settings
31
29
 
32
30
  @options = options
33
- @response = decode_raw_response(response)
34
- @document = XMLSecurity::SignedDocument.new(response)
31
+ @response = OneLogin::RubySaml::Utils.decode_raw_saml(response)
32
+ @document = XMLSecurity::SignedDocument.new(@response)
35
33
  end
36
34
 
37
35
  def validate!
@@ -39,7 +37,7 @@ module OneLogin
39
37
  end
40
38
 
41
39
  def validate(soft = true)
42
- return false unless valid_saml?(soft) && valid_state?(soft)
40
+ return false unless validate_structure(soft)
43
41
 
44
42
  valid_in_response_to?(soft) && valid_issuer?(soft) && success?(soft)
45
43
  end
@@ -53,7 +51,7 @@ module OneLogin
53
51
 
54
52
  def in_response_to
55
53
  @in_response_to ||= begin
56
- node = REXML::XPath.first(document, "/p:LogoutResponse", { "p" => PROTOCOL, "a" => ASSERTION })
54
+ node = REXML::XPath.first(document, "/p:LogoutResponse", { "p" => PROTOCOL })
57
55
  node.nil? ? nil : node.attributes['InResponseTo']
58
56
  end
59
57
  end
@@ -61,7 +59,6 @@ module OneLogin
61
59
  def issuer
62
60
  @issuer ||= begin
63
61
  node = REXML::XPath.first(document, "/p:LogoutResponse/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
64
- node ||= REXML::XPath.first(document, "/p:LogoutResponse/a:Assertion/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
65
62
  Utils.element_text(node)
66
63
  end
67
64
  end
@@ -75,28 +72,7 @@ module OneLogin
75
72
 
76
73
  private
77
74
 
78
- def decode(encoded)
79
- Base64.decode64(encoded)
80
- end
81
-
82
- def inflate(deflated)
83
- zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
84
- zlib.inflate(deflated)
85
- end
86
-
87
- def decode_raw_response(response)
88
- if response =~ /^</
89
- return response
90
- elsif (decoded = decode(response)) =~ /^</
91
- return decoded
92
- elsif (inflated = inflate(decoded)) =~ /^</
93
- return inflated
94
- end
95
-
96
- raise "Couldn't decode SAMLResponse"
97
- end
98
-
99
- def valid_saml?(soft = true)
75
+ def validate_structure(soft = true)
100
76
  Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
101
77
  @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
102
78
  @xml = Nokogiri::XML(self.document.to_s)
@@ -108,26 +84,6 @@ module OneLogin
108
84
  end
109
85
  end
110
86
 
111
- def valid_state?(soft = true)
112
- if response.empty?
113
- return soft ? false : validation_error("Blank response")
114
- end
115
-
116
- if settings.nil?
117
- return soft ? false : validation_error("No settings on response")
118
- end
119
-
120
- if settings.sp_entity_id.nil?
121
- return soft ? false : validation_error("No sp_entity_id in settings")
122
- end
123
-
124
- if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
125
- return soft ? false : validation_error("No fingerprint or certificate on settings")
126
- end
127
-
128
- true
129
- end
130
-
131
87
  def valid_in_response_to?(soft = true)
132
88
  return true unless self.options.has_key? :matches_request_id
133
89
 
@@ -139,8 +95,10 @@ module OneLogin
139
95
  end
140
96
 
141
97
  def valid_issuer?(soft = true)
142
- unless URI.parse(issuer) == URI.parse(self.settings.sp_entity_id)
143
- return soft ? false : validation_error("Doesn't match the issuer, expected: <#{self.settings.sp_entity_id}>, but was: <#{issuer}>")
98
+ return true if settings.nil? || settings.idp_entity_id.nil? || issuer.nil?
99
+
100
+ unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id)
101
+ return soft ? false : validation_error("Doesn't match the issuer, expected: <#{self.settings.idp_entity_id}>, but was: <#{issuer}>")
144
102
  end
145
103
  true
146
104
  end
@@ -1,6 +1,7 @@
1
1
  require "xml_security"
2
2
  require "time"
3
3
  require "nokogiri"
4
+ require "onelogin/ruby-saml/utils"
4
5
  require 'onelogin/ruby-saml/attributes'
5
6
 
6
7
  # Only supports SAML 2.0
@@ -22,7 +23,7 @@ module OneLogin
22
23
  def initialize(response, options = {})
23
24
  raise ArgumentError.new("Response cannot be nil") if response.nil?
24
25
  @options = options
25
- @response = (response =~ /^</) ? response : Base64.decode64(response)
26
+ @response = OneLogin::RubySaml::Utils.decode_raw_saml(response)
26
27
  @document = XMLSecurity::SignedDocument.new(@response)
27
28
  end
28
29
 
@@ -133,6 +134,34 @@ module OneLogin
133
134
  end
134
135
  end
135
136
 
137
+ # Gets the Issuers (from Response and Assertion).
138
+ # (returns the first node that matches the supplied xpath from the Response and from the Assertion)
139
+ # @return [Array] Array with the Issuers (REXML::Element)
140
+ #
141
+ def issuers
142
+ @issuers ||= begin
143
+ issuer_response_nodes = REXML::XPath.match(
144
+ document,
145
+ "/p:Response/a:Issuer",
146
+ { "p" => PROTOCOL, "a" => ASSERTION }
147
+ )
148
+
149
+ unless issuer_response_nodes.size == 1
150
+ error_msg = "Issuer of the Response not found or multiple."
151
+ raise ValidationError.new(error_msg)
152
+ end
153
+
154
+ issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer")
155
+ unless issuer_assertion_nodes.size == 1
156
+ error_msg = "Issuer of the Assertion not found or multiple."
157
+ raise ValidationError.new(error_msg)
158
+ end
159
+
160
+ nodes = issuer_response_nodes + issuer_assertion_nodes
161
+ nodes.map { |node| Utils.element_text(node) }.compact.uniq
162
+ end
163
+ end
164
+
136
165
  # @return [Array] The Audience elements from the Contitions of the SAML Response.
137
166
  #
138
167
  def audiences
@@ -149,14 +178,15 @@ module OneLogin
149
178
  end
150
179
 
151
180
  def validate(soft = true)
152
- validate_success_status &&
153
- validate_num_assertion &&
154
- validate_signed_elements &&
155
- validate_structure(soft) &&
156
- validate_response_state(soft) &&
157
- validate_conditions(soft) &&
158
- validate_audience(soft) &&
159
- document.validate_document(get_fingerprint, soft) &&
181
+ validate_structure(soft) &&
182
+ validate_success_status(soft) &&
183
+ validate_num_assertion &&
184
+ validate_signed_elements(soft) &&
185
+ validate_response_state(soft) &&
186
+ validate_conditions(soft) &&
187
+ validate_audience(soft) &&
188
+ validate_issuer(soft) &&
189
+ validate_signature(soft) &&
160
190
  success?
161
191
  end
162
192
 
@@ -175,10 +205,6 @@ module OneLogin
175
205
  { "a" => ASSERTION }
176
206
  )
177
207
 
178
- unless assertions.size != 0
179
- return soft ? false : validation_error("Encrypted assertion is not supported")
180
- end
181
-
182
208
  unless assertions.size + encrypted_assertions.size == 1
183
209
  return soft ? false : validation_error("SAML Response must contain 1 assertion")
184
210
  end
@@ -190,7 +216,7 @@ module OneLogin
190
216
  # @return [Boolean] True if there is 1 or 2 Elements signed in the SAML Response
191
217
  # an are a Response or an Assertion Element, otherwise False if soft=True
192
218
  #
193
- def validate_signed_elements
219
+ def validate_signed_elements(soft)
194
220
  signature_nodes = REXML::XPath.match(
195
221
  document,
196
222
  "//ds:Signature",
@@ -228,7 +254,6 @@ module OneLogin
228
254
 
229
255
  if verified_seis.include?(sei)
230
256
  return soft ? false : validation_error("Duplicated Reference URI. SAML Response rejected")
231
- return append_error("Duplicated Reference URI. SAML Response rejected")
232
257
  end
233
258
 
234
259
  verified_seis.push(sei)
@@ -249,7 +274,7 @@ module OneLogin
249
274
  # @return [Boolean] True if the SAML Response contains a Success code, otherwise False if soft == false
250
275
  # @raise [ValidationError] if soft == false and validation fails
251
276
  #
252
- def validate_success_status
277
+ def validate_success_status(soft = true)
253
278
  return true if success?
254
279
 
255
280
  return false unless soft
@@ -298,6 +323,21 @@ module OneLogin
298
323
  end
299
324
  end
300
325
 
326
+ # @return [String] the StatusMessage value from a SAML Response.
327
+ #
328
+ def status_message
329
+ @status_message ||= begin
330
+ nodes = REXML::XPath.match(
331
+ document,
332
+ "/p:Response/p:Status/p:StatusMessage",
333
+ { "p" => PROTOCOL }
334
+ )
335
+ if nodes.size == 1
336
+ Utils.element_text(nodes.first)
337
+ end
338
+ end
339
+ end
340
+
301
341
  def validate_structure(soft = true)
302
342
  Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
303
343
  @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
@@ -362,15 +402,6 @@ module OneLogin
362
402
  ))
363
403
  end
364
404
 
365
- def get_fingerprint
366
- if settings.idp_cert
367
- cert = OpenSSL::X509::Certificate.new(settings.idp_cert)
368
- Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
369
- else
370
- settings.idp_cert_fingerprint
371
- end
372
- end
373
-
374
405
  def validate_conditions(soft = true)
375
406
  return true if conditions.nil?
376
407
  return true if options[:skip_conditions]
@@ -388,17 +419,76 @@ module OneLogin
388
419
  true
389
420
  end
390
421
 
422
+ def validate_issuer(soft = true)
423
+ return true if settings.idp_entity_id.nil?
424
+
425
+ begin
426
+ obtained_issuers = issuers
427
+ rescue ValidationError => e
428
+ return soft ? false : validation_error("Error while extracting issuers")
429
+ end
430
+
431
+ obtained_issuers.each do |issuer|
432
+ unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id)
433
+ error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>"
434
+ return soft ? false : validation_error(error_msg)
435
+ end
436
+ end
437
+
438
+ true
439
+ end
440
+
441
+ def validate_signature(soft = true)
442
+ error_msg = "Invalid Signature on SAML Response"
443
+
444
+ sig_elements = REXML::XPath.match(
445
+ document,
446
+ "/p:Response[@ID=$id]/ds:Signature]",
447
+ { "p" => PROTOCOL, "ds" => DSIG },
448
+ { 'id' => document.signed_element_id }
449
+ )
450
+
451
+ # Check signature nodes
452
+ if sig_elements.nil? || sig_elements.size == 0
453
+ sig_elements = REXML::XPath.match(
454
+ document,
455
+ "/p:Response/a:Assertion[@ID=$id]/ds:Signature",
456
+ {"p" => PROTOCOL, "a" => ASSERTION, "ds"=>DSIG},
457
+ { 'id' => document.signed_element_id }
458
+ )
459
+ end
460
+
461
+ if sig_elements.size != 1
462
+ if sig_elements.size == 0
463
+ error_msg += ". Signed element id ##{doc.signed_element_id} is not found"
464
+ else
465
+ error_msg += ". Signed element id ##{doc.signed_element_id} is found more than once"
466
+ end
467
+ return soft ? false : validation_error(error_msg)
468
+ end
469
+
470
+ opts = {}
471
+ opts[:fingerprint_alg] = OpenSSL::Digest::SHA1.new
472
+ opts[:cert] = settings.get_idp_cert
473
+ fingerprint = settings.get_fingerprint
474
+
475
+ unless fingerprint
476
+ return soft ? false : validation_error("No fingerprint or certificate on settings")
477
+ end
478
+
479
+ unless document.validate_document(fingerprint, soft, opts)
480
+ return soft ? false : validation_error(error_msg)
481
+ end
482
+
483
+ true
484
+ end
485
+
391
486
  def parse_time(node, attribute)
392
487
  if node && node.attributes[attribute]
393
488
  Time.parse(node.attributes[attribute])
394
489
  end
395
490
  end
396
491
 
397
- # Validates the Audience, (If the Audience match the Service Provider EntityID)
398
- # If fails, the error is added to the errors array
399
- # @return [Boolean] True if there is an Audience Element that match the Service Provider EntityID, otherwise False if soft=True
400
- # @raise [ValidationError] if soft == false and validation fails
401
- #
402
492
  def validate_audience(soft = true)
403
493
  return true if audiences.empty? || settings.sp_entity_id.nil? || settings.sp_entity_id.empty?
404
494
 
@@ -410,6 +500,7 @@ module OneLogin
410
500
 
411
501
  true
412
502
  end
503
+
413
504
  end
414
505
  end
415
506
  end
@@ -27,6 +27,7 @@ module OneLogin
27
27
  attr_accessor :idp_cert_fingerprint
28
28
  attr_accessor :idp_cert
29
29
  attr_accessor :idp_slo_target_url
30
+ attr_accessor :idp_entity_id
30
31
  #sp data
31
32
  attr_accessor :sp_entity_id
32
33
  attr_accessor :assertion_consumer_service_url
@@ -36,7 +37,6 @@ module OneLogin
36
37
  attr_accessor :name_identifier_value
37
38
  attr_accessor :name_identifier_value_requested
38
39
  attr_accessor :sessionindex
39
- attr_accessor :assertion_consumer_logout_service_url
40
40
  attr_accessor :compress_request
41
41
  attr_accessor :compress_response
42
42
  attr_accessor :double_quote_xml_attribute_values
@@ -117,21 +117,38 @@ module OneLogin
117
117
  @single_logout_service_binding = url
118
118
  end
119
119
 
120
- # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
120
+ # Calculates the fingerprint of the IdP x509 certificate.
121
+ # @return [String] The fingerprint
121
122
  #
122
- def get_sp_cert
123
- return nil if certificate.nil? || certificate.empty?
123
+ def get_fingerprint
124
+ idp_cert_fingerprint || begin
125
+ idp_cert = get_idp_cert
126
+ if idp_cert
127
+ Digest::SHA1.hexdigest(idp_cert.to_der).upcase.scan(/../).join(":")
128
+ end
129
+ end
130
+ end
124
131
 
125
- formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate)
126
- OpenSSL::X509::Certificate.new(formatted_cert)
132
+ # @return [OpenSSL::X509::Certificate|nil] Build the IdP certificate from the settings (previously format it)
133
+ #
134
+ def get_idp_cert
135
+ return nil if idp_cert.nil?
136
+
137
+ if idp_cert.respond_to?(:to_pem)
138
+ idp_cert
139
+ else
140
+ return nil if idp_cert.empty?
141
+ formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
142
+ OpenSSL::X509::Certificate.new(formatted_cert)
143
+ end
127
144
  end
128
145
 
129
- # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings (previously format it)
146
+ # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
130
147
  #
131
- def get_sp_cert_new
132
- return nil if certificate_new.nil? || certificate_new.empty?
148
+ def get_sp_cert
149
+ return nil if certificate.nil? || certificate.empty?
133
150
 
134
- formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate_new)
151
+ formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate)
135
152
  OpenSSL::X509::Certificate.new(formatted_cert)
136
153
  end
137
154