net-ssh 7.3.0 → 7.3.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e941174d3093fe012c56708a9c23e0b86715e7aa0508b59c60e61279ff136fcd
4
- data.tar.gz: '02289d4900c6bd52679c65d70715e1038163ba54c61bd9d8257e837305204c1a'
3
+ metadata.gz: 7d7e10bf0a8b8fd5369085ec9d379b11f88e980fa57586fa6af75f82c04f44e9
4
+ data.tar.gz: a1e5198b60b4209e00a7599f800aa993e588ddf0c501a3ebbfa8c75888909524
5
5
  SHA512:
6
- metadata.gz: 5f9db58c86854a20d4d483129501795292ac9fcb9fe1405250536a6a66577e127ca8fbd287da888642acb2929240c7327663100abe70c0808c319b67a11ccc2a
7
- data.tar.gz: 189d3ba6b2e53b0910941b7b866d0d8f18c94024f061876b20c59f35ae48bd1edbd57d313bbb35efa1f7a99878a1751ee71a49cc602bb83f0204a82feb9afac1
6
+ metadata.gz: ff0336b092a4597e1676a82184efed7fdb1b0f1719d7615097ae430b502471e82e542db0fa0d75db0126bd7fe9c164981e4101dbb67bd19c5578c2d140b39c72
7
+ data.tar.gz: caad46ec0db79f1d9d273b1b2026d9811e53ed9bdcfac50a315749d48c73d5cfb9421f15e7f569b1d045cea6a67112f950ffb21ce16ed01d4154c52396cc35c9
checksums.yaml.gz.sig CHANGED
Binary file
data/.dockerignore CHANGED
@@ -2,5 +2,8 @@
2
2
  .git/
3
3
  .gitignore
4
4
 
5
+ # Vagrant
6
+ test/integration/.vagrant
7
+
5
8
  docker-compose.yml
6
9
  README.md
@@ -12,22 +12,22 @@ jobs:
12
12
 
13
13
  steps:
14
14
  - name: Checkout code
15
- uses: actions/checkout@v3
15
+ uses: actions/checkout@v4
16
16
 
17
17
  - name: Build docker images
18
- run: docker-compose build
18
+ run: docker compose build
19
19
 
20
20
  - name: Run ruby 2.6
21
- run: docker-compose run ruby-2.6
21
+ run: docker compose run ruby-2.6
22
22
 
23
23
  - name: Run ruby 2.7
24
- run: docker-compose run ruby-2.7
24
+ run: docker compose run ruby-2.7
25
25
 
26
26
  - name: Run ruby 3.0
27
- run: docker-compose run ruby-3.0
27
+ run: docker compose run ruby-3.0
28
28
 
29
29
  - name: Run ruby 3.1
30
- run: docker-compose run ruby-3.1
30
+ run: docker compose run ruby-3.1
31
31
 
32
32
  test_openssl3:
33
33
  name: Run test suite with docker and openssl 3.0
@@ -35,10 +35,24 @@ jobs:
35
35
 
36
36
  steps:
37
37
  - name: Checkout code
38
- uses: actions/checkout@v3
38
+ uses: actions/checkout@v4
39
39
 
40
40
  - name: Build docker images
41
41
  run: docker build -t netssh_openssl3 -f Dockerfile.openssl3 .
42
42
 
43
43
  - name: Run ruby 3.0 with openssl3
44
44
  run: docker run --rm netssh_openssl3
45
+
46
+ test_zlib_ng:
47
+ name: Run test suite with docker and zlib-ng
48
+ runs-on: ubuntu-latest
49
+
50
+ steps:
51
+ - name: Checkout code
52
+ uses: actions/checkout@v4
53
+
54
+ - name: Build docker images
55
+ run: docker build -t netssh_zlib_ng -f Dockerfile.zlib_ng .
56
+
57
+ - name: Run ruby with zlib-ng
58
+ run: docker run --rm netssh_zlib_ng
@@ -7,7 +7,7 @@ jobs:
7
7
  runs-on: ubuntu-22.04
8
8
  strategy:
9
9
  matrix:
10
- ruby-version: [2.6.10, 2.7.7, 3.0.6, 3.1.3, 3.2.1, 3.3.0]
10
+ ruby-version: [2.6.10, 2.7.7, 3.0.6, 3.1.3, 3.2.1, 3.3.0, 3.4]
11
11
  steps:
12
12
  - uses: actions/checkout@v3
13
13
 
@@ -21,16 +21,16 @@ jobs:
21
21
  python-version: 3.8
22
22
 
23
23
  - name: Cache bundler
24
- uses: actions/cache@v1
24
+ uses: actions/cache@v4
25
25
  id: bundler-cache
26
26
  with:
27
27
  path: vendor/bundle
28
28
  key: ${{ runner.os }}-${{ matrix.ruby-version }}-gem-v3-${{ hashFiles('**/Gemfile') }}-${{ hashFiles('**/net-ssh.gemspec') }}
29
29
  restore-keys: |
30
30
  ${{ runner.os }}-${{ matrix.ruby-version }}-gem-v3-
31
-
31
+
32
32
  - name: Cache pip
33
- uses: actions/cache@v1
33
+ uses: actions/cache@v4
34
34
  id: pip-cache
35
35
  with:
36
36
  path: ~/.cache/pip
@@ -42,12 +42,12 @@ jobs:
42
42
  gem install bundler ${{ (startsWith(matrix.ruby-version, '2.6.') || startsWith(matrix.ruby-version, '2.7.')) && '-v 2.4.22' || '' }}
43
43
  bundle config set path 'vendor/bundle'
44
44
  bundle config set --local path 'vendor/bundle'
