ruby-saml 0.9.4 → 1.0.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.

Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/LICENSE +1 -1
  4. data/README.md +71 -15
  5. data/changelog.md +15 -6
  6. data/lib/onelogin/ruby-saml.rb +1 -0
  7. data/lib/onelogin/ruby-saml/attribute_service.rb +25 -2
  8. data/lib/onelogin/ruby-saml/attributes.rb +42 -23
  9. data/lib/onelogin/ruby-saml/authrequest.rb +33 -8
  10. data/lib/onelogin/ruby-saml/http_error.rb +7 -0
  11. data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +65 -10
  12. data/lib/onelogin/ruby-saml/logging.rb +14 -10
  13. data/lib/onelogin/ruby-saml/logoutrequest.rb +39 -14
  14. data/lib/onelogin/ruby-saml/logoutresponse.rb +166 -39
  15. data/lib/onelogin/ruby-saml/metadata.rb +40 -23
  16. data/lib/onelogin/ruby-saml/response.rb +562 -88
  17. data/lib/onelogin/ruby-saml/saml_message.rb +80 -14
  18. data/lib/onelogin/ruby-saml/settings.rb +62 -23
  19. data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +210 -20
  20. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +44 -13
  21. data/lib/onelogin/ruby-saml/utils.rb +163 -40
  22. data/lib/onelogin/ruby-saml/version.rb +1 -1
  23. data/lib/schemas/saml-schema-metadata-2.0.xsd +0 -2
  24. data/lib/xml_security.rb +87 -29
  25. data/ruby-saml.gemspec +1 -0
  26. data/test/certificates/{r1_certificate2_base64 → certificate_without_head_foot} +0 -0
  27. data/test/certificates/formatted_certificate +14 -0
  28. data/test/certificates/formatted_private_key +12 -0
  29. data/test/certificates/formatted_rsa_private_key +12 -0
  30. data/test/certificates/invalid_certificate1 +1 -0
  31. data/test/certificates/invalid_certificate2 +1 -0
  32. data/test/certificates/invalid_certificate3 +12 -0
  33. data/test/certificates/invalid_private_key1 +1 -0
  34. data/test/certificates/invalid_private_key2 +1 -0
  35. data/test/certificates/invalid_private_key3 +10 -0
  36. data/test/certificates/invalid_rsa_private_key1 +1 -0
  37. data/test/certificates/invalid_rsa_private_key2 +1 -0
  38. data/test/certificates/invalid_rsa_private_key3 +10 -0
  39. data/test/idp_metadata_parser_test.rb +41 -4
  40. data/test/logging_test.rb +62 -0
  41. data/test/logout_requests/invalid_slo_request.xml +6 -0
  42. data/test/{responses → logout_requests}/slo_request.xml +0 -0
  43. data/test/logout_requests/slo_request.xml.base64 +1 -0
  44. data/test/logout_requests/slo_request_deflated.xml.base64 +1 -0
  45. data/test/logout_requests/slo_request_with_session_index.xml +5 -0
  46. data/test/{responses → logout_responses}/logoutresponse_fixtures.rb +6 -6
  47. data/test/logoutrequest_test.rb +79 -52
  48. data/test/logoutresponse_test.rb +206 -59
  49. data/test/metadata_test.rb +77 -7
  50. data/test/request_test.rb +80 -65
  51. data/test/response_test.rb +862 -189
  52. data/test/responses/attackxee.xml +13 -0
  53. data/test/responses/invalids/invalid_audience.xml.base64 +1 -0
  54. data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +1 -0
  55. data/test/responses/invalids/invalid_issuer_message.xml.base64 +1 -0
  56. data/test/responses/invalids/invalid_signature_position.xml.base64 +1 -0
  57. data/test/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 +1 -0
  58. data/test/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +1 -0
  59. data/test/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 +1 -0
  60. data/test/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 +1 -0
  61. data/test/responses/invalids/multiple_assertions.xml.base64 +2 -0
  62. data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
  63. data/test/responses/invalids/no_id.xml.base64 +1 -0
  64. data/test/responses/invalids/no_saml2.xml.base64 +1 -0
  65. data/test/responses/invalids/no_signature.xml.base64 +1 -0
  66. data/test/responses/invalids/no_status.xml.base64 +1 -0
  67. data/test/responses/invalids/no_status_code.xml.base64 +1 -0
  68. data/test/responses/invalids/no_subjectconfirmation_data.xml.base64 +1 -0
  69. data/test/responses/invalids/no_subjectconfirmation_method.xml.base64 +1 -0
  70. data/test/responses/invalids/response_encrypted_attrs.xml.base64 +1 -0
  71. data/test/responses/invalids/response_invalid_signed_element.xml.base64 +1 -0
  72. data/test/responses/invalids/status_code_responder.xml.base64 +1 -0
  73. data/test/responses/invalids/status_code_responer_and_msg.xml.base64 +1 -0
  74. data/test/responses/{response4.xml.base64 → response_assertion_wrapped.xml.base64} +0 -0
  75. data/test/responses/response_encrypted_nameid.xml.base64 +1 -0
  76. data/test/responses/response_unsigned_xml_base64 +1 -0
  77. data/test/responses/{response5.xml.base64 → response_with_saml2_namespace.xml.base64} +0 -0
  78. data/test/responses/{response3.xml.base64 → response_with_signed_assertion.xml.base64} +0 -0
  79. data/test/responses/{r1_response6.xml.base64 → response_with_signed_assertion_2.xml.base64} +0 -0
  80. data/test/responses/{response1.xml.base64 → response_with_undefined_recipient.xml.base64} +0 -0
  81. data/test/responses/{response2.xml.base64 → response_without_attributes.xml.base64} +0 -0
  82. data/test/responses/{wrapped_response_2.xml.base64 → response_wrapped.xml.base64} +0 -0
  83. data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +1 -0
  84. data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +1 -0
  85. data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +1 -0
  86. data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +1 -0
  87. data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +1 -0
  88. data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +1 -0
  89. data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +1 -0
  90. data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +1 -0
  91. data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +1 -0
  92. data/test/responses/valid_response.xml.base64 +1 -0
  93. data/test/saml_message_test.rb +56 -0
  94. data/test/settings_test.rb +138 -1
  95. data/test/slo_logoutrequest_test.rb +239 -28
  96. data/test/slo_logoutresponse_test.rb +93 -71
  97. data/test/test_helper.rb +138 -31
  98. data/test/utils_test.rb +129 -25
  99. data/test/xml_security_test.rb +140 -71
  100. metadata +142 -25
  101. data/test/responses/response_node_text_attack.xml.base64 +0 -1
