ruby-saml 0.8.8 → 0.8.13

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.

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