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.
Files changed (81) 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 +43 -0
  6. data/.github/workflows/ci-with-docker.yml +20 -9
  7. data/.github/workflows/ci.yml +11 -20
  8. data/.github/workflows/rubocop.yml +3 -1
  9. data/.gitignore +2 -0
  10. data/.rubocop.yml +1 -1
  11. data/.rubocop_todo.yml +4 -1
  12. data/CHANGES.txt +114 -0
  13. data/CONTRIBUTING.md +35 -0
  14. data/Dockerfile +7 -2
  15. data/Dockerfile.openssl3 +1 -1
  16. data/Dockerfile.zlib_ng +29 -0
  17. data/README.md +76 -57
  18. data/Rakefile +117 -30
  19. data/appveyor.yml +4 -16
  20. data/changelog.d/README.md +1 -0
  21. data/docker-compose.yml +2 -8
  22. data/lib/net/ssh/authentication/agent.rb +35 -11
  23. data/lib/net/ssh/authentication/ed25519.rb +141 -40
  24. data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
  25. data/lib/net/ssh/authentication/key_manager.rb +61 -18
  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/pub_key_fingerprint.rb +1 -1
  29. data/lib/net/ssh/authentication/security_key.rb +71 -0
  30. data/lib/net/ssh/authentication/session.rb +34 -15
  31. data/lib/net/ssh/buffer.rb +6 -1
  32. data/lib/net/ssh/buffered_io.rb +41 -4
  33. data/lib/net/ssh/config.rb +30 -13
  34. data/lib/net/ssh/connection/channel.rb +49 -2
  35. data/lib/net/ssh/connection/session.rb +58 -8
  36. data/lib/net/ssh/errors.rb +24 -6
  37. data/lib/net/ssh/key_factory.rb +11 -4
  38. data/lib/net/ssh/known_hosts.rb +64 -11
  39. data/lib/net/ssh/multiplex/client.rb +162 -0
  40. data/lib/net/ssh/multiplex/control_path.rb +120 -0
  41. data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
  42. data/lib/net/ssh/multiplex.rb +130 -0
  43. data/lib/net/ssh/prompt.rb +3 -3
  44. data/lib/net/ssh/proxy/command.rb +28 -3
  45. data/lib/net/ssh/proxy/jump.rb +52 -4
  46. data/lib/net/ssh/service/forward.rb +38 -27
  47. data/lib/net/ssh/test/extensions.rb +3 -2
  48. data/lib/net/ssh/test/socket.rb +1 -1
  49. data/lib/net/ssh/test.rb +2 -1
  50. data/lib/net/ssh/transport/aes128_gcm.rb +40 -0
  51. data/lib/net/ssh/transport/aes256_gcm.rb +40 -0
  52. data/lib/net/ssh/transport/algorithms.rb +96 -34
  53. data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
  54. data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
  55. data/lib/net/ssh/transport/cipher_factory.rb +32 -8
  56. data/lib/net/ssh/transport/constants.rb +1 -0
  57. data/lib/net/ssh/transport/gcm_cipher.rb +228 -0
  58. data/lib/net/ssh/transport/hmac/abstract.rb +16 -0
  59. data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
  60. data/lib/net/ssh/transport/hmac.rb +2 -0
  61. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
  62. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
  63. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
  64. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
  65. data/lib/net/ssh/transport/openssl.rb +2 -1
  66. data/lib/net/ssh/transport/packet_stream.rb +19 -3
  67. data/lib/net/ssh/transport/server_version.rb +33 -13
  68. data/lib/net/ssh/transport/session.rb +60 -46
  69. data/lib/net/ssh/transport/state.rb +1 -1
  70. data/lib/net/ssh/verifiers/always.rb +43 -8
  71. data/lib/net/ssh/version.rb +2 -2
  72. data/lib/net/ssh.rb +142 -62
  73. data/net-ssh-public_cert.pem +19 -18
  74. data/net-ssh.gemspec +7 -8
  75. data/rakelib/changelog.rb +229 -0
  76. data.tar.gz.sig +0 -0
  77. metadata +86 -53
  78. metadata.gz.sig +1 -1
  79. data/Gemfile.noed25519 +0 -12
  80. data/Gemfile.norbnacl +0 -12
  81. data/lib/net/ssh/version.rb.old +0 -68