@@ -3,16 +3,31 @@ require "uuid"
3
3
  require "onelogin/ruby-saml/logging"
4
4
  require "onelogin/ruby-saml/saml_message"
5
5
 
6
+ # Only supports SAML 2.0
6
7
  module OneLogin
7
8
  module RubySaml
9
+
10
+ # SAML2 Logout Response (SLO SP initiated, Parser)
11
+ #
8
12
  class SloLogoutresponse < SamlMessage
9
13
 
10
- attr_reader :uuid # Can be obtained if neccessary
14
+ # Logout Response ID
15
+ attr_reader :uuid
11
16
 
17
+ # Initializes the Logout Response. A SloLogoutresponse Object that is an extension of the SamlMessage class.
18
+ # Asigns an ID, a random uuid.
19
+ #
12
20
  def initialize
13
21
  @uuid = "_" + UUID.new.generate
14
22
  end
15
23
 
24
+ # Creates the Logout Response string.
25
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
26
+ # @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
+ # @param logout_message [String] The Message to be placed as StatusMessage in the logout response
28
+ # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
29
+ # @return [String] Logout Request string that includes the SAMLRequest
30
+ #
16
31
  def create(settings, request_id = nil, logout_message = nil, params = {})
17
32
  params = create_params(settings, request_id, logout_message, params)
18
33
  params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
