From a269255b2f51783ba19cbc363759ba9a61038ea0 Mon Sep 17 00:00:00 2001 From: elijah Date: Thu, 25 Oct 2012 00:49:10 -0700 Subject: cleaned up the code a bit by adding SshKey class. --- lib/leap_cli.rb | 1 + lib/leap_cli/commands/node.rb | 79 +++++--------------------- lib/leap_cli/commands/user.rb | 28 ++++++--- lib/leap_cli/ssh_key.rb | 128 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 75 deletions(-) create mode 100644 lib/leap_cli/ssh_key.rb diff --git a/lib/leap_cli.rb b/lib/leap_cli.rb index a88a8ad..4dccec2 100644 --- a/lib/leap_cli.rb +++ b/lib/leap_cli.rb @@ -9,6 +9,7 @@ require 'leap_cli/init' require 'leap_cli/path' require 'leap_cli/util' require 'leap_cli/log' +require 'leap_cli/ssh_key' require 'leap_cli/config/object' require 'leap_cli/config/object_list' require 'leap_cli/config/manager' diff --git a/lib/leap_cli/commands/node.rb b/lib/leap_cli/commands/node.rb index 46c8fb6..b9640a8 100644 --- a/lib/leap_cli/commands/node.rb +++ b/lib/leap_cli/commands/node.rb @@ -7,11 +7,11 @@ module LeapCli; module Commands ## COMMANDS ## - #desc 'Create a new configuration for a node' - #command :'new-node' do |c| - # c.action do |global_options,options,args| - # end - #end + desc 'not yet implemented... Create a new configuration for a node' + command :'new-node' do |c| + c.action do |global_options,options,args| + end + end desc 'Bootstraps a node, setting up ssh keys and installing prerequisites' arg_name '', :optional => false, :multiple => false @@ -84,28 +84,27 @@ module LeapCli; module Commands # def save_public_host_key(node) progress("Fetching public SSH host key for #{node.name}") - public_key, key_type = get_public_key_for_ip(node.ip_address) + public_key = get_public_key_for_ip(node.ip_address) pub_key_path = Path.named_path([:node_ssh_pub_key, node.name]) if Path.exists?(pub_key_path) - if file_content_equals?(pub_key_path, node_pub_key_contents(key_type, public_key)) + if public_key == SshKey.load_from_file(pub_key_path) progress("Public SSH host key for #{node.name} has not changed") else bail!("WARNING: The public SSH host key we just fetched for #{node.name} doesn't match what we have saved previously. Remove the file #{pub_key_path} if you really want to change it.") end - elsif key_in_known_hosts?(public_key, [node.name, node.ip_address, node.domain.name]) + elsif public_key.in_known_hosts?(node.name, node.ip_address, node.domain.name) progress("Public SSH host key for #{node.name} is trusted (key found in your ~/.ssh/known_hosts)") else - fingerprint, bits = ssh_key_fingerprint(key_type, public_key) puts say("This is the SSH host key you got back from node \"#{node.name}\"") - say("Type -- #{bits} bit #{key_type.upcase}") - say("Fingerprint -- " + fingerprint) - say("Public Key -- " + public_key) + say("Type -- #{public_key.bits} bit #{public_key.type.upcase}") + say("Fingerprint -- " + public_key.fingerprint) + say("Public Key -- " + public_key.key) if !agree("Is this correct? ") bail! else puts - write_file!([:node_ssh_pub_key, node.name], node_pub_key_contents(key_type, public_key)) + write_file!([:node_ssh_pub_key, node.name], public_key.to_s) end end end @@ -116,49 +115,7 @@ module LeapCli; module Commands line = output.split("\n").grep(/^[^#]/).first assert! line, "Got zero host keys back!" ip, key_type, public_key = line.split(' ') - return [public_key, key_type] - end - - # - # returns true if the particular host_key is found in a "known_hosts" file installed for the current user or on this machine. - # - # - host_key: string of ssh public host key - # - identifiers: an array of identifers (which could be an ip address or hostname) - # - def key_in_known_hosts?(host_key, identifiers) - identifiers.each do |identifier| - Net::SSH::KnownHosts.search_for(identifier).each do |key| - # i am not sure what format ssh keys are in, but key.to_pem returns something different than we need. - # this little bit of magic code will encode correctly. I think the format is base64 encoding of bits, exponent, and modulus. - key_string = [Net::SSH::Buffer.from(:key, key).to_s].pack("m*").gsub(/\s/, "") - return true if key_string == host_key - end - end - return false - end - - # - # gets a fingerprint for a key string - # - # i think this could better be done this way: - # blob = Net::SSH::Buffer.from(:key, key).to_s - # fingerprint = OpenSSL::Digest::MD5.hexdigest(blob).scan(/../).join(":") - # - def ssh_key_fingerprint(type, key) - assert_bin!('ssh-keygen') - file = Tempfile.new('leap_cli_public_key_') - begin - file.write(type) - file.write(" ") - file.write(key) - file.close - output = assert_run!("ssh-keygen -l -f #{file.path}", "Failed to run ssh-keygen on public key.") - bits, fingerprint, filename, key_type = output.split(' ') - return [fingerprint, bits] - ensure - file.close - file.unlink - end + return SshKey.load(public_key, key_type) end def ping_node(node) @@ -166,14 +123,4 @@ module LeapCli; module Commands assert_run!("ping -W 1 -c 1 #{node.ip_address}", "Could not ping #{node.name} (address #{node.ip_address}). Try again, we only send a single ping.") end - # - # returns a string that can be used for the contents of the files/nodes/x/x_ssh_key.pub file - # - # We write the file without ipaddress or hostname, because these might change later. - # The ip and host is added at when compiling the combined known_hosts file. - # - def node_pub_key_contents(key_type, public_key) - [key_type, public_key].join(' ') - end - end; end \ No newline at end of file diff --git a/lib/leap_cli/commands/user.rb b/lib/leap_cli/commands/user.rb index a7bf848..5f7702a 100644 --- a/lib/leap_cli/commands/user.rb +++ b/lib/leap_cli/commands/user.rb @@ -40,12 +40,11 @@ module LeapCli if options[:self] username ||= `whoami`.strip - ssh_pub_key ||= pick_ssh_key + ssh_pub_key ||= pick_ssh_key.to_s pgp_pub_key ||= pick_pgp_key end assert!(ssh_pub_key, 'Sorry, could not find SSH public key.') - #assert!(pgp_pub_key, 'Sorry, could not find OpenPGP public key.') if ssh_pub_key write_file!([:user_ssh, username], ssh_pub_key) @@ -61,19 +60,30 @@ module LeapCli # let the the user choose among the ssh public keys that we encounter, or just pick the key if there is only one. # def pick_ssh_key - assert_bin! 'ssh-add' - ssh_fingerprints = `ssh-add -l`.split("\n").compact - assert! ssh_fingerprints.any?, 'Sorry, could not find any SSH public key for you. Have you run ssh-keygen?' + ssh_keys = [] + Dir.glob("#{ENV['HOME']}/.ssh/*.pub").each do |keyfile| + ssh_keys << SshKey.load(keyfile) + end + + if `which ssh-add && ssh-add -L`.strip.any? + `ssh-add -L`.split("\n").compact.each do |line| + key = SshKey.load(line) + key.comment = 'ssh-agent' + ssh_keys << key unless ssh_keys.include?(key) + end + end + ssh_keys.compact! + + assert! ssh_keys.any?, 'Sorry, could not find any SSH public key for you. Have you run ssh-keygen?' - if ssh_fingerprints.length > 1 - key_index = numbered_choice_menu('Choose your SSH public key', ssh_fingerprints) do |key, i| - say("#{i+1}. #{key}") + if ssh_keys.length > 1 + key_index = numbered_choice_menu('Choose your SSH public key', ssh_keys.collect(&:summary)) do |line, i| + say("#{i+1}. #{line}") end else key_index = 0 end - ssh_keys = `ssh-add -L`.split("\n").compact return ssh_keys[key_index] end diff --git a/lib/leap_cli/ssh_key.rb b/lib/leap_cli/ssh_key.rb new file mode 100644 index 0000000..daa8bf0 --- /dev/null +++ b/lib/leap_cli/ssh_key.rb @@ -0,0 +1,128 @@ +# +# A wrapper around OpenSSL::PKey::RSA instances to provide a better api for dealing with SSH keys. +# +# + +require 'net/ssh' +require 'forwardable' + +module LeapCli + class SshKey + extend Forwardable + + attr_accessor :filename + attr_accessor :comment + + ## + ## CLASS METHODS + ## + + def self.load(arg1, arg2=nil) + key = nil + if arg1.is_a? OpenSSL::PKey::RSA + key = SshKey.new arg1 + elsif arg1.is_a? String + if arg1 =~ /^ssh-/ + type, data = arg1.split(' ') + key = SshKey.new load_from_data(data, type) + elsif File.exists? arg1 + key = SshKey.new load_from_file(arg1) + key.filename = arg1 + else + key = SshKey.new load_from_data(arg1, arg2) + end + end + return key + end + + def self.load_from_file(filename) + public_key = nil + private_key = nil + begin + public_key = Net::SSH::KeyFactory.load_public_key(filename) + rescue NotImplementedError, Net::SSH::Exception, OpenSSL::PKey::PKeyError + begin + private_key = Net::SSH::KeyFactory.load_private_key(filename) + rescue NotImplementedError, Net::SSH::Exception, OpenSSL::PKey::PKeyError + end + end + public_key || private_key + end + + def self.load_from_data(data, type='ssh-rsa') + public_key = nil + private_key = nil + begin + public_key = Net::SSH::KeyFactory.load_data_public_key("#{type} #{data}") + rescue NotImplementedError, Net::SSH::Exception, OpenSSL::PKey::PKeyError + begin + private_key = Net::SSH::KeyFactory.load_data_private_key("#{type} #{data}") + rescue NotImplementedError, Net::SSH::Exception, OpenSSL::PKey::PKeyError + end + end + public_key || private_key + end + + ## + ## INSTANCE METHODS + ## + + public + + def initialize(rsa_key) + @key = rsa_key + end + + def_delegator :@key, :fingerprint, :fingerprint + def_delegator :@key, :public?, :public? + def_delegator :@key, :private?, :private? + def_delegator :@key, :ssh_type, :type + def_delegator :@key, :public_encrypt, :public_encrypt + def_delegator :@key, :public_decrypt, :public_decrypt + def_delegator :@key, :private_encrypt, :private_encrypt + def_delegator :@key, :private_decrypt, :private_decrypt + def_delegator :@key, :params, :params + + def public_key + SshKey.new(@key.public_key) + end + + def private_key + SshKey.new(@key.private_key) + end + + # + # not sure if this will always work, but is seems to for now. + # + def bits + Net::SSH::Buffer.from(:key, @key).to_s.split("\001\000").last.size * 8 + end + + def summary + "%s %s %s (%s)" % [self.type, self.bits, self.fingerprint, self.filename || self.comment || ''] + end + + def to_s + self.type + " " + self.key + end + + def key + [Net::SSH::Buffer.from(:key, @key).to_s].pack("m*").gsub(/\s/, "") + end + + def ==(other_key) + return false if other_key.nil? + self.params == other_key.params + end + + def in_known_hosts?(*identifiers) + identifiers.each do |identifier| + Net::SSH::KnownHosts.search_for(identifier).each do |key| + return true if self == key + end + end + return false + end + + end +end -- cgit v1.2.3