ruby-saml 0.8.8 → 0.8.13

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 (45) hide show
  1. checksums.yaml +7 -7
  2. data/Gemfile +11 -1
  3. data/README.md +5 -2
  4. data/Rakefile +0 -14
  5. data/lib/onelogin/ruby-saml/authrequest.rb +86 -20
  6. data/lib/onelogin/ruby-saml/logoutrequest.rb +95 -20
  7. data/lib/onelogin/ruby-saml/logoutresponse.rb +5 -28
  8. data/lib/onelogin/ruby-saml/metadata.rb +5 -5
  9. data/lib/onelogin/ruby-saml/response.rb +187 -4
  10. data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
  11. data/lib/onelogin/ruby-saml/settings.rb +146 -10
  12. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +158 -0
  13. data/lib/onelogin/ruby-saml/utils.rb +169 -0
  14. data/lib/onelogin/ruby-saml/version.rb +1 -1
  15. data/lib/ruby-saml.rb +2 -1
  16. data/lib/xml_security.rb +330 -78
  17. data/test/certificates/ruby-saml-2.crt +15 -0
  18. data/test/certificates/ruby-saml.crt +14 -0
  19. data/test/certificates/ruby-saml.key +15 -0
  20. data/test/logoutrequest_test.rb +177 -44
  21. data/test/logoutresponse_test.rb +25 -29
  22. data/test/request_test.rb +100 -37
  23. data/test/response_test.rb +213 -111
  24. data/test/responses/adfs_response_xmlns.xml +45 -0
  25. data/test/responses/encrypted_new_attack.xml.base64 +1 -0
  26. data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
  27. data/test/responses/invalids/no_signature.xml.base64 +1 -0
  28. data/test/responses/invalids/response_with_concealed_signed_assertion.xml +51 -0
  29. data/test/responses/invalids/response_with_doubled_signed_assertion.xml +49 -0
  30. data/test/responses/invalids/signature_wrapping_attack.xml.base64 +1 -0
  31. data/test/responses/logoutresponse_fixtures.rb +6 -6
  32. data/test/responses/response_with_concealed_signed_assertion.xml +51 -0
  33. data/test/responses/response_with_doubled_signed_assertion.xml +49 -0
  34. data/test/responses/response_with_signed_assertion_3.xml +30 -0
  35. data/test/responses/response_with_signed_message_and_assertion.xml +34 -0
  36. data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
  37. data/test/responses/response_wrapped.xml.base64 +150 -0
  38. data/test/responses/valid_response.xml.base64 +1 -0
  39. data/test/responses/valid_response_without_x509certificate.xml.base64 +1 -0
  40. data/test/settings_test.rb +7 -7
  41. data/test/slo_logoutresponse_test.rb +226 -0
  42. data/test/test_helper.rb +117 -12
  43. data/test/utils_test.rb +10 -10
  44. data/test/xml_security_test.rb +310 -68
  45. metadata +88 -45
@@ -1,15 +1,184 @@
1
+ if RUBY_VERSION < '1.9'
2
+ require 'uuid'
3
+ else
4
+ require 'securerandom'
5
+ end
6
+
7
+ require "base64"
8
+ require "zlib"
9
+
1
10
  module OneLogin
2
11
  module RubySaml
3
12
 
4
13
  # SAML2 Auxiliary class
5
14
  #
6
15
  class Utils
16
+ @@uuid_generator = UUID.new if RUBY_VERSION < '1.9'
17
+
18
+ BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z)
19
+
7
20
  # Given a REXML::Element instance, return the concatenation of all child text nodes. Assumes
8
21
  # that there all children other than text nodes can be ignored (e.g. comments). If nil is
9
22
  # passed, nil will be returned.
10
23
  def self.element_text(element)
11
24
  element.texts.map(&:value).join if element
12
25
  end
