net-ssh 1.0.10 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/bin/rb-keygen +210 -0
  2. data/doc/manual-html/chapter-1.html +2 -2
  3. data/doc/manual-html/chapter-2.html +13 -2
  4. data/doc/manual-html/chapter-3.html +2 -2
  5. data/doc/manual-html/chapter-4.html +2 -2
  6. data/doc/manual-html/chapter-5.html +2 -2
  7. data/doc/manual-html/chapter-6.html +2 -2
  8. data/doc/manual-html/chapter-7.html +2 -2
  9. data/doc/manual-html/index.html +5 -5
  10. data/doc/manual/manual.yml +3 -3
  11. data/doc/manual/parts/0007.txt +2 -0
  12. data/doc/manual/parts/0008.txt +2 -0
  13. data/examples/auth-forward.rb +41 -0
  14. data/lib/net/ssh/connection/driver.rb +15 -1
  15. data/lib/net/ssh/errors.rb +36 -0
  16. data/lib/net/ssh/host-key-verifier.rb +108 -0
  17. data/lib/net/ssh/lenient-host-key-verifier.rb +25 -0
  18. data/lib/net/ssh/null-host-key-verifier.rb +14 -0
  19. data/lib/net/ssh/service/agentforward/driver.rb +78 -0
  20. data/lib/net/ssh/service/agentforward/services.rb +41 -0
  21. data/lib/net/ssh/service/services.rb +2 -0
  22. data/lib/net/ssh/session.rb +47 -1
  23. data/lib/net/ssh/transport/kex/dh.rb +20 -2
  24. data/lib/net/ssh/transport/kex/services.rb +2 -0
  25. data/lib/net/ssh/transport/session.rb +10 -0
  26. data/lib/net/ssh/userauth/agent.rb +11 -0
  27. data/lib/net/ssh/version.rb +2 -2
  28. data/test/connection/tc_integration.rb +4 -2
  29. data/test/service/agentforward/tc_driver.rb +138 -0
  30. data/test/service/process/tc_integration.rb +2 -0
  31. data/test/tc_integration.rb +4 -3
  32. data/test/transport/kex/tc_dh.rb +6 -0
  33. data/test/transport/kex/tc_dh_gex.rb +1 -0
  34. data/test/transport/tc_integration.rb +6 -1
  35. data/test/userauth/tc_integration.rb +2 -0
  36. metadata +70 -60
@@ -23,5 +23,41 @@ module Net
23
23
  # Raised when user authentication failed.
24
24
  class AuthenticationFailed < Exception; end
25
25
 
26
+ # Raised when the cached key for a particular host does not match the
27
+ # key given by the host, which can be indicative of a man-in-the-middle
28
+ # attack. When rescuing this exception, you can inspect the key fingerprint
29
+ # and, if you want to proceed anyway, simply call the remember_host!
30
+ # method on the exception, and then retry.
31
+ class HostKeyMismatch < Exception
32
+ attr_writer :callback, :data
33
+
34
+ def [](key)
35
+ @data[key]
36
+ end
37
+
38
+ def fingerprint
39
+ @data[:fingerprint]
40
+ end
41
+
42
+ def host
43
+ @data[:peer][:host]
44
+ end
45
+
46
+ def port
47
+ @data[:peer][:port]
48
+ end
49
+
50
+ def ip
51
+ @data[:peer][:ip]
52
+ end
53
+
54
+ def key
55
+ @data[:key]
56
+ end
57
+
58
+ def remember_host!
59
+ @callback.call
60
+ end
61
+ end
26
62
  end
27
63
  end
