net-ssh 4.1.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/.gitignore +5 -0
  5. data/.rubocop.yml +8 -2
  6. data/.rubocop_todo.yml +405 -552
  7. data/.travis.yml +23 -22
  8. data/CHANGES.txt +112 -1
  9. data/Gemfile +1 -7
  10. data/{Gemfile.norbnacl → Gemfile.noed25519} +1 -1
  11. data/Manifest +4 -5
  12. data/README.md +287 -0
  13. data/Rakefile +40 -29
  14. data/appveyor.yml +12 -6
  15. data/lib/net/ssh.rb +68 -32
  16. data/lib/net/ssh/authentication/agent.rb +234 -222
  17. data/lib/net/ssh/authentication/certificate.rb +175 -164
  18. data/lib/net/ssh/authentication/constants.rb +17 -14
  19. data/lib/net/ssh/authentication/ed25519.rb +162 -141
  20. data/lib/net/ssh/authentication/ed25519_loader.rb +32 -29
  21. data/lib/net/ssh/authentication/key_manager.rb +40 -9
  22. data/lib/net/ssh/authentication/methods/abstract.rb +53 -47
  23. data/lib/net/ssh/authentication/methods/hostbased.rb +32 -33
  24. data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +1 -1
  25. data/lib/net/ssh/authentication/methods/none.rb +10 -10
  26. data/lib/net/ssh/authentication/methods/password.rb +13 -13
  27. data/lib/net/ssh/authentication/methods/publickey.rb +56 -55
  28. data/lib/net/ssh/authentication/pageant.rb +468 -465
  29. data/lib/net/ssh/authentication/pub_key_fingerprint.rb +43 -0
  30. data/lib/net/ssh/authentication/session.rb +130 -122
  31. data/lib/net/ssh/buffer.rb +345 -312
  32. data/lib/net/ssh/buffered_io.rb +163 -163
  33. data/lib/net/ssh/config.rb +316 -238
  34. data/lib/net/ssh/connection/channel.rb +670 -650
  35. data/lib/net/ssh/connection/constants.rb +30 -26
  36. data/lib/net/ssh/connection/event_loop.rb +108 -105
  37. data/lib/net/ssh/connection/keepalive.rb +54 -50
  38. data/lib/net/ssh/connection/session.rb +682 -671
  39. data/lib/net/ssh/connection/term.rb +180 -176
  40. data/lib/net/ssh/errors.rb +101 -99
  41. data/lib/net/ssh/key_factory.rb +195 -108
  42. data/lib/net/ssh/known_hosts.rb +161 -152
  43. data/lib/net/ssh/loggable.rb +57 -55
  44. data/lib/net/ssh/packet.rb +82 -78
  45. data/lib/net/ssh/prompt.rb +55 -53
  46. data/lib/net/ssh/proxy/command.rb +104 -89
  47. data/lib/net/ssh/proxy/errors.rb +12 -8
  48. data/lib/net/ssh/proxy/http.rb +93 -91
  49. data/lib/net/ssh/proxy/https.rb +42 -39
  50. data/lib/net/ssh/proxy/jump.rb +50 -47
  51. data/lib/net/ssh/proxy/socks4.rb +0 -2
  52. data/lib/net/ssh/proxy/socks5.rb +11 -12
  53. data/lib/net/ssh/service/forward.rb +370 -317
  54. data/lib/net/ssh/test.rb +83 -77
  55. data/lib/net/ssh/test/channel.rb +146 -142
  56. data/lib/net/ssh/test/extensions.rb +150 -146
  57. data/lib/net/ssh/test/kex.rb +35 -31
  58. data/lib/net/ssh/test/local_packet.rb +48 -44
  59. data/lib/net/ssh/test/packet.rb +87 -84
  60. data/lib/net/ssh/test/remote_packet.rb +35 -31
  61. data/lib/net/ssh/test/script.rb +173 -171
  62. data/lib/net/ssh/test/socket.rb +59 -55
  63. data/lib/net/ssh/transport/algorithms.rb +430 -364
  64. data/lib/net/ssh/transport/cipher_factory.rb +95 -91
  65. data/lib/net/ssh/transport/constants.rb +33 -25
  66. data/lib/net/ssh/transport/ctr.rb +33 -11
  67. data/lib/net/ssh/transport/hmac.rb +15 -13
  68. data/lib/net/ssh/transport/hmac/abstract.rb +82 -63
  69. data/lib/net/ssh/transport/hmac/sha2_256.rb +7 -11
  70. data/lib/net/ssh/transport/hmac/sha2_256_96.rb +4 -8
  71. data/lib/net/ssh/transport/hmac/sha2_256_etm.rb +12 -0
  72. data/lib/net/ssh/transport/hmac/sha2_512.rb +6 -9
  73. data/lib/net/ssh/transport/hmac/sha2_512_96.rb +4 -8
  74. data/lib/net/ssh/transport/hmac/sha2_512_etm.rb +12 -0
  75. data/lib/net/ssh/transport/identity_cipher.rb +55 -51
  76. data/lib/net/ssh/transport/kex.rb +14 -13
  77. data/lib/net/ssh/transport/kex/abstract.rb +123 -0
  78. data/lib/net/ssh/transport/kex/abstract5656.rb +72 -0
  79. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +38 -0
  80. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +30 -0
  81. data/lib/net/ssh/transport/kex/diffie_hellman_group14_sha1.rb +33 -40
  82. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +112 -217
  83. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +53 -62
  84. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha256.rb +5 -9
  85. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp256.rb +36 -90
  86. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp384.rb +18 -10
  87. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp521.rb +18 -10
  88. data/lib/net/ssh/transport/key_expander.rb +29 -25
  89. data/lib/net/ssh/transport/openssl.rb +116 -116
  90. data/lib/net/ssh/transport/packet_stream.rb +223 -190
  91. data/lib/net/ssh/transport/server_version.rb +64 -66
  92. data/lib/net/ssh/transport/session.rb +306 -257
  93. data/lib/net/ssh/transport/state.rb +198 -196
  94. data/lib/net/ssh/verifiers/accept_new.rb +35 -0
  95. data/lib/net/ssh/verifiers/accept_new_or_local_tunnel.rb +34 -0
  96. data/lib/net/ssh/verifiers/always.rb +56 -0
  97. data/lib/net/ssh/verifiers/never.rb +21 -0
  98. data/lib/net/ssh/version.rb +55 -53
  99. data/net-ssh-public_cert.pem +18 -19
  100. data/net-ssh.gemspec +12 -11
  101. data/support/ssh_tunnel_bug.rb +2 -2
  102. metadata +86 -75
  103. metadata.gz.sig +0 -0
  104. data/Gemfile.norbnacl.lock +0 -41
  105. data/README.rdoc +0 -169
  106. data/lib/net/ssh/ruby_compat.rb +0 -24
  107. data/lib/net/ssh/verifiers/lenient.rb +0 -30
  108. data/lib/net/ssh/verifiers/null.rb +0 -12
  109. data/lib/net/ssh/verifiers/secure.rb +0 -52
  110. data/lib/net/ssh/verifiers/strict.rb +0 -24
  111. data/support/arcfour_check.rb +0 -20
