net-ssh 7.3.0 → 7.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.dockerignore +3 -0
- data/.github/workflows/ci-with-docker.yml +21 -7
- data/.github/workflows/ci.yml +12 -7
- data/.rubocop_todo.yml +3 -1
- data/CHANGES.txt +43 -0
- data/Dockerfile +3 -0
- data/Dockerfile.zlib_ng +29 -0
- data/Rakefile +20 -12
- data/docker-compose.yml +0 -2
- data/lib/net/ssh/authentication/ed25519.rb +10 -0
- data/lib/net/ssh/authentication/ed25519_loader.rb +2 -2
- data/lib/net/ssh/authentication/key_manager.rb +21 -6
- data/lib/net/ssh/config.rb +2 -1
- data/lib/net/ssh/key_factory.rb +4 -3
- data/lib/net/ssh/known_hosts.rb +44 -0
- data/lib/net/ssh/proxy/command.rb +28 -3
- data/lib/net/ssh/proxy/jump.rb +38 -7
- data/lib/net/ssh/service/forward.rb +9 -3
- data/lib/net/ssh/test/extensions.rb +3 -2
- data/lib/net/ssh/test/socket.rb +1 -1
- data/lib/net/ssh/transport/gcm_cipher.rb +1 -1
- data/lib/net/ssh/transport/packet_stream.rb +4 -0
- data/lib/net/ssh/transport/server_version.rb +33 -13
- data/lib/net/ssh/transport/session.rb +21 -3
- data/lib/net/ssh/verifiers/always.rb +46 -8
- data/lib/net/ssh/version.rb +1 -1
- data/lib/net/ssh.rb +1 -5
- data/net-ssh-public_cert.pem +7 -7
- data/net-ssh.gemspec +3 -0
- data.tar.gz.sig +0 -0
- metadata +41 -12
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a32034e7f363bc87196ff8a13b8c79bd7e9b246e98375c8e8d505cd52ea0b19a
|
|
4
|
+
data.tar.gz: d98d516fa2ddc9a8e2718e60ea4367da99ae49b5082ec0b4897db24cc86a625f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5d09d26af82a639bb793453e27f95b89cb1dbcc15a2bd22ccad8bc7e7106d848008927dbee29b5a8f7c280a15a5bd9eced464c45f004f0c1b64ee2bb1bb8eee
|
|
7
|
+
data.tar.gz: 5b97d998e035e0c3a499ae89168f10b7e484cbf87ef551d16a11ddef1cf9d1f729b26a204d9cdabb7e1ea7b99de0e4322d9e71cba384c3d47ac0db85ccc2b8ac
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/.dockerignore
CHANGED
|
@@ -12,22 +12,22 @@ jobs:
|
|
|
12
12
|
|
|
13
13
|
steps:
|
|
14
14
|
- name: Checkout code
|
|
15
|
-
uses: actions/checkout@
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
16
|
|
|
17
17
|
- name: Build docker images
|
|
18
|
-
run: docker
|
|
18
|
+
run: docker compose build
|
|
19
19
|
|
|
20
20
|
- name: Run ruby 2.6
|
|
21
|
-
run: docker
|
|
21
|
+
run: docker compose run ruby-2.6
|
|
22
22
|
|
|
23
23
|
- name: Run ruby 2.7
|
|
24
|
-
run: docker
|
|
24
|
+
run: docker compose run ruby-2.7
|
|
25
25
|
|
|
26
26
|
- name: Run ruby 3.0
|
|
27
|
-
run: docker
|
|
27
|
+
run: docker compose run ruby-3.0
|
|
28
28
|
|
|
29
29
|
- name: Run ruby 3.1
|
|
30
|
-
run: docker
|
|
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@
|
|
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
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -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, 3.4, '4.0']
|
|
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@
|
|
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@
|
|
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
|
|
46
|
-
BUNDLE_GEMFILE=./Gemfile.noed25519 bundle install --jobs 4 --retry 3
|
|
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:
|
|
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,46 @@
|
|
|
1
|
+
=== 7.3.8
|
|
2
|
+
|
|
3
|
+
* Security: validate every ProxyJump hop, not only the first, as [user@]host[:port] (refusing a path, query, fragment or password) and shell-escape the hops passed to ssh -J; a hop such as `evil/x&$(cmd)` ran `cmd` through the shell [GHSA-g2r2-x3p2-5677]
|
|
4
|
+
|
|
5
|
+
=== 7.3.7
|
|
6
|
+
|
|
7
|
+
* Fixed `unexpected packet type 94 during initial key exchange` (or 93) when channel data arrives during a client-initiated re-key, which broke bulk transfers at the first re-key (e.g. with `rekey_limit`, or after about 1 GB); a regression in 7.3.5 [#1063]
|
|
8
|
+
|
|
9
|
+
=== 7.3.6
|
|
10
|
+
|
|
11
|
+
* Fix "Calling Net::SSH::Buffer methods on HostKeyEntries PubKey is deprecated" warnings printed on every connection to a host in known_hosts since 7.3.4 [#1026, #1028]
|
|
12
|
+
|
|
13
|
+
=== 7.3.5
|
|
14
|
+
|
|
15
|
+
* 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]
|
|
16
|
+
* 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]
|
|
17
|
+
* 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]
|
|
18
|
+
* 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]
|
|
19
|
+
* Security: reject packet lengths below 5 or above 256 KB, as OpenSSH does [GHSA-pfqh-p9vh-wx4p]
|
|
20
|
+
* 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]
|
|
21
|
+
|
|
22
|
+
=== 7.3.4
|
|
23
|
+
|
|
24
|
+
* 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]
|
|
25
|
+
|
|
26
|
+
=== 7.3.3
|
|
27
|
+
|
|
28
|
+
* Support .pub file as IdentityFile to filter/prioritize agent keys (1Password SSH agent use case) [#942]
|
|
29
|
+
* Fix cert signing regression when using a .pub companion file without an agent [#1006]
|
|
30
|
+
* Fix unreadable .pub IdentityFile silently skipped instead of raising [#1006]
|
|
31
|
+
* Allow multiple UserKnownHostsFile entries (space-separated, matching ssh_config(5)) [#979]
|
|
32
|
+
* Stop mutating ENV['HOME'] at require time [#997]
|
|
33
|
+
* Fix ED25519 loader with frozen string literals [#995]
|
|
34
|
+
|
|
35
|
+
=== 7.3.2 rc2
|
|
36
|
+
|
|
37
|
+
* Fix Ruby 2.6 compat: use public_to_der fallback for EC public keys [#993]
|
|
38
|
+
* Fix test framework hang: preserve StringIO position in idle!
|
|
39
|
+
|
|
40
|
+
=== 7.3.2 rc1
|
|
41
|
+
|
|
42
|
+
* Support frozen strings [#958]
|
|
43
|
+
|
|
1
44
|
=== 7.3.0 rc0
|
|
2
45
|
|
|
3
46
|
* aes(128|256)gcm [#946]
|
data/Dockerfile
CHANGED
|
@@ -3,6 +3,9 @@ FROM ruby:${RUBY_VERSION}
|
|
|
3
3
|
|
|
4
4
|
ARG BUNDLERV=
|
|
5
5
|
|
|
6
|
+
# Ruby 2.6 to 3.0 images are built on Debian bullseye, which only archive.debian.org still serves.
|
|
7
|
+
RUN if grep -q bullseye /etc/os-release; then sed -i 's|deb.debian.org|archive.debian.org|; s|security.debian.org|archive.debian.org|' /etc/apt/sources.list; fi
|
|
8
|
+
|
|
6
9
|
RUN apt update && apt install -y openssh-server sudo netcat-openbsd \
|
|
7
10
|
&& useradd --create-home --shell '/bin/bash' --comment 'NetSSH' 'net_ssh_1' \
|
|
8
11
|
&& useradd --create-home --shell '/bin/bash' --comment 'NetSSH' 'net_ssh_2' \
|
data/Dockerfile.zlib_ng
ADDED
|
@@ -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 redhat-rpm-config 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/Rakefile
CHANGED
|
@@ -9,7 +9,13 @@ require "rake"
|
|
|
9
9
|
require "rake/clean"
|
|
10
10
|
require "bundler/gem_tasks"
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
# rdoc is no longer a default gem as of Ruby 4.0; keep the rest of the
|
|
13
|
+
# Rakefile usable when it is not installed.
|
|
14
|
+
begin
|
|
15
|
+
require "rdoc/task"
|
|
16
|
+
rescue LoadError
|
|
17
|
+
# rdoc tasks are optional
|
|
18
|
+
end
|
|
13
19
|
|
|
14
20
|
desc "When releasing make sure NET_SSH_BUILDGEM_SIGNED is set"
|
|
15
21
|
task :check_NET_SSH_BUILDGEM_SIGNED do
|
|
@@ -27,17 +33,19 @@ require_relative "lib/net/ssh/version"
|
|
|
27
33
|
version = Net::SSH::Version::CURRENT
|
|
28
34
|
|
|
29
35
|
extra_files = %w[LICENSE.txt THANKS.txt CHANGES.txt]
|
|
30
|
-
RDoc::Task
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
if defined?(RDoc::Task)
|
|
37
|
+
RDoc::Task.new do |rdoc|
|
|
38
|
+
rdoc.rdoc_dir = "rdoc"
|
|
39
|
+
rdoc.title = "#{name} #{version}"
|
|
40
|
+
rdoc.generator = 'hanna' # gem install hanna-nouveau
|
|
41
|
+
rdoc.main = 'README.md'
|
|
42
|
+
rdoc.rdoc_files.include("README*")
|
|
43
|
+
rdoc.rdoc_files.include("bin/*.rb")
|
|
44
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
|
45
|
+
extra_files.each { |file|
|
|
46
|
+
rdoc.rdoc_files.include(file) if File.exist?(file)
|
|
47
|
+
}
|
|
48
|
+
end
|
|
41
49
|
end
|
|
42
50
|
|
|
43
51
|
namespace :cert do
|
data/docker-compose.yml
CHANGED
|
@@ -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[:
|
|
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
|
|
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
|
|
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: :
|
|
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})" }
|
data/lib/net/ssh/config.rb
CHANGED
|
@@ -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
|
data/lib/net/ssh/key_factory.rb
CHANGED
|
@@ -55,7 +55,7 @@ module Net
|
|
|
55
55
|
prompter = nil
|
|
56
56
|
result =
|
|
57
57
|
begin
|
|
58
|
-
key_type.read(data, passphrase
|
|
58
|
+
key_type.read(data, passphrase)
|
|
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
|
data/lib/net/ssh/known_hosts.rb
CHANGED
|
@@ -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
|
|
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
|
data/lib/net/ssh/proxy/jump.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'shellwords'
|
|
1
2
|
require 'uri'
|
|
2
3
|
require 'net/ssh/proxy/command'
|
|
3
4
|
|
|
@@ -34,20 +35,50 @@ 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
|
-
|
|
38
|
+
raise ArgumentError, "no jump host given" if jump_proxies.empty?
|
|
39
|
+
|
|
38
40
|
config = connection_options && connection_options[:config]
|
|
39
|
-
uri =
|
|
41
|
+
uri, *extra_jumps = jump_proxies.split(",", -1).map { |jump| validate_jump!(jump) }
|
|
40
42
|
|
|
41
43
|
template = "ssh".dup
|
|
42
|
-
template << " -l #{uri.user}"
|
|
43
|
-
template << " -p #{uri.port}"
|
|
44
|
-
template << " -J #{extra_jumps}"
|
|
45
|
-
template << " -F #{config}" if config != true && config
|
|
44
|
+
template << " -l #{escape(uri.user)}" if uri.user
|
|
45
|
+
template << " -p #{uri.port}" if uri.port
|
|
46
|
+
template << " -J #{escape(extra_jumps.map { |jump| jump_spec(jump) }.join(','))}" unless extra_jumps.empty?
|
|
47
|
+
template << " -F #{Shellwords.escape(config)}" if config != true && config
|
|
46
48
|
template << " -W %h:%p "
|
|
47
|
-
template << uri.host
|
|
49
|
+
template << escape(uri.host)
|
|
48
50
|
|
|
49
51
|
@command_line_template = template
|
|
50
52
|
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Escape for the shell, and double any % so it survives the %h/%p
|
|
57
|
+
# substitution done on the template in Command#open.
|
|
58
|
+
def escape(value)
|
|
59
|
+
Shellwords.escape(value).gsub("%", "%%")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Only [user@]host[:port] is checked, so anything else URI accepts (a path,
|
|
63
|
+
# query, fragment or password) must be refused before it reaches the shell.
|
|
64
|
+
def validate_jump!(jump)
|
|
65
|
+
uri = URI.parse("ssh://#{jump}")
|
|
66
|
+
raise URI::InvalidURIError if uri.host.to_s.empty? || jump_spec(uri) != jump
|
|
67
|
+
|
|
68
|
+
validate_user_name!(uri.user) if uri.user
|
|
69
|
+
validate_host_name!(uri.host)
|
|
70
|
+
validate_port!(uri.port) if uri.port
|
|
71
|
+
uri
|
|
72
|
+
rescue URI::InvalidURIError
|
|
73
|
+
raise ArgumentError, "jump host is not [user@]host[:port]: #{jump.inspect}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def jump_spec(uri)
|
|
77
|
+
spec = uri.host.dup
|
|
78
|
+
spec.prepend("#{uri.user}@") if uri.user
|
|
79
|
+
spec << ":#{uri.port}" if uri.port
|
|
80
|
+
spec
|
|
81
|
+
end
|
|
51
82
|
end
|
|
52
83
|
end
|
|
53
84
|
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
|
|
52
|
-
# the port number. In all cases, this method will return the port
|
|
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
|
-
|
|
61
|
-
self.
|
|
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
|
data/lib/net/ssh/test/socket.rb
CHANGED
|
@@ -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
|
-
|
|
52
|
+
deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
@version =
|
|
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
|
|
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,8 @@ module Net
|
|
|
211
216
|
|
|
212
217
|
else
|
|
213
218
|
return packet if algorithms.allow?(packet)
|
|
219
|
+
# Algorithms#rekey! clears initialized?, so the session id is what marks the first key exchange as done.
|
|
220
|
+
raise Net::SSH::Exception, "unexpected packet type #{packet.type} during initial key exchange" unless algorithms.session_id
|
|
214
221
|
|
|
215
222
|
push(packet)
|
|
216
223
|
end
|
|
@@ -234,6 +241,11 @@ module Net
|
|
|
234
241
|
# #poll_message will return packets from the queue in the order they
|
|
235
242
|
# were received.
|
|
236
243
|
def push(packet)
|
|
244
|
+
@queued_bytes += packet.length
|
|
245
|
+
if @queue.size >= MAX_QUEUED_PACKETS || @queued_bytes > MAX_QUEUED_BYTES
|
|
246
|
+
raise Net::SSH::Exception, "too many packets queued during key exchange"
|
|
247
|
+
end
|
|
248
|
+
|
|
237
249
|
@queue.push(packet)
|
|
238
250
|
end
|
|
239
251
|
|
|
@@ -276,6 +288,12 @@ module Net
|
|
|
276
288
|
|
|
277
289
|
private
|
|
278
290
|
|
|
291
|
+
def shift_queue
|
|
292
|
+
packet = @queue.shift
|
|
293
|
+
@queued_bytes -= packet.length
|
|
294
|
+
packet
|
|
295
|
+
end
|
|
296
|
+
|
|
279
297
|
# Compatibility verifier which allows users to keep using
|
|
280
298
|
# custom verifier code without adding new :verify_signature
|
|
281
299
|
# 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
|
-
#
|
|
23
|
-
|
|
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
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
41
|
+
true
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
def verify_signature(&block)
|
|
@@ -43,6 +47,40 @@ 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
|
+
# Plain keys carry no certificate constraints, and respond_to? on this delegator prints a deprecation warning (#1026).
|
|
55
|
+
return if entry.is_a?(Net::SSH::HostKeyEntries::PubKey)
|
|
56
|
+
|
|
57
|
+
cert = arguments[:key]
|
|
58
|
+
|
|
59
|
+
if entry.respond_to?(:matches_validity?) && !entry.matches_validity?(cert)
|
|
60
|
+
reason = if cert.valid_before && cert.valid_before < Time.now
|
|
61
|
+
"Certificate has expired"
|
|
62
|
+
else
|
|
63
|
+
"Certificate is not yet valid"
|
|
64
|
+
end
|
|
65
|
+
process_cache_miss(host_keys, arguments, HostKeyMismatch, reason)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if entry.respond_to?(:matches_principal?) && !entry.matches_principal?(cert, host_keys.hostname)
|
|
69
|
+
process_cache_miss(host_keys, arguments, HostKeyMismatch,
|
|
70
|
+
"Certificate invalid: name is not a listed principal")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if entry.respond_to?(:matches_type?) && !entry.matches_type?(cert)
|
|
74
|
+
process_cache_miss(host_keys, arguments, HostKeyMismatch,
|
|
75
|
+
"Certificate invalid: not a host certificate")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
return unless entry.respond_to?(:critical_options_supported?) && !entry.critical_options_supported?(cert)
|
|
79
|
+
|
|
80
|
+
process_cache_miss(host_keys, arguments, HostKeyMismatch,
|
|
81
|
+
"Certificate invalid: unsupported critical option")
|
|
82
|
+
end
|
|
83
|
+
|
|
46
84
|
def process_cache_miss(host_keys, args, exc_class, message)
|
|
47
85
|
exception = exc_class.new("fingerprint #{args[:fingerprint]} " +
|
|
48
86
|
"#{message} for #{host_keys.host.inspect}")
|
data/lib/net/ssh/version.rb
CHANGED
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
|
|
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.
|
data/net-ssh-public_cert.pem
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
-----BEGIN CERTIFICATE-----
|
|
2
2
|
MIIDeDCCAmCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBBMQ8wDQYDVQQDDAZuZXRz
|
|
3
3
|
c2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZFgNj
|
|
4
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
@@ -30,6 +30,8 @@ Gem::Specification.new do |spec|
|
|
|
30
30
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
31
31
|
spec.require_paths = ["lib"]
|
|
32
32
|
|
|
33
|
+
spec.add_dependency("logger")
|
|
34
|
+
|
|
33
35
|
unless ENV['NET_SSH_NO_ED25519']
|
|
34
36
|
spec.add_development_dependency("bcrypt_pbkdf", "~> 1.0") unless RUBY_PLATFORM == "java"
|
|
35
37
|
spec.add_development_dependency("ed25519", "~> 1.2")
|
|
@@ -42,6 +44,7 @@ Gem::Specification.new do |spec|
|
|
|
42
44
|
spec.add_development_dependency "bundler", ">= 1.17"
|
|
43
45
|
spec.add_development_dependency "minitest", "~> 5.19"
|
|
44
46
|
spec.add_development_dependency "mocha", "~> 2.1.0"
|
|
47
|
+
spec.add_development_dependency "ostruct"
|
|
45
48
|
spec.add_development_dependency "rake", "~> 12.0"
|
|
46
49
|
spec.add_development_dependency "rubocop", "~> 1.28.0"
|
|
47
50
|
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.
|
|
4
|
+
version: 7.3.8
|
|
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
|
-
|
|
17
|
+
b20wHhcNMjYwMjE1MTAxNzUyWhcNMjcwMjE1MTAxNzUyWjBBMQ8wDQYDVQQDDAZu
|
|
18
18
|
ZXRzc2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZ
|
|
19
19
|
FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGJ4TbZ9H+qZ08
|
|
20
20
|
pQfJhPJTHaDCyQvCsKTFrL5O9z3tllQ7B/zksMMM+qFBpNYu9HCcg4yBATacE/PB
|
|
@@ -25,15 +25,29 @@ cert_chain:
|
|
|
25
25
|
3gL7P0axAgMBAAGjezB5MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
|
|
26
26
|
BBQF8qLA7Z4zg0SJGtUbv3eoQ8tjIzAfBgNVHREEGDAWgRRuZXRzc2hAc29sdXRp
|
|
27
27
|
b3VzLmNvbTAfBgNVHRIEGDAWgRRuZXRzc2hAc29sdXRpb3VzLmNvbTANBgkqhkiG
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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:
|
|
35
|
+
date: 2026-10-25 00:00:00.000000000 Z
|
|
36
36
|
dependencies:
|
|
37
|
+
- !ruby/object:Gem::Dependency
|
|
38
|
+
name: logger
|
|
39
|
+
requirement: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
type: :runtime
|
|
45
|
+
prerelease: false
|
|
46
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '0'
|
|
37
51
|
- !ruby/object:Gem::Dependency
|
|
38
52
|
name: bcrypt_pbkdf
|
|
39
53
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -146,6 +160,20 @@ dependencies:
|
|
|
146
160
|
- - "~>"
|
|
147
161
|
- !ruby/object:Gem::Version
|
|
148
162
|
version: 2.1.0
|
|
163
|
+
- !ruby/object:Gem::Dependency
|
|
164
|
+
name: ostruct
|
|
165
|
+
requirement: !ruby/object:Gem::Requirement
|
|
166
|
+
requirements:
|
|
167
|
+
- - ">="
|
|
168
|
+
- !ruby/object:Gem::Version
|
|
169
|
+
version: '0'
|
|
170
|
+
type: :development
|
|
171
|
+
prerelease: false
|
|
172
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
173
|
+
requirements:
|
|
174
|
+
- - ">="
|
|
175
|
+
- !ruby/object:Gem::Version
|
|
176
|
+
version: '0'
|
|
149
177
|
- !ruby/object:Gem::Dependency
|
|
150
178
|
name: rake
|
|
151
179
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -198,6 +226,7 @@ files:
|
|
|
198
226
|
- DEVELOPMENT.md
|
|
199
227
|
- Dockerfile
|
|
200
228
|
- Dockerfile.openssl3
|
|
229
|
+
- Dockerfile.zlib_ng
|
|
201
230
|
- Gemfile
|
|
202
231
|
- Gemfile.noed25519
|
|
203
232
|
- Gemfile.norbnacl
|
|
@@ -315,7 +344,7 @@ licenses:
|
|
|
315
344
|
- MIT
|
|
316
345
|
metadata:
|
|
317
346
|
changelog_uri: https://github.com/net-ssh/net-ssh/blob/master/CHANGES.txt
|
|
318
|
-
post_install_message:
|
|
347
|
+
post_install_message:
|
|
319
348
|
rdoc_options: []
|
|
320
349
|
require_paths:
|
|
321
350
|
- lib
|
|
@@ -331,7 +360,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
331
360
|
version: '0'
|
|
332
361
|
requirements: []
|
|
333
362
|
rubygems_version: 3.3.3
|
|
334
|
-
signing_key:
|
|
363
|
+
signing_key:
|
|
335
364
|
specification_version: 4
|
|
336
365
|
summary: 'Net::SSH: a pure-Ruby implementation of the SSH2 client protocol.'
|
|
337
366
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|