net-ssh 7.3.6 → 8.0.2

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 (68) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/pull_request_template.md +1 -0
  4. data/.github/workflows/changelog.yml +43 -0
  5. data/.github/workflows/ci-with-docker.yml +0 -3
  6. data/.github/workflows/ci.yml +2 -17
  7. data/.github/workflows/rubocop.yml +3 -1
  8. data/.rubocop.yml +5 -1
  9. data/.rubocop_todo.yml +1 -7
  10. data/CHANGES.txt +82 -7
  11. data/CONTRIBUTING.md +35 -0
  12. data/Dockerfile +3 -3
  13. data/Dockerfile.openssl3 +1 -1
  14. data/Dockerfile.zlib_ng +1 -1
  15. data/README.md +71 -57
  16. data/Rakefile +66 -12
  17. data/appveyor.yml +4 -16
  18. data/changelog.d/README.md +1 -0
  19. data/docker-compose.yml +1 -7
  20. data/lib/net/ssh/authentication/agent.rb +35 -11
  21. data/lib/net/ssh/authentication/ed25519.rb +137 -44
  22. data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
  23. data/lib/net/ssh/authentication/key_manager.rb +22 -11
  24. data/lib/net/ssh/authentication/methods/abstract.rb +2 -8
  25. data/lib/net/ssh/authentication/methods/publickey.rb +3 -1
  26. data/lib/net/ssh/authentication/security_key.rb +71 -0
  27. data/lib/net/ssh/authentication/session.rb +29 -17
  28. data/lib/net/ssh/buffer.rb +6 -1
  29. data/lib/net/ssh/buffered_io.rb +40 -3
  30. data/lib/net/ssh/config.rb +28 -12
  31. data/lib/net/ssh/connection/channel.rb +49 -2
  32. data/lib/net/ssh/connection/session.rb +58 -8
  33. data/lib/net/ssh/errors.rb +24 -6
  34. data/lib/net/ssh/key_factory.rb +7 -1
  35. data/lib/net/ssh/known_hosts.rb +18 -8
  36. data/lib/net/ssh/multiplex/client.rb +162 -0
  37. data/lib/net/ssh/multiplex/control_path.rb +120 -0
  38. data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
  39. data/lib/net/ssh/multiplex.rb +126 -0
  40. data/lib/net/ssh/prompt.rb +3 -3
  41. data/lib/net/ssh/proxy/jump.rb +40 -2
  42. data/lib/net/ssh/service/forward.rb +35 -24
  43. data/lib/net/ssh/test.rb +2 -1
  44. data/lib/net/ssh/transport/algorithms.rb +88 -33
  45. data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
  46. data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
  47. data/lib/net/ssh/transport/cipher_factory.rb +23 -1
  48. data/lib/net/ssh/transport/constants.rb +1 -0
  49. data/lib/net/ssh/transport/gcm_cipher.rb +27 -6
  50. data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
  51. data/lib/net/ssh/transport/hmac.rb +2 -0
  52. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
  53. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
  54. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
  55. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
  56. data/lib/net/ssh/transport/openssl.rb +2 -1
  57. data/lib/net/ssh/transport/packet_stream.rb +12 -0
  58. data/lib/net/ssh/transport/session.rb +43 -45
  59. data/lib/net/ssh/verifiers/always.rb +0 -3
  60. data/lib/net/ssh/version.rb +3 -3
  61. data/lib/net/ssh.rb +141 -58
  62. data/net-ssh.gemspec +5 -8
  63. data/rakelib/changelog.rb +229 -0
  64. data.tar.gz.sig +0 -0
  65. metadata +37 -36
  66. metadata.gz.sig +0 -0
  67. data/Gemfile.noed25519 +0 -12
  68. data/Gemfile.norbnacl +0 -12
