net-ssh 7.3.6 → 8.0.0.beta3
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/.github/workflows/ci-with-docker.yml +0 -3
- data/.github/workflows/ci.yml +2 -17
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +1 -0
- data/CHANGES.txt +2 -15
- data/Dockerfile +1 -4
- data/Dockerfile.openssl3 +1 -1
- data/Dockerfile.zlib_ng +1 -1
- data/README.md +10 -13
- data/appveyor.yml +4 -16
- data/docker-compose.yml +1 -7
- data/lib/net/ssh/authentication/agent.rb +4 -4
- data/lib/net/ssh/authentication/ed25519.rb +134 -46
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/key_factory.rb +3 -4
- data/lib/net/ssh/proxy/command.rb +3 -28
- data/lib/net/ssh/proxy/jump.rb +3 -13
- data/lib/net/ssh/service/forward.rb +0 -6
- data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
- data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
- data/lib/net/ssh/transport/cipher_factory.rb +23 -1
- data/lib/net/ssh/transport/gcm_cipher.rb +27 -6
- data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
- data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
- data/lib/net/ssh/transport/packet_stream.rb +0 -4
- data/lib/net/ssh/transport/server_version.rb +13 -33
- data/lib/net/ssh/transport/session.rb +1 -18
- data/lib/net/ssh/verifiers/always.rb +0 -3
- data/lib/net/ssh/version.rb +4 -4
- data/net-ssh.gemspec +3 -7
- data.tar.gz.sig +0 -0
- metadata +12 -42
- metadata.gz.sig +0 -0
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee72c6673cb55fe69cdb90bc49d0b7266b29cb80f7a14794f873e048075cde2b
|
|
4
|
+
data.tar.gz: b9416f51d2b80d5d0bbbcac1b33a4272d5b5add59d81421ca8eaa84db2fd0823
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f1d529b4af06145b892c6ece94d2fcc6a9d2b65b93db633189b71988b8bc03aa64feffdfce3509ce64809378ff542b04f01381419d5a9e11246c7303d258583
|
|
7
|
+
data.tar.gz: 2170524d2a24e1ec8d0c07c3237d140ad00c412c59875f6f178b6f4a2c47ce9d16f8418dee56e0425d8ec8a214f50e0cc96e434d1b6e37998125e1cda9ebb41e
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
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.
|
|
10
|
+
ruby-version: [2.7.8, 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
|
|
|
@@ -39,11 +39,10 @@ jobs:
|
|
|
39
39
|
${{ runner.os }}-pip-v1
|
|
40
40
|
- name: Bundle install
|
|
41
41
|
run: |
|
|
42
|
-
gem install bundler ${{
|
|
42
|
+
gem install bundler ${{ 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
45
|
bundle install --jobs 4 --retry 3
|
|
46
|
-
BUNDLE_GEMFILE=./Gemfile.noed25519 bundle install --jobs 4 --retry 3
|
|
47
46
|
env:
|
|
48
47
|
BUNDLE_PATH: vendor/bundle
|
|
49
48
|
|
|
@@ -79,20 +78,6 @@ jobs:
|
|
|
79
78
|
NET_SSH_RUN_INTEGRATION_TESTS: 1
|
|
80
79
|
CI: 1
|
|
81
80
|
RUBYOPT: '--enable=frozen-string-literal'
|
|
82
|
-
- name: Run tests (without rbnacl)
|
|
83
|
-
run: bundle exec rake test
|
|
84
|
-
env:
|
|
85
|
-
BUNDLE_GEMFILE: ./Gemfile.norbnacl
|
|
86
|
-
NET_SSH_RUN_INTEGRATION_TESTS: 1
|
|
87
|
-
CI: 1
|
|
88
|
-
RUBYOPT: '--enable=frozen-string-literal'
|
|
89
|
-
- name: Run Tests (without ed25519)
|
|
90
|
-
run: bundle exec rake test
|
|
91
|
-
env:
|
|
92
|
-
BUNDLE_GEMFILE: ./Gemfile.noed25519
|
|
93
|
-
NET_SSH_RUN_INTEGRATION_TESTS: 1
|
|
94
|
-
CI: 1
|
|
95
|
-
RUBYOPT: '--enable=frozen-string-literal'
|
|
96
81
|
- name: Run test helper test
|
|
97
82
|
run: bundle exec rake test_test
|
|
98
83
|
env:
|
data/.rubocop.yml
CHANGED
data/.rubocop_todo.yml
CHANGED
|
@@ -335,6 +335,7 @@ Naming/MemoizedInstanceVariableName:
|
|
|
335
335
|
Naming/MethodName:
|
|
336
336
|
Exclude:
|
|
337
337
|
- 'lib/net/ssh/authentication/ed25519_loader.rb'
|
|
338
|
+
- 'lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb'
|
|
338
339
|
- 'lib/net/ssh/transport/kex/curve25519_sha256_loader.rb'
|
|
339
340
|
- 'test/authentication/test_agent.rb'
|
|
340
341
|
- 'test/authentication/test_session.rb'
|
data/CHANGES.txt
CHANGED
|
@@ -1,19 +1,6 @@
|
|
|
1
|
-
===
|
|
1
|
+
=== 8.0.0 beta3
|
|
2
2
|
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
=== 7.3.5
|
|
6
|
-
|
|
7
|
-
* 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]
|
|
8
|
-
* 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]
|
|
9
|
-
* 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]
|
|
10
|
-
* 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]
|
|
11
|
-
* Security: reject packet lengths below 5 or above 256 KB, as OpenSSH does [GHSA-pfqh-p9vh-wx4p]
|
|
12
|
-
* 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]
|
|
13
|
-
|
|
14
|
-
=== 7.3.4
|
|
15
|
-
|
|
16
|
-
* 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]
|
|
3
|
+
* 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 [#1009]
|
|
17
4
|
|
|
18
5
|
=== 7.3.3
|
|
19
6
|
|
data/Dockerfile
CHANGED
|
@@ -3,9 +3,6 @@ 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
|
-
|
|
9
6
|
RUN apt update && apt install -y openssh-server sudo netcat-openbsd \
|
|
10
7
|
&& useradd --create-home --shell '/bin/bash' --comment 'NetSSH' 'net_ssh_1' \
|
|
11
8
|
&& useradd --create-home --shell '/bin/bash' --comment 'NetSSH' 'net_ssh_2' \
|
|
@@ -29,4 +26,4 @@ RUN gem install bundler ${BUNDLERV} && bundle install
|
|
|
29
26
|
|
|
30
27
|
COPY . $INSTALL_PATH/
|
|
31
28
|
|
|
32
|
-
CMD service ssh start && rake test
|
|
29
|
+
CMD service ssh start && rake test
|
data/Dockerfile.openssl3
CHANGED
data/Dockerfile.zlib_ng
CHANGED
|
@@ -11,7 +11,7 @@ ENV INSTALL_PATH="/netssh"
|
|
|
11
11
|
# Ruby/OpenSSL initialises — setting it in the Rakefile is too late.
|
|
12
12
|
ENV OPENSSL_CONF="/netssh/test/openssl3.conf"
|
|
13
13
|
|
|
14
|
-
RUN dnf install -y openssl ruby ruby-devel git make gcc
|
|
14
|
+
RUN dnf install -y openssl openssl-devel ruby ruby-devel git make gcc zlib-ng crypto-policies-scripts && \
|
|
15
15
|
update-crypto-policies --set LEGACY
|
|
16
16
|
|
|
17
17
|
WORKDIR $INSTALL_PATH
|
data/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Unsecure algoritms will definitely be removed in Net::SSH 8.*.
|
|
|
40
40
|
| Name | Support | Details |
|
|
41
41
|
|----------------------|-----------------------|----------|
|
|
42
42
|
| ssh-rsa | OK | |
|
|
43
|
-
| ssh-ed25519 | OK |
|
|
43
|
+
| ssh-ed25519 | OK | Backed by Ruby OpenSSL raw Ed25519 APIs (`openssl >= 3.2.0`) |
|
|
44
44
|
| ecdsa-sha2-nistp521 | OK | [using weak elliptic curves](https://safecurves.cr.yp.to/) |
|
|
45
45
|
| ecdsa-sha2-nistp384 | OK | [using weak elliptic curves](https://safecurves.cr.yp.to/) |
|
|
46
46
|
| ecdsa-sha2-nistp256 | OK | [using weak elliptic curves](https://safecurves.cr.yp.to/) |
|
|
@@ -50,7 +50,7 @@ Unsecure algoritms will definitely be removed in Net::SSH 8.*.
|
|
|
50
50
|
|
|
51
51
|
| Name | Support | Details |
|
|
52
52
|
|--------------------------------------|-----------------------|----------|
|
|
53
|
-
| curve25519-sha256 | OK |
|
|
53
|
+
| curve25519-sha256 | OK | Backed by Ruby OpenSSL raw X25519 APIs (`openssl >= 3.2.0`) |
|
|
54
54
|
| ecdh-sha2-nistp521 | OK | [using weak elliptic curves](https://safecurves.cr.yp.to/) |
|
|
55
55
|
| ecdh-sha2-nistp384 | OK | [using weak elliptic curves](https://safecurves.cr.yp.to/) |
|
|
56
56
|
| ecdh-sha2-nistp256 | OK | [using weak elliptic curves](https://safecurves.cr.yp.to/) |
|
|
@@ -64,7 +64,7 @@ Unsecure algoritms will definitely be removed in Net::SSH 8.*.
|
|
|
64
64
|
| Name | Support | Details |
|
|
65
65
|
|--------------------------------------|-----------------------|----------|
|
|
66
66
|
| aes256-ctr / aes192-ctr / aes128-ctr | OK | |
|
|
67
|
-
| chacha20-poly1305@openssh.com | OK. |
|
|
67
|
+
| chacha20-poly1305@openssh.com | OK. | Backed by Ruby OpenSSL ChaCha20 and Poly1305 APIs (`openssl >= 3.2.0`) |
|
|
68
68
|
| aes256-cbc / aes192-cbc / aes128-cbc | Deprecated in 6.0 | unsecure, will be removed in 8.0 |
|
|
69
69
|
| rijndael-cbc@lysator.liu.se | Deprecated in 6.0 | unsecure, will be removed in 8.0 |
|
|
70
70
|
| blowfish-ctr blowfish-cbc | Deprecated in 6.0 | unsecure, will be removed in 8.0 |
|
|
@@ -148,16 +148,16 @@ See Net::SSH for more documentation, and links to further information.
|
|
|
148
148
|
|
|
149
149
|
## REQUIREMENTS:
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
Net::SSH requires Ruby 2.7 or newer and the Ruby `openssl` gem version `3.2.0` or newer.
|
|
152
|
+
The `openssl` gem uses the OpenSSL library Ruby is built against. You can verify the loaded gem and library with:
|
|
153
153
|
|
|
154
154
|
```sh
|
|
155
|
-
ruby -ropenssl -e 'puts OpenSSL::OPENSSL_VERSION'
|
|
155
|
+
ruby -ropenssl -e 'puts OpenSSL::VERSION; puts OpenSSL::OPENSSL_VERSION'
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
-
If
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
If `openssl` cannot be required, rebuild Ruby with OpenSSL support or install the Ruby `openssl` gem separately.
|
|
159
|
+
|
|
160
|
+
Ed25519, curve25519-sha256, and chacha20-poly1305@openssh.com support also require an OpenSSL backend with Ed25519, X25519, ChaCha20, and Poly1305 enabled.
|
|
161
161
|
|
|
162
162
|
## INSTALL:
|
|
163
163
|
|
|
@@ -188,15 +188,12 @@ gem install net-ssh -P HighSecurity
|
|
|
188
188
|
If you don't add the public key, you'll see an error like "Couldn't verify data signature".
|
|
189
189
|
If you're still having trouble let me know and I'll give you a hand.
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
Ed25519 public key auth is backed by Ruby OpenSSL. The `bcrypt_pbkdf` gem is only needed when loading bcrypt-encrypted OpenSSH private keys.
|
|
192
192
|
|
|
193
193
|
```sh
|
|
194
|
-
gem install ed25519
|
|
195
194
|
gem install bcrypt_pbkdf
|
|
196
195
|
```
|
|
197
196
|
|
|
198
|
-
For curve25519-sha256 kex exchange support your bundle file should contain `x25519` dependency.
|
|
199
|
-
|
|
200
197
|
## RUBY SUPPORT
|
|
201
198
|
|
|
202
199
|
* See [net-ssh.gemspec](https://github.com/net-ssh/net-ssh/blob/master/net-ssh.gemspec) for current versions ruby requirements
|
data/appveyor.yml
CHANGED
|
@@ -4,16 +4,9 @@ skip_tags: true
|
|
|
4
4
|
|
|
5
5
|
environment:
|
|
6
6
|
matrix:
|
|
7
|
-
- ruby_version: "
|
|
8
|
-
- ruby_version: "
|
|
9
|
-
- ruby_version: "
|
|
10
|
-
- ruby_version: "24-x64"
|
|
11
|
-
- ruby_version: "23"
|
|
12
|
-
- ruby_version: "23-x64"
|
|
13
|
-
|
|
14
|
-
matrix:
|
|
15
|
-
allow_failures:
|
|
16
|
-
- ruby_version: "jruby-9.1.2.0"
|
|
7
|
+
- ruby_version: "31-x64"
|
|
8
|
+
- ruby_version: "30-x64"
|
|
9
|
+
- ruby_version: "27-x64"
|
|
17
10
|
|
|
18
11
|
#init:
|
|
19
12
|
# - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
|
|
@@ -27,12 +20,8 @@ platform:
|
|
|
27
20
|
|
|
28
21
|
install:
|
|
29
22
|
- SET PATH=C:\Ruby%ruby_version%\bin;%PATH%
|
|
30
|
-
- if "%ruby_version%" == "jruby-9.1.2.0" ( cinst javaruntime -i )
|
|
31
|
-
- if "%ruby_version%" == "jruby-9.1.2.0" ( cinst jruby --version 9.1.2.0 -i --allow-empty-checksums )
|
|
32
|
-
- if "%ruby_version%" == "jruby-9.1.2.0" ( SET "PATH=C:\jruby-9.1.2.0\bin\;%PATH%" )
|
|
33
23
|
- ruby --version
|
|
34
|
-
- gem install bundler --no-document --user-install -v
|
|
35
|
-
- SET BUNDLE_GEMFILE=Gemfile.noed25519
|
|
24
|
+
- gem install bundler --no-document --user-install -v 2.4.22
|
|
36
25
|
- bundle install --retry=3
|
|
37
26
|
- cinst freesshd
|
|
38
27
|
- cinst putty --allow-empty-checksums
|
|
@@ -51,7 +40,6 @@ install:
|
|
|
51
40
|
}
|
|
52
41
|
|
|
53
42
|
test_script:
|
|
54
|
-
- SET BUNDLE_GEMFILE=Gemfile.noed25519
|
|
55
43
|
- SET NET_SSH_RUN_WIN_INTEGRATION_TESTS=YES
|
|
56
44
|
- bundle exec rake test
|
|
57
45
|
|
data/docker-compose.yml
CHANGED
|
@@ -261,13 +261,13 @@ module Net
|
|
|
261
261
|
when /^ecdsa\-sha2\-(\w*)-cert-v01@openssh\.com$/
|
|
262
262
|
Net::SSH::Buffer.from(:string, priv_key.to_blob, :bignum, priv_key.key.private_key).to_s
|
|
263
263
|
when /^ssh-ed25519$/
|
|
264
|
-
Net::SSH::Buffer.from(:string, priv_key.
|
|
265
|
-
:string, priv_key.
|
|
264
|
+
Net::SSH::Buffer.from(:string, priv_key.public_key_bytes,
|
|
265
|
+
:string, priv_key.private_key_bytes_for_agent).to_s
|
|
266
266
|
when /^ssh-ed25519-cert-v01@openssh\.com$/
|
|
267
267
|
# Unlike the other certificate types, the public key is included after the certifiate.
|
|
268
268
|
Net::SSH::Buffer.from(:string, priv_key.to_blob,
|
|
269
|
-
:string, priv_key.key.
|
|
270
|
-
:string, priv_key.key.
|
|
269
|
+
:string, priv_key.key.public_key_bytes,
|
|
270
|
+
:string, priv_key.key.private_key_bytes_for_agent).to_s
|
|
271
271
|
when /^ssh-rsa$/
|
|
272
272
|
# `n` and `e` are reversed compared to the ordering in `OpenSSL::PKey::RSA#to_blob`.
|
|
273
273
|
Net::SSH::Buffer.from(:bignum, priv_key.n, :bignum, priv_key.e, :bignum, priv_key.d,
|
|
@@ -1,25 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
gem 'bcrypt_pbkdf', '~> 1.0' unless RUBY_PLATFORM == "java"
|
|
3
|
-
|
|
4
|
-
require 'ed25519'
|
|
5
|
-
|
|
1
|
+
require 'openssl'
|
|
6
2
|
require 'net/ssh/transport/cipher_factory'
|
|
7
3
|
require 'net/ssh/authentication/pub_key_fingerprint'
|
|
8
|
-
require 'bcrypt_pbkdf' unless RUBY_PLATFORM == "java"
|
|
9
4
|
|
|
10
5
|
module Net
|
|
11
6
|
module SSH
|
|
12
7
|
module Authentication
|
|
13
8
|
module ED25519
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
raise ArgumentError, "pk does not match sk" unless pk == key.verify_key.to_bytes
|
|
9
|
+
ALGORITHM = "ED25519"
|
|
10
|
+
KEY_BYTES = 32
|
|
11
|
+
PRIVATE_KEY_BYTES = 64
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
class UnsupportedError < StandardError; end
|
|
14
|
+
|
|
15
|
+
def self.ensure_supported!
|
|
16
|
+
unless OpenSSL::PKey.respond_to?(:new_raw_public_key) && OpenSSL::PKey.respond_to?(:new_raw_private_key)
|
|
17
|
+
raise UnsupportedError, "OpenSSL::PKey raw public/private key APIs are unavailable"
|
|
20
18
|
end
|
|
19
|
+
|
|
20
|
+
key = new_private_key("\x00" * KEY_BYTES)
|
|
21
|
+
signature = sign(key, "")
|
|
22
|
+
public_key = new_public_key(key.raw_public_key)
|
|
23
|
+
raise UnsupportedError, "OpenSSL Ed25519 signature verification failed" unless verify(public_key, signature, "")
|
|
24
|
+
rescue OpenSSL::PKey::PKeyError => e
|
|
25
|
+
raise UnsupportedError, e.message
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.new_public_key(key)
|
|
29
|
+
validate_key_bytes!(key, "public key", KEY_BYTES)
|
|
30
|
+
OpenSSL::PKey.new_raw_public_key(ALGORITHM, binary_string(key))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.new_private_key(key)
|
|
34
|
+
validate_key_bytes!(key, "private key", KEY_BYTES)
|
|
35
|
+
OpenSSL::PKey.new_raw_private_key(ALGORITHM, binary_string(key))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.sign(key, data)
|
|
39
|
+
key.sign(nil, data)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.verify(key, signature, data)
|
|
43
|
+
key.verify(nil, signature, data)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.validate_key_bytes!(key, label, expected_bytes)
|
|
47
|
+
raise ArgumentError, "invalid Ed25519 #{label}" unless key.respond_to?(:bytesize) && key.bytesize == expected_bytes
|
|
21
48
|
end
|
|
22
49
|
|
|
50
|
+
def self.binary_string(string)
|
|
51
|
+
string.dup.force_encoding('BINARY')
|
|
52
|
+
end
|
|
53
|
+
private_class_method :binary_string
|
|
54
|
+
|
|
23
55
|
class OpenSSHPrivateKeyLoader
|
|
24
56
|
CipherFactory = Net::SSH::Transport::CipherFactory
|
|
25
57
|
|
|
@@ -27,9 +59,6 @@ module Net
|
|
|
27
59
|
MEND = "-----END OPENSSH PRIVATE KEY-----"
|
|
28
60
|
MAGIC = "openssh-key-v1"
|
|
29
61
|
|
|
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
|
-
|
|
33
62
|
class DecryptError < ArgumentError
|
|
34
63
|
def initialize(message, encrypted_key: false)
|
|
35
64
|
super(message)
|
|
@@ -41,11 +70,6 @@ module Net
|
|
|
41
70
|
end
|
|
42
71
|
end
|
|
43
72
|
|
|
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
|
-
|
|
49
73
|
def self.read(datafull, password)
|
|
50
74
|
datafull = datafull.strip
|
|
51
75
|
raise ArgumentError.new("Expected #{MBEGIN} at start of private key") unless datafull.start_with?(MBEGIN)
|
|
@@ -55,7 +79,7 @@ module Net
|
|
|
55
79
|
data = datab64.unpack1("m")
|
|
56
80
|
raise ArgumentError.new("Expected #{MAGIC} at start of decoded private key") unless data.start_with?(MAGIC)
|
|
57
81
|
|
|
58
|
-
buffer = Net::SSH::Buffer.new(data[MAGIC.size + 1..-1])
|
|
82
|
+
buffer = Net::SSH::Buffer.new(data[(MAGIC.size + 1)..-1])
|
|
59
83
|
|
|
60
84
|
ciphername = buffer.read_string
|
|
61
85
|
raise ArgumentError.new("#{ciphername} in private key is not supported") unless
|
|
@@ -71,29 +95,34 @@ module Net
|
|
|
71
95
|
_pubkey = buffer.read_string
|
|
72
96
|
|
|
73
97
|
len = buffer.read_long
|
|
98
|
+
encrypted_private = buffer.read(len)
|
|
74
99
|
|
|
75
100
|
keylen, blocksize, ivlen = CipherFactory.get_lengths(ciphername, iv_len: true)
|
|
76
101
|
raise ArgumentError.new("Private key len:#{len} is not a multiple of #{blocksize}") if
|
|
77
|
-
(
|
|
102
|
+
(len < blocksize) || ((blocksize > 0) && (len % blocksize) != 0)
|
|
103
|
+
|
|
104
|
+
raise ArgumentError.new("Private key len:#{len} exceeds available data") unless encrypted_private.bytesize == len
|
|
105
|
+
|
|
106
|
+
authlen = CipherFactory.auth_length(ciphername)
|
|
107
|
+
auth_tag = authlen > 0 ? buffer.read(authlen) : nil
|
|
78
108
|
|
|
79
109
|
if kdfname == 'bcrypt'
|
|
80
110
|
salt = kdfopts.read_string
|
|
81
111
|
rounds = kdfopts.read_long
|
|
82
112
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
check_bcrypt_kdf!(rounds, password)
|
|
86
|
-
|
|
87
|
-
key = BCryptPbkdf::key(password, salt, keylen + ivlen, rounds)
|
|
88
|
-
raise DecryptError.new("BCyryptPbkdf failed", encrypted_key: true) unless key
|
|
113
|
+
key = bcrypt_pbkdf_key(password, salt, keylen + ivlen, rounds)
|
|
89
114
|
else
|
|
90
|
-
key =
|
|
115
|
+
key = "\x00" * (keylen + ivlen)
|
|
91
116
|
end
|
|
92
117
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
118
|
+
decoded = decrypt_private_key(
|
|
119
|
+
ciphername,
|
|
120
|
+
encrypted_private,
|
|
121
|
+
auth_tag,
|
|
122
|
+
key: key[0...keylen],
|
|
123
|
+
iv: key[keylen...(keylen + ivlen)],
|
|
124
|
+
encrypted_key: kdfname == 'bcrypt'
|
|
125
|
+
)
|
|
97
126
|
|
|
98
127
|
decoded = Net::SSH::Buffer.new(decoded)
|
|
99
128
|
check1 = decoded.read_long
|
|
@@ -109,15 +138,49 @@ module Net
|
|
|
109
138
|
decoded.read_private_keyblob(type_name)
|
|
110
139
|
end
|
|
111
140
|
end
|
|
141
|
+
|
|
142
|
+
def self.bcrypt_pbkdf_key(password, salt, length, rounds)
|
|
143
|
+
begin
|
|
144
|
+
require_bcrypt_pbkdf
|
|
145
|
+
rescue LoadError
|
|
146
|
+
raise DecryptError.new("bcrypt_pbkdf is required to decrypt bcrypt-encrypted OpenSSH private keys")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
key = BCryptPbkdf::key(password, salt, length, rounds)
|
|
150
|
+
raise DecryptError.new("BCryptPbkdf failed", encrypted_key: true) unless key
|
|
151
|
+
|
|
152
|
+
key
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def self.require_bcrypt_pbkdf
|
|
156
|
+
require 'bcrypt_pbkdf'
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def self.decrypt_private_key(ciphername, encrypted_private, auth_tag, options)
|
|
160
|
+
if auth_tag
|
|
161
|
+
return CipherFactory.decrypt_private_key(
|
|
162
|
+
ciphername,
|
|
163
|
+
encrypted_private,
|
|
164
|
+
auth_tag,
|
|
165
|
+
options[:key],
|
|
166
|
+
options[:iv]
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
cipher = CipherFactory.get(ciphername, key: options[:key], iv: options[:iv], decrypt: true)
|
|
171
|
+
decoded = cipher.update(encrypted_private)
|
|
172
|
+
decoded << cipher.final
|
|
173
|
+
rescue Net::SSH::Exception, OpenSSL::Cipher::CipherError
|
|
174
|
+
raise DecryptError.new("Decrypt failed on private key", encrypted_key: options[:encrypted_key])
|
|
175
|
+
end
|
|
112
176
|
end
|
|
113
177
|
|
|
114
178
|
class PubKey
|
|
115
179
|
include Net::SSH::Authentication::PubKeyFingerprint
|
|
116
180
|
|
|
117
|
-
attr_reader :verify_key
|
|
118
|
-
|
|
119
181
|
def initialize(data)
|
|
120
|
-
|
|
182
|
+
ED25519.validate_key_bytes!(data, "public key", KEY_BYTES)
|
|
183
|
+
@public_key_bytes = data.dup.force_encoding('BINARY')
|
|
121
184
|
end
|
|
122
185
|
|
|
123
186
|
def self.read_keyblob(buffer)
|
|
@@ -125,7 +188,7 @@ module Net
|
|
|
125
188
|
end
|
|
126
189
|
|
|
127
190
|
def to_blob
|
|
128
|
-
Net::SSH::Buffer.from(:mstring, "ssh-ed25519".dup, :string,
|
|
191
|
+
Net::SSH::Buffer.from(:mstring, "ssh-ed25519".dup, :string, public_key_bytes).to_s
|
|
129
192
|
end
|
|
130
193
|
|
|
131
194
|
def ssh_type
|
|
@@ -137,22 +200,26 @@ module Net
|
|
|
137
200
|
end
|
|
138
201
|
|
|
139
202
|
def ssh_do_verify(sig, data, options = {})
|
|
140
|
-
|
|
203
|
+
ED25519.verify(verify_key, sig, data)
|
|
141
204
|
end
|
|
142
205
|
|
|
143
206
|
def to_pem
|
|
144
207
|
# TODO this is not pem
|
|
145
|
-
ssh_type + [
|
|
208
|
+
ssh_type + [public_key_bytes].pack("m")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def public_key_bytes
|
|
212
|
+
@public_key_bytes.dup
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def verify_key
|
|
216
|
+
@verify_key ||= ED25519.new_public_key(@public_key_bytes)
|
|
146
217
|
end
|
|
147
218
|
end
|
|
148
219
|
|
|
149
220
|
class PrivKey
|
|
150
221
|
CipherFactory = Net::SSH::Transport::CipherFactory
|
|
151
222
|
|
|
152
|
-
MBEGIN = "-----BEGIN OPENSSH PRIVATE KEY-----\n"
|
|
153
|
-
MEND = "-----END OPENSSH PRIVATE KEY-----\n"
|
|
154
|
-
MAGIC = "openssh-key-v1"
|
|
155
|
-
|
|
156
223
|
attr_reader :sign_key
|
|
157
224
|
|
|
158
225
|
def initialize(buffer)
|
|
@@ -160,8 +227,17 @@ module Net
|
|
|
160
227
|
sk = buffer.read_string
|
|
161
228
|
_comment = buffer.read_string
|
|
162
229
|
|
|
163
|
-
|
|
164
|
-
|
|
230
|
+
ED25519.validate_key_bytes!(pk, "public key", KEY_BYTES)
|
|
231
|
+
ED25519.validate_key_bytes!(sk, "private key field", PRIVATE_KEY_BYTES)
|
|
232
|
+
|
|
233
|
+
private_key_bytes = sk[0, KEY_BYTES]
|
|
234
|
+
raise ArgumentError, "Ed25519 private key does not include matching public key" unless sk[KEY_BYTES, KEY_BYTES] == pk
|
|
235
|
+
|
|
236
|
+
@sign_key = ED25519.new_private_key(private_key_bytes)
|
|
237
|
+
raise ArgumentError, "Ed25519 public key does not match private key" unless @sign_key.raw_public_key == pk
|
|
238
|
+
|
|
239
|
+
@public_key_bytes = pk.dup.force_encoding('BINARY')
|
|
240
|
+
@private_key_bytes = private_key_bytes.dup.force_encoding('BINARY')
|
|
165
241
|
end
|
|
166
242
|
|
|
167
243
|
def to_blob
|
|
@@ -177,11 +253,23 @@ module Net
|
|
|
177
253
|
end
|
|
178
254
|
|
|
179
255
|
def public_key
|
|
180
|
-
PubKey.new(
|
|
256
|
+
PubKey.new(public_key_bytes)
|
|
181
257
|
end
|
|
182
258
|
|
|
183
259
|
def ssh_do_sign(data, sig_alg = nil)
|
|
184
|
-
|
|
260
|
+
ED25519.sign(@sign_key, data)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def public_key_bytes
|
|
264
|
+
@public_key_bytes.dup
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def private_key_bytes
|
|
268
|
+
@private_key_bytes.dup
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def private_key_bytes_for_agent
|
|
272
|
+
private_key_bytes + public_key_bytes
|
|
185
273
|
end
|
|
186
274
|
|
|
187
275
|
def self.read(data, password)
|
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
module Net
|
|
2
2
|
module SSH
|
|
3
3
|
module Authentication
|
|
4
|
-
# Loads ED25519 support
|
|
5
|
-
# ed25519, bcrypt_pbkdf
|
|
4
|
+
# Loads ED25519 support backed by Ruby OpenSSL raw key APIs.
|
|
6
5
|
module ED25519Loader
|
|
7
6
|
begin
|
|
8
7
|
require 'net/ssh/authentication/ed25519'
|
|
8
|
+
Net::SSH::Authentication::ED25519.ensure_supported!
|
|
9
9
|
LOADED = true
|
|
10
10
|
ERROR = nil
|
|
11
11
|
rescue LoadError => e
|
|
12
12
|
ERROR = e
|
|
13
13
|
LOADED = false
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
if defined?(Net::SSH::Authentication::ED25519::UnsupportedError) &&
|
|
16
|
+
e.is_a?(Net::SSH::Authentication::ED25519::UnsupportedError)
|
|
17
|
+
ERROR = e
|
|
18
|
+
LOADED = false
|
|
19
|
+
else
|
|
20
|
+
raise
|
|
21
|
+
end
|
|
14
22
|
end
|
|
15
23
|
|
|
16
24
|
def self.raiseUnlessLoaded(message)
|
|
17
|
-
description =
|
|
18
|
-
description
|
|
25
|
+
description = LOADED ? '' : ed25519SupportRequired
|
|
26
|
+
description += "#{ERROR.class} : \"#{ERROR.message}\"\n" if ERROR
|
|
19
27
|
raise NotImplementedError, "#{message}\n#{description}" unless LOADED
|
|
20
28
|
end
|
|
21
29
|
|
|
22
|
-
def self.
|
|
23
|
-
|
|
24
|
-
result << " * ed25519 (>= 1.2, < 2.0)\n"
|
|
25
|
-
result << " * bcrypt_pbkdf (>= 1.0, < 2.0)\n" unless RUBY_PLATFORM == "java"
|
|
26
|
-
result << "See https://github.com/net-ssh/net-ssh/issues/565 for more information\n"
|
|
30
|
+
def self.ed25519SupportRequired
|
|
31
|
+
"net-ssh requires openssl >= 3.2.0 and a crypto backend with Ed25519 enabled for ed25519 support.\n"
|
|
27
32
|
end
|
|
28
33
|
end
|
|
29
34
|
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 || 'invalid')
|
|
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,9 +138,8 @@ 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.
|
|
142
141
|
def self.read(key_data, passphrase)
|
|
143
|
-
open_ssl_class.new(key_data, passphrase
|
|
142
|
+
open_ssl_class.new(key_data, passphrase)
|
|
144
143
|
end
|
|
145
144
|
|
|
146
145
|
def self.encrypted_key?(key_data, error)
|
|
@@ -150,7 +149,7 @@ module Net
|
|
|
150
149
|
|
|
151
150
|
class OpenSSLPKeyType < OpenSSLKeyTypeBase
|
|
152
151
|
def self.read(key_data, passphrase)
|
|
153
|
-
open_ssl_class.read(key_data, passphrase
|
|
152
|
+
open_ssl_class.read(key_data, passphrase)
|
|
154
153
|
end
|
|
155
154
|
|
|
156
155
|
def self.open_ssl_class
|