ruby-saml 1.1.2 → 1.2.0

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.

@@ -5,6 +5,7 @@ require 'nokogiri'
5
5
  require 'rexml/document'
6
6
  require 'rexml/xpath'
7
7
  require 'thread'
8
+ require "onelogin/ruby-saml/error_handling"
8
9
 
9
10
  # Only supports SAML 2.0
10
11
  module OneLogin
@@ -69,23 +70,15 @@ module OneLogin
69
70
  end
70
71
  rescue Exception => error
71
72
  return false if soft
72
- validation_error("XML load failed: #{error.message}")
73
+ raise ValidationError.new("XML load failed: #{error.message}")
73
74
  end
74
75
 
75
76
  SamlMessage.schema.validate(xml).map do |error|
76
77
  return false if soft
77
- validation_error("#{error.message}\n\n#{xml.to_s}")
78
+ raise ValidationError.new("#{error.message}\n\n#{xml.to_s}")
78
79
  end
79
80
  end
80
81
 
81
- # Raise a ValidationError with the provided message
82
- # @param message [String] Message of the exception
83
- # @raise [ValidationError]
84
- #
85
- def validation_error(message)
86
- raise ValidationError.new(message)
87
- end
88
-
89
82
  private
90
83
 
91
84
  # Base64 decode and try also to inflate a SAML Message
@@ -28,6 +28,7 @@ module OneLogin
28
28
  attr_accessor :idp_cert
29
29
  attr_accessor :idp_cert_fingerprint
30
30
  attr_accessor :idp_cert_fingerprint_algorithm
31
+ attr_accessor :idp_attribute_names
31
32
  # SP Data
32
33
  attr_accessor :issuer
33
34
  attr_accessor :assertion_consumer_service_url
@@ -153,6 +154,7 @@ module OneLogin
153
154
  :authn_requests_signed => false,
154
155
  :logout_requests_signed => false,
155
156
  :logout_responses_signed => false,
157
+ :want_assertions_signed => false,
156
158
  :metadata_signed => false,
157
159
  :embed_sign => false,
158
160
  :digest_method => XMLSecurity::Document::SHA1,
@@ -11,13 +11,11 @@ module OneLogin
11
11
  # SAML2 Logout Request (SLO IdP initiated, Parser)
12
12
  #
13
13
  class SloLogoutrequest < SamlMessage
14
+ include ErrorHandling
14
15
 
15
16
  # OneLogin::RubySaml::Settings Toolkit settings
16
17
  attr_accessor :settings
17
18
 
18
- # Array with the causes [Array of strings]
19
- attr_accessor :errors
20
-
21
19
  attr_reader :document
22
20
  attr_reader :request
23
21
  attr_reader :options
@@ -32,15 +30,15 @@ module OneLogin
32
30
  # @raise [ArgumentError] If Request is nil
33
31
  #
34
32
  def initialize(request, options = {})
35
- @errors = []
36
33
  raise ArgumentError.new("Request cannot be nil") if request.nil?
37
- @options = options
38
34
 
35
+ @errors = []
36
+ @options = options
39
37
  @soft = true
40
- if !options.empty? && !options[:settings].nil?
38
+ unless options[:settings].nil?
41
39
  @settings = options[:settings]
42
- if !options[:settings].soft.nil?
43
- @soft = options[:settings].soft
40
+ unless @settings.soft.nil?
41
+ @soft = @settings.soft
44
42
  end
45
43
  end
46
44
 
@@ -48,23 +46,12 @@ module OneLogin
48
46
  @document = REXML::Document.new(@request)
49
47
  end
50
48
 
51
- # Append the cause to the errors array, and based on the value of soft, return false or raise
52
- # an exception
53
- def append_error(error_msg)
54
- @errors << error_msg
55
- return soft ? false : validation_error(error_msg)
56
- end
57
-
58
- # Reset the errors array
59
- def reset_errors!
60
- @errors = []
61
- end
62
-
63
49
  # Validates the Logout Request with the default values (soft = true)
50
+ # @param collect_errors [Boolean] Stop validation when first error appears or keep validating.
64
51
  # @return [Boolean] TRUE if the Logout Request is valid
