net-ssh 7.3.6 → 8.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGES.txt +106 -5
  4. data/README.md +71 -57
  5. data/lib/net/ssh/authentication/agent.rb +35 -11
  6. data/lib/net/ssh/authentication/certificate.rb +2 -2
  7. data/lib/net/ssh/authentication/ed25519.rb +137 -44
  8. data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
  9. data/lib/net/ssh/authentication/key_manager.rb +22 -11
  10. data/lib/net/ssh/authentication/methods/abstract.rb +6 -8
  11. data/lib/net/ssh/authentication/methods/hostbased.rb +3 -3
  12. data/lib/net/ssh/authentication/methods/publickey.rb +52 -38
  13. data/lib/net/ssh/authentication/security_key.rb +71 -0
  14. data/lib/net/ssh/authentication/session.rb +30 -17
  15. data/lib/net/ssh/buffer.rb +8 -3
  16. data/lib/net/ssh/buffered_io.rb +40 -3
  17. data/lib/net/ssh/config.rb +123 -50
  18. data/lib/net/ssh/connection/channel.rb +49 -2
  19. data/lib/net/ssh/connection/session.rb +151 -13
  20. data/lib/net/ssh/errors.rb +34 -6
  21. data/lib/net/ssh/key_factory.rb +7 -1
  22. data/lib/net/ssh/known_hosts.rb +103 -38
  23. data/lib/net/ssh/multiplex/client.rb +162 -0
  24. data/lib/net/ssh/multiplex/control_path.rb +120 -0
  25. data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
  26. data/lib/net/ssh/multiplex.rb +127 -0
  27. data/lib/net/ssh/prompt.rb +43 -5
  28. data/lib/net/ssh/proxy/https.rb +28 -4
  29. data/lib/net/ssh/proxy/jump.rb +61 -8
  30. data/lib/net/ssh/service/forward.rb +35 -24
  31. data/lib/net/ssh/test.rb +2 -1
  32. data/lib/net/ssh/transport/algorithms.rb +92 -34
  33. data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
  34. data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
  35. data/lib/net/ssh/transport/cipher_factory.rb +23 -1
  36. data/lib/net/ssh/transport/constants.rb +1 -0
  37. data/lib/net/ssh/transport/gcm_cipher.rb +27 -6
  38. data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
  39. data/lib/net/ssh/transport/hmac.rb +2 -0
  40. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
  41. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
  42. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
  43. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
  44. data/lib/net/ssh/transport/openssl.rb +2 -1
  45. data/lib/net/ssh/transport/packet_stream.rb +12 -0
  46. data/lib/net/ssh/transport/session.rb +61 -45
  47. data/lib/net/ssh/verifiers/accept_new_or_local_tunnel.rb +3 -1
  48. data/lib/net/ssh/verifiers/always.rb +16 -4
  49. data/lib/net/ssh/verifiers/never.rb +2 -1
  50. data/lib/net/ssh/version.rb +3 -3
  51. data/lib/net/ssh.rb +164 -63
  52. data.tar.gz.sig +0 -0
  53. metadata +33 -61
  54. metadata.gz.sig +0 -0
  55. data/.dockerignore +0 -9
  56. data/.github/FUNDING.yml +0 -1
  57. data/.github/config/rubocop_linter_action.yml +0 -4
  58. data/.github/workflows/ci-with-docker.yml +0 -58
  59. data/.github/workflows/ci.yml +0 -99
  60. data/.github/workflows/rubocop.yml +0 -16
  61. data/.gitignore +0 -15
  62. data/.rubocop.yml +0 -22
  63. data/.rubocop_todo.yml +0 -1083
  64. data/DEVELOPMENT.md +0 -23
  65. data/Dockerfile +0 -32
  66. data/Dockerfile.openssl3 +0 -17
  67. data/Dockerfile.zlib_ng +0 -29
  68. data/Gemfile +0 -13
  69. data/Gemfile.noed25519 +0 -12
  70. data/Gemfile.norbnacl +0 -12
  71. data/ISSUE_TEMPLATE.md +0 -30
  72. data/Manifest +0 -132
  73. data/Rakefile +0 -196
  74. data/SECURITY.md +0 -4
  75. data/THANKS.txt +0 -110
  76. data/appveyor.yml +0 -58
  77. data/docker-compose.yml +0 -23
  78. data/net-ssh-public_cert.pem +0 -21
  79. data/net-ssh.gemspec +0 -48
  80. data/support/ssh_tunnel_bug.rb +0 -65
