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