net-ssh 7.3.6 → 8.0.2

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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/pull_request_template.md +1 -0
  4. data/.github/workflows/changelog.yml +43 -0
  5. data/.github/workflows/ci-with-docker.yml +0 -3
  6. data/.github/workflows/ci.yml +2 -17
  7. data/.github/workflows/rubocop.yml +3 -1
  8. data/.rubocop.yml +5 -1
  9. data/.rubocop_todo.yml +1 -7
  10. data/CHANGES.txt +82 -7
  11. data/CONTRIBUTING.md +35 -0
  12. data/Dockerfile +3 -3
  13. data/Dockerfile.openssl3 +1 -1
  14. data/Dockerfile.zlib_ng +1 -1
  15. data/README.md +71 -57
  16. data/Rakefile +66 -12
  17. data/appveyor.yml +4 -16
  18. data/changelog.d/README.md +1 -0
  19. data/docker-compose.yml +1 -7
  20. data/lib/net/ssh/authentication/agent.rb +35 -11
  21. data/lib/net/ssh/authentication/ed25519.rb +137 -44
  22. data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
  23. data/lib/net/ssh/authentication/key_manager.rb +22 -11
  24. data/lib/net/ssh/authentication/methods/abstract.rb +2 -8
  25. data/lib/net/ssh/authentication/methods/publickey.rb +3 -1
  26. data/lib/net/ssh/authentication/security_key.rb +71 -0
  27. data/lib/net/ssh/authentication/session.rb +29 -17
  28. data/lib/net/ssh/buffer.rb +6 -1
  29. data/lib/net/ssh/buffered_io.rb +40 -3
  30. data/lib/net/ssh/config.rb +28 -12
  31. data/lib/net/ssh/connection/channel.rb +49 -2
  32. data/lib/net/ssh/connection/session.rb +58 -8
  33. data/lib/net/ssh/errors.rb +24 -6
  34. data/lib/net/ssh/key_factory.rb +7 -1
  35. data/lib/net/ssh/known_hosts.rb +18 -8
  36. data/lib/net/ssh/multiplex/client.rb +162 -0
  37. data/lib/net/ssh/multiplex/control_path.rb +120 -0
  38. data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
  39. data/lib/net/ssh/multiplex.rb +126 -0
  40. data/lib/net/ssh/prompt.rb +3 -3
  41. data/lib/net/ssh/proxy/jump.rb +40 -2
  42. data/lib/net/ssh/service/forward.rb +35 -24
  43. data/lib/net/ssh/test.rb +2 -1
  44. data/lib/net/ssh/transport/algorithms.rb +88 -33
  45. data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
  46. data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
  47. data/lib/net/ssh/transport/cipher_factory.rb +23 -1
  48. data/lib/net/ssh/transport/constants.rb +1 -0
  49. data/lib/net/ssh/transport/gcm_cipher.rb +27 -6
  50. data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
  51. data/lib/net/ssh/transport/hmac.rb +2 -0
  52. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
  53. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
  54. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
  55. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
  56. data/lib/net/ssh/transport/openssl.rb +2 -1
  57. data/lib/net/ssh/transport/packet_stream.rb +12 -0
  58. data/lib/net/ssh/transport/session.rb +43 -45
  59. data/lib/net/ssh/verifiers/always.rb +0 -3
  60. data/lib/net/ssh/version.rb +3 -3
  61. data/lib/net/ssh.rb +141 -58
  62. data/net-ssh.gemspec +5 -8
  63. data/rakelib/changelog.rb +229 -0
  64. data.tar.gz.sig +0 -0
  65. metadata +37 -36
  66. metadata.gz.sig +0 -0
  67. data/Gemfile.noed25519 +0 -12
  68. data/Gemfile.norbnacl +0 -12
data/Rakefile CHANGED
@@ -9,7 +9,11 @@ require "rake"
9
9
  require "rake/clean"
10
10
  require "bundler/gem_tasks"