@@ -0,0 +1,126 @@
1
+ require 'socket'
2
+
3
+ require 'net/ssh/errors'
4
+ require 'net/ssh/connection/session'
5
+ require 'net/ssh/multiplex/client'
6
+ require 'net/ssh/multiplex/control_path'
7
+ require 'net/ssh/multiplex/proxy_transport'
8
+
9
+ module Net
10
+ module SSH
11
+ # Reuse of an existing OpenSSH ControlMaster connection, enabled by the
12
+ # :control_master option of Net::SSH.start. Net::SSH only ever connects to
13
+ # a master someone else started (e.g. with ssh -M or ControlMaster auto);
14
+ # it never becomes one.
15
+ module Multiplex
16
+ # Used for the mux handshake when the :timeout option isn't set.
17
+ DEFAULT_TIMEOUT = 5
18
+
19
+ # Options for setting up a connection, which the master already did
20
+ # with its own settings.
21
+ MASTER_OWNED_OPTIONS = %i[
22
+ agent_socket_factory append_all_supported_algorithms auth_methods
23
+ bind_address check_host_ip compression compression_level encryption
24
+ fingerprint_hash global_known_hosts_file hmac host_key identity_agent
25
+ keepalive keepalive_interval keepalive_maxcount kex key_data keycert_data
26
+ keycerts keys keys_only known_hosts languages minimum_dh_bits no_delay
27
+ non_interactive number_of_password_prompts passphrase password
28
+ password_prompt proxy pubkey_algorithms rekey_blocks_limit rekey_limit
29
+ rekey_packet_limit use_agent user_known_hosts_file verify_host_key
30
+ ].freeze
31
+
32
+ VALUES = [:auto, false, nil].freeze
33
+
34
+ def self.validate_options!(options)
35
+ return if VALUES.include?(options[:control_master])
36
+
37
+ raise ArgumentError, "control_master must be :auto or false, got #{options[:control_master].inspect}"
38
+ end
39
+
40
+ # Returns a Connection::Session running through the ControlMaster for
41
+ # this connection, or nil (after logging why) when there is no usable
42
+ # master, in which case the caller connects as usual. +explicit+ are
43
+ # the option names the caller passed to Net::SSH.start. Raises
44
+ # ArgumentError for an invalid ControlPath, as ssh does.
45
+ def self.connect(host_arg, host, user, options, explicit: [])
46
+ logger = options[:logger]
47
+ path = socket_path(host_arg, host, user, options)
48
+ return nil unless path
49
+
50
+ socket = open_proxy(path, options)
51
+ return nil unless socket
52
+
53
+ logger.info { "control_master: using ControlMaster at #{path}" }
54
+ log_master_owned(logger, explicit)
55
+ begin
56
+ transport = ProxyTransport.new(socket, host: host, control_path: path, options: options)
57
+ Connection::Session.new(transport, session_options(logger, options))
58
+ rescue StandardError
59
+ socket.close
60
+ raise
61
+ end
62
+ end
63
+
64
+ # ENOENT and ECONNREFUSED just mean no master is running, while the
65
+ # other errors mean a socket is there but can't be used.
66
+ def self.open_proxy(path, options)
67
+ Client.new(path, timeout: options[:timeout] || DEFAULT_TIMEOUT, logger: options[:logger]).connect_proxy
68
+ rescue Errno::ENOENT, Errno::ECONNREFUSED => e
69
+ options[:logger].info { "control_master: no ControlMaster at #{path}: #{e.message} (#{e.class})" }
70
+ nil
71
+ rescue Multiplex::Error, SystemCallError, IOError, SocketError => e
72
+ options[:logger].warn { "control_master: not using ControlMaster at #{path}: #{e.message} (#{e.class})" }
73
+ nil
74
+ end
75
+
76
+ def self.socket_path(host_arg, host, user, options)
77
+ if Gem.win_platform? || !defined?(UNIXSocket)
78
+ options[:logger].debug { "control_master: UNIX sockets are not available on this platform" }
79
+ return nil
80
+ end
81
+
82
+ control_path = options[:control_path]
83
+ if control_path.nil? || control_path.to_s == 'none'
84
+ options[:logger].debug { "control_master: no ControlPath set" }
85
+ return nil
86
+ end
87
+
88
+ expander = ControlPath.new(
89
+ host_arg: host_arg, host: host, port: options[:port] || ProxyTransport::DEFAULT_PORT, remote_user: user,
90
+ jump_host: jump_host(options[:proxy]), host_key_alias: options[:host_key_alias]
91
+ )
92
+ begin
93
+ path = expander.expand(control_path.to_s)
94
+ Socket.sockaddr_un(path)
95
+ rescue ArgumentError => e
96
+ raise ArgumentError, "invalid control_path #{control_path.to_s.inspect}: #{e.message}"
97
+ end
98
+ path
99
+ end
100
+
101
+ def self.jump_host(proxy)
102
+ return '' unless defined?(Net::SSH::Proxy::Jump) && proxy.is_a?(Net::SSH::Proxy::Jump)
103
+
104
+ ControlPath.jump_host(proxy.jump_proxies)
105
+ end
106
+
107
+ def self.log_master_owned(logger, explicit)
108
+ bypassed = explicit & MASTER_OWNED_OPTIONS
109
+ return if bypassed.empty?
110
+
111
+ logger.info { "control_master: ignoring #{bypassed.join(', ')}, the ControlMaster's own settings apply" }
112
+ end
113
+
114
+ # Keepalive is a global request, which the master doesn't allow; it
115
+ # watches its own connection anyway. The master serves agent requests
116
+ # from its own agent only if it has ForwardAgent enabled itself.
117
+ def self.session_options(logger, options)
118
+ logger.info { "control_master: keepalive is left to the ControlMaster" } if options[:keepalive]
119
+ logger.info { "control_master: ignoring ForwardAgent, the ControlMaster's own ForwardAgent setting applies" } if options[:forward_agent]
120
+ options.merge(keepalive: false, forward_agent: false)
121
+ end
122
+
123
+ private_class_method :open_proxy, :socket_path, :jump_host, :log_master_owned, :session_options
124
+ end
125
+ end
126
+ end
@@ -41,9 +41,9 @@ module Net
41
41
  def ask(prompt, echo = true)
