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