65
52
  #
66
- def is_valid?
67
- validate
53
+ def is_valid?(collect_errors = false)
54
+ validate(collect_errors)
68
55
  end
69
56
 
70
57
  # @return [String] Gets the NameID of the Logout Request.
@@ -132,19 +119,32 @@ module OneLogin
132
119
  private
133
120
 
134
121
  # Hard aux function to validate the Logout Request
122
+ # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true)
135
123
  # @return [Boolean] TRUE if the Logout Request is valid
136
124
  # @raise [ValidationError] if soft == false and validation fails
137
125
  #
138
- def validate
126
+ def validate(collect_errors = false)
139
127
  reset_errors!
140
128
 
141
- validate_request_state &&
142
- validate_id &&
143
- validate_version &&
144
- validate_structure &&
145
- validate_not_on_or_after &&
146
- validate_issuer &&
147
- validate_signature
129
+ if collect_errors
130
+ validate_request_state
131
+ validate_id
132
+ validate_version
133
+ validate_structure
134
+ validate_not_on_or_after
135
+ validate_issuer
136
+ validate_signature
137
+
138
+ @errors.empty?
139
+ else
140
+ validate_request_state &&
141
+ validate_id &&
142
+ validate_version &&
143
+ validate_structure &&
144
+ validate_not_on_or_after &&
145
+ validate_issuer &&
146
+ validate_signature
147
+ end
148
148
  end
149
149
 
150
150
  # Validates that the Logout Request contains an ID
@@ -213,7 +213,7 @@ module OneLogin
213
213
  # @raise [ValidationError] if soft == false and validation fails
214
214
  #
215
215
  def validate_issuer
216
- return true if settings.idp_entity_id.nil? || issuer.nil?
216
+ return true if settings.nil? || settings.idp_entity_id.nil? || issuer.nil?
217
217
 
218
218
  unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
219
219
  return append_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>")
@@ -230,7 +230,7 @@ module OneLogin
230
230
  return true if options.nil?
231
231
  return true unless options.has_key? :get_params
232
232
  return true unless options[:get_params].has_key? 'Signature'
233
- return true if settings.nil? || settings.get_idp_cert.nil?
233
+ return true if settings.get_idp_cert.nil?
234
234
 
235
235
  query_string = OneLogin::RubySaml::Utils.build_query(
236
236
  :type => 'SAMLRequest',
@@ -1,7 +1,7 @@
1
- require "uuid"
2
-
3
1
  require "onelogin/ruby-saml/logging"
2
+
4
3
  require "onelogin/ruby-saml/saml_message"
4
+ require "onelogin/ruby-saml/utils"
5
5
 
6
6
  # Only supports SAML 2.0
7
7
  module OneLogin
@@ -18,7 +18,7 @@ module OneLogin
18
18
  # Asigns an ID, a random uuid.
19
19
  #
20
20
  def initialize
21
- @uuid = "_" + UUID.new.generate
21
+ @uuid = OneLogin::RubySaml::Utils.uuid
22
22
  end
23
23
 
24
24
  # Creates the Logout Response string.
@@ -1,9 +1,16 @@
1
+ if RUBY_VERSION < '1.9'
2
+ require 'uuid'
3
+ else
4
+ require 'securerandom'
5
+ end
6
+
1
7
  module OneLogin
2
8
  module RubySaml
3
9
 
4
10
  # SAML2 Auxiliary class
5
- #
11
+ #
6
12
  class Utils
13
+ @@uuid_generator = UUID.new if RUBY_VERSION < '1.9'
7
14
 
8
15
  DSIG = "http://www.w3.org/2000/09/xmldsig#"
9
16
  XENC = "http://www.w3.org/2001/04/xmlenc#"
@@ -30,7 +37,7 @@ module OneLogin
30
37
  # @return [String] The formatted private key
31
38
  #
32
39
  def self.format_private_key(key)
33
- # don't try to format an encoded private key or if is empty
40
+ # don't try to format an encoded private key or if is empty
34
41
  return key if key.nil? || key.empty? || key.match(/\x0d/)
35
42
 
36
43
  # is this an rsa key?
@@ -114,7 +121,7 @@ module OneLogin
114
121
  { 'xenc' => XENC }
115
122
  )
