opscode-pushy-client 2.3.0

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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +88 -0
  3. data/Gemfile +20 -0
  4. data/Gemfile.lock +242 -0
  5. data/LICENSE +201 -0
  6. data/README.md +43 -0
  7. data/RELEASE_PROCESS.md +105 -0
  8. data/Rakefile +42 -0
  9. data/bin/print_execution_environment +18 -0
  10. data/bin/push-apply +47 -0
  11. data/bin/pushy-client +8 -0
  12. data/bin/pushy-service-manager +19 -0
  13. data/jenkins/jenkins_run_tests.sh +9 -0
  14. data/keys/client_private.pem +27 -0
  15. data/keys/server_public.pem +9 -0
  16. data/lib/pushy_client.rb +268 -0
  17. data/lib/pushy_client/cli.rb +168 -0
  18. data/lib/pushy_client/heartbeater.rb +153 -0
  19. data/lib/pushy_client/job_runner.rb +316 -0
  20. data/lib/pushy_client/periodic_reconfigurer.rb +62 -0
  21. data/lib/pushy_client/protocol_handler.rb +508 -0
  22. data/lib/pushy_client/version.rb +23 -0
  23. data/lib/pushy_client/whitelist.rb +66 -0
  24. data/lib/pushy_client/win32.rb +27 -0
  25. data/lib/pushy_client/windows_service.rb +253 -0
  26. data/omnibus/Berksfile +12 -0
  27. data/omnibus/Gemfile +15 -0
  28. data/omnibus/Gemfile.lock +232 -0
  29. data/omnibus/LICENSE +201 -0
  30. data/omnibus/README.md +141 -0
  31. data/omnibus/acceptance/Berksfile +6 -0
  32. data/omnibus/acceptance/Berksfile.lock +35 -0
  33. data/omnibus/acceptance/Makefile +13 -0
  34. data/omnibus/acceptance/README.md +29 -0
  35. data/omnibus/acceptance/metadata.rb +12 -0
  36. data/omnibus/acceptance/recipes/chef-server-user-org.rb +31 -0
  37. data/omnibus/config/projects/push-jobs-client.rb +83 -0
  38. data/omnibus/config/software/opscode-pushy-client.rb +78 -0
  39. data/omnibus/files/mapfiles/solaris +18 -0
  40. data/omnibus/files/openssl-customization/windows/ssl_env_hack.rb +34 -0
  41. data/omnibus/omnibus.rb +54 -0
  42. data/omnibus/package-scripts/push-jobs-client/postinst +55 -0
  43. data/omnibus/package-scripts/push-jobs-client/postrm +39 -0
  44. data/omnibus/resources/push-jobs-client/dmg/background.png +0 -0
  45. data/omnibus/resources/push-jobs-client/dmg/icon.png +0 -0
  46. data/omnibus/resources/push-jobs-client/msi/assets/LICENSE.rtf +197 -0
  47. data/omnibus/resources/push-jobs-client/msi/assets/banner_background.bmp +0 -0
  48. data/omnibus/resources/push-jobs-client/msi/assets/dialog_background.bmp +0 -0
  49. data/omnibus/resources/push-jobs-client/msi/assets/oc.ico +0 -0
  50. data/omnibus/resources/push-jobs-client/msi/assets/oc_16x16.ico +0 -0
  51. data/omnibus/resources/push-jobs-client/msi/assets/oc_32x32.ico +0 -0
  52. data/omnibus/resources/push-jobs-client/msi/localization-en-us.wxl.erb +26 -0
  53. data/omnibus/resources/push-jobs-client/msi/parameters.wxi.erb +9 -0
  54. data/omnibus/resources/push-jobs-client/msi/source.wxs.erb +138 -0
  55. data/omnibus/resources/push-jobs-client/pkg/background.png +0 -0
  56. data/omnibus/resources/push-jobs-client/pkg/license.html.erb +202 -0
  57. data/omnibus/resources/push-jobs-client/pkg/welcome.html.erb +5 -0
  58. data/opscode-pushy-client.gemspec +28 -0
  59. data/pkg/opscode-pushy-client-2.3.0.gem +0 -0
  60. data/spec/pushy_client/protocol_handler_spec.rb +48 -0
  61. data/spec/pushy_client/whitelist_spec.rb +70 -0
  62. data/spec/spec_helper.rb +12 -0
  63. metadata +235 -0
