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