diff options
author | Jamis Buck <jamis@37signals.com> | 2007-08-09 02:32:27 +0000 |
---|---|---|
committer | Jamis Buck <jamis@37signals.com> | 2007-08-09 02:32:27 +0000 |
commit | ca4605cb6a7173408355ced704cb6377a3d920fd (patch) | |
tree | 1977aae92bdc7df61dcec541a78fc7206b84b220 | |
parent | 6c1665335aeeb72f68110a511057ce658a2fb519 (diff) | |
download | net-ssh-ca4605cb6a7173408355ced704cb6377a3d920fd.tar.gz |
host key verification
git-svn-id: http://svn.jamisbuck.org/net-ssh/branches/v2@138 1d2a57f2-1ded-0310-ad52-83097a15a5de
-rw-r--r-- | lib/net/ssh/errors.rb | 37 | ||||
-rw-r--r-- | lib/net/ssh/known_hosts.rb | 38 | ||||
-rw-r--r-- | lib/net/ssh/transport/algorithms.rb | 2 | ||||
-rw-r--r-- | lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb | 7 | ||||
-rw-r--r-- | lib/net/ssh/transport/session.rb | 41 | ||||
-rw-r--r-- | lib/net/ssh/verifiers/lenient.rb | 23 | ||||
-rw-r--r-- | lib/net/ssh/verifiers/null.rb | 11 | ||||
-rw-r--r-- | lib/net/ssh/verifiers/strict.rb | 43 |
8 files changed, 192 insertions, 10 deletions
diff --git a/lib/net/ssh/errors.rb b/lib/net/ssh/errors.rb index 956b951..724ff67 100644 --- a/lib/net/ssh/errors.rb +++ b/lib/net/ssh/errors.rb @@ -4,4 +4,41 @@ module Net; module SSH class AuthenticationFailed < Exception; end class Disconnect < Exception; end + + # Raised when the cached key for a particular host does not match the + # key given by the host, which can be indicative of a man-in-the-middle + # attack. When rescuing this exception, you can inspect the key fingerprint + # and, if you want to proceed anyway, simply call the remember_host! + # method on the exception, and then retry. + class HostKeyMismatch < Exception + attr_writer :callback, :data + + def [](key) + @data[key] + end + + def fingerprint + @data[:fingerprint] + end + + def host + @data[:peer][:host] + end + + def port + @data[:peer][:port] + end + + def ip + @data[:peer][:ip] + end + + def key + @data[:key] + end + + def remember_host! + @callback.call + end + end end; end
\ No newline at end of file diff --git a/lib/net/ssh/known_hosts.rb b/lib/net/ssh/known_hosts.rb index 4b7cbc9..d3fe647 100644 --- a/lib/net/ssh/known_hosts.rb +++ b/lib/net/ssh/known_hosts.rb @@ -5,8 +5,40 @@ module Net; module SSH class KnownHosts attr_reader :source - def self.search_in(files, host) - files.map { |file| KnownHosts.new(file).keys_for(host) }.flatten + class <<self + def search_for(host) + search_in(hostfiles, host) + end + + def search_in(files, host) + files.map { |file| KnownHosts.new(file).keys_for(host) }.flatten + end + + def hostfiles + @hostfiles ||= [ + "#{home_directory}/.ssh/known_hosts", + "#{home_directory}/.ssh/known_hosts2", + "/etc/ssh/ssh_known_hosts", + "/etc/ssh_ssh_known_hosts2" + ] + end + + def home_directory + ENV['HOME'] || + (ENV['HOMEPATH'] && "#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}") || + "/" + end + + def add(host, key) + hostfiles.each do |file| + begin + KnownHosts.new(file).add(host, key) + return + rescue SystemCallError + # try the next hostfile + end + end + end end def initialize(source) @@ -15,6 +47,7 @@ module Net; module SSH def keys_for(host) keys = [] + return keys unless File.readable?(source) File.open(source) do |file| scanner = StringScanner.new("") @@ -44,5 +77,6 @@ module Net; module SSH file.puts "#{host} #{key.ssh_type} #{blob}" end end + end end; end
\ No newline at end of file diff --git a/lib/net/ssh/transport/algorithms.rb b/lib/net/ssh/transport/algorithms.rb index 42b03d8..65b977b 100644 --- a/lib/net/ssh/transport/algorithms.rb +++ b/lib/net/ssh/transport/algorithms.rb @@ -82,7 +82,7 @@ module Net; module SSH; module Transport # make sure the host keys are specified in preference order, where any # existing known key for the host has preference. - existing_keys = KnownHosts.search_in(["#{ENV['HOME']}/.ssh/known_hosts"], session.host_as_string) + existing_keys = KnownHosts.search_for(session.host_as_string) host_keys = existing_keys.map { |key| key.ssh_type }.uniq @algorithms[:host_key].each do |name| host_keys << name unless host_keys.include?(name) 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 90654f4..41414a3 100644 --- a/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +++ b/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb @@ -163,10 +163,9 @@ module Net; module SSH; module Transport; module Kex blob, fingerprint = generate_key_fingerprint(key) - warn "FIXME host key verification" - #unless @host_key_verifier.verify(:key => key, :key_blob => blob, :fingerprint => fingerprint, :peer => session.peer) - # raise Net::SSH::Exception, "host key verification failed" - #end + unless connection.host_key_verifier.verify(:key => key, :key_blob => blob, :fingerprint => fingerprint, :session => connection) + raise Net::SSH::Exception, "host key verification failed" + end end def generate_key_fingerprint(key) diff --git a/lib/net/ssh/transport/session.rb b/lib/net/ssh/transport/session.rb index 835677f..2984872 100644 --- a/lib/net/ssh/transport/session.rb +++ b/lib/net/ssh/transport/session.rb @@ -8,6 +8,9 @@ require 'net/ssh/transport/algorithms' require 'net/ssh/transport/constants' require 'net/ssh/transport/packet_stream' require 'net/ssh/transport/server_version' +require 'net/ssh/verifiers/null' +require 'net/ssh/verifiers/strict' +require 'net/ssh/verifiers/lenient' module Net; module SSH; module Transport class Session @@ -20,6 +23,7 @@ module Net; module SSH; module Transport attr_reader :header attr_reader :server_version attr_reader :algorithms + attr_reader :host_key_verifier def initialize(host, options={}) self.logger = options[:logger] @@ -33,14 +37,18 @@ module Net; module SSH; module Transport @socket.extend(PacketStream) @socket.logger = @logger + @host_key_verifier = select_host_key_verifier(options[:paranoid]) + @server_version = ServerVersion.new(socket, logger) @algorithms = Algorithms.negotiate_via(self) end def host_as_string - string = "#{host}" - string = "[#{string}]:#{port}" if port != DEFAULT_PORT - string + @host_as_string ||= begin + string = "#{host}" + string = "[#{string}]:#{port}" if port != DEFAULT_PORT + string + end end def close @@ -54,6 +62,14 @@ module Net; module SSH; module Transport msg end + def peer + @peer ||= begin + addr = @socket.getpeername + ip_address = Socket.getnameinfo(addr, Socket::NI_NUMERICHOST | Socket::NI_NUMERICSERV).first + { :ip => ip_address, :port => @port.to_i, :host => @host, :canonized => host_as_string } + end + end + def next_message poll_message(:block) end @@ -92,5 +108,24 @@ module Net; module SSH; module Transport def send_message(message) socket.send_packet(message) end + + private + + def select_host_key_verifier(paranoid) + case paranoid + when true, nil then + Net::SSH::Verifiers::Lenient.new + when false then + Net::SSH::Verifiers::Null.new + when :very then + Net::SSH::Verifiers::Strict.new + else + if paranoid.respond_to?(:verify) + paranoid + else + raise ArgumentError, "argument to :paranoid is not valid: #{paranoid.inspect}" + end + end + end end end; end; end
\ No newline at end of file diff --git a/lib/net/ssh/verifiers/lenient.rb b/lib/net/ssh/verifiers/lenient.rb new file mode 100644 index 0000000..da67fe4 --- /dev/null +++ b/lib/net/ssh/verifiers/lenient.rb @@ -0,0 +1,23 @@ +require 'net/ssh/verifiers/strict' + +module Net; module SSH; module Verifiers + + class Lenient < Strict + def verify(arguments) + return true if tunnelled?(arguments) + super + end + + private + + # A connection is potentially being tunnelled if the port is not 22, + # and the ip refers to the localhost. + def tunnelled?(args) + return false if args[:session].port == Net::SSH::Transport::Session::DEFAULT_PORT + + ip = args[:session].peer[:ip] + return ip == "127.0.0.1" || ip == "::1" + end + end + +end; end; end
\ No newline at end of file diff --git a/lib/net/ssh/verifiers/null.rb b/lib/net/ssh/verifiers/null.rb new file mode 100644 index 0000000..a9d8483 --- /dev/null +++ b/lib/net/ssh/verifiers/null.rb @@ -0,0 +1,11 @@ +module Net; module SSH; module Verifiers + + # The NullHostKeyVerifier simply allows every key it sees, without + # bothering to verify. This is simple, but is not particularly secure. + class NullHostKeyVerifier + def verify(arguments) + true + end + end + +end; end; end
\ No newline at end of file diff --git a/lib/net/ssh/verifiers/strict.rb b/lib/net/ssh/verifiers/strict.rb new file mode 100644 index 0000000..6a9aed3 --- /dev/null +++ b/lib/net/ssh/verifiers/strict.rb @@ -0,0 +1,43 @@ +require 'net/ssh/errors' +require 'net/ssh/known_hosts' + +module Net; module SSH; module Verifiers + + class Strict + def verify(arguments) + host = arguments[:session].host_as_string + matches = Net::SSH::KnownHosts.search_for(host) + + # we've never seen this host before, so just automatically add the key. + # not the most secure option (since the first hit might be the one that + # is hacked), but since almost nobody actually compares the key + # fingerprint, this is a reasonable compromise between usability and + # security. + if matches.empty? + Net::SSH::KnownHosts.add(host, arguments[:key]) + return true + end + + # If we found any matches, check to see that the key type and + # blob also match. + found = matches.any? do |key| + key.ssh_type == arguments[:key].ssh_type && + key.to_blob == arguments[:key].to_blob + end + + # If a match was found, return true. Otherwise, raise an exception + # indicating that the key was not recognized. + found || process_cache_miss(host, arguments) + end + + private + + def process_cache_miss(host, args) + exception = HostKeyMismatch.new("fingerprint #{args[:fingerprint]} does not match for #{host.join(',')}") + exception.data = args + exception.callback = Proc.new { Net::SSH::KnownHosts.add(host, args[:key]) } + raise exception + end + end + +end; end; end
\ No newline at end of file |