minmb-net-ssh 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. data/CHANGELOG.rdoc +291 -0
  2. data/Manifest +132 -0
  3. data/README.rdoc +184 -0
  4. data/Rakefile +86 -0
  5. data/Rudyfile +96 -0
  6. data/THANKS.rdoc +19 -0
  7. data/lib/net/ssh.rb +223 -0
  8. data/lib/net/ssh/authentication/agent.rb +23 -0
  9. data/lib/net/ssh/authentication/agent/java_pageant.rb +85 -0
  10. data/lib/net/ssh/authentication/agent/socket.rb +170 -0
  11. data/lib/net/ssh/authentication/constants.rb +18 -0
  12. data/lib/net/ssh/authentication/key_manager.rb +253 -0
  13. data/lib/net/ssh/authentication/methods/abstract.rb +60 -0
  14. data/lib/net/ssh/authentication/methods/hostbased.rb +75 -0
  15. data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +70 -0
  16. data/lib/net/ssh/authentication/methods/password.rb +43 -0
  17. data/lib/net/ssh/authentication/methods/publickey.rb +96 -0
  18. data/lib/net/ssh/authentication/pageant.rb +301 -0
  19. data/lib/net/ssh/authentication/session.rb +154 -0
  20. data/lib/net/ssh/buffer.rb +350 -0
  21. data/lib/net/ssh/buffered_io.rb +207 -0
  22. data/lib/net/ssh/config.rb +207 -0
  23. data/lib/net/ssh/connection/channel.rb +630 -0
  24. data/lib/net/ssh/connection/constants.rb +33 -0
  25. data/lib/net/ssh/connection/session.rb +603 -0
  26. data/lib/net/ssh/connection/term.rb +178 -0
  27. data/lib/net/ssh/errors.rb +88 -0
  28. data/lib/net/ssh/key_factory.rb +107 -0
  29. data/lib/net/ssh/known_hosts.rb +141 -0
  30. data/lib/net/ssh/loggable.rb +61 -0
  31. data/lib/net/ssh/packet.rb +102 -0
  32. data/lib/net/ssh/prompt.rb +93 -0
  33. data/lib/net/ssh/proxy/command.rb +75 -0
  34. data/lib/net/ssh/proxy/errors.rb +14 -0
  35. data/lib/net/ssh/proxy/http.rb +94 -0
  36. data/lib/net/ssh/proxy/socks4.rb +70 -0
  37. data/lib/net/ssh/proxy/socks5.rb +142 -0
  38. data/lib/net/ssh/ruby_compat.rb +77 -0
  39. data/lib/net/ssh/service/forward.rb +327 -0
  40. data/lib/net/ssh/test.rb +89 -0
  41. data/lib/net/ssh/test/channel.rb +129 -0
  42. data/lib/net/ssh/test/extensions.rb +152 -0
  43. data/lib/net/ssh/test/kex.rb +44 -0
  44. data/lib/net/ssh/test/local_packet.rb +51 -0
  45. data/lib/net/ssh/test/packet.rb +81 -0
  46. data/lib/net/ssh/test/remote_packet.rb +38 -0
  47. data/lib/net/ssh/test/script.rb +157 -0
  48. data/lib/net/ssh/test/socket.rb +64 -0
  49. data/lib/net/ssh/transport/algorithms.rb +407 -0
  50. data/lib/net/ssh/transport/cipher_factory.rb +106 -0
  51. data/lib/net/ssh/transport/constants.rb +32 -0
  52. data/lib/net/ssh/transport/ctr.rb +95 -0
  53. data/lib/net/ssh/transport/hmac.rb +45 -0
  54. data/lib/net/ssh/transport/hmac/abstract.rb +79 -0
  55. data/lib/net/ssh/transport/hmac/md5.rb +12 -0
  56. data/lib/net/ssh/transport/hmac/md5_96.rb +11 -0
  57. data/lib/net/ssh/transport/hmac/none.rb +15 -0
  58. data/lib/net/ssh/transport/hmac/ripemd160.rb +13 -0
  59. data/lib/net/ssh/transport/hmac/sha1.rb +13 -0
  60. data/lib/net/ssh/transport/hmac/sha1_96.rb +11 -0
  61. data/lib/net/ssh/transport/hmac/sha2_256.rb +15 -0
  62. data/lib/net/ssh/transport/hmac/sha2_256_96.rb +13 -0
  63. data/lib/net/ssh/transport/hmac/sha2_512.rb +14 -0
  64. data/lib/net/ssh/transport/hmac/sha2_512_96.rb +13 -0
  65. data/lib/net/ssh/transport/identity_cipher.rb +55 -0
  66. data/lib/net/ssh/transport/kex.rb +28 -0
  67. data/lib/net/ssh/transport/kex/diffie_hellman_group14_sha1.rb +44 -0
  68. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +216 -0
  69. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +80 -0
  70. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha256.rb +15 -0
  71. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp256.rb +93 -0
  72. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp384.rb +13 -0
  73. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp521.rb +13 -0
  74. data/lib/net/ssh/transport/key_expander.rb +26 -0
  75. data/lib/net/ssh/transport/openssl.rb +237 -0
  76. data/lib/net/ssh/transport/packet_stream.rb +235 -0
  77. data/lib/net/ssh/transport/server_version.rb +71 -0
  78. data/lib/net/ssh/transport/session.rb +278 -0
  79. data/lib/net/ssh/transport/state.rb +206 -0
  80. data/lib/net/ssh/verifiers/lenient.rb +30 -0
  81. data/lib/net/ssh/verifiers/null.rb +12 -0
  82. data/lib/net/ssh/verifiers/strict.rb +53 -0
  83. data/lib/net/ssh/version.rb +62 -0
  84. data/net-ssh.gemspec +164 -0
  85. data/setup.rb +1585 -0
  86. data/support/arcfour_check.rb +20 -0
  87. data/support/ssh_tunnel_bug.rb +65 -0
  88. data/test/authentication/methods/common.rb +28 -0
  89. data/test/authentication/methods/test_abstract.rb +51 -0
  90. data/test/authentication/methods/test_hostbased.rb +114 -0
  91. data/test/authentication/methods/test_keyboard_interactive.rb +100 -0
  92. data/test/authentication/methods/test_password.rb +52 -0
  93. data/test/authentication/methods/test_publickey.rb +148 -0
  94. data/test/authentication/test_agent.rb +205 -0
  95. data/test/authentication/test_key_manager.rb +218 -0
  96. data/test/authentication/test_session.rb +106 -0
  97. data/test/common.rb +107 -0
  98. data/test/configs/eqsign +3 -0
  99. data/test/configs/exact_match +8 -0
  100. data/test/configs/host_plus +10 -0
  101. data/test/configs/multihost +4 -0
  102. data/test/configs/wild_cards +14 -0
  103. data/test/connection/test_channel.rb +467 -0
  104. data/test/connection/test_session.rb +488 -0
  105. data/test/known_hosts/github +1 -0
  106. data/test/test_all.rb +9 -0
  107. data/test/test_buffer.rb +426 -0
  108. data/test/test_buffered_io.rb +63 -0
  109. data/test/test_config.rb +120 -0
  110. data/test/test_key_factory.rb +121 -0
  111. data/test/test_known_hosts.rb +13 -0
  112. data/test/transport/hmac/test_md5.rb +39 -0
  113. data/test/transport/hmac/test_md5_96.rb +25 -0
  114. data/test/transport/hmac/test_none.rb +34 -0
  115. data/test/transport/hmac/test_ripemd160.rb +34 -0
  116. data/test/transport/hmac/test_sha1.rb +34 -0
  117. data/test/transport/hmac/test_sha1_96.rb +25 -0
  118. data/test/transport/hmac/test_sha2_256.rb +35 -0
  119. data/test/transport/hmac/test_sha2_256_96.rb +25 -0
  120. data/test/transport/hmac/test_sha2_512.rb +35 -0
  121. data/test/transport/hmac/test_sha2_512_96.rb +25 -0
  122. data/test/transport/kex/test_diffie_hellman_group14_sha1.rb +13 -0
  123. data/test/transport/kex/test_diffie_hellman_group1_sha1.rb +146 -0
  124. data/test/transport/kex/test_diffie_hellman_group_exchange_sha1.rb +92 -0
  125. data/test/transport/kex/test_diffie_hellman_group_exchange_sha256.rb +33 -0
  126. data/test/transport/kex/test_ecdh_sha2_nistp256.rb +161 -0
  127. data/test/transport/kex/test_ecdh_sha2_nistp384.rb +37 -0
  128. data/test/transport/kex/test_ecdh_sha2_nistp521.rb +37 -0
  129. data/test/transport/test_algorithms.rb +330 -0
  130. data/test/transport/test_cipher_factory.rb +441 -0
  131. data/test/transport/test_hmac.rb +34 -0
  132. data/test/transport/test_identity_cipher.rb +40 -0
  133. data/test/transport/test_packet_stream.rb +1745 -0
  134. data/test/transport/test_server_version.rb +78 -0
  135. data/test/transport/test_session.rb +315 -0
  136. data/test/transport/test_state.rb +179 -0
  137. metadata +208 -0
