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