@@ -0,0 +1,108 @@
1
+ require 'net/ssh/errors'
2
+
3
+ module Net
4
+ module SSH
5
+
6
+ class HostKeyVerifier
7
+ def verify(arguments)
8
+ # first, find any matches on hostname+port
9
+ matches = keys.select do |item|
10
+ host = item[:host] || arguments[:peer][:host]
11
+ ip = item[:ip] || arguments[:peer][:ip]
12
+ port = item[:port] || arguments[:peer][:port]
13
+
14
+ host == arguments[:peer][:host] &&
15
+ ip == arguments[:peer][:ip] &&
16
+ port == arguments[:peer][:port]
17
+ end
18
+
19
+ # we've never seen this host before, so just automatically add the key.
20
+ # not the most secure option (since the first hit might be the one that
21
+ # is hacked), but since almost nobody actually compares the key
22
+ # fingerprint, this is a reasonable compromise between usability and
23
+ # security.
24
+ if matches.empty?
25
+ add_key(arguments)
26
+ return true
27
+ end
28
+
29
+ # If we found any matches, check to see that the key type and
30
+ # blob also match.
31
+ found = matches.any? do |item|
32
+ item[:type] == arguments[:key].ssh_type &&
33
+ item[:key] == arguments[:key_blob]
34
+ end
35
+
36
+ # If a match was found, return true. Otherwise, raise an exception
37
+ # indicating that the key was not recognized.
38
+ found || process_cache_miss(arguments)
39
+ end
40
+
41
+ private
42
+
43
+ def process_cache_miss(args)
44
+ exception = HostKeyMismatch.new("fingerprint #{args[:fingerprint]} does not match for #{args[:peer][:host]}")
45
+ exception.data = args
46
+ exception.callback = Proc.new { add_key(args) }
47
+ raise exception
48
+ end
49
+
50
+ def home_directory
51
+ ENV['HOME'] ||
52
+ (ENV['HOMEPATH'] && "#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}") ||
53
+ "/"
54
+ end
55
+
56
+ def user_key_file
57
+ @user_key_file ||= "#{home_directory}/.ssh/known_hosts"
58
+ end
59
+
60
+ def add_key(args)
61
+ keys << { :host => args[:peer][:host], :port => args[:peer][:port], :ip => args[:peer][:ip], :type => args[:key].ssh_type, :key => args[:key_blob] }
62
+
63
+ key_directory = File.dirname(user_key_file)
64
+ File.mkdir(key_directory, :mode => 0700) if !File.exists?(key_directory)
65
+
66
+ File.open(user_key_file, "a") do |f|
67
+ host = args[:peer][:host]
68
+ host = "[#{host}]:#{args[:peer][:port]}" if host && args[:peer][:port] != 22
69
+
70
+ ip = args[:peer][:ip]
71
+ ip = nil if ip == "127.0.0.1" || ip == "::1"
72
+
73
+ host = [host, ip].compact.join(",")
74
+ f.puts "%s %s %s" % [host, args[:key].ssh_type, [args[:key_blob]].pack("m*").gsub(/\s/, "")]
75
+ end
76
+ end
77
+
78
+ def keys
79
+ @keys ||= begin
80
+ list = []
81
+ list.concat(load_keys_from(user_key_file)) if File.exist?(user_key_file)
82
+ list
83
+ end
84
+ end
85
+
86
+ def load_keys_from(path)
87
+ File.readlines(path).map do |line|
88
+ host, type, key = line.chomp.split
89
+ host, address = host.split(/,/)
90
+
91
+ if address.nil? && host =~ /^\d+\.\d+\.\d+\.\d+$/
92
+ host, address = address, host
93
+ end
94
+
95
+ if host
96
+ host, port = host.split(/:/, 2)
97
+ host = host.gsub(/[\[\]]/, "")
98
+ end
99
+
100
+ key = key.unpack("m*").first
101
+
102
+ { :host => host, :ip => address, :port => port, :type => type, :key => key }
103
+ end
104
+ end
105
+ end
106
+
107
+ end
108
+ end
@@ -0,0 +1,25 @@
1
+ require 'net/ssh/host-key-verifier'
2
+
3
+ module Net
4
+ module SSH
5
+
6
+ class LenientHostKeyVerifier < HostKeyVerifier
7
+ def verify(arguments)
8
+ return true if tunnelled?(arguments)
9
+ super
10
+ end
11
+
12
+ private
13
+
14
+ # A connection is potentially being tunnelled if the port is not 22,
15
+ # and the ip refers to the localhost.
16
+ def tunnelled?(args)
17
+ return false if args[:peer][:port].to_i == 22
18
+
19
+ ip = args[:peer][:ip]
20
+ return ip == "127.0.0.1" || ip == "::1"
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ module Net
2
+ module SSH
3
+
4
+ # The NullHostKeyVerifier simply allows every key it sees, without
5
+ # bothering to verify. This mimics the pre-1.1 behavior of Net::SSH, but
6
+ # is not very secure.
7
+ class NullHostKeyVerifier
8
+ def verify(arguments)
9
+ true
10
+ end
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,78 @@
1
+ #--
2
+ # =============================================================================
3
+ # Copyright (c) 2007, Chris Andrews (chris@nodnol.org),
4
+ # Jamis Buck (jgb3@email.byu.edu)
5
+ # All rights reserved.
6
+ #
7
+ # This source file is distributed as part of the Net::SSH Secure Shell Client
8
+ # library for Ruby. This file (and the library as a whole) may be used only as
9
+ # allowed by either the BSD license, or the Ruby license (or, by association
10
+ # with the Ruby license, the GPL). See the "doc" subdirectory of the Net::SSH
11
+ # distribution for the texts of these licenses.
12
+ # -----------------------------------------------------------------------------
13
+ # net-ssh website : http://net-ssh.rubyforge.org
14
+ # project website: http://rubyforge.org/projects/net-ssh
15
+ # =============================================================================
16
+ #++
17
+
18
+ module Net
19
+ module SSH
20
+ module Service
21
+ module AgentForward
22
+
23
+ class Driver
24
+
25
+ def initialize( connection, buffers, log, agent )
26
+ @connection = connection
27
+ @buffers = buffers
28
+ @log = log
29
+ @agent = agent
30
+ @data = ''
31
+
32
+ @connection.add_channel_open_handler(
33
+ "auth-agent@openssh.com", &method(:do_open_channel) )
34
+ end
35
+
36
+ def request
37
+ @connection.channel_request( 'auth-agent-req@openssh.com' )
38
+ end
39
+
40
+ def do_open_channel( connection, channel, data )
41
+ channel.on_data &method(:do_data)
42
+ end
43
+
44
+ # handle CHANNEL_DATA packets received on the agent-forward
45
+ # channel - pass complete received packets to the agent.
46
+ def do_data( channel, data )
47
+ @data = @data + data
48
+ reply = call_agent
49
+ if reply
50
+ channel.send_data reply
51
+ @data = ''
52
+ end
53
+ end
54
+
55
+ # Called if we have any data to forward to the
56
+ # agent. Examines the accumulated data to see if we have a
57
+ # complete packet, based on the length field (the first four
58
+ # bytes as a network long).
59
+ def call_agent
60
+ # if we have enough data to check the length of this packet
61
+ if @data.length >= 4
62
+ packet_length = @data[0..3].unpack('N').first
63
+ # send the complete packet to the agent and read the
64
+ # response
65
+ if @data.length == (4 + packet_length)
66
+ @agent.send_raw_packet @data
67
+ buffer = @agent.read_raw_packet
68
+ end
69
+ end
70
+ buffer
71
+ end
72
+
73
+ end
74
+
75
+ end # AgentForward
76
+ end # Service
77
+ end # SSH
78
+ end # Net
@@ -0,0 +1,41 @@
1
+ #--
2
+ # =============================================================================
3
+ # Copyright (c) 2007, Chris Andrews (chris@nodnol.org),
4
+ # Jamis Buck (jgb3@email.byu.edu)
5
+ # All rights reserved.
6
+ #
7
+ # This source file is distributed as part of the Net::SSH Secure Shell Client
8
+ # library for Ruby. This file (and the library as a whole) may be used only as
9
+ # allowed by either the BSD license, or the Ruby license (or, by association
10
+ # with the Ruby license, the GPL). See the "doc" subdirectory of the Net::SSH
11
+ # distribution for the texts of these licenses.
12
+ # -----------------------------------------------------------------------------
13
+ # net-ssh website : http://net-ssh.rubyforge.org
14
+ # project website: http://rubyforge.org/projects/net-ssh
15
+ # =============================================================================
16
+ #++
17
+
18
+ module Net
19
+ module SSH
20
+ module Service
21
+ module AgentForward
22
+
23
+ def register_services( container )
24
+
25
+ container.namespace_define :agentforward do |ns|
26
+ ns.driver :model => :singleton_deferred do |c,p|
27
+ require 'net/ssh/service/agentforward/driver'
28
+ Driver.new( c[:connection][:driver],
29
+ c[:transport][:buffers],
30
+ c[:log_for, p],
31
+ c[:userauth].agent)
32
+ end
33
+ end
34
+
35
+ end
36
+ module_function :register_services
37
+
38
+ end
39
+ end
40
+ end
41
+ end
@@ -34,12 +34,14 @@ module Net
34
34
  ns.require "net/ssh/service/forward/services", "#{self}::Forward"
