net-ssh 7.3.6 → 8.1.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 (80) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGES.txt +106 -5
  4. data/README.md +71 -57
  5. data/lib/net/ssh/authentication/agent.rb +35 -11
  6. data/lib/net/ssh/authentication/certificate.rb +2 -2
  7. data/lib/net/ssh/authentication/ed25519.rb +137 -44
  8. data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
  9. data/lib/net/ssh/authentication/key_manager.rb +22 -11
  10. data/lib/net/ssh/authentication/methods/abstract.rb +6 -8
  11. data/lib/net/ssh/authentication/methods/hostbased.rb +3 -3
  12. data/lib/net/ssh/authentication/methods/publickey.rb +52 -38
  13. data/lib/net/ssh/authentication/security_key.rb +71 -0
  14. data/lib/net/ssh/authentication/session.rb +30 -17
  15. data/lib/net/ssh/buffer.rb +8 -3
  16. data/lib/net/ssh/buffered_io.rb +40 -3
  17. data/lib/net/ssh/config.rb +123 -50
  18. data/lib/net/ssh/connection/channel.rb +49 -2
  19. data/lib/net/ssh/connection/session.rb +151 -13
  20. data/lib/net/ssh/errors.rb +34 -6
  21. data/lib/net/ssh/key_factory.rb +7 -1
  22. data/lib/net/ssh/known_hosts.rb +103 -38
  23. data/lib/net/ssh/multiplex/client.rb +162 -0
  24. data/lib/net/ssh/multiplex/control_path.rb +120 -0
  25. data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
  26. data/lib/net/ssh/multiplex.rb +127 -0
  27. data/lib/net/ssh/prompt.rb +43 -5
  28. data/lib/net/ssh/proxy/https.rb +28 -4
  29. data/lib/net/ssh/proxy/jump.rb +61 -8
  30. data/lib/net/ssh/service/forward.rb +35 -24
  31. data/lib/net/ssh/test.rb +2 -1
  32. data/lib/net/ssh/transport/algorithms.rb +92 -34
  33. data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
  34. data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
  35. data/lib/net/ssh/transport/cipher_factory.rb +23 -1
  36. data/lib/net/ssh/transport/constants.rb +1 -0
  37. data/lib/net/ssh/transport/gcm_cipher.rb +27 -6
  38. data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
  39. data/lib/net/ssh/transport/hmac.rb +2 -0
  40. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
  41. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
  42. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
  43. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
  44. data/lib/net/ssh/transport/openssl.rb +2 -1
  45. data/lib/net/ssh/transport/packet_stream.rb +12 -0
  46. data/lib/net/ssh/transport/session.rb +61 -45
  47. data/lib/net/ssh/verifiers/accept_new_or_local_tunnel.rb +3 -1
  48. data/lib/net/ssh/verifiers/always.rb +16 -4
  49. data/lib/net/ssh/verifiers/never.rb +2 -1
  50. data/lib/net/ssh/version.rb +3 -3
  51. data/lib/net/ssh.rb +164 -63
  52. data.tar.gz.sig +0 -0
  53. metadata +33 -61
  54. metadata.gz.sig +0 -0
  55. data/.dockerignore +0 -9
  56. data/.github/FUNDING.yml +0 -1
  57. data/.github/config/rubocop_linter_action.yml +0 -4
  58. data/.github/workflows/ci-with-docker.yml +0 -58
  59. data/.github/workflows/ci.yml +0 -99
  60. data/.github/workflows/rubocop.yml +0 -16
  61. data/.gitignore +0 -15
  62. data/.rubocop.yml +0 -22
  63. data/.rubocop_todo.yml +0 -1083
  64. data/DEVELOPMENT.md +0 -23
  65. data/Dockerfile +0 -32
  66. data/Dockerfile.openssl3 +0 -17
  67. data/Dockerfile.zlib_ng +0 -29
  68. data/Gemfile +0 -13
  69. data/Gemfile.noed25519 +0 -12
  70. data/Gemfile.norbnacl +0 -12
  71. data/ISSUE_TEMPLATE.md +0 -30
  72. data/Manifest +0 -132
  73. data/Rakefile +0 -196
  74. data/SECURITY.md +0 -4
  75. data/THANKS.txt +0 -110
  76. data/appveyor.yml +0 -58
  77. data/docker-compose.yml +0 -23
  78. data/net-ssh-public_cert.pem +0 -21
  79. data/net-ssh.gemspec +0 -48
  80. data/support/ssh_tunnel_bug.rb +0 -65
