net-ssh 7.2.3 → 8.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.dockerignore +3 -0
  4. data/.github/pull_request_template.md +1 -0
  5. data/.github/workflows/changelog.yml +18 -0
  6. data/.github/workflows/ci-with-docker.yml +20 -9
  7. data/.github/workflows/ci.yml +10 -20
  8. data/.github/workflows/rubocop.yml +3 -1
  9. data/.rubocop.yml +5 -1
  10. data/.rubocop_todo.yml +4 -8
  11. data/CHANGES.txt +122 -0
  12. data/CONTRIBUTING.md +34 -0
  13. data/Dockerfile +4 -1
  14. data/Dockerfile.openssl3 +1 -1
  15. data/Dockerfile.zlib_ng +29 -0
  16. data/Gemfile +2 -0
  17. data/README.md +74 -57
  18. data/Rakefile +30 -12
  19. data/appveyor.yml +4 -16
  20. data/changelog.d/README.md +1 -0
  21. data/docker-compose.yml +1 -9
  22. data/lib/net/ssh/authentication/agent.rb +35 -11
  23. data/lib/net/ssh/authentication/ed25519.rb +140 -37
  24. data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
  25. data/lib/net/ssh/authentication/key_manager.rb +42 -16
  26. data/lib/net/ssh/authentication/methods/abstract.rb +2 -8
  27. data/lib/net/ssh/authentication/methods/publickey.rb +3 -1
  28. data/lib/net/ssh/authentication/security_key.rb +71 -0
  29. data/lib/net/ssh/authentication/session.rb +29 -17
  30. data/lib/net/ssh/buffer.rb +6 -1
  31. data/lib/net/ssh/buffered_io.rb +40 -3
  32. data/lib/net/ssh/config.rb +30 -13
  33. data/lib/net/ssh/connection/channel.rb +49 -2
  34. data/lib/net/ssh/connection/session.rb +151 -13
  35. data/lib/net/ssh/errors.rb +24 -6
  36. data/lib/net/ssh/key_factory.rb +11 -4
  37. data/lib/net/ssh/known_hosts.rb +62 -8
  38. data/lib/net/ssh/multiplex/client.rb +162 -0
  39. data/lib/net/ssh/multiplex/control_path.rb +120 -0
  40. data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
  41. data/lib/net/ssh/multiplex.rb +126 -0
  42. data/lib/net/ssh/prompt.rb +3 -3
  43. data/lib/net/ssh/proxy/command.rb +28 -3
  44. data/lib/net/ssh/proxy/jump.rb +71 -8
  45. data/lib/net/ssh/service/forward.rb +44 -27
  46. data/lib/net/ssh/test/extensions.rb +3 -2
  47. data/lib/net/ssh/test/socket.rb +1 -1
  48. data/lib/net/ssh/test.rb +2 -1
  49. data/lib/net/ssh/transport/aes128_gcm.rb +40 -0
  50. data/lib/net/ssh/transport/aes256_gcm.rb +40 -0
  51. data/lib/net/ssh/transport/algorithms.rb +96 -34
  52. data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
  53. data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
  54. data/lib/net/ssh/transport/cipher_factory.rb +32 -8
  55. data/lib/net/ssh/transport/constants.rb +1 -0
  56. data/lib/net/ssh/transport/gcm_cipher.rb +228 -0
  57. data/lib/net/ssh/transport/hmac/abstract.rb +16 -0
  58. data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
  59. data/lib/net/ssh/transport/hmac.rb +2 -0
  60. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
  61. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
  62. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
  63. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
  64. data/lib/net/ssh/transport/openssl.rb +2 -1
  65. data/lib/net/ssh/transport/packet_stream.rb +19 -3
  66. data/lib/net/ssh/transport/server_version.rb +33 -13
  67. data/lib/net/ssh/transport/session.rb +61 -46
  68. data/lib/net/ssh/transport/state.rb +1 -1
  69. data/lib/net/ssh/verifiers/always.rb +43 -8
  70. data/lib/net/ssh/version.rb +3 -3
  71. data/lib/net/ssh.rb +148 -63
  72. data/net-ssh-public_cert.pem +7 -7
  73. data/net-ssh.gemspec +6 -8
  74. data.tar.gz.sig +0 -0
  75. metadata +64 -46
  76. metadata.gz.sig +0 -0
  77. data/Gemfile.noed25519 +0 -12
  78. data/Gemfile.norbnacl +0 -12
@@ -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
- ssh-rsa-cert-v01@openssh.com
35
- ssh-rsa-cert-v00@openssh.com
36
- ssh-rsa
37
- rsa-sha2-256
38
- rsa-sha2-512],
35
+ rsa-sha2-512
36
+ rsa-sha2-256],
39
37
 
40
38
  kex: %w[ecdh-sha2-nistp521
41
39
  ecdh-sha2-nistp384
42
40
  ecdh-sha2-nistp256
43
41
  diffie-hellman-group-exchange-sha256
44
- diffie-hellman-group14-sha256
45
- diffie-hellman-group14-sha1],
42
+ diffie-hellman-group14-sha256],
46
43
 
