ruby-saml 1.16.0 → 1.18.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.
@@ -55,13 +55,14 @@ module OneLogin
55
55
  attr_accessor :compress_response
56
56
  attr_accessor :double_quote_xml_attribute_values
57
57
  attr_accessor :message_max_bytesize
58
+ attr_accessor :check_malformed_doc
58
59
  attr_accessor :passive
59
60
  attr_reader :protocol_binding
60
61
  attr_accessor :attributes_index
61
62
  attr_accessor :force_authn
62
63
  attr_accessor :certificate
63
- attr_accessor :certificate_new
64
64
  attr_accessor :private_key
65
+ attr_accessor :sp_cert_multi
65
66
  attr_accessor :authn_context
66
67
  attr_accessor :authn_context_comparison
67
68
  attr_accessor :authn_context_decl_ref
@@ -70,6 +71,7 @@ module OneLogin
70
71
  attr_accessor :security
71
72
  attr_accessor :soft
72
73
  # Deprecated
74
+ attr_accessor :certificate_new
73
75
  attr_accessor :assertion_consumer_logout_service_url
74
76
  attr_reader :assertion_consumer_logout_service_binding
75
77
  attr_accessor :issuer
@@ -180,10 +182,7 @@ module OneLogin
180
182
  # @return [OpenSSL::X509::Certificate|nil] Build the IdP certificate from the settings (previously format it)
181
183
  #
182
184
  def get_idp_cert
183
- return nil if idp_cert.nil? || idp_cert.empty?
184
-
185
- formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
186
- OpenSSL::X509::Certificate.new(formatted_cert)
185
+ OneLogin::RubySaml::Utils.build_cert_object(idp_cert)
187
186
  end
188
187
 
189
188
  # @return [Hash with 2 arrays of OpenSSL::X509::Certificate] Build multiple IdP certificates from the settings.
@@ -191,7 +190,7 @@ module OneLogin
191
190
  def get_idp_cert_multi
192
191
  return nil if idp_cert_multi.nil? || idp_cert_multi.empty?
193
192
 
194
- raise ArgumentError.new("Invalid value for idp_cert_multi") if not idp_cert_multi.is_a?(Hash)
193
+ raise ArgumentError.new("Invalid value for idp_cert_multi") unless idp_cert_multi.is_a?(Hash)
195
194
 
196
195
  certs = {:signing => [], :encryption => [] }
197
196
 
@@ -200,49 +199,70 @@ module OneLogin
200
199
  next if !certs_for_type || certs_for_type.empty?
201
200
 
202
201
  certs_for_type.each do |idp_cert|
203
- formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
204
- certs[type].push(OpenSSL::X509::Certificate.new(formatted_cert))
202
+ certs[type].push(OneLogin::RubySaml::Utils.build_cert_object(idp_cert))
205
203
  end
206
204
  end
207
205
 
208
206
  certs
209
207
  end
210
208
 
211
- # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
212
- #
213
- def get_sp_cert
214
- return nil if certificate.nil? || certificate.empty?
209
+ # @return [Hash<Symbol, Array<Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>>>]
210
+ # Build the SP certificates and private keys from the settings. If
211
+ # check_sp_cert_expiration is true, only returns certificates and private keys
212
+ # that are not expired.
213
+ def get_sp_certs
214
+ certs = get_all_sp_certs
215
+ return certs unless security[:check_sp_cert_expiration]
215
216
 
216
- formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate)
217
- cert = OpenSSL::X509::Certificate.new(formatted_cert)
217
+ active_certs = { signing: [], encryption: [] }
218
+ certs.each do |use, pairs|
219
+ next if pairs.empty?
218
220
 
219
- if security[:check_sp_cert_expiration]
220
- if OneLogin::RubySaml::Utils.is_cert_expired(cert)
221
- raise OneLogin::RubySaml::ValidationError.new("The SP certificate expired.")
222
- end
221
+ pairs = pairs.select { |cert, _| !cert || OneLogin::RubySaml::Utils.is_cert_active(cert) }
222
+ raise OneLogin::RubySaml::ValidationError.new("The SP certificate expired.") if pairs.empty?
223
+
224
+ active_certs[use] = pairs.freeze
223
225
  end
226
+ active_certs.freeze
227
+ end
224
228
 
225
- cert
229
+ # @return [Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>]
230
+ # The SP signing certificate and private key.
231
+ def get_sp_signing_pair
232
+ get_sp_certs[:signing].first
226
233
  end
227
234
 
