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