@@ -25,6 +40,13 @@ module OneLogin
25
40
  @logout_url = settings.idp_slo_target_url + response_params
26
41
  end
27
42
 
43
+ # Creates the Get parameters for the logout response.
44
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
45
+ # @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
+ # @param logout_message [String] The Message to be placed as StatusMessage in the logout response
47
+ # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
48
+ # @return [Hash] Parameters
49
+ #
28
50
  def create_params(settings, request_id = nil, logout_message = nil, params = {})
29
51
  # The method expects :RelayState but sometimes we get 'RelayState' instead.
30
52
  # Based on the HashWithIndifferentAccess value in Rails we could experience
@@ -45,11 +67,14 @@ module OneLogin
45
67
 
46
68
  if settings.security[:logout_responses_signed] && !settings.security[:embed_sign] && settings.private_key
47
69
  params['SigAlg'] = settings.security[:signature_method]
48
- url_string = "SAMLResponse=#{CGI.escape(base64_response)}"
49
- url_string << "&RelayState=#{CGI.escape(relay_state)}" if relay_state
50
- url_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}"
51
- private_key = settings.get_sp_key()
52
- signature = private_key.sign(XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]).new, url_string)
70
+ url_string = OneLogin::RubySaml::Utils.build_query(
71
+ :type => 'SAMLResponse',
72
+ :data => base64_response,
73
+ :relay_state => relay_state,
74
+ :sig_alg => params['SigAlg']
75
+ )
76
+ sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
77
+ signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
53
78
  params['Signature'] = encode(signature)
54
79
  end
55
80
 
@@ -60,6 +85,12 @@ module OneLogin
60
85
  response_params
61
86
  end
62
87
 
88
+ # Creates the SAMLResponse String.
89
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
90
+ # @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
+ # @param logout_message [String] The Message to be placed as StatusMessage in the logout response
92
+ # @return [String] The SAMLResponse String.
93
+ #
63
94
  def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil)
64
95
  time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
65
96
 
@@ -73,6 +104,11 @@ module OneLogin
73
104
  root.attributes['InResponseTo'] = request_id unless request_id.nil?
74
105
  root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil?
75
106
 
107
+ if settings.issuer != nil
108
+ issuer = root.add_element "saml:Issuer"
109
+ issuer.text = settings.issuer
110
+ end
111
+
76
112
  # add success message
77
113
  status = root.add_element 'samlp:Status'
78
114
 
@@ -85,15 +121,10 @@ module OneLogin
85
121
  status_message = status.add_element 'samlp:StatusMessage'
86
122
  status_message.text = logout_message
87
123
 
88
- if settings.issuer != nil
89
- issuer = root.add_element "saml:Issuer"
90
- issuer.text = settings.issuer
91
- end
92
-
93
124
  # embed signature
