net-ssh 7.2.0 → 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 (81) 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 +43 -0
  6. data/.github/workflows/ci-with-docker.yml +20 -9
  7. data/.github/workflows/ci.yml +11 -20
  8. data/.github/workflows/rubocop.yml +3 -1
  9. data/.gitignore +2 -0
  10. data/.rubocop.yml +1 -1
  11. data/.rubocop_todo.yml +4 -1
  12. data/CHANGES.txt +114 -0
  13. data/CONTRIBUTING.md +35 -0
  14. data/Dockerfile +7 -2
  15. data/Dockerfile.openssl3 +1 -1
  16. data/Dockerfile.zlib_ng +29 -0
  17. data/README.md +76 -57
  18. data/Rakefile +117 -30
  19. data/appveyor.yml +4 -16
  20. data/changelog.d/README.md +1 -0
  21. data/docker-compose.yml +2 -8
  22. data/lib/net/ssh/authentication/agent.rb +35 -11
  23. data/lib/net/ssh/authentication/ed25519.rb +141 -40
  24. data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
  25. data/lib/net/ssh/authentication/key_manager.rb +61 -18
  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/pub_key_fingerprint.rb +1 -1
  29. data/lib/net/ssh/authentication/security_key.rb +71 -0
  30. data/lib/net/ssh/authentication/session.rb +34 -15
  31. data/lib/net/ssh/buffer.rb +6 -1
  32. data/lib/net/ssh/buffered_io.rb +41 -4
  33. data/lib/net/ssh/config.rb +30 -13
  34. data/lib/net/ssh/connection/channel.rb +49 -2
  35. data/lib/net/ssh/connection/session.rb +58 -8
  36. data/lib/net/ssh/errors.rb +24 -6
  37. data/lib/net/ssh/key_factory.rb +11 -4
  38. data/lib/net/ssh/known_hosts.rb +64 -11
  39. data/lib/net/ssh/multiplex/client.rb +162 -0
  40. data/lib/net/ssh/multiplex/control_path.rb +120 -0
  41. data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
  42. data/lib/net/ssh/multiplex.rb +130 -0
  43. data/lib/net/ssh/prompt.rb +3 -3
  44. data/lib/net/ssh/proxy/command.rb +28 -3
  45. data/lib/net/ssh/proxy/jump.rb +52 -4
  46. data/lib/net/ssh/service/forward.rb +38 -27
  47. data/lib/net/ssh/test/extensions.rb +3 -2
  48. data/lib/net/ssh/test/socket.rb +1 -1
  49. data/lib/net/ssh/test.rb +2 -1
  50. data/lib/net/ssh/transport/aes128_gcm.rb +40 -0
  51. data/lib/net/ssh/transport/aes256_gcm.rb +40 -0
  52. data/lib/net/ssh/transport/algorithms.rb +96 -34
  53. data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
  54. data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
  55. data/lib/net/ssh/transport/cipher_factory.rb +32 -8
  56. data/lib/net/ssh/transport/constants.rb +1 -0
  57. data/lib/net/ssh/transport/gcm_cipher.rb +228 -0
  58. data/lib/net/ssh/transport/hmac/abstract.rb +16 -0
  59. data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
  60. data/lib/net/ssh/transport/hmac.rb +2 -0
  61. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
  62. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
  63. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
  64. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
  65. data/lib/net/ssh/transport/openssl.rb +2 -1
  66. data/lib/net/ssh/transport/packet_stream.rb +19 -3
  67. data/lib/net/ssh/transport/server_version.rb +33 -13
  68. data/lib/net/ssh/transport/session.rb +60 -46
  69. data/lib/net/ssh/transport/state.rb +1 -1
  70. data/lib/net/ssh/verifiers/always.rb +43 -8
  71. data/lib/net/ssh/version.rb +2 -2
  72. data/lib/net/ssh.rb +142 -62
  73. data/net-ssh-public_cert.pem +19 -18
  74. data/net-ssh.gemspec +7 -8
  75. data/rakelib/changelog.rb +229 -0
  76. data.tar.gz.sig +0 -0
  77. metadata +86 -53
  78. metadata.gz.sig +1 -1
  79. data/Gemfile.noed25519 +0 -12
  80. data/Gemfile.norbnacl +0 -12
  81. data/lib/net/ssh/version.rb.old +0 -68
