add two new options to "certmaster-ca"
[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(None, '/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(None,'/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(None,'/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(None,'/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 if with_triggers:
232 self._run_triggers(None,'/var/lib/certmaster/triggers/sign/pre/*')
233
234
235 requesting_host = self._sanitize_cn(csrreq.get_subject().CN)
236 certfile = '%s/%s.cert' % (self.cfg.certroot, requesting_host)
237 self.logger.info("Signing for csr %s requested" % certfile)
238 thiscert = certs.create_slave_certificate(csrreq, self.cakey, self.cacert, self.cfg.cadir)
239
240 destfo = open(certfile, 'w')
241 destfo.write(crypto.dump_certificate(crypto.FILETYPE_PEM, thiscert))
242 destfo.close()
243 del destfo
244
245
246 self.logger.info("csr %s signed" % (certfile))
247 if with_triggers:
248 self._run_triggers(None,'/var/lib/certmaster/triggers/sign/post/*')
249
250
251 if csr_unlink_file and os.path.exists(csr_unlink_file):
252 os.unlink(csr_unlink_file)
253
254 return certfile
255
256 # return a list of already signed certs
257 def get_signed_certs(self, hostglobs=None):
258 certglob = "%s/*.cert" % (self.cfg.certroot)
259
260 certs = []
261 globs = "*"
262 if hostglobs:
263 globs = hostglobs
264
265 for hostglob in globs:
266 certglob = "%s/%s.cert" % (self.cfg.certroot, hostglob)
267 certs = certs + glob.glob(certglob)
268
269 signed_certs = []
270 for cert in certs:
271 # just want the hostname, so strip off path and ext
272 signed_certs.append(os.path.basename(cert).split(".cert", 1)[0])
273
274 return signed_certs
275
276 # return a list of the cert hash string we use to identify systems
277 def get_cert_hashes(self, hostglobs=None):
278 certglob = "%s/*.cert" % (self.cfg.certroot)
279
280 certfiles = []
281 globs = "*"
282 if hostglobs:
283 globs = hostglobs
284
285 for hostglob in globs:
286 certglob = "%s/%s.cert" % (self.cfg.certroot, hostglob)
287 certfiles = certfiles + glob.glob(certglob)
288
289 cert_hashes = []
290 for certfile in certfiles:
291 cert = certs.retrieve_cert_from_file(certfile)
292 cert_hashes.append("%s-%s" % (cert.get_subject().CN, cert.subject_name_hash()))
293
294 return cert_hashes
295
296 def _run_triggers(self, ref, globber):
297 return utils.run_triggers(ref, globber)
298
299
300 class CertmasterXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer):
301 def __init__(self, addr):
302 self.allow_reuse_address = True
303 SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, addr)
304
305
306 def serve(xmlrpcinstance):
307
308 """
309 Code for starting the XMLRPC service.
310 """
311
312
313 server = CertmasterXMLRPCServer((xmlrpcinstance.cfg.listen_addr, CERTMASTER_LISTEN_PORT))
314 server.logRequests = 0 # don't print stuff to console
315 server.register_instance(xmlrpcinstance)
316 xmlrpcinstance.logger.info("certmaster started")
317 xmlrpcinstance.audit_logger.logger.info("certmaster started")
318 server.serve_forever()
319
320
321 def main(argv):
322
323 cm = CertMaster('/etc/certmaster/certmaster.conf')
324
325 if "daemon" in argv or "--daemon" in argv:
326 utils.daemonize("/var/run/certmaster.pid")
327 else:
328 print "serving...\n"
329
330
331 # just let exceptions bubble up for now
332 serve(cm)
333
334
335 if __name__ == "__main__":
336 #textdomain(I18N_DOMAIN)
337 main(sys.argv)