94
125
  if settings.security[:logout_responses_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
95
- private_key = settings.get_sp_key()
96
- cert = settings.get_sp_cert()
126
+ private_key = settings.get_sp_key
127
+ cert = settings.get_sp_cert
97
128
  response_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
98
129
  end
99
130
 
@@ -1,49 +1,172 @@
1
1
  module OneLogin
2
2
  module RubySaml
3
+
4
+ # SAML2 Auxiliary class
5
+ #
3
6
  class Utils
4
- def self.format_cert(cert, heads=true)
5
- cert = cert.delete("\n").delete("\r").delete("\x0D")
6
- if cert
7
- cert = cert.gsub('-----BEGIN CERTIFICATE-----', '')
8
- cert = cert.gsub('-----END CERTIFICATE-----', '')
9
- cert = cert.gsub(' ', '')
10
-
11
- if heads
12
- cert = cert.scan(/.{1,64}/).join("\n")+"\n"
13
- cert = "-----BEGIN CERTIFICATE-----\n" + cert + "-----END CERTIFICATE-----\n"
14
- end
7
+
8
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
9
+ XENC = "http://www.w3.org/2001/04/xmlenc#"
10
+
11
+ # Return a properly formatted x509 certificate
12
+ #
13
+ # @param cert [String] The original certificate
14
+ # @return [String] The formatted certificate
15
+ #
16
+ def self.format_cert(cert)
17
+ # don't try to format an encoded certificate or if is empty or nil
18
+ return cert if cert.nil? || cert.empty? || cert.match(/\x0d/)
19
+
20
+ cert = cert.gsub(/\-{5}\s?(BEGIN|END) CERTIFICATE\s?\-{5}/, "")
21
+ cert = cert.gsub(/[\n\r\s]/, "")
22
+ cert = cert.scan(/.{1,64}/)
23
+ cert = cert.join("\n")
24
+ "-----BEGIN CERTIFICATE-----\n#{cert}\n-----END CERTIFICATE-----"
25
+ end
26
+
27
+ # Return a properly formatted private key
28
+ #
29
+ # @param key [String] The original private key
30
+ # @return [String] The formatted private key
31
+ #
32
+ def self.format_private_key(key)
33
+ # don't try to format an encoded private key or if is empty
34
+ return key if key.nil? || key.empty? || key.match(/\x0d/)
35
+
36
+ # is this an rsa key?
37
+ rsa_key = key.match("RSA PRIVATE KEY")
38
+ key = key.gsub(/\-{5}\s?(BEGIN|END)( RSA)? PRIVATE KEY\s?\-{5}/, "")
39
+ key = key.gsub(/[\n\r\s]/, "")
40
+ key = key.scan(/.{1,64}/)
41
+ key = key.join("\n")
42
+ key_label = rsa_key ? "RSA PRIVATE KEY" : "PRIVATE KEY"
43
+ "-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----"
44
+ end
45
+
46
+ # Build the Query String signature that will be used in the HTTP-Redirect binding
47
+ # to generate the Signature
48
+ # @param params [Hash] Parameters to build the Query String
49
+ # @option params [String] :type 'SAMLRequest' or 'SAMLResponse'
50
+ # @option params [String] :data Base64 encoded SAMLRequest or SAMLResponse
51
+ # @option params [String] :relay_state The RelayState parameter
52
+ # @option params [String] :sig_alg The SigAlg parameter
53
+ # @return [String] The Query String
54
+ #
55
+ def self.build_query(params)
56
+ type, data, relay_state, sig_alg = [:type, :data, :relay_state, :sig_alg].map { |k| params[k]}
57
+
58
+ url_string = "#{type}=#{CGI.escape(data)}"
59
+ url_string << "&RelayState=#{CGI.escape(relay_state)}" if relay_state
60
+ url_string << "&SigAlg=#{CGI.escape(sig_alg)}"
61
+ end
62
+
63
+ # Validate the Signature parameter sent on the HTTP-Redirect binding
64
+ # @param params [Hash] Parameters to be used in the validation process
65
+ # @option params [OpenSSL::X509::Certificate] cert The Identity provider public certtificate
66
+ # @option params [String] sig_alg The SigAlg parameter
67
+ # @option params [String] signature The Signature parameter (base64 encoded)
68
+ # @option params [String] query_string The SigAlg parameter
69
+ # @return [Boolean] True if the Signature is valid, False otherwise
70
+ #
71
+ def self.verify_signature(params)
72
+ cert, sig_alg, signature, query_string = [:cert, :sig_alg, :signature, :query_string].map { |k| params[k]}
73
+ signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(sig_alg)
74
+ return cert.public_key.verify(signature_algorithm.new, Base64.decode64(signature), query_string)
75
+ end
76
+
77
+ # Build the status error message
78
+ # @param status_code [String] StatusCode value
79
+ # @param status_message [Strig] StatusMessage value
80
+ # @return [String] The status error message
81
+ def self.status_error_msg(error_msg, status_code = nil, status_message = nil)
82
+ unless status_code.nil?
83
+ printable_code = status_code.split(':').last
84
+ error_msg << ', was ' + printable_code
15
85
  end
16
- cert
17
- end
18
-
19
- def self.format_private_key(key, heads=true)
20
- key = key.delete("\n").delete("\r").delete("\x0D")
21
- if key
22
- if key.index('-----BEGIN PRIVATE KEY-----') != nil
23
- key = key.gsub('-----BEGIN PRIVATE KEY-----', '')
24
- key = key.gsub('-----END PRIVATE KEY-----', '')
25
- key = key.gsub(' ', '')
26
- if heads
27
- key = key.scan(/.{1,64}/).join("\n")+"\n"
28
- key = "-----BEGIN PRIVATE KEY-----\n" + key + "-----END PRIVATE KEY-----\n"
29
- end
30
- else
31
- key = key.gsub('-----BEGIN RSA PRIVATE KEY-----', '')
32
- key = key.gsub('-----END RSA PRIVATE KEY-----', '')
33
- key = key.gsub(' ', '')
34
- if heads
35
- key = key.scan(/.{1,64}/).join("\n")+"\n"
36
- key = "-----BEGIN RSA PRIVATE KEY-----\n" + key + "-----END RSA PRIVATE KEY-----\n"
37
- end
38
- end
86
+
87
+ unless status_message.nil?
88
+ error_msg << ' -> ' + status_message
39
89
  end
90
+
91
+ error_msg
92
+ end
93
+
94
+ # Obtains the decrypted string from an Encrypted node element in XML
95
+ # @param encrypted_node [REXML::Element] The Encrypted element
96
+ # @param private_key [OpenSSL::PKey::RSA] The Service provider private key
97
+ # @return [String] The decrypted data
98
+ def self.decrypt_data(encrypted_node, private_key)
99
+ encrypt_data = REXML::XPath.first(
100
+ encrypted_node,
101
+ "./xenc:EncryptedData",
102
+ { 'xenc' => XENC }
103
+ )
104
+ symmetric_key = retrieve_symmetric_key(encrypt_data, private_key)
105
+ cipher_value = REXML::XPath.first(
106
+ encrypt_data,
107
+ "//xenc:EncryptedData/xenc:CipherData/xenc:CipherValue",
108
+ { 'xenc' => XENC }
109
+ )
110
+ node = Base64.decode64(cipher_value.text)
111
+ encrypt_method = REXML::XPath.first(
112
+ encrypt_data,
113
+ "//xenc:EncryptedData/xenc:EncryptionMethod",
114
+ { 'xenc' => XENC }
115
+ )
116
+ algorithm = encrypt_method.attributes['Algorithm']
117
+ retrieve_plaintext(node, symmetric_key, algorithm)
118
+ end
119
+
120
+ # Obtains the symmetric key from the EncryptedData element
121
+ # @param encrypt_data [REXML::Element] The EncryptedData element
122
+ # @param private_key [OpenSSL::PKey::RSA] The Service provider private key
123
+ # @return [String] The symmetric key
124
+ def self.retrieve_symmetric_key(encrypt_data, private_key)
125
+ encrypted_symmetric_key_element = REXML::XPath.first(
126
+ encrypt_data,
127
+ "//xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey/xenc:CipherData/xenc:CipherValue",
128
+ { "ds" => DSIG, "xenc" => XENC }
129
+ )
130
+ cipher_text = Base64.decode64(encrypted_symmetric_key_element.text)
131
+ encrypt_method = REXML::XPath.first(
132
+ encrypt_data,
133
+ "//xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey/xenc:EncryptionMethod",
134
+ {"ds" => DSIG, "xenc" => XENC }
135
+ )
136
+ algorithm = encrypt_method.attributes['Algorithm']
137
+ retrieve_plaintext(cipher_text, private_key, algorithm)
40
138
  end
41
- # Given a REXML::Element instance, return the concatenation of all child text nodes. Assumes
42
- # that there all children other than text nodes can be ignored (e.g. comments). If nil is
43
- # passed, nil will be returned.
44
- def self.element_text(element)
45
- element.texts.map(&:value).join if element
139
+
140
+ # Obtains the deciphered text
141
+ # @param cipher_text [String] The ciphered text
142
+ # @param symmetric_key [String] The symetric key used to encrypt the text
143
+ # @param algorithm [String] The encrypted algorithm
144
+ # @return [String] The deciphered text
145
+ def self.retrieve_plaintext(cipher_text, symmetric_key, algorithm)
146
+ case algorithm
147
+ when 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' then cipher = OpenSSL::Cipher.new('DES-EDE3-CBC').decrypt
148
+ when 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' then cipher = OpenSSL::Cipher.new('AES-128-CBC').decrypt
149
+ when 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' then cipher = OpenSSL::Cipher.new('AES-192-CBC').decrypt
150
+ when 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' then cipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt
151
+ when 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' then rsa = symmetric_key
152
+ when 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' then oaep = symmetric_key
153
+ end
154
+
155
+ if cipher
156
+ iv_len = cipher.iv_len
157
+ data = cipher_text[iv_len..-1]
158
+ cipher.padding, cipher.key, cipher.iv = 0, symmetric_key, cipher_text[0..iv_len-1]
159
+ assertion_plaintext = cipher.update(data)
160
+ assertion_plaintext << cipher.final
161
+ elsif rsa
162
+ rsa.private_decrypt(cipher_text)
163
+ elsif oaep
164
+ oaep.private_decrypt(cipher_text, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
165
+ else
166
+ cipher_text
167
+ end
46
168
  end
169
+
47
170
  end
48
171
  end
49
- end
172
+ end
@@ -1,5 +1,5 @@
1
1
  module OneLogin
2
2
  module RubySaml
3
- VERSION = '0.9.4'
3
+ VERSION = '1.0.0'
4
4
  end
5
5
  end
@@ -247,8 +247,6 @@
247
247
  <sequence>
248
248
  <element ref="md:AssertionConsumerService" maxOccurs="unbounded"/>
249
249
  <element ref="md:AttributeConsumingService" minOccurs="0" maxOccurs="unbounded"/>
250
- <element ref="md:SingleLogoutService" minOccurs="0" maxOccurs="unbounded"/>
251
- <element ref="md:KeyDescriptor" minOccurs="0" maxOccurs="unbounded"/>
252
250
  </sequence>
253
251
  <attribute name="AuthnRequestsSigned" type="boolean" use="optional"/>
254
252
  <attribute name="WantAssertionsSigned" type="boolean" use="optional"/>
@@ -29,15 +29,17 @@ require "openssl"
29
29
  require 'nokogiri'
30
30
  require "digest/sha1"
31
31
  require "digest/sha2"
32
- require "onelogin/ruby-saml/utils"
33
32
  require "onelogin/ruby-saml/validation_error"
34
33
 
35
34
  module XMLSecurity
36
35
 
37
36
  class BaseDocument < REXML::Document
37
+ REXML::Document::entity_expansion_limit = 0
38
38
 
39
39
  C14N = "http://www.w3.org/2001/10/xml-exc-c14n#"
40
40
  DSIG = "http://www.w3.org/2000/09/xmldsig#"
41
+ NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT |
42
+ Nokogiri::XML::ParseOptions::NONET
41
43
 
42
44
  def canon_algorithm(element)
43
45
  algorithm = element
@@ -46,7 +48,6 @@ module XMLSecurity
46
48
  end
47
49
 
48
50
  case algorithm
49
- when "http://www.w3.org/2001/10/xml-exc-c14n#" then Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
50
51
  when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" then Nokogiri::XML::XML_C14N_1_0
51
52
  when "http://www.w3.org/2006/12/xml-c14n11" then Nokogiri::XML::XML_C14N_1_1
52
53
  else Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
@@ -108,7 +109,9 @@ module XMLSecurity
108
109
  #<Object />
109
110
  #</Signature>
110
111
  def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_method = SHA1)
111
- noko = Nokogiri.parse(self.to_s)
112
+ noko = Nokogiri.parse(self.to_s) do |options|
113
+ options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
114
+ end
112
115
 
113
116
  signature_element = REXML::Element.new("ds:Signature").add_namespace('ds', DSIG)
114
117
  signed_info_element = signature_element.add_element("ds:SignedInfo")
@@ -130,7 +133,10 @@ module XMLSecurity
130
133
  reference_element.add_element("ds:DigestValue").text = compute_digest(canon_doc, algorithm(digest_method_element))
131
134
 
132
135
  # add SignatureValue
133
- noko_sig_element = Nokogiri.parse(signature_element.to_s)
136
+ noko_sig_element = Nokogiri.parse(signature_element.to_s) do |options|
137
+ options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
138
+ end
139
+
134
140
  noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG)
