ruby-saml 1.7.2 → 1.12.2

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.
Files changed (154) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +37 -15
  3. data/README.md +127 -25
  4. data/changelog.md +61 -0
  5. data/lib/onelogin/ruby-saml/attribute_service.rb +1 -1
  6. data/lib/onelogin/ruby-saml/attributes.rb +24 -1
  7. data/lib/onelogin/ruby-saml/authrequest.rb +29 -6
  8. data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +239 -169
  9. data/lib/onelogin/ruby-saml/logging.rb +4 -1
  10. data/lib/onelogin/ruby-saml/logoutrequest.rb +27 -7
  11. data/lib/onelogin/ruby-saml/logoutresponse.rb +32 -16
  12. data/lib/onelogin/ruby-saml/metadata.rb +11 -3
  13. data/lib/onelogin/ruby-saml/response.rb +91 -30
  14. data/lib/onelogin/ruby-saml/saml_message.rb +15 -5
  15. data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
  16. data/lib/onelogin/ruby-saml/settings.rb +82 -9
  17. data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +26 -7
  18. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +46 -18
  19. data/lib/onelogin/ruby-saml/utils.rb +87 -10
  20. data/lib/onelogin/ruby-saml/version.rb +1 -1
  21. data/lib/xml_security.rb +39 -12
  22. data/ruby-saml.gemspec +16 -8
  23. metadata +40 -274
  24. data/test/certificates/certificate1 +0 -12
  25. data/test/certificates/certificate_without_head_foot +0 -1
  26. data/test/certificates/formatted_certificate +0 -14
  27. data/test/certificates/formatted_chained_certificate +0 -42
  28. data/test/certificates/formatted_private_key +0 -12
  29. data/test/certificates/formatted_rsa_private_key +0 -12
  30. data/test/certificates/invalid_certificate1 +0 -1
  31. data/test/certificates/invalid_certificate2 +0 -1
  32. data/test/certificates/invalid_certificate3 +0 -12
  33. data/test/certificates/invalid_chained_certificate1 +0 -1
  34. data/test/certificates/invalid_private_key1 +0 -1
  35. data/test/certificates/invalid_private_key2 +0 -1
  36. data/test/certificates/invalid_private_key3 +0 -10
  37. data/test/certificates/invalid_rsa_private_key1 +0 -1
  38. data/test/certificates/invalid_rsa_private_key2 +0 -1
  39. data/test/certificates/invalid_rsa_private_key3 +0 -10
  40. data/test/certificates/ruby-saml-2.crt +0 -15
  41. data/test/certificates/ruby-saml.crt +0 -14
  42. data/test/certificates/ruby-saml.key +0 -15
  43. data/test/idp_metadata_parser_test.rb +0 -568
  44. data/test/logging_test.rb +0 -62
  45. data/test/logout_requests/invalid_slo_request.xml +0 -6
  46. data/test/logout_requests/slo_request.xml +0 -4
  47. data/test/logout_requests/slo_request.xml.base64 +0 -1
  48. data/test/logout_requests/slo_request_deflated.xml.base64 +0 -1
  49. data/test/logout_requests/slo_request_with_name_id_format.xml +0 -4
  50. data/test/logout_requests/slo_request_with_session_index.xml +0 -5
  51. data/test/logout_responses/logoutresponse_fixtures.rb +0 -67
  52. data/test/logoutrequest_test.rb +0 -212
  53. data/test/logoutresponse_test.rb +0 -402
  54. data/test/metadata/idp_descriptor.xml +0 -26
  55. data/test/metadata/idp_descriptor_2.xml +0 -56
  56. data/test/metadata/idp_descriptor_3.xml +0 -14
  57. data/test/metadata/idp_metadata_different_sign_and_encrypt_cert.xml +0 -72
  58. data/test/metadata/idp_metadata_multi_certs.xml +0 -75
  59. data/test/metadata/idp_metadata_multi_signing_certs.xml +0 -52
  60. data/test/metadata/idp_metadata_same_sign_and_encrypt_cert.xml +0 -71
  61. data/test/metadata/idp_multiple_descriptors.xml +0 -53
  62. data/test/metadata/no_idp_descriptor.xml +0 -21
  63. data/test/metadata_test.rb +0 -331
  64. data/test/request_test.rb +0 -296
  65. data/test/response_test.rb +0 -1535
  66. data/test/responses/adfs_response_sha1.xml +0 -46
  67. data/test/responses/adfs_response_sha256.xml +0 -46
  68. data/test/responses/adfs_response_sha384.xml +0 -46
  69. data/test/responses/adfs_response_sha512.xml +0 -46
  70. data/test/responses/adfs_response_xmlns.xml +0 -45
  71. data/test/responses/attackxee.xml +0 -13
  72. data/test/responses/invalids/duplicated_attributes.xml.base64 +0 -1
  73. data/test/responses/invalids/empty_destination.xml.base64 +0 -1
  74. data/test/responses/invalids/empty_nameid.xml.base64 +0 -1
  75. data/test/responses/invalids/encrypted_new_attack.xml.base64 +0 -1
  76. data/test/responses/invalids/invalid_audience.xml.base64 +0 -1
  77. data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +0 -1
  78. data/test/responses/invalids/invalid_issuer_message.xml.base64 +0 -1
  79. data/test/responses/invalids/invalid_signature_position.xml.base64 +0 -1
  80. data/test/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 +0 -1
  81. data/test/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +0 -1
  82. data/test/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 +0 -1
  83. data/test/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 +0 -1
  84. data/test/responses/invalids/multiple_assertions.xml.base64 +0 -2
  85. data/test/responses/invalids/multiple_signed.xml.base64 +0 -1
  86. data/test/responses/invalids/no_authnstatement.xml.base64 +0 -1
  87. data/test/responses/invalids/no_conditions.xml.base64 +0 -1
  88. data/test/responses/invalids/no_id.xml.base64 +0 -1
  89. data/test/responses/invalids/no_issuer_assertion.xml.base64 +0 -1
  90. data/test/responses/invalids/no_issuer_response.xml.base64 +0 -1
  91. data/test/responses/invalids/no_nameid.xml.base64 +0 -1
  92. data/test/responses/invalids/no_saml2.xml.base64 +0 -1
  93. data/test/responses/invalids/no_signature.xml.base64 +0 -1
  94. data/test/responses/invalids/no_status.xml.base64 +0 -1
  95. data/test/responses/invalids/no_status_code.xml.base64 +0 -1
  96. data/test/responses/invalids/no_subjectconfirmation_data.xml.base64 +0 -1
  97. data/test/responses/invalids/no_subjectconfirmation_method.xml.base64 +0 -1
  98. data/test/responses/invalids/response_invalid_signed_element.xml.base64 +0 -1
  99. data/test/responses/invalids/response_with_concealed_signed_assertion.xml +0 -51
  100. data/test/responses/invalids/response_with_doubled_signed_assertion.xml +0 -49
  101. data/test/responses/invalids/signature_wrapping_attack.xml.base64 +0 -1
  102. data/test/responses/invalids/status_code_responder.xml.base64 +0 -1
  103. data/test/responses/invalids/status_code_responer_and_msg.xml.base64 +0 -1
  104. data/test/responses/invalids/wrong_spnamequalifier.xml.base64 +0 -1
  105. data/test/responses/no_signature_ns.xml +0 -48
  106. data/test/responses/open_saml_response.xml +0 -56
  107. data/test/responses/response_assertion_wrapped.xml.base64 +0 -93
  108. data/test/responses/response_audience_self_closed_tag.xml.base64 +0 -1
  109. data/test/responses/response_double_status_code.xml.base64 +0 -1
  110. data/test/responses/response_encrypted_attrs.xml.base64 +0 -1
  111. data/test/responses/response_encrypted_nameid.xml.base64 +0 -1
  112. data/test/responses/response_eval.xml +0 -7
  113. data/test/responses/response_no_cert_and_encrypted_attrs.xml +0 -29
  114. data/test/responses/response_node_text_attack.xml.base64 +0 -1
  115. data/test/responses/response_unsigned_xml_base64 +0 -1
  116. data/test/responses/response_with_ampersands.xml +0 -139
  117. data/test/responses/response_with_ampersands.xml.base64 +0 -93
  118. data/test/responses/response_with_ds_namespace_at_the_root.xml.base64 +0 -1
  119. data/test/responses/response_with_multiple_attribute_statements.xml +0 -72
  120. data/test/responses/response_with_multiple_attribute_values.xml +0 -67
  121. data/test/responses/response_with_retrieval_method.xml +0 -26
  122. data/test/responses/response_with_saml2_namespace.xml.base64 +0 -102
  123. data/test/responses/response_with_signed_assertion.xml.base64 +0 -66
  124. data/test/responses/response_with_signed_assertion_2.xml.base64 +0 -1
  125. data/test/responses/response_with_signed_assertion_3.xml +0 -30
  126. data/test/responses/response_with_signed_message_and_assertion.xml +0 -34
  127. data/test/responses/response_with_undefined_recipient.xml.base64 +0 -1
  128. data/test/responses/response_without_attributes.xml.base64 +0 -79
  129. data/test/responses/response_without_reference_uri.xml.base64 +0 -1
  130. data/test/responses/response_wrapped.xml.base64 +0 -150
  131. data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +0 -1
  132. data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +0 -1
  133. data/test/responses/signed_nameid_in_atts.xml +0 -47
  134. data/test/responses/signed_unqual_nameid_in_atts.xml +0 -47
  135. data/test/responses/simple_saml_php.xml +0 -71
  136. data/test/responses/starfield_response.xml.base64 +0 -1
  137. data/test/responses/test_sign.xml +0 -43
  138. data/test/responses/unsigned_encrypted_adfs.xml +0 -23
  139. data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +0 -1
  140. data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +0 -1
  141. data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +0 -1
  142. data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +0 -1
  143. data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +0 -1
  144. data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +0 -1
  145. data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +0 -1
  146. data/test/responses/valid_response.xml.base64 +0 -1
  147. data/test/responses/valid_response_without_x509certificate.xml.base64 +0 -1
  148. data/test/saml_message_test.rb +0 -56
  149. data/test/settings_test.rb +0 -301
  150. data/test/slo_logoutrequest_test.rb +0 -448
  151. data/test/slo_logoutresponse_test.rb +0 -185
  152. data/test/test_helper.rb +0 -323
  153. data/test/utils_test.rb +0 -254
  154. data/test/xml_security_test.rb +0 -421