@@ -0,0 +1,228 @@
1
+ require 'net/ssh/loggable'
2
+
3
+ module Net
4
+ module SSH
5
+ module Transport
6
+ ## Extension module for aes(128|256)gcm ciphers
7
+ module GCMCipher
8
+ BLOCK_SIZE = 16
9
+ IV_BYTES = 12
10
+ AUTH_TAG_BYTES = 16
11
+
12
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
13
+ def self.extended(orig)
14
+ # rubocop:disable Metrics/BlockLength
15
+ orig.class_eval do
16
+ include Net::SSH::Loggable
17
+
18
+ attr_reader :cipher
19
+ attr_reader :key
20
+ attr_reader :nonce
21
+
22
+ #
23
+ # Semantically gcm cipher supplies the OpenSSL iv interface with a nonce
24
+ # as it is not randomly generated due to being supplied from a counter.
25
+ # The RFC's use IV and nonce interchangeably.
26
+ #
27
+ def initialize(encrypt:, key:)
28
+ @cipher = OpenSSL::Cipher.new(algo_name)
29
+ @key = key
30
+ key_len = @cipher.key_len
31
+ if key.size != key_len
32
+ error_message = "#{cipher_name}: keylength does not match"
33
+ error { error_message }
34
+ raise error_message
35
+ end
36
+ encrypt ? @cipher.encrypt : @cipher.decrypt
37
+ @cipher.key = key
38
+
39
+ @nonce = {
40
+ fixed: nil,
41
+ invocation_counter: 0
42
+ }
43
+ end
44
+
45
+ def update_cipher_mac(payload, _sequence_number)
46
+ #
47
+ # --- RFC 5647 7.3 ---
48
+ # When using AES-GCM with secure shell, the packet_length field is to
49
+ # be treated as additional authenticated data, not as plaintext.
50
+ #
51
+ length_data = [payload.bytesize].pack('N')
52
+
53
+ cipher.auth_data = length_data
54
+
55
+ encrypted_data = cipher.update(payload) << cipher.final
56
+
57
+ mac = cipher.auth_tag
58
+
59
+ incr_nonce
60
+ length_data + encrypted_data + mac
61
+ end
62
+
63
+ #
64
+ # --- RFC 5647 ---
65
+ # uint32 packet_length; // 0 <= packet_length < 2^32
66
+ #
67
+ def read_length(data, _sequence_number)
68
+ data.unpack1('N')
69
+ end
70
+
71
+ #
72
+ # --- RFC 5647 ---
73
+ # In AES-GCM secure shell, the inputs to the authenticated encryption
74
+ # are:
75
+ # PT (Plain Text)
76
+ # byte padding_length; // 4 <= padding_length < 256
77
+ # byte[n1] payload; // n1 = packet_length-padding_length-1
78
+ # byte[n2] random_padding; // n2 = padding_length
79
+ # AAD (Additional Authenticated Data)
80
+ # uint32 packet_length; // 0 <= packet_length < 2^32
81
+ # IV (Initialization Vector)
82
+ # As described in section 7.1.
83
+ # BK (Block Cipher Key)
84
+ # The appropriate Encryption Key formed during the Key Exchange.
85
+ #
86
+ def read_and_mac(data, mac, _sequence_number)
87
+ # The authentication tag will be placed in the MAC field at the end of the packet
88
+
89
+ # OpenSSL does not verify auth tag length
90
+ # GCM mode allows arbitrary sizes for the auth_tag up to 128 bytes and a single
91
+ # byte allows authentication to pass. If single byte auth tags are possible
92
+ # an attacker would require no more than 256 attempts to forge a valid tag.
93
+ #
94
+ raise 'incorrect auth_tag length' unless mac.to_s.length == mac_length
95
+
96
+ packet_length = data.unpack1('N')
97
+
98
+ cipher.auth_tag = mac.to_s
99
+ cipher.auth_data = [packet_length].pack('N')
100
+
101
+ result = cipher.update(data[4...]) << cipher.final
102
+ incr_nonce
103
+ result
104
+ end
105
+
106
+ def mac_length
107
+ GCMCipher::AUTH_TAG_BYTES
108
+ end
109
+
110
+ def block_size
111
+ GCMCipher::BLOCK_SIZE
112
+ end
113
+
114
+ def self.block_size
115
+ GCMCipher::BLOCK_SIZE
116
+ end
117
+
118
+ def self.iv_len
119
+ GCMCipher::IV_BYTES
120
+ end
121
+
122
+ def self.auth_length
123
+ GCMCipher::AUTH_TAG_BYTES
124
+ end
125
+
126
+ def self.decrypt_private_key(ciphertext, auth_tag, key, initialization_vector)
127
+ cipher = new(encrypt: false, key: key)
128
+
129
+ cipher.nonce = initialization_vector
130
+ cipher.cipher.auth_tag = auth_tag.to_s
131
+ cipher.cipher.auth_data = ''
132
+ cipher.cipher.update(ciphertext.to_s) << cipher.cipher.final
133
+ end
134
+
135
+ #
136
+ # --- RFC 5647 ---
137
+ # N_MIN minimum nonce (IV) length 12 octets
138
+ # N_MAX maximum nonce (IV) length 12 octets
139
+ #
140
+ def iv_len
141
+ GCMCipher::IV_BYTES
142
+ end
143
+
144
+ #
145
+ # --- RFC 5288 ---
146
+ # Each value of the nonce_explicit MUST be distinct for each distinct
147
+ # invocation of the GCM encrypt function for any fixed key. Failure to
148
+ # meet this uniqueness requirement can significantly degrade security.
149
+ # The nonce_explicit MAY be the 64-bit sequence number.
150
+ #
151
+ # --- RFC 5116 ---
152
+ # (2.1) Applications that can generate distinct nonces SHOULD use the nonce
153
+ # formation method defined in Section 3.2, and MAY use any
154
+ # other method that meets the uniqueness requirement.
155
+ #
156
+ # (3.2) The following method to construct nonces is RECOMMENDED.
157
+ #
158
+ # <- variable -> <- variable ->
159
+ # - - - - - - - - - - - - - -
160
+ # | fixed | counter |
161
+ #
162
+ # Initial octets consist of a fixed field and final octets consist of a
163
+ # Counter field. Implementations SHOULD support 12-octet nonces in which
164
+ # the Counter field is four octets long.
165
+ # The Counter fields of successive nonces form a monotonically increasing
166
+ # sequence, when those fields are regarded as unsignd integers in network
167
+ # byte order.
168
+ # The Counter part SHOULD be equal to zero for the first nonce and increment
169
+ # by one for each successive nonce that is generated.
170
+ # The Fixed field MUST remain constant for all nonces that are generated for
171
+ # a given encryption device.
172
+ #
173
+ # --- RFC 5647 ---
174
+ # The invocation field is treated as a 64-bit integer and is increment after
175
+ # each invocation of AES-GCM to process a binary packet.
176
+ # AES-GCM produces a keystream in blocks of 16-octets that is used to
177
+ # encrypt the plaintext. This keystream is produced by encrypting the
178
+ # following 16-octet data structure:
179
+ #
180
+ # uint32 fixed; // 4 octets
181
+ # uint64 invocation_counter; // 8 octets
182
+ # unit32 block_counter; // 4 octets
183
+ #
184
+ # The block_counter is initially set to one (1) and increment as each block
185
+ # of key is produced.
186
+ #
187
+ # The reader is reminded that SSH requires that the data to be encrypted
188
+ # MUST be padded out to a multiple of the block size (16-octets for AES-GCM).
189
+ #
190
+ def incr_nonce
191
+ return if nonce[:fixed].nil?
192
+
193
+ nonce[:invocation_counter] = [nonce[:invocation_counter].to_s.unpack1('B*').to_i(2) + 1].pack('Q>*')
194
+
195
+ apply_nonce
196
+ end
197
+
198
+ def nonce=(iv_s)
199
+ return if nonce[:fixed]
200
+
201
+ nonce[:fixed] = iv_s[0...4]
202
+ nonce[:invocation_counter] = iv_s[4...12]
203
+
204
+ apply_nonce
205
+ end
206
+
207
+ def apply_nonce
208
+ cipher.iv = "#{nonce[:fixed]}#{nonce[:invocation_counter]}"
209
+ end
210
+
211
+ #
212
+ # --- RFC 5647 ---
213
+ # If AES-GCM is selected as the encryption algorithm for a given
214
+ # tunnel, AES-GCM MUST also be selected as the Message Authentication
215
+ # Code (MAC) algorithm. Conversely, if AES-GCM is selected as the MAC
216
+ # algorithm, it MUST also be selected as the encryption algorithm.
217
+ #
218
+ def implicit_mac?
219
+ true
220
+ end
221
+ end
222
+ end
223
+ # rubocop:enable Metrics/BlockLength
224
+ end
225
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
226
+ end
227
+ end
228
+ end
@@ -8,6 +8,18 @@ module Net
8
8
  # The base class of all OpenSSL-based HMAC algorithm wrappers.
