net-ssh 8.0.4 → 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 +14 -0
- data/lib/net/ssh/authentication/certificate.rb +2 -2
- data/lib/net/ssh/authentication/methods/abstract.rb +4 -0
- data/lib/net/ssh/authentication/methods/hostbased.rb +3 -3
- data/lib/net/ssh/authentication/methods/publickey.rb +50 -38
- data/lib/net/ssh/authentication/session.rb +2 -1
- data/lib/net/ssh/buffer.rb +2 -2
- data/lib/net/ssh/config.rb +95 -38
- data/lib/net/ssh/errors.rb +10 -0
- data/lib/net/ssh/known_hosts.rb +85 -30
- data/lib/net/ssh/multiplex.rb +3 -2
- data/lib/net/ssh/prompt.rb +41 -3
- data/lib/net/ssh/transport/algorithms.rb +4 -1
- data/lib/net/ssh/transport/session.rb +20 -2
- 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 +2 -2
- data/lib/net/ssh.rb +17 -5
- data.tar.gz.sig +0 -0
- metadata +3 -31
- metadata.gz.sig +1 -1
- data/.dockerignore +0 -9
- data/.github/FUNDING.yml +0 -1
- data/.github/config/rubocop_linter_action.yml +0 -4
- data/.github/pull_request_template.md +0 -1
- data/.github/workflows/changelog.yml +0 -18
- data/.github/workflows/ci-with-docker.yml +0 -55
- data/.github/workflows/ci.yml +0 -84
- data/.github/workflows/rubocop.yml +0 -18
- data/.gitignore +0 -15
- data/.rubocop.yml +0 -26
- data/.rubocop_todo.yml +0 -1077
- data/CONTRIBUTING.md +0 -34
- data/DEVELOPMENT.md +0 -23
- data/Dockerfile +0 -32
- data/Dockerfile.openssl3 +0 -17
- data/Dockerfile.zlib_ng +0 -29
- data/Gemfile +0 -15
- data/ISSUE_TEMPLATE.md +0 -30
- data/Manifest +0 -132
- data/Rakefile +0 -214
- data/SECURITY.md +0 -4
- data/THANKS.txt +0 -110
- data/appveyor.yml +0 -46
- data/changelog.d/README.md +0 -1
- data/docker-compose.yml +0 -17
- data/net-ssh-public_cert.pem +0 -21
- data/net-ssh.gemspec +0 -45
- data/support/ssh_tunnel_bug.rb +0 -65
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,17 @@
|
|
|
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
|
+
|
|
5
|
+
=== 8.1.0
|
|
6
|
+
|
|
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]
|
|
8
|
+
* BREAKING: A `Match` block containing a condition net-ssh does not support (`exec`, `canonical`, `final`, `localnetwork`, `tagged`, ...) no longer matches and prints a warning, where the condition used to be ignored; `Match User`, `OriginalHost` and `LocalUser` are now evaluated, so e.g. `Match User x Host y` no longer applies to every user [#782, #1076]
|
|
9
|
+
* BREAKING: Net::SSH.start no longer prompts for passwords, passphrases or keyboard-interactive responses when $stdin is not a TTY, matching OpenSSH; this fixes Errno::EPIPE in background jobs. Pass `prompt_without_tty: true` to prompt anyway, e.g. for a piped password [#967, #1073]
|
|
10
|
+
* Package only `lib`, `CHANGES.txt`, `LICENSE.txt` and `README.md` in the gem, about 10% smaller. Thanks @yuri-zubov [#1081]
|
|
11
|
+
* Buffer#append now forces BINARY encoding so Channel#send_data chunks multibyte UTF-8 data by bytes instead of characters and no longer exceeds the remote maximum packet size [#1080, #1079]
|
|
12
|
+
* The default password prompt runs the `SSH_ASKPASS` program when there is no terminal, following the OpenSSH `SSH_ASKPASS_REQUIRE` and `DISPLAY` rules [#1075]
|
|
13
|
+
* Publickey and hostbased authentication now log key fingerprints using `:fingerprint_hash` (default SHA256) instead of MD5, fixing a crash with debug logging on FIPS-enabled OpenSSL [#936, #1074]
|
|
14
|
+
|
|
1
15
|
=== 8.0.4
|
|
2
16
|
|
|
3
17
|
* Security: validate every ProxyJump hop, not only the first, as [user@]host[:port] (refusing a path, query, fragment or password) and shell-escape the hops passed to ssh -J; a hop such as `evil/x&$(cmd)` ran `cmd` through the shell [GHSA-g2r2-x3p2-5677]
|
|
@@ -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
|
|
@@ -46,7 +65,7 @@ module Net
|
|
|
46
65
|
end
|
|
47
66
|
|
|
48
67
|
def authenticate_with_alg(identity, next_service, username, alg, sig_alg = nil)
|
|
49
|
-
debug { "trying publickey (#{identity
|
|
68
|
+
debug { "trying publickey (#{fingerprint(identity)}) alg #{alg}" }
|
|
50
69
|
send_request(identity, username, next_service, alg)
|
|
51
70
|
|
|
52
71
|
message = session.next_message
|
|
@@ -66,10 +85,10 @@ module Net
|
|
|
66
85
|
|
|
67
86
|
case message.type
|
|
68
87
|
when USERAUTH_SUCCESS
|
|
69
|
-
debug { "publickey succeeded (#{identity
|
|
88
|
+
debug { "publickey succeeded (#{fingerprint(identity)})" }
|
|
70
89
|
return true
|
|
71
90
|
when USERAUTH_FAILURE
|
|
72
|
-
debug { "publickey failed (#{identity
|
|
91
|
+
debug { "publickey failed (#{fingerprint(identity)})" }
|
|
73
92
|
|
|
74
93
|
raise Net::SSH::Authentication::DisallowedMethod unless
|
|
75
94
|
message[:authentications].split(/,/).include? 'publickey'
|
|
@@ -94,43 +113,36 @@ module Net
|
|
|
94
113
|
# username, with the given identity (public key). Returns +true+ if
|
|
95
114
|
# successful, or +false+ otherwise.
|
|
96
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)
|
|
97
128
|
type = identity.ssh_type
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return true
|
|
115
|
-
end
|
|
116
|
-
when "rsa-sha2-256-cert-v01@openssh.com"
|
|
117
|
-
if authenticate_with_alg(identity, next_service, username, pk_alg, "rsa-sha2-256")
|
|
118
|
-
# success
|
|
119
|
-
return true
|
|
120
|
-
end
|
|
121
|
-
when "ssh-rsa-cert-v01@openssh.com"
|
|
122
|
-
if authenticate_with_alg(identity, next_service, username, pk_alg)
|
|
123
|
-
# success
|
|
124
|
-
return true
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
elsif pubkey_algorithms.include?(type) && authenticate_with_alg(identity, next_service, username, type)
|
|
129
|
-
# success
|
|
130
|
-
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)
|
|
131
145
|
end
|
|
132
|
-
# failure
|
|
133
|
-
return false
|
|
134
146
|
end
|
|
135
147
|
end
|
|
136
148
|
end
|
|
@@ -137,7 +137,8 @@ module Net
|
|
|
137
137
|
begin
|
|
138
138
|
auth_class = Methods.const_get(method_name.split(/\W+/).map { |p| p.capitalize }.join)
|
|
139
139
|
method = auth_class.new(self,
|
|
140
|
-
key_manager: key_manager, password_prompt: options[:password_prompt]
|
|
140
|
+
key_manager: key_manager, password_prompt: options[:password_prompt],
|
|
141
|
+
fingerprint_hash: options[:fingerprint_hash])
|
|
141
142
|
rescue NameError
|
|
142
143
|
debug {"Mechanism #{method_name} was requested, but isn't a known type. Ignoring it."}
|
|
143
144
|
return false
|
data/lib/net/ssh/buffer.rb
CHANGED
|
@@ -145,7 +145,7 @@ module Net
|
|
|
145
145
|
# Appends the given text to the end of the buffer. Does not alter the
|
|
146
146
|
# read position. Returns the buffer object itself.
|
|
147
147
|
def append(text)
|
|
148
|
-
@content << text
|
|
148
|
+
@content << text.b
|
|
149
149
|
self
|
|
150
150
|
end
|
|
151
151
|
|
|
@@ -359,7 +359,7 @@ module Net
|
|
|
359
359
|
# Writes the given data literally into the string. Does not alter the
|
|
360
360
|
# read position. Returns the buffer object.
|
|
361
361
|
def write(*data)
|
|
362
|
-
data.each { |datum| @content << datum.
|
|
362
|
+
data.each { |datum| @content << datum.b }
|
|
363
363
|
self
|
|
364
364
|
end
|
|
365
365
|
|
data/lib/net/ssh/config.rb
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
require 'etc'
|
|
2
|
+
require 'set'
|
|
3
|
+
|
|
1
4
|
module Net
|
|
2
5
|
module SSH
|
|
3
6
|
# The Net::SSH::Config class is used to parse OpenSSH configuration files,
|
|
@@ -69,9 +72,19 @@ module Net
|
|
|
69
72
|
# given +files+ (defaulting to the list of files returned by
|
|
70
73
|
# #default_files), translates the resulting hash into the options
|
|
71
74
|
# recognized by Net::SSH, and returns them.
|
|
72
|
-
|
|
75
|
+
#
|
|
76
|
+
# +user+ is the remote user given explicitly (e.g. to Net::SSH.start),
|
|
77
|
+
# used by <tt>Match User</tt>; when nil, the User set so far by the
|
|
78
|
+
# config, then the local user name, is used, as in OpenSSH.
|
|
79
|
+
#
|
|
80
|
+
# Match supports +all+, +Host+, +OriginalHost+, +User+ and +LocalUser+,
|
|
81
|
+
# each optionally negated with a leading "!". A Match block with any
|
|
82
|
+
# other condition (+exec+, +canonical+, +final+, +localnetwork+,
|
|
83
|
+
# +tagged+, ...) never matches, and a warning naming the condition is
|
|
84
|
+
# printed with Kernel#warn once per config line.
|
|
85
|
+
def for(host, files = expandable_default_files, user: nil)
|
|
73
86
|
translate(files.inject({}) { |settings, file|
|
|
74
|
-
load(file, host, settings)
|
|
87
|
+
load(file, host, settings, user: user)
|
|
75
88
|
})
|
|
76
89
|
end
|
|
77
90
|
|
|
@@ -81,15 +94,16 @@ module Net
|
|
|
81
94
|
# ones. Returns a hash containing the OpenSSH options. (See
|
|
82
95
|
# #translate for how to convert the OpenSSH options into Net::SSH
|
|
83
96
|
# options.)
|
|
84
|
-
def load(path, host, settings = {}, base_dir = nil)
|
|
97
|
+
def load(path, host, settings = {}, base_dir = nil, user: nil)
|
|
85
98
|
file = File.expand_path(path)
|
|
86
99
|
base_dir ||= File.dirname(file)
|
|
87
100
|
return settings unless File.readable?(file)
|
|
88
101
|
|
|
89
102
|
globals = {}
|
|
90
|
-
block_matched = false
|
|
91
|
-
|
|
103
|
+
block_matched = block_seen = false
|
|
104
|
+
lineno = 0
|
|
92
105
|
IO.foreach(file) do |line|
|
|
106
|
+
lineno += 1
|
|
93
107
|
next if line =~ /^\s*(?:#.*)?$/
|
|
94
108
|
|
|
95
109
|
# As in OpenSSH, the keyword ends at the first whitespace or "=".
|
|
@@ -127,7 +141,7 @@ module Net
|
|
|
127
141
|
block_seen = true
|
|
128
142
|
settings[key] = host
|
|
129
143
|
elsif key == 'match'
|
|
130
|
-
block_matched = eval_match_conditions(value, host, settings)
|
|
144
|
+
block_matched = eval_match_conditions(value, host, [user, settings['user'], globals['user']].compact.first, "#{file} line #{lineno}")
|
|
131
145
|
block_seen = true
|
|
132
146
|
elsif !block_seen
|
|
133
147
|
case key
|
|
@@ -135,7 +149,7 @@ module Net
|
|
|
135
149
|
(globals[key] ||= []) << value
|
|
136
150
|
when 'include'
|
|
137
151
|
included_file_paths(base_dir, value).each do |file_path|
|
|
138
|
-
globals = load(file_path, host, globals, base_dir)
|
|
152
|
+
globals = load(file_path, host, globals, base_dir, user: user)
|
|
139
153
|
end
|
|
140
154
|
else
|
|
141
155
|
globals[key] = value unless settings.key?(key)
|
|
@@ -146,7 +160,7 @@ module Net
|
|
|
146
160
|
(settings[key] ||= []) << value
|
|
147
161
|
when 'include'
|
|
148
162
|
included_file_paths(base_dir, value).each do |file_path|
|
|
149
|
-
settings = load(file_path, host, settings, base_dir)
|
|
163
|
+
settings = load(file_path, host, settings, base_dir, user: user)
|
|
150
164
|
end
|
|
151
165
|
else
|
|
152
166
|
settings[key] = value unless settings.key?(key)
|
|
@@ -325,7 +339,7 @@ module Net
|
|
|
325
339
|
|
|
326
340
|
# Converts an ssh_config pattern into a regex for matching against
|
|
327
341
|
# host names.
|
|
328
|
-
def pattern2regex(pattern)
|
|
342
|
+
def pattern2regex(pattern, ignore_case: true)
|
|
329
343
|
tail = pattern
|
|
330
344
|
prefix = String.new
|
|
331
345
|
while !tail.empty? do
|
|
@@ -341,7 +355,7 @@ module Net
|
|
|
341
355
|
fail "Unpexpcted sep:#{sep}"
|
|
342
356
|
end
|
|
343
357
|
end
|
|
344
|
-
Regexp.new("^" + prefix + "$",
|
|
358
|
+
Regexp.new("^" + prefix + "$", ignore_case)
|
|
345
359
|
end
|
|
346
360
|
|
|
347
361
|
# Converts the given size into an integer number of bytes.
|
|
@@ -374,39 +388,82 @@ module Net
|
|
|
374
388
|
str.scan(/([^"\s]+)?(?:"([^"]+)")?\s*/).map(&:join)
|
|
375
389
|
end
|
|
376
390
|
|
|
377
|
-
|
|
391
|
+
MATCH_CONDITIONS_WITHOUT_ARGUMENT = %w[all canonical final].freeze
|
|
392
|
+
SUPPORTED_MATCH_CONDITIONS = %w[all host originalhost user localuser].freeze
|
|
393
|
+
|
|
394
|
+
def eval_match_conditions(condition, host, remote_user, location)
|
|
378
395
|
# Not using `\s` for whitespace matching as canonical
|
|
379
396
|
# ssh_config parser implementation (OpenSSH) has specific character set.
|
|
380
397
|
# Ref: https://github.com/openssh/openssh-portable/blob/2581333d564d8697837729b3d07d45738eaf5a54/misc.c#L237-L239
|
|
381
|
-
|
|
382
|
-
return true if
|
|
383
|
-
|
|
384
|
-
conditions =
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
398
|
+
tokens = condition.split(/[ \t\r\n]+|(?<!=)=(?!=)/).reject(&:empty?)
|
|
399
|
+
return true if tokens == ["all"]
|
|
400
|
+
|
|
401
|
+
conditions = parse_match_conditions(tokens)
|
|
402
|
+
unsupported = conditions.find { |_kind, name, exprs|
|
|
403
|
+
!SUPPORTED_MATCH_CONDITIONS.include?(name) || (name != 'all' && exprs.nil?)
|
|
404
|
+
}
|
|
405
|
+
if unsupported
|
|
406
|
+
warn_unsupported_match(unsupported[0], location)
|
|
407
|
+
return false
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
!conditions.empty? && conditions.all? do |kind, name, exprs|
|
|
411
|
+
kind.start_with?('!') ^ match_condition?(name, exprs, host, remote_user || local_user, conditions.size)
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def parse_match_conditions(tokens)
|
|
416
|
+
conditions = []
|
|
417
|
+
until tokens.empty?
|
|
418
|
+
kind = tokens.shift
|
|
419
|
+
name = kind.delete_prefix('!').downcase
|
|
420
|
+
exprs = tokens.shift unless MATCH_CONDITIONS_WITHOUT_ARGUMENT.include?(name)
|
|
421
|
+
conditions << [kind, name, exprs && unquote(exprs)]
|
|
422
|
+
end
|
|
423
|
+
conditions
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def match_condition?(name, exprs, host, remote_user, condition_count)
|
|
427
|
+
case name
|
|
428
|
+
when "all"
|
|
429
|
+
raise "all cannot be mixed with other conditions" if condition_count > 1
|
|
430
|
+
|
|
431
|
+
true
|
|
432
|
+
when "host"
|
|
433
|
+
negated = exprs.start_with?('!')
|
|
434
|
+
exprs = exprs[1..-1] if negated
|
|
435
|
+
negated ^ exprs.split(",").any? { |expr| host =~ pattern2regex(expr) }
|
|
436
|
+
when "originalhost"
|
|
437
|
+
match_pattern_list(host, exprs)
|
|
438
|
+
when "user"
|
|
439
|
+
match_pattern_list(remote_user, exprs, ignore_case: false)
|
|
440
|
+
when "localuser"
|
|
441
|
+
match_pattern_list(local_user, exprs, ignore_case: false)
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# OpenSSH's match_pattern_list: true if any pattern matches and no "!pattern" does.
|
|
446
|
+
def match_pattern_list(string, list, ignore_case: true)
|
|
447
|
+
matched = false
|
|
448
|
+
list.split(",").each do |pattern|
|
|
449
|
+
negated = pattern.start_with?('!')
|
|
450
|
+
next unless string =~ pattern2regex(negated ? pattern[1..-1] : pattern, ignore_case: ignore_case)
|
|
451
|
+
return false if negated
|
|
452
|
+
|
|
453
|
+
matched = true
|
|
407
454
|
end
|
|
455
|
+
matched
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def warn_unsupported_match(kind, location)
|
|
459
|
+
@warned_unsupported_matches ||= Set.new
|
|
460
|
+
return unless @warned_unsupported_matches.add?([location, kind])
|
|
461
|
+
|
|
462
|
+
Kernel.warn("net-ssh: #{location}: unsupported Match condition '#{kind}', the Match block is skipped")
|
|
463
|
+
end
|
|
408
464
|
|
|
409
|
-
|
|
465
|
+
def local_user
|
|
466
|
+
Etc.getpwuid&.name || Etc.getlogin
|
|
410
467
|
end
|
|
411
468
|
|
|
412
469
|
# As in OpenSSH's argv_split, "#" starts a comment only at the start of a token and outside quotes.
|
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
|
data/lib/net/ssh/multiplex.rb
CHANGED
|
@@ -25,8 +25,9 @@ module Net
|
|
|
25
25
|
keepalive keepalive_interval keepalive_maxcount kex key_data keycert_data
|
|
26
26
|
keycerts keys keys_only known_hosts languages minimum_dh_bits no_delay
|
|
27
27
|
non_interactive number_of_password_prompts passphrase password
|
|
28
|
-
password_prompt proxy pubkey_algorithms
|
|
29
|
-
rekey_packet_limit use_agent
|
|
28
|
+
password_prompt prompt_without_tty proxy pubkey_algorithms
|
|
29
|
+
rekey_blocks_limit rekey_limit rekey_packet_limit use_agent
|
|
30
|
+
user_known_hosts_file verify_host_key
|
|
30
31
|
].freeze
|
|
31
32
|
|
|
32
33
|
VALUES = [:auto, false, nil].freeze
|