x509_auth.rb

Hyunwoo Kim, 03/01/2013 04:21 PM

Download (9.6 KB)

 
1
# -------------------------------------------------------------------------- #
2
# Copyright 2002-2012, OpenNebula Project Leads (OpenNebula.org)             #
3
#                                                                            #
4
# Licensed under the Apache License, Version 2.0 (the "License"); you may    #
5
# not use this file except in compliance with the License. You may obtain    #
6
# a copy of the License at                                                   #
7
#                                                                            #
8
# http://www.apache.org/licenses/LICENSE-2.0                                 #
9
#                                                                            #
10
# Unless required by applicable law or agreed to in writing, software        #
11
# distributed under the License is distributed on an "AS IS" BASIS,          #
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   #
13
# See the License for the specific language governing permissions and        #
14
# limitations under the License.                                             #
15
#--------------------------------------------------------------------------- #
16

    
17
require 'openssl'
18
require 'base64'
19
require 'fileutils'
20
require 'yaml'
21

    
22
# X509 authentication class. It can be used as a driver for auth_mad
23
# as auth method is defined. It also holds some helper methods to be used
24
# by oneauth command
25
class X509Auth
26
    ###########################################################################
27
    #Constants with paths to relevant files and defaults
28
    ###########################################################################
29
    if !ENV["ONE_LOCATION"]
30
        ETC_LOCATION      = "/etc/one"
31
    else
32
        ETC_LOCATION      = ENV["ONE_LOCATION"] + "/etc"
33
    end
34

    
35
    LOGIN_PATH = ENV['HOME']+'/.one/one_x509'
36

    
37
    X509_AUTH_CONF_PATH = ETC_LOCATION + "/auth/x509_auth.conf"
38

    
39
    X509_DEFAULTS = {
40
        :ca_dir   => ETC_LOCATION + "/auth/certificates"
41
    }
42

    
43
    ###########################################################################
44
    # Initialize x509Auth object
45
    #
46
    # @param [Hash] default options for path
47
    # @option options [String] :certs_pem
48
    #         cert chain array in colon-separated pem format
49
    # @option options [String] :key_pem
50
    #         key in pem format
51
    # @option options [String] :ca_dir
52
    #         directory of trusted CA's. Needed for auth method, not for login.
53
    def initialize(options={})
54
        @options ||= X509_DEFAULTS
55
        @options.merge!(options)
56

    
57
        load_options(X509_AUTH_CONF_PATH)
58

    
59
        @cert_chain = @options[:certs_pem].collect do |cert_pem|
60
            OpenSSL::X509::Certificate.new(cert_pem)
61
        end
62

    
63
        if @options[:key_pem]
64
            @key  = OpenSSL::PKey::RSA.new(@options[:key_pem])
65
        end
66
    end
67

    
68
    ###########################################################################
69
    # Client side
70
    ###########################################################################
71

    
72
    # Creates the login file for x509 authentication at ~/.one/one_x509.
73
    # By default it is valid as long as the certificate is valid. It can
74
    # be changed to any number of seconds with expire parameter (sec.)
75
    def login(user, expire=0)
76
        write_login(login_token(user,expire))
77
    end
78

    
79
    # Returns a valid password string to create a user using this auth driver.
80
    # In this case the dn of the user certificate.
81
    def password
82
        @cert_chain[0].subject.to_s.delete("\s")
83
    end
84

    
85
    # Generates a login token in the form:
86
    # user_name:x509:user_name:time_expires:cert_chain
87
    #   - user_name:time_expires is encrypted with the user certificate
88
    #   - user_name:time_expires:cert_chain is base64 encoded
89
    def login_token(user, expire)
90
        if expire != 0
91
            expires = Time.now.to_i + expire.to_i
92
        else
93
            expires = @cert_chain[0].not_after.to_i
94
        end
95

    
96
        text_to_sign = "#{user}:#{expires}"
97
        signed_text  = encrypt(text_to_sign)
98

    
99
        certs_pem = @cert_chain.collect{|cert| cert.to_pem}.join(":")
100

    
101
        token     = "#{signed_text}:#{certs_pem}"
102
        token64   = Base64::encode64(token).strip.delete("\n")
103

    
104
        login_out = "#{user}:#{token64}"
105

    
106
        login_out
107
    end
108

    
109
    ###########################################################################
110
    # Server side
111
    ###########################################################################
112
    # auth method for auth_mad
113
    def authenticate(user, pass, signed_text)
114
        begin
115
            # Decryption demonstrates that the user posessed the private key.
116
            _user, expires = decrypt(signed_text).split(':')
117

    
118
            return "User name missmatch" if user != _user
119

    
120
            return "x509 proxy expired"  if Time.now.to_i >= expires.to_i
121

    
122
            # Some DN in the chain must match a DN in the password
123
            dn_ok = @cert_chain.each do |cert|
124
                if pass.split('|').include?(cert.subject.to_s.delete("\s"))
125
                    break true
126
                end
127
            end