9
9
  class Abstract
10
10
  class << self
11
+ def aead(*v)
12
+ @aead = false if !defined?(@aead)
13
+ if v.empty?
14
+ @aead = superclass.aead if @aead.nil? && superclass.respond_to?(:aead)
15
+ return @aead
16
+ elsif v.length == 1
17
+ @aead = v.first
18
+ else
19
+ raise ArgumentError, "wrong number of arguments (#{v.length} for 1)"
20
+ end
21
+ end
22
+
11
23
  def etm(*v)
12
24
  @etm = false if !defined?(@etm)
13
25
  if v.empty?
@@ -57,6 +69,10 @@ module Net
57
69
  end
58
70
  end
59
71
 
72
+ def aead
73
+ self.class.aead
74
+ end
75
+
60
76
  def etm
61
77
  self.class.etm
62
78
  end
@@ -0,0 +1,12 @@
1
+ require 'net/ssh/transport/hmac/abstract'
2
+
3
+ module Net::SSH::Transport::HMAC
4
+ # The SHA1 Encrypt-Then-Mac HMAC algorithm. This has a mac and
5
+ # key length of 20, and uses the SHA1 digest algorithm.
6
+ class SHA1_Etm < Abstract
7
+ etm true
8
+ mac_length 20
9
+ key_length 20
10
+ digest_class OpenSSL::Digest::SHA1
11
+ end
12
+ end
@@ -9,6 +9,7 @@ require 'net/ssh/transport/hmac/sha2_512'
9
9
  require 'net/ssh/transport/hmac/sha2_512_96'