11
11
 
12
- require "rdoc/task"
12
+ begin
13
+ require "rdoc/task"
14
+ rescue LoadError
15
+ # rdoc is no longer a default gem as of Ruby 4.0; the rest of the Rakefile works without it.
16
+ end
13
17
 
14
18
  desc "When releasing make sure NET_SSH_BUILDGEM_SIGNED is set"
15
19
  task :check_NET_SSH_BUILDGEM_SIGNED do
@@ -24,20 +28,23 @@ CLEAN.include ['pkg', 'rdoc']
24
28
  name = "net-ssh"
25
29
 
26
30
  require_relative "lib/net/ssh/version"
31
+ require_relative "rakelib/changelog"
27
32
  version = Net::SSH::Version::CURRENT
28
33
 
29
34
  extra_files = %w[LICENSE.txt THANKS.txt CHANGES.txt]
30
- RDoc::Task.new do |rdoc|
31
- rdoc.rdoc_dir = "rdoc"
32
- rdoc.title = "#{name} #{version}"
33
- rdoc.generator = 'hanna' # gem install hanna-nouveau
34
- rdoc.main = 'README.md'
35
- rdoc.rdoc_files.include("README*")
36
- rdoc.rdoc_files.include("bin/*.rb")
37
- rdoc.rdoc_files.include("lib/**/*.rb")
38
- extra_files.each { |file|
39
- rdoc.rdoc_files.include(file) if File.exist?(file)
40
- }
35
+ if defined?(RDoc::Task)
36
+ RDoc::Task.new do |rdoc|
37
+ rdoc.rdoc_dir = "rdoc"
38
+ rdoc.title = "#{name} #{version}"
39
+ rdoc.generator = 'hanna' # gem install hanna-nouveau
40
+ rdoc.main = 'README.md'
41
+ rdoc.rdoc_files.include("README*")
42
+ rdoc.rdoc_files.include("bin/*.rb")
43
+ rdoc.rdoc_files.include("lib/**/*.rb")
44
+ extra_files.each { |file|
45
+ rdoc.rdoc_files.include(file) if File.exist?(file)
46
+ }
47
+ end
41
48
  end
42
49
 
43
50
  namespace :cert do
@@ -103,6 +110,15 @@ def change_version(&block)
103
110
  raise ArgumentError, "Cound not find line: TINY = \"#{tiny}\" in #{version_file}" unless found[:tiny] || new_tiny == tiny
104
111
  end
105
112
 
113
+ begin
114
+ heading = Changelog.heading(Net::SSH::Version::MAJOR, Net::SSH::Version::MINOR, new_tiny, new_pre)
115
+ section = Changelog.new(__dir__).release(heading)
116
+ rescue StandardError
117
+ FileUtils.rm_f "#{version_file}.new"
118
+ raise
119
+ end
120
+ puts section ? "Added to CHANGES.txt:\n#{section}" : "Nothing to release for a pre-release, CHANGES.txt unchanged"
121
+
106
122
  FileUtils.mv version_file, "#{version_file}.old"
107
123
  FileUtils.mv "#{version_file}.new", version_file
108
124
  end
@@ -142,6 +158,44 @@ namespace :vbump do
142
158
  end
143
159
  end
144
160
 