135
141
  canon_string = noko_signed_info_element.canonicalize(canon_algorithm(C14N))
136
142
 
@@ -180,12 +186,19 @@ module XMLSecurity
180
186
  def initialize(response, errors = [])
181
187
  super(response)
182
188
  @errors = errors
183
- extract_signed_element_id
189
+ end
190
+
191
+ def signed_element_id
192
+ @signed_element_id ||= extract_signed_element_id
184
193
  end
185
194
 
186
195
  def validate_document(idp_cert_fingerprint, soft = true, options = {})
187
196
  # get cert from response
188
- cert_element = REXML::XPath.first(self, "//ds:X509Certificate", { "ds"=>DSIG })
197
+ cert_element = REXML::XPath.first(
198
+ self,
199
+ "//ds:X509Certificate",
200
+ { "ds"=>DSIG }
201
+ )
189
202
  unless cert_element
190
203
  if soft
191
204
  return false
@@ -193,7 +206,7 @@ module XMLSecurity
193
206
  raise OneLogin::RubySaml::ValidationError.new("Certificate element missing in response (ds:X509Certificate)")
194
207
  end
195
208
  end
196
- base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element)
209
+ base64_cert = cert_element.text
197
210
  cert_text = Base64.decode64(base64_cert)
198
211
  cert = OpenSSL::X509::Certificate.new(cert_text)
