net-ssh 7.2.3 → 8.0.4

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 (78) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.dockerignore +3 -0
  4. data/.github/pull_request_template.md +1 -0
  5. data/.github/workflows/changelog.yml +18 -0
  6. data/.github/workflows/ci-with-docker.yml +20 -9
  7. data/.github/workflows/ci.yml +10 -20
  8. data/.github/workflows/rubocop.yml +3 -1
  9. data/.rubocop.yml +5 -1
  10. data/.rubocop_todo.yml +4 -8
  11. data/CHANGES.txt +122 -0
  12. data/CONTRIBUTING.md +34 -0
  13. data/Dockerfile +4 -1
  14. data/Dockerfile.openssl3 +1 -1
  15. data/Dockerfile.zlib_ng +29 -0
  16. data/Gemfile +2 -0
  17. data/README.md +74 -57
  18. data/Rakefile +30 -12
  19. data/appveyor.yml +4 -16
  20. data/changelog.d/README.md +1 -0
  21. data/docker-compose.yml +1 -9
  22. data/lib/net/ssh/authentication/agent.rb +35 -11
  23. data/lib/net/ssh/authentication/ed25519.rb +140 -37
  24. data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
  25. data/lib/net/ssh/authentication/key_manager.rb +42 -16
  26. data/lib/net/ssh/authentication/methods/abstract.rb +2 -8
  27. data/lib/net/ssh/authentication/methods/publickey.rb +3 -1
  28. data/lib/net/ssh/authentication/security_key.rb +71 -0
  29. data/lib/net/ssh/authentication/session.rb +29 -17
  30. data/lib/net/ssh/buffer.rb +6 -1
  31. data/lib/net/ssh/buffered_io.rb +40 -3
  32. data/lib/net/ssh/config.rb +30 -13
  33. data/lib/net/ssh/connection/channel.rb +49 -2
  34. data/lib/net/ssh/connection/session.rb +151 -13
  35. data/lib/net/ssh/errors.rb +24 -6
  36. data/lib/net/ssh/key_factory.rb +11 -4
  37. data/lib/net/ssh/known_hosts.rb +62 -8
  38. data/lib/net/ssh/multiplex/client.rb +162 -0
  39. data/lib/net/ssh/multiplex/control_path.rb +120 -0
  40. data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
  41. data/lib/net/ssh/multiplex.rb +126 -0
  42. data/lib/net/ssh/prompt.rb +3 -3
  43. data/lib/net/ssh/proxy/command.rb +28 -3
  44. data/lib/net/ssh/proxy/jump.rb +71 -8
  45. data/lib/net/ssh/service/forward.rb +44 -27
  46. data/lib/net/ssh/test/extensions.rb +3 -2
  47. data/lib/net/ssh/test/socket.rb +1 -1
  48. data/lib/net/ssh/test.rb +2 -1
  49. data/lib/net/ssh/transport/aes128_gcm.rb +40 -0
  50. data/lib/net/ssh/transport/aes256_gcm.rb +40 -0
  51. data/lib/net/ssh/transport/algorithms.rb +96 -34
  52. data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
  53. data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
  54. data/lib/net/ssh/transport/cipher_factory.rb +32 -8
  55. data/lib/net/ssh/transport/constants.rb +1 -0
  56. data/lib/net/ssh/transport/gcm_cipher.rb +228 -0
  57. data/lib/net/ssh/transport/hmac/abstract.rb +16 -0
  58. data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
  59. data/lib/net/ssh/transport/hmac.rb +2 -0
  60. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
  61. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
  62. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
  63. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
  64. data/lib/net/ssh/transport/openssl.rb +2 -1
  65. data/lib/net/ssh/transport/packet_stream.rb +19 -3
  66. data/lib/net/ssh/transport/server_version.rb +33 -13
  67. data/lib/net/ssh/transport/session.rb +61 -46
  68. data/lib/net/ssh/transport/state.rb +1 -1
  69. data/lib/net/ssh/verifiers/always.rb +43 -8
  70. data/lib/net/ssh/version.rb +3 -3
  71. data/lib/net/ssh.rb +148 -63
  72. data/net-ssh-public_cert.pem +7 -7
  73. data/net-ssh.gemspec +6 -8
  74. data.tar.gz.sig +0 -0
  75. metadata +64 -46
  76. metadata.gz.sig +0 -0
  77. data/Gemfile.noed25519 +0 -12
  78. data/Gemfile.norbnacl +0 -12
@@ -1,24 +1,56 @@
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
48
+ end
49
+
50
+ def self.binary_string(string)
51
+ string.dup.force_encoding('BINARY')
21
52
  end
53
+ private_class_method :binary_string
22
54
 