42
42
  $stdout.print(prompt)
43
43
  $stdout.flush
44
- ret = $stdin.noecho(&:gets).chomp
45
- $stdout.print("\n")
46
- ret
44
+ ret = echo ? $stdin.gets : $stdin.noecho(&:gets)
45
+ $stdout.print("\n") unless echo
46
+ ret.chomp
47
47
  end
48
48
 
49
49
  # success method will be called when the password was accepted
@@ -15,14 +15,33 @@ module Net
15
15
  # Net::SSH.start('host', 'user', :proxy => proxy) do |ssh|
16
16
  # ...
17
17
  # end
18
+ #
19
+ # ssh_config options for the jump host can be given as a Hash via
20
+ # +options:+; each is passed to the ssh command as +-o Key=value+:
21
+ #
22
+ # proxy = Net::SSH::Proxy::Jump.new('user@proxy', options: { 'StrictHostKeyChecking' => 'no' })
23
+ #
24
+ # Options that ssh_config allows more than once (+IdentityFile+,
25
+ # +CertificateFile+ and +SendEnv+) take an Array and are passed once per
26
+ # element; an empty Array passes nothing:
27
+ #
28
+ # proxy = Net::SSH::Proxy::Jump.new('user@proxy', options: { 'IdentityFile' => ['~/.ssh/jump', '~/.ssh/jump_old'] })
29
+ #
30
+ # A +nil+ value, alone or in an Array, raises ArgumentError, as it does
31
+ # for the options passed to Net::SSH.start; omit the option instead.
32
+ #
33
+ # The options apply to the first jump host only; any further hosts in
34
+ # the chain are connected to by that host's own ssh.
18
35
  class Jump < Command
19
36
  # The jump proxies
20
37
  attr_reader :jump_proxies
21
38
 
22
39
  # Create a new socket factory that tunnels via multiple jump proxes as
23
40
  # [user@]host[:port].
24
- def initialize(jump_proxies)
41
+ def initialize(jump_proxies, options: {})
25
42
  @jump_proxies = jump_proxies