10
10
  require 'net/ssh/transport/hmac/sha2_256_etm'
11
11
  require 'net/ssh/transport/hmac/sha2_512_etm'
12
+ require 'net/ssh/transport/hmac/sha1_etm'
12
13
  require 'net/ssh/transport/hmac/ripemd160'
13
14
  require 'net/ssh/transport/hmac/none'
14
15
 
@@ -27,6 +28,7 @@ module Net::SSH::Transport::HMAC
27
28
  'hmac-sha2-512-96' => SHA2_512_96,
28
29
  'hmac-sha2-256-etm@openssh.com' => SHA2_256_Etm,
29
30
  'hmac-sha2-512-etm@openssh.com' => SHA2_512_Etm,
31
+ 'hmac-sha1-etm@openssh.com' => SHA1_Etm,
30
32
  'hmac-ripemd160' => RIPEMD160,
31
33
  'hmac-ripemd160@openssh.com' => RIPEMD160,
32
34
  'none' => None
@@ -1,7 +1,4 @@
1
- gem 'x25519' # raise if the gem x25519 is not installed
2
-
3
- require 'x25519'
4
-
1
+ require 'openssl'
5
2
  require 'net/ssh/transport/constants'
6
3
  require 'net/ssh/transport/kex/abstract5656'
7
4
 
@@ -12,25 +9,60 @@ module Net
12
9
  # A key-exchange service implementing the "curve25519-sha256@libssh.org"
13
10
  # key-exchange algorithm. (defined in https://tools.ietf.org/html/draft-ietf-curdle-ssh-curves-06)
14
11
  class Curve25519Sha256 < Abstract5656
