From 12600a2269afb2873a1745a0265a46182a565e7b Mon Sep 17 00:00:00 2001 From: Daniel Clavijo Coca Date: Mon, 18 Sep 2023 03:07:09 -0600 Subject: [PATCH] F OpenNebula/one#6274: Support OpenSSH formatted ssh priv keys (#2726) --- src/authm_mad/remotes/ssh/ssh_auth.rb | 71 +++++++++++++++++---------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/src/authm_mad/remotes/ssh/ssh_auth.rb b/src/authm_mad/remotes/ssh/ssh_auth.rb index d887eb3ee3..f545c84007 100644 --- a/src/authm_mad/remotes/ssh/ssh_auth.rb +++ b/src/authm_mad/remotes/ssh/ssh_auth.rb @@ -14,24 +14,23 @@ # limitations under the License. # #--------------------------------------------------------------------------- # - -require 'pp' require 'openssl' require 'base64' require 'fileutils' - -module OpenNebula; end +require 'open3' +require 'tempfile' # SSH key 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 class OpenNebula::SshAuth + # Initialize SshAuth object # # @param [Hash] default options for path # @option options [String] :public_key public key for the user # @option options [String] :private_key key private key for the user. - def initialize(options={}) + def initialize(options = {}) @private_key = nil @public_key = nil @@ -39,17 +38,22 @@ class OpenNebula::SshAuth if options[:private_key] begin @private_key = File.read(options[:private_key]) - rescue Exception => e - raise "Cannot read #{options[:private_key]}" + rescue StandardError => e + raise "Cannot read #{options[:private_key]}\n #{e}" end - @private_key_rsa = OpenSSL::PKey::RSA.new(@private_key) + begin + @private_key_rsa = OpenSSL::PKey::RSA.new(@private_key) + rescue OpenSSL::PKey::RSAError + private_key_pem = openssh_to_pem(@private_key) + @private_key_rsa = OpenSSL::PKey::RSA.new(private_key_pem) + end end # Initialize the public key if options[:public_key] @public_key = options[:public_key] - elsif @private_key != nil + elsif !@private_key.nil? # Init ssh keys using private key. public key is extracted in a # format compatible with openssl. The public key does not contain # "---- BEGIN/END PUBLIC KEY ----" and is in a single line @@ -58,16 +62,16 @@ class OpenNebula::SshAuth end if @private_key.nil? && @public_key.nil? - raise "You have to define at least one of the keys" + raise 'You have to define at least one of the keys' end - @public_key_rsa = OpenSSL::PKey::RSA.new(Base64::decode64(@public_key)) + @public_key_rsa = OpenSSL::PKey::RSA.new(Base64.decode64(@public_key)) end # Creates a login token for ssh authentication. # By default it is valid for 1 hour but it can be changed to any number # of seconds with expire parameter (in seconds) - def login_token(user, expire=3600) + def login_token(user, expire = 3600) expire ||= 3600 return encrypt("#{user}:#{Time.now.to_i + expire.to_i}") @@ -83,35 +87,50 @@ class OpenNebula::SshAuth def authenticate(user, token) begin token_plain = decrypt(token) - _user, time = token_plain.split(':') + t_user, time = token_plain.split(':') - if user == _user - if Time.now.to_i >= time.to_i - return "ssh proxy expired, login again to renew it" - else - return true - end - else - return "invalid credentials" - end - rescue - return "error" + return 'invalid credentials' unless user == t_user + return 'ssh proxy expired, login again to renew it' if Time.now.to_i >= time.to_i + + return true + rescue StandardError + return 'error' end end private + def openssh_to_pem(private_key) + temp_file = Tempfile.new('private_key') + + File.write(temp_file.path, private_key) + + # Use ssh-keygen to convert the key + command = "ssh-keygen -p -N '' -m PEM -f #{temp_file.path}" + + _out, err, status = Open3.capture3(command) + + raise "Failed to convert key: #{err}" unless status.success? + + pem_key = File.read(temp_file.path) + return pem_key + ensure + temp_file.close + temp_file.unlink if temp_file + end + ########################################################################### # Methods to handle ssh keys ########################################################################### # Encrypts data with the private key of the user and returns # base 64 encoded output in a single line def encrypt(data) - Base64::encode64(@private_key_rsa.private_encrypt(data)).gsub!(/\n/, '').strip + Base64.encode64(@private_key_rsa.private_encrypt(data)).gsub!("\n", '').strip end # Decrypts base 64 encoded data with pub_key (public key) def decrypt(data) - @public_key_rsa.public_decrypt(Base64::decode64(data)) + @public_key_rsa.public_decrypt(Base64.decode64(data)) end + end