199
212
 
@@ -219,37 +232,63 @@ module XMLSecurity
219
232
  # check for inclusive namespaces
220
233
  inclusive_namespaces = extract_inclusive_namespaces
221
234
 
222
- document = Nokogiri.parse(self.to_s)
235
+ document = Nokogiri.parse(self.to_s) do |options|
236
+ options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
237
+ end
223
238
 
224
239
  # create a working copy so we don't modify the original
225
240
  @working_copy ||= REXML::Document.new(self.to_s).root
226
241
 
227
242
  # store and remove signature node
228
243
  @sig_element ||= begin
229
- element = REXML::XPath.first(@working_copy, "//ds:Signature", {"ds"=>DSIG})
244
+ element = REXML::XPath.first(
245
+ @working_copy,
246
+ "//ds:Signature",
247
+ {"ds"=>DSIG}
248
+ )
230
249
  element.remove
231
250
  end
232
251
 
233
252
  # verify signature
234
- signed_info_element = REXML::XPath.first(@sig_element, "//ds:SignedInfo", {"ds"=>DSIG})
253
+ signed_info_element = REXML::XPath.first(
254
+ @sig_element,
255
+ "//ds:SignedInfo",
256
+ {"ds"=>DSIG}
257
+ )
235
258
  noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
236
259
  noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