@@ -24,7 +24,7 @@ module Net
24
24
  # # 'ssh' is an instance of Net::SSH::Connection::Session
25
25
  # ssh.exec! "/etc/init.d/some_process start"
26
26
  # end
27
- class Session
27
+ class Session # rubocop:disable Metrics/ClassLength
28
28
  include Loggable
29
29
  include Constants
30
30
 
@@ -53,6 +53,10 @@ module Net
53
53
  # The list of callbacks for pending requests. See #send_global_request.
54
54
  attr_reader :pending_requests # :nodoc:
55
55
 
56
+ # A "session" channel opened ahead of time by :preopen_channels, and
57
+ # the server's confirmation for it once that has arrived.
58
+ SpareChannel = Struct.new(:local_id, :remote_id, :window_size, :packet_size) # :nodoc:
59
+
56
60
  class NilChannel
57
61
  def initialize(session)
58
62
  @session = session
@@ -74,13 +78,20 @@ module Net
74
78
  @channel_id_counter = -1
75
79
  @channels = Hash.new(NilChannel.new(self))
76
80
  @listeners = { transport.socket => nil }
81
+ @read_paused = {}
82
+ @draining = {}
77
83
  @pending_requests = []
78
84
  @channel_open_handlers = {}
79
85
  @on_global_request = {}
80
86
  @properties = (options[:properties] || {}).dup
81
87
 
82
88
  @max_pkt_size = (options.key?(:max_pkt_size) ? options[:max_pkt_size] : 0x8000)
83
- @max_win_size = (options.key?(:max_win_size) ? options[:max_win_size] : 0x20000)
89
+ @max_win_size = (options.key?(:max_win_size) ? options[:max_win_size] : Channel::DEFAULT_MAX_WIN_SIZE)
90
+
91
+ @preopen_channels = options.fetch(:preopen_channels, false)
92
+ @spare_channel = nil
93
+ @spare_wanted = false
94
+ @preopened = []
84
95
 
85
96
  @keepalive = Keepalive.new(self)
86
97
 
@@ -119,13 +130,21 @@ module Net
119
130
  # successfully closed, and then closes the underlying transport layer
120
131
  # connection.
121
132
  def close
133
+ stop_preopening
122
134
  info { "closing remaining channels (#{channels.length} open)" }
123
- channels.each { |id, channel| channel.close }
135
+
136
+ channels.each do |id, channel|
137
+ channel.eof!
138
+ channel.close
139
+ end
140
+
124
141
  begin
125
142
  loop(0.1) { channels.any? }
126
143
  rescue Net::SSH::Disconnect
127
144
  raise unless channels.empty?
128
145
  end
146
+ @draining.each_key(&:close)
147
+ @draining.clear
129
148
  transport.close
130
149
  end
131
150
 
@@ -134,6 +153,7 @@ module Net
134
153
  # when the connection needs to close but you don't know the status of the
135
154
  # underlying protocol's state).
136
155
  def shutdown!
156
+ stop_preopening
137
157
  transport.shutdown!
138
158
  end
139
159
 
@@ -146,10 +166,13 @@ module Net
146
166
  # a +true+ value for +include_invisible+, then those will be counted.
147
167
  #
148
168
  # This can be useful for determining whether the event loop should continue
149
- # to be run.
169
+ # to be run. A forwarded socket still writing out data that arrived before
170
+ # its channel closed also counts as busy.
150
171
  #
151
172
  # ssh.loop { ssh.busy? }
152
173
  def busy?(include_invisible = false)
174
+ return true if @draining.any?
175
+
153
176
  if include_invisible
154
177
  channels.any?
155
178
  else
@@ -246,8 +269,10 @@ module Net
246
269
  # Called by event loop to process available data before going to
