opscode-pushy-client 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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