237
- canon_algorithm = canon_algorithm REXML::XPath.first(@sig_element, '//ds:CanonicalizationMethod', 'ds' => DSIG)
260
+ canon_algorithm = canon_algorithm REXML::XPath.first(
261
+ @sig_element,
262
+ '//ds:CanonicalizationMethod',
263
+ 'ds' => DSIG
264
+ )
238
265
  canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
239
266
  noko_sig_element.remove
240
267
 
241
268
  # check digests
242
269
  REXML::XPath.each(@sig_element, "//ds:Reference", {"ds"=>DSIG}) do |ref|
243
- uri = ref.attributes.get_attribute("URI").value
244
-
245
- hashed_element = document.at_xpath("//*[@ID='#{uri[1..-1]}']")
246
- canon_algorithm = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod', 'ds' => DSIG)
247
- canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
248
-
249
- digest_algorithm = algorithm(REXML::XPath.first(ref, "//ds:DigestMethod", 'ds' => DSIG))
250
-
251
- hash = digest_algorithm.digest(canon_hashed_element)
252
- digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>DSIG})))
270
+ uri = ref.attributes.get_attribute("URI").value
271
+
272
+ hashed_element = document.at_xpath("//*[@ID=$uri]", nil, { 'uri' => uri[1..-1] })
273
+ canon_algorithm = canon_algorithm REXML::XPath.first(
274
+ ref,
275
+ '//ds:CanonicalizationMethod',
276
+ { "ds" => DSIG }
277
+ )
278
+ canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
279
+
280
+ digest_algorithm = algorithm(REXML::XPath.first(
281
+ ref,
282
+ "//ds:DigestMethod",
283
+ { "ds" => DSIG }
284
+ ))
285
+ hash = digest_algorithm.digest(canon_hashed_element)
286
+ encoded_digest_value = REXML::XPath.first(
287
+ ref,
288
+ "//ds:DigestValue",
289
+ { "ds" => DSIG }
290
+ ).text
291
+ digest_value = Base64.decode64(encoded_digest_value)
253
292
 
