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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +8 -2
  6. data/.rubocop_todo.yml +392 -379
  7. data/.travis.yml +22 -22
  8. data/CHANGES.txt +63 -0
  9. data/Manifest +0 -1
  10. data/README.md +287 -0
  11. data/Rakefile +1 -2
  12. data/appveyor.yml +4 -2
  13. data/lib/net/ssh.rb +13 -4
  14. data/lib/net/ssh/authentication/agent.rb +9 -3
  15. data/lib/net/ssh/authentication/certificate.rb +10 -1
  16. data/lib/net/ssh/authentication/ed25519.rb +75 -46
  17. data/lib/net/ssh/authentication/ed25519_loader.rb +1 -1
  18. data/lib/net/ssh/authentication/key_manager.rb +35 -6
  19. data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +3 -1
  20. data/lib/net/ssh/authentication/methods/publickey.rb +2 -0
  21. data/lib/net/ssh/authentication/pub_key_fingerprint.rb +0 -1
  22. data/lib/net/ssh/authentication/session.rb +9 -6
  23. data/lib/net/ssh/buffer.rb +41 -10
  24. data/lib/net/ssh/buffered_io.rb +0 -1
  25. data/lib/net/ssh/config.rb +68 -35
  26. data/lib/net/ssh/connection/channel.rb +17 -5
  27. data/lib/net/ssh/connection/event_loop.rb +0 -1
  28. data/lib/net/ssh/connection/session.rb +7 -4
  29. data/lib/net/ssh/key_factory.rb +104 -17
  30. data/lib/net/ssh/known_hosts.rb +41 -26
  31. data/lib/net/ssh/loggable.rb +2 -2
  32. data/lib/net/ssh/proxy/command.rb +0 -1
  33. data/lib/net/ssh/proxy/socks5.rb +0 -1
  34. data/lib/net/ssh/service/forward.rb +2 -1
  35. data/lib/net/ssh/test.rb +8 -7
  36. data/lib/net/ssh/test/extensions.rb +2 -0
  37. data/lib/net/ssh/transport/algorithms.rb +161 -105
  38. data/lib/net/ssh/transport/cipher_factory.rb +11 -27
  39. data/lib/net/ssh/transport/constants.rb +10 -6
  40. data/lib/net/ssh/transport/ctr.rb +1 -7
  41. data/lib/net/ssh/transport/hmac.rb +15 -13
  42. data/lib/net/ssh/transport/hmac/abstract.rb +16 -0
  43. data/lib/net/ssh/transport/hmac/sha2_256.rb +7 -11
  44. data/lib/net/ssh/transport/hmac/sha2_256_96.rb +4 -8
  45. data/lib/net/ssh/transport/hmac/sha2_256_etm.rb +12 -0
  46. data/lib/net/ssh/transport/hmac/sha2_512.rb +6 -9
  47. data/lib/net/ssh/transport/hmac/sha2_512_96.rb +4 -8
  48. data/lib/net/ssh/transport/hmac/sha2_512_etm.rb +12 -0
  49. data/lib/net/ssh/transport/kex.rb +14 -11
  50. data/lib/net/ssh/transport/kex/abstract.rb +123 -0
  51. data/lib/net/ssh/transport/kex/abstract5656.rb +72 -0
  52. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +38 -0
  53. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +30 -0
  54. data/lib/net/ssh/transport/kex/diffie_hellman_group14_sha1.rb +1 -15
  55. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +9 -118
  56. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +0 -6
  57. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha256.rb +5 -9
  58. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp256.rb +18 -79
  59. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp384.rb +5 -4
  60. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp521.rb +5 -4
  61. data/lib/net/ssh/transport/openssl.rb +106 -93
  62. data/lib/net/ssh/transport/packet_stream.rb +52 -20
  63. data/lib/net/ssh/transport/session.rb +26 -4
  64. data/lib/net/ssh/transport/state.rb +1 -1
  65. data/lib/net/ssh/verifiers/accept_new.rb +7 -0
  66. data/lib/net/ssh/verifiers/always.rb +4 -0
  67. data/lib/net/ssh/verifiers/never.rb +4 -0
  68. data/lib/net/ssh/version.rb +3 -3
  69. data/net-ssh-public_cert.pem +18 -19
  70. data/net-ssh.gemspec +9 -7
  71. metadata +56 -40
  72. metadata.gz.sig +0 -0
  73. data/Gemfile.noed25519.lock +0 -41
  74. data/README.rdoc +0 -169
  75. data/lib/net/ssh/ruby_compat.rb +0 -13
  76. data/support/arcfour_check.rb +0 -20