26
+
27
+ # Return a properly formatted x509 certificate
28
+ #
29
+ # @param cert [String] The original certificate
30
+ # @return [String] The formatted certificate
31
+ #
32
+ def self.format_cert(cert)
33
+ # don't try to format an encoded certificate or if is empty or nil
34
+ if cert.respond_to?(:ascii_only?)
35
+ return cert if cert.nil? || cert.empty? || !cert.ascii_only?
36
+ else
37
+ return cert if cert.nil? || cert.empty? || cert.match(/\x0d/)
38
+ end
39
+
40
+ if cert.scan(/BEGIN CERTIFICATE/).length > 1
41
+ formatted_cert = []
42
+ cert.scan(/-{5}BEGIN CERTIFICATE-{5}[\n\r]?.*?-{5}END CERTIFICATE-{5}[\n\r]?/m) {|c|
43
+ formatted_cert << format_cert(c)
44
+ }
45
+ formatted_cert.join("\n")
46
+ else
47
+ cert = cert.gsub(/\-{5}\s?(BEGIN|END) CERTIFICATE\s?\-{5}/, "")
48
+ cert = cert.gsub(/\r/, "")
49
+ cert = cert.gsub(/\n/, "")
50
+ cert = cert.gsub(/\s/, "")
51
+ cert = cert.scan(/.{1,64}/)
52
+ cert = cert.join("\n")
53
+ "-----BEGIN CERTIFICATE-----\n#{cert}\n-----END CERTIFICATE-----"
54
+ end
55
+ end
56
+
57
+ # Return a properly formatted private key
58
+ #
59
+ # @param key [String] The original private key
60
+ # @return [String] The formatted private key
61
+ #
62
+ def self.format_private_key(key)
63
+ # don't try to format an encoded private key or if is empty
64
+ return key if key.nil? || key.empty? || key.match(/\x0d/)
65
+
66
+ # is this an rsa key?
67
+ rsa_key = key.match("RSA PRIVATE KEY")
68
+ key = key.gsub(/\-{5}\s?(BEGIN|END)( RSA)? PRIVATE KEY\s?\-{5}/, "")
69
+ key = key.gsub(/\n/, "")
70
+ key = key.gsub(/\r/, "")
71
+ key = key.gsub(/\s/, "")
72
+ key = key.scan(/.{1,64}/)
73
+ key = key.join("\n")
74
+ key_label = rsa_key ? "RSA PRIVATE KEY" : "PRIVATE KEY"
75
+ "-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----"
76
+ end
77
+
78
+ # Build the Query String signature that will be used in the HTTP-Redirect binding
79
+ # to generate the Signature
80
+ # @param params [Hash] Parameters to build the Query String
81
+ # @option params [String] :type 'SAMLRequest' or 'SAMLResponse'
82
+ # @option params [String] :data Base64 encoded SAMLRequest or SAMLResponse
83
+ # @option params [String] :relay_state The RelayState parameter
84
+ # @option params [String] :sig_alg The SigAlg parameter
85
+ # @return [String] The Query String
86
+ #
87
+ def self.build_query(params)
88
+ type, data, relay_state, sig_alg = [:type, :data, :relay_state, :sig_alg].map { |k| params[k]}
89
+ url_string = "#{type}=#{CGI.escape(data)}"
90
+ url_string << "&RelayState=#{CGI.escape(relay_state)}" if relay_state
91
+ url_string << "&SigAlg=#{CGI.escape(sig_alg)}"
92
+ end
93
+
94
+ def self.uuid
95
+ RUBY_VERSION < '1.9' ? "_#{@@uuid_generator.generate}" : "_#{SecureRandom.uuid}"
96
+ end
97
+
98
+ # Build the status error message
99
+ # @param status_code [String] StatusCode value
100
+ # @param status_message [Strig] StatusMessage value
101
+ # @return [String] The status error message
102
+ def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil)
103
+ unless raw_status_code.nil?
104
+ if raw_status_code.include? "|"
105
+ status_codes = raw_status_code.split(' | ')
106
+ values = status_codes.collect do |status_code|
107
+ status_code.split(':').last
108
+ end
109
+ printable_code = values.join(" => ")
110
+ else
111
+ printable_code = raw_status_code.split(':').last
112
+ end
113
+ error_msg << ', was ' + printable_code
114
+ end
115
+
116
+ unless status_message.nil?
117
+ error_msg << ' -> ' + status_message
118
+ end
119
+
120
+ error_msg
121
+ end
122
+
123
+ # Base64 decode and try also to inflate a SAML Message
124
+ # @param saml [String] The deflated and encoded SAML Message
125
+ # @return [String] The plain SAML Message
126
+ #
127
+ def self.decode_raw_saml(saml)
128
+ return saml unless base64_encoded?(saml)
129
+
130
+ decoded = decode(saml)
131
+ begin
132
+ inflate(decoded)
133
+ rescue
134
+ decoded
135
+ end
136
+ end
137
+
138
+ # Base 64 decode method
139
+ # @param string [String] The string message
140
+ # @return [String] The decoded string
141
+ #
142
+ def self.decode(string)
143
+ Base64.decode64(string)
144
+ end
145
+
146
+ # Base 64 encode method
147
+ # @param string [String] The string
148
+ # @return [String] The encoded string
149
+ #
150
+ def self.encode(string)
151
+ if Base64.respond_to?('strict_encode64')
152
+ Base64.strict_encode64(string)
153
+ else
154
+ Base64.encode64(string).gsub(/\n/, "")
155
+ end
156
+ end
157
+
158
+ # Check if a string is base64 encoded
159
+ # @param string [String] string to check the encoding of
160
+ # @return [true, false] whether or not the string is base64 encoded
161
+ #
162
+ def self.base64_encoded?(string)
163
+ !!string.gsub(/[\r\n]|\\r|\\n|\s/, "").match(BASE64_FORMAT)
164
+ end
165
+
166
+ # Inflate method
167
+ # @param deflated [String] The string
168
+ # @return [String] The inflated string
169
+ #
170
+ def self.inflate(deflated)
171
+ Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(deflated)
172
+ end
173
+
174
+ # Deflate method
175
+ # @param inflated [String] The string
176
+ # @return [String] The deflated string
177
+ #
178
+ def self.deflate(inflated)
179
+ Zlib::Deflate.deflate(inflated, 9)[2..-5]
180
+ end
181
+
13
182
  end