47
- encryption: %w[aes256-ctr aes192-ctr aes128-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] + %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],
76
88
 
77
89
  kex: DEFAULT_ALGORITHMS[:kex] +
78
- %w[diffie-hellman-group-exchange-sha1
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, options[algorithm] || DEFAULT_ALGORITHMS[algorithm],
269
- options[:append_all_supported_algorithms]
291
+ supported,
292
+ DEFAULT_ALGORITHMS[algorithm],
293
+ options[algorithm] || DEFAULT_ALGORITHMS[algorithm],
294
+ options[:append_all_supported_algorithms],
295
+ supported - EXPLICIT_ONLY_ALGORITHMS.fetch(algorithm, [])
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] }.uniq
314
+ host_keys = existing_keys.flat_map { |key| key.respond_to?(:ssh_types) ? key.ssh_types : [key.ssh_type] }
315
+ # A known ssh-rsa key also verifies with rsa-sha2-*, and must not re-enable disabled SHA-1 algorithms.
316
+ disabled_sha1 = %w[ssh-rsa ssh-rsa-cert-v01@openssh.com ssh-rsa-cert-v00@openssh.com] - algorithms[:host_key]
317
+ host_keys = host_keys.flat_map { |type| type == 'ssh-rsa' ? %w[rsa-sha2-512 rsa-sha2-256 ssh-rsa] : [type] }
318
+ host_keys = (host_keys - disabled_sha1).uniq
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
- list = supported.dup
303
-
304
- appends = option.select { |opt| opt.start_with?('+') }.map { |opt| opt[1..-1] }
305
- deletions = option.select { |opt| opt.start_with?('-') }.map { |opt| opt[1..-1] }
333
+ if option.first && option.first.start_with?('+', '-', '^')
334
+ ops = { '^' => [], '+' => [], '-' => [] }
335
+ cur_list = nil
336
+ option.each do |cur_opt|
337
+ if ops.key?(cur_opt[0])
338
+ cur_list = ops[cur_opt[0]]
339
+ cur_opt = cur_opt[1..-1]
340
+ end
341
+ cur_list << cur_opt
342
+ end
306
343
 
307
- list.concat(appends)
344
+ list = ops['^'] + defaults + ops['+']
345
+ list.concat(appendable) if append_all_supported_algorithms
308
346
 
309
- deletions.each do |opt|
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
- supported.each { |name| list << name unless list.include?(name) }
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.read_string.split(/,/)
347
- data[:host_key] = packet.read_string.split(/,/)
348
- data[:encryption_client] = packet.read_string.split(/,/)
349
- data[:encryption_server] = packet.read_string.split(/,/)
350
- data[:hmac_client] = packet.read_string.split(/,/)
351
- data[:hmac_server] = packet.read_string.split(/,/)
352
- data[:compression_client] = packet.read_string.split(/,/)
353
- data[:compression_server] = packet.read_string.split(/,/)
354
- data[:language_client] = packet.read_string.split(/,/)
355
- data[:language_server] = packet.read_string.split(/,/)
397
+ data[:kex] = read_name_list(packet)
398
+ data[:host_key] = read_name_list(packet)
399
+ data[:encryption_client] = read_name_list(packet)
400
+ data[:encryption_server] = read_name_list(packet)
401
+ data[:hmac_client] = read_name_list(packet)
402
+ data[:hmac_server] = read_name_list(packet)
403
+ data[:compression_client] = read_name_list(packet)
404
+ data[:compression_server] = read_name_list(packet)
405
+ data[:language_client] = read_name_list(packet)
406
+ data[:language_server] = read_name_list(packet)
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 'rbnacl'
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 = [0, 0, 0, sequence_number].pack("NNNN")
54
+ iv_data = packet_iv(sequence_number)
47
55
  @chacha_main.iv = iv_data
48
- poly_key = @chacha_main.update(([0] * 32).pack('C32'))
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 += @poly.auth(poly_key, 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 = [0, 0, 0, sequence_number].pack("NNNN")
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 = [0, 0, 0, sequence_number].pack("NNNN")
79
+ iv_data = packet_iv(sequence_number)
72
80
  @chacha_main.iv = iv_data
73
- poly_key = @chacha_main.update(([0] * 32).pack('C32'))
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
- begin
79
- ok = @poly.verify(poly_key, mac, data[0..])
80
- raise Net::SSH::Exception, "corrupted hmac detected #{name}" unless ok
81
- rescue RbNaCl::BadAuthenticatorError
82
- raise Net::SSH::Exception, "corrupted hmac detected #{name}"
83
- end
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
- 16
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
- "chacha20-poly1305@openssh.com"
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 poly1305 support which requires optinal dependency rbnacl
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
- 'chacha20-poly1305@openssh.com' => Net::SSH::Transport::ChaCha20Poly1305Cipher
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
- return [klass.key_length, klass.block_size] unless klass.nil?
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
@@ -12,6 +12,7 @@ module Net
12
12
  DEBUG = 4
13
13
  SERVICE_REQUEST = 5
14
14
  SERVICE_ACCEPT = 6
15
+ EXT_INFO = 7
15
16
 
16
17
  #--
17
18
  # Algorithm negotiation messages