From c77cace5225eb16d35865664754e88f4d67bba7f Mon Sep 17 00:00:00 2001 From: elijah Date: Thu, 23 Jun 2016 15:49:03 -0700 Subject: migrate commands to use new ssh system: node init, test, add-user --- lib/leap_cli/ssh/key.rb | 199 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 lib/leap_cli/ssh/key.rb (limited to 'lib/leap_cli/ssh/key.rb') diff --git a/lib/leap_cli/ssh/key.rb b/lib/leap_cli/ssh/key.rb new file mode 100644 index 00000000..ad1ecf15 --- /dev/null +++ b/lib/leap_cli/ssh/key.rb @@ -0,0 +1,199 @@ +# +# A wrapper around OpenSSL::PKey::RSA instances to provide a better api for +# dealing with SSH keys. +# +# NOTE: cipher 'ssh-ed25519' not supported yet because we are waiting +# for support in Net::SSH +# + +require 'net/ssh' +require 'forwardable' + +module LeapCli + module SSH + class Key + extend Forwardable + + attr_accessor :filename + attr_accessor :comment + + # supported ssh key types, in order of preference + SUPPORTED_TYPES = ['ssh-rsa', 'ecdsa-sha2-nistp256'] + SUPPORTED_TYPES_RE = /(#{SUPPORTED_TYPES.join('|')})/ + + ## + ## CLASS METHODS + ## + + def self.load(arg1, arg2=nil) + key = nil + if arg1.is_a? OpenSSL::PKey::RSA + key = Key.new arg1 + elsif arg1.is_a? String + if arg1 =~ /^ssh-/ + type, data = arg1.split(' ') + key = Key.new load_from_data(data, type) + elsif File.exist? arg1 + key = Key.new load_from_file(arg1) + key.filename = arg1 + else + key = Key.new load_from_data(arg1, arg2) + end + end + return key + rescue StandardError + 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 + + # + # Picks one key out of an array of keys that we think is the "best", + # based on the order of preference in SUPPORTED_TYPES + # + # Currently, this does not take bitsize into account. + # + def self.pick_best_key(keys) + keys.select {|k| + SUPPORTED_TYPES.include?(k.type) + }.sort {|a,b| + SUPPORTED_TYPES.index(a.type) <=> SUPPORTED_TYPES.index(b.type) + }.first + end + + # + # takes a string with one or more ssh keys, one key per line, + # and returns an array of Key objects. + # + # the lines should be in one of these formats: + # + # 1. + # 2. + # + def self.parse_keys(string) + keys = [] + lines = string.split("\n").grep(/^[^#]/) + lines.each do |line| + if line =~ / #{Key::SUPPORTED_TYPES_RE} / + # + keys << line.split(' ')[1..2] + elsif line =~ /^#{Key::SUPPORTED_TYPES_RE} / + # + keys << line.split(' ') + end + end + return keys.map{|k| Key.load(k[1], k[0])} + end + + # + # takes a string with one or more ssh keys, one key per line, + # and returns a string that specified the ssh key algorithms + # that are supported by the keys, in order of preference. + # + # eg: ecdsa-sha2-nistp256,ssh-rsa,ssh-ed25519 + # + def self.supported_host_key_algorithms(string) + if string + self.parse_keys(string).map {|key| + key.type + }.join(',') + else + "" + end + 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_delegator :@key, :to_text, :to_text + + def public_key + Key.new(@key.public_key) + end + + def private_key + Key.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 + if self.filename + "%s %s %s (%s)" % [self.type, self.bits, self.fingerprint, File.basename(self.filename)] + else + "%s %s %s" % [self.type, self.bits, self.fingerprint] + end + 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? + return false if self.class != other_key.class + return self.to_text == other_key.to_text + 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 +end -- cgit v1.2.3 From 205b61dfe721e6d88fc06b050a0497eeb35f4e02 Mon Sep 17 00:00:00 2001 From: elijah Date: Thu, 21 Jul 2016 00:55:12 -0700 Subject: added 'leap vm' command --- lib/leap_cli/ssh/key.rb | 123 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 6 deletions(-) (limited to 'lib/leap_cli/ssh/key.rb') diff --git a/lib/leap_cli/ssh/key.rb b/lib/leap_cli/ssh/key.rb index ad1ecf15..76223b7e 100644 --- a/lib/leap_cli/ssh/key.rb +++ b/lib/leap_cli/ssh/key.rb @@ -2,12 +2,34 @@ # A wrapper around OpenSSL::PKey::RSA instances to provide a better api for # dealing with SSH keys. # -# NOTE: cipher 'ssh-ed25519' not supported yet because we are waiting +# NOTES: +# +# cipher 'ssh-ed25519' not supported yet because we are waiting # for support in Net::SSH # +# there are many ways to represent an SSH key, since SSH keys can be of +# a variety of types. +# +# To confuse matters more, there are multiple binary representations. +# So, for example, an RSA key has a native SSH representation +# (two bignums, e followed by n), and a DER representation. +# +# AWS uses fingerprints of the DER representation, but SSH typically reports +# fingerprints of the SSH representation. +# +# Also, SSH public key files are base64 encoded, but with whitespace removed +# so it all goes on one line. +# +# Some useful links: +# +# https://stackoverflow.com/questions/3162155/convert-rsa-public-key-to-rsa-der +# https://net-ssh.github.io/ssh/v2/api/classes/Net/SSH/Buffer.html +# https://serverfault.com/questions/603982/why-does-my-openssh-key-fingerprint-not-match-the-aws-ec2-console-keypair-finger +# require 'net/ssh' require 'forwardable' +require 'base64' module LeapCli module SSH @@ -72,6 +94,14 @@ module LeapCli public_key || private_key end + def self.my_public_keys + load_keys_from_paths File.join(ENV['HOME'], '.ssh', '*.pub') + end + + def self.provider_public_keys + load_keys_from_paths Path.named_path([:user_ssh, '*']) + end + # # Picks one key out of an array of keys that we think is the "best", # based on the order of preference in SUPPORTED_TYPES @@ -127,19 +157,29 @@ module LeapCli end end + private + + def self.load_keys_from_paths(key_glob) + keys = [] + Dir.glob(key_glob).each do |file| + key = Key.load(file) + if key && key.public? + keys << key + end + end + return keys + end + ## ## INSTANCE METHODS ## public - def initialize(rsa_key) - @key = rsa_key + def initialize(p_key) + @key = p_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 @@ -156,6 +196,70 @@ module LeapCli Key.new(@key.private_key) end + def private? + @key.respond_to?(:private?) ? @key.private? : @key.private_key? + end + + def public? + @key.respond_to?(:public?) ? @key.public? : @key.public_key? + end + + # + # three arguments: + # + # - digest: one of md5, sha1, sha256, etc. (default sha256) + # - encoding: either :hex (default) or :base64 + # - type: fingerprint type, either :ssh (default) or :der + # + # NOTE: + # + # * I am not sure how to make a fingerprint for OpenSSL::PKey::EC::Point + # + # * AWS reports fingerprints using MD5 digest for uploaded ssh keys, + # but SHA1 for keys it created itself. + # + # * Also, AWS fingerprints are digests on the DER encoding of the key. + # But standard SSH fingerprints are digests of SSH encoding of the key. + # + # * Other tools will sometimes display fingerprints in hex and sometimes + # in base64. Arrrgh. + # + def fingerprint(type: :ssh, digest: :sha256, encoding: :hex) + require 'digest' + + digest = digest.to_s.upcase + digester = case digest + when "MD5" then Digest::MD5.new + when "SHA1" then Digest::SHA1.new + when "SHA256" then Digest::SHA256.new + when "SHA384" then Digest::SHA384.new + when "SHA512" then Digest::SHA512.new + else raise ArgumentError, "digest #{digest} is unknown" + end + + keymatter = nil + if type == :der && @key.respond_to?(:to_der) + keymatter = @key.to_der + else + keymatter = self.raw_key.to_s + end + + fp = nil + if encoding == :hex + fp = digester.hexdigest(keymatter) + elsif encoding == :base64 + fp = Base64.encode64(digester.digest(keymatter)).sub(/=$/, '') + else + raise ArgumentError, "encoding #{encoding} not understood" + end + + if digest == "MD5" && encoding == :hex + return fp.scan(/../).join(':') + else + return fp + end + end + # # not sure if this will always work, but is seems to for now. # @@ -175,10 +279,17 @@ module LeapCli self.type + " " + self.key end + # + # base64 encoding of the key, with spaces removed. + # def key [Net::SSH::Buffer.from(:key, @key).to_s].pack("m*").gsub(/\s/, "") end + def raw_key + Net::SSH::Buffer.from(:key, @key) + end + def ==(other_key) return false if other_key.nil? return false if self.class != other_key.class -- cgit v1.2.3