116
123
  algorithm = encrypt_method.attributes['Algorithm']
117
- retrieve_plaintext(node, symmetric_key, algorithm)
124
+ retrieve_plaintext(node, symmetric_key, algorithm)
118
125
  end
119
126
 
120
127
  # Obtains the symmetric key from the EncryptedData element
@@ -122,19 +129,25 @@ module OneLogin
122
129
  # @param private_key [OpenSSL::PKey::RSA] The Service provider private key
123
130
  # @return [String] The symmetric key
124
131
  def self.retrieve_symmetric_key(encrypt_data, private_key)
125
- encrypted_symmetric_key_element = REXML::XPath.first(
132
+ encrypted_key = REXML::XPath.first(
126
133
  encrypt_data,
127
- "//xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey/xenc:CipherData/xenc:CipherValue",
134
+ "//xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey or \
135
+ //xenc:EncryptedKey[@Id=substring-after(//xenc:EncryptedData/ds:KeyInfo/ds:RetrievalMethod/@URI, '#')]",
136
+ { "ds" => DSIG, "xenc" => XENC }
137
+ )
138
+ encrypted_symmetric_key_element = REXML::XPath.first(
139
+ encrypted_key,
140
+ "./xenc:CipherData/xenc:CipherValue",
128
141
  { "ds" => DSIG, "xenc" => XENC }
129
142
  )
130
143
  cipher_text = Base64.decode64(encrypted_symmetric_key_element.text)
131
144
  encrypt_method = REXML::XPath.first(
132
- encrypt_data,
133
- "//xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey/xenc:EncryptionMethod",
145
+ encrypted_key,
146
+ "./xenc:EncryptionMethod",
134
147
  {"ds" => DSIG, "xenc" => XENC }
135
148
  )
136
149
  algorithm = encrypt_method.attributes['Algorithm']
137
- retrieve_plaintext(cipher_text, private_key, algorithm)
150
+ retrieve_plaintext(cipher_text, private_key, algorithm)
138
151
  end
139
152
 
140
153
  # Obtains the deciphered text
@@ -152,7 +165,7 @@ module OneLogin
152
165
  when 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' then oaep = symmetric_key
153
166
  end
154
167
 
155
- if cipher
168
+ if cipher
156
169
  iv_len = cipher.iv_len
157
170
  data = cipher_text[iv_len..-1]
158
171
  cipher.padding, cipher.key, cipher.iv = 0, symmetric_key, cipher_text[0..iv_len-1]
@@ -167,6 +180,9 @@ module OneLogin
167
180
  end
168
181
  end
169
182
 
183
+ def self.uuid
184
+ RUBY_VERSION < '1.9' ? "_#{@@uuid_generator.generate}" : "_#{SecureRandom.uuid}"
185
+ end
170
186
  end
171
187
  end
172
188
  end
@@ -1,5 +1,5 @@
1
1
  module OneLogin
2
2
  module RubySaml
3
- VERSION = '1.1.2'
3
+ VERSION = '1.2.0'
4
4
  end
5
5
  end
data/lib/xml_security.rb CHANGED
@@ -29,7 +29,7 @@ require "openssl"
29
29
  require 'nokogiri'
30
30
  require "digest/sha1"
31
31
  require "digest/sha2"
32
- require "onelogin/ruby-saml/validation_error"
32
+ require "onelogin/ruby-saml/error_handling"
33
33
 
34
34
  module XMLSecurity
35
35
 
@@ -48,9 +48,14 @@ module XMLSecurity
48
48
  end
49
49
 
50
50
  case algorithm
51
- when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" then Nokogiri::XML::XML_C14N_1_0
52
- when "http://www.w3.org/2006/12/xml-c14n11" then Nokogiri::XML::XML_C14N_1_1
53
- else Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
51
+ when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
52
+ "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
53
+ Nokogiri::XML::XML_C14N_1_0
54
+ when "http://www.w3.org/2006/12/xml-c14n11",
55
+ "http://www.w3.org/2006/12/xml-c14n11#WithComments"
56
+ Nokogiri::XML::XML_C14N_1_1
57
+ else
58
+ Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
54
59
  end
55
60
  end
56
61
 
@@ -74,10 +79,10 @@ module XMLSecurity
74
79
  end
75
80
 
76
81
  class Document < BaseDocument
77
- RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
78
- RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
79
- RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
80
- RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
82
+ RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
83
+ RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
84
+ RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
85
+ RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
81
86
  SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
82
87
  SHA256 = "http://www.w3.org/2001/04/xmldsig-more#sha256"
83
88
  SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"
@@ -179,9 +184,9 @@ module XMLSecurity
179
184
  end
180
185
 
181
186
  class SignedDocument < BaseDocument
187
+ include OneLogin::RubySaml::ErrorHandling
182
188
 
183
189
  attr_accessor :signed_element_id
184
- attr_accessor :errors
185
190
 
186
191
  def initialize(response, errors = [])
187
192
  super(response)
@@ -200,10 +205,14 @@ module XMLSecurity
200
205
  { "ds"=>DSIG }
201
206
  )
