|
1 |
require 'openssl'
|
|
2 |
require 'base64'
|
|
3 |
require 'fileutils'
|
|
4 |
|
|
5 |
# X509 authentication class. It can be used as a driver for auth_mad
|
|
6 |
# as auth method is defined. It also holds some helper methods to be used
|
|
7 |
# by oneauth command
|
|
8 |
class X509Auth
|
|
9 |
|
|
10 |
# Client side
|
|
11 |
|
|
12 |
# Creates the login file for x509 authentication at ~/.one/one_x509.
|
|
13 |
# By default it is valid for 1 hour but it can be changed to any number
|
|
14 |
# of seconds with expire parameter (in seconds)
|
|
15 |
def login(user, expire=3600)
|
|
16 |
# Read the proxy file
|
|
17 |
proxy_cert=get_x509_proxy_file
|
|
18 |
|
|
19 |
# Get the proxy certificate
|
|
20 |
proxy_cert_array=proxy_cert.split("\n")
|
|
21 |
begin_lines=proxy_cert_array.select{|l| l.match(/BEGIN CERTIFICATE/)}
|
|
22 |
begin_index=proxy_cert_array.index(begin_lines[0])
|
|
23 |
begin_line=proxy_cert_array[begin_index].to_s
|
|
24 |
|
|
25 |
end_lines=proxy_cert_array.select{|l| l.match(/END CERTIFICATE/)}
|
|
26 |
end_index=proxy_cert_array.index(end_lines[0])
|
|
27 |
end_line=proxy_cert_array[end_index].to_s
|
|
28 |
|
|
29 |
proxy_cert_line=proxy_cert_array[begin_index+1...end_index].join('')
|
|
30 |
proxy_cert_array=proxy_cert_array[end_index+1..-1]
|
|
31 |
|
|
32 |
|
|
33 |
# Get the proxy private key
|
|
34 |
begin_lines=proxy_cert_array.select{|l| l.match(/BEGIN RSA PRIVATE KEY/)}
|
|
35 |
begin_index=proxy_cert_array.index(begin_lines[0])
|
|
36 |
begin_line=proxy_cert_array[begin_index].to_s
|
|
37 |
|
|
38 |
end_lines=proxy_cert_array.select{|l| l.match(/END RSA PRIVATE KEY/)}
|
|
39 |
end_index=proxy_cert_array.index(end_lines[0])
|
|
40 |
end_line=proxy_cert_array[end_index].to_s
|
|
41 |
|
|
42 |
proxy_key_array=proxy_cert_array[begin_index..end_index]
|
|
43 |
|
|
44 |
|
|
45 |
# Get the user certificate
|
|
46 |
begin_lines=proxy_cert_array.select{|l| l.match(/BEGIN CERTIFICATE/)}
|
|
47 |
if begin_lines.length == 0 # No user cert -this is not a proxy
|
|
48 |
user_cert_line = proxy_cert_line
|
|
49 |
proxy_cert_line = ""
|
|
50 |
else
|
|
51 |
begin_index=proxy_cert_array.index(begin_lines[0])
|
|
52 |
begin_line=proxy_cert_array[begin_index].to_s
|
|
53 |
|
|
54 |
end_lines=proxy_cert_array.select{|l| l.match(/END CERTIFICATE/)}
|
|
55 |
end_index=proxy_cert_array.index(end_lines[0])
|
|
56 |
end_line=proxy_cert_array[end_index].to_s
|
|
57 |
|
|
58 |
user_cert_line=proxy_cert_array[begin_index+1...end_index].join('')
|
|
59 |
end
|
|
60 |
|
|
61 |
# Sign the message and compose the login token
|
|
62 |
time=Time.now.to_i+expire
|
|
63 |
text_to_sign="#{user}:#{time}"
|
|
64 |
proxy_key=proxy_key_array.join("\n")
|
|
65 |
signed_text=encrypt(text_to_sign, proxy_key)
|
|
66 |
sig_and_certs="#{signed_text}:#{proxy_cert_line}:#{user_cert_line}"
|
|
67 |
login_token=Base64::encode64(sig_and_certs).strip.delete!("\n")
|
|
68 |
|
|
69 |
|
|
70 |
# Write the login file
|
|
71 |
one_proxy="#{user}:plain:#{login_token}"
|
|
72 |
file=get_one_proxy_file
|
|
73 |
file.write(one_proxy)
|
|
74 |
file.close
|
|
75 |
|
|
76 |
|
|
77 |
# Help string
|
|
78 |
puts "export ONE_AUTH=#{ENV['HOME']}/.one/one_x509"
|
|
79 |
|
|
80 |
login_token
|
|
81 |
end
|
|
82 |
|
|
83 |
|
|
84 |
# Reads proxy file from specified or /tmp directory
|
|
85 |
def get_x509_proxy_file
|
|
86 |
path=ENV['X509_PROXY_CERT']
|
|
87 |
File.read(path)
|
|
88 |
end
|
|
89 |
|
|
90 |
|
|
91 |
# Encrypts data with the private key and returns
|
|
92 |
# base 64 encoded output
|
|
93 |
def encrypt(data, priv_key)
|
|
94 |
rsa=OpenSSL::PKey::RSA.new(priv_key)
|
|
95 |
# base 64 output is joined into a single line as opennebula
|
|
96 |
# ascii protocol ends messages with newline
|
|
97 |
Base64::encode64(rsa.private_encrypt(data)).gsub!(/\n/, '').strip
|
|
98 |
end
|
|
99 |
|
|
100 |
|
|
101 |
# Returns an opened file object to ~/.one/one_x509
|
|
102 |
def get_one_proxy_file
|
|
103 |
one_proxy_dir=ENV['HOME']+'/.one'
|
|
104 |
|
|
105 |
# Creates ~/.one directory if it does not exist
|
|
106 |
begin
|
|
107 |
FileUtils.mkdir_p(one_proxy_dir)
|
|
108 |
rescue Errno::EEXIST
|
|
109 |
end
|
|
110 |
|
|
111 |
File.open(one_proxy_dir+'/one_x509', "w")
|
|
112 |
end
|
|
113 |
|
|
114 |
# Creates the login file for x509 authentication using the host certificate.
|
|
115 |
# By default it is valid forever, but can be give an expiration as an option.
|
|
116 |
def host_login(login_file='', expire=0, username='admin')
|
|
117 |
# Get the host private key
|
|
118 |
begin
|
|
119 |
host_cert = File.read('/etc/grid-security/hostkey.pem')
|
|
120 |
rescue
|
|
121 |
raise failed + "Could not read " + '/etc/grid-security/hostkey.pem'
|
|
122 |
end
|
|
123 |
begin
|
|
124 |
host_cert_array=host_cert.split("\n")
|
|
125 |
begin_lines=host_cert_array.select{|l| l.match(/BEGIN RSA PRIVATE KEY/)}
|
|
126 |
begin_index=host_cert_array.index(begin_lines[0])
|
|
127 |
begin_line=host_cert_array[begin_index].to_s
|
|
128 |
|
|
129 |
end_lines=host_cert_array.select{|l| l.match(/END RSA PRIVATE KEY/)}
|
|
130 |
end_index=host_cert_array.index(end_lines[0])
|
|
131 |
end_line=host_cert_array[end_index].to_s
|
|
132 |
|
|
133 |
host_key_array=host_cert_array[begin_index..end_index]
|
|
134 |
private_key=host_key_array.join("\n")
|
|
135 |
rescue
|
|
136 |
raise failed + "Could not get private key from " + '/etc/grid-security/hostkey.pem'
|
|
137 |
end
|
|
138 |
|
|
139 |
begin
|
|
140 |
rsa=OpenSSL::PKey::RSA.new(private_key)
|
|
141 |
rescue
|
|
142 |
raise failed + "Could not create RSA key from " + '/etc/grid-security/hostkey.pem'
|
|
143 |
end
|
|
144 |
|
|
145 |
# Read the host public certificate
|
|
146 |
begin
|
|
147 |
host_cert = File.read('/etc/grid-security/hostcert.pem')
|
|
148 |
rescue
|
|
149 |
raise failed + "Could not read " + '/etc/grid-security/hostcert.pem'
|
|
150 |
end
|
|
151 |
|
|
152 |
# Get host subject name (to be used as password after decryption)
|
|
153 |
begin
|
|
154 |
cert = OpenSSL::X509::Certificate.new(host_cert)
|
|
155 |
encrypted_DN = Base64::encode64(rsa.private_encrypt(cert.subject.to_s)).gsub!(/\n/, '').strip
|
|
156 |
password = Digest::SHA1.hexdigest(encrypted_DN)
|
|
157 |
rescue
|
|
158 |
raise failed + "Could not create certificate from " + '/etc/grid-security/hostkey.pem'
|
|
159 |
end
|
|
160 |
|
|
161 |
# Set expiration time
|
|
162 |
if expire == 0
|
|
163 |
time = 0
|
|
164 |
else
|
|
165 |
time=Time.now.to_i+expire
|
|
166 |
end
|
|
167 |
|
|
168 |
# Sign with timestamp
|
|
169 |
text_to_sign="#{username}:#{password}:#{time}"
|
|
170 |
begin
|
|
171 |
special_token=Base64::encode64(rsa.private_encrypt(text_to_sign)).gsub!(/\n/, '').strip
|
|
172 |
rescue
|
|
173 |
raise failed + "Could not create host-signed token for " + password
|
|
174 |
end
|
|
175 |
|
|
176 |
# Write the login file
|
|
177 |
one_proxy="#{username}:plain:host-signed:#{special_token}"
|
|
178 |
if login_file == ''
|
|
179 |
file=get_one_proxy_file
|
|
180 |
else
|
|
181 |
begin
|
|
182 |
file=File.open(login_file, "w")
|
|
183 |
rescue
|
|
184 |
raise failed + "Could not open " + login_file + " for writing."
|
|
185 |
end
|
|
186 |
end
|
|
187 |
file.chmod(0660)
|
|
188 |
file.write(one_proxy)
|
|
189 |
file.close
|
|
190 |
|
|
191 |
# Help string
|
|
192 |
puts "export ONE_AUTH=" + file.path
|
|
193 |
puts "Set admin password to " + password
|
|
194 |
|
|
195 |
end
|
|
196 |
|
|
197 |
|
|
198 |
# Server side
|
|
199 |
# auth method for auth_mad
|
|
200 |
def auth(user_id, user, dn, login_token)
|
|
201 |
|
|
202 |
begin
|
|
203 |
failed = 'Authentication failed. '
|
|
204 |
|
|
205 |
special_tag, special_token = login_token.split(':')
|
|
206 |
if special_tag == "host-signed"
|
|
207 |
|
|
208 |
# Get the host public certificate
|
|
209 |
begin
|
|
210 |
cert = OpenSSL::X509::Certificate.new(File.read('/etc/grid-security/hostcert.pem'))
|
|
211 |
rescue
|
|
212 |
raise failed + "Could not open file " + '/etc/grid-security/hostcert.pem'
|
|
213 |
end
|
|
214 |
public_key = extract_public_key(cert)
|
|
215 |
# Decrypt the signed text with the public key
|
|
216 |
decrypted=decrypt(special_token, public_key)
|
|
217 |
username, subjectname, time, last =decrypted.split(':')
|
|
218 |
if last # There was a : in the subjectname, from kerberos X509 credential
|
|
219 |
subjectname = subjectname + ':' + time
|
|
220 |
time = last
|
|
221 |
end
|
|
222 |
|
|
223 |
# Check the expiration, username, and password
|
|
224 |
# Host can specify no expiration by setting time=0
|
|
225 |
if time.to_i != 0
|
|
226 |
now=Time.now
|
|
227 |
raise "Login credential expired at " + Time.at(time.to_i).to_s +
|
|
228 |
". Current time is " + now.localtime.to_s + "." if now.to_i>time.to_i
|
|
229 |
end
|
|
230 |
|
|
231 |
# Check the username
|
|
232 |
raise "Login name " + username + " did not match username " + user + "." if user!=username
|
|
233 |
|
|
234 |
# The user is authorized if their subject name has been set as their password.
|
|
235 |
raise "Login DN " + subjectname + " did not match user DN " + dn + "." if subjectname!=dn
|
|
236 |
|
|
237 |
true
|
|
238 |
else
|
|
239 |
|
|
240 |
# Parse the login message
|
|
241 |
token=Base64::decode64(login_token)
|
|
242 |
signed_text, proxy_cert_line, user_cert_line = token.split(':')
|
|
243 |
|
|
244 |
# Extract the proxy certificate
|
|
245 |
if proxy_cert_line != ""
|
|
246 |
proxy_cert = get_cert(proxy_cert_line)
|
|
247 |
proxy_cert = OpenSSL::X509::Certificate.new(proxy_cert)
|
|
248 |
else
|
|
249 |
proxy_cert = nil
|
|
250 |
end
|
|
251 |
|
|
252 |
|
|
253 |
# Extract the user certificate
|
|
254 |
user_cert = get_cert(user_cert_line)
|
|
255 |
user_cert = OpenSSL::X509::Certificate.new(user_cert)
|
|
256 |
subject_name = user_cert.subject.to_s
|
|
257 |
failed = "Authentication failed for " + subject_name + "."
|
|
258 |
|
|
259 |
dn_ok = dn.split('|').include?(subject_name.gsub(/\s/, ''))
|
|
260 |
if dn_ok
|
|
261 |
ok = "true"
|
|
262 |
else
|
|
263 |
ok = "false"
|
|
264 |
end
|
|
265 |
#raise ok
|
|
266 |
|
|
267 |
# Check that the user's DN has been added to the users database
|
|
268 |
unless dn_ok
|
|
269 |
raise "User " + subject_name + " is not mapped in the user database. " + dn
|
|
270 |
end
|
|
271 |
|
|
272 |
# Extract the public key
|
|
273 |
if proxy_cert.nil?
|
|
274 |
public_key = extract_public_key(user_cert)
|
|
275 |
else
|
|
276 |
public_key = extract_public_key(proxy_cert)
|
|
277 |
end
|
|
278 |
|
|
279 |
|
|
280 |
# Decrypt the signed text with the public key
|
|
281 |
decrypted=decrypt(signed_text, public_key)
|
|
282 |
username, time=decrypted.split(':')
|
|
283 |
|
|
284 |
|
|
285 |
# Check the expiration and user name
|
|
286 |
now=Time.now
|
|
287 |
raise "Login credential expired at " + Time.at(time.to_i).to_s +
|
|
288 |
". Current time is " + now.localtime.to_s + "." if now.to_i>time.to_i
|
|
289 |
raise "Login name " + username + " did not match username " + user + "." if user!=username
|
|
290 |
|
|
291 |
|
|
292 |
# Validate the certificate chain of the proxy
|
|
293 |
validated = validate_chain(proxy_cert, user_cert)
|
|
294 |
raise "Could not validate certificate chain." if not validated
|
|
295 |
|
|
296 |
true
|
|
297 |
end
|
|
298 |
rescue
|
|
299 |
failed + "Error in x509 auth method. " + $!
|
|
300 |
end
|
|
301 |
end
|
|
302 |
|
|
303 |
|
|
304 |
# Decrypts base 64 encoded data with pub_key (public key)
|
|
305 |
def decrypt(data, public_key)
|
|
306 |
|
|
307 |
begin
|
|
308 |
rsa=OpenSSL::PKey::RSA.new(Base64::decode64(public_key))
|
|
309 |
rsa.public_decrypt(Base64::decode64(data))
|
|
310 |
rescue
|
|
311 |
raise "Could not decrypt signed text."
|
|
312 |
end
|
|
313 |
end
|
|
314 |
|
|
315 |
|
|
316 |
# Gets a multi-line version of the one-line certificate
|
|
317 |
def get_cert(cert_line)
|
|
318 |
cert_array=cert_line.scan(/.{64}/)
|
|
319 |
lastline = cert_line[cert_array.length*64..-1]
|
|
320 |
cert_array.push(lastline) if lastline.length > 0
|
|
321 |
cert_array.unshift('-----BEGIN CERTIFICATE-----').push('-----END CERTIFICATE-----')
|
|
322 |
cert=cert_array.join("\n")
|
|
323 |
cert
|
|
324 |
end
|
|
325 |
|
|
326 |
|
|
327 |
# Gets the public key from the certificate.
|
|
328 |
def extract_public_key(cert)
|
|
329 |
# gets rid of "---- BEGIN/END RSA PUBLIC KEY ----" lines and joins result into a single line
|
|
330 |
public_key = cert.public_key.to_s.split("\n").reject {|l| l.match(/RSA PUBLIC KEY/) }.join('')
|
|
331 |
public_key
|
|
332 |
end
|
|
333 |
|
|
334 |
|
|
335 |
# Validates the the certificate chain
|
|
336 |
def validate_chain(proxy, user)
|
|
337 |
failed="Error in x509 validate_chain method. "
|
|
338 |
|
|
339 |
# Check start time of proxy or user cert
|
|
340 |
now=Time.now
|
|
341 |
if proxy.nil?
|
|
342 |
not_before = user.not_before
|
|
343 |
else
|
|
344 |
not_before = proxy.not_before
|
|
345 |
end
|
|
346 |
before_ok = not_before<now
|
|
347 |
if !before_ok
|
|
348 |
raise failed + "Cert not valid before " + not_before.localtime.to_s +
|
|
349 |
". Current time is " + now.localtime.to_s + "."
|
|
350 |
end
|
|
351 |
|
|
352 |
|
|
353 |
# Check end time of proxy
|
|
354 |
if proxy.nil?
|
|
355 |
not_after = user.not_after
|
|
356 |
else
|
|
357 |
not_after = proxy.not_after
|
|
358 |
end
|
|
359 |
after_ok=not_after>now
|
|
360 |
if !after_ok
|
|
361 |
raise failed + "Cert not valid after " + not_after.localtime.to_s +
|
|
362 |
". Current time is " + now.localtime.to_s + "."
|
|
363 |
end
|
|
364 |
|
|
365 |
|
|
366 |
# Check that the issuer of the proxy is the same user as in the user certificate
|
|
367 |
is_proxy=!(proxy.nil?)
|
|
368 |
if is_proxy
|
|
369 |
issuer_ok=proxy.issuer.to_s==user.subject.to_s
|
|
370 |
if !issuer_ok
|
|
371 |
raise failed + "Proxy with issuer " + proxy.issuer.to_s + " does not match user " + user.subject.to_s + "."
|
|
372 |
end
|
|
373 |
end
|
|
374 |
|
|
375 |
|
|
376 |
# Check that the user signed the proxy
|
|
377 |
verified=!is_proxy||proxy.verify(user.public_key)
|
|
378 |
if !verified
|
|
379 |
#proxy_hash = proxy.subject.hash.to_s(16)
|
|
380 |
#user_hash = user.subject.hash.to_s(16)
|
|
381 |
##puts "%8s"%proxy_hash + " was signed by " + "%8s"%user_hash + " ("+user.subject.to_s+")"
|
|
382 |
#else
|
|
383 |
raise failed + "Proxy with issuer " + proxy.subject.to_s + " was not verified by " + user.subject.to_s + "."
|
|
384 |
end
|
|
385 |
|
|
386 |
|
|
387 |
# Check the rest of the certificate chain
|
|
388 |
signee=user
|
|
389 |
begin
|
|
390 |
ca_hash = signee.issuer.hash.to_s(16)
|
|
391 |
begin
|
|
392 |
ca = OpenSSL::X509::Certificate.new(File.read('/etc/grid-security/certificates/'+ca_hash+'.0'))
|
|
393 |
rescue
|
|
394 |
raise failed + "Could not open file " + '/etc/grid-security/certificates/'+ca_hash+'.0' + "."
|
|
395 |
end
|
|
396 |
verified = signee.issuer.to_s==ca.subject.to_s and signee.verify(ca.public_key)
|
|
397 |
if verified
|
|
398 |
#puts "%8s"%signee.subject.hash.to_s(16) + " was signed by " + "%8s"%ca_hash + " ("+ca.subject.to_s+")"
|
|
399 |
signee=ca
|
|
400 |
else
|
|
401 |
raise failed + signee.subject.to_s + " with issuer " + signee.issuer.to_s + " was not verified by " + ca.subject.to_s + "."
|
|
402 |
end
|
|
403 |
end while ca.subject.to_s!=ca.issuer.to_s
|
|
404 |
|
|
405 |
|
|
406 |
#puts ca.subject.hash.to_s(16) + " was issued by " + ca.issuer.hash.to_s(16) if verified
|
|
407 |
|
|
408 |
true
|
|
409 |
end
|
|
410 |
|
|
411 |
end
|
0 |
|
-
|