BATS fell down pushing a process into the background, so I switched to shunit2 /...
[certmaster.git] / certmaster / certmaster.py
1 # FIXME: more intelligent fault raises
2
3 """
4 cert master listener
5
6 Copyright 2007, Red Hat, Inc
7 see AUTHORS
8
9 This software may be freely redistributed under the terms of the GNU
10 general public license.
11
12 You should have received a copy of the GNU General Public License
13 along with this program; if not, write to the Free Software
14 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
15 """
16
17 # standard modules
18 import SimpleXMLRPCServer
19 import string
20 import sys
21 import traceback
22 import os
23 import os.path
24 from OpenSSL import crypto
25
26 try:
27 import hashlib
28 except ImportError:
29 # Python-2.4.z ... gah! (or even 2.3!)
30 import sha
31 class hashlib:
32 @staticmethod
33 def new(algo):
34 if algo == 'sha1':
35 return sha.new()
36 raise ValueError, "Bad checksum type"
37
38 import glob
39 import socket
40 import exceptions
41
42 import certs
43 import codes
44 import utils
45
46 import logger
47
48 from config import read_config
49 from commonconfig import CMConfig
50
51 CERTMASTER_LISTEN_PORT = 51235
52 CERTMASTER_CONFIG = "/etc/certmaster/certmaster.conf"
53
54 class CertMaster(object):
55 def __init__(self, conf_file=CERTMASTER_CONFIG):
56 self.cfg = read_config(conf_file, CMConfig)
57
58 usename = utils.get_hostname(talk_to_certmaster=False)
59
60 self.logger = logger.Logger().logger
61 self.audit_logger = logger.AuditLogger()
62
63 self.cakey = {}
64 self.cacert = {}
65
66 for (s_caname,a_ca) in self.cfg.ca.iteritems():
67 s_cadir = a_ca.cadir
68
69 if s_caname == "":
70 mycn = '%s-CA-KEY' % usename
71 else:
72 mycn = '%s-%s-CA-KEY' % (s_caname.upper(),usename)
73
74 s_ca_key_file = '%s/certmaster.key' % s_cadir
75 s_ca_cert_file = '%s/certmaster.crt' % s_cadir
76
77 # if ca_key_file exists and ca_cert_file is missing == minion only setup
78 if os.path.exists(s_ca_key_file) and not os.path.exists(s_ca_cert_file):
79 continue
80
81 try:
82 if not os.path.exists(s_cadir):
83 os.makedirs(s_cadir)
84 if not os.path.exists(s_ca_key_file) and not os.path.exists(s_ca_cert_file):
85 certs.create_ca(CN=mycn, ca_key_file=s_ca_key_file, ca_cert_file=s_ca_cert_file)
86 except (IOError, OSError), e:
87 print 'Cannot make certmaster certificate authority keys/certs for CA %s, aborting: %s' % (s_caname, e)
88 sys.exit(1)
89
90 # open up the cakey and cacert so we have them available
91 a_ca.cakey = certs.retrieve_key_from_file(s_ca_key_file)
92 a_ca.cacert = certs.retrieve_cert_from_file(s_ca_cert_file)
93
94 for dirpath in [a_ca.cadir, a_ca.certroot, a_ca.csrroot, a_ca.csrroot]:
95 if not os.path.exists(dirpath):
96 os.makedirs(dirpath)
97
98 # setup handlers
99 self.handlers = {
100 'wait_for_cert': self.wait_for_cert,
101 }
102
103
104 def _dispatch(self, method, params):
105 if method == 'trait_names' or method == '_getAttributeNames':
106 return self.handlers.keys()
107
108
109 if method in self.handlers.keys():
110 return self.handlers[method](*params)
111 else:
112 self.logger.info("Unhandled method call for method: %s " % method)
113 raise codes.InvalidMethodException
114
115 def _sanitize_cn(self, commonname):
116 commonname = commonname.replace('/', '')
117 commonname = commonname.replace('\\', '')
118 return commonname
119
120 def wait_for_cert(self, csrbuf, ca_name, with_triggers=True):
121 """
122 takes csr as a string
123 returns True, caller_cert, ca_cert
124 returns False, '', ''
125 """
126
127 try:
128 certauth = self.cfg.ca[ca_name]
129 except:
130 raise codes.CMException("Unknown cert authority: %s" % ca_name)
131
132 try:
133 csrreq = crypto.load_certificate_request(crypto.FILETYPE_PEM, csrbuf)
134 except crypto.Error, e:
135 #XXX need to raise a fault here and document it - but false is just as good
136 return False, '', ''
137
138 requesting_host = self._sanitize_cn(csrreq.get_subject().CN)
139
140 if with_triggers:
141 self._run_triggers(requesting_host, '/var/lib/certmaster/triggers/request/pre/*')
142
143 self.logger.info("%s requested signing of cert %s" % (requesting_host,csrreq.get_subject().CN))
144 # get rid of dodgy characters in the filename we're about to make
145
146 certfile = '%s/%s.cert' % (certauth.certroot, requesting_host)
147 csrfile = '%s/%s.csr' % (certauth.csrroot, requesting_host)
148
149 # check for old csr on disk
150 # if we have it - compare the two - if they are not the same - raise a fault
151 self.logger.debug("csrfile: %s certfile: %s" % (csrfile, certfile))
152
153 if os.path.exists(csrfile):
154 oldfo = open(csrfile)
155 oldcsrbuf = oldfo.read()
156 oldsha = hashlib.new('sha1')
157 oldsha.update(oldcsrbuf)
158 olddig = oldsha.hexdigest()
159 newsha = hashlib.new('sha1')
160 newsha.update(csrbuf)
161 newdig = newsha.hexdigest()
162 if not newdig == olddig:
163 self.logger.info("A cert for %s already exists and does not match the requesting cert" % (requesting_host))
164 # XXX raise a proper fault
165 return False, '', ''
166
167
168 # look for a cert:
169 # if we have it, then return True, etc, etc
170 if os.path.exists(certfile):
171 slavecert = certs.retrieve_cert_from_file(certfile)
172 cert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, slavecert)
173 cacert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, certauth.cacert)
174 if with_triggers:
175 self._run_triggers(requesting_host,'/var/lib/certmaster/triggers/request/post/*')
176 return True, cert_buf, cacert_buf
177
178 # if we don't have a cert then:
179 # if we're autosign then sign it, write out the cert and return True, etc, etc
180 # else write out the csr
181
182 if certauth.autosign:
183 cert_fn = self.sign_this_csr(csrreq,certauth)
184 cert = certs.retrieve_cert_from_file(cert_fn)
185 cert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
186 cacert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, certauth.cacert)
187 self.logger.info("cert for %s for ca %s was autosigned" % (requesting_host,ca_name))
188 if with_triggers:
189 self._run_triggers(None,'/var/lib/certmaster/triggers/request/post/*')
190 return True, cert_buf, cacert_buf
191
192 else:
193 # write the csr out to a file to be dealt with by the admin
194 destfo = open(csrfile, 'w')
195 destfo.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csrreq))
196 destfo.close()
197 del destfo
198 self.logger.info("cert for %s for CA %s created and ready to be signed" % (requesting_host, ca_name))
199 if with_triggers:
200 self._run_triggers(None,'/var/lib/certmaster/triggers/request/post/*')
201 return False, '', ''
202
203 return False, '', ''
204
205 def get_csrs_waiting(self, certauth):
206 hosts = []
207 csrglob = '%s/*.csr' % certauth.csrroot
208 csr_list = glob.glob(csrglob)
209 for f in csr_list:
210 hn = os.path.basename(f)
211 hn = hn[:-4]
212 hosts.append(hn)
213 return hosts
214
215 def remove_this_cert(self, hn, certauth, with_triggers=True):
216 """ removes cert for hostname using unlink """
217 cm = self
218 csrglob = '%s/%s.csr' % (certauth.csrroot, hn)
219 csrs = glob.glob(csrglob)
220 certglob = '%s/%s.cert' % (certauth.certroot, hn)
221 certs = glob.glob(certglob)
222 if not csrs and not certs:
223 # FIXME: should be an exception?
224 print 'No match for %s to clean up' % hn
225 return
226 if with_triggers:
227 self._run_triggers(hn,'/var/lib/certmaster/triggers/remove/pre/*')
228 for fn in csrs + certs:
229 print 'Cleaning out %s for host matching %s' % (fn, hn)
230 self.logger.info('Cleaning out %s for host matching %s' % (fn, hn))
231 os.unlink(fn)
232 if with_triggers:
233 self._run_triggers(hn,'/var/lib/certmaster/triggers/remove/post/*')
234
235 def sign_this_csr(self, csr, certauth,with_triggers=True):
236 """returns the path to the signed cert file"""
237 csr_unlink_file = None
238
239 if type(csr) is type(''):
240 if csr.startswith('/') and os.path.exists(csr): # we have a full path to the file
241 csrfo = open(csr)
242 csr_buf = csrfo.read()
243 csr_unlink_file = csr
244
245 elif os.path.exists('%s/%s' % (certauth.csrroot, csr)): # we have a partial path?
246 csrfo = open('%s/%s' % (certauth.csrroot, csr))
247 csr_buf = csrfo.read()
248 csr_unlink_file = '%s/%s' % (certauth.csrroot, csr)
249
250 # we have a string of some kind
251 else:
252 csr_buf = csr
253
254 try:
255 csrreq = crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_buf)
256 except crypto.Error, e:
257 self.logger.info("Unable to sign %s: Bad CSR" % (csr))
258 raise exceptions.Exception("Bad CSR: %s" % csr)
259
260 else: # assume we got a bare csr req
261 csrreq = csr
262
263
264 requesting_host = self._sanitize_cn(csrreq.get_subject().CN)
265 if with_triggers:
266 self._run_triggers(requesting_host,'/var/lib/certmaster/triggers/sign/pre/*')
267
268
269 certfile = '%s/%s.cert' % (certauth.certroot, requesting_host)
270 self.logger.info("Signing for csr %s requested" % certfile)
271 thiscert = certs.create_slave_certificate(csrreq, certauth.cakey, certauth.cacert, certauth.cadir)
272
273 destfo = open(certfile, 'w')
274 destfo.write(crypto.dump_certificate(crypto.FILETYPE_PEM, thiscert))
275 destfo.close()
276 del destfo
277
278
279 self.logger.info("csr %s signed" % (certfile))
280 if with_triggers:
281 self._run_triggers(requesting_host,'/var/lib/certmaster/triggers/sign/post/*')
282
283
284 if csr_unlink_file and os.path.exists(csr_unlink_file):
285 os.unlink(csr_unlink_file)
286
287 return certfile
288
289 # return a list of already signed certs
290 def get_signed_certs(self, certauth,hostglobs=None):
291 certglob = "%s/*.cert" % (certauth.certroot)
292
293 certs = []
294 globs = "*"
295 if hostglobs:
296 globs = hostglobs
297
298 for hostglob in globs:
299 certglob = "%s/%s.cert" % (certauth.certroot, hostglob)
300 certs = certs + glob.glob(certglob)
301
302 signed_certs = []
303 for cert in certs:
304 # just want the hostname, so strip off path and ext
305 signed_certs.append(os.path.basename(cert).split(".cert", 1)[0])
306
307 return signed_certs
308
309 def get_peer_certs(self):
310 """
311 Returns a list of all certs under peerroot
312 """
313 myglob = os.path.join(self.cfg.peerroot, '*.%s' % self.cfg.cert_extension)
314 return glob.glob(myglob)
315
316 # return a list of the cert hash string we use to identify systems
317 def get_cert_hashes(self, certauth, hostglobs=None):
318 certglob = "%s/*.cert" % (certauth.certroot)
319
320 certfiles = []
321 globs = "*"
322 if hostglobs:
323 globs = hostglobs
324
325 for hostglob in globs:
326 certglob = "%s/%s.cert" % (certauth.certroot, hostglob)
327 certfiles = certfiles + glob.glob(certglob)
328
329 cert_hashes = []
330 for certfile in certfiles:
331 cert = certs.retrieve_cert_from_file(certfile)
332 cert_hashes.append("%s-%s" % (cert.get_subject().CN, cert.subject_name_hash()))
333
334 return cert_hashes
335
336 def _run_triggers(self, ref, globber):
337 return utils.run_triggers(ref, globber)
338
339
340 class CertmasterXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer):
341 def __init__(self, addr):
342 self.allow_reuse_address = True
343 SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, addr)
344
345
346 def serve(xmlrpcinstance):
347
348 """
349 Code for starting the XMLRPC service.
350 """
351
352
353 config = read_config(CERTMASTER_CONFIG, CMConfig)
354 listen_addr = config.listen_addr
355 listen_port = config.listen_port
356 if listen_port == '':
357 listen_port = CERTMASTER_LISTEN_PORT
358 server = CertmasterXMLRPCServer((listen_addr,listen_port))
359 server.logRequests = 0 # don't print stuff to console
360 server.register_instance(xmlrpcinstance)
361 xmlrpcinstance.logger.info("certmaster started")
362 xmlrpcinstance.audit_logger.logger.info("certmaster started")
363 server.serve_forever()
364
365 def excepthook(exctype, value, tracebackobj):
366 exctype_blurb = "Exception occured: %s" % exctype
367 excvalue_blurb = "Exception value: %s" % value
368 exctb_blurb = "Exception Info:\n%s" % string.join(traceback.format_list(traceback.extract_tb(tracebackobj)))
369
370 print exctype_blurb
371 print excvalue_blurb
372 print exctb_blurb
373
374 log = logger.Logger().logger
375 log.info(exctype_blurb)
376 log.info(excvalue_blurb)
377 log.info(exctb_blurb)
378
379
380 def main(argv):
381
382 sys.excepthook = excepthook
383 cm = CertMaster('/etc/certmaster/certmaster.conf')
384
385 if "--version" in sys.argv or "-v" in sys.argv:
386 print >> sys.stderr, file("/etc/certmaster/version").read().strip()
387 sys.exit(0)
388
389 if "daemon" in argv or "--daemon" in argv:
390 utils.daemonize("/var/run/certmaster.pid")
391 else:
392 print "serving...\n"
393
394 # just let exceptions bubble up for now
395 serve(cm)
396
397
398 if __name__ == "__main__":
399 #textdomain(I18N_DOMAIN)
400 main(sys.argv)