summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJamis Buck <jamis@37signals.com>2007-08-09 02:32:27 +0000
committerJamis Buck <jamis@37signals.com>2007-08-09 02:32:27 +0000
commitca4605cb6a7173408355ced704cb6377a3d920fd (patch)
tree1977aae92bdc7df61dcec541a78fc7206b84b220
parent6c1665335aeeb72f68110a511057ce658a2fb519 (diff)
downloadnet-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.rb37
-rw-r--r--lib/net/ssh/known_hosts.rb38
-rw-r--r--lib/net/ssh/transport/algorithms.rb2
-rw-r--r--lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb7
-rw-r--r--lib/net/ssh/transport/session.rb41
-rw-r--r--lib/net/ssh/verifiers/lenient.rb23
-rw-r--r--lib/net/ssh/verifiers/null.rb11
-rw-r--r--lib/net/ssh/verifiers/strict.rb43
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