247
270
  # event multiplexing
248
271
  def ev_preprocess(&block)
272
+ confirm_preopened_channels
249
273
  dispatch_incoming_packets(raise_disconnect_errors: false)
250
274
  each_channel { |id, channel| channel.process unless channel.local_closed? }
275
+ open_spare_channel
251
276
  end
252
277
 
253
278
  # Returns the file descriptors the event loop should wait for read/write events,
@@ -255,7 +280,7 @@ module Net
255
280
  def ev_do_calculate_rw_wait(wait)
256
281
  r = listeners.keys
257
282
  w = r.select { |w2| w2.respond_to?(:pending_write?) && w2.pending_write? }
258
- [r, w, io_select_wait(wait)]
283
+ [r.reject { |io| @read_paused.key?(io) }, w, io_select_wait(wait)]
259
284
  end
260
285
 
261
286
  # This is called internally as part of #process.
@@ -281,6 +306,7 @@ module Net
281
306
 
282
307
  Array(writers).each do |writer|
283
308
  writer.send_pending
309
+ close_if_drained(writer) if @draining.key?(writer)
284
310
  end
285
311
  end
286
312
 
@@ -335,14 +361,20 @@ module Net
335
361
  # end
336
362
  #
337
363
  # channel.wait
364
+ #
365
+ # With the :preopen_channels option a plain "session" channel reuses
366
+ # the spare opened ahead of time, if there is one. Its callback still
367
+ # runs from the event loop, as if the server had just confirmed it.
338
368
  def open_channel(type = "session", *extra, &on_confirm)
339
- local_id = get_next_channel_id
369
+ spare = take_spare_channel if type == "session" && extra.empty?
370
+ local_id = spare ? spare.local_id : get_next_channel_id
340
371
 
341
372
  channel = Channel.new(self, type, local_id, @max_pkt_size, @max_win_size, &on_confirm)
342
- msg = Buffer.from(:byte, CHANNEL_OPEN, :string, type, :long, local_id,
343
- :long, channel.local_maximum_window_size,
344
- :long, channel.local_maximum_packet_size, *extra)
345
- send_message(msg)
373
+ if spare
374
+ @preopened << [channel, spare] if spare.remote_id
375
+ else
376
+ send_channel_open(type, channel.local_id, channel.local_maximum_window_size, channel.local_maximum_packet_size, *extra)
377
+ end
346
378
 
347
379
  channels[local_id] = channel
348
380
  end
@@ -416,7 +448,10 @@ module Net
416
448
  #
417
449
  # matches = ssh.exec!("grep something /some/files")
418
450
  #
419
- # the returned string has an exitstatus method to query its exit status
451
+ # With a block, output goes to the block and an empty string is returned,
452
+ # unless the block sets <tt>ch[:result]</tt>, which is returned instead.
453
+ # Either way the returned string has an exitstatus method to query the
454
+ # command's exit status.
420
455
  def exec!(command, status: nil, &block)
421
456
  block_or_concat = block || Proc.new do |ch, type, data|
422
457
  ch[:result] ||= String.new
@@ -427,10 +462,10 @@ module Net
427
462
  channel = exec(command, status: status, &block_or_concat)
428
463
  channel.wait
429
464
 
430
- channel[:result] ||= String.new unless block
431
465
  channel[:result] &&= channel[:result].force_encoding("UTF-8") unless block
466
+ channel[:result] ||= String.new(encoding: Encoding::UTF_8)
432
467
 
433
- StringWithExitstatus.new(channel[:result], status[:exit_code]) if channel[:result]
468
+ StringWithExitstatus.new(channel[:result], status[:exit_code])
434
469
  end
435
470
 
436
471
  # Enqueues a message to be sent to the server as soon as the socket is
@@ -485,9 +520,36 @@ module Net
485
520
  # Removes the given io object from the listeners collection, so that the
486
521
  # event loop will no longer monitor it.
487
522
  def stop_listening_to(io)
523
+ @read_paused.delete(io)
524
+ @draining.delete(io)
488
525
  listeners.delete(io)