@@ -1,27 +1,57 @@
1
- gem 'ed25519', '~> 1.2'
2
- gem 'bcrypt_pbkdf', '~> 1.0' unless RUBY_PLATFORM == "java"
3
-
4
- require 'ed25519'
5
-
6
- require 'base64'
7
-
1
+ require 'openssl'
8
2
  require 'net/ssh/transport/cipher_factory'
9
3
  require 'net/ssh/authentication/pub_key_fingerprint'
10
- require 'bcrypt_pbkdf' unless RUBY_PLATFORM == "java"
11
4
 
12
5
  module Net
13
6
  module SSH
14
7
  module Authentication
15
8
  module ED25519
16
- class SigningKeyFromFile < SimpleDelegator
17
- def initialize(pk, sk)
18
- key = ::Ed25519::SigningKey.from_keypair(sk)
19
- 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
12
+
13
+ class UnsupportedError < StandardError; end
20
14
 
21
- super(key)
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"
22
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)
23
44
  end
24
45
 
46
+ def self.validate_key_bytes!(key, label, expected_bytes)
47
+ raise ArgumentError, "invalid Ed25519 #{label}" unless key.respond_to?(:bytesize) && key.bytesize == expected_bytes
48
+ end
49
+
50
+ def self.binary_string(string)
51
+ string.dup.force_encoding('BINARY')
52
+ end
53
+ private_class_method :binary_string
54
+
25
55
  class OpenSSHPrivateKeyLoader
26
56
  CipherFactory = Net::SSH::Transport::CipherFactory
27
57
 
@@ -29,6 +59,9 @@ module Net
29
59
  MEND = "-----END OPENSSH PRIVATE KEY-----"
30
60
  MAGIC = "openssh-key-v1"
31
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
+
32
65
  class DecryptError < ArgumentError
33
66
  def initialize(message, encrypted_key: false)
34
67
  super(message)
@@ -41,15 +74,15 @@ module Net
41
74
  end
42
75
 
43
76
  def self.read(datafull, password)
44
- datafull = datafull.strip
77
+ datafull = datafull.strip.encode(universal_newline: true)
45
78
  raise ArgumentError.new("Expected #{MBEGIN} at start of private key") unless datafull.start_with?(MBEGIN)
46
79
  raise ArgumentError.new("Expected #{MEND} at end of private key") unless datafull.end_with?(MEND)
47
80
 
48
81
  datab64 = datafull[MBEGIN.size...-MEND.size]
49
- data = Base64.decode64(datab64)
82
+ data = datab64.unpack1("m")
50
83
  raise ArgumentError.new("Expected #{MAGIC} at start of decoded private key") unless data.start_with?(MAGIC)
51
84
 
52
- buffer = Net::SSH::Buffer.new(data[MAGIC.size + 1..-1])
85
+ buffer = Net::SSH::Buffer.new(data[(MAGIC.size + 1)..-1])
53
86
 
54
87
  ciphername = buffer.read_string
55
88
  raise ArgumentError.new("#{ciphername} in private key is not supported") unless
@@ -65,27 +98,35 @@ module Net
65
98
  _pubkey = buffer.read_string
66
99
 
67
100
  len = buffer.read_long
101
+ encrypted_private = buffer.read(len)
68
102
 
69
103
  keylen, blocksize, ivlen = CipherFactory.get_lengths(ciphername, iv_len: true)
70
104
  raise ArgumentError.new("Private key len:#{len} is not a multiple of #{blocksize}") if