228
- # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings (previously format it)
229
- #
230
- def get_sp_cert_new
231
- return nil if certificate_new.nil? || certificate_new.empty?
235
+ # @return [OpenSSL::X509::Certificate] The SP signing certificate.
236
+ # @deprecated Use get_sp_signing_pair or get_sp_certs instead.
237
+ def get_sp_cert
238
+ node = get_sp_signing_pair
239
+ node[0] if node
240
+ end
232
241
 
233
- formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate_new)
234
- OpenSSL::X509::Certificate.new(formatted_cert)
242
+ # @return [OpenSSL::PKey::RSA] The SP signing key.
243
+ def get_sp_signing_key
244
+ node = get_sp_signing_pair
245
+ node[1] if node
235
246
  end
236
247
 
237
- # @return [OpenSSL::PKey::RSA] Build the SP private from the settings (previously format it)
238
- #
239
- def get_sp_key
240
- return nil if private_key.nil? || private_key.empty?
248
+ # @deprecated Use get_sp_signing_key or get_sp_certs instead.
249
+ alias_method :get_sp_key, :get_sp_signing_key
241
250
 
242
- formatted_private_key = OneLogin::RubySaml::Utils.format_private_key(private_key)
243
- OpenSSL::PKey::RSA.new(formatted_private_key)
251
+ # @return [Array<OpenSSL::PKey::RSA>] The SP decryption keys.
252
+ def get_sp_decryption_keys
253
+ ary = get_sp_certs[:encryption].map { |pair| pair[1] }
254
+ ary.compact!
255
+ ary.uniq!(&:to_pem)
256
+ ary.freeze
244
257
  end
245
258
 
259
+ # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings.
260
+ #
261
+ # @deprecated Use get_sp_certs instead
262
+ def get_sp_cert_new
263
+ node = get_sp_certs[:signing].last
264
+ node[0] if node
265
+ end
246
266
 
247
267
  def idp_binding_from_embed_sign
248
268
  security[:embed_sign] ? Utils::BINDINGS[:post] : Utils::BINDINGS[:redirect]
@@ -262,7 +282,9 @@ module OneLogin
262
282
  :compress_response => true,
263
283
  :message_max_bytesize => 250000,
264
284
  :soft => true,
285
+ :check_malformed_doc => true,
265
286
  :double_quote_xml_attribute_values => false,
287
+
266
288
  :security => {
267
289
  :authn_requests_signed => false,
268
290
  :logout_requests_signed => false,
@@ -280,6 +302,85 @@ module OneLogin
280
302
  :lowercase_url_encoding => false
281
303
  }.freeze
282
304
  }.freeze
305
+
306
+ private
307
+
308
+ # @return [Hash<Symbol, Array<Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>>>]
309
+ # Build the SP certificates and private keys from the settings. Returns all
310
+ # certificates and private keys, even if they are expired.
311
+ def get_all_sp_certs
312
+ validate_sp_certs_params!
313
+ get_sp_certs_multi || get_sp_certs_single
314
+ end
315
+
316
+ # Validate certificate, certificate_new, private_key, and sp_cert_multi params.
317
+ def validate_sp_certs_params!
318
+ multi = sp_cert_multi && !sp_cert_multi.empty?
319
+ cert = certificate && !certificate.empty?
320
+ cert_new = certificate_new && !certificate_new.empty?
321
+ pk = private_key && !private_key.empty?
322
+ if multi && (cert || cert_new || pk)
323
+ raise ArgumentError.new("Cannot specify both sp_cert_multi and certificate, certificate_new, private_key parameters")
324
+ end
325
+ end
326
+
327
+ # Get certs from certificate, certificate_new, and private_key parameters.
328
+ def get_sp_certs_single
329
+ certs = { :signing => [], :encryption => [] }
330
+
331
+ sp_key = OneLogin::RubySaml::Utils.build_private_key_object(private_key)
332
+ cert = OneLogin::RubySaml::Utils.build_cert_object(certificate)
333
+ if cert || sp_key
334
+ ary = [cert, sp_key].freeze
335
+ certs[:signing] << ary
336
+ certs[:encryption] << ary
337
+ end
338
+
339
+ cert_new = OneLogin::RubySaml::Utils.build_cert_object(certificate_new)
340
+ if cert_new
341
+ ary = [cert_new, sp_key].freeze
342
+ certs[:signing] << ary
343
+ certs[:encryption] << ary
344
+ end
345
+
346
+ certs
347
+ end
348
+
349
+ # Get certs from get_sp_cert_multi parameter.
350
+ def get_sp_certs_multi
351
+ return if sp_cert_multi.nil? || sp_cert_multi.empty?
352
+
353
+ raise ArgumentError.new("sp_cert_multi must be a Hash") unless sp_cert_multi.is_a?(Hash)
354
+
355
+ certs = { :signing => [], :encryption => [] }.freeze
356
+
357
+ [:signing, :encryption].each do |type|
358
+ certs_for_type = sp_cert_multi[type] || sp_cert_multi[type.to_s]
359
+ next if !certs_for_type || certs_for_type.empty?
360
+
361
+ unless certs_for_type.is_a?(Array) && certs_for_type.all? { |cert| cert.is_a?(Hash) }
362
+ raise ArgumentError.new("sp_cert_multi :#{type} node must be an Array of Hashes")
363
+ end
364
+
365
+ certs_for_type.each do |pair|
366
+ cert = pair[:certificate] || pair['certificate'] || pair[:cert] || pair['cert']
367
+ key = pair[:private_key] || pair['private_key'] || pair[:key] || pair['key']
368
+
369
+ unless cert && key
370
+ raise ArgumentError.new("sp_cert_multi :#{type} node Hashes must specify keys :certificate and :private_key")
371
+ end
372
+
373
+ certs[type] << [
374
+ OneLogin::RubySaml::Utils.build_cert_object(cert),
375
+ OneLogin::RubySaml::Utils.build_private_key_object(key)
376
+ ].freeze
377
+ end
378
+ end
379
+
380
+ certs.each { |_, ary| ary.freeze }
381
+ certs
382
+ end
283
383
  end
