net-ssh 7.3.6 → 8.0.0

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 +1 -1
  9. data/.rubocop_todo.yml +1 -0
  10. data/CHANGES.txt +73 -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 +130 -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 +29 -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 +41 -44
  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 +139 -57
  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/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
 
@@ -15,6 +15,8 @@ module Net
15
15
  def authenticate(next_service, username, password = nil)
16
16
  return false unless key_manager
17
17
 
18
+ debug { "pubkey_algorithms: #{pubkey_algorithms.join(', ')}" }
19
+
18
20
  key_manager.each_identity do |identity|
19
21
  return true if authenticate_with(identity, next_service, username)
20
22
  end
@@ -123,7 +125,7 @@ module Net
123
125
  end
124
126
  end
125
127
  end
126
- elsif authenticate_with_alg(identity, next_service, username, type)
128
+ elsif pubkey_algorithms.include?(type) && authenticate_with_alg(identity, next_service, username, type)
127
129
  # success
128
130
  return true
129
131
  end
@@ -0,0 +1,71 @@
1
+ require 'net/ssh/errors'
2
+ require 'net/ssh/authentication/pub_key_fingerprint'
3
+
4
+ module Net
5
+ module SSH
6
+ module Authentication
7
+ # FIDO/U2F security keys held by ssh-agent. The agent does the signing (incl. user
8
+ # presence); net-ssh only parses and re-serialises the public key blob (see PROTOCOL.u2f).
9
+ module SecurityKey
10
+ SK_ECDSA_SHA2_NISTP256 = "sk-ecdsa-sha2-nistp256@openssh.com"
11
+ SK_SSH_ED25519 = "sk-ssh-ed25519@openssh.com"
12
+ TYPES = [SK_ECDSA_SHA2_NISTP256, SK_SSH_ED25519].freeze
13
+
14
+ def self.understands?(type)
15
+ TYPES.include?(type)
16
+ end
17
+
18
+ # Public key of an agent-held security key; signing is delegated to the agent.
19
+ class PubKey
20
+ include Net::SSH::Authentication::PubKeyFingerprint
21
+
22
+ attr_reader :ssh_type, :curve, :public_key_data, :application
23
+
24
+ # Wire formats:
25
+ # sk-ecdsa-sha2-nistp256@openssh.com: string curve, string Q, string application
26
+ # sk-ssh-ed25519@openssh.com: string public_key, string application
27
+ def self.read_keyblob(type, buffer)
28
+ case type
29
+ when SK_ECDSA_SHA2_NISTP256
30
+ curve = buffer.read_string
31
+ public_key_data = buffer.read_string
32
+ application = buffer.read_string
33
+ new(type, public_key_data, application, curve: curve)
34
+ when SK_SSH_ED25519
35
+ public_key_data = buffer.read_string
36
+ application = buffer.read_string
37
+ new(type, public_key_data, application)
38
+ else
39
+ raise Net::SSH::UnsupportedKeyType, "unsupported security key type `#{type}'"
40
+ end
41
+ end
42
+
43
+ def initialize(ssh_type, public_key_data, application, curve: nil)
44
+ @ssh_type = ssh_type
45
+ @public_key_data = public_key_data
46
+ @application = application
47
+ @curve = curve
48
+ end
49
+
50
+ def to_blob
51
+ buffer = Net::SSH::Buffer.new
52
+ buffer.write_string(ssh_type)
53
+ buffer.write_string(curve) if curve
54
+ buffer.write_string(public_key_data)
55
+ buffer.write_string(application)
56
+ buffer.to_s
57
+ end
58
+
59
+ def ssh_signature_type
60
+ ssh_type
61
+ end
62
+
63
+ # Only used to de-duplicate identities, so a stable key-specific string suffices.
64
+ def to_pem
65
+ "#{ssh_type} #{[to_blob].pack('m0')}"
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end