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