23
55
  class OpenSSHPrivateKeyLoader
24
56
  CipherFactory = Net::SSH::Transport::CipherFactory
@@ -27,6 +59,9 @@ module Net
27
59
  MEND = "-----END OPENSSH PRIVATE KEY-----"
28
60
  MAGIC = "openssh-key-v1"
29
61
 
62
+ # Same limit as OpenSSH; bcrypt_pbkdf with 2**32 rounds would run for months (GHSA-rpq3-v334-f5pm).
63
+ MAX_KDF_ROUNDS = 1 << 20
64
+
30
65
  class DecryptError < ArgumentError
31
66
  def initialize(message, encrypted_key: false)
32
67
  super(message)
@@ -39,7 +74,7 @@ module Net
39
74
  end
40
75
 
41
76
  def self.read(datafull, password)
42
- datafull = datafull.strip
77
+ datafull = datafull.strip.encode(universal_newline: true)
43
78
  raise ArgumentError.new("Expected #{MBEGIN} at start of private key") unless datafull.start_with?(MBEGIN)
44
79
  raise ArgumentError.new("Expected #{MEND} at end of private key") unless datafull.end_with?(MEND)
45
80
 
@@ -47,7 +82,7 @@ module Net
47
82
  data = datab64.unpack1("m")
48
83
  raise ArgumentError.new("Expected #{MAGIC} at start of decoded private key") unless data.start_with?(MAGIC)
49
84
 
50
- buffer = Net::SSH::Buffer.new(data[MAGIC.size + 1..-1])
85
+ buffer = Net::SSH::Buffer.new(data[(MAGIC.size + 1)..-1])
51
86
 
52
87
  ciphername = buffer.read_string
53
88
  raise ArgumentError.new("#{ciphername} in private key is not supported") unless
@@ -63,27 +98,35 @@ module Net
63
98
  _pubkey = buffer.read_string
64
99
 
65
100
  len = buffer.read_long
101
+ encrypted_private = buffer.read(len)
66
102
 
67
103
  keylen, blocksize, ivlen = CipherFactory.get_lengths(ciphername, iv_len: true)
68
104
  raise ArgumentError.new("Private key len:#{len} is not a multiple of #{blocksize}") if
69
- ((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
70
111
 
71
112
  if kdfname == 'bcrypt'
72
113
  salt = kdfopts.read_string
73
114
  rounds = kdfopts.read_long
115
+ raise ArgumentError.new("Private key bcrypt rounds #{rounds} exceeds #{MAX_KDF_ROUNDS}") if rounds > MAX_KDF_ROUNDS
74
116
 
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
117
+ key = bcrypt_pbkdf_key(password, salt, keylen + ivlen, rounds)
79
118
  else
80
- key = '\x00' * (keylen + ivlen)
119
+ key = "\x00" * (keylen + ivlen)
81
120
  end
82
121
 
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
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
+ )
87
130
 
88
131
  decoded = Net::SSH::Buffer.new(decoded)
89
132
  check1 = decoded.read_long
@@ -99,15 +142,50 @@ module Net
99
142
  decoded.read_private_keyblob(type_name)
100
143
  end
101
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
102
181
  end
103
182
 
104
183
  class PubKey
105
184
  include Net::SSH::Authentication::PubKeyFingerprint
106
185
 
107
- attr_reader :verify_key
108
-
109
186
  def initialize(data)
110
- @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')
111
189
  end
112
190
 
113
191
  def self.read_keyblob(buffer)
@@ -115,7 +193,7 @@ module Net
115
193
  end
116
194
 
117
195
  def to_blob
118
- 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
119
197
  end
120
198
 
121
199
  def ssh_type
@@ -127,22 +205,26 @@ module Net
127
205
  end
128
206
 
129
207
  def ssh_do_verify(sig, data, options = {})
130
- @verify_key.verify(sig, data)
208
+ ED25519.verify(verify_key, sig, data)
131
209
  end
132
210
 
133
211
  def to_pem
134
212
  # TODO this is not pem
135
- 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)
136
222
  end
137
223
  end
138
224
 
139
225
  class PrivKey
140
226
  CipherFactory = Net::SSH::Transport::CipherFactory
141
227
 
142
- MBEGIN = "-----BEGIN OPENSSH PRIVATE KEY-----\n"
143
- MEND = "-----END OPENSSH PRIVATE KEY-----\n"
144
- MAGIC = "openssh-key-v1"
145
-
146
228
  attr_reader :sign_key
147
229
 
148
230
  def initialize(buffer)
@@ -150,8 +232,17 @@ module Net
150
232
  sk = buffer.read_string
151
233
  _comment = buffer.read_string
152
234
 
153
- @pk = pk
154
- @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')
155
246
  end
