net-ssh 7.3.3 → 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 +3 -0
- data/CHANGES.txt +4 -0
- data/Dockerfile +1 -1
- 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 -36
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/known_hosts.rb +42 -0
- 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/verifiers/always.rb +43 -8
- 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 +2 -2
- 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'
|
|
@@ -758,10 +759,12 @@ Style/OpenStructUse:
|
|
|
758
759
|
Exclude:
|
|
759
760
|
- 'test/authentication/test_ed25519.rb'
|
|
760
761
|
- 'test/common.rb'
|
|
762
|
+
- 'test/test_known_hosts.rb'
|
|
761
763
|
- 'test/transport/kex/test_curve25519_sha256.rb'
|
|
762
764
|
- 'test/transport/kex/test_diffie_hellman_group1_sha1.rb'
|
|
763
765
|
- 'test/transport/kex/test_ecdh_sha2_nistp256.rb'
|
|
764
766
|
- 'test/verifiers/test_always.rb'
|
|
767
|
+
- 'test/verifiers/test_cert_authority_checks.rb'
|
|
765
768
|
|
|
766
769
|
# Offense count: 16
|
|
767
770
|
# Configuration parameters: AllowedMethods.
|
data/CHANGES.txt
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
=== 8.0.0 beta3
|
|
2
|
+
|
|
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]
|
|
4
|
+
|
|
1
5
|
=== 7.3.3
|
|
2
6
|
|
|
3
7
|
* Support .pub file as IdentityFile to filter/prioritize agent keys (1Password SSH agent use case) [#942]
|
data/Dockerfile
CHANGED
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 zlib-ng crypto-policies-scripts && \
|
|
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
|
|
21
26
|
end
|
|
22
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
|
|
48
|
+
end
|
|
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
|
|
|
@@ -47,7 +79,7 @@ module Net
|
|
|
47
79
|
data = datab64.unpack1("m")
|
|
48
80
|
raise ArgumentError.new("Expected #{MAGIC} at start of decoded private key") unless data.start_with?(MAGIC)
|
|
49
81
|
|
|
50
|
-
buffer = Net::SSH::Buffer.new(data[MAGIC.size + 1..-1])
|
|
82
|
+
buffer = Net::SSH::Buffer.new(data[(MAGIC.size + 1)..-1])
|
|
51
83
|
|
|
52
84
|
ciphername = buffer.read_string
|
|
53
85
|
raise ArgumentError.new("#{ciphername} in private key is not supported") unless
|
|
@@ -63,27 +95,34 @@ module Net
|
|
|
63
95
|
_pubkey = buffer.read_string
|
|
64
96
|
|
|
65
97
|
len = buffer.read_long
|
|
98
|
+
encrypted_private = buffer.read(len)
|
|
66
99
|
|
|
67
100
|
keylen, blocksize, ivlen = CipherFactory.get_lengths(ciphername, iv_len: true)
|
|
68
101
|
raise ArgumentError.new("Private key len:#{len} is not a multiple of #{blocksize}") if
|
|
69
|
-
(
|
|
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
|
|
70
108
|
|
|
71
109
|
if kdfname == 'bcrypt'
|
|
72
110
|
salt = kdfopts.read_string
|
|
73
111
|
rounds = kdfopts.read_long
|
|
74
112
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
key = BCryptPbkdf::key(password, salt, keylen + ivlen, rounds)
|
|
78
|
-
raise DecryptError.new("BCyryptPbkdf failed", encrypted_key: true) unless key
|
|
113
|
+
key = bcrypt_pbkdf_key(password, salt, keylen + ivlen, rounds)
|
|
79
114
|
else
|
|
80
|
-
key =
|
|
115
|
+
key = "\x00" * (keylen + ivlen)
|
|
81
116
|
end
|
|
82
117
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
)
|
|
87
126
|
|
|
88
127
|
decoded = Net::SSH::Buffer.new(decoded)
|
|
89
128
|
check1 = decoded.read_long
|
|
@@ -99,15 +138,49 @@ module Net
|
|
|
99
138
|
decoded.read_private_keyblob(type_name)
|
|
100
139
|
end
|
|
101
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
|
|
102
176
|
end
|
|
103
177
|
|
|
104
178
|
class PubKey
|
|
105
179
|
include Net::SSH::Authentication::PubKeyFingerprint
|
|
106
180
|
|
|
107
|
-
attr_reader :verify_key
|
|
108
|
-
|
|
109
181
|
def initialize(data)
|
|
110
|
-
|
|
182
|
+
ED25519.validate_key_bytes!(data, "public key", KEY_BYTES)
|
|
183
|
+
@public_key_bytes = data.dup.force_encoding('BINARY')
|
|
111
184
|
end
|
|
112
185
|
|
|
113
186
|
def self.read_keyblob(buffer)
|
|
@@ -115,7 +188,7 @@ module Net
|
|
|
115
188
|
end
|
|
116
189
|
|
|
117
190
|
def to_blob
|
|
118
|
-
Net::SSH::Buffer.from(:mstring, "ssh-ed25519".dup, :string,
|
|
191
|
+
Net::SSH::Buffer.from(:mstring, "ssh-ed25519".dup, :string, public_key_bytes).to_s
|
|
119
192
|
end
|
|
120
193
|
|
|
121
194
|
def ssh_type
|
|
@@ -127,22 +200,26 @@ module Net
|
|
|
127
200
|
end
|
|
128
201
|
|
|
129
202
|
def ssh_do_verify(sig, data, options = {})
|
|
130
|
-
|
|
203
|
+
ED25519.verify(verify_key, sig, data)
|
|
131
204
|
end
|
|
132
205
|
|
|
133
206
|
def to_pem
|
|
134
207
|
# TODO this is not pem
|
|
135
|
-
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)
|
|
136
217
|
end
|
|
137
218
|
end
|
|
138
219
|
|
|
139
220
|
class PrivKey
|
|
140
221
|
CipherFactory = Net::SSH::Transport::CipherFactory
|
|
141
222
|
|
|
142
|
-
MBEGIN = "-----BEGIN OPENSSH PRIVATE KEY-----\n"
|
|
143
|
-
MEND = "-----END OPENSSH PRIVATE KEY-----\n"
|
|
144
|
-
MAGIC = "openssh-key-v1"
|
|
145
|
-
|
|
146
223
|
attr_reader :sign_key
|
|
147
224
|
|
|
148
225
|
def initialize(buffer)
|
|
@@ -150,8 +227,17 @@ module Net
|
|
|
150
227
|
sk = buffer.read_string
|
|
151
228
|
_comment = buffer.read_string
|
|
152
229
|
|
|
153
|
-
|
|
154
|
-
|
|
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')
|
|
155
241
|
end
|
|
156
242
|
|
|
157
243
|
def to_blob
|
|
@@ -167,11 +253,23 @@ module Net
|
|
|
167
253
|
end
|
|
168
254
|
|
|
169
255
|
def public_key
|
|
170
|
-
PubKey.new(
|
|
256
|
+
PubKey.new(public_key_bytes)
|
|
171
257
|
end
|
|
172
258
|
|
|
173
259
|
def ssh_do_sign(data, sig_alg = nil)
|
|
174
|
-
|
|
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
|
|
175
273
|
end
|
|
176
274
|
|
|
177
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/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
|