71
- ((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
72
111
 
73
112
  if kdfname == 'bcrypt'
74
113
  salt = kdfopts.read_string
75
114
  rounds = kdfopts.read_long
115
+ raise ArgumentError.new("Private key bcrypt rounds #{rounds} exceeds #{MAX_KDF_ROUNDS}") if rounds > MAX_KDF_ROUNDS
76
116
 
77
- raise "BCryptPbkdf is not implemented for jruby" if RUBY_PLATFORM == "java"
78
-
79
- key = BCryptPbkdf::key(password, salt, keylen + ivlen, rounds)
80
- raise DecryptError.new("BCyryptPbkdf failed", encrypted_key: true) unless key
117
+ key = bcrypt_pbkdf_key(password, salt, keylen + ivlen, rounds)
81
118
  else
82
- key = '\x00' * (keylen + ivlen)
119
+ key = "\x00" * (keylen + ivlen)
83
120
  end
84
121
 
85
- cipher = CipherFactory.get(ciphername, key: key[0...keylen], iv: key[keylen...keylen + ivlen], decrypt: true)
86
-
87
- decoded = cipher.update(buffer.remainder_as_buffer.to_s)
88
- 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
+ )
89
130
 
90
131
  decoded = Net::SSH::Buffer.new(decoded)
91
132
  check1 = decoded.read_long
@@ -101,15 +142,50 @@ module Net
101
142
  decoded.read_private_keyblob(type_name)
102
143
  end
103
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
104
181
  end
105
182
 
106
183
  class PubKey
107
184
  include Net::SSH::Authentication::PubKeyFingerprint
108
185
 
109
- attr_reader :verify_key
110
-
111
186
  def initialize(data)
112
- @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')
113
189
  end
114
190
 
115
191
  def self.read_keyblob(buffer)
@@ -117,7 +193,7 @@ module Net
117
193
  end
118
194
 
119
195
  def to_blob
120
- 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
121
197
  end
122
198
 
123
199
  def ssh_type
@@ -129,22 +205,26 @@ module Net
129
205
  end
130
206
 
131
207
  def ssh_do_verify(sig, data, options = {})
132
- @verify_key.verify(sig, data)
208
+ ED25519.verify(verify_key, sig, data)
133
209
  end
134
210
 
135
211
  def to_pem
136
212
  # TODO this is not pem
137
- ssh_type + Base64.encode64(@verify_key.to_bytes)
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)
138
222
  end
139
223
  end
140
224
 
141
225
  class PrivKey
142
226
  CipherFactory = Net::SSH::Transport::CipherFactory
143
227
 
144
- MBEGIN = "-----BEGIN OPENSSH PRIVATE KEY-----\n"
145
- MEND = "-----END OPENSSH PRIVATE KEY-----\n"
146
- MAGIC = "openssh-key-v1"
147
-
148
228
  attr_reader :sign_key
149
229
 
150
230
  def initialize(buffer)
@@ -152,8 +232,17 @@ module Net
152
232
  sk = buffer.read_string
153
233
  _comment = buffer.read_string
154
234
 
155
- @pk = pk
156
- @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')
157
246
  end
158
247
 
159
248
  def to_blob
@@ -169,11 +258,23 @@ module Net
169
258
  end
170
259
 
171
260
  def public_key
172
- PubKey.new(@pk)
261
+ PubKey.new(public_key_bytes)
173
262
  end
174
263
 
175
264
  def ssh_do_sign(data, sig_alg = nil)
176
- @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
177
278
  end
178
279
 
179
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
@@ -32,6 +32,9 @@ module Net
32
32
  # The list of user key certificate files that will be examined
33
33
  attr_reader :keycert_files
34
34
 
35
+ # The list of user key certificate data that will be examined
36
+ attr_reader :keycert_data
37
+
35
38
  # The map of loaded identities
36
39
  attr_reader :known_identities
37
40
 
@@ -46,6 +49,7 @@ module Net
46
49
  @key_files = []
47
50
  @key_data = []
48
51
  @keycert_files = []
52
+ @keycert_data = []
49
53
  @use_agent = options[:use_agent] != false
50
54
  @known_identities = {}
51
55
  @agent = nil
@@ -59,6 +63,7 @@ module Net
59
63
  def clear!
60
64
  key_files.clear
61
65
  key_data.clear
66
+ keycert_data.clear
62
67
  known_identities.clear
63
68
  self
64
69
  end
@@ -67,12 +72,22 @@ module Net
67
72
  def add(key_file)
68
73
  key_files.push(File.expand_path(key_file)).uniq!
69
74
  self
75
+ rescue ArgumentError
76
+ self
70
77
  end
71
78
 
72
79
  # Add the given keycert_file to the list of keycert files that will be used.
73
80
  def add_keycert(keycert_file)
74
81
  keycert_files.push(File.expand_path(keycert_file)).uniq!
75
82
  self
83
+ rescue ArgumentError
84
+ self
85
+ end
86
+
87
+ # Add the given keycert_data to the list of keycerts that will be used.
88
+ def add_keycert_data(keycert_data_)
89
+ keycert_data.push(keycert_data_).uniq!
90
+ self
76
91
  end
77
92
 
78
93
  # Add the given key_file to the list of keys that will be used.
@@ -109,17 +124,16 @@ module Net
109
124
 