@@ -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
 
@@ -72,6 +66,10 @@ module Net
72
66
  private
73
67
 
74
68
  attr_reader :prompt
69
+
70
+ def fingerprint(identity)
71
+ identity.fingerprint(@options[:fingerprint_hash] || 'SHA256')
72
+ end
75
73
  end
76
74
  end
77
75
  end
@@ -31,7 +31,7 @@ module Net
31
31
  # Attempts to perform host-based authentication of the user, using
32
32
  # the given host identity (key).
33
33
  def authenticate_with(identity, next_service, username, key_manager)
34
- debug { "trying hostbased (#{identity.fingerprint})" }
34
+ debug { "trying hostbased (#{fingerprint(identity)})" }
35
35
  client_username = ENV['USER'] || username
36
36
 
37
37
  req = build_request(identity, next_service, username, "#{hostname}.", client_username)
@@ -46,10 +46,10 @@ module Net
46
46
 
47
47
  case message.type
48
48
  when USERAUTH_SUCCESS
49
- info { "hostbased succeeded (#{identity.fingerprint})" }
49
+ info { "hostbased succeeded (#{fingerprint(identity)})" }
50
50
  return true
51
51
  when USERAUTH_FAILURE
52
- info { "hostbased failed (#{identity.fingerprint})" }
52
+ info { "hostbased failed (#{fingerprint(identity)})" }
53
53
 
54
54
  raise Net::SSH::Authentication::DisallowedMethod unless
55
55
  message[:authentications].split(/,/).include? 'hostbased'
@@ -8,6 +8,25 @@ module Net
8
8
  module Methods
9
9
  # Implements the "publickey" SSH authentication method.
10
10
  class Publickey < Abstract
11
+ RSA_CERT = "ssh-rsa-cert-v01@openssh.com".freeze
12
+
13
+ # Key algorithms usable with each RSA key type, mapped to the signature algorithm they imply.
14
+ RSA_SIGNATURE_ALGORITHMS = {
15
+ "ssh-rsa" => {
16
+ "rsa-sha2-512" => "rsa-sha2-512",
17
+ "rsa-sha2-256" => "rsa-sha2-256",
18
+ "ssh-rsa" => "ssh-rsa"
19
+ }.freeze,
20
+ RSA_CERT => {
21
+ "rsa-sha2-512-cert-v01@openssh.com" => "rsa-sha2-512",
22
+ "rsa-sha2-256-cert-v01@openssh.com" => "rsa-sha2-256",
23
+ RSA_CERT => "ssh-rsa"
24
+ }.freeze
25
+ }.freeze
26
+
27
+ # Servers that OpenSSH flags with SSH_BUG_SIGTYPE (compat.c): no RSA/SHA-2 certificate signatures.
28
+ SIGTYPE_BUG = /\A(OpenSSH_[2-5]|OpenSSH_6\.[56]|OpenSSH_7\.[0-7]|Sun_SSH_1\.0)/.freeze
29
+
11
30
  # Attempts to perform public-key authentication for the given
12
31
  # username, trying each identity known to the key manager. If any of
13
32
  # them succeed, returns +true+, otherwise returns +false+. This
@@ -15,6 +34,8 @@ module Net
15
34
  def authenticate(next_service, username, password = nil)
16
35
  return false unless key_manager
17
36
 
37
+ debug { "pubkey_algorithms: #{pubkey_algorithms.join(', ')}" }
38
+
18
39
  key_manager.each_identity do |identity|
19
40
  return true if authenticate_with(identity, next_service, username)
20
41
  end
@@ -44,7 +65,7 @@ module Net
44
65
  end
45
66
 
46
67
  def authenticate_with_alg(identity, next_service, username, alg, sig_alg = nil)
