net-ssh 8.0.0.rc2 → 8.0.0.rc4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd6a3f2cc54c2b16873c1bcd92038d6c70e09581fd29c5a6674b708a76c9fec9
4
- data.tar.gz: 80fd4270605a25552b4cf7c0ca0c73b358f3806bd3a6654d15b94682fce0ff81
3
+ metadata.gz: 475db99174313b32d90dc650a358dd0cbec01ae59b0daca8f2e02eeb97a8cb9e
4
+ data.tar.gz: 9cdb86ac170fa3b3ca90357a106bc9742210dd1eae61e7525f4b36f71342a590
5
5
  SHA512:
6
- metadata.gz: 0cd7d51126432199154b9aeeaf28dd6f0df05c6673c4ccf580b27a84c54d6865500b6ff4e2814e9242cc7dc882d3eec7d95000856b5b732dab97d493d1eabe83
7
- data.tar.gz: abce0bd3fcd5b47f74cc9fae9e684282e927b3af17c424e265f87aaf41fb85afbda631a588f080687804102a75692fa2086b8e82bda4af2d99ae824f2251b33d
6
+ metadata.gz: 6ada5144a2d5b86aae33b317a42a915b9107731b921fd01bfac3678da06499e9e1fd9c5f4bb310787f345ff8279445c82b00fb129c1837341d40fe7f9eea1779
7
+ data.tar.gz: 0cf535957b80c52960df55d93073efff00aa7cf89a9e7323a4dc38e59833ae0bf3a2e419c032c282ffe38bb14234486436c4ff151931dff73d0330229a83508c
checksums.yaml.gz.sig CHANGED
Binary file
@@ -1,6 +1,8 @@
1
1
  name: Rubocop
2
2
 
3
- on: [push]
3
+ on:
4
+ pull_request:
5
+ push: { branches: master }
4
6
 
5
7
  jobs:
6
8
  build:
