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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGES.txt +106 -5
- data/README.md +71 -57
- data/lib/net/ssh/authentication/agent.rb +35 -11
- data/lib/net/ssh/authentication/certificate.rb +2 -2
- data/lib/net/ssh/authentication/ed25519.rb +137 -44
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/authentication/key_manager.rb +22 -11
- data/lib/net/ssh/authentication/methods/abstract.rb +6 -8
- data/lib/net/ssh/authentication/methods/hostbased.rb +3 -3
- data/lib/net/ssh/authentication/methods/publickey.rb +52 -38
- data/lib/net/ssh/authentication/security_key.rb +71 -0
- data/lib/net/ssh/authentication/session.rb +30 -17
- data/lib/net/ssh/buffer.rb +8 -3
- data/lib/net/ssh/buffered_io.rb +40 -3
- data/lib/net/ssh/config.rb +123 -50
- data/lib/net/ssh/connection/channel.rb +49 -2
- data/lib/net/ssh/connection/session.rb +151 -13
- data/lib/net/ssh/errors.rb +34 -6
- data/lib/net/ssh/key_factory.rb +7 -1
- data/lib/net/ssh/known_hosts.rb +103 -38
- 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 +127 -0
- data/lib/net/ssh/prompt.rb +43 -5
- data/lib/net/ssh/proxy/https.rb +28 -4
- data/lib/net/ssh/proxy/jump.rb +61 -8
- data/lib/net/ssh/service/forward.rb +35 -24
- data/lib/net/ssh/test.rb +2 -1
- data/lib/net/ssh/transport/algorithms.rb +92 -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 +23 -1
- data/lib/net/ssh/transport/constants.rb +1 -0
- data/lib/net/ssh/transport/gcm_cipher.rb +27 -6
- 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 +12 -0
- data/lib/net/ssh/transport/session.rb +61 -45
- data/lib/net/ssh/verifiers/accept_new_or_local_tunnel.rb +3 -1
- data/lib/net/ssh/verifiers/always.rb +16 -4
- data/lib/net/ssh/verifiers/never.rb +2 -1
- data/lib/net/ssh/version.rb +3 -3
- data/lib/net/ssh.rb +164 -63
- data.tar.gz.sig +0 -0
- metadata +33 -61
- metadata.gz.sig +0 -0
- data/.dockerignore +0 -9
- data/.github/FUNDING.yml +0 -1
- data/.github/config/rubocop_linter_action.yml +0 -4
- data/.github/workflows/ci-with-docker.yml +0 -58
- data/.github/workflows/ci.yml +0 -99
- data/.github/workflows/rubocop.yml +0 -16
- data/.gitignore +0 -15
- data/.rubocop.yml +0 -22
- data/.rubocop_todo.yml +0 -1083
- data/DEVELOPMENT.md +0 -23
- data/Dockerfile +0 -32
- data/Dockerfile.openssl3 +0 -17
- data/Dockerfile.zlib_ng +0 -29
- data/Gemfile +0 -13
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
- data/ISSUE_TEMPLATE.md +0 -30
- data/Manifest +0 -132
- data/Rakefile +0 -196
- data/SECURITY.md +0 -4
- data/THANKS.txt +0 -110
- data/appveyor.yml +0 -58
- data/docker-compose.yml +0 -23
- data/net-ssh-public_cert.pem +0 -21
- data/net-ssh.gemspec +0 -48
- data/support/ssh_tunnel_bug.rb +0 -65
|
@@ -1,25 +1,57 @@
|
|
|
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
|
|
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
|
-
(
|
|
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
|
-
|
|
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 =
|
|
119
|
+
key = "\x00" * (keylen + ivlen)
|
|
91
120
|
end
|
|
92
121
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 + [
|
|
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
|
-
|
|
164
|
-
|
|
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(
|
|
261
|
+
PubKey.new(public_key_bytes)
|
|
181
262
|
end
|
|
182
263
|
|
|
183
264
|
def ssh_do_sign(data, sig_alg = nil)
|
|
184
|
-
|
|
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
|
|
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
|
|
@@ -124,17 +124,16 @@ module Net
|
|
|
124
124
|
|
|
125
125
|
user_identities = load_identities(prepared_identities, false, true)
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
#
|
|
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
|
|
|
@@ -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
|
|
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
|
|
49
|
+
info { "hostbased succeeded (#{fingerprint(identity)})" }
|
|
50
50
|
return true
|
|
51
51
|
when USERAUTH_FAILURE
|
|
52
|
-
info { "hostbased failed (#{identity
|
|
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
|
|
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
|
|
88
|
+
debug { "publickey succeeded (#{fingerprint(identity)})" }
|
|
68
89
|
return true
|
|
69
90
|
when USERAUTH_FAILURE
|
|
70
|
-
debug { "publickey failed (#{identity
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|