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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.dockerignore +3 -0
- data/.github/pull_request_template.md +1 -0
- data/.github/workflows/changelog.yml +18 -0
- data/.github/workflows/ci-with-docker.yml +20 -9
- data/.github/workflows/ci.yml +10 -20
- data/.github/workflows/rubocop.yml +3 -1
- data/.rubocop.yml +5 -1
- data/.rubocop_todo.yml +4 -8
- data/CHANGES.txt +122 -0
- data/CONTRIBUTING.md +34 -0
- data/Dockerfile +4 -1
- data/Dockerfile.openssl3 +1 -1
- data/Dockerfile.zlib_ng +29 -0
- data/Gemfile +2 -0
- data/README.md +74 -57
- data/Rakefile +30 -12
- data/appveyor.yml +4 -16
- data/changelog.d/README.md +1 -0
- data/docker-compose.yml +1 -9
- data/lib/net/ssh/authentication/agent.rb +35 -11
- data/lib/net/ssh/authentication/ed25519.rb +140 -37
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/authentication/key_manager.rb +42 -16
- data/lib/net/ssh/authentication/methods/abstract.rb +2 -8
- data/lib/net/ssh/authentication/methods/publickey.rb +3 -1
- data/lib/net/ssh/authentication/security_key.rb +71 -0
- data/lib/net/ssh/authentication/session.rb +29 -17
- data/lib/net/ssh/buffer.rb +6 -1
- data/lib/net/ssh/buffered_io.rb +40 -3
- data/lib/net/ssh/config.rb +30 -13
- data/lib/net/ssh/connection/channel.rb +49 -2
- data/lib/net/ssh/connection/session.rb +151 -13
- data/lib/net/ssh/errors.rb +24 -6
- data/lib/net/ssh/key_factory.rb +11 -4
- data/lib/net/ssh/known_hosts.rb +62 -8
- data/lib/net/ssh/multiplex/client.rb +162 -0
- data/lib/net/ssh/multiplex/control_path.rb +120 -0
- data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
- data/lib/net/ssh/multiplex.rb +126 -0
- data/lib/net/ssh/prompt.rb +3 -3
- data/lib/net/ssh/proxy/command.rb +28 -3
- data/lib/net/ssh/proxy/jump.rb +71 -8
- data/lib/net/ssh/service/forward.rb +44 -27
- data/lib/net/ssh/test/extensions.rb +3 -2
- data/lib/net/ssh/test/socket.rb +1 -1
- data/lib/net/ssh/test.rb +2 -1
- data/lib/net/ssh/transport/aes128_gcm.rb +40 -0
- data/lib/net/ssh/transport/aes256_gcm.rb +40 -0
- data/lib/net/ssh/transport/algorithms.rb +96 -34
- data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
- data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
- data/lib/net/ssh/transport/cipher_factory.rb +32 -8
- data/lib/net/ssh/transport/constants.rb +1 -0
- data/lib/net/ssh/transport/gcm_cipher.rb +228 -0
- data/lib/net/ssh/transport/hmac/abstract.rb +16 -0
- data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
- data/lib/net/ssh/transport/hmac.rb +2 -0
- data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
- data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
- data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
- data/lib/net/ssh/transport/openssl.rb +2 -1
- data/lib/net/ssh/transport/packet_stream.rb +19 -3
- data/lib/net/ssh/transport/server_version.rb +33 -13
- data/lib/net/ssh/transport/session.rb +61 -46
- data/lib/net/ssh/transport/state.rb +1 -1
- data/lib/net/ssh/verifiers/always.rb +43 -8
- data/lib/net/ssh/version.rb +3 -3
- data/lib/net/ssh.rb +148 -63
- data/net-ssh-public_cert.pem +7 -7
- data/net-ssh.gemspec +6 -8
- data.tar.gz.sig +0 -0
- metadata +64 -46
- metadata.gz.sig +0 -0
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
|
@@ -1,24 +1,56 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
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 =
|
|
119
|
+
key = "\x00" * (keylen + ivlen)
|
|
81
120
|
end
|
|
82
121
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 + [
|
|
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
|
-
|
|
154
|
-
|
|
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(
|
|
261
|
+
PubKey.new(public_key_bytes)
|
|
171
262
|
end
|
|
172
263
|
|
|
173
264
|
def ssh_do_sign(data, sig_alg = nil)
|
|
174
|
-
|
|
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
|
|
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 =
|
|
18
|
-
description
|
|
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.
|
|
23
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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[:
|
|
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
|
|
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
|
|
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: :
|
|
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
|
-
#
|
|
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 =
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
data/lib/net/ssh/buffer.rb
CHANGED
|
@@ -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
|
|
347
|
+
raise Net::SSH::UnsupportedKeyType, "unsupported key type `#{type}'"
|
|
343
348
|
end
|
|
344
349
|
|
|
345
350
|
return key
|