43
+ @options = options
44
+ validate_options!
26
45
  end
27
46
 
28
47
  # Return a new socket connected to the given host and port via the jump
@@ -43,7 +62,10 @@ module Net
43
62
  template << " -l #{uri.user}" if uri.user
44
63
  template << " -p #{uri.port}" if uri.port
45
64
  template << " -J #{extra_jumps}" if extra_jumps
46
- template << " -F #{Shellwords.escape(config)}" if config != true && config
65
+ template << " -F #{escape(config)}" if config != true && config
66
+ @options.each do |key, values|
67
+ Array(values).each { |value| template << " -o #{escape(key)}=#{escape(value)}" }
68
+ end
47
69
  template << " -W %h:%p "
48
70
  template << uri.host
49
71
 
@@ -52,6 +74,22 @@ module Net
52
74
 
53
75
  private
54
76
 
77
+ # Like Net::SSH.start, reject nil option values (alone or in an Array)
78
+ # rather than silently passing nothing or an empty value to ssh.
79
+ def validate_options!
80
+ nil_keys = @options.select { |_key, value| value.nil? || (value.is_a?(Array) && value.include?(nil)) }.keys
81
+ return if nil_keys.empty?
82
+
83
+ raise ArgumentError, "nil is not allowed for #{nil_keys.map(&:inspect).join(', ')} in Net::SSH::Proxy::Jump options; " \
84
+ "omit the option to use the default or ssh_config value"
85
+ end
86
+
87
+ # Escape for the shell, and double any % so it survives the %h/%p
88
+ # substitution done on the template in Command#open.
89
+ def escape(value)
90
+ Shellwords.escape(value).gsub("%", "%%")
91
+ end
92
+
55
93
  def validate_jump!(jump)
56
94
  uri = URI.parse("ssh://#{jump}")
57
95
  validate_user_name!(uri.user) if uri.user
@@ -320,6 +320,11 @@ module Net
320
320
  session.listen_to(client)
321
321
  channel[:socket] = client
322
322
 
323
+ # Grant window only for data the local end has accepted, so a slow
324
+ # reader stalls the remote sender instead of filling our memory.
325
+ channel.grant_window_on_consume!
326
+ client.on_sent { |bytes| channel.consumed(bytes) }
327
+
323
328
  channel.on_data do |ch, data|
324
329
  debug { "data:#{data.length} on #{type} forwarded channel" }
325
330
  ch[:socket].enqueue(data)
@@ -327,36 +332,40 @@ module Net
327
332
 
328
333
  channel.on_eof do |ch|
329
334
  debug { "eof #{type} on #{type} forwarded channel" }
330
- begin
331
- ch[:socket].send_pending
332
- ch[:socket].shutdown Socket::SHUT_WR
333
- rescue IOError => e
334
- if e.message =~ /closed/ then
335
- debug { "epipe in on_eof => shallowing exception:#{e}" }
336
- else
337
- raise
338
- end
339
- rescue Errno::EPIPE => e
340
- debug { "epipe in on_eof => shallowing exception:#{e}" }
341
- rescue Errno::ENOTCONN => e
342
- debug { "enotconn in on_eof => shallowing exception:#{e}" }
343
- end
335
+ ch[:socket].shutdown_write_when_drained
344
336
  end
345
337
 
346
338
  channel.on_close do |ch|
347
339
  debug { "closing #{type} forwarded channel" }
348
- ch[:socket].close if !client.closed?
349
- session.stop_listening_to(ch[:socket])
340
+ session.close_when_drained(ch[:socket])
350
341
  end
351
342
 
352
343
  channel.on_process do |ch|
353
- if ch[:socket].closed?
344
+ socket = ch[:socket]
345
+ if socket.closed?
354
346
  ch.info { "#{type} forwarded connection closed" }
347
+ session.stop_listening_to(socket)
355
348
  ch.close