284
384
  end
285
385
  end
386
+
@@ -91,16 +91,16 @@ module OneLogin
91
91
  end
92
92
 
93
93
  # Decrypts an EncryptedID element
94
- # @param encryptedid_node [REXML::Element] The EncryptedID element
94
+ # @param encrypted_id_node [REXML::Element] The EncryptedID element
95
95
  # @return [REXML::Document] The decrypted EncrypedtID element
96
96
  #
97
- def decrypt_nameid(encrypt_node)
97
+ def decrypt_nameid(encrypted_id_node)
98
98
 
99
- if settings.nil? || !settings.get_sp_key
100
- raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it')
99
+ if settings.nil? || settings.get_sp_decryption_keys.empty?
100
+ raise ValidationError.new('An ' + encrypted_id_node.name + ' found and no SP private key found on the settings to decrypt it')
101
101
  end
102
102
 
103
- elem_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypt_node, settings.get_sp_key)
103
+ elem_plaintext = OneLogin::RubySaml::Utils.decrypt_multi(encrypted_id_node, settings.get_sp_decryption_keys)
104
104
  # If we get some problematic noise in the plaintext after decrypting.
105
105
  # This quick regexp parse will grab only the Element and discard the noise.
106
106
  elem_plaintext = elem_plaintext.match(/(.*<\/(\w+:)?NameID>)/m)[0]
@@ -238,7 +238,8 @@ module OneLogin
238
238
  # @raise [ValidationError] if soft == false and validation fails
239
239
  #
240
240
  def validate_structure
241
- unless valid_saml?(document, soft)
241
+ check_malformed_doc = check_malformed_doc?(settings)
242
+ unless valid_saml?(document, soft, check_malformed_doc)
242
243
  return append_error("Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd")
243
244
  end
244
245
 
@@ -70,7 +70,7 @@ module OneLogin
70
70
  response_doc = create_logout_response_xml_doc(settings, request_id, logout_message, logout_status_code)
71
71
  response_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
72
72
 
73
- response = ""
73
+ response = "".dup
74
74
  response_doc.write(response)
75
75
 
76
76
  Logging.debug "Created SLO Logout Response: #{response}"
@@ -78,9 +78,10 @@ module OneLogin
78
78
  response = deflate(response) if settings.compress_response
79
79
  base64_response = encode(response)
80
80
  response_params = {"SAMLResponse" => base64_response}
81
+ sp_signing_key = settings.get_sp_signing_key
81
82
 
82
- if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_responses_signed] && settings.private_key
83
- params['SigAlg'] = settings.security[:signature_method]
83
+ if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_responses_signed] && sp_signing_key
84
+ params['SigAlg'] = settings.security[:signature_method]
84
85
  url_string = OneLogin::RubySaml::Utils.build_query(
85
86
  :type => 'SAMLResponse',
86
87
  :data => base64_response,
@@ -88,7 +89,7 @@ module OneLogin
88
89
  :sig_alg => params['SigAlg']
89
90
  )
90
91
  sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
91
- signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
92
+ signature = sp_signing_key.sign(sign_algorithm.new, url_string)
92
93
  params['Signature'] = encode(signature)
