net-ssh 7.3.10 → 8.0.0.beta3
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/workflows/ci-with-docker.yml +0 -3
- data/.github/workflows/ci.yml +2 -17
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +1 -0
- data/CHANGES.txt +2 -31
- data/Dockerfile +1 -4
- data/Dockerfile.openssl3 +1 -1
- data/Dockerfile.zlib_ng +1 -1
- data/README.md +10 -13
- data/Rakefile +12 -20
- data/appveyor.yml +4 -16
- data/docker-compose.yml +1 -7
- data/lib/net/ssh/authentication/agent.rb +4 -4
- data/lib/net/ssh/authentication/ed25519.rb +134 -46
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/errors.rb +0 -10
- data/lib/net/ssh/key_factory.rb +3 -4
- data/lib/net/ssh/known_hosts.rb +30 -85
- data/lib/net/ssh/proxy/command.rb +3 -28
- data/lib/net/ssh/proxy/https.rb +4 -28
- data/lib/net/ssh/proxy/jump.rb +7 -38
- data/lib/net/ssh/service/forward.rb +0 -6
- 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/gcm_cipher.rb +27 -6
- 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/packet_stream.rb +0 -4
- data/lib/net/ssh/transport/server_version.rb +13 -33
- data/lib/net/ssh/transport/session.rb +1 -19
- data/lib/net/ssh/verifiers/accept_new_or_local_tunnel.rb +1 -3
- data/lib/net/ssh/verifiers/always.rb +1 -19
- data/lib/net/ssh/verifiers/never.rb +1 -2
- data/lib/net/ssh/version.rb +4 -4
- data/net-ssh.gemspec +3 -9
- data.tar.gz.sig +0 -0
- metadata +8 -52
- metadata.gz.sig +0 -0
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
module Net
|
|
2
2
|
module SSH
|
|
3
3
|
module Authentication
|
|
4
|
-
# Loads ED25519 support
|
|
5
|
-
# ed25519, bcrypt_pbkdf
|
|
4
|
+
# Loads ED25519 support backed by Ruby OpenSSL raw key APIs.
|
|
6
5
|
module ED25519Loader
|
|
7
6
|
begin
|
|
8
7
|
require 'net/ssh/authentication/ed25519'
|
|
8
|
+
Net::SSH::Authentication::ED25519.ensure_supported!
|
|
9
9
|
LOADED = true
|
|
10
10
|
ERROR = nil
|
|
11
11
|
rescue LoadError => e
|
|
12
12
|
ERROR = e
|
|
13
13
|
LOADED = false
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
if defined?(Net::SSH::Authentication::ED25519::UnsupportedError) &&
|
|
16
|
+
e.is_a?(Net::SSH::Authentication::ED25519::UnsupportedError)
|
|
17
|
+
ERROR = e
|
|
18
|
+
LOADED = false
|
|
19
|
+
else
|
|
20
|
+
raise
|
|
21
|
+
end
|
|
14
22
|
end
|
|
15
23
|
|
|
16
24
|
def self.raiseUnlessLoaded(message)
|
|
17
|
-
description =
|
|
18
|
-
description
|
|
25
|
+
description = LOADED ? '' : ed25519SupportRequired
|
|
26
|
+
description += "#{ERROR.class} : \"#{ERROR.message}\"\n" if ERROR
|
|
19
27
|
raise NotImplementedError, "#{message}\n#{description}" unless LOADED
|
|
20
28
|
end
|
|
21
29
|
|
|
22
|
-
def self.
|
|
23
|
-
|
|
24
|
-
result << " * ed25519 (>= 1.2, < 2.0)\n"
|
|
25
|
-
result << " * bcrypt_pbkdf (>= 1.0, < 2.0)\n" unless RUBY_PLATFORM == "java"
|
|
26
|
-
result << "See https://github.com/net-ssh/net-ssh/issues/565 for more information\n"
|
|
30
|
+
def self.ed25519SupportRequired
|
|
31
|
+
"net-ssh requires openssl >= 3.2.0 and a crypto backend with Ed25519 enabled for ed25519 support.\n"
|
|
27
32
|
end
|
|
28
33
|
end
|
|
29
34
|
end
|
data/lib/net/ssh/errors.rb
CHANGED
|
@@ -102,15 +102,5 @@ module Net
|
|
|
102
102
|
# you want to proceed anyway, simply call the remember_host! method on the
|
|
103
103
|
# exception, and then retry.
|
|
104
104
|
class HostKeyUnknown < HostKeyError; end
|
|
105
|
-
|
|
106
|
-
# Raised when the key given by the host, or the CA that signed its
|
|
107
|
-
# certificate, is marked @revoked in a known hosts file. A revoked key must
|
|
108
|
-
# never be accepted, so unlike the other host key errors this one can't be
|
|
109
|
-
# overridden: #remember_host! just raises it again.
|
|
110
|
-
class HostKeyRevoked < HostKeyError
|
|
111
|
-
def remember_host!
|
|
112
|
-
raise self
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
105
|
end
|
|
116
106
|
end
|
data/lib/net/ssh/key_factory.rb
CHANGED
|
@@ -55,7 +55,7 @@ module Net
|
|
|
55
55
|
prompter = nil
|
|
56
56
|
result =
|
|
57
57
|
begin
|
|
58
|
-
key_type.read(data, passphrase)
|
|
58
|
+
key_type.read(data, passphrase || 'invalid')
|
|
59
59
|
rescue *key_type.error_classes => e
|
|
60
60
|
encrypted_key = !!key_type.encrypted_key?(data, e) if encrypted_key.nil?
|
|
61
61
|
if encrypted_key && ask_passphrase
|
|
@@ -138,9 +138,8 @@ module Net
|
|
|
138
138
|
raise Exception, "TODO: subclasses should implement"
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
-
# A nil passphrase would make OpenSSL prompt on the terminal itself.
|
|
142
141
|
def self.read(key_data, passphrase)
|
|
143
|
-
open_ssl_class.new(key_data, passphrase
|
|
142
|
+
open_ssl_class.new(key_data, passphrase)
|
|
144
143
|
end
|
|
145
144
|
|
|
146
145
|
def self.encrypted_key?(key_data, error)
|
|
@@ -150,7 +149,7 @@ module Net
|
|
|
150
149
|
|
|
151
150
|
class OpenSSLPKeyType < OpenSSLKeyTypeBase
|
|
152
151
|
def self.read(key_data, passphrase)
|
|
153
|
-
open_ssl_class.read(key_data, passphrase
|
|
152
|
+
open_ssl_class.read(key_data, passphrase)
|
|
154
153
|
end
|
|
155
154
|
|
|
156
155
|
def self.open_ssl_class
|
data/lib/net/ssh/known_hosts.rb
CHANGED
|
@@ -93,22 +93,6 @@ module Net
|
|
|
93
93
|
server_key.critical_options.empty?
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
|
-
|
|
97
|
-
# @revoked entry
|
|
98
|
-
class Revoked
|
|
99
|
-
def initialize(key, comment: nil)
|
|
100
|
-
@key = key
|
|
101
|
-
@comment = comment
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Returns true for the revoked key itself and for any certificate that
|
|
105
|
-
# wraps it or is signed by it, matching OpenSSH's check_key_not_revoked.
|
|
106
|
-
def matches_key?(server_key)
|
|
107
|
-
keys = [server_key]
|
|
108
|
-
keys += [server_key.key, server_key.signature_key] if server_key.respond_to?(:signature_key)
|
|
109
|
-
keys.any? { |key| key.ssh_type == @key.ssh_type && key.to_blob == @key.to_blob }
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
96
|
end
|
|
113
97
|
|
|
114
98
|
# Represents the result of a search in known hosts
|
|
@@ -117,17 +101,11 @@ module Net
|
|
|
117
101
|
include Enumerable
|
|
118
102
|
attr_reader :host
|
|
119
103
|
|
|
120
|
-
def initialize(host_keys, host, known_hosts, options = {}
|
|
104
|
+
def initialize(host_keys, host, known_hosts, options = {})
|
|
121
105
|
@host_keys = host_keys
|
|
122
106
|
@host = host
|
|
123
107
|
@known_hosts = known_hosts
|
|
124
108
|
@options = options
|
|
125
|
-
@revoked_keys = revoked_keys
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# Returns true if a @revoked known-hosts entry for this host matches +key+.
|
|
129
|
-
def revoked?(key)
|
|
130
|
-
@revoked_keys.any? { |entry| entry.matches_key?(key) }
|
|
131
109
|
end
|
|
132
110
|
|
|
133
111
|
def add_host_key(key)
|
|
@@ -173,14 +151,11 @@ module Net
|
|
|
173
151
|
# Searches all known host files (see KnownHosts.hostfiles) for all keys
|
|
174
152
|
# of the given host. Returns an enumerable of keys found.
|
|
175
153
|
def search_for(host, options = {})
|
|
176
|
-
|
|
177
|
-
revoked_keys = files.flat_map { |file| KnownHosts.new(file).revoked_keys_for(host) }
|
|
178
|
-
HostKeys.new(search_in(files, host, options), host, self, options, revoked_keys)
|
|
154
|
+
HostKeys.new(search_in(hostfiles(options), host, options), host, self, options)
|
|
179
155
|
end
|
|
180
156
|
|
|
181
157
|
# Search for all known keys for the given host, in every file given in
|
|
182
|
-
# the +files+ array. Returns the list of keys.
|
|
183
|
-
# included.
|
|
158
|
+
# the +files+ array. Returns the list of keys.
|
|
184
159
|
def search_in(files, host, options = {})
|
|
185
160
|
files.flat_map { |file| KnownHosts.new(file).keys_for(host, options) }
|
|
186
161
|
end
|
|
@@ -244,50 +219,46 @@ module Net
|
|
|
244
219
|
# "[net.ssh.test]:5555"
|
|
245
220
|
# "[1,2,3,4]:5555"
|
|
246
221
|
# "[net.ssh.test]:5555,[1.2.3.4]:5555
|
|
247
|
-
#
|
|
248
|
-
# @revoked entries are not included, see revoked_keys_for.
|
|
249
222
|
def keys_for(host, options = {})
|
|
250
223
|
keys = []
|
|
224
|
+
return keys unless File.readable?(source)
|
|
225
|
+
|
|
251
226
|
entries = host.split(/,/)
|
|
252
227
|
host_name = entries[0]
|
|
253
228
|
host_ip = entries[1]
|
|
254
229
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
next unless found
|
|
260
|
-
|
|
261
|
-
found = hostlist.include?(host_ip) if options[:check_host_ip] && entries.size > 1 && hostlist.size > 1
|
|
262
|
-
next unless found
|
|
263
|
-
|
|
264
|
-
raw_key = read_key(key_content)
|
|
265
|
-
|
|
266
|
-
keys <<
|
|
267
|
-
if marker == "@cert-authority"
|
|
268
|
-
HostKeyEntries::CertAuthority.new(raw_key, comment: comment)
|
|
230
|
+
File.open(source) do |file|
|
|
231
|
+
file.each_line do |line|
|
|
232
|
+
if line.start_with?('@')
|
|
233
|
+
marker, hosts, type, key_content, comment = line.split(' ')
|
|
269
234
|
else
|
|
270
|
-
|
|
235
|
+
marker = nil
|
|
236
|
+
hosts, type, key_content, comment = line.split(' ')
|
|
271
237
|
end
|
|
272
|
-
end
|
|
273
238
|
|
|
274
|
-
|
|
275
|
-
|
|
239
|
+
# Skip empty line or one that is commented
|
|
240
|
+
next if hosts.nil? || hosts.start_with?('#')
|
|
276
241
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
242
|
+
hostlist = hosts.split(',')
|
|
243
|
+
|
|
244
|
+
next unless SUPPORTED_TYPE.include?(type)
|
|
245
|
+
|
|
246
|
+
found = hostlist.any? { |pattern| match(host_name, pattern) } || known_host_hash?(hostlist, entries)
|
|
247
|
+
next unless found
|
|
283
248
|
|
|
284
|
-
|
|
285
|
-
|
|
249
|
+
found = hostlist.include?(host_ip) if options[:check_host_ip] && entries.size > 1 && hostlist.size > 1
|
|
250
|
+
next unless found
|
|
286
251
|
|
|
287
|
-
|
|
288
|
-
|
|
252
|
+
blob = key_content.unpack("m*").first
|
|
253
|
+
raw_key = Net::SSH::Buffer.new(blob).read_key
|
|
289
254
|
|
|
290
|
-
|
|
255
|
+
keys <<
|
|
256
|
+
if marker == "@cert-authority"
|
|
257
|
+
HostKeyEntries::CertAuthority.new(raw_key, comment: comment)
|
|
258
|
+
else
|
|
259
|
+
HostKeyEntries::PubKey.new(raw_key, comment: comment)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
291
262
|
end
|
|
292
263
|
|
|
293
264
|
keys
|
|
@@ -332,32 +303,6 @@ module Net
|
|
|
332
303
|
file.puts "#{host} #{key.ssh_type} #{blob}"
|
|
333
304
|
end
|
|
334
305
|
end
|
|
335
|
-
|
|
336
|
-
private
|
|
337
|
-
|
|
338
|
-
# Yields the marker, host patterns, key and comment of each usable line.
|
|
339
|
-
def each_entry
|
|
340
|
-
return unless File.readable?(source)
|
|
341
|
-
|
|
342
|
-
File.open(source) do |file|
|
|
343
|
-
file.each_line do |line|
|
|
344
|
-
fields = line.split(' ')
|
|
345
|
-
marker = fields.shift if fields.first&.start_with?('@')
|
|
346
|
-
hosts, type, key_content, comment = fields
|
|
347
|
-
|
|
348
|
-
# Skip empty line or one that is commented
|
|
349
|
-
next if hosts.nil? || hosts.start_with?('#')
|
|
350
|
-
|
|
351
|
-
next unless SUPPORTED_TYPE.include?(type)
|
|
352
|
-
|
|
353
|
-
yield marker, hosts.split(','), key_content, comment
|
|
354
|
-
end
|
|
355
|
-
end
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
def read_key(key_content)
|
|
359
|
-
Net::SSH::Buffer.new(key_content.unpack("m*").first).read_key
|
|
360
|
-
end
|
|
361
306
|
end
|
|
362
307
|
end
|
|
363
308
|
end
|
|
@@ -16,11 +16,6 @@ module Net
|
|
|
16
16
|
# ...
|
|
17
17
|
# end
|
|
18
18
|
class Command
|
|
19
|
-
# The command line is run by the shell, so like OpenSSH 9.6 (CVE-2023-51385) we refuse
|
|
20
|
-
# host and user names containing shell metacharacters rather than substituting them.
|
|
21
|
-
INVALID_HOST_NAME = /\A-|[[:space:][:cntrl:]'`"$\\;&<>|(){},]/.freeze
|
|
22
|
-
INVALID_USER_NAME = /\A-|[[:cntrl:]'`";&<>|(){}]|[[:space:]]-|\\\z/.freeze
|
|
23
|
-
|
|
24
19
|
# The command line template
|
|
25
20
|
attr_reader :command_line_template
|
|
26
21
|
|
|
@@ -46,13 +41,13 @@ module Net
|
|
|
46
41
|
command_line = @command_line_template.gsub(/%(.)/) {
|
|
47
42
|
case $1
|
|
48
43
|
when 'h'
|
|
49
|
-
|
|
44
|
+
host
|
|
50
45
|
when 'p'
|
|
51
|
-
|
|
46
|
+
port.to_s
|
|
52
47
|
when 'r'
|
|
53
48
|
remote_user = connection_options && connection_options[:remote_user]
|
|
54
49
|
if remote_user
|
|
55
|
-
|
|
50
|
+
remote_user
|
|
56
51
|
else
|
|
57
52
|
raise ArgumentError, "remote user name not available"
|
|
58
53
|
end
|
|
@@ -122,26 +117,6 @@ module Net
|
|
|
122
117
|
Process.kill('TERM', io.pid)
|
|
123
118
|
Thread.new { io.close }
|
|
124
119
|
end
|
|
125
|
-
|
|
126
|
-
private
|
|
127
|
-
|
|
128
|
-
def validate_host_name!(host)
|
|
129
|
-
raise ArgumentError, "host name contains invalid characters: #{host.inspect}" if host.to_s.match?(INVALID_HOST_NAME)
|
|
130
|
-
|
|
131
|
-
host
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def validate_user_name!(user)
|
|
135
|
-
raise ArgumentError, "user name contains invalid characters: #{user.inspect}" if user.to_s.match?(INVALID_USER_NAME)
|
|
136
|
-
|
|
137
|
-
user
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def validate_port!(port)
|
|
141
|
-
raise ArgumentError, "port is not a number: #{port.inspect}" unless port.to_s.match?(/\A\d+\z/)
|
|
142
|
-
|
|
143
|
-
port.to_s
|
|
144
|
-
end
|
|
145
120
|
end
|
|
146
121
|
end
|
|
147
122
|
end
|
data/lib/net/ssh/proxy/https.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
require 'socket'
|
|
2
2
|
require 'openssl'
|
|
3
|
-
require 'resolv'
|
|
4
3
|
require 'net/ssh/proxy/errors'
|
|
5
4
|
require 'net/ssh/proxy/http'
|
|
6
5
|
|
|
@@ -16,14 +15,10 @@ module Net
|
|
|
16
15
|
# can be used to tweak this proxy connection. In addition to the options
|
|
17
16
|
# taken by Net::SSH::Proxy::HTTP it supports:
|
|
18
17
|
#
|
|
19
|
-
# * :ssl_context => the SSL configuration to use for the connection
|
|
20
|
-
# default verifies the proxy's certificate and hostname against the
|
|
21
|
-
# system trust store. For a self-signed or private CA certificate pass a
|
|
22
|
-
# context with it in +cert_store+, or, to skip verification and expose
|
|
23
|
-
# the proxy credentials to a man-in-the-middle, a context with
|
|
24
|
-
# +verify_mode+ set to OpenSSL::SSL::VERIFY_NONE.
|
|
18
|
+
# * :ssl_context => the SSL configuration to use for the connection
|
|
25
19
|
def initialize(proxy_host, proxy_port = 80, options = {})
|
|
26
|
-
@ssl_context = options.delete(:ssl_context) ||
|
|
20
|
+
@ssl_context = options.delete(:ssl_context) ||
|
|
21
|
+
OpenSSL::SSL::SSLContext.new
|
|
27
22
|
super(proxy_host, proxy_port, options)
|
|
28
23
|
end
|
|
29
24
|
|
|
@@ -46,28 +41,9 @@ module Net
|
|
|
46
41
|
plain_socket = super(connect_timeout)
|
|
47
42
|
OpenSSL::SSL::SSLSocket.new(plain_socket, @ssl_context).tap do |socket|
|
|
48
43
|
socket.extend(SSLSocketCompatibility)
|
|
49
|
-
|
|
50
|
-
socket.hostname = proxy_host unless Resolv::AddressRegex.match?(proxy_host)
|
|
51
|
-
begin
|
|
52
|
-
socket.connect
|
|
53
|
-
socket.post_connection_check(proxy_host) if verify_peer?
|
|
54
|
-
rescue StandardError
|
|
55
|
-
socket.close
|
|
56
|
-
raise
|
|
57
|
-
end
|
|
44
|
+
socket.connect
|
|
58
45
|
end
|
|
59
46
|
end
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
def default_ssl_context
|
|
64
|
-
OpenSSL::SSL::SSLContext.new.tap(&:set_params)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# verify_mode is nil on a bare SSLContext in older openssl gems.
|
|
68
|
-
def verify_peer?
|
|
69
|
-
@ssl_context.verify_mode.to_i != OpenSSL::SSL::VERIFY_NONE
|
|
70
|
-
end
|
|
71
47
|
end
|
|
72
48
|
end
|
|
73
49
|
end
|
data/lib/net/ssh/proxy/jump.rb
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
require 'shellwords'
|
|
2
1
|
require 'uri'
|
|
3
2
|
require 'net/ssh/proxy/command'
|
|
4
3
|
|
|
@@ -35,50 +34,20 @@ module Net
|
|
|
35
34
|
# We cannot build the ProxyCommand template until we know if the :config
|
|
36
35
|
# option was specified during `Net::SSH.start`.
|
|
37
36
|
def build_proxy_command_equivalent(connection_options = nil)
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
first_jump, extra_jumps = jump_proxies.split(",", 2)
|
|
40
38
|
config = connection_options && connection_options[:config]
|
|
41
|
-
uri
|
|
39
|
+
uri = URI.parse("ssh://#{first_jump}")
|
|
42
40
|
|
|
43
41
|
template = "ssh".dup
|
|
44
|
-
template << " -l #{
|
|
45
|
-
template << " -p #{uri.port}"
|
|
46
|
-
template << " -J #{
|
|
47
|
-
template << " -F #{
|
|
42
|
+
template << " -l #{uri.user}" if uri.user
|
|
43
|
+
template << " -p #{uri.port}" if uri.port
|
|
44
|
+
template << " -J #{extra_jumps}" if extra_jumps
|
|
45
|
+
template << " -F #{config}" if config != true && config
|
|
48
46
|
template << " -W %h:%p "
|
|
49
|
-
template <<
|
|
47
|
+
template << uri.host
|
|
50
48
|
|
|
51
49
|
@command_line_template = template
|
|
52
50
|
end
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
# Escape for the shell, and double any % so it survives the %h/%p
|
|
57
|
-
# substitution done on the template in Command#open.
|
|
58
|
-
def escape(value)
|
|
59
|
-
Shellwords.escape(value).gsub("%", "%%")
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Only [user@]host[:port] is checked, so anything else URI accepts (a path,
|
|
63
|
-
# query, fragment or password) must be refused before it reaches the shell.
|
|
64
|
-
def validate_jump!(jump)
|
|
65
|
-
uri = URI.parse("ssh://#{jump}")
|
|
66
|
-
raise URI::InvalidURIError if uri.host.to_s.empty? || jump_spec(uri) != jump
|
|
67
|
-
|
|
68
|
-
validate_user_name!(uri.user) if uri.user
|
|
69
|
-
validate_host_name!(uri.host)
|
|
70
|
-
validate_port!(uri.port) if uri.port
|
|
71
|
-
uri
|
|
72
|
-
rescue URI::InvalidURIError
|
|
73
|
-
raise ArgumentError, "jump host is not [user@]host[:port]: #{jump.inspect}"
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def jump_spec(uri)
|
|
77
|
-
spec = uri.host.dup
|
|
78
|
-
spec.prepend("#{uri.user}@") if uri.user
|
|
79
|
-
spec << ":#{uri.port}" if uri.port
|
|
80
|
-
spec
|
|
81
|
-
end
|
|
82
51
|
end
|
|
83
52
|
end
|
|
84
53
|
end
|
|
@@ -405,12 +405,6 @@ module Net
|
|
|
405
405
|
|
|
406
406
|
# The callback used when an auth-agent channel is requested by the server.
|
|
407
407
|
def auth_agent_channel(session, channel, packet)
|
|
408
|
-
# Like OpenSSH, only relay the local agent when forwarding was requested (GHSA-qpg5-fx6p-8cq3).
|
|
409
|
-
unless session.options[:forward_agent] || @agent_forwarded
|
|
410
|
-
error { "server tried agent forwarding, which was not requested; refusing" }
|
|
411
|
-
raise Net::SSH::ChannelOpenFailed.new(1, "agent forwarding was not requested")
|
|
412
|
-
end
|
|
413
|
-
|
|
414
408
|
info { "opening auth-agent channel" }
|
|
415
409
|
channel[:invisible] = true
|
|
416
410
|
|
|
@@ -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
|