@@ -3,62 +3,66 @@ require 'stringio'
3
3
  require 'net/ssh/test/extensions'
4
4
  require 'net/ssh/test/script'
5
5
 
6
- module Net; module SSH; module Test
6
+ module Net
7
+ module SSH
8
+ module Test
7
9
 
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.sends(:kexinit)
29
- script.gets(:kexinit, 1, 2, 3, 4, "test", "ssh-rsa", "none", "none", "none", "none", "none", "none", "", "", false)
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, options={})
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
10
+ # A mock socket implementation for use in testing. It implements the minimum
11
+ # necessary interface for interacting with the rest of the Net::SSH::Test
12
+ # system.
13
+ class Socket < StringIO
14
+ attr_reader :host, :port
15
+
16
+ # The Net::SSH::Test::Script object in use by this socket. This is the
17
+ # canonical script instance that should be used for any test depending on
18
+ # this socket instance.
19
+ attr_reader :script
20
+
21
+ # Create a new test socket. This will also instantiate a new Net::SSH::Test::Script
22
+ # and seed it with the necessary events to power the initialization of the
23
+ # connection.
24
+ def initialize
25
+ extend(Net::SSH::Transport::PacketStream)
26
+ super "SSH-2.0-Test\r\n"
27
+
28
+ @script = Script.new
29
+
30
+ script.sends(:kexinit)
31
+ script.gets(:kexinit, 1, 2, 3, 4, "test", "ssh-rsa", "none", "none", "none", "none", "none", "none", "", "", false)
32
+ script.sends(:newkeys)
33
+ script.gets(:newkeys)
34
+ end
35
+
36
+ # This doesn't actually do anything, since we don't really care what gets
37
+ # written.
38
+ def write(data)
39
+ # black hole, because we don't actually care about what gets written
40
+ end
41
+
42
+ # Allows the socket to also mimic a socket factory, simply returning
43
+ # +self+.
44
+ def open(host, port, options={})
45
+ @host, @port = host, port
46
+ self
47
+ end
48
+
49
+ # Returns a sockaddr struct for the port and host that were used when the
50
+ # socket was instantiated.
51
+ def getpeername
52
+ ::Socket.sockaddr_in(port, host)
53
+ end
54
+
55
+ # Alias to #read, but never returns nil (returns an empty string instead).
56
+ def recv(n)
57
+ read(n) || ""
58
+ end
59
+
60
+ def readpartial(n)
61
+ recv(n)
62
+ end
63
+
64
+ end
57
65
 
58
- def readpartial(n)
59
- recv(n)
60
66
  end
61
-
62
67
  end
63
-
64
- end; end; end
68
+ end
@@ -5,429 +5,495 @@ require 'net/ssh/transport/cipher_factory'
5
5
  require 'net/ssh/transport/constants'
6
6
  require 'net/ssh/transport/hmac'
7
7
  require 'net/ssh/transport/kex'
8
+ require 'net/ssh/transport/kex/curve25519_sha256_loader'
8
9
  require 'net/ssh/transport/server_version'
9
10
  require 'net/ssh/authentication/ed25519_loader'
10
11
 