@@ -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
- if defined?(OpenSSL::PKey::EC)
44
- SUPPORTED_TYPE = %w[ssh-rsa ssh-dss
45
- ecdsa-sha2-nistp256
46
- ecdsa-sha2-nistp384
47
- ecdsa-sha2-nistp521]
48
- else
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
- files += Array(options[:global_known_hosts_file] || %w[/etc/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts2]) if which == :all || which == :global
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
- scanner.string = line
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
- scanner.skip(/\s*/)
134
- next if scanner.match?(/$|#/)
137
+ hostlist = hosts.split(',')
135
138
 
136
- hostlist = scanner.scan(/\S+/).split(/,/)
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
- scanner.skip(/\s*/)
142
- type = scanner.scan(/\S+/)
141
+ found = hostlist.any? { |pattern| match(host_name, pattern) } || known_host_hash?(hostlist, entries)
142
+ next unless found
143
143
 
144
- next unless SUPPORTED_TYPE.include?(type)
144
+ found = hostlist.include?(host_ip) if options[:check_host_ip] && entries.size > 1 && hostlist.size > 1
145
+ next unless found
145
146
 
146
- scanner.skip(/\s*/)
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)
@@ -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.name.gsub(/::/, ".").gsub(/([a-z])([A-Z])/, "\\1_\\2").downcase + "[%x]" % object_id
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
@@ -1,7 +1,6 @@
1
1
  require 'socket'
2
2
  require 'rubygems'
3
3
  require 'net/ssh/proxy/errors'
4
- require 'net/ssh/ruby_compat'
5
4
 
6
5
  module Net
7
6
  module SSH
@@ -1,5 +1,4 @@
1
1
  require 'socket'
2
- require 'net/ssh/ruby_compat'
3
2
  require 'net/ssh/proxy/errors'
4
3
 
5
4
  module Net
@@ -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, remote_port, :string, bind_address, local_port_type, local_port) do |achannel|
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
 
@@ -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: false, proxy: socket(options))
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 #{socket.script.events.length} pending"
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
 
@@ -156,6 +156,8 @@ module Net
156
156
  end
157
157
 
158
158
  raise "no readers were ready for reading, and none had any incoming packets" if processed == 0 && wait != 0
159
+
160
+ [[], [], []]
159
161
  end
160
162
  end
161
163
  end
@@ -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
- # Net::SSH.
28
- ALGORITHMS = {
29
- host_key: %w[ssh-rsa ssh-dss
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
- kex: %w[diffie-hellman-group-exchange-sha1
33
- diffie-hellman-group1-sha1
34
- diffie-hellman-group14-sha1
35
- diffie-hellman-group-exchange-sha256],
36
- encryption: %w[aes128-cbc 3des-cbc blowfish-cbc cast128-cbc
37
- aes192-cbc aes256-cbc rijndael-cbc@lysator.liu.se
38
- idea-cbc arcfour128 arcfour256 arcfour
39
- aes128-ctr aes192-ctr aes256-ctr
40
- cast128-ctr blowfish-ctr 3des-ctr none],
41
-
42
- hmac: %w[hmac-sha1 hmac-md5 hmac-sha1-96 hmac-md5-96
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-sha2-256 hmac-sha2-512 hmac-sha2-256-96
45
- hmac-sha2-512-96 none],
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
- if defined?(OpenSSL::PKey::EC)
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 "ssh-rsa-cert-v01@openssh.com", "ssh-rsa-cert-v00@openssh.com"
184
- "ssh-rsa"
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(supported, options[algorithm], options[:append_all_supported_algorithms])
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
- list << option.first[1..-1]
259
- list.concat(option[1..-1])
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 compression_client compression_server language_client language_server].map do |key|
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
- raise Net::SSH::Exception, "could not settle on #{algorithm} algorithm" if match.nil?
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)