356
- elsif ch[:socket].available > 0
357
- data = ch[:socket].read_available(8192)
358
- ch.debug { "read #{data.length} bytes from client, sending over #{type} forwarded connection" }
359
- ch.send_data(data)
349
+ elsif ch.eof?
350
+ # Like OpenSSH's chan_shutdown_read: data already read still goes out before CHANNEL_EOF, the rest is dropped.
351
+ ch.output.append(socket.read_available) if socket.available > 0
352
+ session.pause_reading(socket)
353
+ else
354
+ if socket.available > 0
355
+ data = socket.read_available(8192)
356
+ ch.debug { "read #{data.length} bytes from client, sending over #{type} forwarded connection" }
357
+ ch.send_data(data)
358
+ end
359
+
360
+ # Like OpenSSH's channel_pre_open, only read more once the remote
361
+ # window can take what is already queued; writes to the socket keep
362
+ # flowing. ch.output is still unsent here because Channel#process
363
+ # runs this block before enqueue_pending_output.
364
+ if ch.output.length + socket.available < ch.remote_window_size.to_i
365
+ session.resume_reading(socket)
366
+ else
367
+ session.pause_reading(socket)
368
+ end
360
369
  end
361
370
  end
362
371
  end
@@ -371,6 +380,8 @@ module Net
371
380
  end
372
381
 
373
382
  channel.on_process do |ch|
383
+ next if ch.eof?
384
+
374
385
  data = ch[:socket].read(8192)
375
386
  if data
376
387
  ch.debug { "read #{data.length} bytes from client, sending over #{type} forwarded connection" }
@@ -388,13 +399,13 @@ module Net
388
399
  originator_address = packet.read_string
389
400
  originator_port = packet.read_long
390
401
 
391
- puts "REMOTE 0: #{connected_port} #{connected_address} #{originator_address} #{originator_port}"
402
+ debug { "REMOTE 0: #{connected_port} #{connected_address} #{originator_address} #{originator_port}" }
392
403
  remote = @remote_forwarded_ports[[connected_port, connected_address]]
393
404
  if remote.nil?
394
405
  raise Net::SSH::ChannelOpenFailed.new(1, "unknown request from remote forwarded connection on #{connected_address}:#{connected_port}")
395
406
  end
396
407
 
397
- puts "REMOTE: #{remote.host} #{remote.port}"
408
+ debug { "REMOTE: #{remote.host} #{remote.port}" }
398
409
  client = TCPSocket.new(remote.host, remote.port)
399
410
  info { "connected #{connected_address}:#{connected_port} originator #{originator_address}:#{originator_port}" }
400
411
 
@@ -415,7 +426,7 @@ module Net
415
426
  channel[:invisible] = true
416
427
 
417
428
  begin
418
- agent = Authentication::Agent.connect(logger, session.options[:agent_socket_factory])
429
+ agent = Authentication::Agent.connect(logger, session.options[:agent_socket_factory], session.options[:identity_agent])
419
430
  if (agent.socket.is_a? ::IO)
420
431
  prepare_client(agent.socket, channel, :agent)
421
432
  else
data/lib/net/ssh/test.rb CHANGED
@@ -73,7 +73,8 @@ module Net
73
73
  def transport(options = {})
74
74
  @transport ||= Net::SSH::Transport::Session.new(
75
75
  options[:host] || "localhost",
76
- options.merge(kex: "test", host_key: "ssh-rsa", append_all_supported_algorithms: true, verify_host_key: :never, proxy: socket(options))
76
+ options.merge(kex: "test", host_key: "ssh-rsa", encryption: "+none", hmac: "+none",
77
+ append_all_supported_algorithms: true, verify_host_key: :never, proxy: socket(options))
77
78
  )
78
79
  end
79
80
 
@@ -8,6 +8,7 @@ require 'net/ssh/transport/kex'
8
8
  require 'net/ssh/transport/kex/curve25519_sha256_loader'
9
9
  require 'net/ssh/transport/server_version'
10
10
  require 'net/ssh/authentication/ed25519_loader'
11
+ require 'net/ssh/authentication/security_key'
11
12
 
12
13
  module Net
13
14
  module SSH
@@ -19,7 +20,7 @@ module Net
19
20
  #
