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.
- 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 +43 -0
- data/.github/workflows/ci-with-docker.yml +20 -9
- data/.github/workflows/ci.yml +11 -20
- data/.github/workflows/rubocop.yml +3 -1
- data/.gitignore +2 -0
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +4 -1
- data/CHANGES.txt +114 -0
- data/CONTRIBUTING.md +35 -0
- data/Dockerfile +7 -2
- data/Dockerfile.openssl3 +1 -1
- data/Dockerfile.zlib_ng +29 -0
- data/README.md +76 -57
- data/Rakefile +117 -30
- data/appveyor.yml +4 -16
- data/changelog.d/README.md +1 -0
- data/docker-compose.yml +2 -8
- data/lib/net/ssh/authentication/agent.rb +35 -11
- data/lib/net/ssh/authentication/ed25519.rb +141 -40
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/authentication/key_manager.rb +61 -18
- 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/pub_key_fingerprint.rb +1 -1
- data/lib/net/ssh/authentication/security_key.rb +71 -0
- data/lib/net/ssh/authentication/session.rb +34 -15
- data/lib/net/ssh/buffer.rb +6 -1
- data/lib/net/ssh/buffered_io.rb +41 -4
- data/lib/net/ssh/config.rb +30 -13
- data/lib/net/ssh/connection/channel.rb +49 -2
- data/lib/net/ssh/connection/session.rb +58 -8
- data/lib/net/ssh/errors.rb +24 -6
- data/lib/net/ssh/key_factory.rb +11 -4
- data/lib/net/ssh/known_hosts.rb +64 -11
- 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 +130 -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 +52 -4
- data/lib/net/ssh/service/forward.rb +38 -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 +60 -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 +2 -2
- data/lib/net/ssh.rb +142 -62
- data/net-ssh-public_cert.pem +19 -18
- data/net-ssh.gemspec +7 -8
- data/rakelib/changelog.rb +229 -0
- data.tar.gz.sig +0 -0
- metadata +86 -53
- metadata.gz.sig +1 -1
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
- data/lib/net/ssh/version.rb.old +0 -68
|
@@ -1,27 +1,57 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
ALGORITHM = "ED25519"
|
|
10
|
+
KEY_BYTES = 32
|
|
11
|
+
PRIVATE_KEY_BYTES = 64
|
|
12
|
+
|
|
13
|
+
class UnsupportedError < StandardError; end
|
|
20
14
|
|
|
21
|
-
|
|
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 =
|
|
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
|
-
(
|
|
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
|
-
|
|
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 =
|
|
119
|
+
key = "\x00" * (keylen + ivlen)
|
|
83
120
|
end
|
|
84
121
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 +
|
|
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
|
-
|
|
156
|
-
|
|
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(
|
|
261
|
+
PubKey.new(public_key_bytes)
|
|
173
262
|
end
|
|
174
263
|
|
|
175
264
|
def ssh_do_sign(data, sig_alg = nil)
|
|
176
|
-
|
|
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
|
|
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
|
|
@@ -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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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[:
|
|
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
|
|
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
|
|
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: :
|
|
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
|
-
#
|
|
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
|
|
@@ -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:#{
|
|
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
|