156
247
 
157
248
  def to_blob
@@ -167,11 +258,23 @@ module Net
167
258
  end
168
259
 
169
260
  def public_key
170
- PubKey.new(@pk)
261
+ PubKey.new(public_key_bytes)
171
262
  end
172
263
 
173
264
  def ssh_do_sign(data, sig_alg = nil)
174
- @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
175
278
  end
176
279
 
177
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
@@ -72,12 +72,16 @@ module Net
72
72
  def add(key_file)
73
73
  key_files.push(File.expand_path(key_file)).uniq!
74
74
  self
75
+ rescue ArgumentError
76
+ self
75
77
  end
76
78
 
77
79
  # Add the given keycert_file to the list of keycert files that will be used.
78
80
  def add_keycert(keycert_file)
79
81
  keycert_files.push(File.expand_path(keycert_file)).uniq!
80
82
  self
83
+ rescue ArgumentError
84
+ self
81
85
  end
82
86
 
83
87
  # Add the given keycert_data to the list of keycerts that will be used.
@@ -120,17 +124,16 @@ module Net
120
124
 
121
125
  user_identities = load_identities(prepared_identities, false, true)
122
126
 
123
- if agent
124
- agent.identities.each do |key|
125
- corresponding_user_identity = user_identities.detect { |identity|
126
- identity[:public_key] && identity[:public_key].to_pem == key.to_pem
127
- }
128
- user_identities.delete(corresponding_user_identity) if corresponding_user_identity
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) }
129
133
 
130
- if !options[:keys_only] || corresponding_user_identity
131
- known_identities[key] = { from: :agent, identity: key }
132
- yield key
133
- end
134
+ if !options[:keys_only] || corresponding_user_identity
135
+ known_identities[key] = { from: :agent, identity: key }
136
+ yield key
134
137
  end
135
138
  end
136
139
 
@@ -172,7 +175,11 @@ module Net
172
175
  def sign(identity, data, sig_alg = nil)
173
176
  info = known_identities[identity] or raise KeyManagerError, "the given identity is unknown to the key manager"
174
177
 
175
- if info[:key].nil? && info[:from] == :file
178
+ if info[:from] == :pubkey_file_only
179
+ raise KeyManagerError, "the given identity is a public key only and cannot be used for signing without an agent"
180
+ end
181
+
182
+ if info[:key].nil? && (info[:from] == :file || info[:from] == :pubkey_file)
176
183
  begin
177
184
  info[:key] = KeyFactory.load_private_key(info[:file], options[:passphrase], !options[:non_interactive], options[:password_prompt])
178
185
  rescue OpenSSL::OpenSSLError, Exception => e
@@ -238,6 +245,18 @@ module Net
238
245
 
239
246
  private
240
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
+
241
260
  # Load keycerts from files and data.
242
261
  def keycerts
243
262
  keycert_files.map { |keycert_file| KeyFactory.load_public_key(keycert_file) } +
@@ -248,10 +267,14 @@ module Net
248
267
  def prepare_identities_from_files
249
268
  key_files.map do |file|
250
269
  if readable_file?(file)
251
- identity = {}
270
+ identity = { privkey_file: file }
252
271
  cert_file = file + "-cert.pub"
253
272
  public_key_file = file + ".pub"
254
- if readable_file?(cert_file)
273
+ if file.end_with?(".pub")
274
+ identity[:load_from] = :pubkey_file_only
275
+ identity[:pubkey_file] = file
276
+ identity.delete(:privkey_file)
277
+ elsif readable_file?(cert_file)
255
278
  identity[:load_from] = :pubkey_file
256
279
  identity[:pubkey_file] = cert_file
257
280
  elsif readable_file?(public_key_file)
@@ -260,7 +283,7 @@ module Net
260
283
  else
261
284
  identity[:load_from] = :privkey_file
262
285
  end
263
- identity.merge(privkey_file: file)
286
+ identity
264
287
  end
265
288
  end.compact
266
289
  end
@@ -282,7 +305,10 @@ module Net
282
305
  case identity[:load_from]
283
306
  when :pubkey_file
284
307
  key = KeyFactory.load_public_key(identity[:pubkey_file])
285
- { public_key: key, from: :file, file: identity[:privkey_file] }
308
+ { public_key: key, from: :pubkey_file, file: identity[:privkey_file] }
309
+ when :pubkey_file_only
310
+ key = KeyFactory.load_public_key(identity[:pubkey_file])
311
+ { public_key: key, from: :pubkey_file_only, file: identity[:privkey_file] }
286
312
  when :privkey_file
