net-ssh 7.3.6 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/pull_request_template.md +1 -0
- data/.github/workflows/changelog.yml +43 -0
- data/.github/workflows/ci-with-docker.yml +0 -3
- data/.github/workflows/ci.yml +2 -17
- data/.github/workflows/rubocop.yml +3 -1
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +1 -0
- data/CHANGES.txt +73 -7
- data/CONTRIBUTING.md +35 -0
- data/Dockerfile +3 -3
- data/Dockerfile.openssl3 +1 -1
- data/Dockerfile.zlib_ng +1 -1
- data/README.md +71 -57
- data/Rakefile +66 -12
- data/appveyor.yml +4 -16
- data/changelog.d/README.md +1 -0
- data/docker-compose.yml +1 -7
- data/lib/net/ssh/authentication/agent.rb +35 -11
- data/lib/net/ssh/authentication/ed25519.rb +137 -44
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/authentication/key_manager.rb +22 -11
- data/lib/net/ssh/authentication/methods/abstract.rb +2 -8
- data/lib/net/ssh/authentication/methods/publickey.rb +3 -1
- data/lib/net/ssh/authentication/security_key.rb +71 -0
- data/lib/net/ssh/authentication/session.rb +29 -17
- data/lib/net/ssh/buffer.rb +6 -1
- data/lib/net/ssh/buffered_io.rb +40 -3
- data/lib/net/ssh/config.rb +28 -12
- data/lib/net/ssh/connection/channel.rb +49 -2
- data/lib/net/ssh/connection/session.rb +58 -8
- data/lib/net/ssh/errors.rb +24 -6
- data/lib/net/ssh/key_factory.rb +7 -1
- data/lib/net/ssh/known_hosts.rb +18 -8
- data/lib/net/ssh/multiplex/client.rb +162 -0
- data/lib/net/ssh/multiplex/control_path.rb +120 -0
- data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
- data/lib/net/ssh/multiplex.rb +130 -0
- data/lib/net/ssh/prompt.rb +3 -3
- data/lib/net/ssh/proxy/jump.rb +40 -2
- data/lib/net/ssh/service/forward.rb +29 -24
- data/lib/net/ssh/test.rb +2 -1
- data/lib/net/ssh/transport/algorithms.rb +88 -33
- data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
- data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
- data/lib/net/ssh/transport/cipher_factory.rb +23 -1
- data/lib/net/ssh/transport/constants.rb +1 -0
- data/lib/net/ssh/transport/gcm_cipher.rb +27 -6
- data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
- data/lib/net/ssh/transport/hmac.rb +2 -0
- data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
- data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
- data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
- data/lib/net/ssh/transport/openssl.rb +2 -1
- data/lib/net/ssh/transport/packet_stream.rb +12 -0
- data/lib/net/ssh/transport/session.rb +41 -44
- data/lib/net/ssh/verifiers/always.rb +0 -3
- data/lib/net/ssh/version.rb +3 -3
- data/lib/net/ssh.rb +139 -57
- data/net-ssh.gemspec +5 -8
- data/rakelib/changelog.rb +229 -0
- data.tar.gz.sig +0 -0
- metadata +37 -36
- metadata.gz.sig +0 -0
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
data/lib/net/ssh/proxy/jump.rb
CHANGED
|
@@ -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 #{
|
|
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,36 @@ module Net
|
|
|
327
332
|
|
|
328
333
|
channel.on_eof do |ch|
|
|
329
334
|
debug { "eof #{type} on #{type} forwarded channel" }
|
|
330
|
-
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
|
@@ -388,13 +393,13 @@ module Net
|
|
|
388
393
|
originator_address = packet.read_string
|
|
389
394
|
originator_port = packet.read_long
|
|
390
395
|
|
|
391
|
-
|
|
396
|
+
debug { "REMOTE 0: #{connected_port} #{connected_address} #{originator_address} #{originator_port}" }
|
|
392
397
|
remote = @remote_forwarded_ports[[connected_port, connected_address]]
|
|
393
398
|
if remote.nil?
|
|
394
399
|
raise Net::SSH::ChannelOpenFailed.new(1, "unknown request from remote forwarded connection on #{connected_address}:#{connected_port}")
|
|
395
400
|
end
|
|
396
401
|
|
|
397
|
-
|
|
402
|
+
debug { "REMOTE: #{remote.host} #{remote.port}" }
|
|
398
403
|
client = TCPSocket.new(remote.host, remote.port)
|
|
399
404
|
info { "connected #{connected_address}:#{connected_port} originator #{originator_address}:#{originator_port}" }
|
|
400
405
|
|
|
@@ -415,7 +420,7 @@ module Net
|
|
|
415
420
|
channel[:invisible] = true
|
|
416
421
|
|
|
417
422
|
begin
|
|
418
|
-
agent = Authentication::Agent.connect(logger, session.options[:agent_socket_factory])
|
|
423
|
+
agent = Authentication::Agent.connect(logger, session.options[:agent_socket_factory], session.options[:identity_agent])
|
|
419
424
|
if (agent.socket.is_a? ::IO)
|
|
420
425
|
prepare_client(agent.socket, channel, :agent)
|
|
421
426
|
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",
|
|
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
|
-
|
|
35
|
-
|
|
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] +
|
|
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-
|
|
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,
|
|
273
|
-
|
|
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] }
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
|
344
|
+
list = ops['^'] + defaults + ops['+']
|
|
345
|
+
list.concat(appendable) if append_all_supported_algorithms
|
|
312
346
|
|
|
313
|
-
|
|
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
|
-
|
|
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
|
|
351
|
-
data[:host_key] = packet
|
|
352
|
-
data[:encryption_client] = packet
|
|
353
|
-
data[:encryption_server] = packet
|
|
354
|
-
data[:hmac_client] = packet
|
|
355
|
-
data[:hmac_server] = packet
|
|
356
|
-
data[:compression_client] = packet
|
|
357
|
-
data[:compression_server] = packet
|
|
358
|
-
data[:language_client] = packet
|
|
359
|
-
data[:language_server] = packet
|
|
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)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'openssl'
|
|
2
2
|
require 'net/ssh/loggable'
|
|
3
3
|
|
|
4
4
|
module Net
|
|
@@ -8,6 +8,15 @@ module Net
|
|
|
8
8
|
class ChaCha20Poly1305Cipher
|
|
9
9
|
include Net::SSH::Loggable
|
|
10
10
|
|
|
11
|
+
POLY1305_ALGORITHM = "POLY1305"
|
|
12
|
+
POLY1305_KEY_BYTES = 32
|
|
13
|
+
POLY1305_TAG_BYTES = 16
|
|
14
|
+
NAME = "chacha20-poly1305@openssh.com"
|
|
15
|
+
ZERO_BLOCK = "\x00".b * POLY1305_KEY_BYTES
|
|
16
|
+
ZERO_IV = "\x00".b * 16
|
|
17
|
+
|
|
18
|
+
class UnsupportedError < StandardError; end
|
|
19
|
+
|
|
11
20
|
# Implicit HMAC, no need to do anything
|
|
12
21
|
class ImplicitHMac
|
|
13
22
|
def etm
|
|
@@ -24,7 +33,6 @@ module Net
|
|
|
24
33
|
@chacha_hdr = OpenSSL::Cipher.new("chacha20")
|
|
25
34
|
key_len = @chacha_hdr.key_len
|
|
26
35
|
@chacha_main = OpenSSL::Cipher.new("chacha20")
|
|
27
|
-
@poly = RbNaCl::OneTimeAuths::Poly1305
|
|
28
36
|
if key.size < key_len * 2
|
|
29
37
|
error { "chacha20_poly1305: keylength doesn't match" }
|
|
30
38
|
raise "chacha20_poly1305: keylength doesn't match"
|
|
@@ -43,9 +51,9 @@ module Net
|
|
|
43
51
|
end
|
|
44
52
|
|
|
45
53
|
def update_cipher_mac(payload, sequence_number)
|
|
46
|
-
iv_data =
|
|
54
|
+
iv_data = packet_iv(sequence_number)
|
|
47
55
|
@chacha_main.iv = iv_data
|
|
48
|
-
poly_key = @chacha_main.update(
|
|
56
|
+
poly_key = @chacha_main.update(ZERO_BLOCK)
|
|
49
57
|
|
|
50
58
|
packet_length = payload.size
|
|
51
59
|
length_data = [packet_length].pack("N")
|
|
@@ -57,35 +65,36 @@ module Net
|
|
|
57
65
|
unencrypted_data = payload
|
|
58
66
|
packet += @chacha_main.update(unencrypted_data)
|
|
59
67
|
|
|
60
|
-
packet +=
|
|
68
|
+
packet += self.class.poly1305_auth(poly_key, packet)
|
|
61
69
|
return packet
|
|
62
70
|
end
|
|
63
71
|
|
|
64
72
|
def read_length(data, sequence_number)
|
|
65
|
-
iv_data =
|
|
73
|
+
iv_data = packet_iv(sequence_number)
|
|
66
74
|
@chacha_hdr.iv = iv_data
|
|
67
75
|
@chacha_hdr.update(data).unpack1("N")
|
|
68
76
|
end
|
|
69
77
|
|
|
70
78
|
def read_and_mac(data, mac, sequence_number)
|
|
71
|
-
iv_data =
|
|
79
|
+
iv_data = packet_iv(sequence_number)
|
|
72
80
|
@chacha_main.iv = iv_data
|
|
73
|
-
poly_key = @chacha_main.update(
|
|
81
|
+
poly_key = @chacha_main.update(ZERO_BLOCK)
|
|
74
82
|
|
|
75
83
|
iv_data[0] = 1.chr
|
|
76
84
|
@chacha_main.iv = iv_data
|
|
77
85
|
unencrypted_data = @chacha_main.update(data[4..])
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
|
|
87
|
+
expected_mac = self.class.poly1305_auth(poly_key, data[0..])
|
|
88
|
+
valid_mac = mac.respond_to?(:bytesize) &&
|
|
89
|
+
mac.bytesize == POLY1305_TAG_BYTES &&
|
|
90
|
+
OpenSSL.fixed_length_secure_compare(expected_mac, mac)
|
|
91
|
+
raise Net::SSH::Exception, "corrupted hmac detected #{name}" unless valid_mac
|
|
92
|
+
|
|
84
93
|
return unencrypted_data
|
|
85
94
|
end
|
|
86
95
|
|
|
87
96
|
def mac_length
|
|
88
|
-
|
|
97
|
+
POLY1305_TAG_BYTES
|
|
89
98
|
end
|
|
90
99
|
|
|
91
100
|
def block_size
|
|
@@ -93,7 +102,7 @@ module Net
|
|
|
93
102
|
end
|
|
94
103
|
|
|
95
104
|
def name
|
|
96
|
-
|
|
105
|
+
NAME
|
|
97
106
|
end
|
|
98
107
|
|
|
99
108
|
def implicit_mac?
|
|
@@ -111,6 +120,70 @@ module Net
|
|
|
111
120
|
def self.key_length
|
|
112
121
|
64
|
|
113
122
|
end
|
|
123
|
+
|
|
124
|
+
def self.iv_len
|
|
125
|
+
0
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.auth_length
|
|
129
|
+
POLY1305_TAG_BYTES
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.decrypt_private_key(ciphertext, auth_tag, key, _initialization_vector)
|
|
133
|
+
raise ArgumentError, "chacha20_poly1305: keylength doesn't match" unless
|
|
134
|
+
key.respond_to?(:bytesize) && key.bytesize == key_length
|
|
135
|
+
|
|
136
|
+
ciphertext = binary_string(ciphertext)
|
|
137
|
+
chacha = OpenSSL::Cipher.new("chacha20")
|
|
138
|
+
chacha.decrypt
|
|
139
|
+
chacha.key = binary_string(key[0...POLY1305_KEY_BYTES])
|
|
140
|
+
|
|
141
|
+
iv_data = ZERO_IV.dup
|
|
142
|
+
chacha.iv = iv_data
|
|
143
|
+
poly_key = chacha.update(ZERO_BLOCK)
|
|
144
|
+
|
|
145
|
+
valid_mac = OpenSSL.fixed_length_secure_compare(poly1305_auth(poly_key, ciphertext), auth_tag)
|
|
146
|
+
raise Net::SSH::Exception, "corrupted hmac detected #{NAME}" unless valid_mac
|
|
147
|
+
|
|
148
|
+
iv_data.setbyte(0, 1)
|
|
149
|
+
chacha.iv = iv_data
|
|
150
|
+
chacha.update(ciphertext)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def self.ensure_supported!
|
|
154
|
+
raise UnsupportedError, "OpenSSL::PKey raw private key APIs are unavailable" unless OpenSSL::PKey.respond_to?(:new_raw_private_key)
|
|
155
|
+
|
|
156
|
+
OpenSSL::Cipher.new("chacha20")
|
|
157
|
+
|
|
158
|
+
tag = poly1305_auth("\x00" * POLY1305_KEY_BYTES, "")
|
|
159
|
+
raise UnsupportedError, "OpenSSL Poly1305 authentication failed" unless tag.bytesize == POLY1305_TAG_BYTES
|
|
160
|
+
rescue OpenSSL::Cipher::CipherError, OpenSSL::PKey::PKeyError => e
|
|
161
|
+
raise UnsupportedError, e.message
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.poly1305_auth(poly_key, data)
|
|
165
|
+
validate_poly1305_key!(poly_key)
|
|
166
|
+
OpenSSL::PKey.new_raw_private_key(POLY1305_ALGORITHM, binary_string(poly_key)).sign(nil, binary_string(data))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def self.validate_poly1305_key!(poly_key)
|
|
170
|
+
raise ArgumentError, "invalid Poly1305 key" unless poly_key.respond_to?(:bytesize) && poly_key.bytesize == POLY1305_KEY_BYTES
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def self.binary_string(string)
|
|
174
|
+
string.dup.force_encoding('BINARY')
|
|
175
|
+
end
|
|
176
|
+
private_class_method :binary_string
|
|
177
|
+
|
|
178
|
+
def packet_iv(sequence_number)
|
|
179
|
+
iv_data = ZERO_IV.dup
|
|
180
|
+
iv_data.setbyte(12, (sequence_number >> 24) & 0xff)
|
|
181
|
+
iv_data.setbyte(13, (sequence_number >> 16) & 0xff)
|
|
182
|
+
iv_data.setbyte(14, (sequence_number >> 8) & 0xff)
|
|
183
|
+
iv_data.setbyte(15, sequence_number & 0xff)
|
|
184
|
+
iv_data
|
|
185
|
+
end
|
|
186
|
+
private :packet_iv
|
|
114
187
|
end
|
|
115
188
|
end
|
|
116
189
|
end
|
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
module Net
|
|
2
2
|
module SSH
|
|
3
3
|
module Transport
|
|
4
|
-
# Loads chacha20
|
|
4
|
+
# Loads chacha20-poly1305 support backed by Ruby OpenSSL APIs.
|
|
5
5
|
module ChaCha20Poly1305CipherLoader
|
|
6
6
|
begin
|
|
7
7
|
require 'net/ssh/transport/chacha20_poly1305_cipher'
|
|
8
|
+
Net::SSH::Transport::ChaCha20Poly1305Cipher.ensure_supported!
|
|
8
9
|
LOADED = true
|
|
9
10
|
ERROR = nil
|
|
10
11
|
rescue LoadError => e
|
|
11
12
|
ERROR = e
|
|
12
13
|
LOADED = false
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
if defined?(Net::SSH::Transport::ChaCha20Poly1305Cipher::UnsupportedError) &&
|
|
16
|
+
e.is_a?(Net::SSH::Transport::ChaCha20Poly1305Cipher::UnsupportedError)
|
|
17
|
+
ERROR = e
|
|
18
|
+
LOADED = false
|
|
19
|
+
else
|
|
20
|
+
raise
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.raiseUnlessLoaded(message)
|
|
25
|
+
description = LOADED ? '' : chacha20_poly1305_support_required
|
|
26
|
+
description += "#{ERROR.class} : \"#{ERROR.message}\"\n" if ERROR
|
|
27
|
+
raise NotImplementedError, "#{message}\n#{description}" unless LOADED
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.chacha20_poly1305_support_required
|
|
31
|
+
"net-ssh requires openssl >= 3.2.0 and a crypto backend with ChaCha20 and Poly1305 enabled for chacha20-poly1305@openssh.com support.\n"
|
|
13
32
|
end
|
|
14
33
|
end
|
|
15
34
|
end
|
|
@@ -100,7 +100,11 @@ module Net
|
|
|
100
100
|
# if :iv_len option is supplied the third return value will be ivlen
|
|
101
101
|
def self.get_lengths(name, options = {})
|
|
102
102
|
klass = SSH_TO_CLASS[name]
|
|
103
|
-
|
|
103
|
+
unless klass.nil?
|
|
104
|
+
result = [klass.key_length, klass.block_size]
|
|
105
|
+
result << klass.iv_len if options[:iv_len]
|
|
106
|
+
return result
|
|
107
|
+
end
|
|
104
108
|
|
|
105
109
|
ossl_name = SSH_TO_OSSL[name]
|
|
106
110
|
if ossl_name.nil? || ossl_name == "none"
|
|
@@ -124,6 +128,24 @@ module Net
|
|
|
124
128
|
end
|
|
125
129
|
result
|
|
126
130
|
end
|
|
131
|
+
|
|
132
|
+
def self.auth_length(name)
|
|
133
|
+
klass = SSH_TO_CLASS[name]
|
|
134
|
+
return klass.auth_length if klass.respond_to?(:auth_length)
|
|
135
|
+
|
|
136
|
+
0
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.decrypt_private_key(name, ciphertext, auth_tag, key, initialization_vector)
|
|
140
|
+
klass = SSH_TO_CLASS[name]
|
|
141
|
+
raise Net::SSH::Exception, "#{name} does not support private key decryption" unless
|
|
142
|
+
klass.respond_to?(:decrypt_private_key)
|
|
143
|
+
|
|
144
|
+
raise Net::SSH::Exception, "incorrect auth_tag length" unless
|
|
145
|
+
auth_tag.respond_to?(:bytesize) && auth_tag.bytesize == klass.auth_length
|
|
146
|
+
|
|
147
|
+
klass.decrypt_private_key(ciphertext, auth_tag, key, initialization_vector)
|
|
148
|
+
end
|
|
127
149
|
end
|
|
128
150
|
end
|
|
129
151
|
end
|