ruby-saml 1.1.2 → 1.2.0

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.

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