12
+ ALGORITHM = "X25519"
13
+ KEY_BYTES = 32
14
+
15
+ class UnsupportedError < StandardError; end
16
+
17
+ def self.ensure_supported!
18
+ unless OpenSSL::PKey.respond_to?(:generate_key) && OpenSSL::PKey.respond_to?(:new_raw_public_key)
19
+ raise UnsupportedError, "OpenSSL::PKey raw public key APIs are unavailable"
20
+ end
21
+
22
+ key = generate_x25519_key
23
+ public_key = new_raw_public_key(key.raw_public_key)
24
+ validate_key_bytes!(key.derive(public_key), "shared secret", KEY_BYTES)
25
+ rescue OpenSSL::PKey::PKeyError => e
26
+ raise UnsupportedError, e.message
27
+ end
28
+
29
+ def self.generate_x25519_key
30
+ OpenSSL::PKey.generate_key(ALGORITHM)
31
+ end
32
+
33
+ def self.new_raw_public_key(key)
34
+ validate_key_bytes!(key, "public key", KEY_BYTES)
35
+ OpenSSL::PKey.new_raw_public_key(ALGORITHM, binary_string(key))
36
+ end
37
+
38
+ def self.validate_key_bytes!(key, label, expected_bytes)
39
+ raise ArgumentError, "invalid X25519 #{label}" unless key.respond_to?(:bytesize) && key.bytesize == expected_bytes
40
+ end
41
+
42
+ def self.binary_string(string)
43
+ string.dup.force_encoding('BINARY')
44
+ end
45
+ private_class_method :binary_string
46
+
15
47
  def digester
16
48
  OpenSSL::Digest::SHA256
17
49
  end
18
50
 
19
51
  private
20
52
 
21
- def generate_key # :nodoc:
22
- ::X25519::Scalar.generate
53
+ def generate_key
54
+ self.class.generate_x25519_key
23
55
  end
24
56
 
25
57
  ## string Q_C, client's ephemeral public key octet string
26
58
  def ecdh_public_key_bytes
27
- ecdh.public_key.to_bytes
59
+ ecdh.raw_public_key
28
60
  end
29
61
 
30
62
  # compute shared secret from server's public key and client's private key
31
63
  def compute_shared_secret(server_ecdh_pubkey)
32
- pk = ::X25519::MontgomeryU.new(server_ecdh_pubkey)
33
- OpenSSL::BN.new(ecdh.diffie_hellman(pk).to_bytes, 2)
64
+ pk = self.class.new_raw_public_key(server_ecdh_pubkey)
65
+ OpenSSL::BN.new(ecdh.derive(pk), 2)
34
66
  end
35
67
  end
36
68
  end
@@ -2,26 +2,34 @@ module Net
2
2
  module SSH
3
3
  module Transport
4
4
  module Kex
5
- # Loads Curve25519Sha256 support which requires optinal dependencies
5
+ # Loads Curve25519Sha256 support backed by Ruby OpenSSL X25519 APIs.
6
6
  module Curve25519Sha256Loader
7
7
  begin
8
8
  require 'net/ssh/transport/kex/curve25519_sha256'
9
+ Net::SSH::Transport::Kex::Curve25519Sha256.ensure_supported!
9
10
  LOADED = true
10
11
  ERROR = nil
11
12
  rescue LoadError => e
12
13
  ERROR = e
13
14
  LOADED = false
15
+ rescue StandardError => e
16
+ if defined?(Net::SSH::Transport::Kex::Curve25519Sha256::UnsupportedError) &&
17
+ e.is_a?(Net::SSH::Transport::Kex::Curve25519Sha256::UnsupportedError)
18
+ ERROR = e
19
+ LOADED = false
20
+ else
21
+ raise
22
+ end
14
23
  end
15
24
 
16
25
  def self.raiseUnlessLoaded(message)
17
- description = ERROR.is_a?(LoadError) ? dependenciesRequiredForX25519 : ''
18
- description << "#{ERROR.class} : \"#{ERROR.message}\"\n" if ERROR
26
+ description = LOADED ? '' : x25519SupportRequired
27
+ description += "#{ERROR.class} : \"#{ERROR.message}\"\n" if ERROR
19
28
  raise NotImplementedError, "#{message}\n#{description}" unless LOADED
20
29
  end
21
30
 
22
- def self.dependenciesRequiredForX25519
23
- result = "net-ssh requires the following gems for x25519 support:\n"
24
- result << " * x25519\n"
31
+ def self.x25519SupportRequired
32
+ "net-ssh requires openssl >= 3.2.0 and a crypto backend with X25519 enabled for curve25519-sha256 support.\n"
25
33
  end
26
34
  end
27
35
  end
@@ -102,6 +102,7 @@ module Net
102
102
  result[:key_blob] = buffer.read_string
103
103
  result[:server_key] = Net::SSH::Buffer.new(result[:key_blob]).read_key
104
104
  result[:server_dh_pubkey] = buffer.read_bignum