489
526
  end
490
527
 
528
+ # Internal and experimental, used by port forwarding: stops the event
529
+ # loop from reading +io+ while it keeps flushing its pending output.
530
+ def pause_reading(io) # :nodoc:
531
+ raise ArgumentError, "the transport socket can't be paused" if io.equal?(transport.socket)
532
+
533
+ @read_paused[io] = true if listeners.key?(io)
534
+ end
535
+
536
+ def resume_reading(io) # :nodoc:
537
+ @read_paused.delete(io)
538
+ end
539
+
540
+ def reading_paused?(io) # :nodoc:
541
+ @read_paused.key?(io)
542
+ end
543
+
544
+ # Internal and experimental, used by port forwarding: stops reading +io+
545
+ # but keeps flushing its pending output, then closes it and stops
546
+ # listening, as OpenSSH's chan_rcvd_oclose does.
547
+ def close_when_drained(io) # :nodoc:
548
+ pause_reading(io)
549
+ @draining[io] = true
550
+ close_if_drained(io)
551
+ end
552
+
491
553
  # Returns a reference to the Net::SSH::Service::Forward service, which can
492
554
  # be used for forwarding ports over SSH.
493
555
  def forward
@@ -560,11 +622,20 @@ module Net
560
622
  end
561
623
 
562
624
  def force_channel_cleanup_on_close
625
+ stop_preopening
626
+ @preopened.clear
563
627
  channels.each do |id, channel|
564
628
  channel_closed(channel)
565
629
  end
566
630
  end
567
631
 
632
+ def close_if_drained(io)
633
+ return if io.pending_write? && !io.closed? && !closed? && listeners.key?(io)
634
+
635
+ io.close unless io.closed?
636
+ stop_listening_to(io)
637
+ end
638
+
568
639
  def channel_closed(channel)
569
640
  channel.remote_closed!
570
641
  channel.close
@@ -644,18 +715,37 @@ module Net
644
715
 
645
716
  def channel_open_confirmation(packet)
646
717
  info { "channel_open_confirmation: #{packet[:local_id]} #{packet[:remote_id]} #{packet[:window_size]} #{packet[:packet_size]}" }
718
+ if (spare = spare_channel(packet))
719
+ spare.remote_id = packet[:remote_id]
720
+ spare.window_size = packet[:window_size]
721
+ spare.packet_size = packet[:packet_size]
722
+ return
723
+ end
724
+
647
725
  channel = channels[packet[:local_id]]
648
726
  channel.do_open_confirmation(packet[:remote_id], packet[:window_size], packet[:packet_size])
649
727
  end
650
728
 
651
729
  def channel_open_failure(packet)
652
730
  error { "channel_open_failed: #{packet[:local_id]} #{packet[:reason_code]} #{packet[:description]}" }
731
+ if spare_channel(packet)
732
+ info { "preopen_channels: server refused the spare channel, no longer pre-opening" }
733
+ stop_preopening
734
+ @spare_channel = nil
735
+ return
736
+ end
737
+
653
738
  channel = channels.delete(packet[:local_id])
654
739
  channel.do_open_failed(packet[:reason_code], packet[:description])
655
740
  end
656
741
 
657
742
  def channel_window_adjust(packet)
658
743
  info { "channel_window_adjust: #{packet[:local_id]} +#{packet[:extra_bytes]}" }
744
+ if (spare = spare_channel(packet))
745
+ spare.window_size += packet[:extra_bytes]
746
+ return
747
+ end
748
+
659
749
  channels[packet[:local_id]].do_window_adjust(packet[:extra_bytes])
660
750
  end
661
751
 
@@ -676,11 +766,20 @@ module Net
676
766
 
677
767
  def channel_eof(packet)
678
768
  info { "channel_eof: #{packet[:local_id]}" }
769
+ return if spare_channel(packet)
770
+
679
771
  channels[packet[:local_id]].do_eof
680
772
  end
681
773
 
682
774
  def channel_close(packet)
683
775
  info { "channel_close: #{packet[:local_id]}" }
