0002-Add-x509_login-and-host_login-functions.patch

Ted Hesselroth, 05/03/2011 06:48 PM

Download (14.8 KB)

View differences:

src/authm_mad/x509_auth.rb
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
-