45
- bundle install --jobs 4 --retry 3 --path vendor/bundle
46
- BUNDLE_GEMFILE=./Gemfile.noed25519 bundle install --jobs 4 --retry 3 --path vendor/bundle
45
+ bundle install --jobs 4 --retry 3
46
+ BUNDLE_GEMFILE=./Gemfile.noed25519 bundle install --jobs 4 --retry 3
47
47
  env:
48
48
  BUNDLE_PATH: vendor/bundle
49
49
 
50
-
50
+
51
51
  - name: Add to etc/hosts
52
52
  run: |
53
53
  sudo echo "127.0.0.1 gateway.netssh" | sudo tee -a /etc/hosts
@@ -78,17 +78,22 @@ jobs:
78
78
  env:
79
79
  NET_SSH_RUN_INTEGRATION_TESTS: 1
80
80
  CI: 1
81
+ RUBYOPT: '--enable=frozen-string-literal'
81
82
  - name: Run tests (without rbnacl)
82
83
  run: bundle exec rake test
83
84
  env:
84
85
  BUNDLE_GEMFILE: ./Gemfile.norbnacl
85
86
  NET_SSH_RUN_INTEGRATION_TESTS: 1
86
87
  CI: 1
88
+ RUBYOPT: '--enable=frozen-string-literal'
87
89
  - name: Run Tests (without ed25519)
88
90
  run: bundle exec rake test
89
91
  env:
90
92
  BUNDLE_GEMFILE: ./Gemfile.noed25519
91
93
  NET_SSH_RUN_INTEGRATION_TESTS: 1
92
94
  CI: 1
95
+ RUBYOPT: '--enable=frozen-string-literal'
93
96
  - name: Run test helper test
94
97
  run: bundle exec rake test_test
98
+ env:
99
+ RUBYOPT: '--enable=frozen-string-literal'
data/.rubocop_todo.yml CHANGED
@@ -235,7 +235,7 @@ Lint/UselessTimes:
235
235
  # Offense count: 205
236
236
  # Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
237
237
  Metrics/AbcSize:
238
- Max: 75
238
+ Max: 76
239
239
 
240
240
  # Offense count: 16
241
241
  # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
@@ -758,10 +758,12 @@ Style/OpenStructUse:
758
758
  Exclude:
759
759
  - 'test/authentication/test_ed25519.rb'
760
760
  - 'test/common.rb'
761
+ - 'test/test_known_hosts.rb'
761
762
  - 'test/transport/kex/test_curve25519_sha256.rb'
762
763
  - 'test/transport/kex/test_diffie_hellman_group1_sha1.rb'
763
764
  - 'test/transport/kex/test_ecdh_sha2_nistp256.rb'
764
765
  - 'test/verifiers/test_always.rb'
766
+ - 'test/verifiers/test_cert_authority_checks.rb'
765
767
 
766
768
  # Offense count: 16
767
769
  # Configuration parameters: AllowedMethods.