35
35
  ns.require "net/ssh/service/process/services", "#{self}::Process"
36
36
  ns.require "net/ssh/service/shell/services", "#{self}::Shell"
37
+ ns.require "net/ssh/service/agentforward/services", "#{self}::AgentForward"
37
38
  end
38
39
 
39
40
  # Add the services to the services hash.
40
41
  container.services[ :forward ] = container.service.forward.driver
41
42
  container.services[ :process ] = container.service.process.driver
42
43
  container.services[ :shell ] = container.service.shell.driver
44
+ container.services[ :agentforward ] = container.service.agentforward.driver
43
45
 
44
46
  # Register the external services and add them to the collection of
45
47
  # known services.
@@ -82,6 +82,13 @@ module Net
82
82
  # :warn).
83
83
  # * :log (the name of the file, or the IO object, to which messages will
84
84
  # be logged. Defaults to STDERR.)
85
+ # * :forward_agent (true or false, whether or not to forward requests
86
+ # for the authentication agent. Defaults to false.)
87
+ # * :paranoid (either false, in which case server fingerprints are not
88
+ # verified, true, in which case they are verified and mismatches result
89
+ # in a warning and a prompt, or an object responding to :allow?, which
90
+ # will be invoked and should return true or false for whether or not
91
+ # to allow the connection. Defaults to true.)
85
92
  #