105
+ validate_server_dh_pubkey(result[:server_dh_pubkey])
105
106
  result[:shared_secret] = OpenSSL::BN.new(dh.compute_key(result[:server_dh_pubkey]), 2)
106
107
 
107
108
  sig_buffer = Net::SSH::Buffer.new(buffer.read_string)
@@ -115,6 +116,13 @@ module Net
115
116
 
116
117
  return result
117
118
  end
119
+
120
+ # Rejects degenerate values that would let the server force a
121
+ # predictable shared secret, as OpenSSH's dh_pub_is_valid.
122
+ def validate_server_dh_pubkey(pub)
123
+ valid = pub > 1 && pub < dh.p - 1 && pub.to_i.to_s(2).count("1") >= 4
124
+ raise Net::SSH::Exception, "server's DH public value is invalid" unless valid
125
+ end
118
126
  end
119
127
  end
120
128
  end
@@ -6,26 +6,43 @@ module Net::SSH::Transport::Kex
6
6
  # A key-exchange service implementing the
7
7
  # "diffie-hellman-group-exchange-sha1" key-exchange algorithm.
8
8
  class DiffieHellmanGroupExchangeSHA1 < DiffieHellmanGroup1SHA1
9
- MINIMUM_BITS = 1024
9
+ MINIMUM_BITS = 2048
10
10
  MAXIMUM_BITS = 8192
11
+ CISCO_MAXIMUM_NEED_BITS = 4096
11
12
 
12
13
  private
13
14
 
14
- # Compute the number of bits needed for the given number of bytes.
15
+ # Computes the minimum, preferred and maximum group sizes to request,
16
+ # like OpenSSH's kexgex_client.
15
17
  def compute_need_bits
16
- # for Compatibility: OpenSSH requires (need_bits * 2 + 1) length of parameter
17
- need_bits = data[:need_bytes] * 8 * 2 + 1
18
-
19
18
  data[:minimum_dh_bits] ||= MINIMUM_BITS
19
+ data[:maximum_dh_bits] = [MAXIMUM_BITS, data[:minimum_dh_bits]].max
20
+ data[:need_bits] = estimate_group_bits(data[:need_bytes] * 8).clamp(data[:minimum_dh_bits], maximum_need_bits)
21
+ end
20
22
 
21
- if need_bits < data[:minimum_dh_bits]
22
- need_bits = data[:minimum_dh_bits]
23
- elsif need_bits > MAXIMUM_BITS
24
- need_bits = MAXIMUM_BITS
25
- end
23
+ # Some Cisco servers fail instead of returning their 4096-bit group when asked for more (OpenSSH bz#2209).
24
+ def maximum_need_bits
25
+ return data[:maximum_dh_bits] unless cisco_server?
26
+
27
+ [CISCO_MAXIMUM_NEED_BITS, data[:minimum_dh_bits]].max
28
+ end
29
+
30
+ def cisco_server?
31
+ data[:server_version_string].to_s.match?(/\ASSH-[\d.]+-Cisco-1\./)
32
+ end
26
33
 
27
- data[:need_bits] = need_bits
28
- data[:need_bytes] = need_bits / 8
34
+ # Group size giving at least the given bits of security (NIST SP 800-57),
35
+ # as OpenSSH's dh_estimate.
36
+ def estimate_group_bits(security_bits)
37
+ if security_bits <= 112
38
+ 2048
39
+ elsif security_bits <= 128
40
+ 3072
41
+ elsif security_bits <= 192
42
+ 7680
43
+ else
44
+ 8192
45
+ end
29
46
  end
30
47
 
31
48
  # Returns the DH key parameters for the given session.
@@ -34,7 +51,7 @@ module Net::SSH::Transport::Kex
34
51
 
35
52
  # request the DH key parameters for the given number of bits.
36
53
  buffer = Net::SSH::Buffer.from(:byte, KEXDH_GEX_REQUEST, :long, data[:minimum_dh_bits],
37
- :long, data[:need_bits], :long, MAXIMUM_BITS)
54
+ :long, data[:need_bits], :long, data[:maximum_dh_bits])
38
55
  connection.send_message(buffer)
39
56
 
40
57
  buffer = connection.next_message
@@ -43,9 +60,22 @@ module Net::SSH::Transport::Kex
43
60
  p = buffer.read_bignum
44
61
  g = buffer.read_bignum
45
62
 
63
+ validate_group(p, g)
64
+
46
65
  [p, g]