11
- module Net; module SSH; module Transport
12
-
13
- # Implements the higher-level logic behind an SSH key-exchange. It handles
14
- # both the initial exchange, as well as subsequent re-exchanges (as needed).
15
- # It also encapsulates the negotiation of the algorithms, and provides a
16
- # single point of access to the negotiated algorithms.
17
- #
18
- # You will never instantiate or reference this directly. It is used
19
- # internally by the transport layer.
20
- class Algorithms
21
- include Constants, Loggable
22
-
23
- # Define the default algorithms, in order of preference, supported by
24
- # Net::SSH.
25
- ALGORITHMS = {
26
- host_key: %w(ssh-rsa ssh-dss
27
- ssh-rsa-cert-v01@openssh.com
28
- ssh-rsa-cert-v00@openssh.com),
29
- kex: %w(diffie-hellman-group-exchange-sha1
30
- diffie-hellman-group1-sha1
31
- diffie-hellman-group14-sha1
32
- diffie-hellman-group-exchange-sha256),
33
- encryption: %w(aes128-cbc 3des-cbc blowfish-cbc cast128-cbc
34
- aes192-cbc aes256-cbc rijndael-cbc@lysator.liu.se
35
- idea-cbc none arcfour128 arcfour256 arcfour
36
- aes128-ctr aes192-ctr aes256-ctr
37
- cast128-ctr blowfish-ctr 3des-ctr),
38
-
39
- hmac: %w(hmac-sha1 hmac-md5 hmac-sha1-96 hmac-md5-96
40
- hmac-ripemd160 hmac-ripemd160@openssh.com
41
- hmac-sha2-256 hmac-sha2-512 hmac-sha2-256-96
42
- hmac-sha2-512-96 none),
43
-
44
- compression: %w(none zlib@openssh.com zlib),
45
- language: %w()
46
- }
47
- if defined?(OpenSSL::PKey::EC)
48
- ALGORITHMS[:host_key] += %w(ecdsa-sha2-nistp256
49
- ecdsa-sha2-nistp384
50
- ecdsa-sha2-nistp521)
51
- if Net::SSH::Authentication::ED25519Loader::LOADED
52
- ALGORITHMS[:host_key] += %w(ssh-ed25519)
53
- end
54
- ALGORITHMS[:kex] += %w(ecdh-sha2-nistp256
55
- ecdh-sha2-nistp384
56
- ecdh-sha2-nistp521)
57
- end
12
+ module Net
13
+ module SSH
14
+ module Transport
15
+ # Implements the higher-level logic behind an SSH key-exchange. It handles
16
+ # both the initial exchange, as well as subsequent re-exchanges (as needed).
17
+ # It also encapsulates the negotiation of the algorithms, and provides a
18
+ # single point of access to the negotiated algorithms.
19
+ #
20
+ # You will never instantiate or reference this directly. It is used
21
+ # internally by the transport layer.
22
+ class Algorithms
23
+ include Loggable
24
+ include Constants
25
+
26
+ # Define the default algorithms, in order of preference, supported by Net::SSH.
27
+ DEFAULT_ALGORITHMS = {
28
+ host_key: %w[ecdsa-sha2-nistp521-cert-v01@openssh.com
29
+ ecdsa-sha2-nistp384-cert-v01@openssh.com
30
+ ecdsa-sha2-nistp256-cert-v01@openssh.com
31
+ ecdsa-sha2-nistp521
32
+ ecdsa-sha2-nistp384
33
+ ecdsa-sha2-nistp256
34
+ ssh-rsa-cert-v01@openssh.com
35
+ ssh-rsa-cert-v00@openssh.com
36
+ ssh-rsa],
37
+
38
+ kex: %w[ecdh-sha2-nistp521
39
+ ecdh-sha2-nistp384
40
+ ecdh-sha2-nistp256
41
+ diffie-hellman-group-exchange-sha256
42
+ diffie-hellman-group14-sha1],
43
+
44
+ encryption: %w[aes256-ctr aes192-ctr aes128-ctr],
45
+
46
+ hmac: %w[hmac-sha2-512-etm@openssh.com hmac-sha2-256-etm@openssh.com
47
+ hmac-sha2-512 hmac-sha2-256
48
+ hmac-sha1]
49
+ }.freeze
50
+
51
+ if Net::SSH::Authentication::ED25519Loader::LOADED
52
+ DEFAULT_ALGORITHMS[:host_key].unshift(
53
+ 'ssh-ed25519-cert-v01@openssh.com',
54
+ 'ssh-ed25519'
55
+ )
56
+ end
58
57
 
59
- # The underlying transport layer session that supports this object
60
- attr_reader :session
58
+ if Net::SSH::Transport::Kex::Curve25519Sha256Loader::LOADED
59
+ DEFAULT_ALGORITHMS[:kex].unshift(
60
+ 'curve25519-sha256',
61
+ 'curve25519-sha256@libssh.org'
62
+ )
63
+ end
61
64
 
62
- # The hash of options used to initialize this object
63
- attr_reader :options
65
+ # Define all algorithms, with the deprecated, supported by Net::SSH.
66
+ ALGORITHMS = {
67
+ host_key: DEFAULT_ALGORITHMS[:host_key] + %w[ssh-dss],
64
68
 
65
- # The kex algorithm to use settled on between the client and server.
66
- attr_reader :kex
69
+ kex: DEFAULT_ALGORITHMS[:kex] +
70
+ %w[diffie-hellman-group-exchange-sha1
71
+ diffie-hellman-group1-sha1],
67
72
 
68
- # The type of host key that will be used for this session.
69
- attr_reader :host_key
73
+ encryption: DEFAULT_ALGORITHMS[:encryption] +
74
+ %w[aes256-cbc aes192-cbc aes128-cbc
75
+ rijndael-cbc@lysator.liu.se
76
+ blowfish-ctr blowfish-cbc
77
+ cast128-ctr cast128-cbc
78
+ 3des-ctr 3des-cbc
79
+ idea-cbc
80
+ none],
70
81
 
71
- # The type of the cipher to use to encrypt packets sent from the client to
72
- # the server.
73
- attr_reader :encryption_client
82
+ hmac: DEFAULT_ALGORITHMS[:hmac] +
83
+ %w[hmac-sha2-512-96 hmac-sha2-256-96
84
+ hmac-sha1-96
85
+ hmac-ripemd160 hmac-ripemd160@openssh.com
86
+ hmac-md5 hmac-md5-96
87
+ none],
74
88
 
75
- # The type of the cipher to use to decrypt packets arriving from the server.
76
- attr_reader :encryption_server
89
+ compression: %w[none zlib@openssh.com zlib],
90
+ language: %w[]
91
+ }.freeze
77
92
 
78
- # The type of HMAC to use to sign packets sent by the client.
79
- attr_reader :hmac_client
93
+ # The underlying transport layer session that supports this object
94
+ attr_reader :session
80
95
 
81
- # The type of HMAC to use to validate packets arriving from the server.
82
- attr_reader :hmac_server
96
+ # The hash of options used to initialize this object
97
+ attr_reader :options
83
98
 
84
- # The type of compression to use to compress packets being sent by the client.
85
- attr_reader :compression_client
99
+ # The kex algorithm to use settled on between the client and server.
100
+ attr_reader :kex
86
101
 
87
- # The type of compression to use to decompress packets arriving from the server.
88
- attr_reader :compression_server
102
+ # The type of host key that will be used for this session.
103
+ attr_reader :host_key
89
104
 
90
- # The language that will be used in messages sent by the client.
91
- attr_reader :language_client
105
+ # The type of the cipher to use to encrypt packets sent from the client to
106
+ # the server.
107
+ attr_reader :encryption_client
92
108
 
