diff options
author | Miklos Fazekas <mfazekas@szemafor.com> | 2018-03-22 14:09:57 +0100 |
---|---|---|
committer | Miklos Fazekas <mfazekas@szemafor.com> | 2018-03-22 14:25:11 +0100 |
commit | 2bc09b782dcd4988b09f40d75e6be6e8c271a240 (patch) | |
tree | f7ef5b2352e0b85fe5dd4b50a36465b9b0527906 | |
parent | a3a190d30e054b52cfcf072a6711eb9fc935e96a (diff) | |
download | net-ssh-2bc09b782dcd4988b09f40d75e6be6e8c271a240.tar.gz |
FingerprintHash SHA256|MD5
-rw-r--r-- | CHANGES.txt | 1 | ||||
-rw-r--r-- | lib/net/ssh.rb | 3 | ||||
-rw-r--r-- | lib/net/ssh/authentication/pub_key_fingerprint.rb | 21 | ||||
-rw-r--r-- | lib/net/ssh/config.rb | 2 | ||||
-rw-r--r-- | lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb | 81 | ||||
-rw-r--r-- | test/test_config.rb | 6 | ||||
-rw-r--r-- | test/transport/kex/test_diffie_hellman_group1_sha1.rb | 86 | ||||
-rw-r--r-- | test/transport/kex/test_ecdh_sha2_nistp256.rb | 4 |
8 files changed, 117 insertions, 87 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index d3ae6bf..69fbd91 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,7 @@ === 5.0.0.beta2 * Support for sha256 pubkey fingerprint [Tom Maher, #585] * Don't try to load default_keys if key_data option is used [Josh Larson, #589] + * Added fingerprint_hash defaulting to SHA256 as fingerprint format, and MD5 can be used as an option [Miklós Fazekas, #591] === 5.0.0.beta1 diff --git a/lib/net/ssh.rb b/lib/net/ssh.rb index 00425ae..becaca5 100644 --- a/lib/net/ssh.rb +++ b/lib/net/ssh.rb @@ -73,6 +73,7 @@ module Net max_win_size send_env use_agent number_of_password_prompts append_all_supported_algorithms non_interactive password_prompt agent_socket_factory minimum_dh_bits verify_host_key + fingerprint_hash ] # The standard means of starting a new SSH connection. When used with a @@ -200,7 +201,7 @@ module Net # given to +verify+ is a hash consisting of the +:key+, the +:key_blob+, # the +:fingerprint+ and the +:session+. Returning true accepts the host key, # returning false declines it and closes the connection. - # + # * :fingerprint_hash => 'MD5' or 'SHA256', defaults to 'SHA256' # If +user+ parameter is nil it defaults to USER from ssh_config, or # local username def self.start(host, user=nil, options={}, &block) diff --git a/lib/net/ssh/authentication/pub_key_fingerprint.rb b/lib/net/ssh/authentication/pub_key_fingerprint.rb index e06aeef..dc4ab6b 100644 --- a/lib/net/ssh/authentication/pub_key_fingerprint.rb +++ b/lib/net/ssh/authentication/pub_key_fingerprint.rb @@ -25,15 +25,18 @@ module Net # literal string +SHA256:+ is prepended. def fingerprint(algorithm='MD5') @fingerprint ||= {} - @fingerprint[algorithm] ||= - case algorithm.to_s.upcase - when 'MD5' - OpenSSL::Digest.hexdigest(algorithm, to_blob).scan(/../).join(":") - when 'SHA256' - "SHA256:#{Base64.encode64(OpenSSL::Digest.digest(algorithm, to_blob)).chomp.gsub(/=+\z/, '')}" - else - raise OpenSSL::Digest::DigestError, "unsupported ssh key digest #{algorithm}" - end + @fingerprint[algorithm] ||= PubKeyFingerprint.fingerprint(to_blob, algorithm) + end + + def self.fingerprint(blob, algorithm='MD5') + case algorithm.to_s.upcase + when 'MD5' + OpenSSL::Digest.hexdigest(algorithm, blob).scan(/../).join(":") + when 'SHA256' + "SHA256:#{Base64.encode64(OpenSSL::Digest.digest(algorithm, blob)).chomp.gsub(/=+\z/, '')}" + else + raise OpenSSL::Digest::DigestError, "unsupported ssh key digest #{algorithm}" + end end end end diff --git a/lib/net/ssh/config.rb b/lib/net/ssh/config.rb index 5689c74..6236831 100644 --- a/lib/net/ssh/config.rb +++ b/lib/net/ssh/config.rb @@ -34,6 +34,7 @@ module Net # * User => :user # * UserKnownHostsFile => :user_known_hosts_file # * NumberOfPasswordPrompts => :number_of_password_prompts + # * FingerprintHash => :fingerprint_hash # # Note that you will never need to use this class directly--you can control # whether the OpenSSH configuration files are read by passing the :config @@ -196,6 +197,7 @@ module Net globalknownhostsfile: :global_known_hosts_file, hostkeyalias: :host_key_alias, identityfile: :keys, + fingerprinthash: :fingerprint_hash, port: :port, user: :user, userknownhostsfile: :user_known_hosts_file diff --git a/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb b/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb index 41cde5c..8ae1c13 100644 --- a/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +++ b/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb @@ -4,9 +4,9 @@ require 'net/ssh/loggable' require 'net/ssh/transport/openssl' require 'net/ssh/transport/constants' -module Net - module SSH - module Transport +module Net + module SSH + module Transport module Kex # A key-exchange service implementing the "diffie-hellman-group1-sha1" @@ -14,7 +14,7 @@ module Net class DiffieHellmanGroup1SHA1 include Loggable include Constants - + # The value of 'P', as a string, in hexadecimal P_s = "FFFFFFFF" "FFFFFFFF" "C90FDAA2" "2168C234" + "C4C6628B" "80DC1CD1" "29024E08" "8A67CC74" + @@ -24,13 +24,13 @@ module Net "F44C42E9" "A637ED6B" "0BFF5CB6" "F406B7ED" + "EE386BFB" "5A899FA5" "AE9F2411" "7C4B1FE6" + "49286651" "ECE65381" "FFFFFFFF" "FFFFFFFF" - + # The radix in which P_s represents the value of P P_r = 16 - + # The group constant G = 2 - + attr_reader :p attr_reader :g attr_reader :digester @@ -38,7 +38,7 @@ module Net attr_reader :connection attr_reader :data attr_reader :dh - + # Create a new instance of the DiffieHellmanGroup1SHA1 algorithm. # The data is a Hash of symbols representing information # required by this algorithm, which was acquired during earlier @@ -46,16 +46,16 @@ module Net def initialize(algorithms, connection, data) @p = get_p @g = get_g - + @digester = OpenSSL::Digest::SHA1 @algorithms = algorithms @connection = connection - + @data = data.dup @dh = generate_key @logger = @data.delete(:logger) end - + # Perform the key-exchange for the given session, with the given # data. This method will return a hash consisting of the # following keys: @@ -72,33 +72,33 @@ module Net verify_server_key(result[:server_key]) session_id = verify_signature(result) confirm_newkeys - + return { session_id: session_id, server_key: result[:server_key], shared_secret: result[:shared_secret], hashing_algorithm: digester } end - + private - + def get_p OpenSSL::BN.new(P_s, P_r) end - + def get_g G end - + # Returns the DH key parameters for the current connection. def get_parameters [p, g] end - + # Returns the INIT/REPLY constants used by this algorithm. def get_message_types [KEXDH_INIT, KEXDH_REPLY] end - + # Build the signature buffer to use when verifying a signature from # the server. def build_signature_buffer(result) @@ -113,19 +113,19 @@ module Net result[:shared_secret] response end - + # Generate a DH key with a private key consisting of the given # number of bytes. def generate_key #:nodoc: dh = OpenSSL::PKey::DH.new - + if dh.respond_to?(:set_pqg) p, g = get_parameters dh.set_pqg(p, nil, g) else dh.p, dh.g = get_parameters end - + dh.generate_key! until dh.valid? && dh.priv_key.num_bytes == data[:need_bytes] if dh.respond_to?(:set_key) @@ -137,7 +137,7 @@ module Net end dh end - + # Send the KEXDH_INIT message, and expect the KEXDH_REPLY. Return the # resulting buffer. # @@ -145,22 +145,22 @@ module Net # the extracted values. def send_kexinit #:nodoc: init, reply = get_message_types - + # send the KEXDH_INIT message buffer = Net::SSH::Buffer.from(:byte, init, :bignum, dh.pub_key) connection.send_message(buffer) - + # expect the KEXDH_REPLY message buffer = connection.next_message raise Net::SSH::Exception, "expected REPLY" unless buffer.type == reply - + result = Hash.new - + result[:key_blob] = buffer.read_string result[:server_key] = Net::SSH::Buffer.new(result[:key_blob]).read_key result[:server_dh_pubkey] = buffer.read_bignum result[:shared_secret] = OpenSSL::BN.new(dh.compute_key(result[:server_dh_pubkey]), 2) - + sig_buffer = Net::SSH::Buffer.new(buffer.read_string) sig_type = sig_buffer.read_string if sig_type != algorithms.host_key @@ -169,10 +169,10 @@ module Net "'#{sig_type}' != '#{algorithms.host_key}'" end result[:server_sig] = sig_buffer.read_string - + return result end - + # Verify that the given key is of the expected type, and that it # really is the key for the session's host. Raise Net::SSH::Exception # if it is not. @@ -182,34 +182,35 @@ module Net "host key algorithm mismatch " + "'#{key.ssh_type}' != '#{algorithms.host_key}'" end - + blob, fingerprint = generate_key_fingerprint(key) - + raise Net::SSH::Exception, "host key verification failed" unless connection.host_key_verifier.verify(key: key, key_blob: blob, fingerprint: fingerprint, session: connection) end - + def generate_key_fingerprint(key) blob = Net::SSH::Buffer.from(:key, key).to_s - fingerprint = OpenSSL::Digest::MD5.hexdigest(blob).scan(/../).join(":") - + + fingerprint = Net::SSH::Authentication::PubKeyFingerprint.fingerprint(blob, @connection.options[:fingerprint_hash] || 'SHA256') + [blob, fingerprint] rescue ::Exception => e [nil, "(could not generate fingerprint: #{e.message})"] end - + # Verify the signature that was received. Raise Net::SSH::Exception # if the signature could not be verified. Otherwise, return the new # session-id. def verify_signature(result) #:nodoc: response = build_signature_buffer(result) - + hash = @digester.digest(response.to_s) - + raise Net::SSH::Exception, "could not verify server signature" unless result[:server_key].ssh_do_verify(result[:server_sig], hash) - + return hash end - + # Send the NEWKEYS message, and expect the NEWKEYS message in # reply. def confirm_newkeys #:nodoc: @@ -217,7 +218,7 @@ module Net response = Net::SSH::Buffer.new response.write_byte(NEWKEYS) connection.send_message(response) - + # wait for the server's NEWKEYS message buffer = connection.next_message raise Net::SSH::Exception, "expected NEWKEYS" unless buffer.type == NEWKEYS diff --git a/test/test_config.rb b/test/test_config.rb index 98f1454..ca27cdc 100644 --- a/test/test_config.rb +++ b/test/test_config.rb @@ -124,7 +124,8 @@ class TestConfig < NetSSHTest 'sendenv' => "LC_*", 'numberofpasswordprompts' => '123', 'serveraliveinterval' => '2', - 'serveralivecountmax' => '4' + 'serveralivecountmax' => '4', + 'fingerprinthash' => 'MD5' } net_ssh = Net::SSH::Config.translate(open_ssh) @@ -145,6 +146,7 @@ class TestConfig < NetSSHTest assert_equal 123, net_ssh[:number_of_password_prompts] assert_equal 4, net_ssh[:keepalive_maxcount] assert_equal 2, net_ssh[:keepalive_interval] + assert_equal 'MD5', net_ssh[:fingerprint_hash] assert_equal true, net_ssh[:keepalive] end @@ -302,7 +304,7 @@ class TestConfig < NetSSHTest assert_equal(original_default_auth_methods, Net::SSH::Config.default_auth_methods) end - + def test_load_with_match_block config = Net::SSH::Config.load(config(:match), "test.host") net_ssh = Net::SSH::Config.translate(config) diff --git a/test/transport/kex/test_diffie_hellman_group1_sha1.rb b/test/transport/kex/test_diffie_hellman_group1_sha1.rb index fd96132..d352618 100644 --- a/test/transport/kex/test_diffie_hellman_group1_sha1.rb +++ b/test/transport/kex/test_diffie_hellman_group1_sha1.rb @@ -1,22 +1,22 @@ -require 'common' +require_relative '../../common' require 'net/ssh/transport/kex/diffie_hellman_group1_sha1' require 'ostruct' -module Transport +module Transport module Kex class TestDiffieHellmanGroup1SHA1 < NetSSHTest include Net::SSH::Transport::Constants - + def setup - @dh_options = @dh = @algorithms = @connection = @server_key = + @dh_options = @dh = @algorithms = @connection = @server_key = @packet_data = @shared_secret = nil end - + def digest_type OpenSSL::Digest::SHA1 end - + def test_exchange_keys_should_return_expected_results_when_successful result = exchange! assert_equal session_id, result[:session_id] @@ -24,48 +24,68 @@ module Transport assert_equal shared_secret, result[:shared_secret] assert_equal digest_type, result[:hashing_algorithm] end - + def test_exchange_keys_with_unverifiable_host_should_raise_exception connection.verifier { false } assert_raises(Net::SSH::Exception) { exchange! } end - + def test_exchange_keys_with_signature_key_type_mismatch_should_raise_exception assert_raises(Net::SSH::Exception) { exchange! key_type: "ssh-dss" } end - + def test_exchange_keys_with_host_key_type_mismatch_should_raise_exception algorithms host_key: "ssh-dss" assert_raises(Net::SSH::Exception) { exchange! key_type: "ssh-dss" } end - + def test_exchange_keys_when_server_signature_could_not_be_verified_should_raise_exception @signature = "1234567890" assert_raises(Net::SSH::Exception) { exchange! } end - + def test_exchange_keys_should_pass_expected_parameters_to_host_key_verifier verified = false connection.verifier do |data| verified = true assert_equal server_key.to_blob, data[:key].to_blob - + blob = b(:key, data[:key]).to_s - fingerprint = OpenSSL::Digest::MD5.hexdigest(blob).scan(/../).join(":") - + fingerprint = "SHA256:#{Base64.encode64(OpenSSL::Digest.digest('SHA256', blob)).chomp.gsub(/=+\z/, '')}" + assert_equal blob, data[:key_blob] assert_equal fingerprint, data[:fingerprint] assert_equal connection, data[:session] - + true end - + assert_nothing_raised { exchange! } assert verified end - + + def test_exchange_keys_can_use_md5_fingerprints + verified = false + connection.options[:fingerprint_hash] = 'MD5' + connection.verifier do |data| + verified = true + assert_equal server_key.to_blob, data[:key].to_blob + + blob = b(:key, data[:key]).to_s + + fingerprint = OpenSSL::Digest.hexdigest('MD5', blob).scan(/../).join(":") + + assert_equal fingerprint, data[:fingerprint] + + true + end + + assert_nothing_raised { exchange! } + assert verified + end + private - + def exchange!(options={}) connection.expect do |t, buffer| assert_equal KEXDH_INIT, buffer.type @@ -76,51 +96,51 @@ module Transport t2.return(NEWKEYS) end end - + dh.exchange_keys end - + def dh_options(options={}) @dh_options = options end - + def dh @dh ||= subject.new(algorithms, connection, packet_data.merge(need_bytes: 20).merge(@dh_options || {})) end - + def algorithms(options={}) @algorithms ||= OpenStruct.new(host_key: options[:host_key] || "ssh-rsa") end - + def connection @connection ||= MockTransport.new end - + def subject Net::SSH::Transport::Kex::DiffieHellmanGroup1SHA1 end - + # 512 bits is the smallest possible key that will work with this, so # we use it for speed reasons def server_key(bits=512) @server_key ||= OpenSSL::PKey::RSA.new(bits) end - + def packet_data @packet_data ||= { client_version_string: "client version string", server_version_string: "server version string", server_algorithm_packet: "server algorithm packet", client_algorithm_packet: "client algorithm packet" } end - + def server_dh_pubkey @server_dh_pubkey ||= bn(1234567890) end - + def shared_secret @shared_secret ||= OpenSSL::BN.new(dh.dh.compute_key(server_dh_pubkey), 2) end - + def session_id @session_id ||= begin buffer = Net::SSH::Buffer.from(:string, packet_data[:client_version_string], @@ -134,19 +154,19 @@ module Transport OpenSSL::Digest::SHA1.digest(buffer.to_s) end end - + def signature @signature ||= server_key.ssh_do_sign(session_id) end - + def bn(number, base=10) OpenSSL::BN.new(number.to_s, base) end - + def b(*args) Net::SSH::Buffer.from(*args) end end end -end
\ No newline at end of file +end diff --git a/test/transport/kex/test_ecdh_sha2_nistp256.rb b/test/transport/kex/test_ecdh_sha2_nistp256.rb index 5d0a6b1..c516565 100644 --- a/test/transport/kex/test_ecdh_sha2_nistp256.rb +++ b/test/transport/kex/test_ecdh_sha2_nistp256.rb @@ -3,7 +3,7 @@ require 'openssl' unless defined?(OpenSSL::PKey::EC) puts "Skipping tests for ecdh-sha2-nistp256 key exchange" else - require 'common' + require_relative '../../common' require 'transport/kex/test_diffie_hellman_group1_sha1' require 'net/ssh/transport/kex/ecdh_sha2_nistp256' require 'ostruct' @@ -53,7 +53,7 @@ else assert_equal server_host_key.to_blob, data[:key].to_blob blob = b(:key, data[:key]).to_s - fingerprint = OpenSSL::Digest::MD5.hexdigest(blob).scan(/../).join(":") + fingerprint = "SHA256:#{Base64.encode64(OpenSSL::Digest.digest('SHA256', blob)).chomp.gsub(/=+\z/, '')}" assert_equal blob, data[:key_blob] assert_equal fingerprint, data[:fingerprint] |