47
66
  end
48
67
 
68
+ def validate_group(prime, generator)
69
+ bits = prime.num_bits
70
+ if bits < data[:minimum_dh_bits]
71
+ raise Net::SSH::Exception,
72
+ "server's DH group is #{bits} bits, below the minimum of #{data[:minimum_dh_bits]}; " \
73
+ "pass minimum_dh_bits: #{bits} to allow it"
74
+ end
75
+ raise Net::SSH::Exception, "server's DH group is #{bits} bits, above the maximum of #{data[:maximum_dh_bits]}" if bits > data[:maximum_dh_bits]
76
+ raise Net::SSH::Exception, "server's DH group generator is invalid" unless generator > 1 && generator < prime - 1
77
+ end
78
+
49
79
  # Returns the INIT/REPLY constants used by this algorithm.
50
80
  def get_message_types
51
81
  [KEXDH_GEX_INIT, KEXDH_GEX_REPLY]
@@ -60,9 +90,9 @@ module Net::SSH::Transport::Kex
60
90
  data[:client_algorithm_packet],
61
91
  data[:server_algorithm_packet],
62
92
  result[:key_blob]
63
- response.write_long MINIMUM_BITS,
93
+ response.write_long data[:minimum_dh_bits],
64
94
  data[:need_bits],
65
- MAXIMUM_BITS
95
+ data[:maximum_dh_bits]
66
96
  response.write_bignum dh.p, dh.g, dh.pub_key,
67
97
  result[:server_dh_pubkey],
68
98
  result[:shared_secret]
@@ -1,4 +1,5 @@
1
1
  require 'openssl'
2
+ require 'net/ssh/errors'
2
3
  require 'net/ssh/authentication/pub_key_fingerprint'
3
4
 
4
5
  module OpenSSL
@@ -178,7 +179,7 @@ module OpenSSL
178
179
 
179
180
  return key
180
181
  rescue OpenSSL::PKey::ECError
181
- raise NotImplementedError, "unsupported key type `#{type}'"
182
+ raise Net::SSH::UnsupportedKeyType, "unsupported key type `ecdsa-sha2-#{curve_name_in_type}'"
182
183
  end
183
184
  end
184
185
 
@@ -15,6 +15,9 @@ module Net
15
15
  module PacketStream # rubocop:disable Metrics/ModuleLength
16
16
  PROXY_COMMAND_HOST_IP = '<no hostip for proxy command>'.freeze
17
17
 
18
+ # Same limit as OpenSSH; a larger length would make us buffer whatever the server streams (GHSA-pfqh-p9vh-wx4p).
19
+ PACKET_MAX_SIZE = 256 * 1024
20
+
18
21
  include BufferedIo
19
22
 
20
23
  def self.extended(object)
@@ -128,7 +131,7 @@ module Net
128
131
  payload = client.compress(payload)
129
132
 
130
133
  # the length of the packet, minus the padding
131
- actual_length = (client.hmac.etm ? 0 : 4) + payload.bytesize + 1
134
+ actual_length = (client.hmac.etm || client.hmac.aead ? 0 : 4) + payload.bytesize + 1
132
135
 
133
136
  # compute the padding length
134
137
  padding_length = client.block_size - (actual_length % client.block_size)
@@ -151,7 +154,7 @@ module Net
151
154
  debug { "using encrypt-then-mac" }
152
155
 
153
156
  # Encrypt padding_length, payload, and padding. Take MAC
154
- # from the unencrypted packet_lenght and the encrypted
157
+ # from the unencrypted packet_length and the encrypted
155
158
  # data.
156
159
  length_data = [packet_length].pack("N")
157
160
 
@@ -187,6 +190,7 @@ module Net
187
190
  def cleanup
188
191
  client.cleanup
189
192
  server.cleanup
193
+ hang_up_proxy_command
190
194
  end
191
195
 
192
196
  # If the IO object requires a rekey operation (as indicated by either its
@@ -202,6 +206,17 @@ module Net
202
206
 
203
207
  protected
204
208
 
209
+ # Like OpenSSH, hang up a ProxyCommand child: tunnels such as EC2 Instance
210
+ # Connect don't exit on their own, and IO#close waits for them.
211
+ def hang_up_proxy_command
212
+ return unless respond_to?(:pid)
213
+
214
+ child = pid
215
+ Process.kill('HUP', child) if child
216
+ rescue IOError, Errno::ESRCH
217
+ # already closed, or the child has already been reaped
218
+ end
219
+
205
220
  # Called when this module is used to extend an object. It initializes
