ruby-saml 0.8.11 → 0.8.16
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.
- checksums.yaml +5 -5
- data/Gemfile +3 -1
- data/Rakefile +0 -14
- data/lib/onelogin/ruby-saml/logoutresponse.rb +9 -51
- data/lib/onelogin/ruby-saml/response.rb +121 -30
- data/lib/onelogin/ruby-saml/settings.rb +27 -10
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +101 -0
- data/lib/onelogin/ruby-saml/utils.rb +92 -0
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/ruby-saml.rb +1 -0
- data/lib/xml_security.rb +222 -87
- data/test/certificates/certificate.der +0 -0
- data/test/certificates/formatted_certificate +14 -0
- data/test/certificates/formatted_chained_certificate +42 -0
- data/test/certificates/formatted_private_key +12 -0
- data/test/certificates/formatted_rsa_private_key +12 -0
- data/test/certificates/invalid_certificate1 +1 -0
- data/test/certificates/invalid_certificate2 +1 -0
- data/test/certificates/invalid_certificate3 +12 -0
- data/test/certificates/invalid_chained_certificate1 +1 -0
- data/test/certificates/invalid_private_key1 +1 -0
- data/test/certificates/invalid_private_key2 +1 -0
- data/test/certificates/invalid_private_key3 +10 -0
- data/test/certificates/invalid_rsa_private_key1 +1 -0
- data/test/certificates/invalid_rsa_private_key2 +1 -0
- data/test/certificates/invalid_rsa_private_key3 +10 -0
- data/test/certificates/ruby-saml-2.crt +15 -0
- data/test/logoutrequest_test.rb +124 -126
- data/test/logoutresponse_test.rb +22 -42
- data/test/requests/logoutrequest_fixtures.rb +47 -0
- data/test/response_test.rb +373 -129
- data/test/responses/adfs_response_xmlns.xml +45 -0
- data/test/responses/encrypted_new_attack.xml.base64 +1 -0
- data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +1 -0
- data/test/responses/invalids/invalid_issuer_message.xml.base64 +1 -0
- data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
- data/test/responses/invalids/no_signature.xml.base64 +1 -0
- data/test/responses/invalids/response_with_concealed_signed_assertion.xml +51 -0
- data/test/responses/invalids/response_with_doubled_signed_assertion.xml +49 -0
- data/test/responses/invalids/signature_wrapping_attack.xml.base64 +1 -0
- data/test/responses/logoutresponse_fixtures.rb +4 -4
- data/test/responses/response_with_concealed_signed_assertion.xml +51 -0
- data/test/responses/response_with_doubled_signed_assertion.xml +49 -0
- data/test/responses/response_with_signed_assertion_3.xml +30 -0
- data/test/responses/response_with_signed_message_and_assertion.xml +34 -0
- data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
- data/test/responses/response_wrapped.xml.base64 +150 -0
- data/test/responses/valid_response.xml.base64 +1 -0
- data/test/responses/valid_response_without_x509certificate.xml.base64 +1 -0
- data/test/settings_test.rb +111 -5
- data/test/slo_logoutrequest_test.rb +66 -0
- data/test/test_helper.rb +110 -41
- data/test/utils_test.rb +201 -11
- data/test/xml_security_test.rb +359 -68
- metadata +77 -7
@@ -0,0 +1,101 @@
|
|
1
|
+
require "xml_security"
|
2
|
+
require "time"
|
3
|
+
|
4
|
+
# Only supports SAML 2.0
|
5
|
+
# SAML2 Logout Request (SLO IdP initiated, Parser)
|
6
|
+
module OneLogin
|
7
|
+
module RubySaml
|
8
|
+
class SloLogoutrequest
|
9
|
+
|
10
|
+
ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
|
11
|
+
PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
|
12
|
+
|
13
|
+
# OneLogin::RubySaml::Settings Toolkit settings
|
14
|
+
attr_accessor :settings
|
15
|
+
|
16
|
+
attr_reader :document
|
17
|
+
attr_reader :request
|
18
|
+
attr_reader :options
|
19
|
+
|
20
|
+
def initialize(request, settings = nil, options = {})
|
21
|
+
raise ArgumentError.new("Request cannot be nil") if request.nil?
|
22
|
+
self.settings = settings
|
23
|
+
|
24
|
+
@options = options
|
25
|
+
@request = OneLogin::RubySaml::Utils.decode_raw_saml(request)
|
26
|
+
@document = XMLSecurity::SignedDocument.new(@request)
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate!
|
30
|
+
validate(false)
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate(soft = true)
|
34
|
+
return false unless validate_structure(soft)
|
35
|
+
|
36
|
+
valid_issuer?(soft)
|
37
|
+
end
|
38
|
+
|
39
|
+
def name_id
|
40
|
+
@name_id ||= begin
|
41
|
+
node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
|
42
|
+
Utils.element_text(node)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
alias_method :nameid, :name_id
|
47
|
+
|
48
|
+
def name_id_format
|
49
|
+
@name_id_node ||= REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
|
50
|
+
@name_id_format ||=
|
51
|
+
if @name_id_node && @name_id_node.attribute("Format")
|
52
|
+
@name_id_node.attribute("Format").value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
alias_method :nameid_format, :name_id_format
|
57
|
+
|
58
|
+
def id
|
59
|
+
@id ||= begin
|
60
|
+
node = REXML::XPath.first(document, "/p:LogoutRequest", { "p" => PROTOCOL } )
|
61
|
+
node.nil? ? nil : node.attributes['ID']
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def issuer
|
66
|
+
@issuer ||= begin
|
67
|
+
node = REXML::XPath.first(document, "/p:LogoutRequest/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
|
68
|
+
Utils.element_text(node)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def validate_structure(soft = true)
|
75
|
+
Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
|
76
|
+
@schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
|
77
|
+
@xml = Nokogiri::XML(self.document.to_s)
|
78
|
+
end
|
79
|
+
if soft
|
80
|
+
@schema.validate(@xml).map{ return false }
|
81
|
+
else
|
82
|
+
@schema.validate(@xml).map{ |error| validation_error("#{error.message}\n\n#{@xml.to_s}") }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def valid_issuer?(soft = true)
|
87
|
+
return true if settings.nil? || settings.idp_entity_id.nil? || issuer.nil?
|
88
|
+
|
89
|
+
unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id)
|
90
|
+
return soft ? false : validation_error("Doesn't match the issuer, expected: <#{self.settings.idp_entity_id}>, but was: <#{issuer}>")
|
91
|
+
end
|
92
|
+
true
|
93
|
+
end
|
94
|
+
|
95
|
+
def validation_error(message)
|
96
|
+
raise ValidationError.new(message)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -4,6 +4,9 @@ else
|
|
4
4
|
require 'securerandom'
|
5
5
|
end
|
6
6
|
|
7
|
+
require "base64"
|
8
|
+
require "zlib"
|
9
|
+
|
7
10
|
module OneLogin
|
8
11
|
module RubySaml
|
9
12
|
|
@@ -12,6 +15,8 @@ module OneLogin
|
|
12
15
|
class Utils
|
13
16
|
@@uuid_generator = UUID.new if RUBY_VERSION < '1.9'
|
14
17
|
|
18
|
+
BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z)
|
19
|
+
|
15
20
|
# Given a REXML::Element instance, return the concatenation of all child text nodes. Assumes
|
16
21
|
# that there all children other than text nodes can be ignored (e.g. comments). If nil is
|
17
22
|
# passed, nil will be returned.
|
@@ -114,6 +119,93 @@ module OneLogin
|
|
114
119
|
|
115
120
|
error_msg
|
116
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
|
+
|
182
|
+
# Given two strings, attempt to match them as URIs using Rails' parse method. If they can be parsed,
|
183
|
+
# then the fully-qualified domain name and the host should performa a case-insensitive match, per the
|
184
|
+
# RFC for URIs. If Rails can not parse the string in to URL pieces, return a boolean match of the
|
185
|
+
# two strings. This maintains the previous functionality.
|
186
|
+
# @return [Boolean]
|
187
|
+
def self.uri_match?(destination_url, settings_url)
|
188
|
+
dest_uri = URI.parse(destination_url)
|
189
|
+
acs_uri = URI.parse(settings_url)
|
190
|
+
|
191
|
+
if dest_uri.scheme.nil? || acs_uri.scheme.nil? || dest_uri.host.nil? || acs_uri.host.nil?
|
192
|
+
raise URI::InvalidURIError
|
193
|
+
else
|
194
|
+
dest_uri.scheme.downcase == acs_uri.scheme.downcase &&
|
195
|
+
dest_uri.host.downcase == acs_uri.host.downcase &&
|
196
|
+
dest_uri.path == acs_uri.path &&
|
197
|
+
dest_uri.query == acs_uri.query
|
198
|
+
end
|
199
|
+
rescue URI::InvalidURIError
|
200
|
+
original_uri_match?(destination_url, settings_url)
|
201
|
+
end
|
202
|
+
|
203
|
+
# If Rails' URI.parse can't match to valid URL, default back to the original matching service.
|
204
|
+
# @return [Boolean]
|
205
|
+
def self.original_uri_match?(destination_url, settings_url)
|
206
|
+
destination_url == settings_url
|
207
|
+
end
|
208
|
+
|
117
209
|
end
|
118
210
|
end
|
119
211
|
end
|
data/lib/ruby-saml.rb
CHANGED
@@ -2,6 +2,7 @@ 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_logoutrequest'
|
5
6
|
require 'onelogin/ruby-saml/slo_logoutresponse'
|
6
7
|
require 'onelogin/ruby-saml/response'
|
7
8
|
require 'onelogin/ruby-saml/settings'
|
data/lib/xml_security.rb
CHANGED
@@ -42,40 +42,41 @@ module XMLSecurity
|
|
42
42
|
NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT |
|
43
43
|
Nokogiri::XML::ParseOptions::NONET
|
44
44
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
61
|
+
end
|
62
|
+
|
63
|
+
def algorithm(element)
|
64
|
+
algorithm = element
|
65
|
+
if algorithm.is_a?(REXML::Element)
|
66
|
+
algorithm = element.attribute("Algorithm").value
|
67
|
+
end
|
68
|
+
|
69
|
+
algorithm = algorithm && algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
|
70
|
+
|
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
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
79
80
|
end
|
80
81
|
|
81
82
|
class Document < BaseDocument
|
@@ -98,15 +99,30 @@ module XMLSecurity
|
|
98
99
|
end
|
99
100
|
end
|
100
101
|
|
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>
|
101
117
|
def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_method = SHA1)
|
102
118
|
noko = Nokogiri::XML(self.to_s) do |config|
|
103
|
-
config.options = NOKOGIRI_OPTIONS
|
119
|
+
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
|
104
120
|
end
|
105
121
|
|
106
122
|
signature_element = REXML::Element.new("ds:Signature").add_namespace('ds', DSIG)
|
107
123
|
signed_info_element = signature_element.add_element("ds:SignedInfo")
|
108
124
|
signed_info_element.add_element("ds:CanonicalizationMethod", {"Algorithm" => C14N})
|
109
|
-
signed_info_element.add_element("ds:SignatureMethod", {"Algorithm"=>signature_method})
|
125
|
+
signed_info_element.add_element("ds:SignatureMethod", {"Algorithm" => signature_method})
|
110
126
|
|
111
127
|
# Add Reference
|
112
128
|
reference_element = signed_info_element.add_element("ds:Reference", {"URI" => "##{uuid}"})
|
@@ -124,7 +140,7 @@ module XMLSecurity
|
|
124
140
|
|
125
141
|
# add SignatureValue
|
126
142
|
noko_sig_element = Nokogiri::XML(signature_element.to_s) do |config|
|
127
|
-
config.options = NOKOGIRI_OPTIONS
|
143
|
+
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
|
128
144
|
end
|
129
145
|
|
130
146
|
noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG)
|
@@ -172,88 +188,175 @@ module XMLSecurity
|
|
172
188
|
|
173
189
|
attr_writer :signed_element_id
|
174
190
|
|
175
|
-
def initialize(response)
|
176
|
-
super(response)
|
177
|
-
extract_signed_element_id
|
178
|
-
end
|
179
|
-
|
180
191
|
def signed_element_id
|
181
192
|
@signed_element_id ||= extract_signed_element_id
|
182
193
|
end
|
183
194
|
|
184
|
-
def validate_document(idp_cert_fingerprint, soft = true)
|
195
|
+
def validate_document(idp_cert_fingerprint, soft = true, options = {})
|
185
196
|
# get cert from response
|
186
|
-
cert_element = REXML::XPath.first(
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
197
|
+
cert_element = REXML::XPath.first(
|
198
|
+
self,
|
199
|
+
"//ds:X509Certificate",
|
200
|
+
{ "ds"=>DSIG }
|
201
|
+
)
|
191
202
|
|
192
|
-
|
193
|
-
|
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
|
194
211
|
|
195
|
-
|
196
|
-
|
197
|
-
|
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)
|
198
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
|
+
cert = options[:cert]
|
226
|
+
if cert.is_a? String
|
227
|
+
cert = OpenSSL::X509::Certificate.new(cert)
|
228
|
+
end
|
229
|
+
base64_cert = Base64.encode64(cert.to_pem)
|
230
|
+
else
|
231
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Certificate element missing in response (ds:X509Certificate) and not cert provided at settings"))
|
232
|
+
end
|
233
|
+
end
|
199
234
|
validate_signature(base64_cert, soft)
|
200
235
|
end
|
201
236
|
|
202
|
-
def
|
203
|
-
#
|
237
|
+
def validate_document_with_cert(idp_cert, soft = true)
|
238
|
+
# get cert from response
|
239
|
+
cert_element = REXML::XPath.first(
|
240
|
+
self,
|
241
|
+
"//ds:X509Certificate",
|
242
|
+
{ "ds"=>DSIG }
|
243
|
+
)
|
204
244
|
|
205
|
-
|
206
|
-
|
245
|
+
if cert_element
|
246
|
+
base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element)
|
247
|
+
cert_text = Base64.decode64(base64_cert)
|
248
|
+
begin
|
249
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
250
|
+
rescue OpenSSL::X509::CertificateError => _e
|
251
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Certificate Error"))
|
252
|
+
end
|
253
|
+
|
254
|
+
# check saml response cert matches provided idp cert
|
255
|
+
if idp_cert.to_pem != cert.to_pem
|
256
|
+
return false
|
257
|
+
end
|
258
|
+
elsif not idp_cert
|
259
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Certificate element missing in response (ds:X509Certificate) and not cert provided at settings"))
|
260
|
+
else
|
261
|
+
base64_cert = Base64.encode64(idp_cert.to_pem)
|
262
|
+
end
|
263
|
+
validate_signature(base64_cert, true)
|
264
|
+
end
|
265
|
+
|
266
|
+
def validate_signature(base64_cert, soft = true)
|
207
267
|
|
208
268
|
document = Nokogiri::XML(self.to_s) do |config|
|
209
|
-
config.options = NOKOGIRI_OPTIONS
|
269
|
+
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
|
210
270
|
end
|
211
271
|
|
212
|
-
# create a
|
272
|
+
# create a rexml document
|
213
273
|
@working_copy ||= REXML::Document.new(self.to_s).root
|
214
274
|
|
215
|
-
#
|
216
|
-
|
217
|
-
|
218
|
-
|
275
|
+
# get signature node
|
276
|
+
sig_element = REXML::XPath.first(
|
277
|
+
@working_copy,
|
278
|
+
"//ds:Signature",
|
279
|
+
{"ds"=>DSIG}
|
280
|
+
)
|
281
|
+
|
282
|
+
if sig_element.nil?
|
283
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("No Signature Node"))
|
219
284
|
end
|
220
285
|
|
221
|
-
#
|
222
|
-
|
286
|
+
# signature method
|
287
|
+
sig_alg_value = REXML::XPath.first(
|
288
|
+
sig_element,
|
289
|
+
"./ds:SignedInfo/ds:SignatureMethod",
|
290
|
+
{"ds"=>DSIG}
|
291
|
+
)
|
292
|
+
signature_algorithm = algorithm(sig_alg_value)
|
293
|
+
|
294
|
+
# get signature
|
295
|
+
base64_signature = REXML::XPath.first(
|
296
|
+
sig_element,
|
297
|
+
"./ds:SignatureValue",
|
298
|
+
{"ds" => DSIG}
|
299
|
+
)
|
300
|
+
|
301
|
+
if base64_signature.nil?
|
302
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("SignatureValue not found"))
|
303
|
+
end
|
304
|
+
|
305
|
+
signature = Base64.decode64(OneLogin::RubySaml::Utils.element_text(base64_signature))
|
306
|
+
|
307
|
+
# canonicalization method
|
308
|
+
canon_algorithm = canon_algorithm REXML::XPath.first(
|
309
|
+
sig_element,
|
310
|
+
'./ds:SignedInfo/ds:CanonicalizationMethod',
|
311
|
+
'ds' => DSIG
|
312
|
+
)
|
313
|
+
|
223
314
|
noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
|
224
315
|
noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
|
225
|
-
|
316
|
+
|
226
317
|
canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
|
227
318
|
noko_sig_element.remove
|
228
319
|
|
320
|
+
# get inclusive namespaces
|
321
|
+
inclusive_namespaces = extract_inclusive_namespaces
|
322
|
+
|
229
323
|
# check digests
|
230
|
-
REXML::XPath.
|
231
|
-
uri = ref.attributes.get_attribute("URI").value
|
324
|
+
ref = REXML::XPath.first(sig_element, "//ds:Reference", {"ds"=>DSIG})
|
232
325
|
|
233
|
-
|
234
|
-
canon_algorithm = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod', 'ds' => DSIG)
|
235
|
-
canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
|
326
|
+
hashed_element = document.at_xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
|
236
327
|
|
237
|
-
|
328
|
+
canon_algorithm = canon_algorithm REXML::XPath.first(
|
329
|
+
ref,
|
330
|
+
'//ds:CanonicalizationMethod',
|
331
|
+
{ "ds" => DSIG }
|
332
|
+
)
|
238
333
|
|
239
|
-
|
240
|
-
digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>DSIG})))
|
334
|
+
canon_algorithm = process_transforms(ref, canon_algorithm)
|
241
335
|
|
242
|
-
|
243
|
-
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Digest mismatch"))
|
244
|
-
end
|
245
|
-
end
|
336
|
+
canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
|
246
337
|
|
247
|
-
|
248
|
-
|
338
|
+
digest_algorithm = algorithm(REXML::XPath.first(
|
339
|
+
ref,
|
340
|
+
"//ds:DigestMethod",
|
341
|
+
{ "ds" => DSIG }
|
342
|
+
))
|
343
|
+
hash = digest_algorithm.digest(canon_hashed_element)
|
344
|
+
encoded_digest_value = REXML::XPath.first(
|
345
|
+
ref,
|
346
|
+
"//ds:DigestValue",
|
347
|
+
{ "ds" => DSIG }
|
348
|
+
)
|
349
|
+
digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value))
|
249
350
|
|
250
|
-
|
251
|
-
|
252
|
-
|
351
|
+
unless digests_match?(hash, digest_value)
|
352
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Digest mismatch"))
|
353
|
+
end
|
253
354
|
|
254
|
-
#
|
255
|
-
|
355
|
+
# get certificate object
|
356
|
+
cert_text = Base64.decode64(base64_cert)
|
357
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
256
358
|
|
359
|
+
# verify signature
|
257
360
|
unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
|
258
361
|
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Key validation error"))
|
259
362
|
end
|
@@ -263,6 +366,33 @@ module XMLSecurity
|
|
263
366
|
|
264
367
|
private
|
265
368
|
|
369
|
+
def process_transforms(ref, canon_algorithm)
|
370
|
+
transforms = REXML::XPath.match(
|
371
|
+
ref,
|
372
|
+
"//ds:Transforms/ds:Transform",
|
373
|
+
{ "ds" => DSIG }
|
374
|
+
)
|
375
|
+
|
376
|
+
transforms.each do |transform_element|
|
377
|
+
if transform_element.attributes && transform_element.attributes["Algorithm"]
|
378
|
+
algorithm = transform_element.attributes["Algorithm"]
|
379
|
+
case algorithm
|
380
|
+
when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
|
381
|
+
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
|
382
|
+
canon_algorithm = Nokogiri::XML::XML_C14N_1_0
|
383
|
+
when "http://www.w3.org/2006/12/xml-c14n11",
|
384
|
+
"http://www.w3.org/2006/12/xml-c14n11#WithComments"
|
385
|
+
canon_algorithm = Nokogiri::XML::XML_C14N_1_1
|
386
|
+
when "http://www.w3.org/2001/10/xml-exc-c14n#",
|
387
|
+
"http://www.w3.org/2001/10/xml-exc-c14n#WithComments"
|
388
|
+
canon_algorithm = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
canon_algorithm
|
394
|
+
end
|
395
|
+
|
266
396
|
def digests_match?(hash, digest_value)
|
267
397
|
hash == digest_value
|
268
398
|
end
|
@@ -281,11 +411,16 @@ module XMLSecurity
|
|
281
411
|
end
|
282
412
|
|
283
413
|
def extract_inclusive_namespaces
|
284
|
-
|
414
|
+
element = REXML::XPath.first(
|
415
|
+
self,
|
416
|
+
"//ec:InclusiveNamespaces",
|
417
|
+
{ "ec" => C14N }
|
418
|
+
)
|
419
|
+
if element
|
285
420
|
prefix_list = element.attributes.get_attribute("PrefixList").value
|
286
421
|
prefix_list.split(" ")
|
287
422
|
else
|
288
|
-
|
423
|
+
nil
|
289
424
|
end
|
290
425
|
end
|
291
426
|
|