202
207
 
203
- if cert_element
208
+ if cert_element
204
209
  base64_cert = cert_element.text
205
210
  cert_text = Base64.decode64(base64_cert)
206
- cert = OpenSSL::X509::Certificate.new(cert_text)
211
+ begin
212
+ cert = OpenSSL::X509::Certificate.new(cert_text)
213
+ rescue OpenSSL::X509::CertificateError => e
214
+ return append_error("Certificate Error", soft)
215
+ end
207
216
 
208
217
  if options[:fingerprint_alg]
209
218
  fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(options[:fingerprint_alg]).new
@@ -215,7 +224,7 @@ module XMLSecurity
215
224
  # check cert matches registered idp cert
216
225
  if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
217
226
  @errors << "Fingerprint mismatch"
218
- return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Fingerprint mismatch"))
227
+ return append_error("Fingerprint mismatch", soft)
219
228
  end
220
229
  else
221
230
  if options[:cert]
@@ -224,8 +233,8 @@ module XMLSecurity
224
233
  if soft
225
234
  return false
226
235
  else
227
- raise OneLogin::RubySaml::ValidationError.new("Certificate element missing in response (ds:X509Certificate) and not cert provided at settings")
228
- end
236
+ return append_error("Certificate element missing in response (ds:X509Certificate) and not cert provided at settings", soft)
237
+ end
229
238
  end
230
239
  end
231
240
  validate_signature(base64_cert, soft)
@@ -273,12 +282,6 @@ module XMLSecurity
273
282
  noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
274
283
  noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
275
284
 
276
- # Handle when no URI
277
- noko_signed_info_reference_element_uri_attr = noko_signed_info_element.at_xpath('./ds:Reference', 'ds' => DSIG).attributes["URI"]
278
- if (noko_signed_info_reference_element_uri_attr.value.empty?)
279
- noko_signed_info_reference_element_uri_attr.value = "##{document.root.attribute('ID')}"
280
- end
281
-
282
285
  canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
283
286
  noko_sig_element.remove
284
287
 
@@ -289,8 +292,8 @@ module XMLSecurity
289
292
  ref = REXML::XPath.first(sig_element, "//ds:Reference", {"ds"=>DSIG})
290
293
  uri = ref.attributes.get_attribute("URI").value
291
294
 