14
183
  end
15
184
  end
@@ -1,5 +1,5 @@
1
1
  module OneLogin
2
2
  module RubySaml
3
- VERSION = '0.8.8'
3
+ VERSION = '0.8.13'
4
4
  end
5
5
  end
@@ -2,10 +2,11 @@ require 'onelogin/ruby-saml/logging'
2
2
  require 'onelogin/ruby-saml/authrequest'
3
3
  require 'onelogin/ruby-saml/logoutrequest'
4
4
  require 'onelogin/ruby-saml/logoutresponse'
5
+ require 'onelogin/ruby-saml/slo_logoutresponse'
5
6
  require 'onelogin/ruby-saml/response'
6
7
  require 'onelogin/ruby-saml/settings'
7
8
  require 'onelogin/ruby-saml/utils'
8
9
  require 'onelogin/ruby-saml/validation_error'
9
10
  require 'onelogin/ruby-saml/metadata'
10
11
  require 'onelogin/ruby-saml/version'
11
- require 'onelogin/ruby-saml/attributes'
12
+ require 'onelogin/ruby-saml/attributes'
@@ -34,89 +34,323 @@ require "onelogin/ruby-saml/utils"
34
34
 
35
35
  module XMLSecurity
36
36
 
37
- class SignedDocument < REXML::Document
38
- C14N = "http://www.w3.org/2001/10/xml-exc-c14n#"
39
- DSIG = "http://www.w3.org/2000/09/xmldsig#"
37
+ class BaseDocument < REXML::Document
38
+ REXML::Document::entity_expansion_limit = 0
40
39
 
41
- attr_accessor :signed_element_id
40
+ C14N = "http://www.w3.org/2001/10/xml-exc-c14n#"
41
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
42
+ NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT |
43
+ Nokogiri::XML::ParseOptions::NONET
42
44
 
43
- def initialize(response)
44
- super(response)
45
- extract_signed_element_id
45
+ def canon_algorithm(element)
46
+ algorithm = element
47
+ if algorithm.is_a?(REXML::Element)
48
+ algorithm = element.attribute('Algorithm').value
49
+ end
50
+
51
+ case algorithm
52
+ when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
53
+ "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
54
+ Nokogiri::XML::XML_C14N_1_0
55
+ when "http://www.w3.org/2006/12/xml-c14n11",
56
+ "http://www.w3.org/2006/12/xml-c14n11#WithComments"
57
+ Nokogiri::XML::XML_C14N_1_1
58
+ else
59
+ Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
60
+ end
46
61
  end
47
62
 