@@ -24,9 +24,9 @@ module OneLogin
24
24
 
25
25
  # Constructs the Logout Request. A Logout Request Object that is an extension of the SamlMessage class.
26
26
  # @param request [String] A UUEncoded Logout Request from the IdP.
27
- # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
27
+ # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
28
28
  # Or :allowed_clock_drift for the logout request validation process to allow a clock drift when checking dates with
29
- # Or :relax_signature_validation to accept signatures if no idp certificate registered on settings
29
+ # Or :relax_signature_validation to accept signatures if no idp certificate registered on settings
30
30
  #
31
31
  # @raise [ArgumentError] If Request is nil
32
32
  #
@@ -47,6 +47,10 @@ module OneLogin
47
47
  @document = REXML::Document.new(@request)
48
48
  end
49
49
 
50
+ def request_id
51
+ id(document)
52
+ end
53
+
50
54
  # Validates the Logout Request with the default values (soft = true)
51
55
  # @param collect_errors [Boolean] Stop validation when first error appears or keep validating.
52
56
  # @return [Boolean] TRUE if the Logout Request is valid
@@ -192,7 +196,7 @@ module OneLogin
192
196
 
193
197
  # Validates the Logout Request against the specified schema.
194
198
  # @return [Boolean] True if the XML is valid, otherwise False if soft=True
