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