48
- def validate_document(idp_cert_fingerprint, soft = true)
49
- # get cert from response
50
- cert_element = REXML::XPath.first(self, "//ds:X509Certificate", { "ds"=>DSIG })
51
- raise OneLogin::RubySaml::ValidationError.new("Certificate element missing in response (ds:X509Certificate)") unless cert_element
52
- base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element)
53
- cert_text = Base64.decode64(base64_cert)
54
- cert = OpenSSL::X509::Certificate.new(cert_text)
63
+ def algorithm(element)
64
+ algorithm = element
65
+ if algorithm.is_a?(REXML::Element)
66
+ algorithm = element.attribute("Algorithm").value
67
+ end
55
68
 
56
- # check cert matches registered idp cert
57
- fingerprint = Digest::SHA1.hexdigest(cert.to_der)
69
+ algorithm = algorithm && algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
58
70
 
59
- if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
60
- return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Fingerprint mismatch"))
71
+ case algorithm
72
+ when 256 then OpenSSL::Digest::SHA256
73
+ when 384 then OpenSSL::Digest::SHA384
74
+ when 512 then OpenSSL::Digest::SHA512
75
+ else
76
+ OpenSSL::Digest::SHA1
61
77
  end
78
+ end
62
79
 
63
- validate_signature(base64_cert, soft)
80
+ end
81
+
82
+ class Document < BaseDocument
83
+ RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
84
+ RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
85
+ RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
86
+ RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
87
+ SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
88
+ SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'
89
+ SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"
90
+ SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'
91
+ ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
92
+ INC_PREFIX_LIST = "#default samlp saml ds xs xsi md"
93
+
94
+ attr_writer :uuid
95
+
96
+ def uuid
97
+ @uuid ||= begin
98
+ document.root.nil? ? nil : document.root.attributes['ID']
99
+ end
64
100
  end
65
101
 
66
- def validate_signature(base64_cert, soft = true)
67
- # validate references
102
+ #<Signature>
103
+ #<SignedInfo>
104
+ #<CanonicalizationMethod />
105
+ #<SignatureMethod />
106
+ #<Reference>
107
+ #<Transforms>
108
+ #<DigestMethod>
109
+ #<DigestValue>
110
+ #</Reference>
111
+ #<Reference /> etc.
112
+ #</SignedInfo>
113
+ #<SignatureValue />
114
+ #<KeyInfo />
115
+ #<Object />
116
+ #</Signature>
117
+ def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_method = SHA1)
118
+ noko = Nokogiri::XML(self.to_s) do |config|
119
+ config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
120
+ end
68
121
 
69
- # check for inclusive namespaces
70
- inclusive_namespaces = extract_inclusive_namespaces
122
+ signature_element = REXML::Element.new("ds:Signature").add_namespace('ds', DSIG)
123
+ signed_info_element = signature_element.add_element("ds:SignedInfo")
124
+ signed_info_element.add_element("ds:CanonicalizationMethod", {"Algorithm" => C14N})
125
+ signed_info_element.add_element("ds:SignatureMethod", {"Algorithm" => signature_method})
71
126
 
72
- document = Nokogiri.parse(self.to_s)
127
+ # Add Reference
128
+ reference_element = signed_info_element.add_element("ds:Reference", {"URI" => "##{uuid}"})
73
129
 
74
- # create a working copy so we don't modify the original
75
- @working_copy ||= REXML::Document.new(self.to_s).root
130
+ # Add Transforms
131
+ transforms_element = reference_element.add_element("ds:Transforms")
132
+ transforms_element.add_element("ds:Transform", {"Algorithm" => ENVELOPED_SIG})
133
+ c14element = transforms_element.add_element("ds:Transform", {"Algorithm" => C14N})
134
+ c14element.add_element("ec:InclusiveNamespaces", {"xmlns:ec" => C14N, "PrefixList" => INC_PREFIX_LIST})
76
135
 
77
- # store and remove signature node
78
- @sig_element ||= begin
79
- element = REXML::XPath.first(@working_copy, "//ds:Signature", {"ds"=>DSIG})
80
- element.remove
136
+ digest_method_element = reference_element.add_element("ds:DigestMethod", {"Algorithm" => digest_method})
137
+ inclusive_namespaces = INC_PREFIX_LIST.split(" ")
138
+ canon_doc = noko.canonicalize(canon_algorithm(C14N), inclusive_namespaces)
139
+ reference_element.add_element("ds:DigestValue").text = compute_digest(canon_doc, algorithm(digest_method_element))
140
+
141
+ # add SignatureValue
142
+ noko_sig_element = Nokogiri::XML(signature_element.to_s) do |config|
143
+ config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
81
144
  end