110
125
  user_identities = load_identities(prepared_identities, false, true)
111
126
 
112
- if agent
113
- agent.identities.each do |key|
114
- corresponding_user_identity = user_identities.detect { |identity|
115
- identity[:public_key] && identity[:public_key].to_pem == key.to_pem
116
- }
117
- 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) }
118
133
 
119
- if !options[:keys_only] || corresponding_user_identity
120
- known_identities[key] = { from: :agent, identity: key }
121
- yield key
122
- end
134
+ if !options[:keys_only] || corresponding_user_identity
135
+ known_identities[key] = { from: :agent, identity: key }
136
+ yield key
123
137
  end
124
138
  end
125
139
 
@@ -132,8 +146,8 @@ module Net
132
146
  end
133
147
 
134
148
  known_identity_blobs = known_identities.keys.map(&:to_blob)
135
- keycert_files.each do |keycert_file|
136
- keycert = KeyFactory.load_public_key(keycert_file)
149
+
150
+ keycerts.each do |keycert|
137
151
  next if known_identity_blobs.include?(keycert.to_blob)
138
152
 
139
153
  (_, corresponding_identity) = known_identities.detect { |public_key, _|
@@ -161,7 +175,11 @@ module Net
161
175
  def sign(identity, data, sig_alg = nil)
162
176
  info = known_identities[identity] or raise KeyManagerError, "the given identity is unknown to the key manager"
163
177
 
164
- 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)
165
183
  begin
166
184
  info[:key] = KeyFactory.load_private_key(info[:file], options[:passphrase], !options[:non_interactive], options[:password_prompt])
167
185
  rescue OpenSSL::OpenSSLError, Exception => e
@@ -227,14 +245,36 @@ module Net
227
245
 
228
246
  private
229
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
+
260
+ # Load keycerts from files and data.
261
+ def keycerts
262
+ keycert_files.map { |keycert_file| KeyFactory.load_public_key(keycert_file) } +
263
+ keycert_data.map { |data| KeyFactory.load_data_public_key(data) }
264
+ end
265
+
230
266
  # Prepares identities from user key_files for loading, preserving their order and sources.
231
267
  def prepare_identities_from_files
232
268
  key_files.map do |file|
233
269
  if readable_file?(file)
234
- identity = {}
270
+ identity = { privkey_file: file }
235
271
  cert_file = file + "-cert.pub"
236
272
  public_key_file = file + ".pub"
237
- 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)
238
278
  identity[:load_from] = :pubkey_file
239
279
  identity[:pubkey_file] = cert_file
240
280
  elsif readable_file?(public_key_file)
@@ -243,7 +283,7 @@ module Net
243
283
  else
244
284
  identity[:load_from] = :privkey_file
245
285
  end
246
- identity.merge(privkey_file: file)
286
+ identity
247
287
  end
248
288
  end.compact
249
289
  end
@@ -265,7 +305,10 @@ module Net
265
305
  case identity[:load_from]
266
306
  when :pubkey_file
267
307
  key = KeyFactory.load_public_key(identity[:pubkey_file])
268
- { 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] }
269
312
  when :privkey_file
270
313
  private_key = KeyFactory.load_private_key(
271
314
  identity[:privkey_file], options[:passphrase], ask_passphrase, options[:password_prompt]
@@ -296,7 +339,7 @@ module Net
296
339
 
297
340
  def process_identity_loading_error(identity, e)
298
341
  case identity[:load_from]
299
- when :pubkey_file
342
+ when :pubkey_file, :pubkey_file_only
300
343
  error { "could not load public key file `#{identity[:pubkey_file]}': #{e.class} (#{e.message})" }
301
344
  when :privkey_file
302
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
@@ -32,7 +32,7 @@ module Net
32
32
  when 'MD5'
33
33
  OpenSSL::Digest.hexdigest(algorithm, blob).scan(/../).join(":")
34
34
  when 'SHA256'
35
- "SHA256:#{Base64.encode64(OpenSSL::Digest.digest(algorithm, blob)).chomp.gsub(/=+\z/, '')}"
35
+ "SHA256:#{[OpenSSL::Digest.digest(algorithm, blob)].pack('m').chomp.gsub(/=+\z/, '')}"
36
36
  else
37
37
  raise OpenSSL::Digest::DigestError, "unsupported ssh key digest #{algorithm}"
38
38
  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