292
- hashed_element = uri.empty? ? document : document.at_xpath("//*[@ID=$uri]", nil, { 'uri' => uri[1..-1] })
293
- # hashed_element = document.at_xpath("//*[@ID=$uri]", nil, { 'uri' => uri[1..-1] })
295
+ hashed_element = document.at_xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
296
+
294
297
  canon_algorithm = canon_algorithm REXML::XPath.first(
295
298
  ref,
296
299
  '//ds:CanonicalizationMethod',
@@ -313,7 +316,7 @@ module XMLSecurity
313
316
 
314
317
  unless digests_match?(hash, digest_value)
315
318
  @errors << "Digest mismatch"
316
- return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Digest mismatch"))
319
+ return append_error("Digest mismatch", soft)
317
320
  end
318
321
 
319
322
  # get certificate object
@@ -322,8 +325,7 @@ module XMLSecurity
322
325
 
323
326
  # verify signature
324
327
  unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
325
- @errors << "Key validation error"
326
- return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Key validation error"))
328
+ return append_error("Key validation error", soft)
327
329
  end
328
330
 
329
331
  return true
@@ -344,8 +346,8 @@ module XMLSecurity
344
346
 
345
347
  return nil if reference_element.nil?
346
348
 
347
- sei = reference_element.attribute("URI").value[1..-1]
348
- sei.nil? ? self.root.attribute("ID") : sei
349
+ sei = reference_element.attribute("URI").value[1..-1]
350
+ sei.nil? ? reference_element.parent.parent.parent.attribute("ID").value : sei
349
351
  end
350
352
 
351
353
  def extract_inclusive_namespaces
data/ruby-saml.gemspec CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |s|
25
25
  s.summary = %q{SAML Ruby Tookit}
26
26
  s.test_files = `git ls-files test/*`.split("\n")
27
27
 
28
- s.add_runtime_dependency('uuid', '~> 2.3')
28
+
29
29
 
30
30
  # Because runtime dependencies are determined at build time, we cannot make
31
31
  # Nokogiri's version dependent on the Ruby version, even though we would
@@ -33,6 +33,9 @@ Gem::Specification.new do |s|
33
33
  if defined?(JRUBY_VERSION)
34
34
  s.add_runtime_dependency('nokogiri', '>= 1.6.0')
35
35
  s.add_runtime_dependency('jruby-openssl', '>= 0.9.8')
36
+ elsif RUBY_VERSION < '1.9'
37
+ s.add_runtime_dependency('uuid')
38
+ s.add_runtime_dependency('nokogiri', '<= 1.5.11')
36
39
  else
37
40
  s.add_runtime_dependency('nokogiri', '>= 1.5.10')
38
41
  end
@@ -28,7 +28,34 @@ class IdpMetadataParserTest < Minitest::Test
28
28
  assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
29
29
  assert_equal "https://example.hello.com/access/saml/logout", settings.idp_slo_target_url
30
30
  assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", settings.name_identifier_format
31
+ assert_equal ["AuthToken", "SSOStartPage"], settings.idp_attribute_names
32
+ assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
31
33
  end
34
+
35
+ it "extract certificate from md:KeyDescriptor[@use='signing']" do
36
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
37
+ idp_metadata = read_response("idp_descriptor.xml")
38
+ settings = idp_metadata_parser.parse(idp_metadata)
39
+ assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
40
+ end
41
+
42
+ it "extract certificate from md:KeyDescriptor[@use='encryption']" do
43
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
44
+ idp_metadata = read_response("idp_descriptor.xml")
45
+ idp_metadata = idp_metadata.sub(/<md:KeyDescriptor use="signing">(.*?)<\/md:KeyDescriptor>/m, "")
46
+ settings = idp_metadata_parser.parse(idp_metadata)
47
+ assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
48
+ end
49
+
50
+ it "extract certificate from md:KeyDescriptor" do
51
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
52
+ idp_metadata = read_response("idp_descriptor.xml")
53
+ idp_metadata = idp_metadata.sub(/<md:KeyDescriptor use="signing">(.*?)<\/md:KeyDescriptor>/m, "")
54
+ idp_metadata = idp_metadata.sub('<md:KeyDescriptor use="encryption">', '<md:KeyDescriptor>')
55
+ settings = idp_metadata_parser.parse(idp_metadata)
56
+ assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
57
+ end
58
+
32
59
  end
33
60
 
34
61
  describe "download and parse IdP descriptor file" do
@@ -52,6 +79,7 @@ class IdpMetadataParserTest < Minitest::Test
52
79
  assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
53
80
  assert_equal "https://example.hello.com/access/saml/logout", settings.idp_slo_target_url
54
81
  assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", settings.name_identifier_format
82
+ assert_equal ["AuthToken", "SSOStartPage"], settings.idp_attribute_names
55
83
  assert_equal OpenSSL::SSL::VERIFY_PEER, @http.verify_mode
56
84
  end
57
85