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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d9097275f7c3c0123783d9c29f8c4c4bfe3a3c8d68410357078e91859950f3f
4
- data.tar.gz: b5a1013faa8b1cfbe5645e849329c99b62db9d944894287747a12c33f5356d9a
3
+ metadata.gz: ee72c6673cb55fe69cdb90bc49d0b7266b29cb80f7a14794f873e048075cde2b
4
+ data.tar.gz: b9416f51d2b80d5d0bbbcac1b33a4272d5b5add59d81421ca8eaa84db2fd0823
5
5
  SHA512:
6
- metadata.gz: 04ceefc617349311ddceebb606aa003dec8c6aa671363a13f939486d1bdfa5b55c49a56096651d8667f30f57ea109728e3cdae1f25545aa3c1c892b1db2948bf
7
- data.tar.gz: 3a9f23ad8a0843628f5dc500c83ceabca815756f925fbddc9725a12e0e19a9eeacf42f26f924b9c038edf8e95f832720c2297b0d0c8fdfcd6844f94d72f834b9
6
+ metadata.gz: 4f1d529b4af06145b892c6ece94d2fcc6a9d2b65b93db633189b71988b8bc03aa64feffdfce3509ce64809378ff542b04f01381419d5a9e11246c7303d258583
7
+ data.tar.gz: 2170524d2a24e1ec8d0c07c3237d140ad00c412c59875f6f178b6f4a2c47ce9d16f8418dee56e0425d8ec8a214f50e0cc96e434d1b6e37998125e1cda9ebb41e
checksums.yaml.gz.sig CHANGED
Binary file
@@ -17,9 +17,6 @@ jobs:
17
17
  - name: Build docker images
18
18
  run: docker compose build
19
19
 
20
- - name: Run ruby 2.6
21
- run: docker compose run ruby-2.6
22
-
23
20
  - name: Run ruby 2.7
24
21
  run: docker compose run ruby-2.7
25
22
 
@@ -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, 3.4]
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 ${{ (startsWith(matrix.ruby-version, '2.6.') || startsWith(matrix.ruby-version, '2.7.')) && '-v 2.4.22' || '' }}
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
@@ -4,7 +4,7 @@ AllCops:
4
4
  - "vendor/**/.*"
5
5
  - "vendor/**/*"
6
6
  NewCops: enable
7
- TargetRubyVersion: 2.6
7
+ TargetRubyVersion: 2.7
8
8
 
9
9
  inherit_from: .rubocop_todo.yml
10
10
 
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
@@ -26,4 +26,4 @@ RUN gem install bundler ${BUNDLERV} && bundle install
26
26
 
27
27
  COPY . $INSTALL_PATH/
28
28
 
29
- CMD service ssh start && rake test && NET_SSH_NO_ED25519=1 rake test
29
+ CMD service ssh start && rake test
data/Dockerfile.openssl3 CHANGED
@@ -2,7 +2,7 @@ FROM ubuntu:22.04
2
2
 
3
3
  ENV INSTALL_PATH="/netssh"
4
4
 
5
- RUN apt update && apt install -y openssl ruby ruby-dev git build-essential
5
+ RUN apt update && apt install -y openssl libssl-dev ruby ruby-dev git build-essential
6
6
 
7
7
  WORKDIR $INSTALL_PATH
8
8
 
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 | Require the gem `ed25519` |
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 | Require the gem `x25519` |
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. | Requires the gem `rbnacl` |
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
- The only requirement you might be missing is the OpenSSL bindings for Ruby with a version greather than `1.0.1`.
152
- These are built by default on most platforms, but you can verify that they're built and installed on your system by running the following command line:
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 that spits out something like `OpenSSL 1.0.1 14 Mar 2012`, then you're set.
159
- If you get an error, then you'll need to see about rebuilding ruby with OpenSSL support,
160
- or (if your platform supports it) installing the OpenSSL bindings separately.
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
- For ed25519 public key auth support your bundle file should contain `ed25519`, `bcrypt_pbkdf` dependencies.
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: "jruby-9.1.2.0"
8
- - ruby_version: "26-x64"
9
- - ruby_version: "25-x64"
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 1.17
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
@@ -13,11 +13,5 @@ services:
13
13
  build:
14
14
  context: .
15
15
  args:
16
- RUBY_VERSION: 2.7
16
+ RUBY_VERSION: 2.7.8
17
17
  BUNDLERV: "-v 2.2.28"
18
- ruby-2.6:
19
- build:
20
- context: .
21
- args:
22
- RUBY_VERSION: 2.6
23
- BUNDLERV: "-v 2.4.22"
@@ -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.public_key.verify_key.to_bytes,
265
- :string, priv_key.sign_key.keypair).to_s
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.public_key.verify_key.to_bytes,
270
- :string, priv_key.key.sign_key.keypair).to_s
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
- gem 'ed25519', '~> 1.2'
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
- class SigningKeyFromFile < SimpleDelegator
15
- def initialize(pk, sk)
16
- key = ::Ed25519::SigningKey.from_keypair(sk)
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
- super(key)
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
- ((len < blocksize) || ((blocksize > 0) && (len % blocksize) != 0))
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
- raise "BCryptPbkdf is not implemented for jruby" if RUBY_PLATFORM == "java"
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 = '\x00' * (keylen + ivlen)
115
+ key = "\x00" * (keylen + ivlen)
81
116
  end
82
117
 
83
- cipher = CipherFactory.get(ciphername, key: key[0...keylen], iv: key[keylen...keylen + ivlen], decrypt: true)
84
-
85
- decoded = cipher.update(buffer.remainder_as_buffer.to_s)
86
- decoded << cipher.final
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
- @verify_key = ::Ed25519::VerifyKey.new(data)
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, @verify_key.to_bytes).to_s
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
- @verify_key.verify(sig, data)
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 + [@verify_key.to_bytes].pack("m")
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
- @pk = pk
154
- @sign_key = SigningKeyFromFile.new(pk, sk)
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(@pk)
256
+ PubKey.new(public_key_bytes)
171
257
  end
172
258
 
173
259
  def ssh_do_sign(data, sig_alg = nil)
174
- @sign_key.sign(data)
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 which requires optinal dependecies like
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 = ERROR.is_a?(LoadError) ? dependenciesRequiredForED25519 : +''
18
- description << "#{ERROR.class} : \"#{ERROR.message}\"\n" if ERROR
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.dependenciesRequiredForED25519
23
- result = +"net-ssh requires the following gems for ed25519 support:\n"
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
@@ -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