195
- # @raise [ValidationError] if soft == false and validation fails
199
+ # @raise [ValidationError] if soft == false and validation fails
196
200
  #
197
201
  def validate_structure
198
202
  unless valid_saml?(document, soft)
@@ -202,7 +206,7 @@ module OneLogin
202
206
  true
203
207
  end
204
208
 
205
- # Validates that the Logout Request provided in the initialization is not empty,
209
+ # Validates that the Logout Request provided in the initialization is not empty,
206
210
  # @return [Boolean] True if the required info is found, otherwise False if soft=True
207
211
  # @raise [ValidationError] if soft == false and validation fails
208
212
  #
@@ -280,28 +284,43 @@ module OneLogin
280
284
  :raw_sig_alg => options[:raw_get_params]['SigAlg']
281
285
  )
282
286
 
287
+ expired = false
283
288
  if idp_certs.nil? || idp_certs[:signing].empty?
284
289
  valid = OneLogin::RubySaml::Utils.verify_signature(
285
- :cert => settings.get_idp_cert,
290
+ :cert => idp_cert,
286
291
  :sig_alg => options[:get_params]['SigAlg'],
287
292
  :signature => options[:get_params]['Signature'],
288
293
  :query_string => query_string
289
294
  )
295
+ if valid && settings.security[:check_idp_cert_expiration]
296
+ if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
297
+ expired = true
298
+ end
299
+ end
290
300
  else
291
301
  valid = false
292
- idp_certs[:signing].each do |idp_cert|
302
+ idp_certs[:signing].each do |signing_idp_cert|
293
303
  valid = OneLogin::RubySaml::Utils.verify_signature(
294
- :cert => idp_cert,
304
+ :cert => signing_idp_cert,
295
305
  :sig_alg => options[:get_params]['SigAlg'],
296
306
  :signature => options[:get_params]['Signature'],
297
307
  :query_string => query_string
298
308
  )
299
309
  if valid
310
+ if settings.security[:check_idp_cert_expiration]
311
+ if OneLogin::RubySaml::Utils.is_cert_expired(signing_idp_cert)
312
+ expired = true
313
+ end
314
+ end
300
315
  break
301
316
  end
302
317
  end
