sonixlabs-net-ssh 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. data/CHANGELOG.rdoc +262 -0
  2. data/Manifest +121 -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 +179 -0
  9. data/lib/net/ssh/authentication/constants.rb +18 -0
  10. data/lib/net/ssh/authentication/key_manager.rb +253 -0
  11. data/lib/net/ssh/authentication/methods/abstract.rb +60 -0
  12. data/lib/net/ssh/authentication/methods/hostbased.rb +75 -0
  13. data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +70 -0
  14. data/lib/net/ssh/authentication/methods/password.rb +43 -0
  15. data/lib/net/ssh/authentication/methods/publickey.rb +96 -0
  16. data/lib/net/ssh/authentication/pageant.rb +264 -0
  17. data/lib/net/ssh/authentication/session.rb +146 -0
  18. data/lib/net/ssh/buffer.rb +340 -0
  19. data/lib/net/ssh/buffered_io.rb +198 -0
  20. data/lib/net/ssh/config.rb +207 -0
  21. data/lib/net/ssh/connection/channel.rb +630 -0
  22. data/lib/net/ssh/connection/constants.rb +33 -0
  23. data/lib/net/ssh/connection/session.rb +597 -0
  24. data/lib/net/ssh/connection/term.rb +178 -0
  25. data/lib/net/ssh/errors.rb +88 -0
  26. data/lib/net/ssh/key_factory.rb +102 -0
  27. data/lib/net/ssh/known_hosts.rb +129 -0
  28. data/lib/net/ssh/loggable.rb +61 -0
  29. data/lib/net/ssh/packet.rb +102 -0
  30. data/lib/net/ssh/prompt.rb +93 -0
  31. data/lib/net/ssh/proxy/command.rb +75 -0
  32. data/lib/net/ssh/proxy/errors.rb +14 -0
  33. data/lib/net/ssh/proxy/http.rb +94 -0
  34. data/lib/net/ssh/proxy/socks4.rb +70 -0
  35. data/lib/net/ssh/proxy/socks5.rb +142 -0
  36. data/lib/net/ssh/ruby_compat.rb +43 -0
  37. data/lib/net/ssh/service/forward.rb +298 -0
  38. data/lib/net/ssh/test.rb +89 -0
  39. data/lib/net/ssh/test/channel.rb +129 -0
  40. data/lib/net/ssh/test/extensions.rb +152 -0
  41. data/lib/net/ssh/test/kex.rb +44 -0
  42. data/lib/net/ssh/test/local_packet.rb +51 -0
  43. data/lib/net/ssh/test/packet.rb +81 -0
  44. data/lib/net/ssh/test/remote_packet.rb +38 -0
  45. data/lib/net/ssh/test/script.rb +157 -0
  46. data/lib/net/ssh/test/socket.rb +64 -0
  47. data/lib/net/ssh/transport/algorithms.rb +386 -0
  48. data/lib/net/ssh/transport/cipher_factory.rb +79 -0
  49. data/lib/net/ssh/transport/constants.rb +30 -0
  50. data/lib/net/ssh/transport/hmac.rb +42 -0
  51. data/lib/net/ssh/transport/hmac/abstract.rb +79 -0
  52. data/lib/net/ssh/transport/hmac/md5.rb +12 -0
  53. data/lib/net/ssh/transport/hmac/md5_96.rb +11 -0
  54. data/lib/net/ssh/transport/hmac/none.rb +15 -0
  55. data/lib/net/ssh/transport/hmac/sha1.rb +13 -0
  56. data/lib/net/ssh/transport/hmac/sha1_96.rb +11 -0
  57. data/lib/net/ssh/transport/hmac/sha2_256.rb +15 -0
  58. data/lib/net/ssh/transport/hmac/sha2_256_96.rb +13 -0
  59. data/lib/net/ssh/transport/hmac/sha2_512.rb +14 -0
  60. data/lib/net/ssh/transport/hmac/sha2_512_96.rb +13 -0
  61. data/lib/net/ssh/transport/identity_cipher.rb +55 -0
  62. data/lib/net/ssh/transport/kex.rb +17 -0
  63. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +208 -0
  64. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +80 -0
  65. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha256.rb +15 -0
  66. data/lib/net/ssh/transport/key_expander.rb +26 -0
  67. data/lib/net/ssh/transport/openssl.rb +127 -0
  68. data/lib/net/ssh/transport/packet_stream.rb +235 -0
  69. data/lib/net/ssh/transport/server_version.rb +71 -0
  70. data/lib/net/ssh/transport/session.rb +278 -0
  71. data/lib/net/ssh/transport/state.rb +206 -0
  72. data/lib/net/ssh/verifiers/lenient.rb +30 -0
  73. data/lib/net/ssh/verifiers/null.rb +12 -0
  74. data/lib/net/ssh/verifiers/strict.rb +53 -0
  75. data/lib/net/ssh/version.rb +62 -0
  76. data/lib/sonixlabs-net-ssh.rb +1 -0
  77. data/net-ssh.gemspec +145 -0
  78. data/setup.rb +1585 -0
  79. data/support/arcfour_check.rb +20 -0
  80. data/support/ssh_tunnel_bug.rb +65 -0
  81. data/test/authentication/methods/common.rb +28 -0
  82. data/test/authentication/methods/test_abstract.rb +51 -0
  83. data/test/authentication/methods/test_hostbased.rb +114 -0
  84. data/test/authentication/methods/test_keyboard_interactive.rb +100 -0
  85. data/test/authentication/methods/test_password.rb +52 -0
  86. data/test/authentication/methods/test_publickey.rb +148 -0
  87. data/test/authentication/test_agent.rb +205 -0
  88. data/test/authentication/test_key_manager.rb +171 -0
  89. data/test/authentication/test_session.rb +106 -0
  90. data/test/common.rb +107 -0
  91. data/test/configs/eqsign +3 -0
  92. data/test/configs/exact_match +8 -0
  93. data/test/configs/host_plus +10 -0
  94. data/test/configs/multihost +4 -0
  95. data/test/configs/wild_cards +14 -0
  96. data/test/connection/test_channel.rb +467 -0
  97. data/test/connection/test_session.rb +488 -0
  98. data/test/test_all.rb +9 -0
  99. data/test/test_buffer.rb +336 -0
  100. data/test/test_buffered_io.rb +63 -0
  101. data/test/test_config.rb +120 -0
  102. data/test/test_key_factory.rb +79 -0
  103. data/test/transport/hmac/test_md5.rb +39 -0
  104. data/test/transport/hmac/test_md5_96.rb +25 -0
  105. data/test/transport/hmac/test_none.rb +34 -0
  106. data/test/transport/hmac/test_sha1.rb +34 -0
  107. data/test/transport/hmac/test_sha1_96.rb +25 -0
  108. data/test/transport/hmac/test_sha2_256.rb +35 -0
  109. data/test/transport/hmac/test_sha2_256_96.rb +25 -0
  110. data/test/transport/hmac/test_sha2_512.rb +35 -0
  111. data/test/transport/hmac/test_sha2_512_96.rb +25 -0
  112. data/test/transport/kex/test_diffie_hellman_group1_sha1.rb +146 -0
  113. data/test/transport/kex/test_diffie_hellman_group_exchange_sha1.rb +92 -0
  114. data/test/transport/kex/test_diffie_hellman_group_exchange_sha256.rb +33 -0
  115. data/test/transport/test_algorithms.rb +308 -0
  116. data/test/transport/test_cipher_factory.rb +213 -0
  117. data/test/transport/test_hmac.rb +34 -0
  118. data/test/transport/test_identity_cipher.rb +40 -0
  119. data/test/transport/test_packet_stream.rb +736 -0
  120. data/test/transport/test_server_version.rb +78 -0
  121. data/test/transport/test_session.rb +315 -0
  122. data/test/transport/test_state.rb +179 -0
  123. metadata +178 -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,386 @@
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-group-exchange-sha256),
29
+ :encryption => %w(aes128-cbc 3des-cbc blowfish-cbc cast128-cbc
30
+ aes192-cbc aes256-cbc rijndael-cbc@lysator.liu.se
31
+ idea-cbc none arcfour128 arcfour256),
32
+ :hmac => %w(hmac-sha1 hmac-md5 hmac-sha1-96 hmac-md5-96
33
+ hmac-sha2-256 hmac-sha2-512 hmac-sha2-256-96
34
+ hmac-sha2-512-96 none),
35
+ :compression => %w(none zlib@openssh.com zlib),
36
+ :language => %w()
37
+ }
38
+
39
+ # The underlying transport layer session that supports this object
40
+ attr_reader :session
41
+
42
+ # The hash of options used to initialize this object
43
+ attr_reader :options
44
+
45
+ # The kex algorithm to use settled on between the client and server.
46
+ attr_reader :kex
47
+
48
+ # The type of host key that will be used for this session.
49
+ attr_reader :host_key
50
+
51
+ # The type of the cipher to use to encrypt packets sent from the client to
52
+ # the server.
53
+ attr_reader :encryption_client
54
+
55
+ # The type of the cipher to use to decrypt packets arriving from the server.
56
+ attr_reader :encryption_server
57
+
58
+ # The type of HMAC to use to sign packets sent by the client.
59
+ attr_reader :hmac_client
60
+
61
+ # The type of HMAC to use to validate packets arriving from the server.
62
+ attr_reader :hmac_server
63
+
64
+ # The type of compression to use to compress packets being sent by the client.
65
+ attr_reader :compression_client
66
+
67
+ # The type of compression to use to decompress packets arriving from the server.
68
+ attr_reader :compression_server
69
+
70
+ # The language that will be used in messages sent by the client.
71
+ attr_reader :language_client
72
+
73
+ # The language that will be used in messages sent from the server.
74
+ attr_reader :language_server
75
+
76
+ # The hash of algorithms preferred by the client, which will be told to
77
+ # the server during algorithm negotiation.
78
+ attr_reader :algorithms
79
+
80
+ # The session-id for this session, as decided during the initial key exchange.
81
+ attr_reader :session_id
82
+
83
+ # Returns true if the given packet can be processed during a key-exchange.
84
+ def self.allowed_packet?(packet)
85
+ ( 1.. 4).include?(packet.type) ||
86
+ ( 6..19).include?(packet.type) ||
87
+ (21..49).include?(packet.type)
88
+ end
89
+
90
+ # Instantiates a new Algorithms object, and prepares the hash of preferred
91
+ # algorithms based on the options parameter and the ALGORITHMS constant.
92
+ def initialize(session, options={})
93
+ @session = session
94
+ @logger = session.logger
95
+ @options = options
96
+ @algorithms = {}
97
+ @pending = @initialized = false
98
+ @client_packet = @server_packet = nil
99
+ prepare_preferred_algorithms!
100
+ end
101
+
102
+ # Request a rekey operation. This will return immediately, and does not
103
+ # actually perform the rekey operation. It does cause the session to change
104
+ # state, however--until the key exchange finishes, no new packets will be
105
+ # processed.
106
+ def rekey!
107
+ @client_packet = @server_packet = nil
108
+ @initialized = false
109
+ send_kexinit
110
+ end
111
+
112
+ # Called by the transport layer when a KEXINIT packet is recieved, indicating
113
+ # that the server wants to exchange keys. This can be spontaneous, or it
114
+ # can be in response to a client-initiated rekey request (see #rekey!). Either
115
+ # way, this will block until the key exchange completes.
116
+ def accept_kexinit(packet)
117
+ info { "got KEXINIT from server" }
118
+ @server_data = parse_server_algorithm_packet(packet)
119
+ @server_packet = @server_data[:raw]
120
+ if !pending?
121
+ send_kexinit
122
+ else
123
+ proceed!
124
+ end
125
+ end
126
+
127
+ # A convenience method for accessing the list of preferred types for a
128
+ # specific algorithm (see #algorithms).
129
+ def [](key)
130
+ algorithms[key]
131
+ end
132
+
133
+ # Returns +true+ if a key-exchange is pending. This will be true from the
134
+ # moment either the client or server requests the key exchange, until the
135
+ # exchange completes. While an exchange is pending, only a limited number
136
+ # of packets are allowed, so event processing essentially stops during this
137
+ # period.
138
+ def pending?
139
+ @pending
140
+ end
141
+
142
+ # Returns true if no exchange is pending, and otherwise returns true or
143
+ # false depending on whether the given packet is of a type that is allowed
144
+ # during a key exchange.
145
+ def allow?(packet)
146
+ !pending? || Algorithms.allowed_packet?(packet)
147
+ end
148
+
149
+ # Returns true if the algorithms have been negotiated at all.
150
+ def initialized?
151
+ @initialized
152
+ end
153
+
154
+ private
155
+
156
+ # Sends a KEXINIT packet to the server. If a server KEXINIT has already
157
+ # been received, this will then invoke #proceed! to proceed with the key
158
+ # exchange, otherwise it returns immediately (but sets the object to the
159
+ # pending state).
160
+ def send_kexinit
161
+ info { "sending KEXINIT" }
162
+ @pending = true
163
+ packet = build_client_algorithm_packet
164
+ @client_packet = packet.to_s
165
+ session.send_message(packet)
166
+ proceed! if @server_packet
167
+ end
168
+
169
+ # After both client and server have sent their KEXINIT packets, this
170
+ # will do the algorithm negotiation and key exchange. Once both finish,
171
+ # the object leaves the pending state and the method returns.
172
+ def proceed!
173
+ info { "negotiating algorithms" }
174
+ negotiate_algorithms
175
+ exchange_keys
176
+ @pending = false
177
+ end
178
+
179
+ # Prepares the list of preferred algorithms, based on the options hash
180
+ # that was given when the object was constructed, and the ALGORITHMS
181
+ # constant. Also, when determining the host_key type to use, the known
182
+ # hosts files are examined to see if the host has ever sent a host_key
183
+ # before, and if so, that key type is used as the preferred type for
184
+ # communicating with this server.
185
+ def prepare_preferred_algorithms!
186
+ options[:compression] = %w(zlib@openssh.com zlib) if options[:compression] == true
187
+
188
+ ALGORITHMS.each do |algorithm, list|
189
+ algorithms[algorithm] = list.dup
190
+
191
+ # apply the preferred algorithm order, if any
192
+ if options[algorithm]
193
+ algorithms[algorithm] = Array(options[algorithm]).compact.uniq
194
+ invalid = algorithms[algorithm].detect { |name| !ALGORITHMS[algorithm].include?(name) }
195
+ raise NotImplementedError, "unsupported #{algorithm} algorithm: `#{invalid}'" if invalid
196
+
197
+ # make sure all of our supported algorithms are tacked onto the
198
+ # end, so that if the user tries to give a list of which none are
199
+ # supported, we can still proceed.
200
+ list.each { |name| algorithms[algorithm] << name unless algorithms[algorithm].include?(name) }
201
+ end
202
+ end
203
+
204
+ # for convention, make sure our list has the same keys as the server
205
+ # list
206
+
207
+ algorithms[:encryption_client ] = algorithms[:encryption_server ] = algorithms[:encryption]
208
+ algorithms[:hmac_client ] = algorithms[:hmac_server ] = algorithms[:hmac]
209
+ algorithms[:compression_client] = algorithms[:compression_server] = algorithms[:compression]
210
+ algorithms[:language_client ] = algorithms[:language_server ] = algorithms[:language]
211
+
212
+ if !options.key?(:host_key)
213
+ # make sure the host keys are specified in preference order, where any
214
+ # existing known key for the host has preference.
215
+
216
+ existing_keys = KnownHosts.search_for(options[:host_key_alias] || session.host_as_string, options)
217
+ host_keys = existing_keys.map { |key| key.ssh_type }.uniq
218
+ algorithms[:host_key].each do |name|
219
+ host_keys << name unless host_keys.include?(name)
220
+ end
221
+ algorithms[:host_key] = host_keys
222
+ end
223
+ end
224
+
225
+ # Parses a KEXINIT packet from the server.
226
+ def parse_server_algorithm_packet(packet)
227
+ data = { :raw => packet.content }
228
+
229
+ packet.read(16) # skip the cookie value
230
+
231
+ data[:kex] = packet.read_string.split(/,/)
232
+ data[:host_key] = packet.read_string.split(/,/)
233
+ data[:encryption_client] = packet.read_string.split(/,/)
234
+ data[:encryption_server] = packet.read_string.split(/,/)
235
+ data[:hmac_client] = packet.read_string.split(/,/)
236
+ data[:hmac_server] = packet.read_string.split(/,/)
237
+ data[:compression_client] = packet.read_string.split(/,/)
238
+ data[:compression_server] = packet.read_string.split(/,/)
239
+ data[:language_client] = packet.read_string.split(/,/)
240
+ data[:language_server] = packet.read_string.split(/,/)
241
+
242
+ # TODO: if first_kex_packet_follows, we need to try to skip the
243
+ # actual kexinit stuff and try to guess what the server is doing...
244
+ # need to read more about this scenario.
245
+ first_kex_packet_follows = packet.read_bool
246
+
247
+ return data
248
+ end
249
+
250
+ # Given the #algorithms map of preferred algorithm types, this constructs
251
+ # a KEXINIT packet to send to the server. It does not actually send it,
252
+ # it simply builds the packet and returns it.
253
+ def build_client_algorithm_packet
254
+ kex = algorithms[:kex ].join(",")
255
+ host_key = algorithms[:host_key ].join(",")
256
+ encryption = algorithms[:encryption ].join(",")
257
+ hmac = algorithms[:hmac ].join(",")
258
+ compression = algorithms[:compression].join(",")
259
+ language = algorithms[:language ].join(",")
260
+
261
+ Net::SSH::Buffer.from(:byte, KEXINIT,
262
+ :long, [rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF)],
263
+ :string, [kex, host_key, encryption, encryption, hmac, hmac],
264
+ :string, [compression, compression, language, language],
265
+ :bool, false, :long, 0)
266
+ end
267
+
268
+ # Given the parsed server KEX packet, and the client's preferred algorithm
269
+ # lists in #algorithms, determine which preferred algorithms each has
270
+ # in common and set those as the selected algorithms. If, for any algorithm,
271
+ # no type can be settled on, an exception is raised.
272
+ def negotiate_algorithms
273
+ @kex = negotiate(:kex)
274
+ @host_key = negotiate(:host_key)
275
+ @encryption_client = negotiate(:encryption_client)
276
+ @encryption_server = negotiate(:encryption_server)
277
+ @hmac_client = negotiate(:hmac_client)
278
+ @hmac_server = negotiate(:hmac_server)
279
+ @compression_client = negotiate(:compression_client)
280
+ @compression_server = negotiate(:compression_server)
281
+ @language_client = negotiate(:language_client) rescue ""
282
+ @language_server = negotiate(:language_server) rescue ""
283
+
284
+ debug do
285
+ "negotiated:\n" +
286
+ [:kex, :host_key, :encryption_server, :encryption_client, :hmac_client, :hmac_server, :compression_client, :compression_server, :language_client, :language_server].map do |key|
287
+ "* #{key}: #{instance_variable_get("@#{key}")}"
288
+ end.join("\n")
289
+ end
290
+ end
291
+
292
+ # Negotiates a single algorithm based on the preferences reported by the
293
+ # server and those set by the client. This is called by
294
+ # #negotiate_algorithms.
295
+ def negotiate(algorithm)
296
+ match = self[algorithm].find { |item| @server_data[algorithm].include?(item) }
297
+
298
+ if match.nil?
299
+ raise Net::SSH::Exception, "could not settle on #{algorithm} algorithm"
300
+ end
301
+
302
+ return match
303
+ end
304
+
305
+ # Considers the sizes of the keys and block-sizes for the selected ciphers,
306
+ # and the lengths of the hmacs, and returns the largest as the byte requirement
307
+ # for the key-exchange algorithm.
308
+ def kex_byte_requirement
309
+ sizes = [8] # require at least 8 bytes
310
+
311
+ sizes.concat(CipherFactory.get_lengths(encryption_client))
312
+ sizes.concat(CipherFactory.get_lengths(encryption_server))
313
+
314
+ sizes << HMAC.key_length(hmac_client)
315
+ sizes << HMAC.key_length(hmac_server)
316
+
317
+ sizes.max
318
+ end
319
+
320
+ # Instantiates one of the Transport::Kex classes (based on the negotiated
321
+ # kex algorithm), and uses it to exchange keys. Then, the ciphers and
322
+ # HMACs are initialized and fed to the transport layer, to be used in
323
+ # further communication with the server.
324
+ def exchange_keys
325
+ debug { "exchanging keys" }
326
+
327
+ algorithm = Kex::MAP[kex].new(self, session,
328
+ :client_version_string => Net::SSH::Transport::ServerVersion::PROTO_VERSION,
329
+ :server_version_string => session.server_version.version,
330
+ :server_algorithm_packet => @server_packet,
331
+ :client_algorithm_packet => @client_packet,
332
+ :need_bytes => kex_byte_requirement,
333
+ :logger => logger)
334
+ result = algorithm.exchange_keys
335
+
336
+ secret = result[:shared_secret].to_ssh
337
+ hash = result[:session_id]
338
+ digester = result[:hashing_algorithm]
339
+
340
+ @session_id ||= hash
341
+
342
+ key = Proc.new { |salt| digester.digest(secret + hash + salt + @session_id) }
343
+
344
+ iv_client = key["A"]
345
+ iv_server = key["B"]
346
+ key_client = key["C"]
347
+ key_server = key["D"]
348
+ mac_key_client = key["E"]
349
+ mac_key_server = key["F"]
350
+
351
+ parameters = { :shared => secret, :hash => hash, :digester => digester }
352
+
353
+ cipher_client = CipherFactory.get(encryption_client, parameters.merge(:iv => iv_client, :key => key_client, :encrypt => true))
354
+ cipher_server = CipherFactory.get(encryption_server, parameters.merge(:iv => iv_server, :key => key_server, :decrypt => true))
355
+
356
+ mac_client = HMAC.get(hmac_client, mac_key_client, parameters)
357
+ mac_server = HMAC.get(hmac_server, mac_key_server, parameters)
358
+
359
+ session.configure_client :cipher => cipher_client, :hmac => mac_client,
360
+ :compression => normalize_compression_name(compression_client),
361
+ :compression_level => options[:compression_level],
362
+ :rekey_limit => options[:rekey_limit],
363
+ :max_packets => options[:rekey_packet_limit],
364
+ :max_blocks => options[:rekey_blocks_limit]
365
+
366
+ session.configure_server :cipher => cipher_server, :hmac => mac_server,
367
+ :compression => normalize_compression_name(compression_server),
368
+ :rekey_limit => options[:rekey_limit],
369
+ :max_packets => options[:rekey_packet_limit],
370
+ :max_blocks => options[:rekey_blocks_limit]
371
+
372
+ @initialized = true
373
+ end
374
+
375
+ # Given the SSH name for some compression algorithm, return a normalized
376
+ # name as a symbol.
377
+ def normalize_compression_name(name)
378
+ case name
379
+ when "none" then false
380
+ when "zlib" then :standard
381
+ when "zlib@openssh.com" then :delayed
382
+ else raise ArgumentError, "unknown compression type `#{name}'"
383
+ end
384
+ end
385
+ end
386
+ end; end; end