93
94
  end
94
95
 
@@ -150,9 +151,8 @@ module OneLogin
150
151
 
151
152
  def sign_document(document, settings)
152
153
  # embed signature
153
- if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.private_key && settings.certificate
154
- private_key = settings.get_sp_key
155
- cert = settings.get_sp_cert
154
+ cert, private_key = settings.get_sp_signing_pair
155
+ if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && private_key && cert
156
156
  document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
157
157
  end
158
158
 
@@ -32,18 +32,28 @@ module OneLogin
32
32
  (\d+)W # 8: Weeks
33
33
  )
34
34
  $)x.freeze
35
+
35
36
  UUID_PREFIX = '_'
37
+ @@prefix = '_'
36
38
 
37
- # Checks if the x509 cert provided is expired
38
- #
39
- # @param cert [Certificate] The x509 certificate
39
+ # Checks if the x509 cert provided is expired.
40
40
  #
41
+ # @param cert [OpenSSL::X509::Certificate|String] The x509 certificate.
42
+ # @return [true|false] Whether the certificate is expired.
41
43
  def self.is_cert_expired(cert)
42
- if cert.is_a?(String)
43
- cert = OpenSSL::X509::Certificate.new(cert)
44
- end
44
+ cert = OpenSSL::X509::Certificate.new(cert) if cert.is_a?(String)
45
+
46
+ cert.not_after < Time.now
47
+ end
45
48
 
46
- return cert.not_after < Time.now
49
+ # Checks if the x509 cert provided has both started and has not expired.
50
+ #
51
+ # @param cert [OpenSSL::X509::Certificate|String] The x509 certificate.
52
+ # @return [true|false] Whether the certificate is currently active.
53
+ def self.is_cert_active(cert)
54
+ cert = OpenSSL::X509::Certificate.new(cert) if cert.is_a?(String)
55
+ now = Time.now
56
+ cert.not_before <= now && cert.not_after >= now
47
57
  end
48
58
 
49
59
  # Interprets a ISO8601 duration value relative to a given timestamp.
@@ -61,20 +71,26 @@ module OneLogin
61
71
  matches = duration.match(DURATION_FORMAT)
62
72
 
63
73
  if matches.nil?
64
- raise Exception.new("Invalid ISO 8601 duration")
74
+ raise StandardError.new("Invalid ISO 8601 duration")
65
75
  end
66
76
 
67
77
  sign = matches[1] == '-' ? -1 : 1
68
78
 
69
79
  durYears, durMonths, durDays, durHours, durMinutes, durSeconds, durWeeks =
70
- matches[2..8].map { |match| match ? sign * match.tr(',', '.').to_f : 0.0 }
71
-
72
- initial_datetime = Time.at(timestamp).utc.to_datetime
73
- final_datetime = initial_datetime.next_year(durYears)
74
- final_datetime = final_datetime.next_month(durMonths)
75
- final_datetime = final_datetime.next_day((7*durWeeks) + durDays)
76
- final_timestamp = final_datetime.to_time.utc.to_i + (durHours * 3600) + (durMinutes * 60) + durSeconds
77
- return final_timestamp
80
+ matches[2..8].map do |match|
81
+ if match
82
+ match = match.tr(',', '.').gsub(/\.0*\z/, '')
83
+ sign * (match.include?('.') ? match.to_f : match.to_i)
84
+ else
85
+ 0
86
+ end
87
+ end
88
+
89
+ datetime = Time.at(timestamp).utc.to_datetime
90
+ datetime = datetime.next_year(durYears)
91
+ datetime = datetime.next_month(durMonths)
92
+ datetime = datetime.next_day((7*durWeeks) + durDays)
93
+ datetime.to_time.utc.to_i + (durHours * 3600) + (durMinutes * 60) + durSeconds
78
94
  end
79
95
 
80
96
  # Return a properly formatted x509 certificate
@@ -128,6 +144,28 @@ module OneLogin
128
144
  "-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----"
129
145
  end
130
146
 
147
+ # Given a certificate string, return an OpenSSL::X509::Certificate object.
148
+ #
149
+ # @param cert [String] The original certificate
150
+ # @return [OpenSSL::X509::Certificate] The certificate object
151
+ #
152
+ def self.build_cert_object(cert)
153
+ return nil if cert.nil? || cert.empty?
154
+
155
+ OpenSSL::X509::Certificate.new(format_cert(cert))
156
+ end
157
+
158
+ # Given a private key string, return an OpenSSL::PKey::RSA object.
159
+ #
160
+ # @param cert [String] The original private key
161
+ # @return [OpenSSL::PKey::RSA] The private key object
162
+ #
163
+ def self.build_private_key_object(private_key)
164
+ return nil if private_key.nil? || private_key.empty?
165
+
166
+ OpenSSL::PKey::RSA.new(format_private_key(private_key))
167
+ end
168
+
131
169
  # Build the Query String signature that will be used in the HTTP-Redirect binding