data/CHANGES.txt CHANGED
@@ -1,3 +1,34 @@
1
+ === 7.3.5
2
+
3
+ * Security: refuse agent channels opened by the server unless agent forwarding was requested (`forward_agent`, ForwardAgent, or `Forward#agent`); any use of port forwarding let a malicious server use the local ssh-agent [GHSA-qpg5-fx6p-8cq3]
4
+ * Security: reject host and user names containing shell metacharacters, and non-numeric ports, before substituting them into ProxyCommand/ProxyJump commands, as OpenSSH does since 9.6 [GHSA-3x7f-q7wf-fp73]
5
+ * Security: reject OpenSSH private keys claiming more than 2**20 bcrypt rounds, and only run the KDF once a passphrase is given [GHSA-rpq3-v334-f5pm]
6
+ * Security: bound the server's identification banner like OpenSSH (8192-byte lines, 1024 lines), and apply `:timeout` to the whole version exchange instead of only its first byte [GHSA-r75w-7fhp-84w8]
7
+ * Security: reject packet lengths below 5 or above 256 KB, as OpenSSH does [GHSA-pfqh-p9vh-wx4p]
8
+ * Security: treat unexpected packets during the initial key exchange as a protocol error, and bound packets queued during a re-key [GHSA-ggxh-cfwq-5xqc]
9
+
10
+ === 7.3.4
11
+
12
+ * Verify @cert-authority host certificate constraints — principals (shell-glob, matching OpenSSH), validity window, host type, and critical options — instead of trusting any CA-signed certificate for any host [#1010]
13
+
14
+ === 7.3.3
15
+
16
+ * Support .pub file as IdentityFile to filter/prioritize agent keys (1Password SSH agent use case) [#942]
17
+ * Fix cert signing regression when using a .pub companion file without an agent [#1006]
18
+ * Fix unreadable .pub IdentityFile silently skipped instead of raising [#1006]
19
+ * Allow multiple UserKnownHostsFile entries (space-separated, matching ssh_config(5)) [#979]
20
+ * Stop mutating ENV['HOME'] at require time [#997]
21
+ * Fix ED25519 loader with frozen string literals [#995]
22
+
23
+ === 7.3.2 rc2
24
+
25
+ * Fix Ruby 2.6 compat: use public_to_der fallback for EC public keys [#993]
26
+ * Fix test framework hang: preserve StringIO position in idle!
27
+
28
+ === 7.3.2 rc1
29
+
30
+ * Support frozen strings [#958]
31
+
1
32
  === 7.3.0 rc0
2
33
 
3
34
  * aes(128|256)gcm [#946]
@@ -0,0 +1,29 @@
1
+ FROM fedora:latest
2
+
3
+ ENV INSTALL_PATH="/netssh"
4
+
5
+ # Fedora's OpenSSL 3.x blocks SHA-1 for RSA/DSA signing by default.
6
+ # - LEGACY crypto policy relaxes the system-wide restrictions.
7
+ # - OPENSSL_CONF points to our config which activates the legacy provider,
8
+ # sets SECLEVEL=0, and sets rh-allow-sha1-signatures=yes (Fedora/RHEL
9
+ # OpenSSL patch that gates SHA-1 signing independently of the policy level).
10
+ # OPENSSL_CONF must be set as a Docker ENV so it is in the environment before
11
+ # Ruby/OpenSSL initialises — setting it in the Rakefile is too late.
12
+ ENV OPENSSL_CONF="/netssh/test/openssl3.conf"
13
+
14
+ RUN dnf install -y openssl ruby ruby-devel git make gcc zlib-ng crypto-policies-scripts && \
15
+ update-crypto-policies --set LEGACY
16
+
17
+ WORKDIR $INSTALL_PATH
18
+
19
+ COPY Gemfile net-ssh.gemspec $INSTALL_PATH/
20
+
21
+ COPY lib/net/ssh/version.rb $INSTALL_PATH/lib/net/ssh/version.rb
22
+
23
+ RUN ls -l && gem install bundler && bundle install
24
+
25
+ COPY . $INSTALL_PATH/
26
+
27
+ CMD openssl version && ruby -ropenssl -e 'puts OpenSSL::OPENSSL_VERSION' && \
28
+ rpm -q zlib-ng && ruby -rzlib -e 'puts Zlib.zlib_version' && \
29
+ rake test
data/docker-compose.yml CHANGED
@@ -1,5 +1,3 @@
1
- version: '3'
2
-
3
1
  services:
4
2
  ruby-3.1:
5
3
  build:
@@ -27,6 +27,9 @@ module Net
27
27
  MEND = "-----END OPENSSH PRIVATE KEY-----"
28
28
  MAGIC = "openssh-key-v1"
29
29
 
30
+ # Same limit as OpenSSH; bcrypt_pbkdf with 2**32 rounds would run for months (GHSA-rpq3-v334-f5pm).
31
+ MAX_KDF_ROUNDS = 1 << 20
32
+
30
33
  class DecryptError < ArgumentError
31
34
  def initialize(message, encrypted_key: false)
32
35
  super(message)
@@ -38,6 +41,11 @@ module Net
38
41
  end
39
42
  end
40
43
 
44
+ def self.check_bcrypt_kdf!(rounds, password)
45
+ raise ArgumentError.new("Private key bcrypt rounds #{rounds} exceeds #{MAX_KDF_ROUNDS}") if rounds > MAX_KDF_ROUNDS
46
+ raise DecryptError.new("Passphrase required for encrypted private key", encrypted_key: true) if password.nil? || password.empty?
47
+ end
48
+
41
49
  def self.read(datafull, password)
42
50
  datafull = datafull.strip
43
51
  raise ArgumentError.new("Expected #{MBEGIN} at start of private key") unless datafull.start_with?(MBEGIN)
@@ -74,6 +82,8 @@ module Net
74
82
 
75
83
  raise "BCryptPbkdf is not implemented for jruby" if RUBY_PLATFORM == "java"
76
84
 
85
+ check_bcrypt_kdf!(rounds, password)
86
+
77
87
  key = BCryptPbkdf::key(password, salt, keylen + ivlen, rounds)
78
88
  raise DecryptError.new("BCyryptPbkdf failed", encrypted_key: true) unless key
79
89
  else
@@ -14,13 +14,13 @@ module Net
14
14
  end
15
15
 
16
16
  def self.raiseUnlessLoaded(message)
17
- description = ERROR.is_a?(LoadError) ? dependenciesRequiredForED25519 : ''
17
+ description = ERROR.is_a?(LoadError) ? dependenciesRequiredForED25519 : +''
18
18
  description << "#{ERROR.class} : \"#{ERROR.message}\"\n" if ERROR
19
19
  raise NotImplementedError, "#{message}\n#{description}" unless LOADED
20
20
  end
21
21
 
22
22
  def self.dependenciesRequiredForED25519
23
- result = "net-ssh requires the following gems for ed25519 support:\n"
23
+ result = +"net-ssh requires the following gems for ed25519 support:\n"
24
24
  result << " * ed25519 (>= 1.2, < 2.0)\n"
25
25
  result << " * bcrypt_pbkdf (>= 1.0, < 2.0)\n" unless RUBY_PLATFORM == "java"
26
26
  result << "See https://github.com/net-ssh/net-ssh/issues/565 for more information\n"
@@ -72,12 +72,16 @@ module Net
72
72
  def add(key_file)
73
73
  key_files.push(File.expand_path(key_file)).uniq!
74
74
  self
75
+ rescue ArgumentError
76
+ self
75
77
  end
76
78
 
77
79
  # Add the given keycert_file to the list of keycert files that will be used.
78
80
  def add_keycert(keycert_file)
79
81
  keycert_files.push(File.expand_path(keycert_file)).uniq!
80
82
  self
83
+ rescue ArgumentError
84
+ self
81
85
  end
82
86
 
83
87
  # Add the given keycert_data to the list of keycerts that will be used.
@@ -172,7 +176,11 @@ module Net
172
176
  def sign(identity, data, sig_alg = nil)
173
177
  info = known_identities[identity] or raise KeyManagerError, "the given identity is unknown to the key manager"
174
178
 
175
- if info[:key].nil? && info[:from] == :file
179
+ if info[:from] == :pubkey_file_only
180
+ raise KeyManagerError, "the given identity is a public key only and cannot be used for signing without an agent"
181
+ end
182
+
183
+ if info[:key].nil? && (info[:from] == :file || info[:from] == :pubkey_file)
176
184
  begin
177
185
  info[:key] = KeyFactory.load_private_key(info[:file], options[:passphrase], !options[:non_interactive], options[:password_prompt])
178
186
  rescue OpenSSL::OpenSSLError, Exception => e
@@ -248,10 +256,14 @@ module Net
248
256
  def prepare_identities_from_files
249
257
  key_files.map do |file|
250
258
  if readable_file?(file)
251
- identity = {}
259
+ identity = { privkey_file: file }
252
260
  cert_file = file + "-cert.pub"
253
261
  public_key_file = file + ".pub"
254
- if readable_file?(cert_file)
262
+ if file.end_with?(".pub")
263
+ identity[:load_from] = :pubkey_file_only
264
+ identity[:pubkey_file] = file
265
+ identity.delete(:privkey_file)
266
+ elsif readable_file?(cert_file)
255
267
  identity[:load_from] = :pubkey_file
256
268
  identity[:pubkey_file] = cert_file
257
269
  elsif readable_file?(public_key_file)
@@ -260,7 +272,7 @@ module Net
260
272
  else
261
273
  identity[:load_from] = :privkey_file
262
274
  end
263
- identity.merge(privkey_file: file)
275
+ identity
264
276
  end
265
277
  end.compact
266
278
  end
@@ -282,7 +294,10 @@ module Net
282
294
  case identity[:load_from]
283
295
  when :pubkey_file
284
296
  key = KeyFactory.load_public_key(identity[:pubkey_file])
285
- { public_key: key, from: :file, file: identity[:privkey_file] }
297
+ { public_key: key, from: :pubkey_file, file: identity[:privkey_file] }
298
+ when :pubkey_file_only
299
+ key = KeyFactory.load_public_key(identity[:pubkey_file])
300
+ { public_key: key, from: :pubkey_file_only, file: identity[:privkey_file] }
286
301
  when :privkey_file
287
302
  private_key = KeyFactory.load_private_key(
288
303
  identity[:privkey_file], options[:passphrase], ask_passphrase, options[:password_prompt]
@@ -313,7 +328,7 @@ module Net
313
328
 
314
329
  def process_identity_loading_error(identity, e)
315
330
  case identity[:load_from]
316
- when :pubkey_file
331
+ when :pubkey_file, :pubkey_file_only
317
332
  error { "could not load public key file `#{identity[:pubkey_file]}': #{e.class} (#{e.message})" }
318
333
  when :privkey_file
319
334
  error { "could not load private key file `#{identity[:privkey_file]}': #{e.class} (#{e.message})" }
@@ -229,7 +229,6 @@ module Net
229
229
  fingerprinthash: :fingerprint_hash,
230
230
  port: :port,
231
231
  user: :user,
232
- userknownhostsfile: :user_known_hosts_file,
233
232
  checkhostip: :check_host_ip
234
233
  }.freeze
235
234
  def translate_config_key(hash, key, value, settings)
@@ -293,6 +292,8 @@ module Net
293
292
  hash[:set_env] = Shellwords.split(value.to_s).map { |e| e.split '=', 2 }.to_h
294
293
  when :numberofpasswordprompts
295
294
  hash[:number_of_password_prompts] = value.to_i
295
+ when :userknownhostsfile
296
+ hash[:user_known_hosts_file] = value.split(/\s+/)
296
297
  when *TRANSLATE_CONFIG_KEY_RENAME_MAP.keys
297
298
  hash[TRANSLATE_CONFIG_KEY_RENAME_MAP[key]] = value
298
299
  end
@@ -55,7 +55,7 @@ module Net
55
55
  prompter = nil
56
56
  result =
57
57
  begin
58
- key_type.read(data, passphrase || 'invalid')
58
+ key_type.read(data, passphrase)
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,8 +138,9 @@ 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.
141
142
  def self.read(key_data, passphrase)
142
- open_ssl_class.new(key_data, passphrase)
143
+ open_ssl_class.new(key_data, passphrase || 'invalid')
143
144
  end
144
145
 
145
146
  def self.encrypted_key?(key_data, error)
@@ -149,7 +150,7 @@ module Net
149
150
 
150
151
  class OpenSSLPKeyType < OpenSSLKeyTypeBase
151
152
  def self.read(key_data, passphrase)
152
- open_ssl_class.read(key_data, passphrase)
153
+ open_ssl_class.read(key_data, passphrase || 'invalid')
153
154
  end
154
155
 
155
156
  def self.open_ssl_class
@@ -61,6 +61,37 @@ module Net
61
61
  false
62
62
  end
63
63
  end
64
+
65
+ # Returns true if the certificate lists no principals (unrestricted) or
66
+ # if the given hostname matches a listed principal. Host-certificate
67
+ # principals are shell-glob patterns, matching OpenSSH, whose
68
+ # sshkey_cert_check_host passes wildcard_pattern=1 so principals like
69
+ # "*.example.com" are matched with match_pattern rather than strcmp.
70
+ def matches_principal?(server_key, hostname)
71
+ server_key.valid_principals.empty? ||
72
+ server_key.valid_principals.any? { |principal| File.fnmatch(principal, hostname) }
73
+ end
74
+
75
+ # Returns true if the certificate's validity window covers the current time.
76
+ def matches_validity?(server_key)
77
+ return false if server_key.valid_after && server_key.valid_after > Time.now
78
+ return false if server_key.valid_before && server_key.valid_before < Time.now
79
+
80
+ true
81
+ end
82
+
83
+ # Returns true if the certificate is a host certificate. A user
84
+ # certificate (type :user) must never authenticate a host.
85
+ def matches_type?(server_key)
86
+ server_key.type == :host
87
+ end
88
+
89
+ # Returns true if every critical option in the certificate is one we can
90
+ # enforce. net-ssh enforces none, so any critical option is unsupported
91
+ # and, as OpenSSH does, must cause the certificate to be rejected.
92
+ def critical_options_supported?(server_key)
93
+ server_key.critical_options.empty?
94
+ end
64
95
  end
65
96
  end
66
97
 
@@ -89,6 +120,17 @@ module Net
89
120
  def empty?
90
121
  @host_keys.empty?
91
122
  end
123
+
124
+ # Returns the bare hostname, stripping port and IP portions from the
125
+ # comma-separated host string used internally.
126
+ #
127
+ # Examples:
128
+ # "server.example.com" => "server.example.com"
129
+ # "server.example.com,1.2.3.4" => "server.example.com"
130
+ # "[server.example.com]:2222" => "server.example.com"
131
+ def hostname
132
+ @host.split(",").first.gsub(/\[|\]:\d+/, "")
133
+ end
92
134
  end
93
135
 
94
136
  # Searches an OpenSSH-style known-host file for a given host, and returns all
@@ -160,6 +202,8 @@ module Net
160
202
  # file. The path is expanded file File.expand_path.
161
203
  def initialize(source)
162
204
  @source = File.expand_path(source)
205
+ rescue ArgumentError
206
+ @source = source
163
207
  end
164
208
 
165
209
  # Returns an array of all keys that are known to be associatd with the
@@ -16,6 +16,11 @@ 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
+
19
24
  # The command line template
20
25
  attr_reader :command_line_template
21
26
 
@@ -41,13 +46,13 @@ module Net
41
46
  command_line = @command_line_template.gsub(/%(.)/) {
42
47
  case $1
43
48
  when 'h'
44
- host
49
+ validate_host_name!(host)
45
50
  when 'p'
46
- port.to_s
51
+ validate_port!(port)
47
52
  when 'r'
48
53
  remote_user = connection_options && connection_options[:remote_user]
49
54
  if remote_user
50
- remote_user
55
+ validate_user_name!(remote_user)
51
56
  else
52
57
  raise ArgumentError, "remote user name not available"
53
58
  end
@@ -117,6 +122,26 @@ module Net
117
122
  Process.kill('TERM', io.pid)
118
123
  Thread.new { io.close }
119
124
  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
120
145
  end
121
146
  end
122
147
  end
@@ -1,3 +1,4 @@
1
+ require 'shellwords'
1
2
  require 'uri'
2
3
  require 'net/ssh/proxy/command'
3
4
 
@@ -34,20 +35,29 @@ module Net
34
35
  # We cannot build the ProxyCommand template until we know if the :config
35
36
  # option was specified during `Net::SSH.start`.
36
37
  def build_proxy_command_equivalent(connection_options = nil)
37
- first_jump, extra_jumps = jump_proxies.split(",", 2)
38
+ extra_jumps = jump_proxies.split(",", 2)[1]
38
39
  config = connection_options && connection_options[:config]
39
- uri = URI.parse("ssh://#{first_jump}")
40
+ uri, = jump_proxies.split(",").map { |jump| validate_jump!(jump) }
40
41
 
41
42
  template = "ssh".dup
42
43
  template << " -l #{uri.user}" if uri.user
43
44
  template << " -p #{uri.port}" if uri.port
44
45
  template << " -J #{extra_jumps}" if extra_jumps
45
- template << " -F #{config}" if config != true && config
46
+ template << " -F #{Shellwords.escape(config)}" if config != true && config
46
47
  template << " -W %h:%p "
47
48
  template << uri.host
48
49
 
49
50
  @command_line_template = template
50
51
  end
52
+
53
+ private
54
+
55
+ def validate_jump!(jump)
56
+ uri = URI.parse("ssh://#{jump}")
57
+ validate_user_name!(uri.user) if uri.user
58
+ validate_host_name!(uri.host)
59
+ uri
60
+ end
51
61
  end
52
62
  end
53
63
  end
@@ -48,9 +48,9 @@ module Net
48
48
  # If three arguments are given, it is as if the local bind address is
49
49
  # "127.0.0.1", and the rest are applied as above.
50
50
  #
51
- # To request an ephemeral port on the remote server, provide 0 (zero) for
52
- # the port number. In all cases, this method will return the port that
53
- # has been assigned.
51
+ # To request an ephemeral port on the local server, provide 0 (zero) for
52
+ # the local port number. In all cases, this method will return the port
53
+ # that has been assigned.
54
54
  #
55
55
  # ssh.forward.local(1234, "www.capify.org", 80)
56
56
  # assigned_port = ssh.forward.local("0.0.0.0", 0, "www.capify.org", 80)
@@ -405,6 +405,12 @@ 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
+
408
414
  info { "opening auth-agent channel" }
409
415
  channel[:invisible] = true
410
416
 
@@ -57,8 +57,9 @@ module Net
57
57
  return false unless script.next(:first)
58
58
 
59
59
  if script.next(:first).remote?
60
- self.string << script.next.to_s
61
- self.pos = pos
60
+ current_pos = pos
61
+ self.string = "#{self.string}#{script.next}"
62
+ self.pos = current_pos
62
63
  end
63
64
 
64
65
  return true
@@ -22,7 +22,7 @@ module Net
22
22
  # connection.
23
23
  def initialize
24
24
  extend(Net::SSH::Transport::PacketStream)
25
- super "SSH-2.0-Test\r\n"
25
+ super(+"SSH-2.0-Test\r\n")
26
26
 
27
27
  @script = Script.new
28
28
 
@@ -13,7 +13,7 @@ module Net
13
13
 
14
14
  attr_reader :cipher
15
15
  attr_reader :key
16
- attr_accessor :nonce
16
+ attr_reader :nonce
17
17
 
18
18
  #
19
19
  # Semantically gcm cipher supplies the OpenSSL iv interface with a nonce
@@ -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)
@@ -240,6 +243,7 @@ module Net
240
243
  @packet = Net::SSH::Buffer.new(server.update_cipher(data))
241
244
  @packet_length = @packet.read_long
242
245
  end
246
+ raise Net::SSH::Exception, "bad packet length #{@packet_length}" if @packet_length < 5 || @packet_length > PACKET_MAX_SIZE
243
247
  end
244
248
 
245
249
  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
@@ -27,6 +27,10 @@ module Net
27
27
  # The standard port for the SSH protocol.
28
28
  DEFAULT_PORT = 22
29
29
 
30
+ # Packets held back during a re-key are bounded so a server can't exhaust memory (GHSA-ggxh-cfwq-5xqc).
31
+ MAX_QUEUED_PACKETS = 16_384
32
+ MAX_QUEUED_BYTES = 32 * 1024 * 1024
33
+
30
34
  # The host to connect to, as given to the constructor.
31
35
  attr_reader :host
32
36
 
@@ -79,6 +83,7 @@ module Net
79
83
  debug { "connection established" }
80
84
 
81
85
  @queue = []
86
+ @queued_bytes = 0
82
87
 
83
88
  @host_key_verifier = select_host_key_verifier(options[:verify_host_key])
84
89
 
@@ -102,8 +107,8 @@ module Net
102
107
  # SSH known-host files.
103
108
  def host_as_string
104
109
  @host_as_string ||= begin
105
- string = "#{host}"
106
- string = "[#{string}]:#{port}" if port != DEFAULT_PORT
110
+ string = +"#{host}"
111
+ string = +"[#{string}]:#{port}" if port != DEFAULT_PORT
107
112
 
108
113
  peer_ip = socket.peer_ip
109
114
 
@@ -188,7 +193,7 @@ module Net
188
193
  # read from the queue before the socket is queried.
189
194
  def poll_message(mode = :nonblock, consume_queue = true)
190
195
  loop do
191
- return @queue.shift if consume_queue && @queue.any? && algorithms.allow?(@queue.first)
196
+ return shift_queue if consume_queue && @queue.any? && algorithms.allow?(@queue.first)
192
197
 
193
198
  packet = socket.next_packet(mode, options[:timeout])
194
199
  return nil if packet.nil?
@@ -211,6 +216,7 @@ module Net
211
216
 
212
217
  else
213
218
  return packet if algorithms.allow?(packet)
219
+ raise Net::SSH::Exception, "unexpected packet type #{packet.type} during initial key exchange" unless algorithms.initialized?
214
220
 
215
221
  push(packet)
216
222
  end
@@ -234,6 +240,11 @@ module Net
234
240
  # #poll_message will return packets from the queue in the order they
235
241
  # were received.
236
242
  def push(packet)
243
+ @queued_bytes += packet.length
244
+ if @queue.size >= MAX_QUEUED_PACKETS || @queued_bytes > MAX_QUEUED_BYTES
245
+ raise Net::SSH::Exception, "too many packets queued during key exchange"
246
+ end
247
+
237
248
  @queue.push(packet)
238
249
  end
239
250
 
@@ -276,6 +287,12 @@ module Net
276
287
 
277
288
  private
278
289
 
290
+ def shift_queue
291
+ packet = @queue.shift
292
+ @queued_bytes -= packet.length
293
+ packet
294
+ end
295
+
279
296
  # Compatibility verifier which allows users to keep using
280
297
  # custom verifier code without adding new :verify_signature
281
298
  # method.
@@ -19,10 +19,8 @@ module Net
19
19
  # We've never seen this host before, so raise an exception.
20
20
  process_cache_miss(host_keys, arguments, HostKeyUnknown, "is unknown") if host_keys.empty?
21
21
 
22
- # If we found any matches, check to see that the key type and
23
- # blob also match.
24
-
25
- found = host_keys.any? do |key|
22
+ # Find the known-hosts entry whose key matches the presented certificate.
23
+ found_key = host_keys.find do |key|
26
24
  if key.respond_to?(:matches_key?)
27
25
  key.matches_key?(arguments[:key])
28
26
  else
@@ -30,11 +28,17 @@ module Net
30
28
  end
31
29
  end
32
30
 
33
- # If a match was found, return true. Otherwise, raise an exception
34
- # indicating that the key was not recognized.
35
- process_cache_miss(host_keys, arguments, HostKeyMismatch, "does not match") unless found
31
+ # No matching entry found — key is not recognized.
32
+ process_cache_miss(host_keys, arguments, HostKeyMismatch, "does not match") unless found_key
33
+
34
+ # For @cert-authority entries, enforce the certificate constraints
35
+ # OpenSSH checks in sshkey_cert_check_authority. A certificate signed by
36
+ # the trusted CA but failing any of these is rejected with
37
+ # HostKeyMismatch (not HostKeyUnknown) so AcceptNew does not swallow the
38
+ # failure and remember the rejected certificate.
39
+ verify_certificate!(found_key, host_keys, arguments)
36
40
 
37
- found
41
+ true
38
42
  end
39
43
 
40
44
  def verify_signature(&block)
@@ -43,6 +47,37 @@ module Net
43
47
 
44
48
  private
45
49
 
50
+ # Enforces the @cert-authority certificate constraints for entries that
51
+ # implement them (Net::SSH::HostKeyEntries::CertAuthority). Non-certificate
52
+ # entries do not respond to these predicates and are left untouched.
53
+ def verify_certificate!(entry, host_keys, arguments)
54
+ cert = arguments[:key]
55
+
56
+ if entry.respond_to?(:matches_validity?) && !entry.matches_validity?(cert)
57
+ reason = if cert.valid_before && cert.valid_before < Time.now
58
+ "Certificate has expired"
59
+ else
60
+ "Certificate is not yet valid"
61
+ end
62
+ process_cache_miss(host_keys, arguments, HostKeyMismatch, reason)
63
+ end
64
+
65
+ if entry.respond_to?(:matches_principal?) && !entry.matches_principal?(cert, host_keys.hostname)
66
+ process_cache_miss(host_keys, arguments, HostKeyMismatch,
67
+ "Certificate invalid: name is not a listed principal")
68
+ end
69
+
70
+ if entry.respond_to?(:matches_type?) && !entry.matches_type?(cert)
71
+ process_cache_miss(host_keys, arguments, HostKeyMismatch,
72
+ "Certificate invalid: not a host certificate")
73
+ end
74
+
75
+ return unless entry.respond_to?(:critical_options_supported?) && !entry.critical_options_supported?(cert)
76
+
77
+ process_cache_miss(host_keys, arguments, HostKeyMismatch,
78
+ "Certificate invalid: unsupported critical option")
79
+ end
80
+
46
81
  def process_cache_miss(host_keys, args, exc_class, message)
47
82
  exception = exc_class.new("fingerprint #{args[:fingerprint]} " +
48
83
  "#{message} for #{host_keys.host.inspect}")
@@ -52,7 +52,7 @@ module Net
52
52
  MINOR = 3
53
53
 
54
54
  # The tiny component of this version of the Net::SSH library
55
- TINY = 0
55
+ TINY = 5
56
56
 
57
57
  # The prerelease component of this version of the Net::SSH library
58
58
  # nil allowed
data/lib/net/ssh.rb CHANGED
@@ -1,7 +1,3 @@
1
- # Make sure HOME is set, regardless of OS, so that File.expand_path works
2
- # as expected with tilde characters.
3
- ENV['HOME'] ||= ENV['HOMEPATH'] ? "#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}" : Dir.pwd
4
-
5
1
  require 'logger'
6
2
  require 'etc'
7
3
  require 'shellwords'
@@ -108,7 +104,7 @@ module Net
108
104
  # * :bind_address => the IP address on the connecting machine to use in
109
105
  # establishing connection. (:bind_address is discarded if :proxy
110
106
  # is set.)
111
- # * :check_host_ip => Also ckeck IP address when connecting to remote host.
107
+ # * :check_host_ip => Also check IP address when connecting to remote host.
112
108
  # Defaults to +true+.
113
109
  # * :compression => the compression algorithm to use, or +true+ to use
114
110
  # whatever is supported.
@@ -1,7 +1,7 @@
1
1
  -----BEGIN CERTIFICATE-----
2
2
  MIIDeDCCAmCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBBMQ8wDQYDVQQDDAZuZXRz
3
3
  c2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZFgNj
4
- b20wHhcNMjQwNDAxMDk1NjIxWhcNMjUwNDAxMDk1NjIxWjBBMQ8wDQYDVQQDDAZu
4
+ b20wHhcNMjYwMjE1MTAxNzUyWhcNMjcwMjE1MTAxNzUyWjBBMQ8wDQYDVQQDDAZu
5
5
  ZXRzc2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZ
6
6
  FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGJ4TbZ9H+qZ08
7
7
  pQfJhPJTHaDCyQvCsKTFrL5O9z3tllQ7B/zksMMM+qFBpNYu9HCcg4yBATacE/PB
@@ -12,10 +12,10 @@ KP4yMn+TzaXijyjRg7gECfJr3TGASaA4bQsILFGG5dAWcwO4OMrZedR7SHj/o0Kf
12
12
  3gL7P0axAgMBAAGjezB5MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
13
13
  BBQF8qLA7Z4zg0SJGtUbv3eoQ8tjIzAfBgNVHREEGDAWgRRuZXRzc2hAc29sdXRp
14
14
  b3VzLmNvbTAfBgNVHRIEGDAWgRRuZXRzc2hAc29sdXRpb3VzLmNvbTANBgkqhkiG
15
- 9w0BAQsFAAOCAQEAfY2WbsBKwRtBep4l+Y2/84H1BKH9UVOsFxqQzYkvM2LFDyup
16
- UkjYf8nPSjg3mquhaiA5KSoSVUPpNDfQo+UvY3+mlxRs96ttWiUGwz27fy82rx1B
17
- ZnfKjsWOntemNON6asOD0mtv0xsNBfOB2VNIKW/uqHsiPpa0OaVy5uENhX+5OFan
18
- 2P1Uy+WcMiv38RlRkn4cdEIZUFupDgKFsguYlaJy473/wsae4exUgc5bvi3Splob
19
- 1uE/LmB/qWBVSNW8e9KDtJynhDDZBlpESyQHFQCZj6UapzxlnC46LaDncPoAtJPc
20
- MlWxJ8mKghIcyXc5y4cSyGypNG5BralqnvQUyg==
15
+ 9w0BAQsFAAOCAQEASsiVybehJLRPztFeFV8+SKa2BwbBSNJoAkhARVJKhpJxCfeY
16
+ gO7Kqb7JCykzb0anb1s6xTrzD03cWRNGE2YaoBLqnzeUqW1LPqVwmcySmnSIGH9F
17
+ DlFYlHaAn3oSqglyDxQx9bLngKatetx+tSfB3bLAb7Tr9PO8/lhBENbjlD4+UUwD
18
+ gApoZuD9pknWDH3KsITgzphswETBdo0hd9aLnRNR3EifD0lUIEit/0Yx6euSxWtI
19
+ ufHf4AX2UIkJbh7zCPkiNCqIr7MSWLNFG/9lOlHYsEJM8XujT1ofPobYx6YSFx/C
20
+ 7HBrI7UX7awt6UvBZebhcHzyMHxg/B5PVQllPA==
21
21
  -----END CERTIFICATE-----
data/net-ssh.gemspec CHANGED
@@ -42,6 +42,7 @@ Gem::Specification.new do |spec|
42
42
  spec.add_development_dependency "bundler", ">= 1.17"
43
43
  spec.add_development_dependency "minitest", "~> 5.19"
44
44
  spec.add_development_dependency "mocha", "~> 2.1.0"
45
+ spec.add_development_dependency "ostruct"
45
46
  spec.add_development_dependency "rake", "~> 12.0"
46
47
  spec.add_development_dependency "rubocop", "~> 1.28.0"
47
48
  end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,20 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-ssh
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.3.0
4
+ version: 7.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jamis Buck
8
8
  - Delano Mandelbaum
9
9
  - Miklós Fazekas
10
- autorequire:
10
+ autorequire:
11
11
  bindir: exe
12
12
  cert_chain:
13
13
  - |
14
14
  -----BEGIN CERTIFICATE-----
15
15
  MIIDeDCCAmCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBBMQ8wDQYDVQQDDAZuZXRz
16
16
  c2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZFgNj
17
- b20wHhcNMjQwNDAxMDk1NjIxWhcNMjUwNDAxMDk1NjIxWjBBMQ8wDQYDVQQDDAZu
17
+ b20wHhcNMjYwMjE1MTAxNzUyWhcNMjcwMjE1MTAxNzUyWjBBMQ8wDQYDVQQDDAZu
18
18
  ZXRzc2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZ
19
19
  FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGJ4TbZ9H+qZ08
20
20
  pQfJhPJTHaDCyQvCsKTFrL5O9z3tllQ7B/zksMMM+qFBpNYu9HCcg4yBATacE/PB
@@ -25,14 +25,14 @@ cert_chain:
25
25
  3gL7P0axAgMBAAGjezB5MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
26
26
  BBQF8qLA7Z4zg0SJGtUbv3eoQ8tjIzAfBgNVHREEGDAWgRRuZXRzc2hAc29sdXRp
27
27
  b3VzLmNvbTAfBgNVHRIEGDAWgRRuZXRzc2hAc29sdXRpb3VzLmNvbTANBgkqhkiG
28
- 9w0BAQsFAAOCAQEAfY2WbsBKwRtBep4l+Y2/84H1BKH9UVOsFxqQzYkvM2LFDyup
29
- UkjYf8nPSjg3mquhaiA5KSoSVUPpNDfQo+UvY3+mlxRs96ttWiUGwz27fy82rx1B
30
- ZnfKjsWOntemNON6asOD0mtv0xsNBfOB2VNIKW/uqHsiPpa0OaVy5uENhX+5OFan
31
- 2P1Uy+WcMiv38RlRkn4cdEIZUFupDgKFsguYlaJy473/wsae4exUgc5bvi3Splob
32
- 1uE/LmB/qWBVSNW8e9KDtJynhDDZBlpESyQHFQCZj6UapzxlnC46LaDncPoAtJPc
33
- MlWxJ8mKghIcyXc5y4cSyGypNG5BralqnvQUyg==
28
+ 9w0BAQsFAAOCAQEASsiVybehJLRPztFeFV8+SKa2BwbBSNJoAkhARVJKhpJxCfeY
29
+ gO7Kqb7JCykzb0anb1s6xTrzD03cWRNGE2YaoBLqnzeUqW1LPqVwmcySmnSIGH9F
30
+ DlFYlHaAn3oSqglyDxQx9bLngKatetx+tSfB3bLAb7Tr9PO8/lhBENbjlD4+UUwD
31
+ gApoZuD9pknWDH3KsITgzphswETBdo0hd9aLnRNR3EifD0lUIEit/0Yx6euSxWtI
32
+ ufHf4AX2UIkJbh7zCPkiNCqIr7MSWLNFG/9lOlHYsEJM8XujT1ofPobYx6YSFx/C
33
+ 7HBrI7UX7awt6UvBZebhcHzyMHxg/B5PVQllPA==
34
34
  -----END CERTIFICATE-----
35
- date: 2024-10-02 00:00:00.000000000 Z
35
+ date: 2026-10-05 00:00:00.000000000 Z
36
36
  dependencies:
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: bcrypt_pbkdf
@@ -146,6 +146,20 @@ dependencies:
146
146
  - - "~>"
147
147
  - !ruby/object:Gem::Version
148
148
  version: 2.1.0
149
+ - !ruby/object:Gem::Dependency
150
+ name: ostruct
151
+ requirement: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ type: :development
157
+ prerelease: false
158
+ version_requirements: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
149
163
  - !ruby/object:Gem::Dependency
150
164
  name: rake
151
165
  requirement: !ruby/object:Gem::Requirement
@@ -198,6 +212,7 @@ files:
198
212
  - DEVELOPMENT.md
199
213
  - Dockerfile
200
214
  - Dockerfile.openssl3
215
+ - Dockerfile.zlib_ng
201
216
  - Gemfile
202
217
  - Gemfile.noed25519
203
218
  - Gemfile.norbnacl
@@ -315,7 +330,7 @@ licenses:
315
330
  - MIT
316
331
  metadata:
317
332
  changelog_uri: https://github.com/net-ssh/net-ssh/blob/master/CHANGES.txt
318
- post_install_message:
333
+ post_install_message:
319
334
  rdoc_options: []
320
335
  require_paths:
321
336
  - lib
@@ -331,7 +346,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
331
346
  version: '0'
332
347
  requirements: []
333
348
  rubygems_version: 3.3.3
334
- signing_key:
349
+ signing_key:
335
350
  specification_version: 4
336
351
  summary: 'Net::SSH: a pure-Ruby implementation of the SSH2 client protocol.'
337
352
  test_files: []
metadata.gz.sig CHANGED
Binary file