net-ssh 8.1.0 → 8.1.1
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 +4 -0
- data/lib/net/ssh/errors.rb +10 -0
- data/lib/net/ssh/known_hosts.rb +85 -30
- data/lib/net/ssh/verifiers/accept_new_or_local_tunnel.rb +3 -1
- data/lib/net/ssh/verifiers/always.rb +16 -1
- data/lib/net/ssh/verifiers/never.rb +2 -1
- data/lib/net/ssh/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +2 -2
- metadata.gz.sig +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 07eacca644427ed0543e1a428d7075c1d5fd4a0da378e83959dcd312cd7f3500
|
|
4
|
+
data.tar.gz: eb50984faecccca334a68cd2b8fc04a749010e85e3cad2acb5e868b44bd168e6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0eab425719d3294df5ece8aff6758c4a627a67b366b661d1a4475b9979c5dea5e614ab583f559fd3a4d36ccd7d8f0cb35776e99a955007830a6ed0b3b37c04bb
|
|
7
|
+
data.tar.gz: 4c5a4eaeff4882604a01156bf22322d81c864f26b0637cb0400554206c514aa137a00f9b83ed637001a7ffb4ef599d49d512862963b1dd1041db6886dae44754
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGES.txt
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
=== 8.1.1
|
|
2
|
+
|
|
3
|
+
* Security: refuse host keys marked `@revoked` in known_hosts, and certificates of such a key or signed by it, by raising the new `Net::SSH::HostKeyRevoked`, which `remember_host!` can't override; these lines were loaded as trusted keys, so every verifier except `:never` accepted the revoked key [GHSA-hjwv-j52r-xcg7]
|
|
4
|
+
|
|
1
5
|
=== 8.1.0
|
|
2
6
|
|
|
3
7
|
* BREAKING: Publickey auth offers each RSA key and certificate once, with the first `pubkey_algorithms` entry the server lists in RFC 8308 `server-sig-algs`, as OpenSSH does, so refused RSA keys no longer use up `MaxAuthTries` two or three at a time. Servers that send no `server-sig-algs` (OpenSSH 7.1 and older, some other servers) are offered RSA keys only as `ssh-rsa`, and only when it's enabled (e.g. `pubkey_algorithms: %w[+ssh-rsa]`); by default RSA keys are skipped there instead of being tried with rsa-sha2-512/256; OpenSSH 7.2 to 9.5 servers that accept only `ssh-rsa` still list rsa-sha2-* in `server-sig-algs`, so they need `ssh-rsa` first (`pubkey_algorithms: %w[^ssh-rsa]`) [#886, #1077]
|
data/lib/net/ssh/errors.rb
CHANGED
|
@@ -120,5 +120,15 @@ module Net
|
|
|
120
120
|
# you want to proceed anyway, simply call the remember_host! method on the
|
|
121
121
|
# exception, and then retry.
|
|
122
122
|
class HostKeyUnknown < HostKeyError; end
|
|
123
|
+
|
|
124
|
+
# Raised when the key given by the host, or the CA that signed its
|
|
125
|
+
# certificate, is marked @revoked in a known hosts file. A revoked key must
|
|
126
|
+
# never be accepted, so unlike the other host key errors this one can't be
|
|
127
|
+
# overridden: #remember_host! just raises it again.
|
|
128
|
+
class HostKeyRevoked < HostKeyError
|
|
129
|
+
def remember_host!
|
|
130
|
+
raise self
|
|
131
|
+
end
|
|
132
|
+
end
|
|
123
133
|
end
|
|
124
134
|
end
|
data/lib/net/ssh/known_hosts.rb
CHANGED
|
@@ -103,6 +103,22 @@ module Net
|
|
|
103
103
|
server_key.critical_options.empty?
|
|
104
104
|
end
|
|
105
105
|
end
|
|
106
|
+
|
|
107
|
+
# @revoked entry
|
|
108
|
+
class Revoked
|
|
109
|
+
def initialize(key, comment: nil)
|
|
110
|
+
@key = key
|
|
111
|
+
@comment = comment
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Returns true for the revoked key itself and for any certificate that
|
|
115
|
+
# wraps it or is signed by it, matching OpenSSH's check_key_not_revoked.
|
|
116
|
+
def matches_key?(server_key)
|
|
117
|
+
keys = [server_key]
|
|
118
|
+
keys += [server_key.key, server_key.signature_key] if server_key.respond_to?(:signature_key)
|
|
119
|
+
keys.any? { |key| key.ssh_type == @key.ssh_type && key.to_blob == @key.to_blob }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
106
122
|
end
|
|
107
123
|
|
|
108
124
|
# Represents the result of a search in known hosts
|
|
@@ -111,11 +127,17 @@ module Net
|
|
|
111
127
|
include Enumerable
|
|
112
128
|
attr_reader :host
|
|
113
129
|
|
|
114
|
-
def initialize(host_keys, host, known_hosts, options = {})
|
|
130
|
+
def initialize(host_keys, host, known_hosts, options = {}, revoked_keys = [])
|
|
115
131
|
@host_keys = host_keys
|
|
116
132
|
@host = host
|
|
117
133
|
@known_hosts = known_hosts
|
|
118
134
|
@options = options
|
|
135
|
+
@revoked_keys = revoked_keys
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns true if a @revoked known-hosts entry for this host matches +key+.
|
|
139
|
+
def revoked?(key)
|
|
140
|
+
@revoked_keys.any? { |entry| entry.matches_key?(key) }
|
|
119
141
|
end
|
|
120
142
|
|
|
121
143
|
def add_host_key(key)
|
|
@@ -161,11 +183,14 @@ module Net
|
|
|
161
183
|
# Searches all known host files (see KnownHosts.hostfiles) for all keys
|
|
162
184
|
# of the given host. Returns an enumerable of keys found.
|
|
163
185
|
def search_for(host, options = {})
|
|
164
|
-
|
|
186
|
+
files = hostfiles(options)
|
|
187
|
+
revoked_keys = files.flat_map { |file| KnownHosts.new(file).revoked_keys_for(host) }
|
|
188
|
+
HostKeys.new(search_in(files, host, options), host, self, options, revoked_keys)
|
|
165
189
|
end
|
|
166
190
|
|
|
167
191
|
# Search for all known keys for the given host, in every file given in
|
|
168
|
-
# the +files+ array. Returns the list of keys.
|
|
192
|
+
# the +files+ array. Returns the list of keys. @revoked entries are not
|
|
193
|
+
# included.
|
|
169
194
|
def search_in(files, host, options = {})
|
|
170
195
|
files.flat_map { |file| KnownHosts.new(file).keys_for(host, options) }
|
|
171
196
|
end
|
|
@@ -229,46 +254,50 @@ module Net
|
|
|
229
254
|
# "[net.ssh.test]:5555"
|
|
230
255
|
# "[1,2,3,4]:5555"
|
|
231
256
|
# "[net.ssh.test]:5555,[1.2.3.4]:5555
|
|
257
|
+
#
|
|
258
|
+
# @revoked entries are not included, see revoked_keys_for.
|
|
232
259
|
def keys_for(host, options = {})
|
|
233
260
|
keys = []
|
|
234
|
-
return keys unless File.readable?(source)
|
|
235
|
-
|
|
236
261
|
entries = host.split(/,/)
|
|
237
262
|
host_name = entries[0]
|
|
238
263
|
host_ip = entries[1]
|
|
239
264
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if line.start_with?('@')
|
|
243
|
-
marker, hosts, type, key_content, comment = line.split(' ')
|
|
244
|
-
else
|
|
245
|
-
marker = nil
|
|
246
|
-
hosts, type, key_content, comment = line.split(' ')
|
|
247
|
-
end
|
|
265
|
+
each_entry do |marker, hostlist, key_content, comment|
|
|
266
|
+
next if marker == "@revoked"
|
|
248
267
|
|
|
249
|
-
|
|
250
|
-
|
|
268
|
+
found = hostlist.any? { |pattern| match(host_name, pattern) } || known_host_hash?(hostlist, entries)
|
|
269
|
+
next unless found
|
|
251
270
|
|
|
252
|
-
|
|
271
|
+
found = hostlist.include?(host_ip) if options[:check_host_ip] && entries.size > 1 && hostlist.size > 1
|
|
272
|
+
next unless found
|
|
253
273
|
|
|
254
|
-
|
|
274
|
+
raw_key = read_key(key_content)
|
|
275
|
+
|
|
276
|
+
keys <<
|
|
277
|
+
if marker == "@cert-authority"
|
|
278
|
+
HostKeyEntries::CertAuthority.new(raw_key, comment: comment)
|
|
279
|
+
else
|
|
280
|
+
HostKeyEntries::PubKey.new(raw_key, comment: comment)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
255
283
|
|
|
256
|
-
|
|
257
|
-
|
|
284
|
+
keys
|
|
285
|
+
end
|
|
258
286
|
|
|
259
|
-
|
|
260
|
-
|
|
287
|
+
# Returns the @revoked entries for the given host, which takes the same
|
|
288
|
+
# formats as keys_for. An entry matching any of the comma-separated names
|
|
289
|
+
# counts, so :check_host_ip can't narrow a revocation.
|
|
290
|
+
def revoked_keys_for(host)
|
|
291
|
+
keys = []
|
|
292
|
+
entries = host.split(/,/)
|
|
261
293
|
|
|
262
|
-
|
|
263
|
-
|
|
294
|
+
each_entry do |marker, hostlist, key_content, comment|
|
|
295
|
+
next unless marker == "@revoked"
|
|
264
296
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
HostKeyEntries::PubKey.new(raw_key, comment: comment)
|
|
270
|
-
end
|
|
271
|
-
end
|
|
297
|
+
found = entries.any? { |entry| hostlist.any? { |pattern| match(entry, pattern) } }
|
|
298
|
+
next unless found || known_host_hash?(hostlist, entries)
|
|
299
|
+
|
|
300
|
+
keys << HostKeyEntries::Revoked.new(read_key(key_content), comment: comment)
|
|
272
301
|
end
|
|
273
302
|
|
|
274
303
|
keys
|
|
@@ -313,6 +342,32 @@ module Net
|
|
|
313
342
|
file.puts "#{host} #{key.ssh_type} #{blob}"
|
|
314
343
|
end
|
|
315
344
|
end
|
|
345
|
+
|
|
346
|
+
private
|
|
347
|
+
|
|
348
|
+
# Yields the marker, host patterns, key and comment of each usable line.
|
|
349
|
+
def each_entry
|
|
350
|
+
return unless File.readable?(source)
|
|
351
|
+
|
|
352
|
+
File.open(source) do |file|
|
|
353
|
+
file.each_line do |line|
|
|
354
|
+
fields = line.split(' ')
|
|
355
|
+
marker = fields.shift if fields.first&.start_with?('@')
|
|
356
|
+
hosts, type, key_content, comment = fields
|
|
357
|
+
|
|
358
|
+
# Skip empty line or one that is commented
|
|
359
|
+
next if hosts.nil? || hosts.start_with?('#')
|
|
360
|
+
|
|
361
|
+
next unless SUPPORTED_TYPE.include?(type)
|
|
362
|
+
|
|
363
|
+
yield marker, hosts.split(','), key_content, comment
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def read_key(key_content)
|
|
369
|
+
Net::SSH::Buffer.new(key_content.unpack("m*").first).read_key
|
|
370
|
+
end
|
|
316
371
|
end
|
|
317
372
|
end
|
|
318
373
|
end
|
|
@@ -7,11 +7,13 @@ module Net
|
|
|
7
7
|
# verify a connection if the server is the localhost and the port is a
|
|
8
8
|
# nonstandard port number. Those two conditions will typically mean the
|
|
9
9
|
# connection is being tunnelled through a forwarded port, so the known-hosts
|
|
10
|
-
# file will not be helpful (in general).
|
|
10
|
+
# file will not be helpful (in general). A key marked @revoked is refused
|
|
11
|
+
# either way.
|
|
11
12
|
class AcceptNewOrLocalTunnel < AcceptNew
|
|
12
13
|
# Tries to determine if the connection is being tunnelled, and if so,
|
|
13
14
|
# returns true. Otherwise, performs the standard strict verification.
|
|
14
15
|
def verify(arguments)
|
|
16
|
+
refuse_revoked!(arguments)
|
|
15
17
|
return true if tunnelled?(arguments)
|
|
16
18
|
|
|
17
19
|
super
|
|
@@ -10,10 +10,13 @@ module Net
|
|
|
10
10
|
# (HostKeyUnknown). This is in contrast to the "Strict" class, which will
|
|
11
11
|
# silently add the key to your known_hosts file. If the server does appear at
|
|
12
12
|
# least once, but the key given does not match any known for the server, an
|
|
13
|
-
# exception will be raised (HostKeyMismatch).
|
|
13
|
+
# exception will be raised (HostKeyMismatch). A key marked @revoked in a
|
|
14
|
+
# host file is always refused (HostKeyRevoked).
|
|
14
15
|
# Otherwise, this returns true.
|
|
15
16
|
class Always
|
|
16
17
|
def verify(arguments)
|
|
18
|
+
refuse_revoked!(arguments)
|
|
19
|
+
|
|
17
20
|
host_keys = arguments[:session].host_keys
|
|
18
21
|
|
|
19
22
|
# We've never seen this host before, so raise an exception.
|
|
@@ -47,6 +50,18 @@ module Net
|
|
|
47
50
|
|
|
48
51
|
private
|
|
49
52
|
|
|
53
|
+
# Custom :known_hosts objects may return a host_keys collection with no
|
|
54
|
+
# revocation support.
|
|
55
|
+
def refuse_revoked!(arguments)
|
|
56
|
+
host_keys = arguments[:session].host_keys
|
|
57
|
+
return unless host_keys.respond_to?(:revoked?) && host_keys.revoked?(arguments[:key])
|
|
58
|
+
|
|
59
|
+
exception = HostKeyRevoked.new("fingerprint #{arguments[:fingerprint]} " +
|
|
60
|
+
"is revoked for #{host_keys.host.inspect}")
|
|
61
|
+
exception.data = arguments
|
|
62
|
+
raise exception
|
|
63
|
+
end
|
|
64
|
+
|
|
50
65
|
# Enforces the @cert-authority certificate constraints for entries that
|
|
51
66
|
# implement them (Net::SSH::HostKeyEntries::CertAuthority). Non-certificate
|
|
52
67
|
# entries do not respond to these predicates and are left untouched.
|
|
@@ -3,7 +3,8 @@ module Net
|
|
|
3
3
|
module Verifiers
|
|
4
4
|
# This host key verifier simply allows every key it sees, without
|
|
5
5
|
# any verification. This is simple, but very insecure because it
|
|
6
|
-
# exposes you to MiTM attacks.
|
|
6
|
+
# exposes you to MiTM attacks. It does not read the known hosts files,
|
|
7
|
+
# so it accepts keys marked @revoked as well.
|
|
7
8
|
class Never
|
|
8
9
|
# Returns true.
|
|
9
10
|
def verify(arguments)
|
data/lib/net/ssh/version.rb
CHANGED
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: net-ssh
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 8.1.
|
|
4
|
+
version: 8.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jamis Buck
|
|
@@ -32,7 +32,7 @@ cert_chain:
|
|
|
32
32
|
ufHf4AX2UIkJbh7zCPkiNCqIr7MSWLNFG/9lOlHYsEJM8XujT1ofPobYx6YSFx/C
|
|
33
33
|
7HBrI7UX7awt6UvBZebhcHzyMHxg/B5PVQllPA==
|
|
34
34
|
-----END CERTIFICATE-----
|
|
35
|
-
date: 2026-11-
|
|
35
|
+
date: 2026-11-06 00:00:00.000000000 Z
|
|
36
36
|
dependencies:
|
|
37
37
|
- !ruby/object:Gem::Dependency
|
|
38
38
|
name: logger
|
metadata.gz.sig
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
����K�#��إ�TJ�q�3v|�6e`�gI;f�s�"O)�R��/�?��
|