86
93
  # Also, any options recognized by Net::SSH::Transport::Session may be
87
94
  # given, and will be passed through to initialize the transport session.
@@ -92,6 +99,8 @@ module Net
92
99
  # returned (and must be closed explicitly).
93
100
  def initialize( *args )
94
101
  @open = false
102
+ @agent_forwarded = false
103
+
95
104
  process_arguments( *args )
96
105
 
97
106
  @registry.define do |b|
@@ -103,6 +112,8 @@ module Net
103
112
  b.userauth_host_keys { @host_keys }
104
113
  b.userauth_method_order { @auth_methods }
105
114
 
115
+ b.host_key_verifier { @host_key_verifier }
116
+
106
117
  # Register myself with the registry, so that other services may
107
118
  # access me.
108
119
  b.session( :pipeline => [] ) { self }
@@ -152,6 +163,17 @@ module Net
152
163
  sanity_check
153
164
  channel = @connection.open_channel( type, data )
154
165
  channel.on_confirm_open(&block)
166
+
167
+ # If we have an agent, and agent-forwarding is enabled, set up
168
+ # the forwarding. Do this once only, after the first channel
169
+ # is opened.
170
+ if @forward_agent && @registry[:userauth].agent
171
+ unless @agent_forwarded
172
+ agentforward.request
173
+ @agent_forwarded = true
174
+ end
175
+ end
176
+
155
177
  channel