93
- # The language that will be used in messages sent from the server.
94
- attr_reader :language_server
109
+ # The type of the cipher to use to decrypt packets arriving from the server.
110
+ attr_reader :encryption_server
95
111
 
96
- # The hash of algorithms preferred by the client, which will be told to
97
- # the server during algorithm negotiation.
98
- attr_reader :algorithms
112
+ # The type of HMAC to use to sign packets sent by the client.
113
+ attr_reader :hmac_client
99
114
 
100
- # The session-id for this session, as decided during the initial key exchange.
101
- attr_reader :session_id
115
+ # The type of HMAC to use to validate packets arriving from the server.
116
+ attr_reader :hmac_server
102
117
 
103
- # Returns true if the given packet can be processed during a key-exchange.
104
- def self.allowed_packet?(packet)
105
- ( 1.. 4).include?(packet.type) ||
106
- ( 6..19).include?(packet.type) ||
107
- (21..49).include?(packet.type)
108
- end
118
+ # The type of compression to use to compress packets being sent by the client.
119
+ attr_reader :compression_client
109
120
 
110
- # Instantiates a new Algorithms object, and prepares the hash of preferred
111
- # algorithms based on the options parameter and the ALGORITHMS constant.
112
- def initialize(session, options={})
113
- @session = session
114
- @logger = session.logger
115
- @options = options
116
- @algorithms = {}
117
- @pending = @initialized = false
118
- @client_packet = @server_packet = nil
119
- prepare_preferred_algorithms!
120
- end
121
+ # The type of compression to use to decompress packets arriving from the server.
122
+ attr_reader :compression_server
121
123
 
122
- # Start the algorithm negotation
123
- def start
124
- raise ArgumentError, "Cannot call start if it's negotiation started or done" if @pending || @initialized
125
- send_kexinit
126
- end
124
+ # The language that will be used in messages sent by the client.
125
+ attr_reader :language_client
127
126
 
128
- # Request a rekey operation. This will return immediately, and does not
129
- # actually perform the rekey operation. It does cause the session to change
130
- # state, however--until the key exchange finishes, no new packets will be
131
- # processed.
132
- def rekey!
133
- @client_packet = @server_packet = nil
134
- @initialized = false
135
- send_kexinit
136
- end
127
+ # The language that will be used in messages sent from the server.
128
+ attr_reader :language_server
137
129
 
138
- # Called by the transport layer when a KEXINIT packet is received, indicating
139
- # that the server wants to exchange keys. This can be spontaneous, or it
140
- # can be in response to a client-initiated rekey request (see #rekey!). Either
141
- # way, this will block until the key exchange completes.
142
- def accept_kexinit(packet)
143
- info { "got KEXINIT from server" }
144
- @server_data = parse_server_algorithm_packet(packet)
145
- @server_packet = @server_data[:raw]
146
- if !pending?
147
- send_kexinit
148
- else
149
- proceed!
150
- end
151
- end
130
+ # The hash of algorithms preferred by the client, which will be told to
131
+ # the server during algorithm negotiation.
132
+ attr_reader :algorithms
152
133
 
153
- # A convenience method for accessing the list of preferred types for a
154
- # specific algorithm (see #algorithms).
155
- def [](key)
156
- algorithms[key]
157
- end
134
+ # The session-id for this session, as decided during the initial key exchange.
135
+ attr_reader :session_id
158
136
 
159
- # Returns +true+ if a key-exchange is pending. This will be true from the
160
- # moment either the client or server requests the key exchange, until the
161
- # exchange completes. While an exchange is pending, only a limited number
162
- # of packets are allowed, so event processing essentially stops during this
163
- # period.
164
- def pending?
165
- @pending
166
- end
137
+ # Returns true if the given packet can be processed during a key-exchange.
138
+ def self.allowed_packet?(packet)
139
+ (1..4).include?(packet.type) ||
140
+ (6..19).include?(packet.type) ||
141
+ (21..49).include?(packet.type)
142
+ end
167
143
 
168
- # Returns true if no exchange is pending, and otherwise returns true or
169
- # false depending on whether the given packet is of a type that is allowed
170
- # during a key exchange.
171
- def allow?(packet)
172
- !pending? || Algorithms.allowed_packet?(packet)
173
- end
144
+ # Instantiates a new Algorithms object, and prepares the hash of preferred
145
+ # algorithms based on the options parameter and the ALGORITHMS constant.
146
+ def initialize(session, options={})
147
+ @session = session
148
+ @logger = session.logger
149
+ @options = options
150
+ @algorithms = {}
151
+ @pending = @initialized = false
152
+ @client_packet = @server_packet = nil
153
+ prepare_preferred_algorithms!
154
+ end
174
155
 
175
- # Returns true if the algorithms have been negotiated at all.
176
- def initialized?
177
- @initialized
178
- end
156
+ # Start the algorithm negotation
157
+ def start
158
+ raise ArgumentError, "Cannot call start if it's negotiation started or done" if @pending || @initialized
179
159
 
180
- private
181
-
182
- # Sends a KEXINIT packet to the server. If a server KEXINIT has already
183
- # been received, this will then invoke #proceed! to proceed with the key
184
- # exchange, otherwise it returns immediately (but sets the object to the
185
- # pending state).
186
- def send_kexinit
187
- info { "sending KEXINIT" }
188
- @pending = true
189
- packet = build_client_algorithm_packet
190
- @client_packet = packet.to_s
191
- session.send_message(packet)
192
- proceed! if @server_packet
193
- end
160
+ send_kexinit
161
+ end
194
162
 
195
- # After both client and server have sent their KEXINIT packets, this
196
- # will do the algorithm negotiation and key exchange. Once both finish,
197
- # the object leaves the pending state and the method returns.
198
- def proceed!
199
- info { "negotiating algorithms" }
200
- negotiate_algorithms
201
- exchange_keys
202
- @pending = false
203
- end
163
+ # Request a rekey operation. This will return immediately, and does not
164
+ # actually perform the rekey operation. It does cause the session to change
165
+ # state, however--until the key exchange finishes, no new packets will be
166
+ # processed.
167
+ def rekey!
168
+ @client_packet = @server_packet = nil
169
+ @initialized = false
170
+ send_kexinit
171
+ end
204
172
 
