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.
- data/bin/rb-keygen +210 -0
- data/doc/manual-html/chapter-1.html +2 -2
- data/doc/manual-html/chapter-2.html +13 -2
- data/doc/manual-html/chapter-3.html +2 -2
- data/doc/manual-html/chapter-4.html +2 -2
- data/doc/manual-html/chapter-5.html +2 -2
- data/doc/manual-html/chapter-6.html +2 -2
- data/doc/manual-html/chapter-7.html +2 -2
- data/doc/manual-html/index.html +5 -5
- data/doc/manual/manual.yml +3 -3
- data/doc/manual/parts/0007.txt +2 -0
- data/doc/manual/parts/0008.txt +2 -0
- data/examples/auth-forward.rb +41 -0
- data/lib/net/ssh/connection/driver.rb +15 -1
- data/lib/net/ssh/errors.rb +36 -0
- data/lib/net/ssh/host-key-verifier.rb +108 -0
- data/lib/net/ssh/lenient-host-key-verifier.rb +25 -0
- data/lib/net/ssh/null-host-key-verifier.rb +14 -0
- data/lib/net/ssh/service/agentforward/driver.rb +78 -0
- data/lib/net/ssh/service/agentforward/services.rb +41 -0
- data/lib/net/ssh/service/services.rb +2 -0
- data/lib/net/ssh/session.rb +47 -1
- data/lib/net/ssh/transport/kex/dh.rb +20 -2
- data/lib/net/ssh/transport/kex/services.rb +2 -0
- data/lib/net/ssh/transport/session.rb +10 -0
- data/lib/net/ssh/userauth/agent.rb +11 -0
- data/lib/net/ssh/version.rb +2 -2
- data/test/connection/tc_integration.rb +4 -2
- data/test/service/agentforward/tc_driver.rb +138 -0
- data/test/service/process/tc_integration.rb +2 -0
- data/test/tc_integration.rb +4 -3
- data/test/transport/kex/tc_dh.rb +6 -0
- data/test/transport/kex/tc_dh_gex.rb +1 -0
- data/test/transport/tc_integration.rb +6 -1
- data/test/userauth/tc_integration.rb +2 -0
- metadata +70 -60
data/lib/net/ssh/errors.rb
CHANGED
@@ -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.
|
data/lib/net/ssh/session.rb
CHANGED
@@ -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
|
-
|
161
|
-
|
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
|