161
+ namespace :changelog do
162
+ changelog = Changelog.new(__dir__)
163
+
164
+ desc "Validate the fragments in changelog.d/"
165
+ task :check do
166
+ count = changelog.check.size
167
+ puts "#{count} valid changelog fragment#{'s' unless count == 1}"
168
+ rescue Changelog::Error => e
169
+ abort e.message
170
+ end
171
+
172
+ desc "Print the CHANGES.txt section the next version bump would write"
173
+ task :preview, [:heading] do |_t, args|
174
+ puts changelog.section(args[:heading] || "Unreleased") || "Nothing to release"
175
+ rescue Changelog::Error => e
176
+ abort e.message
177
+ end
178
+
179
+ desc "Create a fragment: rake \"changelog:new[1234,change,Text of the entry]\""
180
+ task :new, [:number, :type] do |_t, args|
181
+ abort "Usage: rake \"changelog:new[<PR number>,<#{Changelog::TYPES.keys.join('|')}>,<text>]\"" unless args[:number] && args[:type]
182
+ # Rake splits arguments on commas and strips the spaces around them, so take the text from the raw task string.
183
+ raw = Rake.application.top_level_tasks.find { |t| t.start_with?("changelog:new[") }
184
+ text = raw&.[](/\A[^\[]*\[[^,]*,[^,]*,\s*(.*)\]\z/m, 1) || args.extras.join(", ")
185
+ puts "Created #{changelog.new_fragment(args[:number], args[:type], text)}"
186
+ rescue Changelog::Error => e
187
+ abort e.message
188
+ end
189
+
190
+ desc "Write the fragments to CHANGES.txt under a heading, e.g. rake \"changelog:release[7.3.4]\" (vbump does this)"
191
+ task :release, [:heading] do |_t, args|
192
+ abort "Usage: rake \"changelog:release[<version heading>]\"" unless args[:heading]
193
+ puts changelog.release(args[:heading]) || "Nothing to release"
194
+ rescue Changelog::Error => e
195
+ abort e.message
196
+ end
197
+ end
198
+
145
199
  namespace :rdoc do
146
200
  desc "Update gh-pages branch"
147
201
  task :publish do
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
 
@@ -0,0 +1 @@
1
+ Changelog fragments: one `<PR number>.<type>.txt` file per user-visible change, assembled into `CHANGES.txt` when the version is bumped. See [CONTRIBUTING.md](../CONTRIBUTING.md#changelog).
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"
@@ -17,6 +17,9 @@ module Net
17
17
  # An exception for indicating that the SSH agent is not available.
18
18
  class AgentNotAvailable < AgentError; end
19
19
 
20
+ # Raised when the agent closes the connection before replying.
21
+ class AgentClosedConnection < AgentError; end
22
+
20
23
  # This class implements a simple client for the ssh-agent protocol. It
21
24
  # does not implement any specific protocol, but instead copies the
22
25
  # behavior of the ssh-agent functions in the OpenSSH library (3.8).
@@ -83,12 +86,18 @@ module Net
83
86
  # socket reports that it is an SSH2-compatible agent, this will fail
84
87
  # (it only supports the ssh-agent distributed by OpenSSH).
85
88
  def connect!(agent_socket_factory = nil, identity_agent = nil)
89
+ @agent_socket_factory = agent_socket_factory
90
+ @identity_agent = identity_agent
91
+ open_agent_socket!
92
+ end
93
+
94
+ def open_agent_socket!
86
95
  debug { "connecting to ssh-agent" }
87
96
  @socket =
88
- if agent_socket_factory
89
- agent_socket_factory.call
90
- elsif identity_agent
91
- unix_socket_class.open(File.expand_path(identity_agent))
97
+ if @agent_socket_factory
98
+ @agent_socket_factory.call
99
+ elsif @identity_agent
100
+ unix_socket_class.open(File.expand_path(@identity_agent))
92
101
  elsif ENV['SSH_AUTH_SOCK'] && unix_socket_class
93
102
  unix_socket_class.open(File.expand_path(ENV['SSH_AUTH_SOCK']))
94
103
  elsif Gem.win_platform? && RUBY_ENGINE != "jruby"
@@ -100,6 +109,7 @@ module Net
100
109
  error { "could not connect to ssh-agent: #{e.message}" }
101
110
  raise AgentNotAvailable, $!.message
102
111
  end
112
+ private :open_agent_socket!
103
113
 
104
114
  # Attempts to negotiate the SSH agent protocol version. Raises an error
105
115
  # if the version could not be negotiated successfully.
@@ -114,6 +124,11 @@ module Net
114
124
  elsif type != SSH_AGENT_RSA_IDENTITIES_ANSWER1 && type != SSH_AGENT_RSA_IDENTITIES_ANSWER2
115
125
  raise AgentNotAvailable, "unknown response from agent: #{type}, #{body.to_s.inspect}"
116
126
  end
127
+ rescue AgentClosedConnection
128
+ # Agents built on ssh-agent-lib <= 0.6.0 (e.g. older Proton Pass) drop the connection on
129
+ # this legacy request but handle the rest of the protocol (#1008).
130
+ debug { "ssh-agent closed the connection on the version request; assuming a modern agent and reconnecting" }
131
+ open_agent_socket!
117
132
  end
118
133
 
119
134
  # Return an array of all identities (public keys) known to the agent.
@@ -137,7 +152,7 @@ module Net
137
152
  key.extend(Comment)
138
153
  key.comment = comment_str
139
154
  identities.push key
140
- rescue NotImplementedError => e
155
+ rescue Net::SSH::UnsupportedKeyType, NotImplementedError => e
141
156
  error { "ignoring unimplemented key:#{e.message} #{comment_str}" }
142
157
  end
143
158
  end
@@ -223,8 +238,14 @@ module Net
223
238
  # tuple consisting of the packet type, and the packet's body (which
224
239
  # is returned as a Net::SSH::Buffer).
225
240
  def read_packet
226
- buffer = Net::SSH::Buffer.new(@socket.read(4))
227
- buffer.append(@socket.read(buffer.read_long))
241
+ header = @socket.read(4)
242
+ raise AgentClosedConnection, "agent closed the connection" if header.nil?
243
+
244
+ buffer = Net::SSH::Buffer.new(header)
245
+ body = @socket.read(buffer.read_long)
246
+ raise AgentClosedConnection, "agent closed the connection" if body.nil?
247
+
248
+ buffer.append(body)
228
249
  type = buffer.read_byte
229
250
  debug { "received agent packet #{type} len #{buffer.length - 4}" }
230
251
  return type, buffer
@@ -235,6 +256,9 @@ module Net
235
256
  def send_and_wait(type, *args)
236
257
  send_packet(type, *args)
237
258
  read_packet
259
+ rescue Errno::ECONNRESET, Errno::EPIPE => e
260
+ # Linux reports an agent that closes with our request unread as a reset, not EOF.
261
+ raise AgentClosedConnection, "agent closed the connection (#{e.class})"
238
262
  end
239
263
 
240
264
  # Returns +true+ if the parameter indicates a "failure" response from
@@ -261,13 +285,13 @@ module Net
261
285
  when /^ecdsa\-sha2\-(\w*)-cert-v01@openssh\.com$/
262
286
  Net::SSH::Buffer.from(:string, priv_key.to_blob, :bignum, priv_key.key.private_key).to_s
263
287
  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
288
+ Net::SSH::Buffer.from(:string, priv_key.public_key_bytes,
289
+ :string, priv_key.private_key_bytes_for_agent).to_s
266
290
  when /^ssh-ed25519-cert-v01@openssh\.com$/
267
291
  # Unlike the other certificate types, the public key is included after the certifiate.
268
292
  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
293
+ :string, priv_key.key.public_key_bytes,
294
+ :string, priv_key.key.private_key_bytes_for_agent).to_s
271
295
  when /^ssh-rsa$/
272
296
  # `n` and `e` are reversed compared to the ordering in `OpenSSL::PKey::RSA#to_blob`.
273
297
  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
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
 
@@ -41,13 +73,8 @@ module Net
41
73
  end
42
74
  end
43
75
 
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
76
  def self.read(datafull, password)
50
- datafull = datafull.strip
77
+ datafull = datafull.strip.encode(universal_newline: true)
51
78
  raise ArgumentError.new("Expected #{MBEGIN} at start of private key") unless datafull.start_with?(MBEGIN)
52
79
  raise ArgumentError.new("Expected #{MEND} at end of private key") unless datafull.end_with?(MEND)
53
80
 
@@ -55,7 +82,7 @@ module Net
55
82
  data = datab64.unpack1("m")
56
83
  raise ArgumentError.new("Expected #{MAGIC} at start of decoded private key") unless data.start_with?(MAGIC)
57
84
 
58
- buffer = Net::SSH::Buffer.new(data[MAGIC.size + 1..-1])
85
+ buffer = Net::SSH::Buffer.new(data[(MAGIC.size + 1)..-1])
59
86
 
60
87
  ciphername = buffer.read_string
61
88
  raise ArgumentError.new("#{ciphername} in private key is not supported") unless
@@ -71,29 +98,35 @@ module Net
71
98
  _pubkey = buffer.read_string
72
99
 
73
100
  len = buffer.read_long
101
+ encrypted_private = buffer.read(len)
74
102
 
75
103
  keylen, blocksize, ivlen = CipherFactory.get_lengths(ciphername, iv_len: true)
76
104
  raise ArgumentError.new("Private key len:#{len} is not a multiple of #{blocksize}") if
77
- ((len < blocksize) || ((blocksize > 0) && (len % blocksize) != 0))
105
+ (len < blocksize) || ((blocksize > 0) && (len % blocksize) != 0)
106
+
107
+ raise ArgumentError.new("Private key len:#{len} exceeds available data") unless encrypted_private.bytesize == len
108
+
109
+ authlen = CipherFactory.auth_length(ciphername)
110
+ auth_tag = authlen > 0 ? buffer.read(authlen) : nil
78
111
 
79
112
  if kdfname == 'bcrypt'
80
113
  salt = kdfopts.read_string
81
114
  rounds = kdfopts.read_long
115
+ raise ArgumentError.new("Private key bcrypt rounds #{rounds} exceeds #{MAX_KDF_ROUNDS}") if rounds > MAX_KDF_ROUNDS
82
116
 
83
- raise "BCryptPbkdf is not implemented for jruby" if RUBY_PLATFORM == "java"
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
117
+ key = bcrypt_pbkdf_key(password, salt, keylen + ivlen, rounds)
89
118
  else
90
- key = '\x00' * (keylen + ivlen)
119
+ key = "\x00" * (keylen + ivlen)
91
120
  end
92
121
 
93
- cipher = CipherFactory.get(ciphername, key: key[0...keylen], iv: key[keylen...keylen + ivlen], decrypt: true)
94
-
95
- decoded = cipher.update(buffer.remainder_as_buffer.to_s)
96
- decoded << cipher.final
122
+ decoded = decrypt_private_key(
123
+ ciphername,
124
+ encrypted_private,
125
+ auth_tag,
126
+ key: key[0...keylen],
127
+ iv: key[keylen...(keylen + ivlen)],
128
+ encrypted_key: kdfname == 'bcrypt'
129
+ )
97
130
 
98
131
  decoded = Net::SSH::Buffer.new(decoded)
99
132
  check1 = decoded.read_long
@@ -109,15 +142,50 @@ module Net
109
142
  decoded.read_private_keyblob(type_name)
110
143
  end
111
144
  end
145
+
146
+ def self.bcrypt_pbkdf_key(password, salt, length, rounds)
147
+ begin
148
+ require_bcrypt_pbkdf
149
+ rescue LoadError
150
+ raise DecryptError.new("bcrypt_pbkdf is required to decrypt bcrypt-encrypted OpenSSH private keys")
151
+ end
152
+ raise DecryptError.new("Passphrase required for encrypted private key", encrypted_key: true) if password.nil? || password.empty?
153
+
154
+ key = BCryptPbkdf::key(password, salt, length, rounds)
155
+ raise DecryptError.new("BCryptPbkdf failed", encrypted_key: true) unless key
156
+
157
+ key
158
+ end
159
+
160
+ def self.require_bcrypt_pbkdf
161
+ require 'bcrypt_pbkdf'
162
+ end
163
+
164
+ def self.decrypt_private_key(ciphername, encrypted_private, auth_tag, options)
165
+ if auth_tag
166
+ return CipherFactory.decrypt_private_key(
167
+ ciphername,
168
+ encrypted_private,
169
+ auth_tag,
170
+ options[:key],
171
+ options[:iv]
172
+ )
173
+ end
174
+
175
+ cipher = CipherFactory.get(ciphername, key: options[:key], iv: options[:iv], decrypt: true)
176
+ decoded = cipher.update(encrypted_private)
177
+ decoded << cipher.final
178
+ rescue Net::SSH::Exception, OpenSSL::Cipher::CipherError
179
+ raise DecryptError.new("Decrypt failed on private key", encrypted_key: options[:encrypted_key])
180
+ end
112
181
  end
113
182
 
114
183
  class PubKey
115
184
  include Net::SSH::Authentication::PubKeyFingerprint
116
185
 
117
- attr_reader :verify_key
118
-
119
186
  def initialize(data)
120
- @verify_key = ::Ed25519::VerifyKey.new(data)
187
+ ED25519.validate_key_bytes!(data, "public key", KEY_BYTES)
188
+ @public_key_bytes = data.dup.force_encoding('BINARY')
121
189
  end
122
190
 
123
191
  def self.read_keyblob(buffer)
@@ -125,7 +193,7 @@ module Net
125
193
  end
126
194
 
127
195
  def to_blob
128
- Net::SSH::Buffer.from(:mstring, "ssh-ed25519".dup, :string, @verify_key.to_bytes).to_s
196
+ Net::SSH::Buffer.from(:mstring, "ssh-ed25519".dup, :string, public_key_bytes).to_s
129
197
  end
130
198
 
131
199
  def ssh_type
@@ -137,22 +205,26 @@ module Net
137
205
  end
138
206
 
139
207
  def ssh_do_verify(sig, data, options = {})
140
- @verify_key.verify(sig, data)
208
+ ED25519.verify(verify_key, sig, data)
141
209
  end
142
210
 
143
211
  def to_pem
144
212
  # TODO this is not pem
145
- ssh_type + [@verify_key.to_bytes].pack("m")
213
+ ssh_type + [public_key_bytes].pack("m")
214
+ end
215
+
216
+ def public_key_bytes
217
+ @public_key_bytes.dup
218
+ end
219
+
220
+ def verify_key
221
+ @verify_key ||= ED25519.new_public_key(@public_key_bytes)
146
222
  end
147
223
  end
148
224
 
149
225
  class PrivKey
150
226
  CipherFactory = Net::SSH::Transport::CipherFactory
151
227
 
152
- MBEGIN = "-----BEGIN OPENSSH PRIVATE KEY-----\n"
153
- MEND = "-----END OPENSSH PRIVATE KEY-----\n"
154
- MAGIC = "openssh-key-v1"
155
-
156
228
  attr_reader :sign_key
157
229
 
158
230
  def initialize(buffer)
@@ -160,8 +232,17 @@ module Net
160
232
  sk = buffer.read_string
161
233
  _comment = buffer.read_string
162
234
 
163
- @pk = pk
164
- @sign_key = SigningKeyFromFile.new(pk, sk)
235
+ ED25519.validate_key_bytes!(pk, "public key", KEY_BYTES)
236
+ ED25519.validate_key_bytes!(sk, "private key field", PRIVATE_KEY_BYTES)
237
+
238
+ private_key_bytes = sk[0, KEY_BYTES]
239
+ raise ArgumentError, "Ed25519 private key does not include matching public key" unless sk[KEY_BYTES, KEY_BYTES] == pk
240
+
241
+ @sign_key = ED25519.new_private_key(private_key_bytes)
242
+ raise ArgumentError, "Ed25519 public key does not match private key" unless @sign_key.raw_public_key == pk
243
+
244
+ @public_key_bytes = pk.dup.force_encoding('BINARY')
245
+ @private_key_bytes = private_key_bytes.dup.force_encoding('BINARY')
165
246
  end
166
247
 
167
248
  def to_blob
@@ -177,11 +258,23 @@ module Net
177
258
  end
178
259
 
179
260
  def public_key
180
- PubKey.new(@pk)
261
+ PubKey.new(public_key_bytes)
181
262
  end
182
263
 
183
264
  def ssh_do_sign(data, sig_alg = nil)
184
- @sign_key.sign(data)
265
+ ED25519.sign(@sign_key, data)
266
+ end
267
+
268
+ def public_key_bytes
269
+ @public_key_bytes.dup
270
+ end
271
+
272
+ def private_key_bytes
273
+ @private_key_bytes.dup
274
+ end
275
+
276
+ def private_key_bytes_for_agent
277
+ private_key_bytes + public_key_bytes
185
278
  end
186
279
 
187
280
  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
@@ -124,17 +124,16 @@ module Net
124
124
 
125
125
  user_identities = load_identities(prepared_identities, false, true)
126
126
 
127
- if agent
128
- agent.identities.each do |key|
129
- corresponding_user_identity = user_identities.detect { |identity|
130
- identity[:public_key] && identity[:public_key].to_pem == key.to_pem
131
- }
132
- user_identities.delete(corresponding_user_identity) if corresponding_user_identity
133
-
134
- if !options[:keys_only] || corresponding_user_identity
135
- known_identities[key] = { from: :agent, identity: key }
136
- yield key
137
- end
127
+ agent_identities.each do |key|
128
+ corresponding_user_identity = user_identities.detect { |identity|
129
+ identity[:public_key] && identity[:public_key].to_blob == key.to_blob
130
+ }
131
+ # By identity: EC::Point#== raises TypeError when compared with other key types.
132
+ user_identities.reject! { |identity| identity.equal?(corresponding_user_identity) }
133
+
134
+ if !options[:keys_only] || corresponding_user_identity
135
+ known_identities[key] = { from: :agent, identity: key }
136
+ yield key
138
137
  end
139
138
  end
140
139
 
@@ -246,6 +245,18 @@ module Net
246
245
 
247
246
  private
248
247
 
248
+ # Like OpenSSH's pubkey_prepare, an agent that fails to list its keys is
249
+ # dropped for the session so that key files are still tried.
250
+ def agent_identities
251
+ return [] unless agent
252
+
253
+ agent.identities
254
+ rescue AgentError => e
255
+ error { "could not get identities from agent: #{e.class} (#{e.message})" }
256
+ self.use_agent = false
257
+ []
258
+ end
259
+
249
260
  # Load keycerts from files and data.
250
261
  def keycerts
251
262
  keycert_files.map { |keycert_file| KeyFactory.load_public_key(keycert_file) } +
@@ -20,9 +20,7 @@ module Net
20
20
  # this.
21
21
  attr_reader :key_manager
22
22
 
23
- # So far only affects algorithms used for rsa keys, but can be
24
- # extended to other keys, e.g after reading of
25
- # PubkeyAcceptedAlgorithms option from ssh_config file is implemented.
23
+ # public key algorithms to be used
26
24
  attr_reader :pubkey_algorithms
27
25
 
28
26
  # Instantiates a new authentication method.
@@ -31,11 +29,7 @@ module Net
31
29
  @key_manager = options[:key_manager]
32
30
  @options = options
33
31
  @prompt = options[:password_prompt]
34
- @pubkey_algorithms = options[:pubkey_algorithms] \
35
- || %w[rsa-sha2-256-cert-v01@openssh.com
36
- ssh-rsa-cert-v01@openssh.com
37
- rsa-sha2-256
38
- ssh-rsa]
32
+ @pubkey_algorithms = session.transport.pubkey_algorithms
39
33
  self.logger = session.logger
40
34
  end
41
35