net-ssh 7.2.0 → 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/.dockerignore +3 -0
- data/.github/pull_request_template.md +1 -0
- data/.github/workflows/changelog.yml +43 -0
- data/.github/workflows/ci-with-docker.yml +20 -9
- data/.github/workflows/ci.yml +11 -20
- data/.github/workflows/rubocop.yml +3 -1
- data/.gitignore +2 -0
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +4 -1
- data/CHANGES.txt +114 -0
- data/CONTRIBUTING.md +35 -0
- data/Dockerfile +7 -2
- data/Dockerfile.openssl3 +1 -1
- data/Dockerfile.zlib_ng +29 -0
- data/README.md +76 -57
- data/Rakefile +117 -30
- data/appveyor.yml +4 -16
- data/changelog.d/README.md +1 -0
- data/docker-compose.yml +2 -8
- data/lib/net/ssh/authentication/agent.rb +35 -11
- data/lib/net/ssh/authentication/ed25519.rb +141 -40
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/authentication/key_manager.rb +61 -18
- 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/pub_key_fingerprint.rb +1 -1
- data/lib/net/ssh/authentication/security_key.rb +71 -0
- data/lib/net/ssh/authentication/session.rb +34 -15
- data/lib/net/ssh/buffer.rb +6 -1
- data/lib/net/ssh/buffered_io.rb +41 -4
- data/lib/net/ssh/config.rb +30 -13
- 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 +11 -4
- data/lib/net/ssh/known_hosts.rb +64 -11
- 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/command.rb +28 -3
- data/lib/net/ssh/proxy/jump.rb +52 -4
- data/lib/net/ssh/service/forward.rb +38 -27
- data/lib/net/ssh/test/extensions.rb +3 -2
- data/lib/net/ssh/test/socket.rb +1 -1
- data/lib/net/ssh/test.rb +2 -1
- data/lib/net/ssh/transport/aes128_gcm.rb +40 -0
- data/lib/net/ssh/transport/aes256_gcm.rb +40 -0
- data/lib/net/ssh/transport/algorithms.rb +96 -34
- 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 +32 -8
- data/lib/net/ssh/transport/constants.rb +1 -0
- data/lib/net/ssh/transport/gcm_cipher.rb +228 -0
- data/lib/net/ssh/transport/hmac/abstract.rb +16 -0
- 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 +19 -3
- data/lib/net/ssh/transport/server_version.rb +33 -13
- data/lib/net/ssh/transport/session.rb +60 -46
- data/lib/net/ssh/transport/state.rb +1 -1
- data/lib/net/ssh/verifiers/always.rb +43 -8
- data/lib/net/ssh/version.rb +2 -2
- data/lib/net/ssh.rb +142 -62
- data/net-ssh-public_cert.pem +19 -18
- data/net-ssh.gemspec +7 -8
- data/rakelib/changelog.rb +229 -0
- data.tar.gz.sig +0 -0
- metadata +86 -53
- metadata.gz.sig +1 -1
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
- data/lib/net/ssh/version.rb.old +0 -68
|
@@ -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,24 +32,34 @@ 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
|
-
encryption: %w[aes256-ctr
|
|
44
|
+
encryption: %w[aes256-ctr
|
|
45
|
+
aes192-ctr
|
|
46
|
+
aes128-ctr
|
|
47
|
+
aes256-gcm@openssh.com
|
|
48
|
+
aes128-gcm@openssh.com],
|
|
48
49
|
|
|
49
50
|
hmac: %w[hmac-sha2-512-etm@openssh.com hmac-sha2-256-etm@openssh.com
|
|
51
|
+
hmac-sha1-etm@openssh.com
|
|
50
52
|
hmac-sha2-512 hmac-sha2-256
|
|
51
|
-
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]
|
|
52
63
|
}.freeze
|
|
53
64
|
|
|
54
65
|
if Net::SSH::Transport::ChaCha20Poly1305CipherLoader::LOADED
|
|
@@ -72,10 +83,12 @@ module Net
|
|
|
72
83
|
|
|
73
84
|
# Define all algorithms, with the deprecated, supported by Net::SSH.
|
|
74
85
|
ALGORITHMS = {
|
|
75
|
-
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],
|
|
76
88
|
|
|
77
89
|
kex: DEFAULT_ALGORITHMS[:kex] +
|
|
78
|
-
%w[diffie-hellman-
|
|
90
|
+
%w[diffie-hellman-group14-sha1
|
|
91
|
+
diffie-hellman-group-exchange-sha1
|
|
79
92
|
diffie-hellman-group1-sha1],
|
|
80
93
|
|
|
81
94
|
encryption: DEFAULT_ALGORITHMS[:encryption] +
|
|
@@ -94,10 +107,20 @@ module Net
|
|
|
94
107
|
hmac-md5 hmac-md5-96
|
|
95
108
|
none],
|
|
96
109
|
|
|
110
|
+
pubkey_algorithms: DEFAULT_ALGORITHMS[:pubkey_algorithms] + %w[ssh-rsa-cert-v01@openssh.com ssh-rsa ssh-dss],
|
|
111
|
+
|
|
97
112
|
compression: %w[none zlib@openssh.com zlib],
|
|
98
113
|
language: %w[]
|
|
99
114
|
}.freeze
|
|
100
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
|
+
|
|
101
124
|
# The underlying transport layer session that supports this object
|
|
102
125
|
attr_reader :session
|
|
103
126
|
|
|
@@ -265,11 +288,16 @@ module Net
|
|
|
265
288
|
|
|
266
289
|
ALGORITHMS.each do |algorithm, supported|
|
|
267
290
|
algorithms[algorithm] = compose_algorithm_list(
|
|
268
|
-
supported,
|
|
269
|
-
|
|
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, [])
|
|
270
296
|
)
|
|
271
297
|
end
|
|
272
298
|
|
|
299
|
+
warn_deprecated_host_key if host_key_explicitly_named?(DEPRECATED_HOST_KEY)
|
|
300
|
+
|
|
273
301
|
# for convention, make sure our list has the same keys as the server
|
|
274
302
|
# list
|
|
275
303
|
|
|
@@ -283,7 +311,11 @@ module Net
|
|
|
283
311
|
# existing known key for the host has preference.
|
|
284
312
|
|
|
285
313
|
existing_keys = session.host_keys
|
|
286
|
-
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
|
|
287
319
|
algorithms[:host_key].each do |name|
|
|
288
320
|
host_keys << name unless host_keys.include?(name)
|
|
289
321
|
end
|
|
@@ -292,21 +324,27 @@ module Net
|
|
|
292
324
|
end
|
|
293
325
|
|
|
294
326
|
# Composes the list of algorithms by taking supported algorithms and matching with supplied options.
|
|
295
|
-
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)
|
|
296
328
|
return supported.dup unless option
|
|
297
329
|
|
|
298
330
|
list = []
|
|
299
331
|
option = Array(option).compact.uniq
|
|
300
332
|
|
|
301
|
-
if option.first && option.first.start_with?('+', '-')
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
306
343
|
|
|
307
|
-
list
|
|
344
|
+
list = ops['^'] + defaults + ops['+']
|
|
345
|
+
list.concat(appendable) if append_all_supported_algorithms
|
|
308
346
|
|
|
309
|
-
|
|
347
|
+
ops['-'].each do |opt|
|
|
310
348
|
if opt.include?('*')
|
|
311
349
|
opt_escaped = Regexp.escape(opt)
|
|
312
350
|
algo_re = /\A#{opt_escaped.gsub('\*', '[A-Za-z\d\-@\.]*')}\z/
|
|
@@ -321,7 +359,7 @@ module Net
|
|
|
321
359
|
list = option
|
|
322
360
|
|
|
323
361
|
if append_all_supported_algorithms
|
|
324
|
-
|
|
362
|
+
appendable.each { |name| list << name unless list.include?(name) }
|
|
325
363
|
end
|
|
326
364
|
end
|
|
327
365
|
|
|
@@ -337,22 +375,35 @@ module Net
|
|
|
337
375
|
list
|
|
338
376
|
end
|
|
339
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
|
+
|
|
340
391
|
# Parses a KEXINIT packet from the server.
|
|
341
392
|
def parse_server_algorithm_packet(packet)
|
|
342
393
|
data = { raw: packet.content }
|
|
343
394
|
|
|
344
395
|
packet.read(16) # skip the cookie value
|
|
345
396
|
|
|
346
|
-
data[:kex] = packet
|
|
347
|
-
data[:host_key] = packet
|
|
348
|
-
data[:encryption_client] = packet
|
|
349
|
-
data[:encryption_server] = packet
|
|
350
|
-
data[:hmac_client] = packet
|
|
351
|
-
data[:hmac_server] = packet
|
|
352
|
-
data[:compression_client] = packet
|
|
353
|
-
data[:compression_server] = packet
|
|
354
|
-
data[:language_client] = packet
|
|
355
|
-
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)
|
|
356
407
|
|
|
357
408
|
# TODO: if first_kex_packet_follows, we need to try to skip the
|
|
358
409
|
# actual kexinit stuff and try to guess what the server is doing...
|
|
@@ -362,6 +413,13 @@ module Net
|
|
|
362
413
|
return data
|
|
363
414
|
end
|
|
364
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
|
+
|
|
365
423
|
# Given the #algorithms map of preferred algorithm types, this constructs
|
|
366
424
|
# a KEXINIT packet to send to the server. It does not actually send it,
|
|
367
425
|
# it simply builds the packet and returns it.
|
|
@@ -387,6 +445,7 @@ module Net
|
|
|
387
445
|
def negotiate_algorithms
|
|
388
446
|
@kex = negotiate(:kex)
|
|
389
447
|
@host_key = negotiate(:host_key)
|
|
448
|
+
warn_deprecated_host_key if @host_key == DEPRECATED_HOST_KEY
|
|
390
449
|
@encryption_client = negotiate(:encryption_client)
|
|
391
450
|
@encryption_server = negotiate(:encryption_server)
|
|
392
451
|
@hmac_client = negotiate(:hmac_client)
|
|
@@ -492,6 +551,9 @@ module Net
|
|
|
492
551
|
HMAC.get(hmac_server, mac_key_server, parameters)
|
|
493
552
|
end
|
|
494
553
|
|
|
554
|
+
cipher_client.nonce = iv_client if mac_client.respond_to?(:aead) && mac_client.aead
|
|
555
|
+
cipher_server.nonce = iv_server if mac_server.respond_to?(:aead) && mac_client.aead
|
|
556
|
+
|
|
495
557
|
session.configure_client cipher: cipher_client, hmac: mac_client,
|
|
496
558
|
compression: normalize_compression_name(compression_client),
|
|
497
559
|
compression_level: options[:compression_level],
|
|
@@ -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
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
require 'openssl'
|
|
2
2
|
require 'net/ssh/transport/ctr.rb'
|
|
3
|
+
require 'net/ssh/transport/aes128_gcm'
|
|
4
|
+
require 'net/ssh/transport/aes256_gcm'
|
|
3
5
|
require 'net/ssh/transport/key_expander'
|
|
4
6
|
require 'net/ssh/transport/identity_cipher'
|
|
5
7
|
require 'net/ssh/transport/chacha20_poly1305_cipher_loader'
|
|
@@ -31,15 +33,15 @@ module Net
|
|
|
31
33
|
'none' => 'none'
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
SSH_TO_CLASS =
|
|
36
|
+
SSH_TO_CLASS = {
|
|
37
|
+
'aes256-gcm@openssh.com' => Net::SSH::Transport::AES256_GCM,
|
|
38
|
+
'aes128-gcm@openssh.com' => Net::SSH::Transport::AES128_GCM
|
|
39
|
+
}.tap do |hash|
|
|
35
40
|
if Net::SSH::Transport::ChaCha20Poly1305CipherLoader::LOADED
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
else
|
|
40
|
-
{
|
|
41
|
-
}
|
|
41
|
+
hash['chacha20-poly1305@openssh.com'] =
|
|
42
|
+
Net::SSH::Transport::ChaCha20Poly1305Cipher
|
|
42
43
|
end
|
|
44
|
+
end
|
|
43
45
|
|
|
44
46
|
# Returns true if the underlying OpenSSL library supports the given cipher,
|
|
45
47
|
# and false otherwise.
|
|
@@ -98,7 +100,11 @@ module Net
|
|
|
98
100
|
# if :iv_len option is supplied the third return value will be ivlen
|
|
99
101
|
def self.get_lengths(name, options = {})
|
|
100
102
|
klass = SSH_TO_CLASS[name]
|
|
101
|
-
|
|
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
|
|
102
108
|
|
|
103
109
|
ossl_name = SSH_TO_OSSL[name]
|
|
104
110
|
if ossl_name.nil? || ossl_name == "none"
|
|
@@ -122,6 +128,24 @@ module Net
|
|
|
122
128
|
end
|
|
123
129
|
result
|
|
124
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
|
|
125
149
|
end
|
|
126
150
|
end
|
|
127
151
|
end
|