20
21
  # You will never instantiate or reference this directly. It is used
21
22
  # internally by the transport layer.
22
- class Algorithms
23
+ class Algorithms # rubocop:disable Metrics/ClassLength
23
24
  include Loggable
24
25
  include Constants
25
26
 
@@ -31,18 +32,14 @@ module Net
31
32
  ecdsa-sha2-nistp521
32
33
  ecdsa-sha2-nistp384
33
34
  ecdsa-sha2-nistp256
34
- ssh-rsa-cert-v01@openssh.com
35
- ssh-rsa-cert-v00@openssh.com
36
- ssh-rsa
37
- rsa-sha2-256
38
- rsa-sha2-512],
35
+ rsa-sha2-512
36
+ rsa-sha2-256],
39
37
 
40
38
  kex: %w[ecdh-sha2-nistp521
41
39
  ecdh-sha2-nistp384
42
40
  ecdh-sha2-nistp256
43
41
  diffie-hellman-group-exchange-sha256
44
- diffie-hellman-group14-sha256
45
- diffie-hellman-group14-sha1],
42
+ diffie-hellman-group14-sha256],
46
43
 
47
44
  encryption: %w[aes256-ctr
48
45
  aes192-ctr
@@ -51,8 +48,18 @@ module Net
51
48
  aes128-gcm@openssh.com],
52
49
 
53
50
  hmac: %w[hmac-sha2-512-etm@openssh.com hmac-sha2-256-etm@openssh.com
51
+ hmac-sha1-etm@openssh.com
54
52
  hmac-sha2-512 hmac-sha2-256
55
- hmac-sha1]
53
+ hmac-sha1],
54
+
55
+ pubkey_algorithms: %w[ssh-ed25519-cert-v01@openssh.com
56
+ ecdsa-sha2-nistp256-cert-v01@openssh.com ecdsa-sha2-nistp384-cert-v01@openssh.com
57
+ ecdsa-sha2-nistp521-cert-v01@openssh.com
58
+ rsa-sha2-512-cert-v01@openssh.com rsa-sha2-256-cert-v01@openssh.com
59
+ ssh-ed25519
60
+ ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521] +
61
+ [Authentication::SecurityKey::SK_SSH_ED25519, Authentication::SecurityKey::SK_ECDSA_SHA2_NISTP256] +
62
+ %w[rsa-sha2-512 rsa-sha2-256]
56
63
  }.freeze
57
64
 
58
65
  if Net::SSH::Transport::ChaCha20Poly1305CipherLoader::LOADED
@@ -76,10 +83,12 @@ module Net
76
83
 
77
84
  # Define all algorithms, with the deprecated, supported by Net::SSH.
78
85
  ALGORITHMS = {
79
- host_key: DEFAULT_ALGORITHMS[:host_key] + %w[ssh-dss],
86
+ host_key: DEFAULT_ALGORITHMS[:host_key] +
87
+ %w[ssh-rsa-cert-v01@openssh.com ssh-rsa-cert-v00@openssh.com ssh-rsa ssh-dss],
80
88
 
81
89
  kex: DEFAULT_ALGORITHMS[:kex] +
82
- %w[diffie-hellman-group-exchange-sha1
90
+ %w[diffie-hellman-group14-sha1
91
+ diffie-hellman-group-exchange-sha1
83
92
  diffie-hellman-group1-sha1],
84
93
 
85
94
  encryption: DEFAULT_ALGORITHMS[:encryption] +
@@ -98,10 +107,20 @@ module Net
98
107
  hmac-md5 hmac-md5-96
99
108
  none],
100
109
 
110
+ pubkey_algorithms: DEFAULT_ALGORITHMS[:pubkey_algorithms] + %w[ssh-rsa-cert-v01@openssh.com ssh-rsa ssh-dss],
111
+
101
112
  compression: %w[none zlib@openssh.com zlib],
102
113
  language: %w[]
103
114
  }.freeze
104
115
 
