x509_auth.rb
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
|