206
221
  # the states and generally prepares the object for use as a packet stream.
207
222
  def initialize_ssh
@@ -219,7 +234,7 @@ module Net
219
234
  # new Packet object.
220
235
  # rubocop:disable Metrics/AbcSize
221
236
  def poll_next_packet
222
- aad_length = server.hmac.etm ? 4 : 0
237
+ aad_length = server.hmac.etm || server.hmac.aead ? 4 : 0
223
238
 
224
239
  if @packet.nil?
225
240
  minimum = server.block_size < 4 ? 4 : server.block_size
@@ -240,6 +255,7 @@ module Net
240
255
  @packet = Net::SSH::Buffer.new(server.update_cipher(data))
241
256
  @packet_length = @packet.read_long
242
257
  end
258
+ raise Net::SSH::Exception, "bad packet length #{@packet_length}" if @packet_length < 5 || @packet_length > PACKET_MAX_SIZE
243
259
  end
244
260
 
245
261
  need = @packet_length + 4 - aad_length - server.block_size
@@ -18,6 +18,10 @@ module Net
18
18
  # The SSH version string as reported by Net::SSH
19
19
  PROTO_VERSION = "SSH-2.0-Ruby/Net::SSH_#{Net::SSH::Version::CURRENT} #{RUBY_PLATFORM}"
20
20
 
21
+ # Same limits as OpenSSH, so a server can't make us buffer its banner without bound (GHSA-r75w-7fhp-84w8).
22
+ MAX_LINE_LENGTH = 8192
23
+ MAX_HEADER_LINES = 1024
24
+
21
25
  # Any header text sent by the server prior to sending the version.
22
26
  attr_reader :header
23
27
 
@@ -45,24 +49,15 @@ module Net
45
49
  socket.write "#{PROTO_VERSION}\r\n"
46
50
  socket.flush
47
51
 
48
- raise Net::SSH::ConnectionTimeout, "timeout during server version negotiating" if timeout && !IO.select([socket], nil, nil, timeout)
52
+ deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
49
53
 
50
- loop do
51
- @version = String.new
52
- loop do
53
- begin
54
- b = socket.readpartial(1)
55
- raise Net::SSH::Disconnect, "connection closed by remote host" if b.nil?
56
- rescue EOFError
57
- raise Net::SSH::Disconnect, "connection closed by remote host"
58
- end
59
- @version << b
60
- break if b == "\n"
61
- end
54
+ MAX_HEADER_LINES.times do
55
+ @version = read_line(socket, deadline)
62
56
  break if @version.match(/^SSH-/)
63
57
 
64
58
  @header << @version
65
59
  end
60
+ raise Net::SSH::Exception, "no SSH version received in the first #{MAX_HEADER_LINES} lines" unless @version.match(/^SSH-/)
66
61
 
67
62
  @version.chomp!
68
63
  debug { "remote is `#{@version}'" }
@@ -71,6 +66,31 @@ module Net
71
66
 
72
67
  raise Net::SSH::ConnectionTimeout, "timeout during client version negotiating" if timeout && !IO.select(nil, [socket], nil, timeout)
73
68
  end
69
+
70
+ def read_line(socket, deadline)
71
+ line = String.new
72
+ loop do
73
+ wait_readable(socket, deadline)
74
+ begin
75
+ b = socket.readpartial(1)
76
+ raise Net::SSH::Disconnect, "connection closed by remote host" if b.nil?
77
+ rescue EOFError
78
+ raise Net::SSH::Disconnect, "connection closed by remote host"
79
+ end
80
+ line << b
81
+ return line if b == "\n"
82
+ raise Net::SSH::Exception, "server version line exceeds #{MAX_LINE_LENGTH} bytes" if line.bytesize > MAX_LINE_LENGTH
83
+ end
84
+ end
85
+
86
+ def wait_readable(socket, deadline)
87
+ return unless deadline
88
+
89
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
90
+ return if remaining > 0 && IO.select([socket], nil, nil, remaining)
91
+
92
+ raise Net::SSH::ConnectionTimeout, "timeout during server version negotiating"
93
+ end
74
94
  end
75
95
  end
76
96
  end