82
145
 
146
+ noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG)
147
+ canon_string = noko_signed_info_element.canonicalize(canon_algorithm(C14N))
83
148
 
84
- # verify signature
85
- signed_info_element = REXML::XPath.first(@sig_element, "//ds:SignedInfo", {"ds"=>DSIG})
86
- noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
87
- noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
88
- canon_algorithm = canon_algorithm REXML::XPath.first(@sig_element, '//ds:CanonicalizationMethod', 'ds' => DSIG)
89
- canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
90
- noko_sig_element.remove
149
+ signature = compute_signature(private_key, algorithm(signature_method).new, canon_string)
150
+ signature_element.add_element("ds:SignatureValue").text = signature
91
151
 
92
- # check digests
93
- REXML::XPath.each(@sig_element, "//ds:Reference", {"ds"=>DSIG}) do |ref|
94
- uri = ref.attributes.get_attribute("URI").value
152
+ # add KeyInfo
153
+ key_info_element = signature_element.add_element("ds:KeyInfo")
154
+ x509_element = key_info_element.add_element("ds:X509Data")
155
+ x509_cert_element = x509_element.add_element("ds:X509Certificate")
156
+ if certificate.is_a?(String)
157
+ certificate = OpenSSL::X509::Certificate.new(certificate)
158
+ end
159
+ x509_cert_element.text = Base64.encode64(certificate.to_der).gsub(/\n/, "")
160
+
161
+ # add the signature
162
+ issuer_element = self.elements["//saml:Issuer"]
163
+ if issuer_element
164
+ self.root.insert_after issuer_element, signature_element
165
+ else
166
+ if sp_sso_descriptor = self.elements["/md:EntityDescriptor"]
167
+ self.root.insert_before sp_sso_descriptor, signature_element
168
+ else
169
+ self.root.add_element(signature_element)
170
+ end
171
+ end
172
+ end
173
+
174
+ protected
175
+
176
+ def compute_signature(private_key, signature_algorithm, document)
177
+ Base64.encode64(private_key.sign(signature_algorithm, document)).gsub(/\n/, "")
178
+ end
95
179
 
96
- hashed_element = document.at_xpath("//*[@ID='#{uri[1..-1]}']")
97
- canon_algorithm = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod', 'ds' => DSIG)
98
- canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
180
+ def compute_digest(document, digest_algorithm)
181
+ digest = digest_algorithm.digest(document)
182
+ Base64.encode64(digest).strip!
183
+ end
184
+
185
+ end
186
+
187
+ class SignedDocument < BaseDocument
188
+
189
+ attr_writer :signed_element_id
190
+
191
+ def signed_element_id
192
+ @signed_element_id ||= extract_signed_element_id
193
+ end
99
194
 
100
- digest_algorithm = algorithm(REXML::XPath.first(ref, "//ds:DigestMethod", 'ds' => DSIG))
195
+ def validate_document(idp_cert_fingerprint, soft = true, options = {})
196
+ # get cert from response
197
+ cert_element = REXML::XPath.first(
198
+ self,
199
+ "//ds:X509Certificate",
200
+ { "ds"=>DSIG }
201
+ )
202
+
203
+ if cert_element
204
+ base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element)
205
+ cert_text = Base64.decode64(base64_cert)
206
+ begin
207
+ cert = OpenSSL::X509::Certificate.new(cert_text)
208
+ rescue OpenSSL::X509::CertificateError => _e
209
+ return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Certificate Error"))
210
+ end
211
+
212
+ if options[:fingerprint_alg]
213
+ fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(options[:fingerprint_alg]).new
214
+ else
215
+ fingerprint_alg = OpenSSL::Digest::SHA1.new
216
+ end
217
+ fingerprint = fingerprint_alg.hexdigest(cert.to_der)
218
+
219
+ # check cert matches registered idp cert
220
+ if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
221
+ return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Fingerprint mismatch"))
222
+ end
223
+ else
224
+ if options[:cert]
225
+ base64_cert = Base64.encode64(options[:cert].to_pem)
226
+ else
227
+ return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Certificate element missing in response (ds:X509Certificate) and not cert provided at settings"))
228
+ end
229
+ end
230
+ validate_signature(base64_cert, soft)
231
+ end
101
232
 