205
- # Prepares the list of preferred algorithms, based on the options hash
206
- # that was given when the object was constructed, and the ALGORITHMS
207
- # constant. Also, when determining the host_key type to use, the known
208
- # hosts files are examined to see if the host has ever sent a host_key
209
- # before, and if so, that key type is used as the preferred type for
210
- # communicating with this server.
211
- def prepare_preferred_algorithms!
212
- options[:compression] = %w(zlib@openssh.com zlib) if options[:compression] == true
213
-
214
- ALGORITHMS.each do |algorithm, supported|
215
- algorithms[algorithm] = compose_algorithm_list(supported, options[algorithm], options[:append_all_supported_algorithms])
173
+ # Called by the transport layer when a KEXINIT packet is received, indicating
174
+ # that the server wants to exchange keys. This can be spontaneous, or it
175
+ # can be in response to a client-initiated rekey request (see #rekey!). Either
176
+ # way, this will block until the key exchange completes.
177
+ def accept_kexinit(packet)
178
+ info { "got KEXINIT from server" }
179
+ @server_data = parse_server_algorithm_packet(packet)
180
+ @server_packet = @server_data[:raw]
181
+ if !pending?
182
+ send_kexinit
183
+ else
184
+ proceed!
185
+ end
216
186
  end
217
187
 
218
- # for convention, make sure our list has the same keys as the server
219
- # list
188
+ # A convenience method for accessing the list of preferred types for a
189
+ # specific algorithm (see #algorithms).
190
+ def [](key)
191
+ algorithms[key]
192
+ end
220
193
 
221
- algorithms[:encryption_client ] = algorithms[:encryption_server ] = algorithms[:encryption]
222
- algorithms[:hmac_client ] = algorithms[:hmac_server ] = algorithms[:hmac]
223
- algorithms[:compression_client] = algorithms[:compression_server] = algorithms[:compression]
224
- algorithms[:language_client ] = algorithms[:language_server ] = algorithms[:language]
194
+ # Returns +true+ if a key-exchange is pending. This will be true from the
195
+ # moment either the client or server requests the key exchange, until the
196
+ # exchange completes. While an exchange is pending, only a limited number
197
+ # of packets are allowed, so event processing essentially stops during this
198
+ # period.
199
+ def pending?
200
+ @pending
201
+ end
225
202
 
226
- if !options.key?(:host_key)
227
- # make sure the host keys are specified in preference order, where any
228
- # existing known key for the host has preference.
203
+ # Returns true if no exchange is pending, and otherwise returns true or
204
+ # false depending on whether the given packet is of a type that is allowed
205
+ # during a key exchange.
206
+ def allow?(packet)
207
+ !pending? || Algorithms.allowed_packet?(packet)
208
+ end
209
+
210
+ # Returns true if the algorithms have been negotiated at all.
211
+ def initialized?
212
+ @initialized
213
+ end
229
214
 
230
- existing_keys = session.host_keys
231
- host_keys = existing_keys.map { |key| key.ssh_type }.uniq
232
- algorithms[:host_key].each do |name|
233
- host_keys << name unless host_keys.include?(name)
215
+ def host_key_format
216
+ case host_key
217
+ when /^([a-z0-9-]+)-cert-v\d{2}@openssh.com$/
218
+ Regexp.last_match[1]
219
+ else
220
+ host_key
234
221
  end
235
- algorithms[:host_key] = host_keys
236
222
  end
237
- end
238
223
 
239
- # Composes the list of algorithms by taking supported algorithms and matching with supplied options.
240
- def compose_algorithm_list(supported, option, append_all_supported_algorithms = false)
241
- return supported.dup unless option
224
+ private
225
+
226
+ # Sends a KEXINIT packet to the server. If a server KEXINIT has already
227
+ # been received, this will then invoke #proceed! to proceed with the key
228
+ # exchange, otherwise it returns immediately (but sets the object to the
229
+ # pending state).
230
+ def send_kexinit
231
+ info { "sending KEXINIT" }
232
+ @pending = true
233
+ packet = build_client_algorithm_packet
234
+ @client_packet = packet.to_s
235
+ session.send_message(packet)
236
+ proceed! if @server_packet
237
+ end
238
+
239
+ # After both client and server have sent their KEXINIT packets, this
240
+ # will do the algorithm negotiation and key exchange. Once both finish,
241
+ # the object leaves the pending state and the method returns.
242
+ def proceed!
243
+ info { "negotiating algorithms" }
244
+ negotiate_algorithms
245
+ exchange_keys
246
+ @pending = false
247
+ end
248
+
249
+ # Prepares the list of preferred algorithms, based on the options hash
250
+ # that was given when the object was constructed, and the ALGORITHMS
251
+ # constant. Also, when determining the host_key type to use, the known
252
+ # hosts files are examined to see if the host has ever sent a host_key
253
+ # before, and if so, that key type is used as the preferred type for
254
+ # communicating with this server.
255
+ def prepare_preferred_algorithms!
256
+ options[:compression] = %w[zlib@openssh.com zlib] if options[:compression] == true
257
+
258
+ ALGORITHMS.each do |algorithm, supported|
259
+ algorithms[algorithm] = compose_algorithm_list(
260
+ supported, options[algorithm] || DEFAULT_ALGORITHMS[algorithm],
261
+ options[:append_all_supported_algorithms]
262
+ )
263
+ end
264
+
265
+ # for convention, make sure our list has the same keys as the server
266
+ # list
242
267
 