254
293
  unless digests_match?(hash, digest_value)
255
294
  @errors << "Digest mismatch"
@@ -257,15 +296,25 @@ module XMLSecurity
257
296
  end
258
297
  end
259
298
 
260
- base64_signature = OneLogin::RubySaml::Utils.element_text(REXML::XPath.first(@sig_element, "//ds:SignatureValue", {"ds"=>DSIG}))
261
- signature = Base64.decode64(base64_signature)
299
+ base64_signature = REXML::XPath.first(
300
+ @sig_element,
301
+ "//ds:SignatureValue",
302
+ {"ds" => DSIG}
303
+ ).text
304
+
305
+ signature = Base64.decode64(base64_signature)
262
306
 
263
307
  # get certificate object
264
- cert_text = Base64.decode64(base64_cert)
265
- cert = OpenSSL::X509::Certificate.new(cert_text)
308
+ cert_text = Base64.decode64(base64_cert)
309
+ cert = OpenSSL::X509::Certificate.new(cert_text)
266
310
 
267
311
  # signature method
268
- signature_algorithm = algorithm(REXML::XPath.first(signed_info_element, "//ds:SignatureMethod", {"ds"=>DSIG}))
312
+ sig_alg_value = REXML::XPath.first(
313
+ signed_info_element,
314
+ "//ds:SignatureMethod",
315
+ {"ds"=>DSIG}
316
+ )
317
+ signature_algorithm = algorithm(sig_alg_value)
269
318
 
270
319
  unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
271
320
  @errors << "Key validation error"
@@ -282,12 +331,21 @@ module XMLSecurity
282
331
  end
283
332
 
284
333
  def extract_signed_element_id
285
- reference_element = REXML::XPath.first(self, "//ds:Signature/ds:SignedInfo/ds:Reference", {"ds"=>DSIG})
286
- self.signed_element_id = reference_element.attribute("URI").value[1..-1] unless reference_element.nil?
334
+ reference_element = REXML::XPath.first(
335
+ self,
336
+ "//ds:Signature/ds:SignedInfo/ds:Reference",
337
+ {"ds"=>DSIG}
338
+ )
339
+ self.signed_element_id = reference_element.attribute("URI").value[1..-1] unless reference_element.nil?
287
340
  end
288
341
 
289
342
  def extract_inclusive_namespaces
290
- if element = REXML::XPath.first(self, "//ec:InclusiveNamespaces", { "ec" => C14N })
343
+ element = REXML::XPath.first(
344
+ self,
345
+ "//ec:InclusiveNamespaces",
346
+ { "ec" => C14N }
347
+ )
348
+ if element
291
349
  prefix_list = element.attributes.get_attribute("PrefixList").value
292
350
  prefix_list.split(" ")
293
351
  else