303
318
  end
304
319
 
320
+ if expired
321
+ error_msg = "IdP x509 certificate expired"
322
+ return append_error(error_msg)
323
+ end
305
324
  unless valid
306
325
  return append_error("Invalid Signature on Logout Request")
307
326
  end
@@ -2,6 +2,7 @@ require "onelogin/ruby-saml/logging"
2
2
 
3
3
  require "onelogin/ruby-saml/saml_message"
4
4
  require "onelogin/ruby-saml/utils"
5
+ require "onelogin/ruby-saml/setting_error"
5
6
 
6
7
  # Only supports SAML 2.0
7
8
  module OneLogin
@@ -21,23 +22,30 @@ module OneLogin
21
22
  @uuid = OneLogin::RubySaml::Utils.uuid
22
23
  end
23
24
 
25
+ def response_id
26
+ @uuid
27
+ end
28
+
24
29
  # Creates the Logout Response string.
25
30
  # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
26
31
  # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
27
32
  # @param logout_message [String] The Message to be placed as StatusMessage in the logout response
28
33
  # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
34
+ # @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response
29
35
  # @return [String] Logout Request string that includes the SAMLRequest
30
36
  #
31
- def create(settings, request_id = nil, logout_message = nil, params = {})
32
- params = create_params(settings, request_id, logout_message, params)
33
- params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
37
+ def create(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil)
38
+ params = create_params(settings, request_id, logout_message, params, logout_status_code)
39
+ params_prefix = (settings.idp_slo_service_url =~ /\?/) ? '&' : '?'
40
+ url = settings.idp_slo_response_service_url || settings.idp_slo_service_url
34
41
  saml_response = CGI.escape(params.delete("SAMLResponse"))
35
42
  response_params = "#{params_prefix}SAMLResponse=#{saml_response}"
36
43
  params.each_pair do |key, value|
37
44
  response_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
38
45
  end
39
46
 
40
- @logout_url = settings.idp_slo_target_url + response_params
47
+ raise SettingError.new "Invalid settings, idp_slo_service_url is not set!" if url.nil? or url.empty?
48
+ @logout_url = url + response_params
41
49
  end
42
50
 
43
51
  # Creates the Get parameters for the logout response.
@@ -45,15 +53,21 @@ module OneLogin
45
53
  # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
46
54
  # @param logout_message [String] The Message to be placed as StatusMessage in the logout response
47
55
  # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
56
+ # @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response
48
57
  # @return [Hash] Parameters
49
58
  #
50
- def create_params(settings, request_id = nil, logout_message = nil, params = {})
59
+ def create_params(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil)
51
60
  # The method expects :RelayState but sometimes we get 'RelayState' instead.
52
61
  # Based on the HashWithIndifferentAccess value in Rails we could experience
53
62
  # conflicts so this line will solve them.
54
63
  relay_state = params[:RelayState] || params['RelayState']
55
64
 
56
- response_doc = create_logout_response_xml_doc(settings, request_id, logout_message)
65
+ if relay_state.nil?
66
+ params.delete(:RelayState)
67
+ params.delete('RelayState')
68
+ end
69
+
70
+ response_doc = create_logout_response_xml_doc(settings, request_id, logout_message, logout_status_code)
57
71
  response_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
58
72
 
59
73
  response = ""
@@ -89,46 +103,60 @@ module OneLogin
89
103
  # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
90
104
  # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
91
105
  # @param logout_message [String] The Message to be placed as StatusMessage in the logout response
106
+ # @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response
92
107
  # @return [String] The SAMLResponse String.
93
108
  #
94
- def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil)
109
+ def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil, logout_status_code = nil)
110
+ document = create_xml_document(settings, request_id, logout_message, logout_status_code)
111
+ sign_document(document, settings)
112
+ end
113
+
114
+ def create_xml_document(settings, request_id = nil, logout_message = nil, status_code = nil)
95
115
  time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
96
116
 
97
117
  response_doc = XMLSecurity::Document.new
98
118
  response_doc.uuid = uuid
99
119
 
