net-ssh 8.0.4 → 8.1.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/CHANGES.txt +10 -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/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/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: 8a3eb4e5ac79bff25a68aaecd501c437bf0145a7458c8edd00f90e3b9ae14641
|
|
4
|
+
data.tar.gz: cabb7b40f56ed19ca61a1a09eccff1745618a62e258a9c2ba1804fdfbf02c44b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e748a76a0dff8328c8d12da44f8372814c15260fa04f428459145f626cbde231173475d0b8547f907f0d5c983e4abf9ca8f23ef0eb26469a0558d259ee7997e1
|
|
7
|
+
data.tar.gz: c54425a607b9056a9bbd15985791911948a86548a1a6b0f8894aa193dc35aa703a2435ad25c11a0b7c622a7b6605d4815dcd8006864f311c8c0443b8a02acd63
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGES.txt
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
=== 8.1.0
|
|
2
|
+
|
|
3
|
+
* 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]
|
|
4
|
+
* 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]
|
|
5
|
+
* 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]
|
|
6
|
+
* Package only `lib`, `CHANGES.txt`, `LICENSE.txt` and `README.md` in the gem, about 10% smaller. Thanks @yuri-zubov [#1081]
|
|
7
|
+
* 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]
|
|
8
|
+
* The default password prompt runs the `SSH_ASKPASS` program when there is no terminal, following the OpenSSH `SSH_ASKPASS_REQUIRE` and `DISPLAY` rules [#1075]
|
|
9
|
+
* 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]
|
|
10
|
+
|
|
1
11
|
=== 8.0.4
|
|
2
12
|
|
|
3
13
|
* 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/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
|
data/lib/net/ssh/prompt.rb
CHANGED
|
@@ -26,22 +26,48 @@ module Net
|
|
|
26
26
|
|
|
27
27
|
def initialize(options = {}); end
|
|
28
28
|
|
|
29
|
+
# Whether the default prompt has a terminal or an askpass program to ask with.
|
|
30
|
+
def self.can_ask?
|
|
31
|
+
$stdin.tty? || !askpass.nil?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The SSH_ASKPASS program to ask with instead of the terminal, chosen with
|
|
35
|
+
# the SSH_ASKPASS_REQUIRE and DISPLAY rules described in ssh(1).
|
|
36
|
+
def self.askpass
|
|
37
|
+
program = ENV['SSH_ASKPASS'].to_s
|
|
38
|
+
return nil if program.empty?
|
|
39
|
+
|
|
40
|
+
display = !ENV['DISPLAY'].to_s.empty? || !ENV['WAYLAND_DISPLAY'].to_s.empty?
|
|
41
|
+
case ENV['SSH_ASKPASS_REQUIRE'].to_s.downcase
|
|
42
|
+
when 'force' then program
|
|
43
|
+
when 'never' then nil
|
|
44
|
+
when 'prefer' then (program if display)
|
|
45
|
+
else (program if display && !$stdin.tty?)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
29
49
|
# default prompt object implementation. More sophisticated implemenetations
|
|
30
50
|
# might implement caching.
|
|
31
51
|
class Prompter
|
|
32
52
|
def initialize(info)
|
|
53
|
+
@askpass = Prompt.askpass
|
|
54
|
+
@header = []
|
|
33
55
|
if info[:type] == 'keyboard-interactive'
|
|
34
|
-
|
|
35
|
-
|
|
56
|
+
@header << info[:name] unless info[:name].empty?
|
|
57
|
+
@header << info[:instruction] unless info[:instruction].empty?
|
|
36
58
|
end
|
|
59
|
+
$stdout.puts(@header) unless @askpass || @header.empty?
|
|
37
60
|
end
|
|
38
61
|
|
|
39
62
|
# ask input from user, a prompter might ask for multiple inputs
|
|
40
63
|
# (like user and password) in a single session.
|
|
41
64
|
def ask(prompt, echo = true)
|
|
65
|
+
return ask_askpass(prompt) if @askpass
|
|
66
|
+
|
|
42
67
|
$stdout.print(prompt)
|
|
43
68
|
$stdout.flush
|
|
44
|
-
|
|
69
|
+
hide = !echo && $stdin.tty?
|
|
70
|
+
ret = hide ? $stdin.noecho(&:gets) : $stdin.gets
|
|
45
71
|
$stdout.print("\n") unless echo
|
|
46
72
|
ret.chomp
|
|
47
73
|
end
|
|
@@ -49,6 +75,18 @@ module Net
|
|
|
49
75
|
# success method will be called when the password was accepted
|
|
50
76
|
# It's a good time to save password asked to a cache.
|
|
51
77
|
def success; end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Like OpenSSH, a cancelled or failed askpass answers with an empty string.
|
|
82
|
+
def ask_askpass(prompt)
|
|
83
|
+
text = (@header + [prompt]).join("\n")
|
|
84
|
+
@header = []
|
|
85
|
+
answer = IO.popen([@askpass, text], &:read)
|
|
86
|
+
Process.last_status.success? ? answer.chomp : ""
|
|
87
|
+
rescue SystemCallError
|
|
88
|
+
""
|
|
89
|
+
end
|
|
52
90
|
end
|
|
53
91
|
|
|
54
92
|
# start password session. Multiple questions might be asked multiple times
|
|
@@ -424,7 +424,10 @@ module Net
|
|
|
424
424
|
# a KEXINIT packet to send to the server. It does not actually send it,
|
|
425
425
|
# it simply builds the packet and returns it.
|
|
426
426
|
def build_client_algorithm_packet
|
|
427
|
-
|
|
427
|
+
kex_names = algorithms[:kex]
|
|
428
|
+
# RFC 8308: ext-info-c is only meaningful in the first key exchange.
|
|
429
|
+
kex_names += %w[ext-info-c] unless session_id
|
|
430
|
+
kex = kex_names.join(",")
|
|
428
431
|
host_key = algorithms[:host_key].join(",")
|
|
429
432
|
encryption = algorithms[:encryption].join(",")
|
|
430
433
|
hmac = algorithms[:hmac].join(",")
|
|
@@ -56,6 +56,11 @@ module Net
|
|
|
56
56
|
# The hash of options that were given to the object at initialization.
|
|
57
57
|
attr_reader :options
|
|
58
58
|
|
|
59
|
+
# The signature algorithms the server accepts for publickey
|
|
60
|
+
# authentication, from its RFC 8308 server-sig-algs extension, or nil if
|
|
61
|
+
# it did not send one.
|
|
62
|
+
attr_reader :server_sig_algs
|
|
63
|
+
|
|
59
64
|
# Instantiates a new transport layer abstraction. This will block until
|
|
60
65
|
# the initial key exchange completes, leaving you with a ready-to-use
|
|
61
66
|
# transport session.
|
|
@@ -219,9 +224,8 @@ module Net
|
|
|
219
224
|
when DEBUG
|
|
220
225
|
send(packet[:always_display] ? :fatal : :debug) { packet[:message] }
|
|
221
226
|
|
|
222
|
-
# We don't advertise ext-info-c, but some servers send EXT_INFO anyway (#955).
|
|
223
227
|
when EXT_INFO
|
|
224
|
-
|
|
228
|
+
accept_ext_info(packet)
|
|
225
229
|
|
|
226
230
|
when KEXINIT
|
|
227
231
|
algorithms.accept_kexinit(packet)
|
|
@@ -300,6 +304,20 @@ module Net
|
|
|
300
304
|
|
|
301
305
|
private
|
|
302
306
|
|
|
307
|
+
# Parses an RFC 8308 EXT_INFO packet, which servers send after the first
|
|
308
|
+
# NEWKEYS and possibly again just before USERAUTH_SUCCESS.
|
|
309
|
+
def accept_ext_info(packet)
|
|
310
|
+
count = packet.read_long or raise Net::SSH::Exception, "malformed EXT_INFO packet from server"
|
|
311
|
+
count.times do
|
|
312
|
+
name = packet.read_string
|
|
313
|
+
value = packet.read_string
|
|
314
|
+
raise Net::SSH::Exception, "malformed EXT_INFO packet from server" if name.nil? || value.nil?
|
|
315
|
+
|
|
316
|
+
debug { "EXT_INFO #{name}: #{value}" }
|
|
317
|
+
@server_sig_algs = value.split(",") if name == "server-sig-algs"
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
303
321
|
def shift_queue
|
|
304
322
|
packet = @queue.shift
|
|
305
323
|
@queued_bytes -= packet.length
|
data/lib/net/ssh/version.rb
CHANGED
|
@@ -49,10 +49,10 @@ module Net
|
|
|
49
49
|
MAJOR = 8
|
|
50
50
|
|
|
51
51
|
# The minor component of this version of the Net::SSH library
|
|
52
|
-
MINOR =
|
|
52
|
+
MINOR = 1
|
|
53
53
|
|
|
54
54
|
# The tiny component of this version of the Net::SSH library
|
|
55
|
-
TINY =
|
|
55
|
+
TINY = 0
|
|
56
56
|
|
|
57
57
|
# The prerelease component of this version of the Net::SSH library
|
|
58
58
|
# nil allowed
|
data/lib/net/ssh.rb
CHANGED
|
@@ -69,7 +69,7 @@ module Net
|
|
|
69
69
|
host_name user properties passphrase keys_only max_pkt_size
|
|
70
70
|
max_win_size send_env set_env use_agent number_of_password_prompts
|
|
71
71
|
append_all_supported_algorithms non_interactive password_prompt
|
|
72
|
-
agent_socket_factory minimum_dh_bits verify_host_key
|
|
72
|
+
prompt_without_tty agent_socket_factory minimum_dh_bits verify_host_key
|
|
73
73
|
fingerprint_hash check_host_ip pubkey_algorithms no_delay
|
|
74
74
|
control_master control_path preopen_channels
|
|
75
75
|
]
|
|
@@ -216,7 +216,10 @@ module Net
|
|
|
216
216
|
# * :non_interactive => set to true if your app is non interactive and prefers
|
|
217
217
|
# authentication failure vs password prompt. Non-interactive applications
|
|
218
218
|
# should set it to true to prefer failing a password/etc auth methods vs.
|
|
219
|
-
# asking for password.
|
|
219
|
+
# asking for password. Like OpenSSH, Net::SSH never prompts when $stdin is
|
|
220
|
+
# not a TTY (e.g. in a background job), so it defaults to +true+ then,
|
|
221
|
+
# unless :password_prompt or :prompt_without_tty is given or an
|
|
222
|
+
# SSH_ASKPASS program can be used (see Net::SSH::Prompt.askpass).
|
|
220
223
|
# * :passphrase => the passphrase to use when loading a private key (default
|
|
221
224
|
# is +nil+, for no passphrase)
|
|
222
225
|
# * :password => the password to use to login
|
|
@@ -227,6 +230,10 @@ module Net
|
|
|
227
230
|
# costs one round trip instead of two. The spare counts against the
|
|
228
231
|
# server's MaxSessions; if the server refuses it, pre-opening stops for
|
|
229
232
|
# the session. Defaults to +false+.
|
|
233
|
+
# * :prompt_without_tty => set to +true+ to prompt for passwords, key
|
|
234
|
+
# passphrases and keyboard-interactive responses on $stdout/$stdin even
|
|
235
|
+
# when $stdin is not a TTY, e.g. to read a password piped into the
|
|
236
|
+
# process. Defaults to +false+.
|
|
230
237
|
# * :properties => a hash of key/value pairs to add to the new connection's
|
|
231
238
|
# properties (see Net::SSH::Connection::Session#properties)
|
|
232
239
|
# * :proxy => a proxy instance (see Proxy) to use when connecting
|
|
@@ -306,7 +313,7 @@ module Net
|
|
|
306
313
|
assign_defaults(options)
|
|
307
314
|
|
|
308
315
|
options[:user] = user if user
|
|
309
|
-
options = configuration_for(host, options.fetch(:config, true)).merge(options)
|
|
316
|
+
options = configuration_for(host, options.fetch(:config, true), user: options[:user]).merge(options)
|
|
310
317
|
host = options.fetch(:host_name, host)
|
|
311
318
|
|
|
312
319
|
options[:check_host_ip] = true unless options.key?(:check_host_ip)
|
|
@@ -365,15 +372,17 @@ module Net
|
|
|
365
372
|
# be a file name (or array of file names) of SSH configuration file(s)
|
|
366
373
|
# to read.
|
|
367
374
|
#
|
|
375
|
+
# +user+ is the explicitly requested remote user, matched by <tt>Match User</tt>.
|
|
376
|
+
#
|
|
368
377
|
# See Net::SSH::Config for the full description of all supported options.
|
|
369
|
-
def self.configuration_for(host, use_ssh_config)
|
|
378
|
+
def self.configuration_for(host, use_ssh_config, user: nil)
|
|
370
379
|
files = case use_ssh_config
|
|
371
380
|
when true then Net::SSH::Config.expandable_default_files
|
|
372
381
|
when false, nil then return {}
|
|
373
382
|
else Array(use_ssh_config)
|
|
374
383
|
end
|
|
375
384
|
|
|
376
|
-
Net::SSH::Config.for(host, files)
|
|
385
|
+
Net::SSH::Config.for(host, files, user: user)
|
|
377
386
|
end
|
|
378
387
|
|
|
379
388
|
def self.assign_defaults(options)
|
|
@@ -382,6 +391,9 @@ module Net
|
|
|
382
391
|
options[:logger].level = Logger::FATAL
|
|
383
392
|
end
|
|
384
393
|
|
|
394
|
+
unless options[:password_prompt] || options[:prompt_without_tty] || Prompt.can_ask?
|
|
395
|
+
options[:non_interactive] = true
|
|
396
|
+
end
|
|
385
397
|
options[:password_prompt] ||= Prompt.default(options)
|
|
386
398
|
|
|
387
399
|
%i[password passphrase].each do |key|
|
data.tar.gz.sig
CHANGED
|
Binary file
|