data/CHANGES.txt CHANGED
@@ -1,3 +1,25 @@
1
+ === 8.0.0 rc4
2
+
3
+ * Security: RSA signatures using SHA-1 (`ssh-rsa`, `ssh-rsa-cert-v01@openssh.com`) are no longer offered by default for host keys or public key authentication, as in OpenSSH since 8.8; RSA keys keep working via rsa-sha2-256/512. For servers that only support SHA-1 add `HostKeyAlgorithms +ssh-rsa` / `PubkeyAcceptedAlgorithms +ssh-rsa` in ssh_config (or `host_key: '+ssh-rsa'` / `pubkey_algorithms: %w[+ssh-rsa]`), or pass `append_all_supported_algorithms: true` [#1039]
4
+ * Security: ssh_config `PubkeyAcceptedAlgorithms` (and its old name `PubkeyAcceptedKeyTypes`) is now honoured, including `+`, `-` and `^`, and `pubkey_algorithms` now applies to all key types instead of only RSA, adding `rsa-sha2-512`; DSA (`ssh-dss`) keys are no longer offered by default, as in OpenSSH since 7.0 — add `+ssh-dss` or pass `append_all_supported_algorithms: true` to re-enable [#923]
5
+ * Security: ssh_config `Ciphers`, `MACs`, `KexAlgorithms` and `HostKeyAlgorithms` lists starting with `+`, `-` or `^` now append to, remove from or prepend to the default list, as in OpenSSH, instead of silently enabling every supported algorithm including weak ones that are off by default; every listed item is honoured and `-` accepts wildcards. Pass `append_all_supported_algorithms: true` to also get the full supported list [#925]
6
+ * Support agent-backed FIDO/U2F security keys (sk-ecdsa-sha2-nistp256@openssh.com, sk-ssh-ed25519@openssh.com) for publickey auth [#1007]
7
+ * Port forwarding no longer buffers a whole stream in memory when one side reads slowly: like OpenSSH, window is granted only as data is written to the local socket, and the local socket is read only while the remote window has room. An application that writes everything to a forwarded port before reading anything now blocks once the window fills, as it would with `ssh -L` or a direct TCP connection; the old unbounded buffering hid this [#1037, #1003]
8
+ * Data forwarded to a local socket just before the remote end sends EOF or closes the channel is no longer truncated or dropped: it is written out before the socket is shut down or closed, without blocking the event loop until the local application reads [#1037]
9
+ * Multi-factor authentication works when the server's AuthenticationMethods order differs from net-ssh's: after each partial success the allowed methods are tried again, as OpenSSH does [#873, #872]
10
+ * `HostKeyError#host`, `#port` and `#ip` now return the connected host, port and IP (`ip` is nil through a ProxyCommand) instead of always nil [#963]
11
+ * OpenSSH-format private keys with Windows (CRLF) line endings now load [#922, #921]
12
+ * Send CHANNEL_EOF before CHANNEL_CLOSE when closing a session's remaining channels, as OpenSSH does; fixes hangs at close with servers that wait for EOF, such as some SFTP servers [#672]
13
+ * ssh_config: trailing `# comments` are now ignored, as in OpenSSH; a `#` inside quotes or in the middle of a word is kept [#990]
14
+
15
+ === 8.0.0 rc3
16
+
17
+ * Fix "Calling Net::SSH::Buffer methods on HostKeyEntries PubKey is deprecated" warnings printed on every connection to a host in known_hosts [#1026, #1027]
18
+ * `ProxyJump none` now means no proxy instead of jumping through a host named "none", and `ProxyCommand none` is matched case-insensitively, as in OpenSSH [#893, #972, #1030]
19
+ * Send SIGHUP to the ProxyCommand process on close, as OpenSSH does, so tunnels such as EC2 Instance Connect Endpoint no longer hang at exit [#947]
20
+ * Reconnect when the ssh-agent closes the connection on the legacy version request (e.g. older Proton Pass) instead of raising FrozenError [#1008]
21
+ * ssh_config: parse `Key = value` lines like OpenSSH (the value no longer keeps the leading space, and the keyword ends at the first `=`), and honor ProxyJump/ProxyCommand before the first Host [#1034]
22
+
1
23
  === 8.0.0 rc2
2
24
 
3
25
  * Security: refuse agent channels opened by the server unless agent forwarding was requested (`forward_agent`, ForwardAgent, or `Forward#agent`); any use of port forwarding let a malicious server use the local ssh-agent [GHSA-qpg5-fx6p-8cq3]
data/README.md CHANGED
@@ -39,13 +39,17 @@ Unsecure algoritms will definitely be removed in Net::SSH 8.*.
39
39
 
40
40
  | Name | Support | Details |
41
41
  |----------------------|-----------------------|----------|
42
- | ssh-rsa | OK | |
42
+ | rsa-sha2-512 | OK | |
43
+ | rsa-sha2-256 | OK | |
44
+ | ssh-rsa | Disabled by default in 8.0 | SHA-1 signatures, off by default as in OpenSSH 8.8; enable with `HostKeyAlgorithms +ssh-rsa` or `host_key: '+ssh-rsa'` |
43
45
  | ssh-ed25519 | OK | Backed by Ruby OpenSSL raw Ed25519 APIs (`openssl >= 3.2.0`) |
44
46
  | ecdsa-sha2-nistp521 | OK | [using weak elliptic curves](https://safecurves.cr.yp.to/) |
45
47
  | ecdsa-sha2-nistp384 | OK | [using weak elliptic curves](https://safecurves.cr.yp.to/) |
46
48
  | ecdsa-sha2-nistp256 | OK | [using weak elliptic curves](https://safecurves.cr.yp.to/) |
47
49
  | ssh-dss | Deprecated in 6.0 | unsecure, will be removed in 8.0 |
48
50
 
51
+ RSA public key authentication likewise uses rsa-sha2-512 or rsa-sha2-256 by default; ssh-rsa and ssh-dss keys are only offered when enabled, e.g. with `PubkeyAcceptedAlgorithms +ssh-rsa` or `pubkey_algorithms: %w[+ssh-rsa]`.
52
+
49
53
  ### Key Exchange
50
54
 
51
55
  | Name | Support | Details |
@@ -194,6 +198,11 @@ Ed25519 public key auth is backed by Ruby OpenSSL. The `bcrypt_pbkdf` gem is onl
194
198
  gem install bcrypt_pbkdf
195
199
  ```
196
200
 
201
+ ### FIDO/U2F security keys
202
+
203
+ `sk-ecdsa-sha2-nistp256@openssh.com` and `sk-ssh-ed25519@openssh.com` keys work when held in `ssh-agent`.
204
+ Loading `sk-*` private key files from disk is not supported.
205
+
197
206
  ## RUBY SUPPORT
198
207
 
199
208
  * See [net-ssh.gemspec](https://github.com/net-ssh/net-ssh/blob/master/net-ssh.gemspec) for current versions ruby requirements
@@ -17,6 +17,9 @@ module Net
17
17
  # An exception for indicating that the SSH agent is not available.
18
18
  class AgentNotAvailable < AgentError; end
19
19
 
20
+ # Raised when the agent closes the connection before replying.
21
+ class AgentClosedConnection < AgentError; end
22
+
20
23
  # This class implements a simple client for the ssh-agent protocol. It
21
24
  # does not implement any specific protocol, but instead copies the
22
25
  # behavior of the ssh-agent functions in the OpenSSH library (3.8).
@@ -83,12 +86,18 @@ module Net
83
86
  # socket reports that it is an SSH2-compatible agent, this will fail
84
87
  # (it only supports the ssh-agent distributed by OpenSSH).
85
88
  def connect!(agent_socket_factory = nil, identity_agent = nil)
89
+ @agent_socket_factory = agent_socket_factory
90
+ @identity_agent = identity_agent
91
+ open_agent_socket!
92
+ end
93
+
94
+ def open_agent_socket!
86
95
  debug { "connecting to ssh-agent" }
87
96
  @socket =
88
- if agent_socket_factory
89
- agent_socket_factory.call
90
- elsif identity_agent
91
- unix_socket_class.open(File.expand_path(identity_agent))
97
+ if @agent_socket_factory
98
+ @agent_socket_factory.call
99
+ elsif @identity_agent
100
+ unix_socket_class.open(File.expand_path(@identity_agent))
92
101
  elsif ENV['SSH_AUTH_SOCK'] && unix_socket_class
93
102
  unix_socket_class.open(File.expand_path(ENV['SSH_AUTH_SOCK']))
94
103
  elsif Gem.win_platform? && RUBY_ENGINE != "jruby"
@@ -100,6 +109,7 @@ module Net
100
109
  error { "could not connect to ssh-agent: #{e.message}" }
101
110
  raise AgentNotAvailable, $!.message
102
111
  end
112
+ private :open_agent_socket!
103
113
 
104
114
  # Attempts to negotiate the SSH agent protocol version. Raises an error
105
115
  # if the version could not be negotiated successfully.
@@ -114,6 +124,11 @@ module Net
114
124
  elsif type != SSH_AGENT_RSA_IDENTITIES_ANSWER1 && type != SSH_AGENT_RSA_IDENTITIES_ANSWER2
115
125
  raise AgentNotAvailable, "unknown response from agent: #{type}, #{body.to_s.inspect}"
116
126
  end
127
+ rescue AgentClosedConnection
128
+ # Agents built on ssh-agent-lib <= 0.6.0 (e.g. older Proton Pass) drop the connection on
129
+ # this legacy request but handle the rest of the protocol (#1008).
130
+ debug { "ssh-agent closed the connection on the version request; assuming a modern agent and reconnecting" }
131
+ open_agent_socket!
117
132
  end
118
133
 
119
134
  # Return an array of all identities (public keys) known to the agent.
@@ -223,8 +238,14 @@ module Net
223
238
  # tuple consisting of the packet type, and the packet's body (which
224
239
  # is returned as a Net::SSH::Buffer).
225
240
  def read_packet
226
- buffer = Net::SSH::Buffer.new(@socket.read(4))
227
- buffer.append(@socket.read(buffer.read_long))
241
+ header = @socket.read(4)
242
+ raise AgentClosedConnection, "agent closed the connection" if header.nil?
243
+
244
+ buffer = Net::SSH::Buffer.new(header)
245
+ body = @socket.read(buffer.read_long)
246
+ raise AgentClosedConnection, "agent closed the connection" if body.nil?
247
+
248
+ buffer.append(body)
228
249
  type = buffer.read_byte
229
250
  debug { "received agent packet #{type} len #{buffer.length - 4}" }
230
251
  return type, buffer
@@ -74,7 +74,7 @@ module Net
74
74
  end
75
75
 
76
76
  def self.read(datafull, password)
77
- datafull = datafull.strip
77
+ datafull = datafull.strip.encode(universal_newline: true)
78
78
  raise ArgumentError.new("Expected #{MBEGIN} at start of private key") unless datafull.start_with?(MBEGIN)
79
79
  raise ArgumentError.new("Expected #{MEND} at end of private key") unless datafull.end_with?(MEND)
80
80
 
@@ -20,9 +20,7 @@ module Net
20
20
  # this.
21
21
  attr_reader :key_manager
22
22
 
23
- # So far only affects algorithms used for rsa keys, but can be
24
- # extended to other keys, e.g after reading of
25
- # PubkeyAcceptedAlgorithms option from ssh_config file is implemented.
23
+ # public key algorithms to be used
26
24
  attr_reader :pubkey_algorithms
27
25
 
28
26
  # Instantiates a new authentication method.
@@ -31,11 +29,7 @@ module Net
31
29
  @key_manager = options[:key_manager]
32
30
  @options = options
33
31
  @prompt = options[:password_prompt]
34
- @pubkey_algorithms = options[:pubkey_algorithms] \
35
- || %w[rsa-sha2-256-cert-v01@openssh.com
36
- ssh-rsa-cert-v01@openssh.com
37
- rsa-sha2-256
38
- ssh-rsa]
32
+ @pubkey_algorithms = session.transport.pubkey_algorithms
39
33
  self.logger = session.logger
40
34
  end
41
35
 
@@ -15,6 +15,8 @@ module Net
15
15
  def authenticate(next_service, username, password = nil)
16
16
  return false unless key_manager
17
17
 
18
+ debug { "pubkey_algorithms: #{pubkey_algorithms.join(', ')}" }
19
+
18
20
  key_manager.each_identity do |identity|
19
21
  return true if authenticate_with(identity, next_service, username)
20
22
  end
@@ -123,7 +125,7 @@ module Net
123
125
  end
124
126
  end
125
127
  end
126
- elsif authenticate_with_alg(identity, next_service, username, type)
128
+ elsif pubkey_algorithms.include?(type) && authenticate_with_alg(identity, next_service, username, type)
127
129
  # success
128
130
  return true
129
131
  end
@@ -0,0 +1,70 @@
1
+ require 'net/ssh/authentication/pub_key_fingerprint'
2
+
3
+ module Net
4
+ module SSH
5
+ module Authentication
6
+ # FIDO/U2F security keys held by ssh-agent. The agent does the signing (incl. user
7
+ # presence); net-ssh only parses and re-serialises the public key blob (see PROTOCOL.u2f).
8
+ module SecurityKey
9
+ SK_ECDSA_SHA2_NISTP256 = "sk-ecdsa-sha2-nistp256@openssh.com"
10
+ SK_SSH_ED25519 = "sk-ssh-ed25519@openssh.com"
11
+ TYPES = [SK_ECDSA_SHA2_NISTP256, SK_SSH_ED25519].freeze
12
+
13
+ def self.understands?(type)
14
+ TYPES.include?(type)
15
+ end
16
+
17
+ # Public key of an agent-held security key; signing is delegated to the agent.
18
+ class PubKey
19
+ include Net::SSH::Authentication::PubKeyFingerprint
20
+
21
+ attr_reader :ssh_type, :curve, :public_key_data, :application
22
+
23
+ # Wire formats:
24
+ # sk-ecdsa-sha2-nistp256@openssh.com: string curve, string Q, string application
25
+ # sk-ssh-ed25519@openssh.com: string public_key, string application
26
+ def self.read_keyblob(type, buffer)
27
+ case type
28
+ when SK_ECDSA_SHA2_NISTP256
29
+ curve = buffer.read_string
30
+ public_key_data = buffer.read_string
31
+ application = buffer.read_string
32
+ new(type, public_key_data, application, curve: curve)
33
+ when SK_SSH_ED25519
34
+ public_key_data = buffer.read_string
35
+ application = buffer.read_string
36
+ new(type, public_key_data, application)
37
+ else
38
+ raise NotImplementedError, "unsupported security key type `#{type}'"
39
+ end
40
+ end
41
+
42
+ def initialize(ssh_type, public_key_data, application, curve: nil)
43
+ @ssh_type = ssh_type
44
+ @public_key_data = public_key_data
45
+ @application = application
46
+ @curve = curve
47
+ end
48
+
49
+ def to_blob
50
+ buffer = Net::SSH::Buffer.new
51
+ buffer.write_string(ssh_type)
52
+ buffer.write_string(curve) if curve
53
+ buffer.write_string(public_key_data)
54
+ buffer.write_string(application)
55
+ buffer.to_s
56
+ end
57
+
58
+ def ssh_signature_type
59
+ ssh_type
60
+ end
61
+
62
+ # Only used to de-duplicate identities, so a stable key-specific string suffices.
63
+ def to_pem
64
+ "#{ssh_type} #{[to_blob].pack('m0')}"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -69,27 +69,22 @@ module Net
69
69
 
70
70
  attempted = []
71
71
 
72
- @auth_methods.each do |name|
73
- next unless @allowed_auth_methods.include?(name)
74
-
75
- attempted << name
76
-
77
- debug { "trying #{name}" }
78
- begin
79
- auth_class = Methods.const_get(name.split(/\W+/).map { |p| p.capitalize }.join)
80
- method = auth_class.new(self,
81
- key_manager: key_manager, password_prompt: options[:password_prompt],
82
- pubkey_algorithms: options[:pubkey_algorithms] || nil)
83
- rescue NameError
84
- debug {"Mechanism #{name} was requested, but isn't a known type. Ignoring it."}
85
- next
72
+ # After a partial success (sshd AuthenticationMethods) restart from the most preferred method the server now allows, as OpenSSH does.
73
+ loop do
74
+ @partial_success = false
75
+
76
+ @auth_methods.each do |name|
77
+ next unless @allowed_auth_methods.include?(name)
78
+
79
+ attempted << name
80
+ return true if try_authenticate(name, next_service, username, password, key_manager)
81
+ break if @partial_success
86
82
  end
87
83
 
88
- return true if method.authenticate(next_service, username, password)
89
- rescue Net::SSH::Authentication::DisallowedMethod
84
+ break unless @partial_success
90
85
  end
91
86
 
92
- error { "all authorization methods failed (tried #{attempted.join(', ')})" }
87
+ error { "all authorization methods failed (tried #{attempted.uniq.join(', ')})" }
93
88
  return false
94
89
  ensure
95
90
  key_manager.finish if key_manager
@@ -109,6 +104,7 @@ module Net
109
104
 
110
105
  when USERAUTH_FAILURE
111
106
  @allowed_auth_methods = packet[:authentications].split(/,/)
107
+ @partial_success ||= packet[:partial_success]
112
108
  debug { "allowed methods: #{packet[:authentications]}" }
113
109
  return packet
114
110
 
@@ -136,6 +132,22 @@ module Net
136
132
 
137
133
  private
138
134
 
135
+ def try_authenticate(method_name, next_service, username, password, key_manager)
136
+ debug { "trying #{method_name}" }
137
+ begin
138
+ auth_class = Methods.const_get(method_name.split(/\W+/).map { |p| p.capitalize }.join)
139
+ method = auth_class.new(self,
140
+ key_manager: key_manager, password_prompt: options[:password_prompt])
141
+ rescue NameError
142
+ debug {"Mechanism #{method_name} was requested, but isn't a known type. Ignoring it."}
143
+ return false
144
+ end
145
+
146
+ method.authenticate(next_service, username, password)
147
+ rescue Net::SSH::Authentication::DisallowedMethod
148
+ false
149
+ end
150
+
139
151
  # Returns an array of paths to the key files usually defined
140
152
  # by system default.
141
153
  def default_keys
@@ -2,6 +2,7 @@ require 'net/ssh/transport/openssl'
2
2
 
3
3
  require 'net/ssh/authentication/certificate'
4
4
  require 'net/ssh/authentication/ed25519_loader'
5
+ require 'net/ssh/authentication/security_key'
5
6
 
6
7
  module Net
7
8
  module SSH
@@ -338,6 +339,9 @@ module Net
338
339
  key = Net::SSH::Authentication::ED25519::PubKey.read_keyblob(self)
339
340
  when /^ecdsa\-sha2\-(\w*)$/
340
341
  key = OpenSSL::PKey::EC.read_keyblob($1, self)
342
+ when Net::SSH::Authentication::SecurityKey::SK_ECDSA_SHA2_NISTP256,
343
+ Net::SSH::Authentication::SecurityKey::SK_SSH_ED25519
344
+ key = Net::SSH::Authentication::SecurityKey::PubKey.read_keyblob(type, self)
341
345
  else
342
346
  raise NotImplementedError, "unsupported key type `#{type}'"
343
347
  end
@@ -166,6 +166,11 @@ module Net
166
166
  # http://github.com/net-ssh/net-ssh/tree/portfwfix
167
167
  #
168
168
  module ForwardedBufferedIo
169
+ def self.extended(object) # :nodoc:
170
+ object.instance_variable_set(:@on_sent, nil)
171
+ object.instance_variable_set(:@shutdown_write_when_drained, false)
172
+ end
173
+
169
174
  def fill(n = 8192)
170
175
  begin
171
176
  super(n)
@@ -182,11 +187,25 @@ module Net
182
187
  end
183
188
  end
184
189
 
190
+ # Internal and experimental: registers a block called with the number of
191
+ # bytes each #send_pending wrote to the socket.
192
+ def on_sent(&block) # :nodoc:
193
+ @on_sent = block
194
+ end
195
+
185
196
  def send_pending
186
197
  begin
187
- super
188
- rescue Errno::ECONNRESET => e
189
- debug { "connection was reset => shallowing exception:#{e}" }
198
+ pending = output.length
199
+ result = super
200
+ @on_sent.call(pending - output.length) if @on_sent && output.length < pending
201
+ shutdown_write_if_drained
202
+ result
203
+ rescue Errno::EPIPE, Errno::ECONNRESET => e
204
+ # Like OpenSSH's chan_write_failed: the peer is gone, so drop what it
205
+ # will never read and close, letting the forwarded channel close too.
206
+ debug { "write failed => discarding #{output.length} bytes and closing:#{e}" }
207
+ output.clear!
208
+ close
190
209
  return 0
191
210
  rescue IOError => e
192
211
  if e.message =~ /closed/ then
@@ -197,6 +216,24 @@ module Net
197
216
  end
198
217
  end
199
218
  end
219
+
220
+ # Internal: shuts down the write side once pending output has been sent,
221
+ # as OpenSSH does on CHANNEL_EOF (chan_rcvd_ieof, chan_obuf_empty).
222
+ def shutdown_write_when_drained # :nodoc:
223
+ @shutdown_write_when_drained = true
224
+ shutdown_write_if_drained
225
+ end
226
+
227
+ private
228
+
229
+ def shutdown_write_if_drained
230
+ return unless @shutdown_write_when_drained && output.empty? && !closed?
231
+
232
+ @shutdown_write_when_drained = false
233
+ shutdown(Socket::SHUT_WR)
234
+ rescue Errno::EPIPE, Errno::ENOTCONN => e
235
+ debug { "shutdown failed => shallowing exception:#{e}" }
236
+ end
200
237
  end
201
238
  end
202
239
  end
@@ -31,6 +31,7 @@ module Net
31
31
  # * PreferredAuthentications => maps to the :auth_methods option
32
32
  # * ProxyCommand => maps to the :proxy option
33
33
  # * ProxyJump => maps to the :proxy option
34
+ # * PubkeyAcceptedAlgorithms (or PubkeyAcceptedKeyTypes) => maps to :pubkey_algorithms
34
35
  # * PubKeyAuthentication => maps to the :auth_methods option
35
36
  # * RekeyLimit => :rekey_limit
36
37
  # * StrictHostKeyChecking => :verify_host_key
@@ -88,16 +89,14 @@ module Net
88
89
  IO.foreach(file) do |line|
89
90
  next if line =~ /^\s*(?:#.*)?$/
90
91
 
91
- if line =~ /^\s*(\S+)\s*=(.*)$/
92
- key, value = $1, $2
93
- else
94
- key, value = line.strip.split(/\s+/, 2)
95
- end
92
+ # As in OpenSSH, the keyword ends at the first whitespace or "=".
93
+ key, value = line.strip.split(/\s*=\s*|\s+/, 2)
96
94
 
97
95
  # silently ignore malformed entries
98
96
  next if value.nil?
99
97
 
100
98
  key.downcase!
99
+ value = uncomment(value)
101
100
  value = unquote(value)
102
101
 
103
102
  value = case value.strip
@@ -152,9 +151,11 @@ module Net
152
151
  end
153
152
 
154
153
  # ProxyCommand and ProxyJump override each other so they need to be tracked togeather
155
- %w[proxyjump proxycommand].each do |proxy_key|
156
- if (proxy_value = settings.delete(proxy_key))
157
- settings['proxy'] ||= [proxy_key, proxy_value]
154
+ [globals, settings].each do |hash|
155
+ %w[proxyjump proxycommand].each do |proxy_key|
156
+ if (proxy_value = hash.delete(proxy_key))
157
+ hash['proxy'] ||= [proxy_key, proxy_value]
158
+ end
158
159
  end
159
160
  end
160
161
  end
@@ -231,7 +232,7 @@ module Net
231
232
  user: :user,
232
233
  checkhostip: :check_host_ip
233
234
  }.freeze
234
- def translate_config_key(hash, key, value, settings)
235
+ def translate_config_key(hash, key, value, settings) # rubocop:disable Metrics/AbcSize
235
236
  case key
236
237
  when :stricthostkeychecking
237
238
  hash[:verify_host_key] = translate_verify_host_key(value)
@@ -277,6 +278,8 @@ module Net
277
278
  if (proxy = setup_proxy(*value))
278
279
  hash[:proxy] = proxy
279
280
  end
281
+ when :pubkeyacceptedalgorithms, :pubkeyacceptedkeytypes
282
+ hash[:pubkey_algorithms] = value.split(/,/)
280
283
  when :pubkeyauthentication
281
284
  if value
282
285
  (hash[:auth_methods] << 'publickey').uniq!
@@ -302,13 +305,15 @@ module Net
302
305
  def setup_proxy(type, value)
303
306
  case type
304
307
  when 'proxycommand'
305
- if value !~ /^none$/
308
+ if value !~ /^none$/i
306
309
  require 'net/ssh/proxy/command'
307
310
  Net::SSH::Proxy::Command.new(value)
308
311
  end
309
312
  when 'proxyjump'
310
- require 'net/ssh/proxy/jump'
311
- Net::SSH::Proxy::Jump.new(value)
313
+ if value !~ /^none$/i
314
+ require 'net/ssh/proxy/jump'
315
+ Net::SSH::Proxy::Jump.new(value)
316
+ end
312
317
  end
313
318
  end
314
319
 
@@ -398,6 +403,11 @@ module Net
398
403
  !condition_matches.empty? && condition_matches.all?
399
404
  end
400
405
 
406
+ # As in OpenSSH's argv_split, "#" starts a comment only at the start of a token and outside quotes.
407
+ def uncomment(string)
408
+ string[/\A(?:\s*(?!#)(?:\\.|"[^"]*"?|'[^']*'?|[^\s"'])+)*/]
409
+ end
410
+
401
411
  def unquote(string)
402
412
  string =~ /^"(.*)"$/ ? Regexp.last_match(1) : string
403
413
  end
@@ -118,6 +118,8 @@ module Net
118
118
 
119
119
  @local_maximum_packet_size = max_pkt_size
120
120
  @local_window_size = @local_maximum_window_size = max_win_size
121
+ @local_consumed = 0
122
+ @grant_window_on_consume = false
121
123
 
122
124
  @on_confirm_open = on_confirm_open
123
125
 
@@ -254,6 +256,26 @@ module Net
254
256
  output.append(data.to_s)
255
257
  end
256
258
 
259
+ # Internal and experimental, used by port forwarding: from now on window
260
+ # is granted only for bytes reported via #consumed, so a slow consumer
261
+ # stalls the remote sender instead of buffering everything it sends.
262
+ # Must be called before any data is received.
263
+ def grant_window_on_consume! # :nodoc:
264
+ raise ArgumentError, "grant_window_on_consume! must be called before any data is received" if local_window_size != local_maximum_window_size
265
+
266
+ @grant_window_on_consume = true
267
+ end
268
+
269
+ # Internal and experimental: reports that +bytes+ of received data have
270
+ # been processed, letting the remote end send that much more.
271
+ def consumed(bytes) # :nodoc:
272
+ raise ArgumentError, "consumed requires grant_window_on_consume!" unless @grant_window_on_consume
273
+ raise ArgumentError, "consumed bytes must not be negative" if bytes.negative?
274
+
275
+ @local_consumed += bytes
276
+ adjust_window_for_consumed
277
+ end
278
+
257
279
  # Returns true if the channel exists in the channel list of the session,
258
280
  # and false otherwise. This can be used to determine whether a channel has
259
281
  # been closed or not.
@@ -598,7 +620,12 @@ module Net
598
620
  # type, and data.
599
621
  def do_extended_data(type, data)
600
622
  update_local_window_size(data.length)
601
- @on_extended_data.call(self, type, data) if @on_extended_data
623
+ if @on_extended_data
624
+ @on_extended_data.call(self, type, data)
625
+ elsif @grant_window_on_consume
626
+ # Nothing will ever consume it, so don't let it shrink the window.
627
+ consumed(data.length)
628
+ end
602
629
  end
603
630
 
604
631
  # Invokes the #on_eof callback when the server indicates that no
@@ -651,6 +678,8 @@ module Net
651
678
  # server telling it that the window size has grown.
652
679
  def update_local_window_size(size)
653
680
  @local_window_size -= size
681
+ return adjust_window_for_consumed if @grant_window_on_consume
682
+
654
683
  if local_window_size < local_maximum_window_size / 2
655
684
  connection.send_message(
656
685
  Buffer.from(:byte, CHANNEL_WINDOW_ADJUST, :long, remote_id, :long, LOCAL_WINDOW_SIZE_INCREMENT)
@@ -663,6 +692,21 @@ module Net
663
692
  end
664
693
  end
665
694
 
695
+ # Mirrors OpenSSH's channel_check_window: once the window is below half,
696
+ # give back what was consumed, growing the maximum window the same way
697
+ # #update_local_window_size does.
698
+ def adjust_window_for_consumed
699
+ return if @local_consumed.zero? || local_window_size >= local_maximum_window_size / 2
700
+ return if remote_id.nil? || local_closed? || remote_closed?
701
+
702
+ growth = (GOOD_LOCAL_MAXIUMUM_WINDOW_SIZE - local_maximum_window_size).clamp(0, LOCAL_WINDOW_SIZE_INCREMENT)
703
+ adjust = @local_consumed + growth
704
+ connection.send_message(Buffer.from(:byte, CHANNEL_WINDOW_ADJUST, :long, remote_id, :long, adjust))
705
+ @local_window_size += adjust
706
+ @local_maximum_window_size += growth
707
+ @local_consumed = 0
708
+ end
709
+
666
710
  # Gets an +Array+ of local environment variables in the remote process'
667
711
  # environment.
668
712
  # A variable name can either be described by a +Regexp+ or +String+.
@@ -24,7 +24,7 @@ module Net
24
24
  # # 'ssh' is an instance of Net::SSH::Connection::Session
25
25
  # ssh.exec! "/etc/init.d/some_process start"
26
26
  # end
27
- class Session
27
+ class Session # rubocop:disable Metrics/ClassLength
28
28
  include Loggable
29
29
  include Constants
30
30
 
@@ -74,6 +74,8 @@ module Net
74
74
  @channel_id_counter = -1
75
75
  @channels = Hash.new(NilChannel.new(self))
76
76
  @listeners = { transport.socket => nil }
77
+ @read_paused = {}
78
+ @draining = {}
77
79
  @pending_requests = []
78
80
  @channel_open_handlers = {}
79
81
  @on_global_request = {}
@@ -120,12 +122,19 @@ module Net
120
122
  # connection.
121
123
  def close
122
124
  info { "closing remaining channels (#{channels.length} open)" }
123
- channels.each { |id, channel| channel.close }
125
+
126
+ channels.each do |id, channel|
127
+ channel.eof!
128
+ channel.close
129
+ end
130
+
124
131
  begin
125
132
  loop(0.1) { channels.any? }
126
133
  rescue Net::SSH::Disconnect
127
134
  raise unless channels.empty?
128
135
  end
136
+ @draining.each_key(&:close)
137
+ @draining.clear
129
138
  transport.close
130
139
  end
131
140
 
@@ -146,10 +155,13 @@ module Net
146
155
  # a +true+ value for +include_invisible+, then those will be counted.
147
156
  #
148
157
  # This can be useful for determining whether the event loop should continue
149
- # to be run.
158
+ # to be run. A forwarded socket still writing out data that arrived before
159
+ # its channel closed also counts as busy.
150
160
  #
151
161
  # ssh.loop { ssh.busy? }
152
162
  def busy?(include_invisible = false)
163
+ return true if @draining.any?
164
+
153
165
  if include_invisible
154
166
  channels.any?
155
167
  else
@@ -255,7 +267,7 @@ module Net
255
267
  def ev_do_calculate_rw_wait(wait)
256
268
  r = listeners.keys
257
269
  w = r.select { |w2| w2.respond_to?(:pending_write?) && w2.pending_write? }
258
- [r, w, io_select_wait(wait)]
270
+ [r.reject { |io| @read_paused.key?(io) }, w, io_select_wait(wait)]
259
271
  end
260
272
 
261
273
  # This is called internally as part of #process.
@@ -281,6 +293,7 @@ module Net
281
293
 
282
294
  Array(writers).each do |writer|
283
295
  writer.send_pending
296
+ close_if_drained(writer) if @draining.key?(writer)
284
297
  end
285
298
  end
286
299
 
@@ -485,9 +498,36 @@ module Net
485
498
  # Removes the given io object from the listeners collection, so that the
486
499
  # event loop will no longer monitor it.
487
500
  def stop_listening_to(io)
501
+ @read_paused.delete(io)
502
+ @draining.delete(io)
488
503
  listeners.delete(io)
489
504
  end
490
505
 
506
+ # Internal and experimental, used by port forwarding: stops the event
507
+ # loop from reading +io+ while it keeps flushing its pending output.
508
+ def pause_reading(io) # :nodoc:
509
+ raise ArgumentError, "the transport socket can't be paused" if io.equal?(transport.socket)
510
+
511
+ @read_paused[io] = true if listeners.key?(io)
512
+ end
513
+
514
+ def resume_reading(io) # :nodoc:
515
+ @read_paused.delete(io)
516
+ end
517
+
518
+ def reading_paused?(io) # :nodoc:
519
+ @read_paused.key?(io)
520
+ end
521
+
522
+ # Internal and experimental, used by port forwarding: stops reading +io+
523
+ # but keeps flushing its pending output, then closes it and stops
524
+ # listening, as OpenSSH's chan_rcvd_oclose does.
525
+ def close_when_drained(io) # :nodoc:
526
+ pause_reading(io)
527
+ @draining[io] = true
528
+ close_if_drained(io)
529
+ end
530
+
491
531
  # Returns a reference to the Net::SSH::Service::Forward service, which can
492
532
  # be used for forwarding ports over SSH.
493
533
  def forward
@@ -565,6 +605,13 @@ module Net
565
605
  end
566
606
  end
567
607
 
608
+ def close_if_drained(io)
609
+ return if io.pending_write? && !io.closed? && !closed? && listeners.key?(io)
610
+
611
+ io.close unless io.closed?
612
+ stop_listening_to(io)
613
+ end
614
+
568
615
  def channel_closed(channel)
569
616
  channel.remote_closed!
570
617
  channel.close
@@ -62,19 +62,21 @@ module Net
62
62
  @data && @data[:fingerprint]
63
63
  end
64
64
 
65
- # Returns the host name for the remote host, as reported by the socket.
65
+ # Returns the host name that was connected to.
66
66
  def host
67
- @data && @data[:peer] && @data[:peer][:host]
67
+ session&.host
68
68
  end
69
69
 
70
- # Returns the port number for the remote host, as reported by the socket.
70
+ # Returns the port number that was connected to.
71
71
  def port
72
- @data && @data[:peer] && @data[:peer][:port]
72
+ session&.port
73
73
  end
74
74
 
75
- # Returns the IP address of the remote host, as reported by the socket.
75
+ # Returns the IP address of the remote host, as reported by the socket,
76
+ # or nil when connected through a ProxyCommand.
76
77
  def ip
77
- @data && @data[:peer] && @data[:peer][:ip]
78
+ peer_ip = session&.socket&.peer_ip
79
+ peer_ip unless peer_ip == Net::SSH::Transport::PacketStream::PROXY_COMMAND_HOST_IP
78
80
  end
79
81
 
80
82
  # Returns the key itself, as reported by the remote host.
@@ -87,6 +89,12 @@ module Net
87
89
  def remember_host!
88
90
  @callback.call
89
91
  end
92
+
93
+ private
94
+
95
+ def session
96
+ @data && @data[:session]
97
+ end
90
98
  end
91
99
 
92
100
  # Raised when the cached key for a particular host does not match the
@@ -2,6 +2,7 @@ require 'net/ssh/transport/openssl'
2
2
  require 'net/ssh/prompt'
3
3
 
4
4
  require 'net/ssh/authentication/ed25519_loader'
5
+ require 'net/ssh/authentication/security_key'
5
6
 
6
7
  module Net
7
8
  module SSH
@@ -92,7 +93,7 @@ module Net
92
93
  blob = nil
93
94
  begin
94
95
  blob = fields.shift
95
- end while !blob.nil? && !/^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp\d+)(-cert-v01@openssh\.com)?$/.match(blob)
96
+ end while !blob.nil? && !public_key_type?(blob)
96
97
  blob = fields.shift
97
98
 
98
99
  raise Net::SSH::Exception, "public key at #{filename} is not valid" if blob.nil?
@@ -104,6 +105,11 @@ module Net
104
105
 
105
106
  private
106
107
 
108
+ def public_key_type?(type)
109
+ /^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp\d+)(-cert-v01@openssh\.com)?$/.match?(type) ||
110
+ Net::SSH::Authentication::SecurityKey.understands?(type)
111
+ end
112
+
107
113
  # rubocop:disable Style/Documentation, Lint/DuplicateMethods
108
114
  class KeyType
109
115
  def self.read(key_data, passphrase)
@@ -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,36 @@ 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
+ else
350
+ if socket.available > 0
351
+ data = socket.read_available(8192)
352
+ ch.debug { "read #{data.length} bytes from client, sending over #{type} forwarded connection" }
353
+ ch.send_data(data)
354
+ end
355
+
356
+ # Like OpenSSH's channel_pre_open, only read more once the remote
357
+ # window can take what is already queued; writes to the socket keep
358
+ # flowing. ch.output is still unsent here because Channel#process
359
+ # runs this block before enqueue_pending_output.
360
+ if ch.output.length + socket.available < ch.remote_window_size.to_i
361
+ session.resume_reading(socket)
362
+ else
363
+ session.pause_reading(socket)
364
+ end
360
365
  end
361
366
  end
362
367
  end
@@ -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,11 +32,8 @@ 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
@@ -52,7 +50,16 @@ module Net
52
50
 
53
51
  hmac: %w[hmac-sha2-512-etm@openssh.com hmac-sha2-256-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,7 +83,8 @@ 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
90
  %w[diffie-hellman-group-exchange-sha1
@@ -98,6 +106,8 @@ module Net
98
106
  hmac-md5 hmac-md5-96
99
107
  none],
100
108
 
109
+ pubkey_algorithms: DEFAULT_ALGORITHMS[:pubkey_algorithms] + %w[ssh-rsa-cert-v01@openssh.com ssh-rsa ssh-dss],
110
+
101
111
  compression: %w[none zlib@openssh.com zlib],
102
112
  language: %w[]
103
113
  }.freeze
@@ -269,7 +279,9 @@ module Net
269
279
 
270
280
  ALGORITHMS.each do |algorithm, supported|
271
281
  algorithms[algorithm] = compose_algorithm_list(
272
- supported, options[algorithm] || DEFAULT_ALGORITHMS[algorithm],
282
+ supported,
283
+ DEFAULT_ALGORITHMS[algorithm],
284
+ options[algorithm] || DEFAULT_ALGORITHMS[algorithm],
273
285
  options[:append_all_supported_algorithms]
274
286
  )
275
287
  end
@@ -287,7 +299,11 @@ module Net
287
299
  # existing known key for the host has preference.
288
300
 
289
301
  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
302
+ host_keys = existing_keys.flat_map { |key| key.respond_to?(:ssh_types) ? key.ssh_types : [key.ssh_type] }
303
+ # A known ssh-rsa key also verifies with rsa-sha2-*, and must not re-enable disabled SHA-1 algorithms.
304
+ disabled_sha1 = %w[ssh-rsa ssh-rsa-cert-v01@openssh.com ssh-rsa-cert-v00@openssh.com] - algorithms[:host_key]
305
+ host_keys = host_keys.flat_map { |type| type == 'ssh-rsa' ? %w[rsa-sha2-512 rsa-sha2-256 ssh-rsa] : [type] }
306
+ host_keys = (host_keys - disabled_sha1).uniq
291
307
  algorithms[:host_key].each do |name|
292
308
  host_keys << name unless host_keys.include?(name)
293
309
  end
@@ -296,21 +312,27 @@ module Net
296
312
  end
297
313
 
298
314
  # 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)
315
+ def compose_algorithm_list(supported, defaults, option, append_all_supported_algorithms = false)
300
316
  return supported.dup unless option
301
317
 
302
318
  list = []
303
319
  option = Array(option).compact.uniq
304
320
 
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] }
321
+ if option.first && option.first.start_with?('+', '-', '^')
322
+ ops = { '^' => [], '+' => [], '-' => [] }
323
+ cur_list = nil
324
+ option.each do |cur_opt|
325
+ if ops.key?(cur_opt[0])
326
+ cur_list = ops[cur_opt[0]]
327
+ cur_opt = cur_opt[1..-1]
328
+ end
329
+ cur_list << cur_opt
330
+ end
310
331
 
311
- list.concat(appends)
332
+ list = ops['^'] + defaults + ops['+']
333
+ list.concat(supported) if append_all_supported_algorithms
312
334
 
313
- deletions.each do |opt|
335
+ ops['-'].each do |opt|
314
336
  if opt.include?('*')
315
337
  opt_escaped = Regexp.escape(opt)
316
338
  algo_re = /\A#{opt_escaped.gsub('\*', '[A-Za-z\d\-@\.]*')}\z/
@@ -190,6 +190,7 @@ module Net
190
190
  def cleanup
191
191
  client.cleanup
192
192
  server.cleanup
193
+ hang_up_proxy_command
193
194
  end
194
195
 
195
196
  # If the IO object requires a rekey operation (as indicated by either its
@@ -205,6 +206,17 @@ module Net
205
206
 
206
207
  protected
207
208
 
209
+ # Like OpenSSH, hang up a ProxyCommand child: tunnels such as EC2 Instance
210
+ # Connect don't exit on their own, and IO#close waits for them.
211
+ def hang_up_proxy_command
212
+ return unless respond_to?(:pid)
213
+
214
+ child = pid
215
+ Process.kill('HUP', child) if child
216
+ rescue IOError, Errno::ESRCH
217
+ # already closed, or the child has already been reaped
218
+ end
219
+
208
220
  # Called when this module is used to extend an object. It initializes
209
221
  # the states and generally prepares the object for use as a packet stream.
210
222
  def initialize_ssh
@@ -107,6 +107,10 @@ module Net
107
107
  end
108
108
  end
109
109
 
110
+ def pubkey_algorithms
111
+ algorithms[:pubkey_algorithms]
112
+ end
113
+
110
114
  # Returns the host (and possibly IP address) in a format compatible with
111
115
  # SSH known-host files.
112
116
  def host_as_string
@@ -51,6 +51,9 @@ module Net
51
51
  # implement them (Net::SSH::HostKeyEntries::CertAuthority). Non-certificate
52
52
  # entries do not respond to these predicates and are left untouched.
53
53
  def verify_certificate!(entry, host_keys, arguments)
54
+ # Plain keys carry no certificate constraints, and respond_to? on this delegator prints a deprecation warning (#1026).
55
+ return if entry.is_a?(Net::SSH::HostKeyEntries::PubKey)
56
+
54
57
  cert = arguments[:key]
55
58
 
56
59
  if entry.respond_to?(:matches_validity?) && !entry.matches_validity?(cert)
@@ -56,7 +56,7 @@ module Net
56
56
 
57
57
  # The prerelease component of this version of the Net::SSH library
58
58
  # nil allowed
59
- PRE = "rc2"
59
+ PRE = "rc4"
60
60
 
61
61
  # The current version of the Net::SSH library as a Version instance
62
62
  CURRENT = new(*[MAJOR, MINOR, TINY, PRE].compact)
data/lib/net/ssh.rb CHANGED
@@ -174,11 +174,13 @@ module Net
174
174
  # * :properties => a hash of key/value pairs to add to the new connection's
175
175
  # properties (see Net::SSH::Connection::Session#properties)
176
176
  # * :proxy => a proxy instance (see Proxy) to use when connecting
177
- # * :pubkey_algorithms => the public key authentication algorithms to use for
178
- # this connection. Valid values are 'rsa-sha2-256-cert-v01@openssh.com',
179
- # 'ssh-rsa-cert-v01@openssh.com', 'rsa-sha2-256', 'ssh-rsa'. Currently, this
180
- # option is only used for RSA public key authentication and ignored for other
181
- # types.
177
+ # * :pubkey_algorithms => the public key authentication algorithms to offer,
178
+ # in order of preference (e.g. %w[ssh-ed25519 rsa-sha2-512 rsa-sha2-256]).
179
+ # Keys whose type isn't listed are skipped. Like PubkeyAcceptedAlgorithms,
180
+ # entries starting with +, - or ^ modify the default list. ssh-rsa (SHA-1),
181
+ # ssh-rsa-cert-v01@openssh.com and ssh-dss are supported but not offered
182
+ # unless listed here (e.g. '+ssh-rsa') or :append_all_supported_algorithms
183
+ # is set.
182
184
  # * :rekey_blocks_limit => the max number of blocks to process before rekeying
183
185
  # * :rekey_limit => the max number of bytes to process before rekeying
184
186
  # * :rekey_packet_limit => the max number of packets to process before rekeying
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-ssh
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.0.rc2
4
+ version: 8.0.0.rc4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jamis Buck
@@ -32,7 +32,7 @@ cert_chain:
32
32
  ufHf4AX2UIkJbh7zCPkiNCqIr7MSWLNFG/9lOlHYsEJM8XujT1ofPobYx6YSFx/C
33
33
  7HBrI7UX7awt6UvBZebhcHzyMHxg/B5PVQllPA==
34
34
  -----END CERTIFICATE-----
35
- date: 2026-10-05 00:00:00.000000000 Z
35
+ date: 2026-10-11 00:00:00.000000000 Z
36
36
  dependencies:
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: logger
@@ -224,6 +224,7 @@ files:
224
224
  - lib/net/ssh/authentication/methods/publickey.rb
225
225
  - lib/net/ssh/authentication/pageant.rb
226
226
  - lib/net/ssh/authentication/pub_key_fingerprint.rb
227
+ - lib/net/ssh/authentication/security_key.rb
227
228
  - lib/net/ssh/authentication/session.rb
228
229
  - lib/net/ssh/buffer.rb
229
230
  - lib/net/ssh/buffered_io.rb
metadata.gz.sig CHANGED
Binary file