776
+ if (spare = spare_channel(packet))
777
+ send_message(Buffer.from(:byte, CHANNEL_CLOSE, :long, spare.remote_id))
778
+ @spare_channel = nil
779
+ # Wait for the next plain open_channel before opening another, so a server closing spares can't make us loop.
780
+ @spare_wanted = false
781
+ return
782
+ end
684
783
 
685
784
  channel = channels[packet[:local_id]]
686
785
  channel_closed(channel)
@@ -696,6 +795,45 @@ module Net
696
795
  channels[packet[:local_id]].do_failure
697
796
  end
698
797
 
798
+ def send_channel_open(type, local_id, window_size, packet_size, *extra)
799
+ send_message(Buffer.from(:byte, CHANNEL_OPEN, :string, type, :long, local_id,
800
+ :long, window_size, :long, packet_size, *extra))
801
+ end
802
+
803
+ def take_spare_channel
804
+ return unless @preopen_channels
805
+
806
+ @spare_wanted = true
807
+ spare = @spare_channel
808
+ @spare_channel = nil
809
+ spare
810
+ end
811
+
812
+ def open_spare_channel
813
+ return unless @spare_wanted && @preopen_channels && @spare_channel.nil? && !closed?
814
+
815
+ @spare_channel = SpareChannel.new(get_next_channel_id)
816
+ info { "preopen_channels: opening spare channel #{@spare_channel.local_id}" }
817
+ send_channel_open("session", @spare_channel.local_id, @max_win_size, @max_pkt_size)
818
+ end
819
+
820
+ # An unused spare is never closed, as that would cost round trips just before the transport goes away;
821
+ # it stays in @spare_channel to absorb the server's late replies for it.
822
+ def stop_preopening
823
+ @preopen_channels = false
824
+ end
825
+
826
+ def confirm_preopened_channels
827
+ until @preopened.empty?
828
+ channel, spare = @preopened.shift
829
+ channel.do_open_confirmation(spare.remote_id, spare.window_size, spare.packet_size)
830
+ end
831
+ end
832
+
833
+ def spare_channel(packet)
834
+ @spare_channel if @spare_channel && @spare_channel.local_id == packet[:local_id]
835
+ end
836
+
699
837
  def io_select_wait(wait)
700
838
  [wait, max_select_wait_time].compact.min
701
839
  end
@@ -19,6 +19,16 @@ module Net
19
19
  # timeouted unexpectedly.
20
20
  class Timeout < Disconnect; end
21
21
 
22
+ # This exception is raised when a key blob has a key type that Net::SSH
23
+ # cannot parse.
24
+ class UnsupportedKeyType < Net::SSH::Exception; end
25
+
26
+ # Raised when a session connected through an OpenSSH ControlMaster (see the
27
+ # :control_master option of Net::SSH.start) is asked for something the
28
+ # master's proxy mode cannot carry, such as remote port forwarding, agent
29
+ # or X11 forwarding, or other global requests.
30
+ class NotSupportedOverControlMaster < Net::SSH::Exception; end
31
+
22
32
  # This exception is primarily used internally, but if you have a channel
23
33
  # request handler (see Net::SSH::Connection::Channel#on_request) that you
24
34
  # want to fail in such a way that the server knows it failed, you can
@@ -62,19 +72,21 @@ module Net
62
72
  @data && @data[:fingerprint]
63
73
  end
64
74
 
65
- # Returns the host name for the remote host, as reported by the socket.
75
+ # Returns the host name that was connected to.
66
76
  def host
67
- @data && @data[:peer] && @data[:peer][:host]
77
+ session&.host
68
78
  end
69
79
 
70
- # Returns the port number for the remote host, as reported by the socket.
80
+ # Returns the port number that was connected to.
71
81
  def port
72
- @data && @data[:peer] && @data[:peer][:port]
82
+ session&.port
73
83
  end
74
84
 
75
- # Returns the IP address of the remote host, as reported by the socket.
85
+ # Returns the IP address of the remote host, as reported by the socket,
86
+ # or nil when connected through a ProxyCommand.
76
87
  def ip