120
+ destination = settings.idp_slo_response_service_url || settings.idp_slo_service_url
121
+
122
+
100
123
  root = response_doc.add_element 'samlp:LogoutResponse', { 'xmlns:samlp' => 'urn:oasis:names:tc:SAML:2.0:protocol', "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
101
124
  root.attributes['ID'] = uuid
102
125
  root.attributes['IssueInstant'] = time
103
126
  root.attributes['Version'] = '2.0'
104
127
  root.attributes['InResponseTo'] = request_id unless request_id.nil?
105
- root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil?
128
+ root.attributes['Destination'] = destination unless destination.nil? or destination.empty?
106
129
 
107
- if settings.issuer != nil
130
+ if settings.sp_entity_id != nil
108
131
  issuer = root.add_element "saml:Issuer"
109
- issuer.text = settings.issuer
132
+ issuer.text = settings.sp_entity_id
110
133
  end
111
-
112
- # add success message
134
+
135
+ # add status
113
136
  status = root.add_element 'samlp:Status'
114
137
 
115
- # success status code
116
- status_code = status.add_element 'samlp:StatusCode'
117
- status_code.attributes['Value'] = 'urn:oasis:names:tc:SAML:2.0:status:Success'
138
+ # status code
139
+ status_code ||= 'urn:oasis:names:tc:SAML:2.0:status:Success'
140
+ status_code_elem = status.add_element 'samlp:StatusCode'
141
+ status_code_elem.attributes['Value'] = status_code
118
142
 
119
- # success status message
143
+ # status message
120
144
  logout_message ||= 'Successfully Signed Out'
121
145
  status_message = status.add_element 'samlp:StatusMessage'
122
146
  status_message.text = logout_message
123
147
 
148
+ response_doc
149
+ end
150
+
151
+ def sign_document(document, settings)
124
152
  # embed signature
125
153
  if settings.security[:logout_responses_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
126
154
  private_key = settings.get_sp_key
127
155
  cert = settings.get_sp_cert
128
- response_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
156
+ document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
129
157
  end
130
158
 
131
- response_doc
159
+ document
132
160
  end
133
161
 
134
162
  end
@@ -3,6 +3,7 @@ if RUBY_VERSION < '1.9'
3
3
  else
4
4
  require 'securerandom'
5
5
  end
6
+ require "openssl"
6
7
 
7
8
  module OneLogin
8
9
  module RubySaml
@@ -14,6 +15,61 @@ module OneLogin
14
15
 
15
16
  DSIG = "http://www.w3.org/2000/09/xmldsig#"
16
17
  XENC = "http://www.w3.org/2001/04/xmlenc#"
18
+ DURATION_FORMAT = %r(^(-?)P(?:(?:(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?)|(?:(\d+)W))$)
19
+
20
+ # Checks if the x509 cert provided is expired
21
+ #
22
+ # @param cert [Certificate] The x509 certificate
23
+ #
24
+ def self.is_cert_expired(cert)
25
+ if cert.is_a?(String)
26
+ cert = OpenSSL::X509::Certificate.new(cert)
27
+ end
28
+
29
+ return cert.not_after < Time.now
30
+ end
31
+
32
+ # Interprets a ISO8601 duration value relative to a given timestamp.
33
+ #
34
+ # @param duration [String] The duration, as a string.
35
+ # @param timestamp [Integer] The unix timestamp we should apply the
36
+ # duration to. Optional, default to the
37
+ # current time.
38
+ #
39
+ # @return [Integer] The new timestamp, after the duration is applied.
40
+ #
41
+ def self.parse_duration(duration, timestamp=Time.now.utc)
42
+ matches = duration.match(DURATION_FORMAT)
43
+
44
+ if matches.nil?
45
+ raise Exception.new("Invalid ISO 8601 duration")
46
+ end
47
+
48
+ durYears = matches[2].to_i
49
+ durMonths = matches[3].to_i
50
+ durDays = matches[4].to_i
51
+ durHours = matches[5].to_i
52
+ durMinutes = matches[6].to_i
53
+ durSeconds = matches[7].to_f
54
+ durWeeks = matches[8].to_i
55
+
56
+ if matches[1] == "-"
57
+ durYears = -durYears
58
+ durMonths = -durMonths
59
+ durDays = -durDays
60
+ durHours = -durHours
61
+ durMinutes = -durMinutes
62
+ durSeconds = -durSeconds
63
+ durWeeks = -durWeeks
64
+ end
65
+
66
+ initial_datetime = Time.at(timestamp).utc.to_datetime
67
+ final_datetime = initial_datetime.next_year(durYears)
68
+ final_datetime = final_datetime.next_month(durMonths)
69
+ final_datetime = final_datetime.next_day((7*durWeeks) + durDays)
70
+ final_timestamp = final_datetime.to_time.utc.to_i + (durHours * 3600) + (durMinutes * 60) + durSeconds
71
+ return final_timestamp
72
+ end
17
73
 
18
74
  # Return a properly formatted x509 certificate
19
75
  #
@@ -22,7 +78,11 @@ module OneLogin
22
78
  #
23
79
  def self.format_cert(cert)
24
80
  # don't try to format an encoded certificate or if is empty or nil
25
- return cert if cert.nil? || cert.empty? || cert.match(/\x0d/)
81
+ if cert.respond_to?(:ascii_only?)
82
+ return cert if cert.nil? || cert.empty? || !cert.ascii_only?
83
+ else
84
+ return cert if cert.nil? || cert.empty? || cert.match(/\x0d/)
85
+ end
26
86
 
27
87
  if cert.scan(/BEGIN CERTIFICATE/).length > 1
28
88
  formatted_cert = []
@@ -32,7 +92,9 @@ module OneLogin
32
92
  formatted_cert.join("\n")
33
93
  else
34
94
  cert = cert.gsub(/\-{5}\s?(BEGIN|END) CERTIFICATE\s?\-{5}/, "")
35
- cert = cert.gsub(/[\n\r\s]/, "")
95
+ cert = cert.gsub(/\r/, "")
96
+ cert = cert.gsub(/\n/, "")
97
+ cert = cert.gsub(/\s/, "")
36
98
  cert = cert.scan(/.{1,64}/)
37
99
  cert = cert.join("\n")
38
100
  "-----BEGIN CERTIFICATE-----\n#{cert}\n-----END CERTIFICATE-----"
@@ -51,7 +113,9 @@ module OneLogin
51
113
  # is this an rsa key?
52
114
  rsa_key = key.match("RSA PRIVATE KEY")
53
115
  key = key.gsub(/\-{5}\s?(BEGIN|END)( RSA)? PRIVATE KEY\s?\-{5}/, "")
54
- key = key.gsub(/[\n\r\s]/, "")
116
+ key = key.gsub(/\n/, "")
117
+ key = key.gsub(/\r/, "")
118
+ key = key.gsub(/\s/, "")
55
119
  key = key.scan(/.{1,64}/)
56
120
  key = key.join("\n")
57
121
  key_label = rsa_key ? "RSA PRIVATE KEY" : "PRIVATE KEY"
@@ -98,7 +162,7 @@ module OneLogin
98
162
  # @param rawparams [Hash] Raw GET Parameters
99
163
  # @param params [Hash] GET Parameters
100
164
  # @return [Hash] New raw parameters
101
- #
165
+ #
102
166
  def self.prepare_raw_get_params(rawparams, params)
103
167
  rawparams ||= {}
104
168
 
@@ -107,7 +171,7 @@ module OneLogin
107
171
  end
108
172
  if rawparams['SAMLResponse'].nil? && !params['SAMLResponse'].nil?
109
173
  rawparams['SAMLResponse'] = CGI.escape(params['SAMLResponse'])
110
- end
174
+ end
111
175
  if rawparams['RelayState'].nil? && !params['RelayState'].nil?
112
176
  rawparams['RelayState'] = CGI.escape(params['RelayState'])
113
177
  end
@@ -136,16 +200,16 @@ module OneLogin
136
200
  # @param status_code [String] StatusCode value
137
201
  # @param status_message [Strig] StatusMessage value
138
202
  # @return [String] The status error message
139
- def self.status_error_msg(error_msg, status_code = nil, status_message = nil)
140
- unless status_code.nil?
141
- if status_code.include? "|"
142
- status_codes = status_code.split(' | ')
203
+ def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil)
204
+ unless raw_status_code.nil?
205
+ if raw_status_code.include? "|"
206
+ status_codes = raw_status_code.split(' | ')
143
207
  values = status_codes.collect do |status_code|
144
208
  status_code.split(':').last
145
209
  end
146
210
  printable_code = values.join(" => ")
147
211
  else
148
- printable_code = status_code.split(':').last
212
+ printable_code = raw_status_code.split(':').last
149
213
  end
150
214
  error_msg << ', was ' + printable_code
151
215
  end
@@ -232,6 +296,9 @@ module OneLogin
232
296
  when 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' then cipher = OpenSSL::Cipher.new('AES-128-CBC').decrypt
233
297
  when 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' then cipher = OpenSSL::Cipher.new('AES-192-CBC').decrypt
234
298
  when 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' then cipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt
299
+ when 'http://www.w3.org/2009/xmlenc11#aes128-gcm' then auth_cipher = OpenSSL::Cipher::AES.new(128, :GCM).decrypt
300
+ when 'http://www.w3.org/2009/xmlenc11#aes192-gcm' then auth_cipher = OpenSSL::Cipher::AES.new(192, :GCM).decrypt
301
+ when 'http://www.w3.org/2009/xmlenc11#aes256-gcm' then auth_cipher = OpenSSL::Cipher::AES.new(256, :GCM).decrypt
235
302
  when 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' then rsa = symmetric_key
236
303
  when 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' then oaep = symmetric_key
237
304
  end
@@ -242,6 +309,16 @@ module OneLogin
242
309
  cipher.padding, cipher.key, cipher.iv = 0, symmetric_key, cipher_text[0..iv_len-1]
243
310
  assertion_plaintext = cipher.update(data)
244
311
  assertion_plaintext << cipher.final
312
+ elsif auth_cipher
313
+ iv_len, text_len, tag_len = auth_cipher.iv_len, cipher_text.length, 16
314
+ data = cipher_text[iv_len..text_len-1-tag_len]
315
+ auth_cipher.padding = 0
316
+ auth_cipher.key = symmetric_key
317
+ auth_cipher.iv = cipher_text[0..iv_len-1]
318
+ auth_cipher.auth_data = ''
319
+ auth_cipher.auth_tag = cipher_text[text_len-tag_len..-1]
320
+ assertion_plaintext = auth_cipher.update(data)
321
+ assertion_plaintext << auth_cipher.final
245
322
  elsif rsa
246
323
  rsa.private_decrypt(cipher_text)
247
324
  elsif oaep
@@ -1,5 +1,5 @@
1
1
  module OneLogin
2
2
  module RubySaml
3
- VERSION = '1.7.2'
3
+ VERSION = '1.12.2'
4
4
  end
5
5
  end
data/lib/xml_security.rb CHANGED
@@ -91,7 +91,7 @@ module XMLSecurity
91
91
  ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
92
92
  INC_PREFIX_LIST = "#default samlp saml ds xs xsi md"
93
93
 
94
- attr_accessor :uuid
94
+ attr_writer :uuid
95
95
 
96
96
  def uuid
97
97
  @uuid ||= begin
@@ -187,7 +187,7 @@ module XMLSecurity
187
187
  class SignedDocument < BaseDocument
188
188
  include OneLogin::RubySaml::ErrorHandling
189
189
 
190
- attr_accessor :signed_element_id
190
+ attr_writer :signed_element_id
191
191
 
192
192
  def initialize(response, errors = [])
193
193
  super(response)
@@ -211,8 +211,8 @@ module XMLSecurity
211
211
  cert_text = Base64.decode64(base64_cert)
212
212
  begin
213
213
  cert = OpenSSL::X509::Certificate.new(cert_text)
214
- rescue OpenSSL::X509::CertificateError => e
215
- return append_error("Certificate Error", soft)
214
+ rescue OpenSSL::X509::CertificateError => _e
215
+ return append_error("Document Certificate Error", soft)
216
216
  end
217
217
 
218
218
  if options[:fingerprint_alg]
@@ -224,7 +224,6 @@ module XMLSecurity
224
224
 
225
225
  # check cert matches registered idp cert
226
226
  if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
227
- @errors << "Fingerprint mismatch"
228
227
  return append_error("Fingerprint mismatch", soft)
229
228
  end
230
229
  else
@@ -241,7 +240,7 @@ module XMLSecurity
241
240
  validate_signature(base64_cert, soft)
242
241
  end
243
242
 
244
- def validate_document_with_cert(idp_cert)
243
+ def validate_document_with_cert(idp_cert, soft = true)
245
244
  # get cert from response
246
245
  cert_element = REXML::XPath.first(
247
246
  self,
@@ -254,13 +253,13 @@ module XMLSecurity
254
253
  cert_text = Base64.decode64(base64_cert)
255
254
  begin
256
255
  cert = OpenSSL::X509::Certificate.new(cert_text)
257
- rescue OpenSSL::X509::CertificateError => e
258
- return append_error("Certificate Error", soft)
256
+ rescue OpenSSL::X509::CertificateError => _e
257
+ return append_error("Document Certificate Error", soft)
259
258
  end
260
259
 
261
260
  # check saml response cert matches provided idp cert
262
261
  if idp_cert.to_pem != cert.to_pem
263
- return false
262
+ return append_error("Certificate of the Signature element does not match provided certificate", soft)
264
263
  end
265
264
  else
266
265
  base64_cert = Base64.encode64(idp_cert.to_pem)
@@ -318,15 +317,17 @@ module XMLSecurity
318
317
 
319
318
  # check digests
320
319
  ref = REXML::XPath.first(sig_element, "//ds:Reference", {"ds"=>DSIG})
321
- uri = ref.attributes.get_attribute("URI").value
322
320
 
323
321
  hashed_element = document.at_xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
324
-
322
+
325
323
  canon_algorithm = canon_algorithm REXML::XPath.first(
326
324
  ref,
327
325
  '//ds:CanonicalizationMethod',
328
326
  { "ds" => DSIG }
329
327
  )
328
+
329
+ canon_algorithm = process_transforms(ref, canon_algorithm)
330
+
330
331
  canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
331
332
 
332
333
  digest_algorithm = algorithm(REXML::XPath.first(
@@ -343,7 +344,6 @@ module XMLSecurity
343
344
  digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value))
344
345
 
345
346
  unless digests_match?(hash, digest_value)
346
- @errors << "Digest mismatch"
347
347
  return append_error("Digest mismatch", soft)
348
348
  end
349
349
 
@@ -361,6 +361,33 @@ module XMLSecurity
361
361
 
362
362
  private
363
363
 
364
+ def process_transforms(ref, canon_algorithm)
365
+ transforms = REXML::XPath.match(
366
+ ref,
367
+ "//ds:Transforms/ds:Transform",
368
+ { "ds" => DSIG }
369
+ )
370
+
371
+ transforms.each do |transform_element|
372
+ if transform_element.attributes && transform_element.attributes["Algorithm"]
373
+ algorithm = transform_element.attributes["Algorithm"]
374
+ case algorithm
375
+ when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
376
+ "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
377
+ canon_algorithm = Nokogiri::XML::XML_C14N_1_0
378
+ when "http://www.w3.org/2006/12/xml-c14n11",
379
+ "http://www.w3.org/2006/12/xml-c14n11#WithComments"
380
+ canon_algorithm = Nokogiri::XML::XML_C14N_1_1
381
+ when "http://www.w3.org/2001/10/xml-exc-c14n#",
382
+ "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"
383
+ canon_algorithm = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
384
+ end
385
+ end
386
+ end
387
+
388
+ canon_algorithm
389
+ end
390
+
364
391
  def digests_match?(hash, digest_value)
365
392
  hash == digest_value
366
393
  end
data/ruby-saml.gemspec CHANGED
@@ -15,36 +15,44 @@ Gem::Specification.new do |s|
15
15
  "LICENSE",
16
16
  "README.md"
17
17
  ]
18
- s.files = `git ls-files`.split("\n")
19
- s.homepage = %q{http://github.com/onelogin/ruby-saml}
20
- s.rubyforge_project = %q{http://www.rubygems.org/gems/ruby-saml}
18
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ s.homepage = %q{https://github.com/onelogin/ruby-saml}
21
20
  s.rdoc_options = ["--charset=UTF-8"]
22
21
  s.require_paths = ["lib"]
23
22
  s.rubygems_version = %q{1.3.7}
24
23
  s.required_ruby_version = '>= 1.8.7'
25
24
  s.summary = %q{SAML Ruby Tookit}
26
- s.test_files = `git ls-files test/*`.split("\n")
27
25
 
28
26
  # Because runtime dependencies are determined at build time, we cannot make
29
27
  # Nokogiri's version dependent on the Ruby version, even though we would
30
28
  # have liked to constrain Ruby 1.8.7 to install only the 1.5.x versions.
31
29
  if defined?(JRUBY_VERSION)
32
- s.add_runtime_dependency('nokogiri', '>= 1.6.0')
33
- s.add_runtime_dependency('jruby-openssl', '>= 0.9.8')
30
+ if JRUBY_VERSION < '9.2.0.0'
31
+ s.add_runtime_dependency('nokogiri', '>= 1.8.2', '<= 1.8.5')
32
+ s.add_runtime_dependency('jruby-openssl', '>= 0.9.8')
33
+ s.add_runtime_dependency('json', '< 2.3.0')
34
+ else
35
+ s.add_runtime_dependency('nokogiri', '>= 1.8.2')
36
+ end
34
37
  elsif RUBY_VERSION < '1.9'
35
38
  s.add_runtime_dependency('uuid')
36
39
  s.add_runtime_dependency('nokogiri', '<= 1.5.11')
37
40
  elsif RUBY_VERSION < '2.1'
38
41
  s.add_runtime_dependency('nokogiri', '>= 1.5.10', '<= 1.6.8.1')
42
+ s.add_runtime_dependency('json', '< 2.3.0')
43
+ elsif RUBY_VERSION < '2.3'
44
+ s.add_runtime_dependency('nokogiri', '>= 1.9.1', '<= 1.10.0')
39
45
  else
40
- s.add_runtime_dependency('nokogiri', '>= 1.5.10')
46
+ s.add_runtime_dependency('nokogiri', '>= 1.10.5')
47
+ s.add_runtime_dependency('rexml')
41
48
  end
42
49
 
50
+ s.add_development_dependency('coveralls')
43
51
  s.add_development_dependency('minitest', '~> 5.5')
44
52
  s.add_development_dependency('mocha', '~> 0.14')
45
53
  s.add_development_dependency('rake', '~> 10')
46
54
  s.add_development_dependency('shoulda', '~> 2.11')
47
- s.add_development_dependency('simplecov','~> 0.9.0')
55
+ s.add_development_dependency('simplecov')
48
56
  s.add_development_dependency('systemu', '~> 2')
49
57
  s.add_development_dependency('timecop', '<= 0.6.0')
50
58