156
178
  end
157
179
 
@@ -218,7 +240,9 @@ module Net
218
240
  @keys = @options[ :keys ]
219
241
  @host_keys = @options[ :host_keys ]
220
242
  @auth_methods = @options[ :auth_methods ]
243
+ @forward_agent = @options[ :forward_agent ] || false
221
244
  @crypto_backend = @options.fetch( :crypto_backend, :ossl )
245
+ @host_key_verifier = host_key_verifier_from(@options.fetch(:paranoid, true))
222
246
 
223
247
  verbose = @options.fetch( :verbose, :warn )
224
248
  log = @options.fetch( :log, STDERR )
@@ -238,7 +262,8 @@ module Net
238
262
  Needle::Registry.new( @registry_options )
239
263
 
240
264
  [ :keys, :host_keys, :auth_methods, :username, :password,
241
- :crypto_backend, :registry_options, :container, :log, :verbose
265
+ :crypto_backend, :registry_options, :container, :log, :verbose,
266
+ :forward_agent, :paranoid
242
267
  ].each do |i|
243
268
  @options.delete i
244
269
  end
@@ -247,6 +272,27 @@ module Net
247
272
  end
248
273
  private :process_arguments
249
274
 
275
+ def host_key_verifier_from(paranoia)
276
+ case paranoia
277
+ when true then
278
+ require 'net/ssh/lenient-host-key-verifier'
279
+ Net::SSH::LenientHostKeyVerifier.new
280
+ when false then
281
+ require 'net/ssh/null-host-key-verifier'
282
+ Net::SSH::NullHostKeyVerifier.new
283
+ when :very then
284
+ require 'net/ssh/host-key-verifier'
285
+ Net::SS::HostKeyVerifier.new
286
+ else
287
+ if paranoia.respond_to?(:verify)
288
+ paranoia
289
+ else
290
+ raise ArgumentError, "argument to :paranoid is not valid: #{paranoia.inspect}"
291
+ end
292
+ end
293
+ end
294
+ private :host_key_verifier_from
295
+
250
296
  # Make sure we're in an acceptible state.
251
297
  def sanity_check
252
298
  raise Net::SSH::Exception, "session not open" unless @open
@@ -49,6 +49,9 @@ module Net
49
49
  # The reference to the buffer factory to use.
50
50
  attr_writer :buffers
51
51
 
52
+ # The reference to the host key verifier to use to verify host keys.
53
+ attr_writer :host_key_verifier
54
+
52
55
  # Create a new instance of the DiffieHellmanGroup1SHA1 algorithm.
53
56
  # The parameters are, respectively, a factory for creating new
54
57
  # Bignum instances, and a factory for obtaining digester objects.
@@ -157,8 +160,23 @@ module Net
157
160
  "'#{key.ssh_type}' != '#{session.algorithms.host_key}'"
158
161
  end
159
162
 
160
- # TODO: verify that the server key is really the key for the given
161
- # host, probably by having a service that can verify server keys.
163
+ blob, fingerprint = generate_key_fingerprint(key)
164
+
165
+ unless @host_key_verifier.verify(:key => key, :key_blob => blob, :fingerprint => fingerprint, :peer => session.peer)
166
+ raise Net::SSH::Exception, "host key verification failed"
167
+ end
168
+ end
169
+
170
+ def generate_key_fingerprint(key)
171
+ writer = @buffers.writer
172
+ writer.write_key(key)
173
+
174
+ blob = writer.to_s
175
+ fingerprint = OpenSSL::Digest::MD5.hexdigest(blob).scan(/../).join(":")
176
+
177
+ [blob, fingerprint]
178
+ rescue ::Exception => e
179
+ [nil, "(could not generate fingerprint: #{e.message})"]
162
180
  end
163
181
 
164
182
  # Verify the signature that was received. Raise Net::SSH::Exception