47
- debug { "trying publickey (#{identity.fingerprint}) alg #{alg}" }
68
+ debug { "trying publickey (#{fingerprint(identity)}) alg #{alg}" }
48
69
  send_request(identity, username, next_service, alg)
49
70
 
50
71
  message = session.next_message
@@ -64,10 +85,10 @@ module Net
64
85
 
65
86
  case message.type
66
87
  when USERAUTH_SUCCESS
67
- debug { "publickey succeeded (#{identity.fingerprint})" }
88
+ debug { "publickey succeeded (#{fingerprint(identity)})" }
68
89
  return true
69
90
  when USERAUTH_FAILURE
70
- debug { "publickey failed (#{identity.fingerprint})" }
91
+ debug { "publickey failed (#{fingerprint(identity)})" }
71
92
 
72
93
  raise Net::SSH::Authentication::DisallowedMethod unless
73
94
  message[:authentications].split(/,/).include? 'publickey'
@@ -92,43 +113,36 @@ module Net
92
113
  # username, with the given identity (public key). Returns +true+ if
93
114
  # successful, or +false+ otherwise.
94
115
  def authenticate_with(identity, next_service, username)
116
+ alg = key_algorithm(identity)
117
+ if alg.nil?
118
+ debug { "skipping publickey (#{identity.fingerprint}): no mutual signature algorithm for #{identity.ssh_type}" }
119
+ return false
120
+ end
121
+
122
+ authenticate_with_alg(identity, next_service, username, alg, RSA_SIGNATURE_ALGORITHMS.dig(identity.ssh_type, alg))
123
+ end
124
+
125
+ # Picks the single algorithm to offer for the identity, following
126
+ # key_sig_algorithm() in OpenSSH 10.2 sshconnect2.c.
127
+ def key_algorithm(identity)
95
128
  type = identity.ssh_type
96
- if type == "ssh-rsa"
97
- pubkey_algorithms.each do |pk_alg|
98
- case pk_alg
99
- when "rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"
100
- if authenticate_with_alg(identity, next_service, username, pk_alg, pk_alg)
101
- # success
102
- return true
103
- end
104
- end
105
- end
106
- elsif type == "ssh-rsa-cert-v01@openssh.com"
107
- pubkey_algorithms.each do |pk_alg|
108
- case pk_alg
109
- when "rsa-sha2-512-cert-v01@openssh.com"
110
- if authenticate_with_alg(identity, next_service, username, pk_alg, "rsa-sha2-512")
111
- # success
112
- return true
113
- end
114
- when "rsa-sha2-256-cert-v01@openssh.com"
115
- if authenticate_with_alg(identity, next_service, username, pk_alg, "rsa-sha2-256")
116
- # success
117
- return true
118
- end
119
- when "ssh-rsa-cert-v01@openssh.com"
120
- if authenticate_with_alg(identity, next_service, username, pk_alg)
121
- # success
122
- return true
123
- end
124
- end
125
- end
126
- elsif authenticate_with_alg(identity, next_service, username, type)
127
- # success
128
- return true
129
+ server_sig_algs = session.transport.server_sig_algs
130
+ server_software = session.transport.server_version.version.to_s.sub(/\ASSH-[\d.]+-/, "")
131
+
132
+ if server_sig_algs.nil? || !RSA_SIGNATURE_ALGORITHMS.key?(type) ||
133
+ (type == RSA_CERT && server_software.match?(SIGTYPE_BUG))
134
+ return type if pubkey_algorithms.include?(type)
135
+
136
+ return nil
137
+ end
138
+
139
+ # OpenSSH 7.4 accepts RSA/SHA-2 signatures but leaves them out of server-sig-algs.
140
+ server_sig_algs = %w[rsa-sha2-256 rsa-sha2-512] if type == "ssh-rsa" && server_software.start_with?("OpenSSH_7.4")
141
+
142
+ pubkey_algorithms.find do |alg|
143
+ sig_alg = RSA_SIGNATURE_ALGORITHMS[type][alg]
144
+ sig_alg && server_sig_algs.include?(sig_alg)
129
145
  end
130
- # failure
131
- return false
132
146
  end
133
147
  end
134
148
  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