77
- @data && @data[:peer] && @data[:peer][:ip]
88
+ peer_ip = session&.socket&.peer_ip
89
+ peer_ip unless peer_ip == Net::SSH::Transport::PacketStream::PROXY_COMMAND_HOST_IP
78
90
  end
79
91
 
80
92
  # Returns the key itself, as reported by the remote host.
@@ -87,6 +99,12 @@ module Net
87
99
  def remember_host!
88
100
  @callback.call
89
101
  end
102
+
103
+ private
104
+
105
+ def session
106
+ @data && @data[:session]
107
+ end
90
108
  end
91
109
 
92
110
  # Raised when the cached key for a particular host does not match the
@@ -102,5 +120,15 @@ module Net
102
120
  # you want to proceed anyway, simply call the remember_host! method on the
103
121
  # exception, and then retry.
104
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
105
133
  end
106
134
  end
@@ -2,6 +2,7 @@ require 'net/ssh/transport/openssl'
2
2
  require 'net/ssh/prompt'
3
3
 
4
4
  require 'net/ssh/authentication/ed25519_loader'
5
+ require 'net/ssh/authentication/security_key'
5
6
 
6
7
  module Net
7
8
  module SSH
@@ -92,7 +93,7 @@ module Net
92
93
  blob = nil
93
94
  begin
94
95
  blob = fields.shift
95
- end while !blob.nil? && !/^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp\d+)(-cert-v01@openssh\.com)?$/.match(blob)
96
+ end while !blob.nil? && !public_key_type?(blob)
96
97
  blob = fields.shift
97
98
 
98
99
  raise Net::SSH::Exception, "public key at #{filename} is not valid" if blob.nil?
@@ -104,6 +105,11 @@ module Net
104
105
 
105
106
  private
106
107
 
108
+ def public_key_type?(type)
109
+ /^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp\d+)(-cert-v01@openssh\.com)?$/.match?(type) ||
110
+ Net::SSH::Authentication::SecurityKey.understands?(type)
111
+ end
112
+
107
113
  # rubocop:disable Style/Documentation, Lint/DuplicateMethods
108
114
  class KeyType
109
115
  def self.read(key_data, passphrase)
@@ -1,6 +1,5 @@
1
1
  require 'strscan'
2
2
  require 'openssl'
3
- require 'delegate'
4
3
  require 'net/ssh/buffer'
5
4
  require 'net/ssh/authentication/ed25519_loader'
6
5
 
@@ -8,8 +7,11 @@ module Net
8
7
  module SSH
9
8
  module HostKeyEntries
10
9
  # regular public key entry
11
- class PubKey < Delegator
12
- def initialize(key, comment: nil) # rubocop:disable Lint/MissingSuper
10
+ class PubKey
11
+ # The public key itself, e.g. an OpenSSL::PKey::PKey.
12
+ attr_reader :key
13
+
14
+ def initialize(key, comment: nil)
13
15
  @key = key
14
16
  @comment = comment
15
17
  end
@@ -26,14 +28,22 @@ module Net
26
28
  @key.to_blob
27
29
  end
28
30
 
29
- def __getobj__
30
- Kernel.warn("Calling Net::SSH::Buffer methods on HostKeyEntries PubKey is deprecated")
31
- @key
32
- end
33
-
34
31
  def matches_key?(server_key)
35
32
  @key.ssh_type == server_key.ssh_type && @key.to_blob == server_key.to_blob
36
33
  end
34
+
35
+ def method_missing(name, *args, &block)
36
+ return super unless @key.respond_to?(name)
37
+
38
+ raise NoMethodError.new(
39
+ "undefined method `#{name}' for #{self.class}; call it on #key, the wrapped public key",
40
+ name
41
+ )
42
+ end
43
+
44
+ def respond_to_missing?(_name, _include_private = false)
45
+ false
46
+ end
37
47
  end
38
48
 
39
49
  # @cert-authority entry
@@ -93,6 +103,22 @@ module Net
93
103
  server_key.critical_options.empty?
94
104
  end
95
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
96
122
  end
97
123
 
98
124
  # Represents the result of a search in known hosts
@@ -101,11 +127,17 @@ module Net
101
127
  include Enumerable
