35d9ce5338c51c750113468dce408b330c7e5d57
[packages/precise/mcollective.git] / plugins / mcollective / security / ssl.rb
1 require 'base64'
2 require 'openssl'
3
4 module MCollective
5   module Security
6     # Impliments a public/private key based message validation system using SSL
7     # public and private keys.
8     #
9     # The design goal of the plugin is two fold:
10     #
11     # - give different security credentials to clients and servers to avoid
12     #   a compromised server from sending new client requests.
13     # - create a token that uniquely identify the client - based on the filename
14     #   of the public key
15     #
16     # To setup you need to create a SSL key pair that is shared by all nodes.
17     #
18     #   openssl genrsa -out mcserver-private.pem 1024
19     #   openssl rsa -in mcserver-private.pem -out mcserver-public.pem -outform PEM -pubout
20     #
21     # Distribute the private and public file to /etc/mcollective/ssl on all the nodes.
22     # Distribute the public file to /etc/mcollective/ssl everywhere the client code runs.
23     #
24     # Now you should create a key pair for every one of your clients, here we create one
25     # for user john - you could also if you are less concerned with client id create one
26     # pair and share it with all clients:
27     #
28     #   openssl genrsa -out john-private.pem 1024
29     #   openssl rsa -in john-private.pem -out john-public.pem -outform PEM -pubout
30     #
31     # Each user has a unique userid, this is based on the name of the public key.
32     # In this example case the userid would be 'john-public'.
33     #
34     # Store these somewhere like:
35     #
36     #     /home/john/.mc/john-private.pem
37     #     /home/john/.mc/john-public.pem
38     #
39     # Every users public key needs to be distributed to all the nodes, save the john one
40     # in a file called:
41     #
42     #   /etc/mcollective/ssl/clients/john-public.pem
43     #
44     # If you wish to use registration or auditing that sends connections over MC to a
45     # central host you will need also put the server-public.pem in the clients directory.
46     #
47     # You should be aware if you do add the node public key to the clients dir you will in
48     # effect be weakening your overall security.  You should consider doing this only if
49     # you also set up an Authorization method that limits the requests the nodes can make.
50     #
51     # client.cfg:
52     #
53     #   securityprovider = ssl
54     #   plugin.ssl_server_public = /etc/mcollective/ssl/server-public.pem
55     #   plugin.ssl_client_private = /home/john/.mc/john-private.pem
56     #   plugin.ssl_client_public = /home/john/.mc/john-public.pem
57     #
58     # If you have many clients per machine and dont want to configure the main config file
59     # with the public/private keys you can set the following environment variables:
60     #
61     #   export MCOLLECTIVE_SSL_PRIVATE=/home/john/.mc/john-private.pem
62     #   export MCOLLECTIVE_SSL_PUBLIC=/home/john/.mc/john-public.pem
63     #
64     # server.cfg:
65     #
66     #   securityprovider = ssl
67     #   plugin.ssl_server_private = /etc/mcollective/ssl/server-private.pem
68     #   plugin.ssl_server_public = /etc/mcollective/ssl/server-public.pem
69     #   plugin.ssl_client_cert_dir = /etc/mcollective/etc/ssl/clients/
70     #
71     #   # Log but accept messages that may have been tampered with
72     #   plugin.ssl.enforce_ttl = 0
73     #
74     # Serialization can be configured to use either Marshal or YAML, data types
75     # in and out of mcollective will be preserved from client to server and reverse
76     #
77     # You can configure YAML serialization:
78     #
79     #    plugins.ssl_serializer = yaml
80     #
81     # else the default is Marshal.  Use YAML if you wish to write a client using
82     # a language other than Ruby that doesn't support Marshal.
83     #
84     # Validation is as default and is provided by MCollective::Security::Base
85     #
86     # Initial code was contributed by Vladimir Vuksan and modified by R.I.Pienaar
87     class Ssl < Base
88       # Decodes a message by unserializing all the bits etc, it also validates
89       # it as valid using the psk etc
90       def decodemsg(msg)
91         body = deserialize(msg.payload)
92
93         should_process_msg?(msg, body[:requestid])
94
95         if validrequest?(body)
96           body[:body] = deserialize(body[:body])
97
98           unless @initiated_by == :client
99             if body[:body].is_a?(Hash)
100               update_secure_property(body, :ssl_ttl, :ttl, "TTL")
101               update_secure_property(body, :ssl_msgtime, :msgtime, "Message Time")
102
103               body[:body] = body[:body][:ssl_msg] if body[:body].include?(:ssl_msg)
104             else
105               unless @config.pluginconf["ssl.enforce_ttl"] == nil
106                 raise "Message %s is in an unknown or older security protocol, ignoring" % [request_description(body)]
107               end
108             end
109           end
110
111           return body
112         else
113           nil
114         end
115       end
116
117       # To avoid tampering we turn the origin body into a hash and copy some of the protocol keys
118       # like :ttl and :msg_time into the hash before hashing it.
119       #
120       # This function compares and updates the unhashed ones based on the hashed ones.  By
121       # default it enforces matching and presense by raising exceptions, if ssl.enforce_ttl is set
122       # to 0 it will only log warnings about violations
123       def update_secure_property(msg, secure_property, property, description)
124         req = request_description(msg)
125
126         unless @config.pluginconf["ssl.enforce_ttl"] == "0"
127           raise "Request #{req} does not have a secure #{description}" unless msg[:body].include?(secure_property)
128           raise "Request #{req} #{description} does not match encrypted #{description} - possible tampering"  unless msg[:body][secure_property] == msg[property]
129         else
130           if msg[:body].include?(secure_property)
131             Log.warn("Request #{req} #{description} does not match encrypted #{description} - possible tampering") unless msg[:body][secure_property] == msg[property]
132           else
133             Log.warn("Request #{req} does not have a secure #{description}") unless msg[:body].include?(secure_property)
134           end
135         end
136
137         msg[property] = msg[:body][secure_property] if msg[:body].include?(secure_property)
138         msg[:body].delete(secure_property)
139       end
140
141       # Encodes a reply
142       def encodereply(sender, msg, requestid, requestcallerid=nil)
143         serialized  = serialize(msg)
144         digest = makehash(serialized)
145
146
147         req = create_reply(requestid, sender, serialized)
148         req[:hash] = digest
149
150         serialize(req)
151       end
152
153       # Encodes a request msg
154       def encoderequest(sender, msg, requestid, filter, target_agent, target_collective, ttl=60)
155         req = create_request(requestid, filter, "", @initiated_by, target_agent, target_collective, ttl)
156
157         ssl_msg = {:ssl_msg => msg,
158                    :ssl_ttl => ttl,
159                    :ssl_msgtime => req[:msgtime]}
160
161         serialized = serialize(ssl_msg)
162         digest = makehash(serialized)
163
164         req[:hash] = digest
165         req[:body] = serialized
166
167         serialize(req)
168       end
169
170       # Checks the SSL signature in the request body
171       def validrequest?(req)
172         message = req[:body]
173         signature = req[:hash]
174
175         Log.debug("Validating request from #{req[:callerid]}")
176
177         if verify(public_key_file(req[:callerid]), signature, message.to_s)
178           @stats.validated
179           return true
180         else
181           @stats.unvalidated
182           raise(SecurityValidationFailed, "Received an invalid signature in message")
183         end
184       end
185
186
187       # sets the caller id to the md5 of the public key
188       def callerid
189         if @initiated_by == :client
190           id = "cert=#{File.basename(client_public_key).gsub(/\.pem$/, '')}"
191           raise "Invalid callerid generated from client public key" unless valid_callerid?(id)
192         else
193           # servers need to set callerid as well, not usually needed but
194           # would be if you're doing registration or auditing or generating
195           # requests for some or other reason
196           id = "cert=#{File.basename(server_public_key).gsub(/\.pem$/, '')}"
197           raise "Invalid callerid generated from server public key" unless valid_callerid?(id)
198         end
199
200         return id
201       end
202
203       private
204       # Serializes a message using the configured encoder
205       def serialize(msg)
206         serializer = @config.pluginconf["ssl_serializer"] || "marshal"
207
208         Log.debug("Serializing using #{serializer}")
209
210         case serializer
211           when "yaml"
212             return YAML.dump(msg)
213           else
214             return Marshal.dump(msg)
215         end
216       end
217
218       # De-Serializes a message using the configured encoder
219       def deserialize(msg)
220         serializer = @config.pluginconf["ssl_serializer"] || "marshal"
221
222         Log.debug("De-Serializing using #{serializer}")
223
224         case serializer
225         when "yaml"
226           return YAML.load(msg)
227         else
228           return Marshal.load(msg)
229         end
230       end
231
232       # Figures out where to get our private key
233       def private_key_file
234         if ENV.include?("MCOLLECTIVE_SSL_PRIVATE")
235           return ENV["MCOLLECTIVE_SSL_PRIVATE"]
236         else
237           if @initiated_by == :node
238             return server_private_key
239           else
240             return client_private_key
241           end
242         end
243       end
244
245       # Figures out the public key to use
246       #
247       # If the node is asking do it based on caller id
248       # If the client is asking just get the node public key
249       def public_key_file(callerid = nil)
250         if @initiated_by == :client
251           return server_public_key
252         else
253           if callerid =~ /cert=([\w\.\-]+)/
254             cid = $1
255
256             if File.exist?("#{client_cert_dir}/#{cid}.pem")
257               return "#{client_cert_dir}/#{cid}.pem"
258             else
259               raise("Could not find a public key for #{cid} in #{client_cert_dir}/#{cid}.pem")
260             end
261           else
262             raise("Caller id is not in the expected format")
263           end
264         end
265       end
266
267       # Figures out the client private key either from MCOLLECTIVE_SSL_PRIVATE or the
268       # plugin.ssl_client_private config option
269       def client_private_key
270         return ENV["MCOLLECTIVE_SSL_PRIVATE"] if ENV.include?("MCOLLECTIVE_SSL_PRIVATE")
271
272         raise("No plugin.ssl_client_private configuration option specified") unless @config.pluginconf.include?("ssl_client_private")
273
274         return @config.pluginconf["ssl_client_private"]
275       end
276
277       # Figures out the client public key either from MCOLLECTIVE_SSL_PUBLIC or the
278       # plugin.ssl_client_public config option
279       def client_public_key
280         return ENV["MCOLLECTIVE_SSL_PUBLIC"] if ENV.include?("MCOLLECTIVE_SSL_PUBLIC")
281
282         raise("No plugin.ssl_client_public configuration option specified") unless @config.pluginconf.include?("ssl_client_public")
283
284         return @config.pluginconf["ssl_client_public"]
285       end
286
287       # Figures out the server private key from the plugin.ssl_server_private config option
288       def server_private_key
289         raise("No plugin.ssl_server_private configuration option specified") unless @config.pluginconf.include?("ssl_server_private")
290         @config.pluginconf["ssl_server_private"]
291       end
292
293       # Figures out the server public key from the plugin.ssl_server_public config option
294       def server_public_key
295         raise("No ssl_server_public configuration option specified") unless @config.pluginconf.include?("ssl_server_public")
296         return @config.pluginconf["ssl_server_public"]
297       end
298
299       # Figures out where to get client public certs from the plugin.ssl_client_cert_dir config option
300       def client_cert_dir
301         raise("No plugin.ssl_client_cert_dir configuration option specified") unless @config.pluginconf.include?("ssl_client_cert_dir")
302         @config.pluginconf["ssl_client_cert_dir"]
303       end
304
305       # Retrieves the value of plugin.psk and builds a hash with it and the passed body
306       def makehash(body)
307         Log.debug("Creating message hash using #{private_key_file}")
308
309         sign(private_key_file, body.to_s)
310       end
311
312       # Code adapted from http://github.com/adamcooke/basicssl
313       # signs a message
314       def sign(key, string)
315         SSL.new(nil, key).sign(string, true)
316       end
317
318       # verifies a signature
319       def verify(key, signature, string)
320         SSL.new(key).verify_signature(signature, string, true)
321       end
322
323       def request_description(msg)
324         "%s from %s@%s" % [msg[:requestid], msg[:callerid], msg[:senderid]]
325       end
326     end
327   end
328 end