243
- list = []
244
- option = Array(option).compact.uniq
268
+ algorithms[:encryption_client ] = algorithms[:encryption_server ] = algorithms[:encryption]
269
+ algorithms[:hmac_client ] = algorithms[:hmac_server ] = algorithms[:hmac]
270
+ algorithms[:compression_client] = algorithms[:compression_server] = algorithms[:compression]
271
+ algorithms[:language_client ] = algorithms[:language_server ] = algorithms[:language]
245
272
 
246
- if option.first && option.first.start_with?('+')
247
- list = supported.dup
248
- list << option.first[1..-1]
249
- list.concat(option[1..-1])
250
- list.uniq!
251
- else
252
- list = option
273
+ if !options.key?(:host_key)
274
+ # make sure the host keys are specified in preference order, where any
275
+ # existing known key for the host has preference.
253
276
 
254
- if append_all_supported_algorithms
255
- supported.each { |name| list << name unless list.include?(name) }
277
+ existing_keys = session.host_keys
278
+ host_keys = existing_keys.map { |key| key.ssh_type }.uniq
279
+ algorithms[:host_key].each do |name|
280
+ host_keys << name unless host_keys.include?(name)
281
+ end
282
+ algorithms[:host_key] = host_keys
256
283
  end
257
284
  end
258
285
 
259
- unsupported = []
260
- list.select! do |name|
261
- is_supported = supported.include?(name)
262
- unsupported << name unless is_supported
263
- is_supported
264
- end
286
+ # Composes the list of algorithms by taking supported algorithms and matching with supplied options.
287
+ def compose_algorithm_list(supported, option, append_all_supported_algorithms = false)
288
+ return supported.dup unless option
265
289
 