102
- hash = digest_algorithm.digest(canon_hashed_element)
103
- digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>DSIG})))
233
+ def validate_document_with_cert(idp_cert, soft = true)
234
+ # get cert from response
235
+ cert_element = REXML::XPath.first(
236
+ self,
237
+ "//ds:X509Certificate",
238
+ { "ds"=>DSIG }
239
+ )
240
+
241
+ if cert_element
242
+ base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element)
243
+ cert_text = Base64.decode64(base64_cert)
244
+ begin
245
+ cert = OpenSSL::X509::Certificate.new(cert_text)
246
+ rescue OpenSSL::X509::CertificateError => _e
247
+ return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Certificate Error"))
248
+ end
104
249
 
105
- unless digests_match?(hash, digest_value)
106
- return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Digest mismatch"))
250
+ # check saml response cert matches provided idp cert
251
+ if idp_cert.to_pem != cert.to_pem
252
+ return false
107
253
  end
254
+ else
255
+ base64_cert = Base64.encode64(idp_cert.to_pem)
108
256
  end
257
+ validate_signature(base64_cert, true)
258
+ end
259
+
260
+ def validate_signature(base64_cert, soft = true)
109
261
 
110
- base64_signature = OneLogin::RubySaml::Utils.element_text(REXML::XPath.first(@sig_element, "//ds:SignatureValue", {"ds"=>DSIG}))
111
- signature = Base64.decode64(base64_signature)
262
+ document = Nokogiri::XML(self.to_s) do |config|
263
+ config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
264
+ end
112
265
 
113
- # get certificate object
114
- cert_text = Base64.decode64(base64_cert)
115
- cert = OpenSSL::X509::Certificate.new(cert_text)
266
+ # create a rexml document
267
+ @working_copy ||= REXML::Document.new(self.to_s).root
268
+
269
+ # get signature node
270
+ sig_element = REXML::XPath.first(
271
+ @working_copy,
272
+ "//ds:Signature",
273
+ {"ds"=>DSIG}
274
+ )
275
+
276
+ if sig_element.nil?
277
+ return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("No Signature Node"))
278
+ end
116
279
 
117
280
  # signature method
118
- signature_algorithm = algorithm(REXML::XPath.first(signed_info_element, "//ds:SignatureMethod", {"ds"=>DSIG}))
281
+ sig_alg_value = REXML::XPath.first(
282
+ sig_element,
283
+ "./ds:SignedInfo/ds:SignatureMethod",
284
+ {"ds"=>DSIG}
285
+ )
286
+ signature_algorithm = algorithm(sig_alg_value)
287
+
288
+ # get signature
289
+ base64_signature = REXML::XPath.first(
290
+ sig_element,
291
+ "./ds:SignatureValue",
292
+ {"ds" => DSIG}
293
+ )
294
+
295
+ if base64_signature.nil?
296
+ return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("SignatureValue not found"))
297
+ end
298
+
299
+ signature = Base64.decode64(OneLogin::RubySaml::Utils.element_text(base64_signature))
300
+
301
+ # canonicalization method
302
+ canon_algorithm = canon_algorithm REXML::XPath.first(
303
+ sig_element,
304
+ './ds:SignedInfo/ds:CanonicalizationMethod',
305
+ 'ds' => DSIG
306
+ )
307
+
308
+ noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
309
+ noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
310
+
311
+ canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
312
+ noko_sig_element.remove
119
313
 
314
+ # get inclusive namespaces
315
+ inclusive_namespaces = extract_inclusive_namespaces
316
+
317
+ # check digests
318
+ ref = REXML::XPath.first(sig_element, "//ds:Reference", {"ds"=>DSIG})
319
+
320
+ hashed_element = document.at_xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
321
+
322
+ canon_algorithm = canon_algorithm REXML::XPath.first(
323
+ ref,
324
+ '//ds:CanonicalizationMethod',
325
+ { "ds" => DSIG }
326
+ )
327
+
328
+ canon_algorithm = process_transforms(ref, canon_algorithm)
329
+
330
+ canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
331
+
332
+ digest_algorithm = algorithm(REXML::XPath.first(
333
+ ref,
334
+ "//ds:DigestMethod",
335
+ { "ds" => DSIG }
336
+ ))
337
+ hash = digest_algorithm.digest(canon_hashed_element)
338
+ encoded_digest_value = REXML::XPath.first(
339
+ ref,
340
+ "//ds:DigestValue",
341
+ { "ds" => DSIG }
342
+ )
343
+ digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value))
344
+
345
+ unless digests_match?(hash, digest_value)
346
+ return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Digest mismatch"))
347
+ end
348
+
349
+ # get certificate object
350
+ cert_text = Base64.decode64(base64_cert)
351
+ cert = OpenSSL::X509::Certificate.new(cert_text)
352
+
353
+ # verify signature
120
354
  unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
