3fcb78fd290b1f95c2ed1a443cdcd85141a13359
[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 self.cakey[s_caname] = certs.retrieve_key_from_file(s_ca_key_file)
92 self.cacert[s_caname] = 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='', 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 csrreq = crypto.load_certificate_request(crypto.FILETYPE_PEM, csrbuf)
129 except crypto.Error, e:
130 #XXX need to raise a fault here and document it - but false is just as good
131 return False, '', ''
132
133 requesting_host = self._sanitize_cn(csrreq.get_subject().CN)
134
135 if with_triggers:
136 self._run_triggers(requesting_host, '/var/lib/certmaster/triggers/request/pre/*')
137
138 self.logger.info("%s requested signing of cert %s" % (requesting_host,csrreq.get_subject().CN))
139 # get rid of dodgy characters in the filename we're about to make
140
141 certfile = '%s/%s.cert' % (self.cfg.ca[ca]['certroot'], requesting_host)
142 csrfile = '%s/%s.csr' % (self.cfg.ca[ca]['csrroot'], requesting_host)
143
144 # check for old csr on disk
145 # if we have it - compare the two - if they are not the same - raise a fault
146 self.logger.debug("csrfile: %s certfile: %s" % (csrfile, certfile))
147
148 if os.path.exists(csrfile):
149 oldfo = open(csrfile)
150 oldcsrbuf = oldfo.read()
151 oldsha = hashlib.new('sha1')
152 oldsha.update(oldcsrbuf)
153 olddig = oldsha.hexdigest()
154 newsha = hashlib.new('sha1')
155 newsha.update(csrbuf)
156 newdig = newsha.hexdigest()
157 if not newdig == olddig:
158 self.logger.info("A cert for %s already exists and does not match the requesting cert" % (requesting_host))
159 # XXX raise a proper fault
160 return False, '', ''
161
162
163 # look for a cert:
164 # if we have it, then return True, etc, etc
165 if os.path.exists(certfile):
166 slavecert = certs.retrieve_cert_from_file(certfile)
167 cert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, slavecert)
168 cacert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, self.cacert)
169 if with_triggers:
170 self._run_triggers(requesting_host,'/var/lib/certmaster/triggers/request/post/*')
171 return True, cert_buf, cacert_buf
172
173 # if we don't have a cert then:
174 # if we're autosign then sign it, write out the cert and return True, etc, etc
175 # else write out the csr
176
177 if self.cfg.ca[ca]['autosign']:
178 cert_fn = self.sign_this_csr(csrreq)
179 cert = certs.retrieve_cert_from_file(cert_fn)
180 cert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
181 cacert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, self.cacert)
182 self.logger.info("cert for %s for ca %s was autosigned" % (requesting_host,ca))
183 if with_triggers:
184 self._run_triggers(None,'/var/lib/certmaster/triggers/request/post/*')
185 return True, cert_buf, cacert_buf
186
187 else:
188 # write the csr out to a file to be dealt with by the admin
189 destfo = open(csrfile, 'w')
190 destfo.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csrreq))
191 destfo.close()
192 del destfo
193 self.logger.info("cert for %s for CA %s created and ready to be signed" % (requesting_host, ca))
194 if with_triggers:
195 self._run_triggers(None,'/var/lib/certmaster/triggers/request/post/*')
196 return False, '', ''
197
198 return False, '', ''
199
200 def get_csrs_waiting(self, ca=''):
201 hosts = []
202 csrglob = '%s/*.csr' % self.cfg.ca[ca]['csrroot']
203 csr_list = glob.glob(csrglob)
204 for f in csr_list:
205 hn = os.path.basename(f)
206 hn = hn[:-4]
207 hosts.append(hn)
208 return hosts
209
210 def remove_this_cert(self, hn, with_triggers=True, ca=''):
211 """ removes cert for hostname using unlink """
212 cm = self
213 csrglob = '%s/%s.csr' % (cm.cfg.ca[ca]['csrroot'], hn)
214 csrs = glob.glob(csrglob)
215 certglob = '%s/%s.cert' % (cm.cfg.ca[ca]['certroot'], hn)
216 certs = glob.glob(certglob)
217 if not csrs and not certs:
218 # FIXME: should be an exception?
219 print 'No match for %s to clean up' % hn
220 return
221 if with_triggers:
222 self._run_triggers(hn,'/var/lib/certmaster/triggers/remove/pre/*')
223 for fn in csrs + certs:
224 print 'Cleaning out %s for host matching %s' % (fn, hn)
225 self.logger.info('Cleaning out %s for host matching %s' % (fn, hn))
226 os.unlink(fn)
227 if with_triggers:
228 self._run_triggers(hn,'/var/lib/certmaster/triggers/remove/post/*')
229
230 def sign_this_csr(self, csr, with_triggers=True,ca=''):
231 """returns the path to the signed cert file"""
232 csr_unlink_file = None
233
234 if type(csr) is type(''):
235 if csr.startswith('/') and os.path.exists(csr): # we have a full path to the file
236 csrfo = open(csr)
237 csr_buf = csrfo.read()
238 csr_unlink_file = csr
239
240 elif os.path.exists('%s/%s' % (self.cfg.ca[ca]['csrroot'], csr)): # we have a partial path?
241 csrfo = open('%s/%s' % (self.cfg.ca[ca]['csrroot'], csr))
242 csr_buf = csrfo.read()
243 csr_unlink_file = '%s/%s' % (self.cfg.ca[ca]['csrroot'], csr)
244
245 # we have a string of some kind
246 else:
247 csr_buf = csr
248
249 try:
250 csrreq = crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_buf)
251 except crypto.Error, e:
252 self.logger.info("Unable to sign %s: Bad CSR" % (csr))
253 raise exceptions.Exception("Bad CSR: %s" % csr)
254
255 else: # assume we got a bare csr req
256 csrreq = csr
257
258
259 requesting_host = self._sanitize_cn(csrreq.get_subject().CN)
260 if with_triggers:
261 self._run_triggers(requesting_host,'/var/lib/certmaster/triggers/sign/pre/*')
262
263
264 certfile = '%s/%s.cert' % (self.cfg.ca[ca]['certroot'], requesting_host)
265 self.logger.info("Signing for csr %s requested" % certfile)
266 thiscert = certs.create_slave_certificate(csrreq, self.cakey, self.cacert, self.cfg.ca[ca]['cadir'])
267
268 destfo = open(certfile, 'w')
269 destfo.write(crypto.dump_certificate(crypto.FILETYPE_PEM, thiscert))
270 destfo.close()
271 del destfo
272
273
274 self.logger.info("csr %s signed" % (certfile))
275 if with_triggers:
276 self._run_triggers(requesting_host,'/var/lib/certmaster/triggers/sign/post/*')
277
278
279 if csr_unlink_file and os.path.exists(csr_unlink_file):
280 os.unlink(csr_unlink_file)
281
282 return certfile
283
284 # return a list of already signed certs
285 def get_signed_certs(self, hostglobs=None, ca=''):
286 certglob = "%s/*.cert" % (self.cfg.ca[ca]['certroot'])
287
288 certs = []
289 globs = "*"
290 if hostglobs:
291 globs = hostglobs
292
293 for hostglob in globs:
294 certglob = "%s/%s.cert" % (self.cfg.ca[ca]['certroot'], hostglob)
295 certs = certs + glob.glob(certglob)
296
297 signed_certs = []
298 for cert in certs:
299 # just want the hostname, so strip off path and ext
300 signed_certs.append(os.path.basename(cert).split(".cert", 1)[0])
301
302 return signed_certs
303
304 def get_peer_certs(self):
305 """
306 Returns a list of all certs under peerroot
307 """
308 myglob = os.path.join(self.cfg.peerroot, '*.%s' % self.cfg.cert_extension)
309 return glob.glob(myglob)
310
311 # return a list of the cert hash string we use to identify systems
312 def get_cert_hashes(self, hostglobs=None,ca=''):
313 certglob = "%s/*.cert" % (self.cfg.ca[ca]['certroot'])
314
315 certfiles = []
316 globs = "*"
317 if hostglobs:
318 globs = hostglobs
319
320 for hostglob in globs:
321 certglob = "%s/%s.cert" % (self.cfg.ca[ca]['certroot'], hostglob)
322 certfiles = certfiles + glob.glob(certglob)
323
324 cert_hashes = []
325 for certfile in certfiles:
326 cert = certs.retrieve_cert_from_file(certfile)
327 cert_hashes.append("%s-%s" % (cert.get_subject().CN, cert.subject_name_hash()))
328
329 return cert_hashes
330
331 def _run_triggers(self, ref, globber):
332 return utils.run_triggers(ref, globber)
333
334
335 class CertmasterXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer):
336 def __init__(self, addr):
337 self.allow_reuse_address = True
338 SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, addr)
339
340
341 def serve(xmlrpcinstance):
342
343 """
344 Code for starting the XMLRPC service.
345 """
346
347
348 config = read_config(CERTMASTER_CONFIG, CMConfig)
349 listen_addr = config.listen_addr
350 listen_port = config.listen_port
351 if listen_port == '':
352 listen_port = CERTMASTER_LISTEN_PORT
353 server = CertmasterXMLRPCServer((listen_addr,listen_port))
354 server.logRequests = 0 # don't print stuff to console
355 server.register_instance(xmlrpcinstance)
356 xmlrpcinstance.logger.info("certmaster started")
357 xmlrpcinstance.audit_logger.logger.info("certmaster started")
358 server.serve_forever()
359
360 def excepthook(exctype, value, tracebackobj):
361 exctype_blurb = "Exception occured: %s" % exctype
362 excvalue_blurb = "Exception value: %s" % value
363 exctb_blurb = "Exception Info:\n%s" % string.join(traceback.format_list(traceback.extract_tb(tracebackobj)))
364
365 print exctype_blurb
366 print excvalue_blurb
367 print exctb_blurb
368
369 log = logger.Logger().logger
370 log.info(exctype_blurb)
371 log.info(excvalue_blurb)
372 log.info(exctb_blurb)
373
374
375 def main(argv):
376
377 sys.excepthook = excepthook
378 cm = CertMaster('/etc/certmaster/certmaster.conf')
379
380 if "--version" in sys.argv or "-v" in sys.argv:
381 print >> sys.stderr, file("/etc/certmaster/version").read().strip()
382 sys.exit(0)
383
384 if "daemon" in argv or "--daemon" in argv:
385 utils.daemonize("/var/run/certmaster.pid")
386 else:
387 print "serving...\n"
388
389 # just let exceptions bubble up for now
390 serve(cm)
391
392
393 if __name__ == "__main__":
394 #textdomain(I18N_DOMAIN)
395 main(sys.argv)