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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGES.txt +106 -5
- data/README.md +71 -57
- data/lib/net/ssh/authentication/agent.rb +35 -11
- data/lib/net/ssh/authentication/certificate.rb +2 -2
- data/lib/net/ssh/authentication/ed25519.rb +137 -44
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/authentication/key_manager.rb +22 -11
- data/lib/net/ssh/authentication/methods/abstract.rb +6 -8
- data/lib/net/ssh/authentication/methods/hostbased.rb +3 -3
- data/lib/net/ssh/authentication/methods/publickey.rb +52 -38
- data/lib/net/ssh/authentication/security_key.rb +71 -0
- data/lib/net/ssh/authentication/session.rb +30 -17
- data/lib/net/ssh/buffer.rb +8 -3
- data/lib/net/ssh/buffered_io.rb +40 -3
- data/lib/net/ssh/config.rb +123 -50
- data/lib/net/ssh/connection/channel.rb +49 -2
- data/lib/net/ssh/connection/session.rb +151 -13
- data/lib/net/ssh/errors.rb +34 -6
- data/lib/net/ssh/key_factory.rb +7 -1
- data/lib/net/ssh/known_hosts.rb +103 -38
- data/lib/net/ssh/multiplex/client.rb +162 -0
- data/lib/net/ssh/multiplex/control_path.rb +120 -0
- data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
- data/lib/net/ssh/multiplex.rb +127 -0
- data/lib/net/ssh/prompt.rb +43 -5
- data/lib/net/ssh/proxy/https.rb +28 -4
- data/lib/net/ssh/proxy/jump.rb +61 -8
- data/lib/net/ssh/service/forward.rb +35 -24
- data/lib/net/ssh/test.rb +2 -1
- data/lib/net/ssh/transport/algorithms.rb +92 -34
- data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
- data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
- data/lib/net/ssh/transport/cipher_factory.rb +23 -1
- data/lib/net/ssh/transport/constants.rb +1 -0
- data/lib/net/ssh/transport/gcm_cipher.rb +27 -6
- data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
- data/lib/net/ssh/transport/hmac.rb +2 -0
- data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
- data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
- data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
- data/lib/net/ssh/transport/openssl.rb +2 -1
- data/lib/net/ssh/transport/packet_stream.rb +12 -0
- data/lib/net/ssh/transport/session.rb +61 -45
- data/lib/net/ssh/verifiers/accept_new_or_local_tunnel.rb +3 -1
- data/lib/net/ssh/verifiers/always.rb +16 -4
- data/lib/net/ssh/verifiers/never.rb +2 -1
- data/lib/net/ssh/version.rb +3 -3
- data/lib/net/ssh.rb +164 -63
- data.tar.gz.sig +0 -0
- metadata +33 -61
- metadata.gz.sig +0 -0
- data/.dockerignore +0 -9
- data/.github/FUNDING.yml +0 -1
- data/.github/config/rubocop_linter_action.yml +0 -4
- data/.github/workflows/ci-with-docker.yml +0 -58
- data/.github/workflows/ci.yml +0 -99
- data/.github/workflows/rubocop.yml +0 -16
- data/.gitignore +0 -15
- data/.rubocop.yml +0 -22
- data/.rubocop_todo.yml +0 -1083
- data/DEVELOPMENT.md +0 -23
- data/Dockerfile +0 -32
- data/Dockerfile.openssl3 +0 -17
- data/Dockerfile.zlib_ng +0 -29
- data/Gemfile +0 -13
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
- data/ISSUE_TEMPLATE.md +0 -30
- data/Manifest +0 -132
- data/Rakefile +0 -196
- data/SECURITY.md +0 -4
- data/THANKS.txt +0 -110
- data/appveyor.yml +0 -58
- data/docker-compose.yml +0 -23
- data/net-ssh-public_cert.pem +0 -21
- data/net-ssh.gemspec +0 -48
- 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] :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
#
|
|
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])
|
|
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
|
data/lib/net/ssh/errors.rb
CHANGED
|
@@ -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
|
|
75
|
+
# Returns the host name that was connected to.
|
|
66
76
|
def host
|
|
67
|
-
|
|
77
|
+
session&.host
|
|
68
78
|
end
|
|
69
79
|
|
|
70
|
-
# Returns the port number
|
|
80
|
+
# Returns the port number that was connected to.
|
|
71
81
|
def port
|
|
72
|
-
|
|
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
|
-
|
|
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
|
data/lib/net/ssh/key_factory.rb
CHANGED
|
@@ -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? &&
|
|
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)
|
data/lib/net/ssh/known_hosts.rb
CHANGED
|
@@ -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
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
240
|
-
|
|
268
|
+
found = hostlist.any? { |pattern| match(host_name, pattern) } || known_host_hash?(hostlist, entries)
|
|
269
|
+
next unless found
|
|
241
270
|
|
|
242
|
-
|
|
271
|
+
found = hostlist.include?(host_ip) if options[:check_host_ip] && entries.size > 1 && hostlist.size > 1
|
|
272
|
+
next unless found
|
|
243
273
|
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
|
|
250
|
-
|
|
294
|
+
each_entry do |marker, hostlist, key_content, comment|
|
|
295
|
+
next unless marker == "@revoked"
|
|
251
296
|
|
|
252
|
-
|
|
253
|
-
|
|
297
|
+
found = entries.any? { |entry| hostlist.any? { |pattern| match(entry, pattern) } }
|
|
298
|
+
next unless found || known_host_hash?(hostlist, entries)
|
|
254
299
|
|
|
255
|
-
|
|
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
|