128

    
129
            unless dn_ok == true
130
                return "Certificate subject missmatch"
131
            end
132

    
133
            validate
134

    
135
            return true
136
        rescue => e
137
            return e.message
138
        end
139
    end
140

    
141
private
142
    # Writes a login_txt to the login file as defined in LOGIN_PATH
143
    # constant
144
    def write_login(login_txt)
145
        # Inits login file path and creates ~/.one directory if needed
146
        # Set instance variables
147
        login_dir = File.dirname(LOGIN_PATH)
148

    
149
        begin
150
            FileUtils.mkdir_p(login_dir)
151
        rescue Errno::EEXIST
152
        end
153

    
154
        file = File.open(LOGIN_PATH, "w")
155
        file.write(login_txt)
156
        file.close
157

    
158
        File.chmod(0600,LOGIN_PATH)
159
    end
160

    
161
    # Load class options form a configuration file (yaml syntax)
162
    def load_options(conf_file)
163
        if File.readable?(conf_file)
164
            conf_txt = File.read(conf_file)
165
            conf_opt = YAML::load(conf_txt)
166

    
167
            @options.merge!(conf_opt) if conf_opt != false
168
        end
169
    end
170

    
171
    ###########################################################################
172
    #                       Methods to encrpyt/decrypt keys
173
    ###########################################################################
174
    # Encrypts data with the private key of the user and returns
175
    # base 64 encoded output in a single line
176
    def encrypt(data)
177
        return nil if !@key
178
        Base64::encode64(@key.private_encrypt(data)).delete("\n").strip
179
    end
180

    
181
    # Decrypts base 64 encoded data with pub_key (public key)
182
    def decrypt(data)
183
        @cert_chain[0].public_key.public_decrypt(Base64::decode64(data))
184
    end
185

    
186
    ###########################################################################
187
    # Validate the user certificate
188
    ###########################################################################
189
    def validate
190
        now    = Time.now
191
        failed = "Could not validate user credentials: "
192

    
193
        # Check start time and end time of certificates
194
        @cert_chain.each do |cert|
195
            if cert.not_before > now || cert.not_after < now
196
                raise failed +  "Certificate not valid. Current time is " +
197
                  now.localtime.to_s + "."
198
            end
199
        end
200

    
201
        begin
202
            # Validate the proxy certifcates
203
            signee = @cert_chain[0]
204

    
205
## FC-Start
206
          fc_ca_hash = signee.issuer.hash.to_s(16)
207
          fc_ca_path = @options[:ca_dir] + '/' + fc_ca_hash + '.0'
208
          fc_ca_cert = OpenSSL::X509::Certificate.new( File.read(fc_ca_path) )
209
          fc_rl_path = @options[:ca_dir] + '/' + fc_ca_hash + '.r0'
210
          fc_rl_cert = OpenSSL::X509::CRL.new(         File.read(fc_rl_path) )
211

    
212
          ## First verify the CRL itself with its signer(DOEGrids.pem)
213
          unless fc_rl_cert.verify( fc_ca_cert.public_key ) then
214
            raise failed + "CRL is not verified by its Signer"
215
          end
216

    
217
          fcarray = fc_rl_cert.revoked ## Extract the list of revoked certificates from the CRL
218

    
219
          ## Loop over the list and compare with the target personal certificate
220
          fcarray.each do |e| 
221
          if e.serial.eql?(signee.serial) then #  if e.serial == hkcert.serial then
222
#            puts "A = e.serial and B = signee.serial"
223
            raise failed + "#{signee.subject.to_s} is found in the CRL, i.e. it is revoked"
224
          end
225
        end
226
## FC-End
227

    
228
            @cert_chain[1..-1].each do |cert|
229
                if !((signee.issuer.to_s == cert.subject.to_s) &&
230
                     (signee.verify(cert.public_key)))
231
                    raise  failed + signee.subject.to_s + " with issuer " +
232
                           signee.issuer.to_s + " was not verified by " +
233
                           cert.subject.to_s + "."
234
                end
235
                signee = cert
236
            end
237

    
238
            # Validate the End Entity certificate
239
            if !@options[:ca_dir]
240
                raise failed + "No certifcate authority directory was specified."
241
            end
242

    
243
            begin
244
                ca_hash = signee.issuer.hash.to_s(16)
245
                ca_path = @options[:ca_dir] + '/' + ca_hash + '.0'
246

    
247
                ca_cert = OpenSSL::X509::Certificate.new(File.read(ca_path))
248

    
249
                if !((signee.issuer.to_s == ca_cert.subject.to_s) &&
250
                     (signee.verify(ca_cert.public_key)))
251
                    raise  failed + signee.subject.to_s + " with issuer " +
252
                           signee.issuer.to_s + " was not verified by " +
253
                           ca_cert.subject.to_s + "."
254
                end
255

    
256
                signee = ca_cert
257
            end while ca_cert.subject.to_s != ca_cert.issuer.to_s
258
        rescue
259
            raise
260
        end
261
    end
262
end