116
+ # Not added by append_all_supported_algorithms; only used when named explicitly.
117
+ EXPLICIT_ONLY_ALGORITHMS = {
118
+ encryption: %w[none],
119
+ hmac: %w[none]
120
+ }.freeze
121
+
122
+ DEPRECATED_HOST_KEY = 'ssh-rsa-cert-v00@openssh.com'.freeze
123
+
105
124
  # The underlying transport layer session that supports this object
106
125
  attr_reader :session
107
126
 
@@ -269,11 +288,16 @@ module Net
269
288
 
270
289
  ALGORITHMS.each do |algorithm, supported|
271
290
  algorithms[algorithm] = compose_algorithm_list(
272
- supported, options[algorithm] || DEFAULT_ALGORITHMS[algorithm],
273
- options[:append_all_supported_algorithms]
291
+ supported,
292
+ DEFAULT_ALGORITHMS[algorithm],
293
+ options[algorithm] || DEFAULT_ALGORITHMS[algorithm],
294
+ options[:append_all_supported_algorithms],
295
+ supported - EXPLICIT_ONLY_ALGORITHMS.fetch(algorithm, [])
274
296
  )
275
297
  end
276
298
 
299
+ warn_deprecated_host_key if host_key_explicitly_named?(DEPRECATED_HOST_KEY)
300
+
277
301
  # for convention, make sure our list has the same keys as the server
278
302
  # list
279
303
 
@@ -287,7 +311,11 @@ module Net
287
311
  # existing known key for the host has preference.
288
312
 
289
313
  existing_keys = session.host_keys
290
- host_keys = existing_keys.flat_map { |key| key.respond_to?(:ssh_types) ? key.ssh_types : [key.ssh_type] }.uniq
314
+ host_keys = existing_keys.flat_map { |key| key.respond_to?(:ssh_types) ? key.ssh_types : [key.ssh_type] }
315
+ # A known ssh-rsa key also verifies with rsa-sha2-*, and must not re-enable disabled SHA-1 algorithms.
316
+ disabled_sha1 = %w[ssh-rsa ssh-rsa-cert-v01@openssh.com ssh-rsa-cert-v00@openssh.com] - algorithms[:host_key]
317
+ host_keys = host_keys.flat_map { |type| type == 'ssh-rsa' ? %w[rsa-sha2-512 rsa-sha2-256 ssh-rsa] : [type] }
318
+ host_keys = (host_keys - disabled_sha1).uniq
291
319
  algorithms[:host_key].each do |name|
292
320
  host_keys << name unless host_keys.include?(name)
293
321
  end
@@ -296,21 +324,27 @@ module Net
296
324
  end
297
325
 
298
326
  # Composes the list of algorithms by taking supported algorithms and matching with supplied options.
299
- def compose_algorithm_list(supported, option, append_all_supported_algorithms = false)
327
+ def compose_algorithm_list(supported, defaults, option, append_all_supported_algorithms = false, appendable = supported)
300
328
  return supported.dup unless option
301
329
 
302
330
  list = []
303
331
  option = Array(option).compact.uniq
304
332
 
305
- if option.first && option.first.start_with?('+', '-')
306
- list = supported.dup
307
-
308
- appends = option.select { |opt| opt.start_with?('+') }.map { |opt| opt[1..-1] }
309
- deletions = option.select { |opt| opt.start_with?('-') }.map { |opt| opt[1..-1] }
333
+ if option.first && option.first.start_with?('+', '-', '^')
334
+ ops = { '^' => [], '+' => [], '-' => [] }
335
+ cur_list = nil
336
+ option.each do |cur_opt|
337
+ if ops.key?(cur_opt[0])
338
+ cur_list = ops[cur_opt[0]]
339
+ cur_opt = cur_opt[1..-1]
340
+ end
341
+ cur_list << cur_opt
342
+ end
310
343
 
311
- list.concat(appends)
344
+ list = ops['^'] + defaults + ops['+']
345
+ list.concat(appendable) if append_all_supported_algorithms
312
346
 
313
- deletions.each do |opt|
347
+ ops['-'].each do |opt|
314
348
  if opt.include?('*')