132
170
  # to generate the Signature
133
171
  # @param params [Hash] Parameters to build the Query String
@@ -199,7 +237,7 @@ module OneLogin
199
237
 
200
238
  # Validate the Signature parameter sent on the HTTP-Redirect binding
201
239
  # @param params [Hash] Parameters to be used in the validation process
202
- # @option params [OpenSSL::X509::Certificate] cert The Identity provider public certtificate
240
+ # @option params [OpenSSL::X509::Certificate] cert The IDP public certificate
203
241
  # @option params [String] sig_alg The SigAlg parameter
204
242
  # @option params [String] signature The Signature parameter (base64 encoded)
205
243
  # @option params [String] query_string The full GET Query String to be compared
@@ -216,6 +254,8 @@ module OneLogin
216
254
  # @param status_message [Strig] StatusMessage value
217
255
  # @return [String] The status error message
218
256
  def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil)
257
+ error_msg = error_msg.dup
258
+
219
259
  unless raw_status_code.nil?
220
260
  if raw_status_code.include? "|"
221
261
  status_codes = raw_status_code.split(' | ')
@@ -236,9 +276,29 @@ module OneLogin
236
276
  error_msg
237
277
  end
238
278
 
279
+ # Obtains the decrypted string from an Encrypted node element in XML,
280
+ # given multiple private keys to try.
281
+ # @param encrypted_node [REXML::Element] The Encrypted element
282
+ # @param private_keys [Array<OpenSSL::PKey::RSA>] The Service provider private key
283
+ # @return [String] The decrypted data
284
+ def self.decrypt_multi(encrypted_node, private_keys)
285
+ raise ArgumentError.new('private_keys must be specified') if !private_keys || private_keys.empty?
286
+
287
+ error = nil
288
+ private_keys.each do |key|
289
+ begin
290
+ return decrypt_data(encrypted_node, key)
291
+ rescue OpenSSL::PKey::PKeyError => e
292
+ error ||= e
293
+ end
294
+ end
295
+
296
+ raise(error) if error
297
+ end
298
+
239
299
  # Obtains the decrypted string from an Encrypted node element in XML
240
- # @param encrypted_node [REXML::Element] The Encrypted element
241
- # @param private_key [OpenSSL::PKey::RSA] The Service provider private key
300
+ # @param encrypted_node [REXML::Element] The Encrypted element
301
+ # @param private_key [OpenSSL::PKey::RSA] The Service provider private key
242
302
  # @return [String] The decrypted data
243
303
  def self.decrypt_data(encrypted_node, private_key)
244
304
  encrypt_data = REXML::XPath.first(
@@ -302,7 +362,7 @@ module OneLogin
302
362
 
303
363
  # Obtains the deciphered text
304
364
  # @param cipher_text [String] The ciphered text
305
- # @param symmetric_key [String] The symetric key used to encrypt the text
365
+ # @param symmetric_key [String] The symmetric key used to encrypt the text
306
366
  # @param algorithm [String] The encrypted algorithm
307
367
  # @return [String] The deciphered text
308
368
  def self.retrieve_plaintext(cipher_text, symmetric_key, algorithm)
@@ -344,11 +404,15 @@ module OneLogin
344
404
  end
345
405
 
346
406
  def self.set_prefix(value)
347
- UUID_PREFIX.replace value
407
+ @@prefix = value
408
+ end
409
+
410
+ def self.prefix
411
+ @@prefix
348
412
  end
349
413
 
350
414
  def self.uuid
351
- "#{UUID_PREFIX}" + (RUBY_VERSION < '1.9' ? "#{@@uuid_generator.generate}" : "#{SecureRandom.uuid}")
415
+ "#{prefix}" + (RUBY_VERSION < '1.9' ? "#{@@uuid_generator.generate}" : "#{SecureRandom.uuid}")
352
416
  end
353
417
 
354
418
  # Given two strings, attempt to match them as URIs using Rails' parse method. If they can be parsed,
@@ -1,5 +1,5 @@
1
1
  module OneLogin
2
2
  module RubySaml
3
- VERSION = '1.16.0'
3
+ VERSION = '1.18.0'
4
4
  end
5
5
  end