102
128
  attr_reader :host
103
129
 
104
- def initialize(host_keys, host, known_hosts, options = {})
130
+ def initialize(host_keys, host, known_hosts, options = {}, revoked_keys = [])
105
131
  @host_keys = host_keys
106
132
  @host = host
107
133
  @known_hosts = known_hosts
108
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) }
109
141
  end
110
142
 
111
143
  def add_host_key(key)
@@ -151,11 +183,14 @@ module Net
151
183
  # Searches all known host files (see KnownHosts.hostfiles) for all keys
152
184
  # of the given host. Returns an enumerable of keys found.
153
185
  def search_for(host, options = {})
154
- HostKeys.new(search_in(hostfiles(options), host, options), host, self, options)
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)
155
189
  end
156
190
 
157
191
  # Search for all known keys for the given host, in every file given in
158
- # the +files+ array. Returns the list of keys.
192
+ # the +files+ array. Returns the list of keys. @revoked entries are not
193
+ # included.
159
194
  def search_in(files, host, options = {})
160
195
  files.flat_map { |file| KnownHosts.new(file).keys_for(host, options) }
161
196
  end
@@ -219,46 +254,50 @@ module Net
219
254
  # "[net.ssh.test]:5555"
220
255
  # "[1,2,3,4]:5555"
221
256
  # "[net.ssh.test]:5555,[1.2.3.4]:5555
257
+ #
258
+ # @revoked entries are not included, see revoked_keys_for.
222
259
  def keys_for(host, options = {})
223
260
  keys = []
224
- return keys unless File.readable?(source)
225
-
226
261
  entries = host.split(/,/)
227
262
  host_name = entries[0]
228
263
  host_ip = entries[1]
229
264
 
230
- File.open(source) do |file|
231
- file.each_line do |line|
232
- if line.start_with?('@')
233
- marker, hosts, type, key_content, comment = line.split(' ')
234
- else
235
- marker = nil
236
- hosts, type, key_content, comment = line.split(' ')
237
- end
265
+ each_entry do |marker, hostlist, key_content, comment|
266
+ next if marker == "@revoked"
238
267
 
239
- # Skip empty line or one that is commented
240
- next if hosts.nil? || hosts.start_with?('#')
268
+ found = hostlist.any? { |pattern| match(host_name, pattern) } || known_host_hash?(hostlist, entries)
269
+ next unless found
241
270
 
242
- hostlist = hosts.split(',')
271
+ found = hostlist.include?(host_ip) if options[:check_host_ip] && entries.size > 1 && hostlist.size > 1
272
+ next unless found
243
273
 
244
- next unless SUPPORTED_TYPE.include?(type)
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
283
+
284
+ keys
285
+ end
245
286
 
246
- found = hostlist.any? { |pattern| match(host_name, pattern) } || known_host_hash?(hostlist, entries)
247
- next unless found
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(/,/)
248
293
 
249
- found = hostlist.include?(host_ip) if options[:check_host_ip] && entries.size > 1 && hostlist.size > 1
250
- next unless found
294
+ each_entry do |marker, hostlist, key_content, comment|
295
+ next unless marker == "@revoked"
251
296
 
252
- blob = key_content.unpack("m*").first
253
- raw_key = Net::SSH::Buffer.new(blob).read_key
297
+ found = entries.any? { |entry| hostlist.any? { |pattern| match(entry, pattern) } }
298
+ next unless found || known_host_hash?(hostlist, entries)
254
299
 
255
- keys <<
256
- if marker == "@cert-authority"
257
- HostKeyEntries::CertAuthority.new(raw_key, comment: comment)
258
- else
259
- HostKeyEntries::PubKey.new(raw_key, comment: comment)
260
- end
261
- end
300
+ keys << HostKeyEntries::Revoked.new(read_key(key_content), comment: comment)
262
301
  end
263
302
 
264
303
  keys
@@ -303,6 +342,32 @@ module Net
303
342
  file.puts "#{host} #{key.ssh_type} #{blob}"
304
343
  end
305
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
306
371
  end
307
372
  end
308
373
  end