class OpenNebula::X509Auth
X509 authentication class. It can be used as a driver for auth_mad as auth method is defined. It also holds some helper methods to be used by oneauth command
Constants
- ETC_LOCATION
- X509_AUTH_CONF_PATH
- X509_DEFAULTS
Public Class Methods
# File lib/opennebula/x509_auth.rb, line 43 def self.escape_dn(dn) dn.gsub(/\s/) { |s| "\\"+s[0].ord.to_s(16) } end
Initialize x509Auth object
@param [Hash] default options for path @option options [String] :certs_pem
cert chain array in colon-separated pem format
@option options [String] :key_pem
key in pem format
@option options [String] :ca_dir
directory of trusted CA's. Needed for auth method, not for login.
# File lib/opennebula/x509_auth.rb, line 61 def initialize(options={}) @options ||= X509_DEFAULTS @options.merge!(options) load_options(X509_AUTH_CONF_PATH) @cert_chain = @options[:certs_pem].collect do |cert_pem| OpenSSL::X509::Certificate.new(cert_pem) end if @options[:key_pem] @key = OpenSSL::PKey::RSA.new(@options[:key_pem]) end end
# File lib/opennebula/x509_auth.rb, line 47 def self.unescape_dn(dn) dn.gsub(/\\[0-9a-f]{2}/) { |s| s[1,2].to_i(16).chr } end
Public Instance Methods
Server side
auth method for auth_mad
# File lib/opennebula/x509_auth.rb, line 112 def authenticate(user, pass, signed_text) begin # Decryption demonstrates that the user posessed the private key. _user, expires = decrypt(signed_text).split(':') return "User name missmatch" if user != _user return "x509 proxy expired" if Time.now.to_i >= expires.to_i # Some DN in the chain must match a DN in the password dn_ok = @cert_chain.each do |cert| if pass.split('|').include?( self.class.escape_dn(cert.subject.to_s)) break true end end unless dn_ok == true return "Certificate subject missmatch" end validate return true rescue => e return e.message end end
Generates a login token in the form: user_name:x509:user_name:time_expires:cert_chain
- user_name:time_expires is encrypted with the user certificate - user_name:time_expires:cert_chain is base64 encoded.
By default it is valid as long as the certificate is valid. It can be changed to any number of seconds with expire parameter (sec.)
# File lib/opennebula/x509_auth.rb, line 92 def login_token(user, expire=0) if expire != 0 expires = Time.now.to_i + expire.to_i else expires = @cert_chain[0].not_after.to_i end text_to_sign = "#{user}:#{expires}" signed_text = encrypt(text_to_sign) certs_pem = @cert_chain.collect{|cert| cert.to_pem}.join(":") token = "#{signed_text}:#{certs_pem}" return Base64::encode64(token).strip.delete("\n") end
Returns a valid password string to create a user using this auth driver. In this case the dn of the user certificate.
# File lib/opennebula/x509_auth.rb, line 82 def password self.class.escape_dn(@cert_chain[0].subject.to_s) end
Private Instance Methods
# File lib/opennebula/x509_auth.rb, line 222 def check_crl(signee) failed = "Could not validate user credentials: " ca_hash = signee.issuer.hash.to_s(16) ca_path = @options[:ca_dir] + '/' + ca_hash + '.0' crl_path = @options[:ca_dir] + '/' + ca_hash + '.r0' if !File.exist?(crl_path) if @options[:check_crl] raise failed + "CRL file #{crl_path} does not exist" else return end end ca_cert = OpenSSL::X509::Certificate.new( File.read(ca_path) ) crl_cert = OpenSSL::X509::CRL.new( File.read(crl_path) ) # First verify the CRL itself with its signer unless crl_cert.verify( ca_cert.public_key ) then raise failed + "CRL is not verified by its Signer" end # Extract the list of revoked certificates from the CRL rc_array = crl_cert.revoked # Loop over the list and compare with the target personal # certificate rc_array.each do |e| if e.serial.eql?(signee.serial) then raise failed + "#{signee.subject.to_s} is found in the "<< "CRL, i.e. it is revoked" end end end
Decrypts base 64 encoded data with pub_key (public key)
# File lib/opennebula/x509_auth.rb, line 163 def decrypt(data) @cert_chain[0].public_key.public_decrypt(Base64::decode64(data)) end
Methods to encrpyt/decrypt keys
Encrypts data with the private key of the user and returns base 64 encoded output in a single line
# File lib/opennebula/x509_auth.rb, line 157 def encrypt(data) return nil if !@key Base64::encode64(@key.private_encrypt(data)).delete("\n").strip end
Load class options form a configuration file (yaml syntax)
# File lib/opennebula/x509_auth.rb, line 143 def load_options(conf_file) if File.readable?(conf_file) conf_txt = File.read(conf_file) conf_opt = YAML::load(conf_txt) @options.merge!(conf_opt) if conf_opt != false end end
Validate the user certificate
# File lib/opennebula/x509_auth.rb, line 170 def validate now = Time.now # Check start time and end time of certificates @cert_chain.each do |cert| if cert.not_before > now || cert.not_after < now raise failed + "Certificate not valid. Current time is " + now.localtime.to_s + "." end end begin # Validate the proxy certifcates signee = @cert_chain[0] check_crl(signee) @cert_chain[1..-1].each do |cert| if !((signee.issuer.to_s == cert.subject.to_s) && (signee.verify(cert.public_key))) raise failed + signee.subject.to_s + " with issuer " + signee.issuer.to_s + " was not verified by " + cert.subject.to_s + "." end signee = cert end # Validate the End Entity certificate if !@options[:ca_dir] raise failed + "No certifcate authority directory was specified." end begin ca_hash = signee.issuer.hash.to_s(16) ca_path = @options[:ca_dir] + '/' + ca_hash + '.0' ca_cert = OpenSSL::X509::Certificate.new(File.read(ca_path)) if !((signee.issuer.to_s == ca_cert.subject.to_s) && (signee.verify(ca_cert.public_key))) raise failed + signee.subject.to_s + " with issuer " + signee.issuer.to_s + " was not verified by " + ca_cert.subject.to_s + "." end signee = ca_cert end while ca_cert.subject.to_s != ca_cert.issuer.to_s rescue raise end end