@@ -0,0 +1,62 @@
1
+ # @copyright Copyright 2014 Chef Software, Inc. All Rights Reserved.
2
+ #
3
+ # This file is provided to you under the Apache License,
4
+ # Version 2.0 (the "License"); you may not use this file
5
+ # except in compliance with the License. You may obtain
6
+ # a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing,
11
+ # software distributed under the License is distributed on an
12
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13
+ # KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations
15
+ # under the License.
16
+ #
17
+
18
+ class PushyClient
19
+ class PeriodicReconfigurer
20
+ SPLAY = 0.10
21
+ def initialize(client)
22
+ @client = client
23
+ end
24
+
25
+ attr_reader :client
26
+ attr_reader :lifetime
27
+
28
+ def node_name
29
+ client.node_name
30
+ end
31
+
32
+ def start
33
+ @lifetime = client.config['lifetime']
34
+ prng = Random.new
35
+ @reconfigure_thread = Thread.new do
36
+ Chef::Log.info "[#{node_name}] Starting reconfigure thread. Will reconfigure / reload keys after #{@lifetime} seconds, less up to splay #{SPLAY}."
37
+ while true
38
+ begin
39
+ @delay = @lifetime * (1 - prng.rand(SPLAY))
40
+ sleep(@delay)
41
+ Chef::Log.info "[#{node_name}] Config is now #{@delay} seconds old. Reconfiguring / reloading keys ..."
42
+ client.trigger_reconfigure
43
+ rescue
44
+ client.log_exception("Error in reconfigure thread", $!)
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def stop
51
+ Chef::Log.info "[#{node_name}] Stopping reconfigure thread ..."
52
+ @reconfigure_thread.kill
53
+ @reconfigure_thread.join
54
+ @reconfigure_thread = nil
55
+ end
56
+
57
+ def reconfigure
58
+ stop
59
+ start
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,508 @@
1
+ # @copyright Copyright 2014 Chef Software, Inc. All Rights Reserved.
2
+ #
3
+ # This file is provided to you under the Apache License,
4
+ # Version 2.0 (the "License"); you may not use this file
5
+ # except in compliance with the License. You may obtain
6
+ # a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing,
11
+ # software distributed under the License is distributed on an
12
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13
+ # KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations
15
+ # under the License.
16
+ #
17
+
18
+ require 'ffi-rzmq'
19
+ require 'ffi-rzmq-core'
20
+ require 'json'
21
+ require 'time'
22
+ require 'resolv'
23
+ require 'openssl'
24
+ require 'mixlib/authentication/digester'
25
+ require_relative "version"
26
+
27
+ class PushyClient
28
+
29
+ # Wrap the context with a lock.
30
+ # ZMQ gets grouchy if multiple threads access the context. Wrap it with a lock
31
+ # to block this. This mostly applies to the test system, where we run multiple clients in the same
32
+ # process
33
+ class ZmqContext
34
+ ZMQ_CONTEXT = ZMQ::Context.new(1)
35
+ @@lock = Mutex.new
36
+
37
+ def self.socket(arg)
38
+ @@lock.synchronize do
39
+ ZMQ_CONTEXT.socket(arg)
40
+ end
41
+ end
42
+ end
43
+
44
+ class ProtocolHandler
45
+ ##
46
+ ## Allow send and receive times to be independently stubbed in testing.
47
+ ##
48
+ class TimeSendWrapper
49
+ def self.now
50
+ Time.now()
51
+ end
52
+ end
53
+ class TimeRecvWrapper
54
+ def self.now
55
+ Time.now()
56
+ end
57
+ end
58
+
59
+ # The maximum size, in bytes, allowed for a message body. This is not
60
+ # configurable because it is configurable (though not documented) on the
61
+ # server, and we don't want to make users have to sync the two values.
62
+ # The max on the server is actually 65536, but we leave a little room since
63
+ # the server is measuring the signed message and we're just counting
64
+ # the size of the stderr and stdout.
65
+ MAX_BODY_SIZE = 63000
66
+
67
+ def initialize(client)
68
+ @client = client
69
+ # We synchronize on this when we change the socket (so if you want a
70
+ # valid socket to send or receive on, synchronize on this)
71
+ @socket_lock = Mutex.new
72
+ # This holds the same purpose, but receive blocks for a while so it gets
73
+ # its own lock to avoid blocking sends. reconfigure will take both locks.
74
+ @receive_socket_lock = Mutex.new
75
+
76
+ # When the server goes down, close and reopen sockets.
77
+ client.on_server_availability_change do |available|
78
+ if !available
79
+ Thread.new do
80
+ begin
81
+ Chef::Log.info "[#{node_name}] Closing and reopening sockets since server is down ..."
82
+ reconfigure
83
+ Chef::Log.info "[#{node_name}] Done closing and reopening sockets."
84
+ rescue
85
+ client.log_exception("Error reconfiguring sockets when server went down", $!)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ attr_reader :client
93
+ attr_reader :server_heartbeat_address
94
+ attr_reader :command_address
95
+ attr_reader :server_public_key
96
+ attr_reader :session_key
97
+ attr_reader :session_method
98
+ attr_reader :client_private_key
99
+
100
+ def node_name
101
+ client.node_name
102
+ end
103
+
104
+ def start
105
+ server_address = URI(client.config['push_jobs']['heartbeat']['out_addr']).host
106
+ check_server_address(node_name, server_address)
107
+
108
+ @server_heartbeat_address = client.config['push_jobs']['heartbeat']['out_addr']
109
+ @command_address = client.config['push_jobs']['heartbeat']['command_addr']
110
+ @server_public_key = OpenSSL::PKey::RSA.new(client.config['public_key'])
111
+ @client_private_key = ProtocolHandler::load_key(client.client_key)
112
+ @max_message_skew = client.config['max_message_skew']
113
+
114
+ if client.using_curve
115
+ server_curve_pub_key = client.config['curve_public_key']
116
+
117
+ # decode and extract session key
118
+ begin
119
+ @session_method = client.config['encoded_session_key']['method']
120
+ enc_session_key = Base64::decode64(client.config['encoded_session_key']['key'])
121
+ @session_key = @client_private_key.private_decrypt(enc_session_key)
122
+ rescue =>_
123
+ Chef::Log.error "[#{node_name}] No session key found in config"
124
+ exit(-1)
125
+ end
126
+ else
127
+ # decode and extract session key
128
+ begin
129
+ @session_method = client.config['encoded_session_key']['method']
130
+ enc_session_key = Base64::decode64(client.config['encoded_session_key']['key'])
131
+ @session_key = @client_private_key.private_decrypt(enc_session_key)
132
+ rescue =>e
133
+ Chef::Log.error "[#{node_name}] No session key found in config"
134
+ exit(-1)
135
+ end
136
+ end
137
+
138
+ Chef::Log.info "[#{node_name}] Starting ZMQ version #{LibZMQ.version}"
139
+
140
+ # Server heartbeat socket
141
+ Chef::Log.info "[#{node_name}] Listening for server heartbeat at #{@server_heartbeat_address}"
142
+ @server_heartbeat_socket = PushyClient::ZmqContext.socket(ZMQ::SUB)
143
+ @server_heartbeat_socket.connect(@server_heartbeat_address)
144
+ @server_heartbeat_socket.setsockopt(ZMQ::SUBSCRIBE, "")
145
+ @server_heartbeat_seq_no = -1
146
+
147
+ # Command socket
148
+ Chef::Log.info "[#{node_name}] Connecting to command channel at #{@command_address}"
149
+ # TODO
150
+ # This needs to be set up to be able to handle bidirectional messages; right now this is Tx only
151
+ # Probably need to set it up with a handler, like the subscriber socket above.
152
+ @command_socket = PushyClient::ZmqContext.socket(ZMQ::DEALER)
153
+ @command_socket.setsockopt(ZMQ::LINGER, 0)
154
+ # Note setting this to '1' causes the client to crash on send, but perhaps that
155
+ # beats storming the server when the server restarts
156
+ @command_socket.setsockopt(ZMQ::RCVHWM, 0)
157
+ # Buffering more than a few heartbeats can cause trauma on the server after restart
158
+ @command_socket.setsockopt(ZMQ::SNDHWM, 3)
159
+
160
+ if client.using_curve
161
+ @command_socket.setsockopt(ZMQ::CURVE_SERVERKEY, server_curve_pub_key)
162
+ @command_socket.setsockopt(ZMQ::CURVE_PUBLICKEY, client.client_curve_pub_key)
163
+ @command_socket.setsockopt(ZMQ::CURVE_SECRETKEY, client.client_curve_sec_key)
164
+ end
165
+
166
+ @command_socket.connect(@command_address)
167
+ @command_socket_server_seq_no = -1
168
+
169
+ @command_socket_outgoing_seq = 0
170
+
171
+ @receive_thread = start_receive_thread
172
+ end
173
+
174
+ def stop
175
+ @socket_lock.synchronize do
176
+ @receive_socket_lock.synchronize do
177
+ internal_stop
178
+ end
179
+ end
180
+ end
181
+
182
+ def reconfigure
183
+ @socket_lock.synchronize do
184
+ @receive_socket_lock.synchronize do
185
+ internal_stop
186
+ start # Start picks up new configuration
187
+ end
188
+ end
189
+ end
190
+
191
+ def send_command(message_type, job_id, params)
192
+ Chef::Log.debug("[#{node_name}] Sending command #{message_type} for job #{job_id}")
193
+ # Nil params will be stripped by JSON.generate()
194
+ message = {
195
+ :node => node_name,
196
+ :client => client.hostname,
197
+ :protocol_version => PushyClient::PROTOCOL_VERSION,
198
+ :org => client.org_name,
199
+ :type => message_type,
200
+ :sequence => -1,
201
+ :timestamp => TimeSendWrapper.now.httpdate,
202
+ :incarnation_id => client.incarnation_id,
203
+ :job_id => job_id
204
+ }.merge(validate_params(params))
205
+
206
+ send_signed_json_command(:hmac_sha256, message)
207
+ end
208
+
209
+ def send_heartbeat(sequence)
210
+ Chef::Log.debug("[#{node_name}] Sending heartbeat (sequence ##{sequence})")
211
+ job_state = client.job_state
212
+ message = {
213
+ :node => node_name,
214
+ :client => client.hostname,
215
+ :protocol_version => PushyClient::PROTOCOL_VERSION,
216
+ :org => client.org_name,
217
+ :type => :heartbeat,
218
+ :sequence => -1,
219
+ :timestamp => TimeSendWrapper.now.httpdate,
220
+ :incarnation_id => client.incarnation_id,
221
+ :job_state => job_state[:state],
222
+ :job_id => job_state[:job_id]
223
+ }
224
+
225
+ send_signed_json_command(:hmac_sha256, message)
226
+ end
227
+
228
+ private
229
+
230
+ def internal_stop
231
+ Chef::Log.info "[#{node_name}] Stopping command / server heartbeat receive thread and destroying sockets ..."
232
+ @command_socket.close
233
+ @command_socket = nil
234
+ @server_heartbeat_socket.close
235
+ @server_heartbeat_socket = nil
236
+ @receive_thread.kill
237
+ @receive_thread.join
238
+ @receive_thread = nil
239
+ end
240
+
241
+ def start_receive_thread
242
+ Thread.new do
243
+ Chef::Log.info "[#{node_name}] Starting command / server heartbeat receive thread ..."
244
+ received_command = false
245
+ seconds_since_connection = 0
246
+ poller = ZMQ::Poller.new
247
+ poller.register_readable(@command_socket)
248
+ poller.register_readable(@server_heartbeat_socket)
249
+ while true
250
+ begin
251
+ messages = []
252
+ @receive_socket_lock.synchronize do
253
+ # Time out after 1 second to relinquish the lock and give
254
+ # reconfigure a chance.
255
+ poller.poll(1000)
256
+ ready_sockets = poller.readables
257
+ # Grab messages from the socket, but don't process them yet (we
258
+ # want to relinquish the socket_lock as soon as we can)
259
+ if ready_sockets
260
+ ready_sockets.each do |socket|
261
+ header = ''
262
+ socket.recv_string(header)
263
+ if socket.more_parts?
264
+ message = ''
265
+ socket.recv_string(message)
266
+ if !socket.more_parts?
267
+ Chef::Log.debug("[#{node_name}] Received ZMQ message (#{header}, #{message.length}")
268
+ messages << [header, message]
269
+ if socket == @command_socket
270
+ received_command = true
271
+ end
272
+ else
273
+ # Eat up the useless packets
274
+ begin
275
+ s = ''
276
+ socket.recv(s)
277
+ end while socket.more_parts?
278
+ Chef::Log.error "[#{node_name}] Received ZMQ message with more than two packets! Should only have header and data packets."
279
+ end
280
+ else
281
+ Chef::Log.error "[#{node_name}] Received ZMQ message with only one packet! Need both header and data packets."
282
+ end
283
+ end
284
+ end
285
+ end
286
+
287
+ # Need to do this to ensure reconfigure thread gets a chance to
288
+ # wake up and grab the lock.
289
+ sleep(0.005)
290
+
291
+ messages.each do |message|
292
+ if ProtocolHandler::valid?(message[0], message[1], @server_public_key, @session_key)
293
+ handle_message(message[1])
294
+ else
295
+ Chef::Log.error "[#{node_name}] Received invalid message: header=#{message[0]}, message=#{message[1]}}"
296
+ end
297
+ end
298
+
299
+ if !received_command && !@client.legacy_mode
300
+ seconds_since_connection += 1
301
+ if (seconds_since_connection > 3 )
302
+ Chef::Log.error "[#{node_name}] No messages being received on command port in #{seconds_since_connection}s. Possible encryption problem?"
303
+ client.trigger_reconfigure
304
+ break
305
+ end
306
+ end
307
+
308
+ rescue
309
+ client.log_exception "Error in command / server heartbeat receive thread", $!
310
+ end
311
+ end
312
+ end
313
+ end
314
+
315
+ def handle_message(message)
316
+ begin
317
+ json = JSON.parse(message, :create_additions => false)
318
+
319
+ # Verify timestamp
320
+ if !json.has_key?('timestamp')
321
+ Chef::Log.error "[#{node_name}] Received invalid message: missing timestamp"
322
+ return
323
+ end
324
+ begin
325
+ ts = Time.parse(json['timestamp'])
326
+ delta = ts - TimeRecvWrapper.now
327
+ if delta > @max_message_skew
328
+ Chef::Log.error "[#{node_name}] Received message with timestamp too far from current time (Msg: #{json['timestamp']}, delta #{delta}, max allowed #{@max_message_skew} )"
329
+ return
330
+ end
331
+ rescue
332
+ Chef::Log.error "[#{node_name}] Received message unparseable timestamp (Msg: #{json['timestamp']})"
333
+ return
334
+ end
335
+
336
+ case json['type']
337
+ when "heartbeat"
338
+ incarnation_id = json['incarnation_id']
339
+ if !incarnation_id
340
+ Chef::Log.error "[#{node_name}] Missing incarnation_id in heartbeat message: #{message}"
341
+ end
342
+ sequence = json['sequence']
343
+ if !sequence
344
+ Chef::Log.error "[#{node_name}] Missing sequence in heartbeat message: #{message}"
345
+ end
346
+ client.heartbeat_received(incarnation_id, sequence)
347
+
348
+ when "commit"
349
+ job_id = json['job_id']
350
+ if job_id
351
+ command = json['command']
352
+ if command
353
+ opts = {}
354
+ opts.merge!('user' => json['user']) if json['user']
355
+ opts.merge!('dir' => json['dir']) if json['dir']
356
+ opts.merge!('env' => json['env']) if json['env']
357
+ opts.merge!('capture' => json['capture']) if json['capture']
358
+ opts.merge!('file' => json['file']) if json['file']
359
+ client.commit(job_id, command, opts)
360
+ else
361
+ Chef::Log.error "[#{node_name}] Missing command in commit message: #{message}"
362
+ client.send_command(:nack_commit, command)
363
+ end
364
+ else
365
+ Chef::Log.error "[#{node_name}] Missing job_id in commit message: #{message}"
366
+ client.send_command(:nack_commit, job_id)
367
+ end
368
+
369
+ when "run"
370
+ job_id = json['job_id']
371
+ if job_id
372
+ client.run(job_id)
373
+ else
374
+ Chef::Log.error "[#{node_name}] Missing job_id in commit message: #{message}"
375
+ client.send_command(:nack_run, job_id)
376
+ end
377
+
378
+ when "abort"
379
+ client.abort
380
+
381
+ when "ack"
382
+ # Do nothing. If this _didn't_ come through, it might mean there was
383
+ # an encryption problem in the command port.
384
+ nil
385
+
386
+ else
387
+ Chef::Log.error "[#{node_name}] Missing type in ZMQ message: #{message}"
388
+ end
389
+ rescue JSON::ParserError
390
+ Chef::Log.error "[#{node_name}] Invalid JSON in ZMQ message: #{message}"
391
+ end
392
+ end
393
+
394
+ def check_server_address(name, server_address)
395
+ if server_address =~ Resolv::IPv4::Regex || server_address =~ Resolv::IPv6::Regex
396
+ return true
397
+ else
398
+ begin
399
+ addrs = Resolv::DNS.new.getaddresses(server_address) + Resolv::Hosts.new.getaddresses(server_address)
400
+ Chef::Log.info "[#{node_name}] Resolved #{server_address} to '#{addrs.first}' and #{addrs.length-1} others"
401
+ return true
402
+ rescue =>_
403
+ Chef::Log.error "[#{node_name}] Could not resolve #{server_address}"
404
+ return false
405
+ end
406
+
407
+ end
408
+ end
409
+
410
+ # Take the params and check to see if the stdout and stderr keys exceed the
411
+ # MAX_BODY_SIZE. If they do, log a warning and return the params without the
412
+ # stdout and stderr keys. If they do not, return the params hash unmodified.
413
+ def validate_params(params = {})
414
+ stdout_bytes = params[:stdout].to_s.bytesize
415
+ stderr_bytes = params[:stderr].to_s.bytesize
416
+
417
+ if (stdout_bytes + stderr_bytes) > MAX_BODY_SIZE
418
+ Chef::Log.warn("Command output too long. Will not be sent to server.")
419
+ params.delete_if { |k| [:stdout, :stderr].include? k }
420
+ else
421
+ params
422
+ end
423
+ end
424
+
425
+ # Message authentication (on receive)
426
+ def self.valid?(header, message, server_public_key, session_key)
427
+ headers = header.split(';')
428
+ header_map = headers.inject({}) do |a,e|
429
+ k,v = e.split(':')
430
+ a[k] = v
431
+ a
432
+ end
433
+
434
+ auth_sig = header_map["Signature"]
435
+ if !auth_sig
436
+ return false
437
+ end
438
+
439
+ binary_sig = Base64.decode64(auth_sig)
440
+
441
+ auth_method = header_map["SigningMethod"]
442
+ case auth_method
443
+ when "rsa2048_sha1"
444
+ rsa_valid?(message, binary_sig, server_public_key)
445
+ when "hmac_sha256"
446
+ hmac_valid?(message, binary_sig, session_key)
447
+ else
448
+ false
449
+ end
450
+ end
451
+
452
+ def self.rsa_valid?(message, sig, server_public_key)
453
+ decrypted_checksum = server_public_key.public_decrypt(sig)
454
+ hashed_message = Mixlib::Authentication::Digester.hash_string(message)
455
+ decrypted_checksum == hashed_message
456
+ end
457
+
458
+ def self.hmac_valid?(message, sig, session_key)
459
+ message_sig = OpenSSL::HMAC.digest('sha256', session_key, message)
460
+ # Defeat timing attacks; attacking this requires breaking SHA.
461
+ sha = OpenSSL::Digest::SHA512.new
462
+ sha.digest(sig) == sha.digest(message_sig)
463
+ end
464
+
465
+ # Message signing and sending (on send)
466
+ def send_signed_json_command(method, json)
467
+ @socket_lock.synchronize do
468
+ @command_socket_outgoing_seq += 1
469
+ json[:sequence] = @command_socket_outgoing_seq
470
+ message = JSON.generate(json)
471
+ if @command_socket
472
+ ProtocolHandler::send_signed_message(@command_socket, method, @client_private_key, @session_key, message)
473
+ else
474
+ Chef::Log.warn("[#{node_name}] Dropping packet because client was stopped: #{message}")
475
+ end
476
+ end
477
+ end
478
+
479
+ def self.send_signed_message(socket, method, client_private_key, session_key, message)
480
+ auth = case method
481
+ when :rsa2048_sha1
482
+ make_header_rsa(message, client_private_key)
483
+ when :hmac_sha256
484
+ make_header_hmac(message, session_key)
485
+ end
486
+ socket.send_string(auth, ZMQ::SNDMORE)
487
+ socket.send_string(message)
488
+ end
489
+
490
+ def self.load_key(key_path)
491
+ raw_key = IO.read(key_path).strip
492
+ OpenSSL::PKey::RSA.new(raw_key)
493
+ end
494
+
495
+ def self.make_header_rsa(json, client_private_key)
496
+ checksum = Mixlib::Authentication::Digester.hash_string(json)
497
+ b64_sig = Base64.encode64(client_private_key.private_encrypt(checksum)).chomp
498
+ "Version:2.0;SigningMethod:rsa2048_sha1;Signature:#{b64_sig}"
499
+ end
500
+
501
+ def self.make_header_hmac(json, session_key)
502
+ sig = OpenSSL::HMAC.digest('sha256', session_key, json)
503
+ b64_sig = Base64.encode64(sig).chomp
504
+ "Version:2.0;SigningMethod:hmac_sha256;Signature:#{b64_sig}"
505
+ end
506
+
507
+ end
508
+ end