summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiklos Fazekas <mfazekas@szemafor.com>2018-03-22 14:09:57 +0100
committerMiklos Fazekas <mfazekas@szemafor.com>2018-03-22 14:25:11 +0100
commit2bc09b782dcd4988b09f40d75e6be6e8c271a240 (patch)
treef7ef5b2352e0b85fe5dd4b50a36465b9b0527906
parenta3a190d30e054b52cfcf072a6711eb9fc935e96a (diff)
downloadnet-ssh-2bc09b782dcd4988b09f40d75e6be6e8c271a240.tar.gz
FingerprintHash SHA256|MD5
-rw-r--r--CHANGES.txt1
-rw-r--r--lib/net/ssh.rb3
-rw-r--r--lib/net/ssh/authentication/pub_key_fingerprint.rb21
-rw-r--r--lib/net/ssh/config.rb2
-rw-r--r--lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb81
-rw-r--r--test/test_config.rb6
-rw-r--r--test/transport/kex/test_diffie_hellman_group1_sha1.rb86
-rw-r--r--test/transport/kex/test_ecdh_sha2_nistp256.rb4
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]