121
355
  return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Key validation error"))
122
356
  end
@@ -126,43 +360,61 @@ module XMLSecurity
126
360
 
127
361
  private
128
362
 
363
+ def process_transforms(ref, canon_algorithm)
364
+ transforms = REXML::XPath.match(
365
+ ref,
366
+ "//ds:Transforms/ds:Transform",
367
+ { "ds" => DSIG }
368
+ )
369
+
370
+ transforms.each do |transform_element|
371
+ if transform_element.attributes && transform_element.attributes["Algorithm"]
372
+ algorithm = transform_element.attributes["Algorithm"]
373
+ case algorithm
374
+ when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
375
+ "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
376
+ canon_algorithm = Nokogiri::XML::XML_C14N_1_0
377
+ when "http://www.w3.org/2006/12/xml-c14n11",
378
+ "http://www.w3.org/2006/12/xml-c14n11#WithComments"
379
+ canon_algorithm = Nokogiri::XML::XML_C14N_1_1
380
+ when "http://www.w3.org/2001/10/xml-exc-c14n#",
381
+ "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"
382
+ canon_algorithm = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
383
+ end
384
+ end
385
+ end
386
+
387
+ canon_algorithm
388
+ end
389
+
129
390
  def digests_match?(hash, digest_value)
130
391
  hash == digest_value
131
392
  end
132
393
 
133
394
  def extract_signed_element_id
134
- reference_element = REXML::XPath.first(self, "//ds:Signature/ds:SignedInfo/ds:Reference", {"ds"=>DSIG})
135
- self.signed_element_id = reference_element.attribute("URI").value[1..-1] unless reference_element.nil?
136
- end
395
+ reference_element = REXML::XPath.first(
396
+ self,
397
+ "//ds:Signature/ds:SignedInfo/ds:Reference",
398
+ {"ds"=>DSIG}
399
+ )
137
400
 
138
- def canon_algorithm(element)
139
- algorithm = element.attribute('Algorithm').value if element
140
- case algorithm
141
- when "http://www.w3.org/2001/10/xml-exc-c14n#" then Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
142
- when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" then Nokogiri::XML::XML_C14N_1_0
143
- when "http://www.w3.org/2006/12/xml-c14n11" then Nokogiri::XML::XML_C14N_1_1
144
- else Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
145
- end
146
- end
401
+ return nil if reference_element.nil?
147
402
 
148
- def algorithm(element)
149
- algorithm = element.attribute("Algorithm").value if element
150
- algorithm = algorithm && algorithm =~ /sha(.*?)$/i && $1.to_i
151
- case algorithm
152
- when 256 then OpenSSL::Digest::SHA256
153
- when 384 then OpenSSL::Digest::SHA384
154
- when 512 then OpenSSL::Digest::SHA512
155
- else
156
- OpenSSL::Digest::SHA1
157
- end
403
+ sei = reference_element.attribute("URI").value[1..-1]
404
+ sei.nil? ? reference_element.parent.parent.parent.attribute("ID").value : sei
158
405
  end
159
406
 
160
407
  def extract_inclusive_namespaces
161
- if element = REXML::XPath.first(self, "//ec:InclusiveNamespaces", { "ec" => C14N })
408
+ element = REXML::XPath.first(
409
+ self,
410
+ "//ec:InclusiveNamespaces",
411
+ { "ec" => C14N }
412
+ )
413
+ if element
162
414
  prefix_list = element.attributes.get_attribute("PrefixList").value
163
415
  prefix_list.split(" ")
164
416
  else
165
- []
417
+ nil
166
418
  end
167
419
  end
168
420