287
313
  private_key = KeyFactory.load_private_key(
288
314
  identity[:privkey_file], options[:passphrase], ask_passphrase, options[:password_prompt]
@@ -313,7 +339,7 @@ module Net
313
339
 
314
340
  def process_identity_loading_error(identity, e)
315
341
  case identity[:load_from]
316
- when :pubkey_file
342
+ when :pubkey_file, :pubkey_file_only
317
343
  error { "could not load public key file `#{identity[:pubkey_file]}': #{e.class} (#{e.message})" }
318
344
  when :privkey_file
319
345
  error { "could not load private key file `#{identity[:privkey_file]}': #{e.class} (#{e.message})" }
@@ -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
@@ -69,27 +69,22 @@ module Net
69
69
 
70
70
  attempted = []
71
71
 
72
- @auth_methods.each do |name|
73
- next unless @allowed_auth_methods.include?(name)
74
-
75
- attempted << name
76
-
77
- debug { "trying #{name}" }
78
- begin
79
- auth_class = Methods.const_get(name.split(/\W+/).map { |p| p.capitalize }.join)
80
- method = auth_class.new(self,
81
- key_manager: key_manager, password_prompt: options[:password_prompt],
82
- pubkey_algorithms: options[:pubkey_algorithms] || nil)
83
- rescue NameError
84
- debug {"Mechanism #{name} was requested, but isn't a known type. Ignoring it."}
85
- next
72
+ # After a partial success (sshd AuthenticationMethods) restart from the most preferred method the server now allows, as OpenSSH does.
73
+ loop do
74
+ @partial_success = false
75
+
76
+ @auth_methods.each do |name|
77
+ next unless @allowed_auth_methods.include?(name)
78
+
79
+ attempted << name
80
+ return true if try_authenticate(name, next_service, username, password, key_manager)
81
+ break if @partial_success
86
82
  end
87
83
 
88
- return true if method.authenticate(next_service, username, password)
89
- rescue Net::SSH::Authentication::DisallowedMethod
84
+ break unless @partial_success
90
85
  end
91
86
 
92
- error { "all authorization methods failed (tried #{attempted.join(', ')})" }
87
+ error { "all authorization methods failed (tried #{attempted.uniq.join(', ')})" }
93
88
  return false
94
89
  ensure
95
90
  key_manager.finish if key_manager
@@ -109,6 +104,7 @@ module Net
109
104
 
110
105
  when USERAUTH_FAILURE
111
106
  @allowed_auth_methods = packet[:authentications].split(/,/)
107
+ @partial_success ||= packet[:partial_success]
112
108
  debug { "allowed methods: #{packet[:authentications]}" }
113
109
  return packet
114
110
 
@@ -136,6 +132,22 @@ module Net
136
132
 
137
133
  private
138
134
 
135
+ def try_authenticate(method_name, next_service, username, password, key_manager)
136
+ debug { "trying #{method_name}" }
137
+ begin
138
+ auth_class = Methods.const_get(method_name.split(/\W+/).map { |p| p.capitalize }.join)
139
+ method = auth_class.new(self,
140
+ key_manager: key_manager, password_prompt: options[:password_prompt])
141
+ rescue NameError
142
+ debug {"Mechanism #{method_name} was requested, but isn't a known type. Ignoring it."}
143
+ return false
144
+ end
145
+
146
+ method.authenticate(next_service, username, password)
147
+ rescue Net::SSH::Authentication::DisallowedMethod
148
+ false
149
+ end
150
+
139
151
  # Returns an array of paths to the key files usually defined
140
152
  # by system default.
141
153
  def default_keys
@@ -1,7 +1,9 @@
1
+ require 'net/ssh/errors'
1
2
  require 'net/ssh/transport/openssl'
2
3
 
3
4
  require 'net/ssh/authentication/certificate'
4
5
  require 'net/ssh/authentication/ed25519_loader'
6
+ require 'net/ssh/authentication/security_key'
5
7
 
6
8
  module Net
7
9
  module SSH
@@ -338,8 +340,11 @@ module Net
338
340
  key = Net::SSH::Authentication::ED25519::PubKey.read_keyblob(self)
339
341
  when /^ecdsa\-sha2\-(\w*)$/
340
342
  key = OpenSSL::PKey::EC.read_keyblob($1, self)
343
+ when Net::SSH::Authentication::SecurityKey::SK_ECDSA_SHA2_NISTP256,
344
+ Net::SSH::Authentication::SecurityKey::SK_SSH_ED25519
345
+ key = Net::SSH::Authentication::SecurityKey::PubKey.read_keyblob(type, self)
341
346
  else
342
- raise NotImplementedError, "unsupported key type `#{type}'"
347
+ raise Net::SSH::UnsupportedKeyType, "unsupported key type `#{type}'"
343
348
  end
344
349
 
345
350
  return key