315
349
  opt_escaped = Regexp.escape(opt)
316
350
  algo_re = /\A#{opt_escaped.gsub('\*', '[A-Za-z\d\-@\.]*')}\z/
@@ -325,7 +359,7 @@ module Net
325
359
  list = option
326
360
 
327
361
  if append_all_supported_algorithms
328
- supported.each { |name| list << name unless list.include?(name) }
362
+ appendable.each { |name| list << name unless list.include?(name) }
329
363
  end
330
364
  end
331
365
 
@@ -341,22 +375,35 @@ module Net
341
375
  list
342
376
  end
343
377
 
378
+ def host_key_explicitly_named?(name)
379
+ algorithms[:host_key].include?(name) &&
380
+ Array(options[:host_key]).any? { |opt| opt.to_s.delete_prefix('+').delete_prefix('^') == name }
381
+ end
382
+
383
+ def warn_deprecated_host_key
384
+ return if @deprecated_host_key_warned
385
+
386
+ @deprecated_host_key_warned = true
387
+ Kernel.warn("#{DEPRECATED_HOST_KEY} is deprecated and will be removed in a future version; " \
388
+ "net-ssh cannot parse v00 certificates, use ssh-rsa-cert-v01@openssh.com or a modern host key type instead")
389
+ end
390
+
344
391
  # Parses a KEXINIT packet from the server.
345
392
  def parse_server_algorithm_packet(packet)
346
393
  data = { raw: packet.content }
347
394
 
348
395
  packet.read(16) # skip the cookie value
349
396
 
350
- data[:kex] = packet.read_string.split(/,/)
351
- data[:host_key] = packet.read_string.split(/,/)
352
- data[:encryption_client] = packet.read_string.split(/,/)
353
- data[:encryption_server] = packet.read_string.split(/,/)
354
- data[:hmac_client] = packet.read_string.split(/,/)
355
- data[:hmac_server] = packet.read_string.split(/,/)
356
- data[:compression_client] = packet.read_string.split(/,/)
357
- data[:compression_server] = packet.read_string.split(/,/)
358
- data[:language_client] = packet.read_string.split(/,/)
359
- data[:language_server] = packet.read_string.split(/,/)
397
+ data[:kex] = read_name_list(packet)
398
+ data[:host_key] = read_name_list(packet)
399
+ data[:encryption_client] = read_name_list(packet)
400
+ data[:encryption_server] = read_name_list(packet)
401
+ data[:hmac_client] = read_name_list(packet)
402
+ data[:hmac_server] = read_name_list(packet)
403
+ data[:compression_client] = read_name_list(packet)
404
+ data[:compression_server] = read_name_list(packet)
405
+ data[:language_client] = read_name_list(packet)
406
+ data[:language_server] = read_name_list(packet)
360
407
 
361
408
  # TODO: if first_kex_packet_follows, we need to try to skip the
362
409
  # actual kexinit stuff and try to guess what the server is doing...
@@ -366,6 +413,13 @@ module Net
366
413
  return data
367
414
  end
368
415
 
416
+ # Reads one comma-separated algorithm name-list from a KEXINIT packet.
417
+ def read_name_list(packet)
418
+ list = packet.read_string or raise Net::SSH::Exception, "malformed KEXINIT packet from server"
419
+
420
+ list.split(/,/)
421
+ end
422
+
369
423
  # Given the #algorithms map of preferred algorithm types, this constructs
370
424
  # a KEXINIT packet to send to the server. It does not actually send it,
371
425
  # it simply builds the packet and returns it.
@@ -391,6 +445,7 @@ module Net
391
445
  def negotiate_algorithms
392
446
  @kex = negotiate(:kex)
393
447
  @host_key = negotiate(:host_key)
448
+ warn_deprecated_host_key if @host_key == DEPRECATED_HOST_KEY
394
449
  @encryption_client = negotiate(:encryption_client)
395
450
  @encryption_server = negotiate(:encryption_server)
396
451
  @hmac_client = negotiate(:hmac_client)