net-ssh 5.0.2 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +8 -2
- data/.rubocop_todo.yml +392 -379
- data/.travis.yml +22 -22
- data/CHANGES.txt +63 -0
- data/Manifest +0 -1
- data/README.md +287 -0
- data/Rakefile +1 -2
- data/appveyor.yml +4 -2
- data/lib/net/ssh.rb +13 -4
- data/lib/net/ssh/authentication/agent.rb +9 -3
- data/lib/net/ssh/authentication/certificate.rb +10 -1
- data/lib/net/ssh/authentication/ed25519.rb +75 -46
- data/lib/net/ssh/authentication/ed25519_loader.rb +1 -1
- data/lib/net/ssh/authentication/key_manager.rb +35 -6
- data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +3 -1
- data/lib/net/ssh/authentication/methods/publickey.rb +2 -0
- data/lib/net/ssh/authentication/pub_key_fingerprint.rb +0 -1
- data/lib/net/ssh/authentication/session.rb +9 -6
- data/lib/net/ssh/buffer.rb +41 -10
- data/lib/net/ssh/buffered_io.rb +0 -1
- data/lib/net/ssh/config.rb +68 -35
- data/lib/net/ssh/connection/channel.rb +17 -5
- data/lib/net/ssh/connection/event_loop.rb +0 -1
- data/lib/net/ssh/connection/session.rb +7 -4
- data/lib/net/ssh/key_factory.rb +104 -17
- data/lib/net/ssh/known_hosts.rb +41 -26
- data/lib/net/ssh/loggable.rb +2 -2
- data/lib/net/ssh/proxy/command.rb +0 -1
- data/lib/net/ssh/proxy/socks5.rb +0 -1
- data/lib/net/ssh/service/forward.rb +2 -1
- data/lib/net/ssh/test.rb +8 -7
- data/lib/net/ssh/test/extensions.rb +2 -0
- data/lib/net/ssh/transport/algorithms.rb +161 -105
- data/lib/net/ssh/transport/cipher_factory.rb +11 -27
- data/lib/net/ssh/transport/constants.rb +10 -6
- data/lib/net/ssh/transport/ctr.rb +1 -7
- data/lib/net/ssh/transport/hmac.rb +15 -13
- data/lib/net/ssh/transport/hmac/abstract.rb +16 -0
- data/lib/net/ssh/transport/hmac/sha2_256.rb +7 -11
- data/lib/net/ssh/transport/hmac/sha2_256_96.rb +4 -8
- data/lib/net/ssh/transport/hmac/sha2_256_etm.rb +12 -0
- data/lib/net/ssh/transport/hmac/sha2_512.rb +6 -9
- data/lib/net/ssh/transport/hmac/sha2_512_96.rb +4 -8
- data/lib/net/ssh/transport/hmac/sha2_512_etm.rb +12 -0
- data/lib/net/ssh/transport/kex.rb +14 -11
- data/lib/net/ssh/transport/kex/abstract.rb +123 -0
- data/lib/net/ssh/transport/kex/abstract5656.rb +72 -0
- data/lib/net/ssh/transport/kex/curve25519_sha256.rb +38 -0
- data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +30 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group14_sha1.rb +1 -15
- data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +9 -118
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +0 -6
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha256.rb +5 -9
- data/lib/net/ssh/transport/kex/ecdh_sha2_nistp256.rb +18 -79
- data/lib/net/ssh/transport/kex/ecdh_sha2_nistp384.rb +5 -4
- data/lib/net/ssh/transport/kex/ecdh_sha2_nistp521.rb +5 -4
- data/lib/net/ssh/transport/openssl.rb +106 -93
- data/lib/net/ssh/transport/packet_stream.rb +52 -20
- data/lib/net/ssh/transport/session.rb +26 -4
- data/lib/net/ssh/transport/state.rb +1 -1
- data/lib/net/ssh/verifiers/accept_new.rb +7 -0
- data/lib/net/ssh/verifiers/always.rb +4 -0
- data/lib/net/ssh/verifiers/never.rb +4 -0
- data/lib/net/ssh/version.rb +3 -3
- data/net-ssh-public_cert.pem +18 -19
- data/net-ssh.gemspec +9 -7
- metadata +56 -40
- metadata.gz.sig +0 -0
- data/Gemfile.noed25519.lock +0 -41
- data/README.rdoc +0 -169
- data/lib/net/ssh/ruby_compat.rb +0 -13
- data/support/arcfour_check.rb +0 -20
data/lib/net/ssh/known_hosts.rb
CHANGED
@@ -2,6 +2,7 @@ require 'strscan'
|
|
2
2
|
require 'openssl'
|
3
3
|
require 'base64'
|
4
4
|
require 'net/ssh/buffer'
|
5
|
+
require 'net/ssh/authentication/ed25519_loader'
|
5
6
|
|
6
7
|
module Net
|
7
8
|
module SSH
|
@@ -40,26 +41,24 @@ module Net
|
|
40
41
|
# This is used internally by Net::SSH, and will never need to be used directly
|
41
42
|
# by consumers of the library.
|
42
43
|
class KnownHosts
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
SUPPORTED_TYPE = %w[ssh-rsa ssh-dss]
|
50
|
-
end
|
44
|
+
SUPPORTED_TYPE = %w[ssh-rsa ssh-dss
|
45
|
+
ecdsa-sha2-nistp256
|
46
|
+
ecdsa-sha2-nistp384
|
47
|
+
ecdsa-sha2-nistp521]
|
48
|
+
|
49
|
+
SUPPORTED_TYPE.push('ssh-ed25519') if Net::SSH::Authentication::ED25519Loader::LOADED
|
51
50
|
|
52
51
|
class <<self
|
53
52
|
# Searches all known host files (see KnownHosts.hostfiles) for all keys
|
54
53
|
# of the given host. Returns an enumerable of keys found.
|
55
54
|
def search_for(host, options={})
|
56
|
-
HostKeys.new(search_in(hostfiles(options), host), host, self, options)
|
55
|
+
HostKeys.new(search_in(hostfiles(options), host, options), host, self, options)
|
57
56
|
end
|
58
57
|
|
59
58
|
# Search for all known keys for the given host, in every file given in
|
60
59
|
# the +files+ array. Returns the list of keys.
|
61
|
-
def search_in(files, host)
|
62
|
-
files.flat_map { |file| KnownHosts.new(file).keys_for(host) }
|
60
|
+
def search_in(files, host, options = {})
|
61
|
+
files.flat_map { |file| KnownHosts.new(file).keys_for(host, options) }
|
63
62
|
end
|
64
63
|
|
65
64
|
# Looks in the given +options+ hash for the :user_known_hosts_file and
|
@@ -76,7 +75,9 @@ module Net
|
|
76
75
|
|
77
76
|
files += Array(options[:user_known_hosts_file] || %w[~/.ssh/known_hosts ~/.ssh/known_hosts2]) if which == :all || which == :user
|
78
77
|
|
79
|
-
|
78
|
+
if which == :all || which == :global
|
79
|
+
files += Array(options[:global_known_hosts_file] || %w[/etc/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts2])
|
80
|
+
end
|
80
81
|
|
81
82
|
return files
|
82
83
|
end
|
@@ -119,32 +120,31 @@ module Net
|
|
119
120
|
# "[net.ssh.test]:5555"
|
120
121
|
# "[1,2,3,4]:5555"
|
121
122
|
# "[net.ssh.test]:5555,[1.2.3.4]:5555
|
122
|
-
def keys_for(host)
|
123
|
+
def keys_for(host, options = {})
|
123
124
|
keys = []
|
124
125
|
return keys unless File.readable?(source)
|
125
126
|
|
126
127
|
entries = host.split(/,/)
|
128
|
+
host_name = entries[0]
|
129
|
+
host_ip = entries[1]
|
127
130
|
|
128
131
|
File.open(source) do |file|
|
129
|
-
scanner = StringScanner.new("")
|
130
132
|
file.each_line do |line|
|
131
|
-
|
133
|
+
hosts, type, key_content = line.split(' ')
|
134
|
+
# Skip empty line or one that is commented
|
135
|
+
next if hosts.nil? || hosts.start_with?('#')
|
132
136
|
|
133
|
-
|
134
|
-
next if scanner.match?(/$|#/)
|
137
|
+
hostlist = hosts.split(',')
|
135
138
|
|
136
|
-
|
137
|
-
found = entries.all? { |entry| hostlist.include?(entry) } ||
|
138
|
-
known_host_hash?(hostlist, entries)
|
139
|
-
next unless found
|
139
|
+
next unless SUPPORTED_TYPE.include?(type)
|
140
140
|
|
141
|
-
|
142
|
-
|
141
|
+
found = hostlist.any? { |pattern| match(host_name, pattern) } || known_host_hash?(hostlist, entries)
|
142
|
+
next unless found
|
143
143
|
|
144
|
-
|
144
|
+
found = hostlist.include?(host_ip) if options[:check_host_ip] && entries.size > 1 && hostlist.size > 1
|
145
|
+
next unless found
|
145
146
|
|
146
|
-
|
147
|
-
blob = scanner.rest.unpack("m*").first
|
147
|
+
blob = key_content.unpack("m*").first
|
148
148
|
keys << Net::SSH::Buffer.new(blob).read_key
|
149
149
|
end
|
150
150
|
end
|
@@ -152,6 +152,21 @@ module Net
|
|
152
152
|
keys
|
153
153
|
end
|
154
154
|
|
155
|
+
def match(host, pattern)
|
156
|
+
if pattern.include?('*') || pattern.include?('?')
|
157
|
+
# see man 8 sshd for pattern details
|
158
|
+
pattern_regexp = pattern.split('*').map do |x|
|
159
|
+
x.split('?').map do |y|
|
160
|
+
Regexp.escape(y)
|
161
|
+
end.join('.')
|
162
|
+
end.join('[^.]*')
|
163
|
+
|
164
|
+
host =~ Regexp.new("\\A#{pattern_regexp}\\z")
|
165
|
+
else
|
166
|
+
host == pattern
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
155
170
|
# Indicates whether one of the entries matches an hostname that has been
|
156
171
|
# stored as a HMAC-SHA1 hash in the known hosts.
|
157
172
|
def known_host_hash?(hostlist, entries)
|
data/lib/net/ssh/loggable.rb
CHANGED
@@ -56,8 +56,8 @@ module Net
|
|
56
56
|
# originates. It defaults to the name of class with the object_id
|
57
57
|
# appended.
|
58
58
|
def facility
|
59
|
-
@facility ||= self.class.
|
59
|
+
@facility ||= self.class.to_s.gsub(/::/, ".").gsub(/([a-z])([A-Z])/, "\\1_\\2").downcase + "[%x]" % object_id
|
60
60
|
end
|
61
61
|
end
|
62
62
|
end
|
63
|
-
end
|
63
|
+
end
|
data/lib/net/ssh/proxy/socks5.rb
CHANGED
@@ -85,7 +85,8 @@ module Net
|
|
85
85
|
client = server.accept
|
86
86
|
debug { "received connection on #{socket}" }
|
87
87
|
|
88
|
-
channel = session.open_channel("direct-tcpip", :string, remote_host, :long,
|
88
|
+
channel = session.open_channel("direct-tcpip", :string, remote_host, :long,
|
89
|
+
remote_port, :string, bind_address, local_port_type, local_port) do |achannel|
|
89
90
|
achannel.info { "direct channel established" }
|
90
91
|
end
|
91
92
|
|
data/lib/net/ssh/test.rb
CHANGED
@@ -3,7 +3,7 @@ require 'net/ssh/connection/session'
|
|
3
3
|
require 'net/ssh/test/kex'
|
4
4
|
require 'net/ssh/test/socket'
|
5
5
|
|
6
|
-
module Net
|
6
|
+
module Net
|
7
7
|
module SSH
|
8
8
|
|
9
9
|
# This module may be used in unit tests, for when you want to test that your
|
@@ -54,30 +54,30 @@ module Net
|
|
54
54
|
Net::SSH::Test::Extensions::IO.with_test_extension { yield socket.script if block_given? }
|
55
55
|
return socket.script
|
56
56
|
end
|
57
|
-
|
57
|
+
|
58
58
|
# Returns the test socket instance to use for these tests (see
|
59
59
|
# Net::SSH::Test::Socket).
|
60
60
|
def socket(options={})
|
61
61
|
@socket ||= Net::SSH::Test::Socket.new
|
62
62
|
end
|
63
|
-
|
63
|
+
|
64
64
|
# Returns the connection session (Net::SSH::Connection::Session) for use
|
65
65
|
# in these tests. It is a fully functional SSH session, operating over
|
66
66
|
# a mock socket (#socket).
|
67
67
|
def connection(options={})
|
68
68
|
@connection ||= Net::SSH::Connection::Session.new(transport(options), options)
|
69
69
|
end
|
70
|
-
|
70
|
+
|
71
71
|
# Returns the transport session (Net::SSH::Transport::Session) for use
|
72
72
|
# in these tests. It is a fully functional SSH transport session, operating
|
73
73
|
# over a mock socket (#socket).
|
74
74
|
def transport(options={})
|
75
75
|
@transport ||= Net::SSH::Transport::Session.new(
|
76
76
|
options[:host] || "localhost",
|
77
|
-
options.merge(kex: "test", host_key: "ssh-rsa", verify_host_key:
|
77
|
+
options.merge(kex: "test", host_key: "ssh-rsa", append_all_supported_algorithms: true, verify_host_key: :never, proxy: socket(options))
|
78
78
|
)
|
79
79
|
end
|
80
|
-
|
80
|
+
|
81
81
|
# First asserts that a story has been described (see #story). Then yields,
|
82
82
|
# and then asserts that all items described in the script have been
|
83
83
|
# processed. Typically, this is called immediately after a story has
|
@@ -86,7 +86,8 @@ module Net
|
|
86
86
|
def assert_scripted
|
87
87
|
raise "there is no script to be processed" if socket.script.events.empty?
|
88
88
|
Net::SSH::Test::Extensions::IO.with_test_extension { yield }
|
89
|
-
assert socket.script.events.empty?, "there should not be any remaining scripted events, but there are still
|
89
|
+
assert socket.script.events.empty?, "there should not be any remaining scripted events, but there are still" \
|
90
|
+
"#{socket.script.events.length} pending"
|
90
91
|
end
|
91
92
|
end
|
92
93
|
|
@@ -5,13 +5,13 @@ require 'net/ssh/transport/cipher_factory'
|
|
5
5
|
require 'net/ssh/transport/constants'
|
6
6
|
require 'net/ssh/transport/hmac'
|
7
7
|
require 'net/ssh/transport/kex'
|
8
|
+
require 'net/ssh/transport/kex/curve25519_sha256_loader'
|
8
9
|
require 'net/ssh/transport/server_version'
|
9
10
|
require 'net/ssh/authentication/ed25519_loader'
|
10
11
|
|
11
|
-
module Net
|
12
|
-
module SSH
|
12
|
+
module Net
|
13
|
+
module SSH
|
13
14
|
module Transport
|
14
|
-
|
15
15
|
# Implements the higher-level logic behind an SSH key-exchange. It handles
|
16
16
|
# both the initial exchange, as well as subsequent re-exchanges (as needed).
|
17
17
|
# It also encapsulates the negotiation of the algorithms, and provides a
|
@@ -22,92 +22,125 @@ module Net
|
|
22
22
|
class Algorithms
|
23
23
|
include Loggable
|
24
24
|
include Constants
|
25
|
-
|
26
|
-
# Define the default algorithms, in order of preference, supported by
|
27
|
-
|
28
|
-
|
29
|
-
|
25
|
+
|
26
|
+
# Define the default algorithms, in order of preference, supported by Net::SSH.
|
27
|
+
DEFAULT_ALGORITHMS = {
|
28
|
+
host_key: %w[ecdsa-sha2-nistp521-cert-v01@openssh.com
|
29
|
+
ecdsa-sha2-nistp384-cert-v01@openssh.com
|
30
|
+
ecdsa-sha2-nistp256-cert-v01@openssh.com
|
31
|
+
ecdsa-sha2-nistp521
|
32
|
+
ecdsa-sha2-nistp384
|
33
|
+
ecdsa-sha2-nistp256
|
30
34
|
ssh-rsa-cert-v01@openssh.com
|
31
|
-
ssh-rsa-cert-v00@openssh.com
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
hmac: %w[hmac-
|
35
|
+
ssh-rsa-cert-v00@openssh.com
|
36
|
+
ssh-rsa],
|
37
|
+
|
38
|
+
kex: %w[ecdh-sha2-nistp521
|
39
|
+
ecdh-sha2-nistp384
|
40
|
+
ecdh-sha2-nistp256
|
41
|
+
diffie-hellman-group-exchange-sha256
|
42
|
+
diffie-hellman-group14-sha1],
|
43
|
+
|
44
|
+
encryption: %w[aes256-ctr aes192-ctr aes128-ctr],
|
45
|
+
|
46
|
+
hmac: %w[hmac-sha2-512-etm@openssh.com hmac-sha2-256-etm@openssh.com
|
47
|
+
hmac-sha2-512 hmac-sha2-256
|
48
|
+
hmac-sha1]
|
49
|
+
}.freeze
|
50
|
+
|
51
|
+
if Net::SSH::Authentication::ED25519Loader::LOADED
|
52
|
+
DEFAULT_ALGORITHMS[:host_key].unshift(
|
53
|
+
'ssh-ed25519-cert-v01@openssh.com',
|
54
|
+
'ssh-ed25519'
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
if Net::SSH::Transport::Kex::Curve25519Sha256Loader::LOADED
|
59
|
+
DEFAULT_ALGORITHMS[:kex].unshift(
|
60
|
+
'curve25519-sha256',
|
61
|
+
'curve25519-sha256@libssh.org'
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Define all algorithms, with the deprecated, supported by Net::SSH.
|
66
|
+
ALGORITHMS = {
|
67
|
+
host_key: DEFAULT_ALGORITHMS[:host_key] + %w[ssh-dss],
|
68
|
+
|
69
|
+
kex: DEFAULT_ALGORITHMS[:kex] +
|
70
|
+
%w[diffie-hellman-group-exchange-sha1
|
71
|
+
diffie-hellman-group1-sha1],
|
72
|
+
|
73
|
+
encryption: DEFAULT_ALGORITHMS[:encryption] +
|
74
|
+
%w[aes256-cbc aes192-cbc aes128-cbc
|
75
|
+
rijndael-cbc@lysator.liu.se
|
76
|
+
blowfish-ctr blowfish-cbc
|
77
|
+
cast128-ctr cast128-cbc
|
78
|
+
3des-ctr 3des-cbc
|
79
|
+
idea-cbc
|
80
|
+
none],
|
81
|
+
|
82
|
+
hmac: DEFAULT_ALGORITHMS[:hmac] +
|
83
|
+
%w[hmac-sha2-512-96 hmac-sha2-256-96
|
84
|
+
hmac-sha1-96
|
43
85
|
hmac-ripemd160 hmac-ripemd160@openssh.com
|
44
|
-
hmac-
|
45
|
-
|
46
|
-
|
86
|
+
hmac-md5 hmac-md5-96
|
87
|
+
none],
|
88
|
+
|
47
89
|
compression: %w[none zlib@openssh.com zlib],
|
48
90
|
language: %w[]
|
49
|
-
}
|
50
|
-
|
51
|
-
ALGORITHMS[:host_key] += %w[ecdsa-sha2-nistp256
|
52
|
-
ecdsa-sha2-nistp384
|
53
|
-
ecdsa-sha2-nistp521]
|
54
|
-
ALGORITHMS[:host_key] += %w[ssh-ed25519] if Net::SSH::Authentication::ED25519Loader::LOADED
|
55
|
-
ALGORITHMS[:kex] += %w[ecdh-sha2-nistp256
|
56
|
-
ecdh-sha2-nistp384
|
57
|
-
ecdh-sha2-nistp521]
|
58
|
-
end
|
59
|
-
|
91
|
+
}.freeze
|
92
|
+
|
60
93
|
# The underlying transport layer session that supports this object
|
61
94
|
attr_reader :session
|
62
|
-
|
95
|
+
|
63
96
|
# The hash of options used to initialize this object
|
64
97
|
attr_reader :options
|
65
|
-
|
98
|
+
|
66
99
|
# The kex algorithm to use settled on between the client and server.
|
67
100
|
attr_reader :kex
|
68
|
-
|
101
|
+
|
69
102
|
# The type of host key that will be used for this session.
|
70
103
|
attr_reader :host_key
|
71
|
-
|
104
|
+
|
72
105
|
# The type of the cipher to use to encrypt packets sent from the client to
|
73
106
|
# the server.
|
74
107
|
attr_reader :encryption_client
|
75
|
-
|
108
|
+
|
76
109
|
# The type of the cipher to use to decrypt packets arriving from the server.
|
77
110
|
attr_reader :encryption_server
|
78
|
-
|
111
|
+
|
79
112
|
# The type of HMAC to use to sign packets sent by the client.
|
80
113
|
attr_reader :hmac_client
|
81
|
-
|
114
|
+
|
82
115
|
# The type of HMAC to use to validate packets arriving from the server.
|
83
116
|
attr_reader :hmac_server
|
84
|
-
|
117
|
+
|
85
118
|
# The type of compression to use to compress packets being sent by the client.
|
86
119
|
attr_reader :compression_client
|
87
|
-
|
120
|
+
|
88
121
|
# The type of compression to use to decompress packets arriving from the server.
|
89
122
|
attr_reader :compression_server
|
90
|
-
|
123
|
+
|
91
124
|
# The language that will be used in messages sent by the client.
|
92
125
|
attr_reader :language_client
|
93
|
-
|
126
|
+
|
94
127
|
# The language that will be used in messages sent from the server.
|
95
128
|
attr_reader :language_server
|
96
|
-
|
129
|
+
|
97
130
|
# The hash of algorithms preferred by the client, which will be told to
|
98
131
|
# the server during algorithm negotiation.
|
99
132
|
attr_reader :algorithms
|
100
|
-
|
133
|
+
|
101
134
|
# The session-id for this session, as decided during the initial key exchange.
|
102
135
|
attr_reader :session_id
|
103
|
-
|
136
|
+
|
104
137
|
# Returns true if the given packet can be processed during a key-exchange.
|
105
138
|
def self.allowed_packet?(packet)
|
106
139
|
(1..4).include?(packet.type) ||
|
107
140
|
(6..19).include?(packet.type) ||
|
108
141
|
(21..49).include?(packet.type)
|
109
142
|
end
|
110
|
-
|
143
|
+
|
111
144
|
# Instantiates a new Algorithms object, and prepares the hash of preferred
|
112
145
|
# algorithms based on the options parameter and the ALGORITHMS constant.
|
113
146
|
def initialize(session, options={})
|
@@ -119,13 +152,14 @@ module Net
|
|
119
152
|
@client_packet = @server_packet = nil
|
120
153
|
prepare_preferred_algorithms!
|
121
154
|
end
|
122
|
-
|
155
|
+
|
123
156
|
# Start the algorithm negotation
|
124
157
|
def start
|
125
158
|
raise ArgumentError, "Cannot call start if it's negotiation started or done" if @pending || @initialized
|
159
|
+
|
126
160
|
send_kexinit
|
127
161
|
end
|
128
|
-
|
162
|
+
|
129
163
|
# Request a rekey operation. This will return immediately, and does not
|
130
164
|
# actually perform the rekey operation. It does cause the session to change
|
131
165
|
# state, however--until the key exchange finishes, no new packets will be
|
@@ -135,7 +169,7 @@ module Net
|
|
135
169
|
@initialized = false
|
136
170
|
send_kexinit
|
137
171
|
end
|
138
|
-
|
172
|
+
|
139
173
|
# Called by the transport layer when a KEXINIT packet is received, indicating
|
140
174
|
# that the server wants to exchange keys. This can be spontaneous, or it
|
141
175
|
# can be in response to a client-initiated rekey request (see #rekey!). Either
|
@@ -150,13 +184,13 @@ module Net
|
|
150
184
|
proceed!
|
151
185
|
end
|
152
186
|
end
|
153
|
-
|
187
|
+
|
154
188
|
# A convenience method for accessing the list of preferred types for a
|
155
189
|
# specific algorithm (see #algorithms).
|
156
190
|
def [](key)
|
157
191
|
algorithms[key]
|
158
192
|
end
|
159
|
-
|
193
|
+
|
160
194
|
# Returns +true+ if a key-exchange is pending. This will be true from the
|
161
195
|
# moment either the client or server requests the key exchange, until the
|
162
196
|
# exchange completes. While an exchange is pending, only a limited number
|
@@ -180,8 +214,8 @@ module Net
|
|
180
214
|
|
181
215
|
def host_key_format
|
182
216
|
case host_key
|
183
|
-
when
|
184
|
-
|
217
|
+
when /^([a-z0-9-]+)-cert-v\d{2}@openssh.com$/
|
218
|
+
Regexp.last_match[1]
|
185
219
|
else
|
186
220
|
host_key
|
187
221
|
end
|
@@ -201,7 +235,7 @@ module Net
|
|
201
235
|
session.send_message(packet)
|
202
236
|
proceed! if @server_packet
|
203
237
|
end
|
204
|
-
|
238
|
+
|
205
239
|
# After both client and server have sent their KEXINIT packets, this
|
206
240
|
# will do the algorithm negotiation and key exchange. Once both finish,
|
207
241
|
# the object leaves the pending state and the method returns.
|
@@ -211,7 +245,7 @@ module Net
|
|
211
245
|
exchange_keys
|
212
246
|
@pending = false
|
213
247
|
end
|
214
|
-
|
248
|
+
|
215
249
|
# Prepares the list of preferred algorithms, based on the options hash
|
216
250
|
# that was given when the object was constructed, and the ALGORITHMS
|
217
251
|
# constant. Also, when determining the host_key type to use, the known
|
@@ -220,23 +254,26 @@ module Net
|
|
220
254
|
# communicating with this server.
|
221
255
|
def prepare_preferred_algorithms!
|
222
256
|
options[:compression] = %w[zlib@openssh.com zlib] if options[:compression] == true
|
223
|
-
|
257
|
+
|
224
258
|
ALGORITHMS.each do |algorithm, supported|
|
225
|
-
algorithms[algorithm] = compose_algorithm_list(
|
259
|
+
algorithms[algorithm] = compose_algorithm_list(
|
260
|
+
supported, options[algorithm] || DEFAULT_ALGORITHMS[algorithm],
|
261
|
+
options[:append_all_supported_algorithms]
|
262
|
+
)
|
226
263
|
end
|
227
|
-
|
264
|
+
|
228
265
|
# for convention, make sure our list has the same keys as the server
|
229
266
|
# list
|
230
|
-
|
267
|
+
|
231
268
|
algorithms[:encryption_client ] = algorithms[:encryption_server ] = algorithms[:encryption]
|
232
269
|
algorithms[:hmac_client ] = algorithms[:hmac_server ] = algorithms[:hmac]
|
233
270
|
algorithms[:compression_client] = algorithms[:compression_server] = algorithms[:compression]
|
234
271
|
algorithms[:language_client ] = algorithms[:language_server ] = algorithms[:language]
|
235
|
-
|
272
|
+
|
236
273
|
if !options.key?(:host_key)
|
237
274
|
# make sure the host keys are specified in preference order, where any
|
238
275
|
# existing known key for the host has preference.
|
239
|
-
|
276
|
+
|
240
277
|
existing_keys = session.host_keys
|
241
278
|
host_keys = existing_keys.map { |key| key.ssh_type }.uniq
|
242
279
|
algorithms[:host_key].each do |name|
|
@@ -245,45 +282,59 @@ module Net
|
|
245
282
|
algorithms[:host_key] = host_keys
|
246
283
|
end
|
247
284
|
end
|
248
|
-
|
285
|
+
|
249
286
|
# Composes the list of algorithms by taking supported algorithms and matching with supplied options.
|
250
287
|
def compose_algorithm_list(supported, option, append_all_supported_algorithms = false)
|
251
288
|
return supported.dup unless option
|
252
|
-
|
289
|
+
|
253
290
|
list = []
|
254
291
|
option = Array(option).compact.uniq
|
255
|
-
|
256
|
-
if option.first && option.first.start_with?('+')
|
292
|
+
|
293
|
+
if option.first && option.first.start_with?('+', '-')
|
257
294
|
list = supported.dup
|
258
|
-
|
259
|
-
|
295
|
+
|
296
|
+
appends = option.select { |opt| opt.start_with?('+') }.map { |opt| opt[1..-1] }
|
297
|
+
deletions = option.select { |opt| opt.start_with?('-') }.map { |opt| opt[1..-1] }
|
298
|
+
|
299
|
+
list.concat(appends)
|
300
|
+
|
301
|
+
deletions.each do |opt|
|
302
|
+
if opt.include?('*')
|
303
|
+
opt_escaped = Regexp.escape(opt)
|
304
|
+
algo_re = /\A#{opt_escaped.gsub('\*', '[A-Za-z\d\-@\.]*')}\z/
|
305
|
+
list.delete_if { |existing_opt| algo_re.match(existing_opt) }
|
306
|
+
else
|
307
|
+
list.delete(opt)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
260
311
|
list.uniq!
|
261
312
|
else
|
262
313
|
list = option
|
263
|
-
|
314
|
+
|
264
315
|
if append_all_supported_algorithms
|
265
316
|
supported.each { |name| list << name unless list.include?(name) }
|
266
317
|
end
|
267
318
|
end
|
268
|
-
|
319
|
+
|
269
320
|
unsupported = []
|
270
321
|
list.select! do |name|
|
271
322
|
is_supported = supported.include?(name)
|
272
323
|
unsupported << name unless is_supported
|
273
324
|
is_supported
|
274
325
|
end
|
275
|
-
|
326
|
+
|
276
327
|
lwarn { %(unsupported algorithm: `#{unsupported}') } unless unsupported.empty?
|
277
|
-
|
328
|
+
|
278
329
|
list
|
279
330
|
end
|
280
|
-
|
331
|
+
|
281
332
|
# Parses a KEXINIT packet from the server.
|
282
333
|
def parse_server_algorithm_packet(packet)
|
283
334
|
data = { raw: packet.content }
|
284
|
-
|
335
|
+
|
285
336
|
packet.read(16) # skip the cookie value
|
286
|
-
|
337
|
+
|
287
338
|
data[:kex] = packet.read_string.split(/,/)
|
288
339
|
data[:host_key] = packet.read_string.split(/,/)
|
289
340
|
data[:encryption_client] = packet.read_string.split(/,/)
|
@@ -294,15 +345,15 @@ module Net
|
|
294
345
|
data[:compression_server] = packet.read_string.split(/,/)
|
295
346
|
data[:language_client] = packet.read_string.split(/,/)
|
296
347
|
data[:language_server] = packet.read_string.split(/,/)
|
297
|
-
|
348
|
+
|
298
349
|
# TODO: if first_kex_packet_follows, we need to try to skip the
|
299
350
|
# actual kexinit stuff and try to guess what the server is doing...
|
300
351
|
# need to read more about this scenario.
|
301
352
|
# first_kex_packet_follows = packet.read_bool
|
302
|
-
|
353
|
+
|
303
354
|
return data
|
304
355
|
end
|
305
|
-
|
356
|
+
|
306
357
|
# Given the #algorithms map of preferred algorithm types, this constructs
|
307
358
|
# a KEXINIT packet to send to the server. It does not actually send it,
|
308
359
|
# it simply builds the packet and returns it.
|
@@ -313,14 +364,14 @@ module Net
|
|
313
364
|
hmac = algorithms[:hmac].join(",")
|
314
365
|
compression = algorithms[:compression].join(",")
|
315
366
|
language = algorithms[:language].join(",")
|
316
|
-
|
367
|
+
|
317
368
|
Net::SSH::Buffer.from(:byte, KEXINIT,
|
318
369
|
:long, [rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF)],
|
319
370
|
:mstring, [kex, host_key, encryption, encryption, hmac, hmac],
|
320
371
|
:mstring, [compression, compression, language, language],
|
321
372
|
:bool, false, :long, 0)
|
322
373
|
end
|
323
|
-
|
374
|
+
|
324
375
|
# Given the parsed server KEX packet, and the client's preferred algorithm
|
325
376
|
# lists in #algorithms, determine which preferred algorithms each has
|
326
377
|
# in common and set those as the selected algorithms. If, for any algorithm,
|
@@ -336,48 +387,53 @@ module Net
|
|
336
387
|
@compression_server = negotiate(:compression_server)
|
337
388
|
@language_client = negotiate(:language_client) rescue ""
|
338
389
|
@language_server = negotiate(:language_server) rescue ""
|
339
|
-
|
390
|
+
|
340
391
|
debug do
|
341
392
|
"negotiated:\n" +
|
342
|
-
%i[kex host_key encryption_server encryption_client hmac_client hmac_server
|
393
|
+
%i[kex host_key encryption_server encryption_client hmac_client hmac_server
|
394
|
+
compression_client compression_server language_client language_server].map do |key|
|
343
395
|
"* #{key}: #{instance_variable_get("@#{key}")}"
|
344
396
|
end.join("\n")
|
345
397
|
end
|
346
398
|
end
|
347
|
-
|
399
|
+
|
348
400
|
# Negotiates a single algorithm based on the preferences reported by the
|
349
401
|
# server and those set by the client. This is called by
|
350
402
|
# #negotiate_algorithms.
|
351
403
|
def negotiate(algorithm)
|
352
404
|
match = self[algorithm].find { |item| @server_data[algorithm].include?(item) }
|
353
|
-
|
354
|
-
|
355
|
-
|
405
|
+
|
406
|
+
if match.nil?
|
407
|
+
raise Net::SSH::Exception, "could not settle on #{algorithm} algorithm\n"\
|
408
|
+
"Server #{algorithm} preferences: #{@server_data[algorithm].join(',')}\n"\
|
409
|
+
"Client #{algorithm} preferences: #{self[algorithm].join(',')}"
|
410
|
+
end
|
411
|
+
|
356
412
|
return match
|
357
413
|
end
|
358
|
-
|
414
|
+
|
359
415
|
# Considers the sizes of the keys and block-sizes for the selected ciphers,
|
360
416
|
# and the lengths of the hmacs, and returns the largest as the byte requirement
|
361
417
|
# for the key-exchange algorithm.
|
362
418
|
def kex_byte_requirement
|
363
419
|
sizes = [8] # require at least 8 bytes
|
364
|
-
|
420
|
+
|
365
421
|
sizes.concat(CipherFactory.get_lengths(encryption_client))
|
366
422
|
sizes.concat(CipherFactory.get_lengths(encryption_server))
|
367
|
-
|
423
|
+
|
368
424
|
sizes << HMAC.key_length(hmac_client)
|
369
425
|
sizes << HMAC.key_length(hmac_server)
|
370
|
-
|
426
|
+
|
371
427
|
sizes.max
|
372
428
|
end
|
373
|
-
|
429
|
+
|
374
430
|
# Instantiates one of the Transport::Kex classes (based on the negotiated
|
375
431
|
# kex algorithm), and uses it to exchange keys. Then, the ciphers and
|
376
432
|
# HMACs are initialized and fed to the transport layer, to be used in
|
377
433
|
# further communication with the server.
|
378
434
|
def exchange_keys
|
379
435
|
debug { "exchanging keys" }
|
380
|
-
|
436
|
+
|
381
437
|
algorithm = Kex::MAP[kex].new(self, session,
|
382
438
|
client_version_string: Net::SSH::Transport::ServerVersion::PROTO_VERSION,
|
383
439
|
server_version_string: session.server_version.version,
|
@@ -387,46 +443,46 @@ module Net
|
|
387
443
|
minimum_dh_bits: options[:minimum_dh_bits],
|
388
444
|
logger: logger)
|
389
445
|
result = algorithm.exchange_keys
|
390
|
-
|
446
|
+
|
391
447
|
secret = result[:shared_secret].to_ssh
|
392
448
|
hash = result[:session_id]
|
393
449
|
digester = result[:hashing_algorithm]
|
394
|
-
|
450
|
+
|
395
451
|
@session_id ||= hash
|
396
|
-
|
452
|
+
|
397
453
|
key = Proc.new { |salt| digester.digest(secret + hash + salt + @session_id) }
|
398
|
-
|
454
|
+
|
399
455
|
iv_client = key["A"]
|
400
456
|
iv_server = key["B"]
|
401
457
|
key_client = key["C"]
|
402
458
|
key_server = key["D"]
|
403
459
|
mac_key_client = key["E"]
|
404
460
|
mac_key_server = key["F"]
|
405
|
-
|
461
|
+
|
406
462
|
parameters = { shared: secret, hash: hash, digester: digester }
|
407
|
-
|
463
|
+
|
408
464
|
cipher_client = CipherFactory.get(encryption_client, parameters.merge(iv: iv_client, key: key_client, encrypt: true))
|
409
465
|
cipher_server = CipherFactory.get(encryption_server, parameters.merge(iv: iv_server, key: key_server, decrypt: true))
|
410
|
-
|
466
|
+
|
411
467
|
mac_client = HMAC.get(hmac_client, mac_key_client, parameters)
|
412
468
|
mac_server = HMAC.get(hmac_server, mac_key_server, parameters)
|
413
|
-
|
469
|
+
|
414
470
|
session.configure_client cipher: cipher_client, hmac: mac_client,
|
415
471
|
compression: normalize_compression_name(compression_client),
|
416
472
|
compression_level: options[:compression_level],
|
417
473
|
rekey_limit: options[:rekey_limit],
|
418
474
|
max_packets: options[:rekey_packet_limit],
|
419
475
|
max_blocks: options[:rekey_blocks_limit]
|
420
|
-
|
476
|
+
|
421
477
|
session.configure_server cipher: cipher_server, hmac: mac_server,
|
422
478
|
compression: normalize_compression_name(compression_server),
|
423
479
|
rekey_limit: options[:rekey_limit],
|
424
480
|
max_packets: options[:rekey_packet_limit],
|
425
481
|
max_blocks: options[:rekey_blocks_limit]
|
426
|
-
|
482
|
+
|
427
483
|
@initialized = true
|
428
484
|
end
|
429
|
-
|
485
|
+
|
430
486
|
# Given the SSH name for some compression algorithm, return a normalized
|
431
487
|
# name as a symbol.
|
432
488
|
def normalize_compression_name(name)
|