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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGES.txt +10 -0
  4. data/lib/net/ssh/authentication/certificate.rb +2 -2
  5. data/lib/net/ssh/authentication/methods/abstract.rb +4 -0
  6. data/lib/net/ssh/authentication/methods/hostbased.rb +3 -3
  7. data/lib/net/ssh/authentication/methods/publickey.rb +50 -38
  8. data/lib/net/ssh/authentication/session.rb +2 -1
  9. data/lib/net/ssh/buffer.rb +2 -2
  10. data/lib/net/ssh/config.rb +95 -38
  11. data/lib/net/ssh/multiplex.rb +3 -2
  12. data/lib/net/ssh/prompt.rb +41 -3
  13. data/lib/net/ssh/transport/algorithms.rb +4 -1
  14. data/lib/net/ssh/transport/session.rb +20 -2
  15. data/lib/net/ssh/version.rb +2 -2
  16. data/lib/net/ssh.rb +17 -5
  17. data.tar.gz.sig +0 -0
  18. metadata +3 -31
  19. metadata.gz.sig +1 -1
  20. data/.dockerignore +0 -9
  21. data/.github/FUNDING.yml +0 -1
  22. data/.github/config/rubocop_linter_action.yml +0 -4
  23. data/.github/pull_request_template.md +0 -1
  24. data/.github/workflows/changelog.yml +0 -18
  25. data/.github/workflows/ci-with-docker.yml +0 -55
  26. data/.github/workflows/ci.yml +0 -84
  27. data/.github/workflows/rubocop.yml +0 -18
  28. data/.gitignore +0 -15
  29. data/.rubocop.yml +0 -26
  30. data/.rubocop_todo.yml +0 -1077
  31. data/CONTRIBUTING.md +0 -34
  32. data/DEVELOPMENT.md +0 -23
  33. data/Dockerfile +0 -32
  34. data/Dockerfile.openssl3 +0 -17
  35. data/Dockerfile.zlib_ng +0 -29
  36. data/Gemfile +0 -15
  37. data/ISSUE_TEMPLATE.md +0 -30
  38. data/Manifest +0 -132
  39. data/Rakefile +0 -214
  40. data/SECURITY.md +0 -4
  41. data/THANKS.txt +0 -110
  42. data/appveyor.yml +0 -46
  43. data/changelog.d/README.md +0 -1
  44. data/docker-compose.yml +0 -17
  45. data/net-ssh-public_cert.pem +0 -21
  46. data/net-ssh.gemspec +0 -45
  47. data/support/ssh_tunnel_bug.rb +0 -65
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 110d479784d8cc56b52a5a3e9b14e5f23dbf556873b64a8e1563df31b1205911
4
- data.tar.gz: e0f0d31f6476b896ddc6354906cc1b861f99687365a0783527b738e69f4d4e15
3
+ metadata.gz: 8a3eb4e5ac79bff25a68aaecd501c437bf0145a7458c8edd00f90e3b9ae14641
4
+ data.tar.gz: cabb7b40f56ed19ca61a1a09eccff1745618a62e258a9c2ba1804fdfbf02c44b
5
5
  SHA512:
6
- metadata.gz: 985c71acd2df6c9e5a287c8e64c6e960cd80bd47c5d18837e1381a7c38f09ee2bbad51e3946f8d26d5f467c4999fca322e3ce4b48aa16115c2d434f675cf52cd
7
- data.tar.gz: 81a1286e7e73a6c6b17a4a64e5c5004844a734bfa0118d39041ff291eb0e364c9fa4d3777cd7c745fb0ba038bd1aed2cef191b9c001cfb47937b7154a0506813
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]
@@ -78,8 +78,8 @@ module Net
78
78
  key.to_pem
79
79
  end
80
80
 
81
- def fingerprint
82
- key.fingerprint
81
+ def fingerprint(algorithm = 'MD5')
82
+ key.fingerprint(algorithm)
83
83
  end
84
84
 
85
85
  # Signs the certificate with key.
@@ -66,6 +66,10 @@ module Net
66
66
  private
67
67
 
68
68
  attr_reader :prompt
69
+
70
+ def fingerprint(identity)
71
+ identity.fingerprint(@options[:fingerprint_hash] || 'SHA256')
72
+ end
69
73
  end
70
74
  end
71
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.fingerprint})" }
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.fingerprint})" }
49
+ info { "hostbased succeeded (#{fingerprint(identity)})" }
50
50
  return true
51
51
  when USERAUTH_FAILURE
52
- info { "hostbased failed (#{identity.fingerprint})" }
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.fingerprint}) alg #{alg}" }
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.fingerprint})" }
88
+ debug { "publickey succeeded (#{fingerprint(identity)})" }
70
89
  return true
71
90
  when USERAUTH_FAILURE
72
- debug { "publickey failed (#{identity.fingerprint})" }
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
- if type == "ssh-rsa"
99
- pubkey_algorithms.each do |pk_alg|
100
- case pk_alg
101
- when "rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"
102
- if authenticate_with_alg(identity, next_service, username, pk_alg, pk_alg)
103
- # success
104
- return true
105
- end
106
- end
107
- end
108
- elsif type == "ssh-rsa-cert-v01@openssh.com"
109
- pubkey_algorithms.each do |pk_alg|
110
- case pk_alg
111
- when "rsa-sha2-512-cert-v01@openssh.com"
112
- if authenticate_with_alg(identity, next_service, username, pk_alg, "rsa-sha2-512")
113
- # success
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
@@ -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.dup.force_encoding('BINARY') }
362
+ data.each { |datum| @content << datum.b }
363
363
  self
364
364
  end
365
365
 
@@ -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
- def for(host, files = expandable_default_files)
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
- block_seen = false
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 + "$", true)
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
- def eval_match_conditions(condition, host, settings)
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
- conditions = condition.split(/[ \t\r\n]+|(?<!=)=(?!=)/).reject(&:empty?)
382
- return true if conditions == ["all"]
383
-
384
- conditions = conditions.each_slice(2)
385
- condition_matches = []
386
- conditions.each do |(kind, exprs)|
387
- exprs = unquote(exprs)
388
-
389
- case kind.downcase
390
- when "all"
391
- raise "all cannot be mixed with other conditions"
392
- when "host"
393
- if exprs.start_with?('!')
394
- negated = true
395
- exprs = exprs[1..-1]
396
- else
397
- negated = false
398
- end
399
- condition_met = false
400
- exprs.split(",").each do |expr|
401
- condition_met = condition_met || host =~ pattern2regex(expr)
402
- end
403
- condition_matches << (true && negated ^ condition_met)
404
- # else
405
- # warn "net-ssh: Unsupported expr in Match block: #{kind}"
406
- end
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
- !condition_matches.empty? && condition_matches.all?
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.
@@ -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 rekey_blocks_limit rekey_limit
29
- rekey_packet_limit use_agent user_known_hosts_file verify_host_key
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
@@ -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
- $stdout.puts(info[:name]) unless info[:name].empty?
35
- $stdout.puts(info[:instruction]) unless info[:instruction].empty?
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
- ret = echo ? $stdin.gets : $stdin.noecho(&:gets)
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
- kex = algorithms[:kex].join(",")
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
- debug { "ignoring EXT_INFO packet" }
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
@@ -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 = 0
52
+ MINOR = 1
53
53
 
54
54
  # The tiny component of this version of the Net::SSH library
55
- TINY = 4
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