add support for --version to certmaster daemon
[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 def get_peer_certs(self):
280 """
281 Returns a list of all certs under peerroot
282 """
283 myglob = os.path.join(self.cfg.peerroot, '*.%s' % self.cfg.cert_extension)
284 return glob.glob(myglob)
285
286 # return a list of the cert hash string we use to identify systems
287 def get_cert_hashes(self, hostglobs=None):
288 certglob = "%s/*.cert" % (self.cfg.certroot)
289
290 certfiles = []
291 globs = "*"
292 if hostglobs:
293 globs = hostglobs
294
295 for hostglob in globs:
296 certglob = "%s/%s.cert" % (self.cfg.certroot, hostglob)
297 certfiles = certfiles + glob.glob(certglob)
298
299 cert_hashes = []
300 for certfile in certfiles:
301 cert = certs.retrieve_cert_from_file(certfile)
302 cert_hashes.append("%s-%s" % (cert.get_subject().CN, cert.subject_name_hash()))
303
304 return cert_hashes
305
306 def _run_triggers(self, ref, globber):
307 return utils.run_triggers(ref, globber)
308
309
310 class CertmasterXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer):
311 def __init__(self, addr):
312 self.allow_reuse_address = True
313 SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, addr)
314
315
316 def serve(xmlrpcinstance):
317
318 """
319 Code for starting the XMLRPC service.
320 """
321
322
323 config = read_config(CERTMASTER_CONFIG, CMConfig)
324 listen_addr = config.listen_addr
325 listen_port = config.listen_port
326 if listen_port == '':
327 listen_port = CERTMASTER_LISTEN_PORT
328 server = CertmasterXMLRPCServer((listen_addr,listen_port))
329 server.logRequests = 0 # don't print stuff to console
330 server.register_instance(xmlrpcinstance)
331 xmlrpcinstance.logger.info("certmaster started")
332 xmlrpcinstance.audit_logger.logger.info("certmaster started")
333 server.serve_forever()
334
335 def excepthook(exctype, value, tracebackobj):
336 exctype_blurb = "Exception occured: %s" % exctype
337 excvalue_blurb = "Exception value: %s" % value
338 exctb_blurb = "Exception Info:\n%s" % string.join(traceback.format_list(traceback.extract_tb(tracebackobj)))
339
340 print exctype_blurb
341 print excvalue_blurb
342 print exctb_blurb
343
344 log = logger.Logger().logger
345 log.info(exctype_blurb)
346 log.info(excvalue_blurb)
347 log.info(exctb_blurb)
348
349
350 def main(argv):
351
352 sys.excepthook = excepthook
353 cm = CertMaster('/etc/certmaster/certmaster.conf')
354
355 if "--version" in sys.argv or "-v" in sys.argv:
356 print >> sys.stderr, file("/etc/certmaster/version").read().strip()
357 sys.exit(0)
358
359 if "daemon" in argv or "--daemon" in argv:
360 utils.daemonize("/var/run/certmaster.pid")
361 else:
362 print "serving...\n"
363
364 # just let exceptions bubble up for now
365 serve(cm)
366
367
368 if __name__ == "__main__":
369 #textdomain(I18N_DOMAIN)
370 main(sys.argv)