-
-
Notifications
You must be signed in to change notification settings - Fork 591
Expand file tree
/
Copy pathutils.rb
More file actions
380 lines (339 loc) · 15.5 KB
/
utils.rb
File metadata and controls
380 lines (339 loc) · 15.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
if RUBY_VERSION < '1.9'
require 'uuid'
else
require 'securerandom'
end
require "openssl"
module OneLogin
module RubySaml
# SAML2 Auxiliary class
#
class Utils
@@uuid_generator = UUID.new if RUBY_VERSION < '1.9'
BINDINGS = { :post => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".freeze,
:redirect => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect".freeze }.freeze
DSIG = "http://www.w3.org/2000/09/xmldsig#".freeze
XENC = "http://www.w3.org/2001/04/xmlenc#".freeze
DURATION_FORMAT = %r(^
(-?)P # 1: Duration sign
(?:
(?:(\d+)Y)? # 2: Years
(?:(\d+)M)? # 3: Months
(?:(\d+)D)? # 4: Days
(?:T
(?:(\d+)H)? # 5: Hours
(?:(\d+)M)? # 6: Minutes
(?:(\d+(?:[.,]\d+)?)S)? # 7: Seconds
)?
|
(\d+)W # 8: Weeks
)
$)x.freeze
UUID_PREFIX = '_'
# Checks if the x509 cert provided is expired
#
# @param cert [Certificate] The x509 certificate
#
def self.is_cert_expired(cert)
if cert.is_a?(String)
cert = OpenSSL::X509::Certificate.new(cert)
end
return cert.not_after < Time.now
end
# Interprets a ISO8601 duration value relative to a given timestamp.
#
# @param duration [String] The duration, as a string.
# @param timestamp [Integer] The unix timestamp we should apply the
# duration to. Optional, default to the
# current time.
#
# @return [Integer] The new timestamp, after the duration is applied.
#
def self.parse_duration(duration, timestamp=Time.now.utc)
return nil if RUBY_VERSION < '1.9' # 1.8.7 not supported
matches = duration.match(DURATION_FORMAT)
if matches.nil?
raise Exception.new("Invalid ISO 8601 duration")
end
sign = matches[1] == '-' ? -1 : 1
durYears, durMonths, durDays, durHours, durMinutes, durSeconds, durWeeks =
matches[2..8].map { |match| match ? sign * match.tr(',', '.').to_f : 0.0 }
initial_datetime = Time.at(timestamp).utc.to_datetime
final_datetime = initial_datetime.next_year(durYears)
final_datetime = final_datetime.next_month(durMonths)
final_datetime = final_datetime.next_day((7*durWeeks) + durDays)
final_timestamp = final_datetime.to_time.utc.to_i + (durHours * 3600) + (durMinutes * 60) + durSeconds
return final_timestamp
end
# Return a properly formatted x509 certificate
#
# @param cert [String] The original certificate
# @return [String] The formatted certificate
#
def self.format_cert(cert)
# don't try to format an encoded certificate or if is empty or nil
if cert.respond_to?(:ascii_only?)
return cert if cert.nil? || cert.empty? || !cert.ascii_only?
else
return cert if cert.nil? || cert.empty? || cert.match(/\x0d/)
end
if cert.scan(/BEGIN CERTIFICATE/).length > 1
formatted_cert = []
cert.scan(/-{5}BEGIN CERTIFICATE-{5}[\n\r]?.*?-{5}END CERTIFICATE-{5}[\n\r]?/m) {|c|
formatted_cert << format_cert(c)
}
formatted_cert.join("\n")
else
cert = cert.gsub(/\-{5}\s?(BEGIN|END) CERTIFICATE\s?\-{5}/, "")
cert = cert.gsub(/\r/, "")
cert = cert.gsub(/\n/, "")
cert = cert.gsub(/\s/, "")
cert = cert.scan(/.{1,64}/)
cert = cert.join("\n")
"-----BEGIN CERTIFICATE-----\n#{cert}\n-----END CERTIFICATE-----"
end
end
# Return a properly formatted private key
#
# @param key [String] The original private key
# @return [String] The formatted private key
#
def self.format_private_key(key)
# don't try to format an encoded private key or if is empty
return key if key.nil? || key.empty? || key.match(/\x0d/)
# is this an rsa key?
rsa_key = key.match("RSA PRIVATE KEY")
key = key.gsub(/\-{5}\s?(BEGIN|END)( RSA)? PRIVATE KEY\s?\-{5}/, "")
key = key.gsub(/\n/, "")
key = key.gsub(/\r/, "")
key = key.gsub(/\s/, "")
key = key.scan(/.{1,64}/)
key = key.join("\n")
key_label = rsa_key ? "RSA PRIVATE KEY" : "PRIVATE KEY"
"-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----"
end
# Build the Query String signature that will be used in the HTTP-Redirect binding
# to generate the Signature
# @param params [Hash] Parameters to build the Query String
# @option params [String] :type 'SAMLRequest' or 'SAMLResponse'
# @option params [String] :data Base64 encoded SAMLRequest or SAMLResponse
# @option params [String] :relay_state The RelayState parameter
# @option params [String] :sig_alg The SigAlg parameter
# @return [String] The Query String
#
def self.build_query(params)
type, data, relay_state, sig_alg = [:type, :data, :relay_state, :sig_alg].map { |k| params[k]}
url_string = "#{type}=#{CGI.escape(data)}"
url_string << "&RelayState=#{CGI.escape(relay_state)}" if relay_state
url_string << "&SigAlg=#{CGI.escape(sig_alg)}"
end
# Reconstruct a canonical query string from raw URI-encoded parts, to be used in verifying a signature
#
# @param params [Hash] Parameters to build the Query String
# @option params [String] :type 'SAMLRequest' or 'SAMLResponse'
# @option params [String] :raw_data URI-encoded, base64 encoded SAMLRequest or SAMLResponse, as sent by IDP
# @option params [String] :raw_relay_state URI-encoded RelayState parameter, as sent by IDP
# @option params [String] :raw_sig_alg URI-encoded SigAlg parameter, as sent by IDP
# @return [String] The Query String
#
def self.build_query_from_raw_parts(params)
type, raw_data, raw_relay_state, raw_sig_alg = [:type, :raw_data, :raw_relay_state, :raw_sig_alg].map { |k| params[k]}
url_string = "#{type}=#{raw_data}"
url_string << "&RelayState=#{raw_relay_state}" if raw_relay_state
url_string << "&SigAlg=#{raw_sig_alg}"
end
# Prepare raw GET parameters (build them from normal parameters
# if not provided).
#
# @param rawparams [Hash] Raw GET Parameters
# @param params [Hash] GET Parameters
# @return [Hash] New raw parameters
#
def self.prepare_raw_get_params(rawparams, params)
rawparams ||= {}
if rawparams['SAMLRequest'].nil? && !params['SAMLRequest'].nil?
rawparams['SAMLRequest'] = CGI.escape(params['SAMLRequest'])
end
if rawparams['SAMLResponse'].nil? && !params['SAMLResponse'].nil?
rawparams['SAMLResponse'] = CGI.escape(params['SAMLResponse'])
end
if rawparams['RelayState'].nil? && !params['RelayState'].nil?
rawparams['RelayState'] = CGI.escape(params['RelayState'])
end
if rawparams['SigAlg'].nil? && !params['SigAlg'].nil?
rawparams['SigAlg'] = CGI.escape(params['SigAlg'])
end
rawparams
end
# Validate the Signature parameter sent on the HTTP-Redirect binding
# @param params [Hash] Parameters to be used in the validation process
# @option params [OpenSSL::X509::Certificate] cert The Identity provider public certtificate
# @option params [String] sig_alg The SigAlg parameter
# @option params [String] signature The Signature parameter (base64 encoded)
# @option params [String] query_string The full GET Query String to be compared
# @return [Boolean] True if the Signature is valid, False otherwise
#
def self.verify_signature(params)
cert, sig_alg, signature, query_string = [:cert, :sig_alg, :signature, :query_string].map { |k| params[k]}
signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(sig_alg)
return cert.public_key.verify(signature_algorithm.new, Base64.decode64(signature), query_string)
end
# Build the status error message
# @param status_code [String] StatusCode value
# @param status_message [Strig] StatusMessage value
# @return [String] The status error message
def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil)
unless raw_status_code.nil?
if raw_status_code.include? "|"
status_codes = raw_status_code.split(' | ')
values = status_codes.collect do |status_code|
status_code.split(':').last
end
printable_code = values.join(" => ")
else
printable_code = raw_status_code.split(':').last
end
error_msg << ', was ' + printable_code
end
unless status_message.nil?
error_msg << ' -> ' + status_message
end
error_msg
end
# Obtains the decrypted string from an Encrypted node element in XML
# @param encrypted_node [REXML::Element] The Encrypted element
# @param private_key [OpenSSL::PKey::RSA] The Service provider private key
# @return [String] The decrypted data
def self.decrypt_data(encrypted_node, private_key)
encrypt_data = REXML::XPath.first(
encrypted_node,
"./xenc:EncryptedData",
{ 'xenc' => XENC }
)
symmetric_key = retrieve_symmetric_key(encrypt_data, private_key)
cipher_value = REXML::XPath.first(
encrypt_data,
"./xenc:CipherData/xenc:CipherValue",
{ 'xenc' => XENC }
)
node = Base64.decode64(element_text(cipher_value))
encrypt_method = REXML::XPath.first(
encrypt_data,
"./xenc:EncryptionMethod",
{ 'xenc' => XENC }
)
algorithm = encrypt_method.attributes['Algorithm']
retrieve_plaintext(node, symmetric_key, algorithm)
end
# Obtains the symmetric key from the EncryptedData element
# @param encrypt_data [REXML::Element] The EncryptedData element
# @param private_key [OpenSSL::PKey::RSA] The Service provider private key
# @return [String] The symmetric key
def self.retrieve_symmetric_key(encrypt_data, private_key)
encrypted_key = REXML::XPath.first(
encrypt_data,
"./ds:KeyInfo/xenc:EncryptedKey | ./KeyInfo/xenc:EncryptedKey | //xenc:EncryptedKey[@Id=$id]",
{ "ds" => DSIG, "xenc" => XENC },
{ "id" => self.retrieve_symetric_key_reference(encrypt_data) }
)
encrypted_symmetric_key_element = REXML::XPath.first(
encrypted_key,
"./xenc:CipherData/xenc:CipherValue",
"xenc" => XENC
)
cipher_text = Base64.decode64(element_text(encrypted_symmetric_key_element))
encrypt_method = REXML::XPath.first(
encrypted_key,
"./xenc:EncryptionMethod",
"xenc" => XENC
)
algorithm = encrypt_method.attributes['Algorithm']
retrieve_plaintext(cipher_text, private_key, algorithm)
end
def self.retrieve_symetric_key_reference(encrypt_data)
REXML::XPath.first(
encrypt_data,
"substring-after(./ds:KeyInfo/ds:RetrievalMethod/@URI, '#')",
{ "ds" => DSIG }
)
end
# Obtains the deciphered text
# @param cipher_text [String] The ciphered text
# @param symmetric_key [String] The symetric key used to encrypt the text
# @param algorithm [String] The encrypted algorithm
# @return [String] The deciphered text
def self.retrieve_plaintext(cipher_text, symmetric_key, algorithm)
case algorithm
when 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' then cipher = OpenSSL::Cipher.new('DES-EDE3-CBC').decrypt
when 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' then cipher = OpenSSL::Cipher.new('AES-128-CBC').decrypt
when 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' then cipher = OpenSSL::Cipher.new('AES-192-CBC').decrypt
when 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' then cipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt
when 'http://www.w3.org/2009/xmlenc11#aes128-gcm' then auth_cipher = OpenSSL::Cipher::AES.new(128, :GCM).decrypt
when 'http://www.w3.org/2009/xmlenc11#aes192-gcm' then auth_cipher = OpenSSL::Cipher::AES.new(192, :GCM).decrypt
when 'http://www.w3.org/2009/xmlenc11#aes256-gcm' then auth_cipher = OpenSSL::Cipher::AES.new(256, :GCM).decrypt
when 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' then rsa = symmetric_key
when 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' then oaep = symmetric_key
end
if cipher
iv_len = cipher.iv_len
data = cipher_text[iv_len..-1]
cipher.padding, cipher.key, cipher.iv = 0, symmetric_key, cipher_text[0..iv_len-1]
assertion_plaintext = cipher.update(data)
assertion_plaintext << cipher.final
elsif auth_cipher
iv_len, text_len, tag_len = auth_cipher.iv_len, cipher_text.length, 16
data = cipher_text[iv_len..text_len-1-tag_len]
auth_cipher.padding = 0
auth_cipher.key = symmetric_key
auth_cipher.iv = cipher_text[0..iv_len-1]
auth_cipher.auth_data = ''
auth_cipher.auth_tag = cipher_text[text_len-tag_len..-1]
assertion_plaintext = auth_cipher.update(data)
assertion_plaintext << auth_cipher.final
elsif rsa
rsa.private_decrypt(cipher_text)
elsif oaep
oaep.private_decrypt(cipher_text, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
else
cipher_text
end
end
def self.set_prefix(value)
UUID_PREFIX.replace value
end
def self.uuid
"#{UUID_PREFIX}" + (RUBY_VERSION < '1.9' ? "#{@@uuid_generator.generate}" : "#{SecureRandom.uuid}")
end
# Given two strings, attempt to match them as URIs using Rails' parse method. If they can be parsed,
# then the fully-qualified domain name and the host should performa a case-insensitive match, per the
# RFC for URIs. If Rails can not parse the string in to URL pieces, return a boolean match of the
# two strings. This maintains the previous functionality.
# @return [Boolean]
def self.uri_match?(destination_url, settings_url)
dest_uri = URI.parse(destination_url)
acs_uri = URI.parse(settings_url)
if dest_uri.scheme.nil? || acs_uri.scheme.nil? || dest_uri.host.nil? || acs_uri.host.nil?
raise URI::InvalidURIError
else
dest_uri.scheme.downcase == acs_uri.scheme.downcase &&
dest_uri.host.downcase == acs_uri.host.downcase &&
dest_uri.path == acs_uri.path &&
dest_uri.query == acs_uri.query
end
rescue URI::InvalidURIError
original_uri_match?(destination_url, settings_url)
end
# If Rails' URI.parse can't match to valid URL, default back to the original matching service.
# @return [Boolean]
def self.original_uri_match?(destination_url, settings_url)
destination_url == settings_url
end
# Given a REXML::Element instance, return the concatenation of all child text nodes. Assumes
# that there all children other than text nodes can be ignored (e.g. comments). If nil is
# passed, nil will be returned.
def self.element_text(element)
element.texts.map(&:value).join if element
end
end
end
end