266
- lwarn { %(unsupported algorithm: `#{unsupported}') } unless unsupported.empty?
290
+ list = []
291
+ option = Array(option).compact.uniq
267
292
 
268
- list
269
- end
293
+ if option.first && option.first.start_with?('+', '-')
294
+ list = supported.dup
270
295
 
271
- # Parses a KEXINIT packet from the server.
272
- def parse_server_algorithm_packet(packet)
273
- data = { raw: packet.content }
274
-
275
- packet.read(16) # skip the cookie value
276
-
277
- data[:kex] = packet.read_string.split(/,/)
278
- data[:host_key] = packet.read_string.split(/,/)
279
- data[:encryption_client] = packet.read_string.split(/,/)
280
- data[:encryption_server] = packet.read_string.split(/,/)
281
- data[:hmac_client] = packet.read_string.split(/,/)
282
- data[:hmac_server] = packet.read_string.split(/,/)
283
- data[:compression_client] = packet.read_string.split(/,/)
284
- data[:compression_server] = packet.read_string.split(/,/)
285
- data[:language_client] = packet.read_string.split(/,/)
286
- data[:language_server] = packet.read_string.split(/,/)
287
-
288
- # TODO: if first_kex_packet_follows, we need to try to skip the
289
- # actual kexinit stuff and try to guess what the server is doing...
290
- # need to read more about this scenario.
291
- # first_kex_packet_follows = packet.read_bool
292
-
293
- return data
294
- end
296
+ appends = option.select { |opt| opt.start_with?('+') }.map { |opt| opt[1..-1] }
297
+ deletions = option.select { |opt| opt.start_with?('-') }.map { |opt| opt[1..-1] }
295
298
 
296
- # Given the #algorithms map of preferred algorithm types, this constructs
297
- # a KEXINIT packet to send to the server. It does not actually send it,
298
- # it simply builds the packet and returns it.
299
- def build_client_algorithm_packet
300
- kex = algorithms[:kex ].join(",")
301
- host_key = algorithms[:host_key ].join(",")
302
- encryption = algorithms[:encryption ].join(",")
303
- hmac = algorithms[:hmac ].join(",")
304
- compression = algorithms[:compression].join(",")
305
- language = algorithms[:language ].join(",")
306
-
307
- Net::SSH::Buffer.from(:byte, KEXINIT,
308
- :long, [rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF)],
309
- :mstring, [kex, host_key, encryption, encryption, hmac, hmac],
310
- :mstring, [compression, compression, language, language],
311
- :bool, false, :long, 0)
312
- end
299
+ list.concat(appends)
300
+
301
+ deletions.each do |opt|
302
+ if opt.include?('*')
303
+ opt_escaped = Regexp.escape(opt)
304
+ algo_re = /\A#{opt_escaped.gsub('\*', '[A-Za-z\d\-@\.]*')}\z/
305
+ list.delete_if { |existing_opt| algo_re.match(existing_opt) }
306
+ else
307
+ list.delete(opt)
308
+ end
309
+ end
313
310
 
314
- # Given the parsed server KEX packet, and the client's preferred algorithm
315
- # lists in #algorithms, determine which preferred algorithms each has
316
- # in common and set those as the selected algorithms. If, for any algorithm,
317
- # no type can be settled on, an exception is raised.
318
- def negotiate_algorithms
319
- @kex = negotiate(:kex)
320
- @host_key = negotiate(:host_key)
321
- @encryption_client = negotiate(:encryption_client)
322
- @encryption_server = negotiate(:encryption_server)
323
- @hmac_client = negotiate(:hmac_client)
324
- @hmac_server = negotiate(:hmac_server)
325
- @compression_client = negotiate(:compression_client)
326
- @compression_server = negotiate(:compression_server)
327
- @language_client = negotiate(:language_client) rescue ""
328
- @language_server = negotiate(:language_server) rescue ""
329
-
330
- debug do
331
- "negotiated:\n" +
332
- [:kex, :host_key, :encryption_server, :encryption_client, :hmac_client, :hmac_server, :compression_client, :compression_server, :language_client, :language_server].map do |key|
333
- "* #{key}: #{instance_variable_get("@#{key}")}"
334
- end.join("\n")
311
+ list.uniq!
312
+ else
313
+ list = option
314
+
315
+ if append_all_supported_algorithms
316
+ supported.each { |name| list << name unless list.include?(name) }
317
+ end
318
+ end
319
+
320
+ unsupported = []
321
+ list.select! do |name|
322
+ is_supported = supported.include?(name)
323
+ unsupported << name unless is_supported
324
+ is_supported
325
+ end
326
+
327
+ lwarn { %(unsupported algorithm: `#{unsupported}') } unless unsupported.empty?
328
+
329
+ list
335
330
  end
336
- end
337
331
 
338
- # Negotiates a single algorithm based on the preferences reported by the
339
- # server and those set by the client. This is called by
340
- # #negotiate_algorithms.
341
- def negotiate(algorithm)
342
- match = self[algorithm].find { |item| @server_data[algorithm].include?(item) }
332
+ # Parses a KEXINIT packet from the server.
333
+ def parse_server_algorithm_packet(packet)
334
+ data = { raw: packet.content }
335
+
336
+ packet.read(16) # skip the cookie value
337
+
338
+ data[:kex] = packet.read_string.split(/,/)
339
+ data[:host_key] = packet.read_string.split(/,/)
340
+ data[:encryption_client] = packet.read_string.split(/,/)
341
+ data[:encryption_server] = packet.read_string.split(/,/)
342
+ data[:hmac_client] = packet.read_string.split(/,/)
343
+ data[:hmac_server] = packet.read_string.split(/,/)
344
+ data[:compression_client] = packet.read_string.split(/,/)
345
+ data[:compression_server] = packet.read_string.split(/,/)
346
+ data[:language_client] = packet.read_string.split(/,/)
347
+ data[:language_server] = packet.read_string.split(/,/)
348
+
349
+ # TODO: if first_kex_packet_follows, we need to try to skip the
350
+ # actual kexinit stuff and try to guess what the server is doing...
351
+ # need to read more about this scenario.
352
+ # first_kex_packet_follows = packet.read_bool
353
+
354
+ return data
355
+ end
343
356
 
344
- if match.nil?
345
- raise Net::SSH::Exception, "could not settle on #{algorithm} algorithm"
357
+ # Given the #algorithms map of preferred algorithm types, this constructs
358
+ # a KEXINIT packet to send to the server. It does not actually send it,
359
+ # it simply builds the packet and returns it.
360
+ def build_client_algorithm_packet
361
+ kex = algorithms[:kex].join(",")
362
+ host_key = algorithms[:host_key].join(",")
363
+ encryption = algorithms[:encryption].join(",")
364
+ hmac = algorithms[:hmac].join(",")
365
+ compression = algorithms[:compression].join(",")
366
+ language = algorithms[:language].join(",")
367
+
368
+ Net::SSH::Buffer.from(:byte, KEXINIT,
369
+ :long, [rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF)],
370
+ :mstring, [kex, host_key, encryption, encryption, hmac, hmac],
371
+ :mstring, [compression, compression, language, language],
372
+ :bool, false, :long, 0)
346
373
  end
347
374
 
348
- return match
349
- end
375
+ # Given the parsed server KEX packet, and the client's preferred algorithm
376
+ # lists in #algorithms, determine which preferred algorithms each has
377
+ # in common and set those as the selected algorithms. If, for any algorithm,
378
+ # no type can be settled on, an exception is raised.
379
+ def negotiate_algorithms
380
+ @kex = negotiate(:kex)
381
+ @host_key = negotiate(:host_key)
382
+ @encryption_client = negotiate(:encryption_client)
383
+ @encryption_server = negotiate(:encryption_server)
384
+ @hmac_client = negotiate(:hmac_client)
385
+ @hmac_server = negotiate(:hmac_server)
386
+ @compression_client = negotiate(:compression_client)
387
+ @compression_server = negotiate(:compression_server)
388
+ @language_client = negotiate(:language_client) rescue ""
389
+ @language_server = negotiate(:language_server) rescue ""
390
+
391
+ debug do
392
+ "negotiated:\n" +
393
+ %i[kex host_key encryption_server encryption_client hmac_client hmac_server
394
+ compression_client compression_server language_client language_server].map do |key|
395
+ "* #{key}: #{instance_variable_get("@#{key}")}"
396
+ end.join("\n")
397
+ end
398
+ end
350
399
 
351
- # Considers the sizes of the keys and block-sizes for the selected ciphers,
352
- # and the lengths of the hmacs, and returns the largest as the byte requirement
353
- # for the key-exchange algorithm.
354
- def kex_byte_requirement
355
- sizes = [8] # require at least 8 bytes
400
+ # Negotiates a single algorithm based on the preferences reported by the
401
+ # server and those set by the client. This is called by
402
+ # #negotiate_algorithms.
403
+ def negotiate(algorithm)
404
+ match = self[algorithm].find { |item| @server_data[algorithm].include?(item) }
356
405
 
357
- sizes.concat(CipherFactory.get_lengths(encryption_client))
358
- sizes.concat(CipherFactory.get_lengths(encryption_server))
406
+ if match.nil?
407
+ raise Net::SSH::Exception, "could not settle on #{algorithm} algorithm\n"\
408
+ "Server #{algorithm} preferences: #{@server_data[algorithm].join(',')}\n"\
409
+ "Client #{algorithm} preferences: #{self[algorithm].join(',')}"
410
+ end
359
411
 
360
- sizes << HMAC.key_length(hmac_client)
361
- sizes << HMAC.key_length(hmac_server)
412
+ return match
413
+ end
362
414
 
363
- sizes.max
364
- end
415
+ # Considers the sizes of the keys and block-sizes for the selected ciphers,
416
+ # and the lengths of the hmacs, and returns the largest as the byte requirement
417
+ # for the key-exchange algorithm.
418
+ def kex_byte_requirement
419
+ sizes = [8] # require at least 8 bytes
365
420
 
366
- # Instantiates one of the Transport::Kex classes (based on the negotiated
367
- # kex algorithm), and uses it to exchange keys. Then, the ciphers and
368
- # HMACs are initialized and fed to the transport layer, to be used in
369
- # further communication with the server.
370
- def exchange_keys
371
- debug { "exchanging keys" }
372
-
373
- algorithm = Kex::MAP[kex].new(self, session,
374
- client_version_string: Net::SSH::Transport::ServerVersion::PROTO_VERSION,
375
- server_version_string: session.server_version.version,
376
- server_algorithm_packet: @server_packet,
377
- client_algorithm_packet: @client_packet,
378
- need_bytes: kex_byte_requirement,
379
- minimum_dh_bits: options[:minimum_dh_bits],
380
- logger: logger)
381
- result = algorithm.exchange_keys
382
-
383
- secret = result[:shared_secret].to_ssh
384
- hash = result[:session_id]
385
- digester = result[:hashing_algorithm]
386
-
387
- @session_id ||= hash
388
-
389
- key = Proc.new { |salt| digester.digest(secret + hash + salt + @session_id) }
390
-
391
- iv_client = key["A"]
392
- iv_server = key["B"]
393
- key_client = key["C"]
394
- key_server = key["D"]
395
- mac_key_client = key["E"]
396
- mac_key_server = key["F"]
397
-
398
- parameters = { shared: secret, hash: hash, digester: digester }
399
-
400
- cipher_client = CipherFactory.get(encryption_client, parameters.merge(iv: iv_client, key: key_client, encrypt: true))
401
- cipher_server = CipherFactory.get(encryption_server, parameters.merge(iv: iv_server, key: key_server, decrypt: true))
402
-
403
- mac_client = HMAC.get(hmac_client, mac_key_client, parameters)
404
- mac_server = HMAC.get(hmac_server, mac_key_server, parameters)
405
-
406
- session.configure_client cipher: cipher_client, hmac: mac_client,
407
- compression: normalize_compression_name(compression_client),
408
- compression_level: options[:compression_level],
409
- rekey_limit: options[:rekey_limit],
410
- max_packets: options[:rekey_packet_limit],
411
- max_blocks: options[:rekey_blocks_limit]
412
-
413
- session.configure_server cipher: cipher_server, hmac: mac_server,
414
- compression: normalize_compression_name(compression_server),
415
- rekey_limit: options[:rekey_limit],
416
- max_packets: options[:rekey_packet_limit],
417
- max_blocks: options[:rekey_blocks_limit]
418
-
419
- @initialized = true
420
- end
421
+ sizes.concat(CipherFactory.get_lengths(encryption_client))
422
+ sizes.concat(CipherFactory.get_lengths(encryption_server))
421
423
 
422
- # Given the SSH name for some compression algorithm, return a normalized
423
- # name as a symbol.
424
- def normalize_compression_name(name)
425
- case name
426
- when "none" then false
427
- when "zlib" then :standard
428
- when "zlib@openssh.com" then :delayed
429
- else raise ArgumentError, "unknown compression type `#{name}'"
424
+ sizes << HMAC.key_length(hmac_client)
425
+ sizes << HMAC.key_length(hmac_server)
426
+
427
+ sizes.max
428
+ end
429
+
430
+ # Instantiates one of the Transport::Kex classes (based on the negotiated
431
+ # kex algorithm), and uses it to exchange keys. Then, the ciphers and
432
+ # HMACs are initialized and fed to the transport layer, to be used in
433
+ # further communication with the server.
434
+ def exchange_keys
435
+ debug { "exchanging keys" }
436
+
437
+ algorithm = Kex::MAP[kex].new(self, session,
438
+ client_version_string: Net::SSH::Transport::ServerVersion::PROTO_VERSION,
439
+ server_version_string: session.server_version.version,
440
+ server_algorithm_packet: @server_packet,
441
+ client_algorithm_packet: @client_packet,
442
+ need_bytes: kex_byte_requirement,
443
+ minimum_dh_bits: options[:minimum_dh_bits],
444
+ logger: logger)
445
+ result = algorithm.exchange_keys
446
+
447
+ secret = result[:shared_secret].to_ssh
448
+ hash = result[:session_id]
449
+ digester = result[:hashing_algorithm]
450
+
451
+ @session_id ||= hash
452
+
453
+ key = Proc.new { |salt| digester.digest(secret + hash + salt + @session_id) }
454
+
455
+ iv_client = key["A"]
456
+ iv_server = key["B"]
457
+ key_client = key["C"]
458
+ key_server = key["D"]
459
+ mac_key_client = key["E"]
460
+ mac_key_server = key["F"]
461
+
462
+ parameters = { shared: secret, hash: hash, digester: digester }
463
+
464
+ cipher_client = CipherFactory.get(encryption_client, parameters.merge(iv: iv_client, key: key_client, encrypt: true))
465
+ cipher_server = CipherFactory.get(encryption_server, parameters.merge(iv: iv_server, key: key_server, decrypt: true))
466
+
467
+ mac_client = HMAC.get(hmac_client, mac_key_client, parameters)
468
+ mac_server = HMAC.get(hmac_server, mac_key_server, parameters)
469
+
470
+ session.configure_client cipher: cipher_client, hmac: mac_client,
471
+ compression: normalize_compression_name(compression_client),
472
+ compression_level: options[:compression_level],
473
+ rekey_limit: options[:rekey_limit],
474
+ max_packets: options[:rekey_packet_limit],
475
+ max_blocks: options[:rekey_blocks_limit]
476
+
477
+ session.configure_server cipher: cipher_server, hmac: mac_server,
478
+ compression: normalize_compression_name(compression_server),
479
+ rekey_limit: options[:rekey_limit],
480
+ max_packets: options[:rekey_packet_limit],
481
+ max_blocks: options[:rekey_blocks_limit]
482
+
483
+ @initialized = true
484
+ end
485
+
486
+ # Given the SSH name for some compression algorithm, return a normalized
487
+ # name as a symbol.
488
+ def normalize_compression_name(name)
489
+ case name
490
+ when "none" then false
491
+ when "zlib" then :standard
492
+ when "zlib@openssh.com" then :delayed
493
+ else raise ArgumentError, "unknown compression type `#{name}'"
494
+ end
430
495
  end
431
496
  end
497
+ end
432
498
  end
433
- end; end; end
499
+ end