This repository has been archived by the owner on Nov 28, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 15
/
sshkey.rb
390 lines (333 loc) · 15.4 KB
/
sshkey.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
module MCollective
module Security
# Configuration
#
# Client:
# client.private_key : A private key used to sign requests with - defaults to ssh-agent
# client.known_hosts : The known_hosts file to use - defaults to /home/callerid/.ssh/known_hosts
# client.send_key : Send the client's public key with the request - doesn not send the key by default.
# To send a key specify the key file to send.
#
# Server:
# server.private_key : The private key used to sign replies with - Defaults to /etc/ssh/ssh_host_rsa_key * required
# server.authorized_keys : The authorized_keys file to use - defaults to the caller's authorized_keys file in his home directory
# server.send_key : Send the server's public key with the request - does not send the key by default.
# To send a key specify the key file to send.
#
# Shared:
# (client|server).publickey_dir : Directory to store received keys - defaults to none
# (client|server).learn_public_keys : Allow writing public keys to publickey_dir - defaults to not sending.
# (client|server).overwrite_stored_keys : Overwrite received keys - defaults to false
class Sshkey < Base
gem 'sshkeyauth', '>= 0.0.4'
require 'ssh/key/signer'
require 'ssh/key/verifier'
require 'etc'
def initialize
@known_hosts_cache = {}
@known_hosts_mtime = 0
super
end
def decodemsg(msg)
body = Marshal.load(msg.payload)
if validrequest?(body)
body[:body] = Marshal.load(body[:body])
return body
else
nil
end
end
def encodereply(sender, msg, requestid, requestcallerid=nil)
serialized_msg = Marshal.dump(msg)
reply = create_reply(requestid, sender, serialized_msg)
reply[:serialized_data] = Marshal.dump(create_hash_fields(serialized_msg, reply[:msgtime], requestid))
reply[:hash] = makehash(reply[:serialized_data])
if server_key = lookup_config_option('send_key')
if File.exists?(server_key)
reply[:public_key] = load_key(server_key)
else
raise("Cannot create reply. sshkey.server.send_key set but key '%s' does not exist." % server_key)
end
end
Marshal.dump(reply)
end
def encoderequest(sender, msg, requestid, filter, target_agent, target_collective, ttl=60)
serialized_msg = Marshal.dump(msg)
req = create_request(requestid, filter, serialized_msg, @initiated_by, target_agent, target_collective, ttl)
if client_key = lookup_config_option('send_key')
if File.exists?(client_key)
req[:public_key] = load_key(client_key)
else
raise("Cannot create request. sshkey.client.send_key set but key '%s' does not exist." % client_key)
end
end
req[:serialized_data] = Marshal.dump(create_hash_fields(serialized_msg, req[:msgtime], requestid, ttl, callerid))
req[:hash] = makehash(req[:serialized_data])
Marshal.dump(req)
end
def validrequest?(req)
# Check if verification keys are correctly configured
valid_configuration?
# Check if key should be written to disk and write it
write_key_to_disk(req[:public_key], (req[:callerid] || req[:senderid]).split('=')[-1] ) if req[:public_key]
if @initiated_by == :client
Log.debug('Validating reply from node %s' % req[:senderid])
verifier = client_verifier(req[:senderid])
else
Log.debug('Validating request from client %s' % req[:callerid])
verifier = node_verifier(req[:callerid], (req[:agent] == 'registration'), req[:public_key])
end
signatures = Marshal.load(req[:hash])
if verifier.verify?(signatures, req[:serialized_data])
@stats.validated
return true
else
@stats.unvalidated
Log.debug('Received an invalid signature in message.')
raise SecurityValidationFailed
end
end
def callerid
'sshkey=%s' % (ENV['MCOLLECTIVE_SSH_CALLERID'] ? ENV['MCOLLECTIVE_SSH_CALLERID'] : Etc.getpwuid(Process.uid).name)
end
private
# Checks that publickey_dir and known_hosts|authorized_keys are not set at the same time.
def valid_configuration?
if @initiated_by == :client
if lookup_config_option('publickey_dir') && lookup_config_option('known_hosts')
raise('Both publickey_dir and known_hosts are defined in client config. Cannot lookup public key')
end
elsif @initiated_by == :node
if lookup_config_option('publickey_dir') && lookup_config_option('authorized_keys')
raise('Both publickey_dir and authorized_keys are defiend in server config. Cannot lookup public key')
end
end
end
# Checks if the attached public key needs to be stored locally
# Overwriting is disabled by default
# - The publickey_directory config option needs to be set before
# the file will be written.
# - The directory must exist before writing.
# - The learn_public_keys configuration option must be enabled.
def write_key_to_disk(key, identity)
# Writing is disabled. Don't bother checking any other states.
return unless lookup_config_option('learn_public_keys') =~ /^1|y/
publickey_dir = lookup_config_option('publickey_dir')
unless publickey_dir
Log.info("Public key sent with request but no publickey_dir defined in configuration. Not writing key to disk.")
return
end
if File.directory?(publickey_dir)
# Reject identity if it would result in directory traversal.
old_keyfile = File.join(File.expand_path(publickey_dir), "#{identity}_pub.pem")
unless File.expand_path(old_keyfile) == old_keyfile
Log.warn("Identity returned by server would result in directory traversal. Not writing key to disk.")
return
end
if File.exists?(old_keyfile)
old_key = File.read(old_keyfile).chomp
unless old_key == key
unless lookup_config_option('overwrite_stored_keys', 'n') =~ /^1|y/
Log.warn("Public key sent from '%s' does not match the stored key. Not overwriting." % identity)
else
Log.warn("Public key sent from '%s' does not match the stored key. Overwriting." % identity)
File.open(old_keyfile, 'w') { |f| f.puts key }
end
end
else
Log.debug("Discovered a new public key for '%s'. Writing to '%s'" % [identity, publickey_dir])
File.open(old_keyfile, 'w') { |f| f.puts key }
end
else
raise("Cannot write public key to '%s'. Directory does not exist." % publickey_dir)
end
end
# Fetches the correct configuration option for a client or a server
def lookup_config_option(opt, default = nil)
if @initiated_by == :client
result = @config.pluginconf.fetch("sshkey.client.#{opt}", default)
if result && ["authorized_keys", "private_key", "send_key", "publickey_dir", "known_hosts"].include?(opt)
return File.expand_path(result)
else
return result
end
elsif @initiated_by == :node
return @config.pluginconf.fetch("sshkey.server.#{opt}", default)
end
end
# Creates a hash of the fields used to sign a message
# Response messages use the msg, msgtime and requestid fields.
# Request messages use the same fields as response, but include
# ttl and callerid.
def create_hash_fields(msg, msgtime, requestid, ttl = nil, callerid = nil)
map = {:msg => msg,
:msgtime => msgtime,
:requestid => requestid}
# Check if this is a server hash
return map if (ttl == nil && callerid == nil)
map[:ttl] = ttl
map[:callerid] = callerid
map
end
# Adds a key to a signer object and disables ssh-agent
def add_key_to_signer(signer, key, passphrase=nil)
if passphrase != nil
signer.add_key_file(key, passphrase)
else
signer.add_key_file(key)
end
signer.use_agent = false
end
# Creates a signed hash of fields using the node's private key
def makehash(data)
signer = SSH::Key::Signer.new
# Check if the client is signing its request with a predefined
# private key. If this is the case, disable ssh-agent.
if @initiated_by == :client
if ENV['MCOLLECTIVE_SSH_KEY']
add_key_to_signer(signer, ENV['MCOLLECTIVE_SSH_KEY'], ENV['MCOLLECTIVE_SSH_KEY_PASSPHRASE'])
elsif private_key = lookup_config_option('private_key')
unless File.exists?(private_key)
raise("Cannot sign request - private key not found: '%s'" % private_key)
else
add_key_to_signer(signer, private_key, lookup_config_option('private_key_passphrase'))
end
end
elsif @initiated_by == :node
if private_key = lookup_config_option('private_key')
add_key_to_signer(signer, private_key)
else
# First try and default to ssh_host_dsa_key
if File.exists?(private_key = '/etc/ssh/ssh_host_dsa_key')
add_key_to_signer(signer, private_key)
# If that fails, try ssh_host_rsa_key
elsif File.exists?(private_key = '/etc/ssh/ssh_host_rsa_key')
add_key_to_signer(signer, private_key)
else
raise("Cannot sign reply - private key not found: 's'" % private_key)
end
end
end
# Default to using ssh-agent for key signing
signatures = signer.sign(data).collect { |s| s.signature }
Marshal.dump(signatures)
end
#Returns the contents of a key file on disk
def load_key(key)
if File.exists?(key)
return File.read(key).strip
else
nil
end
end
# Looks for a specific key in known hosts file
def find_key_in_known_hosts(hostname, known_hosts)
parse_known_hosts_file known_hosts
key = @known_hosts_cache[hostname]
unless key
Log.warn("Could not find a key for host '%s' in file '%s'" % [hostname, known_hosts])
raise SecurityValidationFailed
end
key
end
# This should be safe, as we parse the known hosts file only in the client...
def parse_known_hosts_file(known_hosts)
if File.exists?(known_hosts)
known_hosts_mtime = File.mtime(known_hosts).to_i
return if known_hosts_mtime == @known_hosts_mtime
File.read(known_hosts).each_line do |line|
next if line =~ /^#/
fields = line.split
next if fields.count < 3
key = fields[-2] << ' ' << fields[-1]
fields[0].split(',').each do |host|
@known_hosts_cache[host] = key
end
end
@known_hosts_mtime = known_hosts_mtime
else
@known_hosts_mtime = 0
@known_hosts_cache = {}
end
end
# Create a client verifier object which uses the correct public key
def client_verifier(senderid)
verifier = SSH::Key::Verifier.new(senderid)
verifier.use_authorized_keys = false
if publickey_dir = lookup_config_option('publickey_dir')
Log.debug("Using public key directory: '%s'" % publickey_dir)
verifier.add_public_key_data(find_shared_public_key(publickey_dir, senderid))
elsif (known_hosts = lookup_config_option('known_hosts'))
Log.debug("Using custom known_hosts file: '%s'" % known_hosts)
verifier.add_public_key_data(find_key_in_known_hosts(senderid, known_hosts))
elsif (authorized_keys = lookup_config_option('authorized_keys'))
Log.debug("Found custom authorized_keys file: '%s'" % authorized_keys)
verifier.authorized_keys_file = authorized_keys
verifier.use_authorized_keys = true
else
begin
user = Etc.getpwuid(Process.uid).name
known_hosts = File.join(Etc.getpwnam(user).dir, '.ssh', 'known_hosts')
Log.debug("Using default known_hosts file for user '%s': ''" % [user, known_hosts])
verifier.add_public_key_data(find_key_in_known_hosts(senderid, "%s" % known_hosts))
rescue => e
raise("Cannot find known_hosts file for user '%s': '%s'" % [user, known_hosts])
end
end
verifier.use_agent = false
verifier
end
# Looks for a public key in a shared directory
def find_shared_public_key(dir, id)
unless File.directory?(dir)
raise("Cannot read shared public key directory: '%s'" % dir)
end
if File.exists?(key_file = File.join(dir, "#{id}_pub.pem"))
return File.read(key_file)
else
Log.warn("Cannot find public key for id '%s': '%s'" % [id, File.join(dir, "#{id}_pub.pem")])
raise SecurityValidationFailed
end
end
# Create a node verifier object which uses the correct public key
def node_verifier(callerid, registration = false, pubkey = nil)
user = callerid.split('=')[-1]
verifier = SSH::Key::Verifier.new(user)
verifier.use_agent = false
# Here we deal with the special case where a registration message
# is being validated. send_key has to be defined in the configuration.
# TODO : This is a stop gap measure we should remove when we fix
# registration
if registration && pubkey
Log.debug("Found registration message. Using sender's public key")
verifier.add_public_key_data(pubkey)
verifier.use_authorized_keys = false
elsif registration && !pubkey
Log.warn("Cannot verify registration request. Server did not send its public key")
raise SecurityValidationFailed
elsif publickey_dir = lookup_config_option('publickey_dir')
if File.directory?(publickey_dir)
Log.debug("Found shared public key directory: '%s'" % publickey_dir)
verifier.add_public_key_data(find_shared_public_key(publickey_dir, user))
verifier.use_authorized_keys = false
else
raise("Public key directory '%s' does not exist" % publickey_dir)
end
elsif (authorized_keys = lookup_config_option('authorized_keys'))
authorized_keys = authorized_keys.sub('%u') { |c| user }
Log.debug("Found custom authorized_keys file: '%s'" % authorized_keys)
verifier.authorized_keys_file = authorized_keys
else
begin
authorized_keys = File.join(Etc.getpwnam(user).dir, '.ssh', 'authorized_keys')
Log.debug("No authorized_keys file or publickey_dir specified. Using '%s'" % authorized_keys)
verifier.authorized_keys_file = authorized_keys
rescue => e
raise("Cannot find authorized_keys file for user '%s': '%s'" % [user, authorized_keys])
end
end
verifier
end
end
end
end