@@ -0,0 +1,81 @@
1
+ require 'net/ssh/connection/constants'
2
+ require 'net/ssh/transport/constants'
3
+
4
+ module Net; module SSH; module Test
5
+
6
+ # This is an abstract class, not to be instantiated directly, subclassed by
7
+ # Net::SSH::Test::LocalPacket and Net::SSH::Test::RemotePacket. It implements
8
+ # functionality common to those subclasses.
9
+ #
10
+ # These packets are not true packets, in that they don't represent what was
11
+ # actually sent between the hosst; rather, they represent what was expected
12
+ # to be sent, as dictated by the script (Net::SSH::Test::Script). Thus,
13
+ # though they are defined with data elements, these data elements are used
14
+ # to either validate data that was sent by the local host (Net::SSH::Test::LocalPacket)
15
+ # or to mimic the sending of data by the remote host (Net::SSH::Test::RemotePacket).
16
+ class Packet
17
+ include Net::SSH::Transport::Constants
18
+ include Net::SSH::Connection::Constants
19
+
20
+ # Ceate a new packet of the given +type+, and with +args+ being a list of
21
+ # data elements in the order expected for packets of the given +type+
22
+ # (see #types).
23
+ def initialize(type, *args)
24
+ @type = self.class.const_get(type.to_s.upcase)
25
+ @data = args
26
+ end
27
+
28
+ # The default for +remote?+ is false. Subclasses should override as necessary.
29
+ def remote?
30
+ false
31
+ end
32
+
33
+ # The default for +local?+ is false. Subclasses should override as necessary.
34
+ def local?
35
+ false
36
+ end
37
+
38
+ # Instantiates the packets data elements. When the packet was first defined,
39
+ # some elements may not have been fully realized, and were described as
40
+ # Proc objects rather than atomic types. This invokes those Proc objects
41
+ # and replaces them with their returned values. This allows for values
42
+ # like Net::SSH::Test::Channel#remote_id to be used in scripts before
43
+ # the remote_id is known (since it is only known after a channel has been
44
+ # confirmed open).
45
+ def instantiate!
46
+ @data.map! { |i| i.respond_to?(:call) ? i.call : i }
47
+ end
48
+
49
+ # Returns an array of symbols describing the data elements for packets of
50
+ # the same type as this packet. These types are used to either validate
51
+ # sent packets (Net::SSH::Test::LocalPacket) or build received packets
52
+ # (Net::SSH::Test::RemotePacket).
53
+ #
54
+ # Not all packet types are defined here. As new packet types are required
55
+ # (e.g., a unit test needs to test that the remote host sent a packet that
56
+ # is not implemented here), the description of that packet should be
57
+ # added. Unsupported packet types will otherwise raise an exception.
58
+ def types
59
+ @types ||= case @type
60
+ when KEXINIT then
61
+ [:long, :long, :long, :long,
62
+ :string, :string, :string, :string, :string, :string, :string, :string, :string, :string,
63
+ :bool]
64
+ when NEWKEYS then []
65
+ when CHANNEL_OPEN then [:string, :long, :long, :long]
66
+ when CHANNEL_OPEN_CONFIRMATION then [:long, :long, :long, :long]
67
+ when CHANNEL_DATA then [:long, :string]
68
+ when CHANNEL_EOF, CHANNEL_CLOSE, CHANNEL_SUCCESS, CHANNEL_FAILURE then [:long]
69
+ when CHANNEL_REQUEST
70
+ parts = [:long, :string, :bool]
71
+ case @data[1]
72
+ when "exec", "subsystem" then parts << :string
73
+ when "exit-status" then parts << :long
74
+ else raise "don't know what to do about #{@data[1]} channel request"
75
+ end
76
+ else raise "don't know how to parse packet type #{@type}"
77
+ end
78
+ end
79
+ end
80
+
81
+ end; end; end
@@ -0,0 +1,38 @@
1
+ require 'net/ssh/buffer'
2
+ require 'net/ssh/test/packet'
3
+
4
+ module Net; module SSH; module Test
5
+
6
+ # This is a specialization of Net::SSH::Test::Packet for representing mock
7
+ # packets that are received by the local (client) host. These are created
8
+ # automatically by Net::SSH::Test::Script and Net::SSH::Test::Channel by any
9
+ # of the gets_* methods.
10
+ class RemotePacket < Packet
11
+ # Returns +true+; this is a remote packet.
12
+ def remote?
13
+ true
14
+ end
15
+
16
+ # The #process method should only be called on Net::SSH::Test::LocalPacket
17
+ # packets; if it is attempted on a remote packet, then it is an expectation
18
+ # mismatch (a remote packet was received when a local packet was expected
19
+ # to be sent). This will happen when either your test script
20
+ # (Net::SSH::Test::Script) or your program are wrong.
21
+ def process(packet)
22
+ raise "received packet type #{packet.read_byte} and was not expecting any packet"
23
+ end
24
+
25
+ # Returns this remote packet as a string, suitable for parsing by
26
+ # Net::SSH::Transport::PacketStream and friends. When a remote packet is
27
+ # received, this method is called and the result concatenated onto the
28
+ # input buffer for the packet stream.
29
+ def to_s
30
+ @to_s ||= begin
31
+ instantiate!
32
+ string = Net::SSH::Buffer.from(:byte, @type, *types.zip(@data).flatten).to_s
33
+ [string.length, string].pack("NA*")
34
+ end
35
+ end
36
+ end
37
+
38
+ end; end; end
@@ -0,0 +1,157 @@
1
+ require 'net/ssh/test/channel'
2
+ require 'net/ssh/test/local_packet'
3
+ require 'net/ssh/test/remote_packet'
4
+
5
+ module Net; module SSH; module Test
6
+
7
+ # Represents a sequence of scripted events that identify the behavior that
8
+ # a test expects. Methods named "sends_*" create events for packets being
9
+ # sent from the local to the remote host, and methods named "gets_*" create
10
+ # events for packets being received by the local from the remote host.
11
+ #
12
+ # A reference to a script. is generally obtained in a unit test via the
13
+ # Net::SSH::Test#story helper method:
14
+ #
15
+ # story do |script|
16
+ # channel = script.opens_channel
17
+ # ...
18
+ # end
19
+ class Script
20
+ # The list of scripted events. These will be Net::SSH::Test::LocalPacket
21
+ # and Net::SSH::Test::RemotePacket instances.
22
+ attr_reader :events
23
+
24
+ # Create a new, empty script.
25
+ def initialize
26
+ @events = []
27
+ end
28
+
29
+ # Scripts the opening of a channel by adding a local packet sending the
30
+ # channel open request, and if +confirm+ is true (the default), also
31
+ # adding a remote packet confirming the new channel.
32
+ #
33
+ # A new Net::SSH::Test::Channel instance is returned, which can be used
34
+ # to script additional channel operations.
35
+ def opens_channel(confirm=true)
36
+ channel = Channel.new(self)
37
+ channel.remote_id = 5555
38
+
39
+ events << LocalPacket.new(:channel_open) { |p| channel.local_id = p[:remote_id] }
40
+
41
+ if confirm
42
+ events << RemotePacket.new(:channel_open_confirmation, channel.local_id, channel.remote_id, 0x20000, 0x10000)
43
+ end
44
+
45
+ channel
46
+ end
47
+
48
+ # A convenience method for adding an arbitrary local packet to the events
49
+ # list.
50
+ def sends(type, *args, &block)
51
+ events << LocalPacket.new(type, *args, &block)
52
+ end
53
+
54
+ # A convenience method for adding an arbitrary remote packet to the events
55
+ # list.
56
+ def gets(type, *args)
57
+ events << RemotePacket.new(type, *args)
58
+ end
59
+
60
+ # Scripts the sending of a new channel request packet to the remote host.
61
+ # +channel+ should be an instance of Net::SSH::Test::Channel. +request+
62
+ # is a string naming the request type to send, +reply+ is a boolean
63
+ # indicating whether a response to this packet is required , and +data+
64
+ # is any additional request-specific data that this packet should send.
65
+ # +success+ indicates whether the response (if one is required) should be
66
+ # success or failure.
67
+ #
68
+ # If a reply is desired, a remote packet will also be queued, :channel_success
69
+ # if +success+ is true, or :channel_failure if +success+ is false.
70
+ #
71
+ # This will typically be called via Net::SSH::Test::Channel#sends_exec or
72
+ # Net::SSH::Test::Channel#sends_subsystem.
73
+ def sends_channel_request(channel, request, reply, data, success=true)
74
+ events << LocalPacket.new(:channel_request, channel.remote_id, request, reply, data)
75
+ if reply
76
+ if success
77
+ events << RemotePacket.new(:channel_success, channel.local_id)
78
+ else
79
+ events << RemotePacket.new(:channel_failure, channel.local_id)
80
+ end
81
+ end
82
+ end
83
+
84
+ # Scripts the sending of a channel data packet. +channel+ must be a
85
+ # Net::SSH::Test::Channel object, and +data+ is the (string) data to
86
+ # expect will be sent.
87
+ #
88
+ # This will typically be called via Net::SSH::Test::Channel#sends_data.
89
+ def sends_channel_data(channel, data)
90
+ events << LocalPacket.new(:channel_data, channel.remote_id, data)
91
+ end
92
+
93
+ # Scripts the sending of a channel EOF packet from the given
94
+ # Net::SSH::Test::Channel +channel+. This will typically be called via
95
+ # Net::SSH::Test::Channel#sends_eof.
96
+ def sends_channel_eof(channel)
97
+ events << LocalPacket.new(:channel_eof, channel.remote_id)
98
+ end
99
+
100
+ # Scripts the sending of a channel close packet from the given
101
+ # Net::SSH::Test::Channel +channel+. This will typically be called via
102
+ # Net::SSH::Test::Channel#sends_close.
103
+ def sends_channel_close(channel)
104
+ events << LocalPacket.new(:channel_close, channel.remote_id)
105
+ end
106
+
107
+ # Scripts the reception of a channel data packet from the remote host by
108
+ # the given Net::SSH::Test::Channel +channel+. This will typically be
109
+ # called via Net::SSH::Test::Channel#gets_data.
110
+ def gets_channel_data(channel, data)
111
+ events << RemotePacket.new(:channel_data, channel.local_id, data)
112
+ end
113
+
114
+ # Scripts the reception of a channel request packet from the remote host by
115
+ # the given Net::SSH::Test::Channel +channel+. This will typically be
116
+ # called via Net::SSH::Test::Channel#gets_exit_status.
117
+ def gets_channel_request(channel, request, reply, data)
118
+ events << RemotePacket.new(:channel_request, channel.local_id, request, reply, data)
119
+ end
120
+
121
+ # Scripts the reception of a channel EOF packet from the remote host by
122
+ # the given Net::SSH::Test::Channel +channel+. This will typically be
123
+ # called via Net::SSH::Test::Channel#gets_eof.
124
+ def gets_channel_eof(channel)
125
+ events << RemotePacket.new(:channel_eof, channel.local_id)
126
+ end
127
+
128
+ # Scripts the reception of a channel close packet from the remote host by
129
+ # the given Net::SSH::Test::Channel +channel+. This will typically be
130
+ # called via Net::SSH::Test::Channel#gets_close.
131
+ def gets_channel_close(channel)
132
+ events << RemotePacket.new(:channel_close, channel.local_id)
133
+ end
134
+
135
+ # By default, removes the next event in the list and returns it. However,
136
+ # this can also be used to non-destructively peek at the next event in the
137
+ # list, by passing :first as the argument.
138
+ #
139
+ # # remove the next event and return it
140
+ # event = script.next
141
+ #
142
+ # # peek at the next event
143
+ # event = script.next(:first)
144
+ def next(mode=:shift)
145
+ events.send(mode)
146
+ end
147
+
148
+ # Compare the given packet against the next event in the list. If there is
149
+ # no next event, an exception will be raised. This is called by
150
+ # Net::SSH::Test::Extensions::PacketStream#test_enqueue_packet.
151
+ def process(packet)
152
+ event = events.shift or raise "end of script reached, but got a packet type #{packet.read_byte}"
153
+ event.process(packet)
154
+ end
155
+ end
156
+
157
+ end; end; end
@@ -0,0 +1,64 @@
1
+ require 'socket'
2
+ require 'stringio'
3
+ require 'net/ssh/test/extensions'
4
+ require 'net/ssh/test/script'
5
+
6
+ module Net; module SSH; module Test
7
+
8
+ # A mock socket implementation for use in testing. It implements the minimum
9
+ # necessary interface for interacting with the rest of the Net::SSH::Test
10
+ # system.
11
+ class Socket < StringIO
12
+ attr_reader :host, :port
13
+
14
+ # The Net::SSH::Test::Script object in use by this socket. This is the
15
+ # canonical script instance that should be used for any test depending on
16
+ # this socket instance.
17
+ attr_reader :script
18
+
19
+ # Create a new test socket. This will also instantiate a new Net::SSH::Test::Script
20
+ # and seed it with the necessary events to power the initialization of the
21
+ # connection.
22
+ def initialize
23
+ extend(Net::SSH::Transport::PacketStream)
24
+ super "SSH-2.0-Test\r\n"
25
+
26
+ @script = Script.new
27
+
28
+ script.gets(:kexinit, 1, 2, 3, 4, "test", "ssh-rsa", "none", "none", "none", "none", "none", "none", "", "", false)
29
+ script.sends(:kexinit)
30
+ script.sends(:newkeys)
31
+ script.gets(:newkeys)
32
+ end
33
+
34
+ # This doesn't actually do anything, since we don't really care what gets
35
+ # written.
36
+ def write(data)
37
+ # black hole, because we don't actually care about what gets written
38
+ end
39
+
40
+ # Allows the socket to also mimic a socket factory, simply returning
41
+ # +self+.
42
+ def open(host, port)
43
+ @host, @port = host, port
44
+ self
45
+ end
46
+
47
+ # Returns a sockaddr struct for the port and host that were used when the
48
+ # socket was instantiated.
49
+ def getpeername
50
+ ::Socket.sockaddr_in(port, host)
51
+ end
52
+
53
+ # Alias to #read, but never returns nil (returns an empty string instead).
54
+ def recv(n)
55
+ read(n) || ""
56
+ end
57
+
58
+ def readpartial(n)
59
+ recv(n)
60
+ end
61
+
62
+ end
63
+
64
+ end; end; end
@@ -0,0 +1,407 @@
1
+ require 'net/ssh/buffer'
2
+ require 'net/ssh/known_hosts'
3
+ require 'net/ssh/loggable'
4
+ require 'net/ssh/transport/cipher_factory'
5
+ require 'net/ssh/transport/constants'
6
+ require 'net/ssh/transport/hmac'
7
+ require 'net/ssh/transport/kex'
8
+ require 'net/ssh/transport/server_version'
9
+
10
+ module Net; module SSH; module Transport
11
+
12
+ # Implements the higher-level logic behind an SSH key-exchange. It handles
13
+ # both the initial exchange, as well as subsequent re-exchanges (as needed).
14
+ # It also encapsulates the negotiation of the algorithms, and provides a
15
+ # single point of access to the negotiated algorithms.
16
+ #
17
+ # You will never instantiate or reference this directly. It is used
18
+ # internally by the transport layer.
19
+ class Algorithms
20
+ include Constants, Loggable
21
+
22
+ # Define the default algorithms, in order of preference, supported by
23
+ # Net::SSH.
24
+ ALGORITHMS = {
25
+ :host_key => %w(ssh-rsa ssh-dss),
26
+ :kex => %w(diffie-hellman-group-exchange-sha1
27
+ diffie-hellman-group1-sha1
28
+ diffie-hellman-group14-sha1
29
+ diffie-hellman-group-exchange-sha256),
30
+ :encryption => %w(aes128-cbc 3des-cbc blowfish-cbc cast128-cbc
31
+ aes192-cbc aes256-cbc rijndael-cbc@lysator.liu.se
32
+ idea-cbc none arcfour128 arcfour256 arcfour
33
+ aes128-ctr aes192-ctr aes256-ctr
34
+ camellia128-cbc camellia192-cbc camellia256-cbc
35
+ camellia128-cbc@openssh.org
36
+ camellia192-cbc@openssh.org
37
+ camellia256-cbc@openssh.org
38
+ camellia128-ctr camellia192-ctr camellia256-ctr
39
+ camellia128-ctr@openssh.org
40
+ camellia192-ctr@openssh.org
41
+ camellia256-ctr@openssh.org
42
+ cast128-ctr blowfish-ctr 3des-ctr
43
+ ),
44
+ :hmac => %w(hmac-sha1 hmac-md5 hmac-sha1-96 hmac-md5-96
45
+ hmac-ripemd160 hmac-ripemd160@openssh.com
46
+ hmac-sha2-256 hmac-sha2-512 hmac-sha2-256-96
47
+ hmac-sha2-512-96 none),
48
+ :compression => %w(none zlib@openssh.com zlib),
49
+ :language => %w()
50
+ }
51
+ if defined?(OpenSSL::PKey::EC)
52
+ ALGORITHMS[:host_key] += %w(ecdsa-sha2-nistp256
53
+ ecdsa-sha2-nistp384
54
+ ecdsa-sha2-nistp521)
55
+ ALGORITHMS[:kex] += %w(ecdh-sha2-nistp256
56
+ ecdh-sha2-nistp384
57
+ ecdh-sha2-nistp521)
58
+ end
59
+
60
+ # The underlying transport layer session that supports this object
61
+ attr_reader :session
62
+
63
+ # The hash of options used to initialize this object
64
+ attr_reader :options
65
+
66
+ # The kex algorithm to use settled on between the client and server.
67
+ attr_reader :kex
68
+
69
+ # The type of host key that will be used for this session.
70
+ attr_reader :host_key
71
+
72
+ # The type of the cipher to use to encrypt packets sent from the client to
73
+ # the server.
74
+ attr_reader :encryption_client
75
+
76
+ # The type of the cipher to use to decrypt packets arriving from the server.
77
+ attr_reader :encryption_server
78
+
79
+ # The type of HMAC to use to sign packets sent by the client.
80
+ attr_reader :hmac_client
81
+
82
+ # The type of HMAC to use to validate packets arriving from the server.
83
+ attr_reader :hmac_server
84
+
85
+ # The type of compression to use to compress packets being sent by the client.
86
+ attr_reader :compression_client
87
+
88
+ # The type of compression to use to decompress packets arriving from the server.
89
+ attr_reader :compression_server
90
+
91
+ # The language that will be used in messages sent by the client.
92
+ attr_reader :language_client
93
+
94
+ # The language that will be used in messages sent from the server.
95
+ attr_reader :language_server
96
+
97
+ # The hash of algorithms preferred by the client, which will be told to
98
+ # the server during algorithm negotiation.
99
+ attr_reader :algorithms
100
+
101
+ # The session-id for this session, as decided during the initial key exchange.
102
+ attr_reader :session_id
103
+
104
+ # Returns true if the given packet can be processed during a key-exchange.
105
+ def self.allowed_packet?(packet)
106
+ ( 1.. 4).include?(packet.type) ||
107
+ ( 6..19).include?(packet.type) ||
108
+ (21..49).include?(packet.type)
109
+ end
110
+
111
+ # Instantiates a new Algorithms object, and prepares the hash of preferred
112
+ # algorithms based on the options parameter and the ALGORITHMS constant.
113
+ def initialize(session, options={})
114
+ @session = session
115
+ @logger = session.logger
116
+ @options = options
117
+ @algorithms = {}
118
+ @pending = @initialized = false
119
+ @client_packet = @server_packet = nil
120
+ prepare_preferred_algorithms!
121
+ end
122
+
123
+ # Request a rekey operation. This will return immediately, and does not
124
+ # actually perform the rekey operation. It does cause the session to change
125
+ # state, however--until the key exchange finishes, no new packets will be
126
+ # processed.
127
+ def rekey!
128
+ @client_packet = @server_packet = nil
129
+ @initialized = false
130
+ send_kexinit
131
+ end
132
+
133
+ # Called by the transport layer when a KEXINIT packet is recieved, indicating
134
+ # that the server wants to exchange keys. This can be spontaneous, or it
135
+ # can be in response to a client-initiated rekey request (see #rekey!). Either
136
+ # way, this will block until the key exchange completes.
137
+ def accept_kexinit(packet)
138
+ info { "got KEXINIT from server" }
139
+ @server_data = parse_server_algorithm_packet(packet)
140
+ @server_packet = @server_data[:raw]
141
+ if !pending?
142
+ send_kexinit
143
+ else
144
+ proceed!
145
+ end
146
+ end
147
+
148
+ # A convenience method for accessing the list of preferred types for a
149
+ # specific algorithm (see #algorithms).
150
+ def [](key)
151
+ algorithms[key]
152
+ end
153
+
154
+ # Returns +true+ if a key-exchange is pending. This will be true from the
155
+ # moment either the client or server requests the key exchange, until the
156
+ # exchange completes. While an exchange is pending, only a limited number
157
+ # of packets are allowed, so event processing essentially stops during this
158
+ # period.
159
+ def pending?
160
+ @pending
161
+ end
162
+
163
+ # Returns true if no exchange is pending, and otherwise returns true or
164
+ # false depending on whether the given packet is of a type that is allowed
165
+ # during a key exchange.
166
+ def allow?(packet)
167
+ !pending? || Algorithms.allowed_packet?(packet)
168
+ end
169
+
170
+ # Returns true if the algorithms have been negotiated at all.
171
+ def initialized?
172
+ @initialized
173
+ end
174
+
175
+ private
176
+
177
+ # Sends a KEXINIT packet to the server. If a server KEXINIT has already
178
+ # been received, this will then invoke #proceed! to proceed with the key
179
+ # exchange, otherwise it returns immediately (but sets the object to the
180
+ # pending state).
181
+ def send_kexinit
182
+ info { "sending KEXINIT" }
183
+ @pending = true
184
+ packet = build_client_algorithm_packet
185
+ @client_packet = packet.to_s
186
+ session.send_message(packet)
187
+ proceed! if @server_packet
188
+ end
189
+
190
+ # After both client and server have sent their KEXINIT packets, this
191
+ # will do the algorithm negotiation and key exchange. Once both finish,
192
+ # the object leaves the pending state and the method returns.
193
+ def proceed!
194
+ info { "negotiating algorithms" }
195
+ negotiate_algorithms
196
+ exchange_keys
197
+ @pending = false
198
+ end
199
+
200
+ # Prepares the list of preferred algorithms, based on the options hash
201
+ # that was given when the object was constructed, and the ALGORITHMS
202
+ # constant. Also, when determining the host_key type to use, the known
203
+ # hosts files are examined to see if the host has ever sent a host_key
204
+ # before, and if so, that key type is used as the preferred type for
205
+ # communicating with this server.
206
+ def prepare_preferred_algorithms!
207
+ options[:compression] = %w(zlib@openssh.com zlib) if options[:compression] == true
208
+
209
+ ALGORITHMS.each do |algorithm, list|
210
+ algorithms[algorithm] = list.dup
211
+
212
+ # apply the preferred algorithm order, if any
213
+ if options[algorithm]
214
+ algorithms[algorithm] = Array(options[algorithm]).compact.uniq
215
+ invalid = algorithms[algorithm].detect { |name| !ALGORITHMS[algorithm].include?(name) }
216
+ raise NotImplementedError, "unsupported #{algorithm} algorithm: `#{invalid}'" if invalid
217
+
218
+ # make sure all of our supported algorithms are tacked onto the
219
+ # end, so that if the user tries to give a list of which none are
220
+ # supported, we can still proceed.
221
+ list.each { |name| algorithms[algorithm] << name unless algorithms[algorithm].include?(name) }
222
+ end
223
+ end
224
+
225
+ # for convention, make sure our list has the same keys as the server
226
+ # list
227
+
228
+ algorithms[:encryption_client ] = algorithms[:encryption_server ] = algorithms[:encryption]
229
+ algorithms[:hmac_client ] = algorithms[:hmac_server ] = algorithms[:hmac]
230
+ algorithms[:compression_client] = algorithms[:compression_server] = algorithms[:compression]
231
+ algorithms[:language_client ] = algorithms[:language_server ] = algorithms[:language]
232
+
233
+ if !options.key?(:host_key)
234
+ # make sure the host keys are specified in preference order, where any
235
+ # existing known key for the host has preference.
236
+
237
+ existing_keys = KnownHosts.search_for(options[:host_key_alias] || session.host_as_string, options)
238
+ host_keys = existing_keys.map { |key| key.ssh_type }.uniq
239
+ algorithms[:host_key].each do |name|
240
+ host_keys << name unless host_keys.include?(name)
241
+ end
242
+ algorithms[:host_key] = host_keys
243
+ end
244
+ end
245
+
246
+ # Parses a KEXINIT packet from the server.
247
+ def parse_server_algorithm_packet(packet)
248
+ data = { :raw => packet.content }
249
+
250
+ packet.read(16) # skip the cookie value
251
+
252
+ data[:kex] = packet.read_string.split(/,/)
253
+ data[:host_key] = packet.read_string.split(/,/)
254
+ data[:encryption_client] = packet.read_string.split(/,/)
255
+ data[:encryption_server] = packet.read_string.split(/,/)
256
+ data[:hmac_client] = packet.read_string.split(/,/)
257
+ data[:hmac_server] = packet.read_string.split(/,/)
258
+ data[:compression_client] = packet.read_string.split(/,/)
259
+ data[:compression_server] = packet.read_string.split(/,/)
260
+ data[:language_client] = packet.read_string.split(/,/)
261
+ data[:language_server] = packet.read_string.split(/,/)
262
+
263
+ # TODO: if first_kex_packet_follows, we need to try to skip the
264
+ # actual kexinit stuff and try to guess what the server is doing...
265
+ # need to read more about this scenario.
266
+ first_kex_packet_follows = packet.read_bool
267
+
268
+ return data
269
+ end
270
+
271
+ # Given the #algorithms map of preferred algorithm types, this constructs
272
+ # a KEXINIT packet to send to the server. It does not actually send it,
273
+ # it simply builds the packet and returns it.
274
+ def build_client_algorithm_packet
275
+ kex = algorithms[:kex ].join(",")
276
+ host_key = algorithms[:host_key ].join(",")
277
+ encryption = algorithms[:encryption ].join(",")
278
+ hmac = algorithms[:hmac ].join(",")
279
+ compression = algorithms[:compression].join(",")
280
+ language = algorithms[:language ].join(",")
281
+
282
+ Net::SSH::Buffer.from(:byte, KEXINIT,
283
+ :long, [rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF)],
284
+ :string, [kex, host_key, encryption, encryption, hmac, hmac],
285
+ :string, [compression, compression, language, language],
286
+ :bool, false, :long, 0)
287
+ end
288
+
289
+ # Given the parsed server KEX packet, and the client's preferred algorithm
290
+ # lists in #algorithms, determine which preferred algorithms each has
291
+ # in common and set those as the selected algorithms. If, for any algorithm,
292
+ # no type can be settled on, an exception is raised.
293
+ def negotiate_algorithms
294
+ @kex = negotiate(:kex)
295
+ @host_key = negotiate(:host_key)
296
+ @encryption_client = negotiate(:encryption_client)
297
+ @encryption_server = negotiate(:encryption_server)
298
+ @hmac_client = negotiate(:hmac_client)
299
+ @hmac_server = negotiate(:hmac_server)
300
+ @compression_client = negotiate(:compression_client)
301
+ @compression_server = negotiate(:compression_server)
302
+ @language_client = negotiate(:language_client) rescue ""
303
+ @language_server = negotiate(:language_server) rescue ""
304
+
305
+ debug do
306
+ "negotiated:\n" +
307
+ [:kex, :host_key, :encryption_server, :encryption_client, :hmac_client, :hmac_server, :compression_client, :compression_server, :language_client, :language_server].map do |key|
308
+ "* #{key}: #{instance_variable_get("@#{key}")}"
309
+ end.join("\n")
310
+ end
311
+ end
312
+
313
+ # Negotiates a single algorithm based on the preferences reported by the
314
+ # server and those set by the client. This is called by
315
+ # #negotiate_algorithms.
316
+ def negotiate(algorithm)
317
+ match = self[algorithm].find { |item| @server_data[algorithm].include?(item) }
318
+
319
+ if match.nil?
320
+ raise Net::SSH::Exception, "could not settle on #{algorithm} algorithm"
321
+ end
322
+
323
+ return match
324
+ end
325
+
326
+ # Considers the sizes of the keys and block-sizes for the selected ciphers,
327
+ # and the lengths of the hmacs, and returns the largest as the byte requirement
328
+ # for the key-exchange algorithm.
329
+ def kex_byte_requirement
330
+ sizes = [8] # require at least 8 bytes
331
+
332
+ sizes.concat(CipherFactory.get_lengths(encryption_client))
333
+ sizes.concat(CipherFactory.get_lengths(encryption_server))
334
+
335
+ sizes << HMAC.key_length(hmac_client)
336
+ sizes << HMAC.key_length(hmac_server)
337
+
338
+ sizes.max
339
+ end
340
+
341
+ # Instantiates one of the Transport::Kex classes (based on the negotiated
342
+ # kex algorithm), and uses it to exchange keys. Then, the ciphers and
343
+ # HMACs are initialized and fed to the transport layer, to be used in
344
+ # further communication with the server.
345
+ def exchange_keys
346
+ debug { "exchanging keys" }
347
+
348
+ algorithm = Kex::MAP[kex].new(self, session,
349
+ :client_version_string => Net::SSH::Transport::ServerVersion::PROTO_VERSION,
350
+ :server_version_string => session.server_version.version,
351
+ :server_algorithm_packet => @server_packet,
352
+ :client_algorithm_packet => @client_packet,
353
+ :need_bytes => kex_byte_requirement,
354
+ :logger => logger)
355
+ result = algorithm.exchange_keys
356
+
357
+ secret = result[:shared_secret].to_ssh
358
+ hash = result[:session_id]
359
+ digester = result[:hashing_algorithm]
360
+
361
+ @session_id ||= hash
362
+
363
+ key = Proc.new { |salt| digester.digest(secret + hash + salt + @session_id) }
364
+
365
+ iv_client = key["A"]
366
+ iv_server = key["B"]
367
+ key_client = key["C"]
368
+ key_server = key["D"]
369
+ mac_key_client = key["E"]
370
+ mac_key_server = key["F"]
371
+
372
+ parameters = { :shared => secret, :hash => hash, :digester => digester }
373
+
374
+ cipher_client = CipherFactory.get(encryption_client, parameters.merge(:iv => iv_client, :key => key_client, :encrypt => true))
375
+ cipher_server = CipherFactory.get(encryption_server, parameters.merge(:iv => iv_server, :key => key_server, :decrypt => true))
376
+
377
+ mac_client = HMAC.get(hmac_client, mac_key_client, parameters)
378
+ mac_server = HMAC.get(hmac_server, mac_key_server, parameters)
379
+
380
+ session.configure_client :cipher => cipher_client, :hmac => mac_client,
381
+ :compression => normalize_compression_name(compression_client),
382
+ :compression_level => options[:compression_level],
383
+ :rekey_limit => options[:rekey_limit],
384
+ :max_packets => options[:rekey_packet_limit],
385
+ :max_blocks => options[:rekey_blocks_limit]
386
+
387
+ session.configure_server :cipher => cipher_server, :hmac => mac_server,
388
+ :compression => normalize_compression_name(compression_server),
389
+ :rekey_limit => options[:rekey_limit],
390
+ :max_packets => options[:rekey_packet_limit],
391
+ :max_blocks => options[:rekey_blocks_limit]
392
+
393
+ @initialized = true
394
+ end
395
+
396
+ # Given the SSH name for some compression algorithm, return a normalized
397
+ # name as a symbol.
398
+ def normalize_compression_name(name)
399
+ case name
400
+ when "none" then false
401
+ when "zlib" then :standard
402
+ when "zlib@openssh.com" then :delayed
403
+ else raise ArgumentError, "unknown compression type `#{name}'"
404
+ end
405
+ end
406
+ end
407
+ end; end; end